Complete HTTP API reference for the VibeCLI daemon (vibecli serve).
Overview
Start the daemon:
vibecli --serve --port 7878 --provider ollama
On startup, a Bearer token is printed to stderr. All authenticated endpoints require this token.
| Property | Value |
|---|---|
| Base URL | http://localhost:7878 |
| Content-Type | application/json |
| Auth | Authorization: Bearer <token> |
| Max body | 1 MB |
| CORS origins | localhost, 127.0.0.1, tauri://localhost |
Authentication
All endpoints except /health, /webhook/github, /pair, /acp/v1/capabilities, and /ws/collab/:room_id require a Bearer token.
# Token is printed on startup:
# [serve] API token: abc123...
export VIBECLI_TOKEN="abc123..."
Unauthenticated requests receive:
{ "error": "Missing or invalid Authorization: Bearer <token>" }
Status: 401 Unauthorized
API Key Rotation
Restart the daemon to generate a new token. A fresh token is printed to stderr on each startup.
Error Handling
All errors return a consistent JSON structure:
{ "error": "Human-readable error message" }
| Status Code | Meaning |
|---|---|
400 |
Bad request (malformed JSON, missing fields) |
401 |
Missing or invalid Bearer token |
404 |
Resource not found (session, job, task) |
429 |
Rate limit exceeded |
500 |
Internal server error (provider failure) |
User-supplied input in error messages is sanitized (alphanumeric + -_. only, truncated to 200 chars).
Rate Limiting
Two rate limit tiers apply:
| Tier | Limit | Window | Applies to |
|---|---|---|---|
| Authenticated | 60 requests | 60 seconds | All authed endpoints |
| Public | 10 requests | 60 seconds | /health, /webhook/github, etc. |
When the limit is exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 5
{ "error": "Rate limit exceeded. Try again shortly." }
Endpoints
GET /health
Liveness and identity check. No authentication required.
Response 200 OK (abridged — the live response also reports provider,
graph, skillforge and token-freshness status):
{
"status": "ok",
"service": "vibecli",
"version": "0.5.8"
}
curl http://localhost:7878/health
The full document also carries graph, skillforge, embedding,
kv_cache_codec_probe and api_token blocks — see
Embeddings → GET /health for the embedding one.
service is the identity check every client’s daemon-autostart requires: a
process answering on 7878 that reports a different service is not the daemon.
Clients must check
service, not just the status code. A 200 from this port only proves something is listening; any local service could answer.service: "vibecli"is the contract that distinguishes “the daemon is here” from “the port is taken by another program” — and those two need very different messages in the UI. The autostart path (vibecli_cli::daemon_bootstrap::probe) requires an exact match, and so should every client health check.
The daemon port defaults to 7878 and is overridable with
VIBECLI_DAEMON_PORT (the legacy VIBEDESK_DAEMON_PORT is still honoured).
POST /chat
Single-turn chat completion (non-streaming). Collects the full response before returning.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
messages |
ChatMessage[] |
Yes | Conversation history |
provider |
string |
No | Provider to answer with (e.g. "anthropic", "ollama") |
model |
string |
No | Model to answer with |
provider and model are honoured only together — both non-empty, matching
POST /agent. Either one alone, an unknown provider, or a missing API key falls
back to the daemon’s configured provider rather than failing the turn. Applies to
/chat and /chat/stream alike.
ChatMessage:
| Field | Type | Values |
|---|---|---|
role |
string |
"user", "assistant", "system" |
content |
string |
Message text |
Response 200 OK:
{
"content": "The AI response text..."
}
Example:
curl -X POST http://localhost:7878/chat \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "Explain Rust lifetimes in 3 sentences"}
]
}'
Errors:
| Status | Cause |
|---|---|
500 |
LLM provider error: ... or Stream error: ... |
POST /chat/stream
Streaming chat completion via Server-Sent Events (SSE). Returns tokens as they are generated.
Request body: Same as POST /chat.
SSE event types:
| Event | Data | Description |
|---|---|---|
message (default) |
Token text | Incremental content chunk |
error |
Error string | Provider or stream error |
done |
"" (empty) |
Stream finished |
Keep-alive: Every 15 seconds.
Example:
curl -N -X POST http://localhost:7878/chat/stream \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "system", "content": "You are a Rust expert."},
{"role": "user", "content": "Write a binary search function"}
]
}'
Response stream:
data: fn binary_search
data: <T: Ord>(arr: &[T],
data: target: &T) -> Option<usize>
event: done
data:
POST /agent
Start a background agent task. Returns immediately with a session ID. Subscribe to events via GET /stream/:session_id.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
task |
string |
Yes | Natural language task description |
approval |
string |
No | Override approval policy: "suggest", "auto-edit", or "full-auto" |
max_step_extensions |
number |
No | How many times a run that is still making progress may extend its step budget past max_steps. Ceiling is max_steps × (1 + this). Omit for the harness default (3); 0 restores a hard max_steps wall |
Approval over HTTP. Only
"full-auto"and"auto-edit"let a run finish unattended. There is no interactive tool-approval channel on this route, so a tool gated by"suggest"is rejected with asystemevent explaining why — the run continues and adapts rather than hanging.
Step budget.
max_steps(50) is a runaway guard. A run that exhausts it while still landing successful tool calls, with a healthy circuit breaker, is granted more runway instead of stopping mid-plan; a stalled or spinning run is not. Raisemax_step_extensionsfor long tasks, lower it to cap cost.
Response 200 OK:
{
"session_id": "a1b2c3d4e5f6..."
}
The session_id is a cryptographically random 128-bit hex string.
Example:
curl -X POST http://localhost:7878/agent \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"task": "Add input validation to src/api/handler.rs",
"approval": "full-auto"
}'
GET /stream/:session_id
Subscribe to real-time agent events via SSE. Connect after calling POST /agent.
SSE event data (JSON):
Each event’s data field is a JSON object with these fields:
| Field | Type | Present when |
|---|---|---|
type |
string |
Always. One of: chunk, step, system, retry, complete, partial, error |
content |
string |
chunk, system, retry, complete, partial, error |
step_num |
number |
step |
tool_name |
string |
step |
success |
boolean |
step |
steps_completed |
number |
partial |
steps_planned |
number |
partial |
remaining_plan |
string[] |
partial |
attempt |
number |
retry (0-based) |
max_attempts |
number |
retry |
backoff_ms |
number |
retry |
Event types:
| Type | Terminal | Description |
|---|---|---|
chunk |
no | Incremental text from the LLM |
step |
no | A tool was executed (e.g., read_file, bash) |
system |
no | Daemon advisory that isn’t model output |
retry |
no | Transient provider error; backing off before another attempt |
complete |
yes | Agent finished the task. content has the summary |
partial |
yes | Agent stopped with planned work outstanding |
error |
yes | Agent failed. content has the error message |
The stream closes after a terminal event. Break your read loop on complete,
partial or error — treating only complete as success is wrong:
partial means the agent stopped before finishing everything it planned, and
remaining_plan lists exactly what it never executed. The corresponding job
record ends in status partial (terminal, but neither complete nor
failed), and the run is resumable from its checkpoint.
Unknown type values should be ignored rather than treated as errors — new
non-terminal kinds may be added.
A partial run:
data: {"type":"step","step_num":1,"tool_name":"read_file","success":true}
data: {"type":"partial","content":"Refactored the parser.\n\nRemaining (2 of 3 steps not done):\n 2. update the call sites\n 3. run the test suite","steps_completed":1,"steps_planned":3,"remaining_plan":["update the call sites","run the test suite"]}
Example:
curl -N http://localhost:7878/stream/a1b2c3d4e5f6... \
-H "Authorization: Bearer $VIBECLI_TOKEN"
Response stream:
data: {"type":"chunk","content":"Reading the file..."}
data: {"type":"step","step_num":1,"tool_name":"read_file","success":true}
data: {"type":"chunk","content":"Adding validation..."}
data: {"type":"step","step_num":2,"tool_name":"write_file","success":true}
data: {"type":"complete","content":"Added input validation for all 3 handler functions."}
Errors:
| Status | Cause |
|---|---|
404 |
Session '<id>' not found |
GET /jobs
List all persisted job records, sorted by most recent first.
Response 200 OK:
[
{
"session_id": "a1b2c3d4...",
"task": "Add input validation",
"status": "complete",
"provider": "ollama",
"started_at": 1710700000000,
"finished_at": 1710700060000,
"summary": "Added input validation for all 3 handler functions."
}
]
JobRecord fields:
| Field | Type | Description |
|---|---|---|
session_id |
string |
Unique job identifier |
task |
string |
Original task description |
status |
string |
"queued", "running", "complete", "partial", "failed", "cancelled". "partial" is terminal but means the agent stopped with planned work outstanding — resumable, and not a success |
provider |
string |
AI provider name |
started_at |
number |
Unix timestamp (milliseconds) |
finished_at |
number? |
Unix timestamp (milliseconds), null if running |
summary |
string? |
Completion summary or error message |
curl http://localhost:7878/jobs \
-H "Authorization: Bearer $VIBECLI_TOKEN"
GET /jobs/:id
Get a single job record by session ID.
Response 200 OK: A single JobRecord object (same schema as above).
curl http://localhost:7878/jobs/a1b2c3d4... \
-H "Authorization: Bearer $VIBECLI_TOKEN"
Errors: 404 if not found.
POST /jobs/:id/cancel
Cancel a running job. Removes the SSE stream and marks the job as cancelled.
Response 200 OK: The updated JobRecord with status: "cancelled".
curl -X POST http://localhost:7878/jobs/a1b2c3d4.../cancel \
-H "Authorization: Bearer $VIBECLI_TOKEN"
Errors: 404 if not found. If the job is already finished, it returns the record unchanged.
POST /voice/transcribe
Speech to text. The single transcription surface for every client — panels, mobile, watch, the editor plugins and the SDK all go through this rather than each calling a speech provider directly.
The daemon picks the engine: a downloaded local whisper model when
voice.prefer_local is set or no cloud key is configured, Groq’s
whisper-large-v3 otherwise, with fallback between them. The response says
which one ran.
Two body forms are accepted:
Content-Type |
Body |
|---|---|
application/json |
{"audio_base64": "…", "mime_type": "audio/webm", "language": "en", "prefer_local": false} |
| an audio type | the raw audio bytes; hints go in X-Voice-Language / X-Voice-Prefer-Local |
Supported audio types: audio/webm, audio/wav, audio/mp4 (m4a),
audio/mpeg, audio/ogg, audio/flac, audio/aac. Anything else is a 415
rather than a guess — a wrong extension surfaces later as an opaque ffmpeg
error.
Body limit: 16 MB, overriding the daemon-wide 1 MB cap (~5 minutes of 16 kHz mono WAV).
Response 200 OK:
{ "text": "add a test for the parser", "engine": "local_whisper" }
engine is local_whisper (audio never left the machine) or cloud_whisper
(audio was uploaded to Groq).
# Raw bytes — the easiest form from a shell or a non-JS client.
curl -X POST http://localhost:7878/voice/transcribe \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: audio/wav" \
--data-binary @clip.wav
Errors: 400 empty or malformed body · 415 unsupported audio type ·
503 no engine available. The 503 body carries setup guidance (“run
/voice download base”, “set GROQ_API_KEY”) — surface it to the user
verbatim rather than reporting the status code.
GET /voice/status
What the voice stack can do on this machine. Call it once to decide whether to offer a mic button, and to explain a disabled one.
Response 200 OK:
{
"cloud_stt_configured": false,
"cloud_tts_configured": false,
"local_model": "base",
"local_model_size_mb": 142,
"local_model_downloaded": true,
"prefer_local": true,
"language": "en",
"whisper_cpp_installed": true,
"whisper_python_installed": false,
"sox_installed": true,
"ffmpeg_installed": false,
"can_transcribe": true,
"upload_limit_bytes": 16777216
}
can_transcribe is the field to branch on: a downloaded model with no runtime
to execute it is not a usable engine, and this accounts for that. Results are
cached for 60 s because the probe shells out to whisper-cli, whisper, sox
and ffmpeg.
ffmpeg_installed matters only for local transcription of non-WAV audio.
Browser clients record WebM, so on a machine with a local model but no ffmpeg
their recordings can only be transcribed in the cloud — the 503 says so
explicitly rather than blaming the Whisper runtime.
GET /sessions
HTML page listing all agent sessions. Useful for browsing in a web browser.
curl http://localhost:7878/sessions \
-H "Authorization: Bearer $VIBECLI_TOKEN"
GET /sessions.json
JSON list of all sessions (machine-readable alternative to /sessions).
curl http://localhost:7878/sessions.json \
-H "Authorization: Bearer $VIBECLI_TOKEN"
GET /view/:id
HTML page for a specific session with full conversation history.
curl http://localhost:7878/view/a1b2c3d4... \
-H "Authorization: Bearer $VIBECLI_TOKEN"
GET /share/:id
Read-only shareable session view. Displays a “Shared” banner at the top.
curl http://localhost:7878/share/a1b2c3d4... \
-H "Authorization: Bearer $VIBECLI_TOKEN"
WS /ws/collab/:room_id
WebSocket endpoint for real-time CRDT collaboration. No Bearer token required (public).
Connect:
websocat ws://localhost:7878/ws/collab/my-room
Message format: Binary CRDT sync messages from the vibe-collab crate. Messages are broadcast to all peers in the room.
Related REST endpoints (authenticated):
| Method | Path | Description |
|---|---|---|
POST |
/collab/rooms |
Create a new collaboration room |
GET |
/collab/rooms |
List all active rooms |
GET |
/collab/rooms/:room_id/peers |
List peers in a room |
POST /acp/v1/tasks
Create a task via the Agent Client Protocol. Runs the agent in full-auto mode.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
task |
string |
Yes | Task description |
context |
object |
No | Optional context |
context.workspace_root |
string |
No | Override workspace directory |
Response 201 Created:
{
"id": "acp-a1b2c3d4e5f6...",
"status": "pending",
"summary": "Task queued: Add tests for auth module",
"files_modified": [],
"steps_completed": 0
}
curl -X POST http://localhost:7878/acp/v1/tasks \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "Add tests for auth module"}'
GET /acp/v1/tasks/:id
Get ACP task status.
Response 200 OK:
{
"id": "acp-a1b2c3d4e5f6...",
"status": "complete",
"summary": "ACP task completed",
"files_modified": [],
"steps_completed": 0
}
curl http://localhost:7878/acp/v1/tasks/acp-a1b2c3d4e5f6... \
-H "Authorization: Bearer $VIBECLI_TOKEN"
GET /acp/v1/capabilities
ACP capability advertisement. No authentication required.
curl http://localhost:7878/acp/v1/capabilities
POST /webhook/github
GitHub App webhook endpoint. No Bearer token required. Uses HMAC-SHA256 signature verification via the X-Hub-Signature-256 header.
Headers:
| Header | Description |
|---|---|
X-GitHub-Event |
Event type (e.g., pull_request) |
X-Hub-Signature-256 |
HMAC-SHA256 signature |
Response 200 OK:
{
"status": "reviewed",
"findings": 3,
"summary": "Found 3 issues in the PR"
}
Unhandled event types return {"status": "ignored"}.
POST /webhook/skill/:skill_name
Trigger a skill by its webhook_trigger name. Requires authentication.
curl -X POST http://localhost:7878/webhook/skill/deploy-prod \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-d '{"ref": "main"}'
Response 200 OK:
{
"triggered": true,
"skill": "deploy-production",
"body_length": 16
}
Errors: 404 if no skill has a matching webhook_trigger.
Memory Endpoints
The OpenMemory cognitive memory engine provides persistent, queryable memory across two storage layers: the cognitive store (5-sector vector graph) and the verbatim drawer store (lossless 800-char chunks).
All memory endpoints require authentication (Authorization: Bearer $VIBECLI_TOKEN).
Cognitive store
| Method | Path | Description |
|---|---|---|
POST |
/memory/add |
Add a memory entry (sector auto-classified) |
POST |
/memory/query |
Semantic search with composite scoring |
GET |
/memory/list |
List all memories (supports ?sector= and ?limit= params) |
GET |
/memory/stats |
Counts by sector, storage size, encryption status, drawer count |
POST |
/memory/fact |
Add a temporal fact (auto-closes previous same-key fact) |
GET |
/memory/facts |
List active and closed facts |
POST |
/memory/decay |
Run exponential salience decay |
POST |
/memory/consolidate |
Sleep-cycle consolidation — merge weak memories, generate reflections |
GET |
/memory/export |
Export all memories as JSON |
POST |
/memory/import |
Import memories from mem0 / Zep / native JSON |
POST |
/memory/pin |
Pin a memory by ID (exempt from decay and purge) |
POST |
/memory/unpin |
Remove the pin flag from a memory |
POST |
/memory/delete |
Delete a memory permanently by ID |
Verbatim drawer layer (MemPalace)
| Method | Path | Description |
|---|---|---|
POST |
/memory/chunk |
Ingest text as verbatim 800-char chunks |
GET |
/memory/drawers/stats |
Drawer count, Wing/Room distribution, dedup hit rate |
POST |
/memory/tunnel |
Create a cross-project waypoint between two memories |
POST |
/memory/auto-tunnel |
Auto-detect and create tunnel waypoints across stores |
GET |
/memory/benchmark |
Run LongMemEval recall@K (supports ?k= param, default 5) |
4-layer context
| Method | Path | Description |
|---|---|---|
POST |
/memory/context |
Get the full 4-layer context block the agent would receive |
# Add a cognitive memory
curl -X POST http://localhost:7878/memory/add \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "The auth module uses JWT with RS256 signing"}'
# Semantic query
curl -X POST http://localhost:7878/memory/query \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "How does authentication work?", "limit": 5}'
# Ingest raw text as verbatim chunks
curl -X POST http://localhost:7878/memory/chunk \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "Runbook step 3: restart payment-worker pods after migration 0047..."}'
# Get 4-layer agent context
curl -X POST http://localhost:7878/memory/context \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "deployment process", "l1_tokens": 700, "l2_limit": 8}'
# Run recall benchmark at k=5
curl "http://localhost:7878/memory/benchmark?k=5" \
-H "Authorization: Bearer $VIBECLI_TOKEN"
# Pin a memory (survives decay and consolidation purge)
curl -X POST http://localhost:7878/memory/pin \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id": "mem_c2a9"}'
# Remove a pin
curl -X POST http://localhost:7878/memory/unpin \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id": "mem_c2a9"}'
# Delete a memory permanently
curl -X POST http://localhost:7878/memory/delete \
-H "Authorization: Bearer $VIBECLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id": "mem_d1f6"}'
/memory/pin, /memory/unpin, /memory/delete responses:
{ "ok": true }
All three endpoints return {"ok": false, "error": "memory not found"} when the id does not match any stored memory.
/memory/stats response:
{
"total_memories": 47,
"total_waypoints": 12,
"total_facts": 9,
"total_drawers": 132,
"encryption": false,
"sectors": [
{ "sector": "Semantic", "count": 18, "avg_salience": 0.82, "pinned_count": 3 },
{ "sector": "Episodic", "count": 14, "avg_salience": 0.61, "pinned_count": 1 },
{ "sector": "Procedural", "count": 11, "avg_salience": 0.75, "pinned_count": 2 },
{ "sector": "Reflective", "count": 3, "avg_salience": 0.90, "pinned_count": 3 },
{ "sector": "Emotional", "count": 1, "avg_salience": 0.45, "pinned_count": 0 }
],
"embedding_dim": 512,
"embedding_compression_ratio": 10.7,
"embedding_backend": "turboquant"
}
The embedding_* fields describe the in-process vector index. embedding_backend is currently always "turboquant" (~3 bits/dim compressed); clients should treat the field as opaque so future backends (e.g. "hnsw_f32", "candle_bert") can be added without breaking parsers.
/memory/benchmark response:
{
"k": 5,
"total_memories": 47,
"total_drawers": 132,
"probes": 20,
"hits_cognitive": 15,
"hits_verbatim": 18,
"recall_cognitive": 0.75,
"recall_verbatim": 0.90,
"recall_combined": 0.975,
"cases": [
{ "sector": "episodic", "query": "What was the last project I worked on?", "found_cognitive": true, "found_verbatim": true },
{ "sector": "preference", "query": "What coding style does the user prefer?", "found_cognitive": false, "found_verbatim": true }
]
}
Tauri Commands (VibeCoder)
The following Tauri commands are available for the VibeCoder frontend via invoke(). All commands are registered in vibecoder/src-tauri/src/lib.rs.
Memory commands
| Command | Arguments | Returns |
|---|---|---|
openmemory_stats |
— | { total_memories, total_waypoints, total_facts, total_drawers, sectors[] } |
openmemory_add |
content: string, tags?: string[] |
{ id, sector, tags, weight, created_at } |
openmemory_query |
query: string, limit?: number, sector?: string |
QueryResult[] |
openmemory_list |
offset?: number, limit?: number, sector?: string |
Memory[] |
openmemory_facts |
— | TemporalFact[] |
openmemory_add_fact |
subject, predicate, object: string |
TemporalFact |
openmemory_decay |
— | { decayed: number, remaining: number } |
openmemory_consolidate |
— | { merged: number, reflections_created: number } |
openmemory_export |
— | string (markdown) |
openmemory_enable_encryption |
key?: string |
{ enabled: boolean } |
openmemory_pin |
id: string |
{ ok: boolean } |
openmemory_unpin |
id: string |
{ ok: boolean } |
openmemory_delete |
id: string |
{ ok: boolean } |
Verbatim drawer commands
| Command | Arguments | Returns |
|---|---|---|
openmemory_drawer_stats |
— | { total_drawers, wings[], rooms[] } |
openmemory_layered_context |
query: string, l1_tokens?: number, l2_limit?: number |
{ l1_essential_story, l2_scoped[], l3_drawers[], total_drawers } |
openmemory_benchmark |
k?: number |
{ k, recall_cognitive, recall_verbatim, recall_combined, cases[], … } |
// Example: run benchmark and display results
const result = await invoke<BenchmarkResult>('openmemory_benchmark', { k: 5 });
console.log(`Combined Recall@5: ${(result.recall_combined * 100).toFixed(1)}%`);
// Example: get layered context for a query
const ctx = await invoke('openmemory_layered_context', {
query: 'deployment process',
l1Tokens: 700,
l2Limit: 8,
});
GET /pair
Generate a one-time device pairing URL. No authentication required.
curl http://localhost:7878/pair
Response 200 OK:
{
"url": "http://localhost:7878/pair?token=...",
"token": "abc123...",
"instructions": "Open this URL in your device's browser to pair with this VibeCLI instance."
}
Embeddings — /embeddings/*, /index/*
Semantic search / RAG. Full guide: embeddings.md.
GET /embeddings/models
Every embedding provider, its models, availability, the selected model, and the embedding models pulled into the local Ollama.
providers[].availability.state is ready | needs_api_key |
not_compiled_in. providers[].is_local says whether embedding leaves the
machine — relevant before indexing a private repo with a cloud model.
ollama_installed.status is ok or unreachable; the latter is distinct from
an empty model list, which would read as “nothing installed”.
POST /embeddings/embed
{ "texts": ["fn main() {}"], "kind": "document", "provider": "voyage", "model": "voyage-code-3" }
kind is "document" (default) or "query". Asymmetric models place stored
passages and search queries in different regions of the space; embedding a
query as a document does not error, it just costs recall. provider/model
override the configured selection.
Response dimension is the length actually returned, not a catalog value.
GET /index/status
{
"selected": { "provider": "ollama", "model": "nomic-embed-text" },
"description": "ollama/nomic-embed-text (768d, local)",
"built": true,
"current": { "format_version": 2, "chunk_count": 4180, "file_count": 611, "dimension": 768 },
"available": [ /* every index on disk, including other models */ ]
}
built refers to the selected model. available lists every per-model
index present — each one switchable to without re-embedding.
POST /index/build
{ "provider": "voyage", "model": "voyage-code-3" }
Both fields optional; omit to use the configured model. Responds when the index is written, not when the job starts — embedding a workspace takes real time and, on a paid provider, real money.
GET /health → embedding
"embedding": {
"status": "indexed",
"provider": "ollama",
"model": "nomic-embed-text",
"dimensions": 768,
"local": true,
"chunks": 4180,
"files": 611,
"other_indexes": ["voyage/voyage-code-3"]
}
status is indexed |
not_indexed |
misconfigured. Never includes the key. |
Goals — /v1/goals/*
Durable execution-intent primitive. See design/goal/README.md for the full data model + cross-client surface table.
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/goals |
Create. Body: { title, statement?, workspace?, success_criteria?, tags?, parent_goal_id? }. Returns 201 + Goal. 409 on (workspace, title) conflict. |
GET |
/v1/goals |
List. Query: status, workspace, tag, limit (default 50). Returns { goals, count }. |
GET |
/v1/goals/:id |
Detail. Returns { goal, links }. |
PATCH |
/v1/goals/:id |
Partial update. workspace and parent_goal_id use double-Option semantics (omit / null / value). Editing statement or success_criteria auto-clears current_plan. |
DELETE |
/v1/goals/:id |
Hard delete; links cascade. |
POST |
/v1/goals/:id/plan |
Generate ExecutionPlan via PlannerAgent. Body: { provider?, model? }. Per-request override honored when both are present and the API key resolves (env or profile_settings.db); otherwise falls back to the daemon’s configured provider. Response carries plan_provider_override_applied, plan_provider_requested, plan_model_requested. |
POST |
/v1/goals/:id/link |
Attach a session / job / recap / note. Body: { kind, target_id, note? }. |
POST |
/v1/goals/:id/start |
Spawn a session bound to this goal. Body: { task?, provider?, model? }. Returns { session_id, link_id, goal_id }. |
POST |
/v1/goals/:id/recap |
Cross-store aggregate recap. Body: { provider?, model? }. When both fields are supplied and the named provider is reachable, the daemon synthesizes the headline + bullets via LLM and sets recap_synthesizer: "llm". Otherwise the heuristic fold runs and recap_synthesizer: "heuristic" is returned. Per-target recaps are still collected via two-phase store split. |
GET |
/v1/goals/:id/children |
One-level tree query. Returns { parent_goal_id, children, count }. Walk iteratively for a full tree. |
GET |
/v1/goals/:id/tree |
Recursive subtree walk. Query: depth (default 3, clamped to 1..10). Returns { root, depth, tree: { goal, children, [truncated, direct_child_count, cycle] } }. Re-visited nodes set cycle: true so clients don’t recurse. |
GET |
/v1/goals/current |
Look up the pinned goal. Query: workspace? (empty / absent = global slot). Returns { workspace, goal_id, pinned_at, goal } or { workspace, goal_id: null }. |
PUT |
/v1/goals/current |
Pin or replace the current goal. Body: { goal_id, workspace? }. 404 if goal_id is unknown. |
DELETE |
/v1/goals/current |
Clear the pin. Query: workspace?. Returns { workspace, removed }. |
Watch (curated proxies)
The Apple Watch / Wear OS never hits /v1/* directly. Use the curated read-only /watch/goals pair instead.
| Method | Path | Notes |
|---|---|---|
GET |
/watch/goals |
Active goals only, ≤25, slim payload ({ id, title, status, workspace_label, updated_at, pinned }). pinned is true when the row is the workspace-specific OR global current pin (G11.2). Older daemons that lack the field decode cleanly on the watch side. |
GET |
/watch/goals/:id |
Envelope { goal, links, pinned } (G12.1 added pinned: bool at the envelope level so the watch detail / tile can render the ★ without a separate /v1/goals/current lookup; watch never hits /v1/*). |
POST |
/watch/goals/:id/start |
Curated wrapper for do_v1_exec_goal_start. Body: { task? }. Returns { session_id, link_id, goal_id }. |