GitHub Docs

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
CORS methods GET, POST, PUT, PATCH, DELETE, OPTIONS

A method the router serves has to appear in that list or the request is unreachable from any browser client: the preflight is refused before the request is sent, and the caller sees the browser’s own transport error (Load failed in WKWebView) with no status code to explain it.

Authentication

Every route requires Authorization: Bearer <token> except the ones below, and “except” covers three different mechanisms rather than one:

No credential at all — /health, /models, /web, /favicon.svg, /pair, /acp/v1/capabilities, /v1/capabilities, /mobile/beacon, /.well-known/mcp.json.

Authenticated by query parameter — /ws/collab/{room_id} and /ws/voice/duplex. These bypass the bearer middleware because a WebSocket handshake cannot set headers; each validates ?token= itself with a constant-time compare and answers 401 Invalid token when it does not match. They are not open.

Authenticated by signature — /webhook/github verifies X-Hub-Signature-256 against the configured webhook secret.

Everything else is a 401 without the header. There is no unauthenticated mode, so a client that omits it is completely broken, not degraded.

# 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.19"
}
curl http://localhost:7878/health

The full document also carries graph, skillforge, embedding, kv_cache_codec_probe, api_token and features.soffice (LibreOffice availability — see Office) 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 a system event 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. Raise max_step_extensions for 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 /harness/profile

The harness settings for one (provider, model) pair — what that model is given beyond the conversation. See Model Harness Profiles.

Query: provider (required), model (optional — omit or pass * for the provider-wide entry).

Response 200 OK:

{
  "provider": "claude",
  "model": "claude-opus-5",
  "effective": {
    "tool_transport": "native",
    "prompt_dialect": "compact",
    "prompt_cache": true
  },
  "builtin": {
    "tool_transport": "native",
    "prompt_dialect": "compact",
    "prompt_cache": true
  }
}

effective is what will actually be sent; builtin is what this build ships. provider_override and model_override appear only when set, and name exactly the fields the user changed.

Absent fields — max_output_tokens, temperature, context_window_fallback, thinking_budgets — mean the provider decides. They are never zero and never unlimited: VibeCody ships no vendor number it cannot verify.

honored_fields lists the fields this provider can actually act on. prompt_cache is Anthropic-only, parallel_tool_calls is OpenAI-shaped, and a token thinking budget means nothing to a provider whose reasoning dial is an effort word. Clients render controls only for the fields listed; setting one of the others stores fine but would never reach a request.

PUT /harness/profile

Set an override for one pair. The body is a patch — only the fields being changed. Returns the newly resolved profile in the shape above.

Query: provider (required), model (optional).

{ "tool_transport": "prose", "prompt_dialect": "full" }

Takes effect on the next request; no daemon restart is needed.

DELETE /harness/profile

Remove an override, returning the pair to its built-in default. Returns the newly resolved profile.

An empty PUT body does the same thing, deliberately: storing a patch of nothing would pin the pair to today’s defaults, so an improved default in a later release would never reach it.

GET /harness/profiles

Every override currently in effect, plus each one resolved.

Response 200 OK:

{
  "overrides": {
    "ollama/qwen3-coder": { "tool_transport": "prose" }
  },
  "profiles": [
    { "provider": "ollama", "model": "qwen3-coder", "effective": {}, "builtin": {} }
  ]
}

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 /voice/settings

Which speech engine speaks the assistant’s replies, and what else this machine could run. This is the daemon’s own setting rather than a per-client preference: the daemon is what speaks, so three clients keeping local copies would be three settings disagreeing about one machine.

Response 200 OK:

{
  "engine": "kokoro",
  "engines": [
    { "id": "system", "label": "System", "available": true,
      "detail": "The platform voice. Fastest to speak." },
    { "id": "kokoro", "label": "Neural (Kokoro)", "available": true,
      "detail": "Natural voices, 9 languages. Slower to start speaking." }
  ],
  "voice": "af_heart",
  "language": "auto",
  "voices": [
    { "id": "af_heart", "name": "Heart", "lang": "en", "quality": "neural" }
  ],
  "languages": ["en", "en-gb", "es", "fr", "hi", "it", "ja", "pt", "zh"],
  "provider": "ollama",
  "model": "llama3.2:latest",
  "models": [
    { "id": "ollama/llama3.2:latest", "name": "llama3.2:latest", "provider": "ollama",
      "size_bytes": 2019393189 }
  ]
}

provider and model are the model that answers a spoken turn, and both are null until someone sets them — in which case the model the client sends answers, as it always did. models is the same catalog GET /models returns, included here so a settings pane does not need a second source for the same question.

The setting exists because the two jobs have different budgets. The composer picks a model to write code with; a spoken turn is silence while it waits, and the model is nearly all of it — measured on a 24 GB Mac, a 20B answered a one-sentence question in 5.0 s warm and 42 s cold (26.6 s of model load, 12.0 s of prefill), against 0.58 s of recognition and milliseconds of speech. When it is set it wins over the provider/model the client sends: a setting that deferred to the client would be visible, saved, and inert on all three desktop shells, every one of which sends its own pair.

Every engine is listed whether or not it can run here, and an unavailable one carries in detail why — the command that would install it. A client that hides the unavailable engines leaves the user with no way to discover that the neural one exists; one that greys them out with no reason shows what reads as a bug.

voices comes from the engine itself (the sidecar’s --voices), not from a table in the daemon, which would be wrong the moment someone installs a language pack. language: "auto" detects per turn.

PUT /voice/settings

Change the engine, the language, or the voice. Send only the fields you are changing.

curl -X PUT http://localhost:7878/voice/settings \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"engine":"kokoro","voice":"af_heart"}'

Response 200 OK — the settings as they now stand:

{ "engine": "kokoro", "voice": "af_heart", "language": "auto",
  "provider": "ollama", "model": "llama3.2:latest" }

provider and model travel together: send both to choose the model that answers spoken turns, or both as "" to hand the choice back to the client. Half a pair is refused with 400 rather than stored — the daemon needs both to build an override and falls back to its own provider given one, so a stored half would read back as a choice and behave as none. Omit both fields to leave the setting alone; that is a different request from clearing it.

Selecting an engine writes its sidecar paths too: "kokoro" with no interpreter configured is a setting that reads back correctly and does nothing. Changing any of the three drops the parked warm engine, or the next reply would be spoken by the voice you just changed away from.

Errors: 400 with an error string when the requested engine cannot run here. That string is the whole answer — “the neural engine is not installed. Run: make voice-kokoro” — so surface it verbatim rather than the status code.

Changing the engine changes which voices exist, so re-read GET /voice/settings after a change rather than echoing the patch back into your UI.

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/voice/duplex

Full-duplex voice: one open microphone, one speaking assistant, interruptible mid-sentence. The whole pipeline — voice-activity detection, turn-taking, transcription, the model call and speech synthesis — runs here; a client contributes a microphone and speakers and nothing else.

Requires a Bearer token, but a WebSocket cannot set headers, so it is passed as ?token= (the same mechanism /ws/collab uses).

Query param Meaning
token Bearer token. Required.
provider, model Which model answers. Both must be set to take effect — with only one, the daemon falls back to its startup provider, which may be a different vendor.
language en for the fast path, auto to detect per turn across 99 languages, or a language code to pin one.
voice TTS voice identifier.

Client → server

  • Binary frames: 16 kHz mono little-endian i16 PCM.
  • Text frames: {"type":"set_voice","id":"…"}, {"type":"set_language","lang":"…"}, {"type":"set_context","context":"…"}.

set_context is what the user can see — file tree, open file, pinned notes. Send it on connect and again whenever it changes. It is folded into the turn’s system prompt and bounded at 32k characters; an empty block clears it rather than leaving it unchanged, so closing a project does not leave the assistant answering about the old one.

Server → client

  • Binary frames: a u32 little-endian sample rate followed by f32 samples. Self-describing, because the streaming and batch synthesisers produce different rates and a wrong rate does not fail — it plays at the wrong pitch.
  • Text frames:
type Meaning
ready Which engines resolved: asr is whisper-server or whisper-cli, tts is streaming or batch. Worth checking — batch costs a few hundred ms of first-audio.
state listening · hearing · thinking · speaking.
transcript What was heard, with asr_ms and the detected lang.
speaking A sentence about to be spoken. Fires once per sentence — it is live text, not a turn. Build a chat log from reply. Reasoning blocks are filtered out upstream, so a reasoning model’s deliberation is neither sent nor spoken.
flush Barge-in — discard queued audio immediately.
carried The user kept talking before this turn could answer, so its words join the next turn rather than being dropped.
latency first_audio_ms from end of speech.
reply The turn: the full text plus asr_ms, llm_ttft_ms, total_ms. One per answer. Never empty — a turn that produced no answer sends error instead and no reply at all.
error Something the user should be told. Sent for a turn failure as well as a fatal one — the socket stays open and the next utterance starts a new turn, so a client must not treat it as the end of the conversation.
websocat "ws://localhost:7878/ws/voice/duplex?token=$TOKEN&provider=ollama&model=granite4.1:3b"

Echo cancellation is a precondition, not a nicety. With an open microphone and no AEC the assistant’s own voice trips the voice-activity detector and it interrupts itself every sentence. Surfaces without it should use POST /voice/transcribe push-to-talk instead. See Full-duplex voice.


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.

Developer Excellence Endpoints

DORA four keys, engineering-practice maturity, and onboarding readiness, computed from the repository rather than asserted. Full guide: Developer Excellence.

All Developer Excellence endpoints require authentication (Authorization: Bearer $VIBECLI_TOKEN) and are read-only.

Method Path Description
GET /devex/dora The four keys, each with its band, sample size and proxy, plus unmeasured for those that could not be computed
GET /devex/practices Practice maturity per signal, capped at level 3 (“defined”), with detection caveats
GET /devex/onboarding Bootstrap readiness signals and first-time contributors
GET /devex/scorecard Delivery and practices in one payload, with dora_coverage and a delivery grade over the metrics that exist
GET /devex/scorecard.md The same, rendered as a markdown briefing (text/markdown)
GET /devex/space The five SPACE dimensions: measures with their source, and for each dimension this repository cannot answer, the system that holds it. No aggregate score field exists
GET /devex/survey.md The quarterly experience-survey instrument (text/markdown)

Query parameters (all endpoints):

Parameter Required Description
path yes Repository or workspace to measure. Never inferred — a daemon that fell back to its own cwd would measure an unrelated tree and label the answer with the caller’s repository
window no Measurement window in days, 1–1825. Default 90
marker no How deployments are identified: tags (default) or merges
branch no Branch consulted when marker=merges. Default HEAD

Errors: 400 for a missing path, an unknown marker (the expected values are named), a window outside the range (the bound is named), or a path that is not inside a git repository. Out-of-range values are rejected, not clamped — a clamped value rendered back as the requested one is a number the caller never asked for.

Absence is not zero. A metric with no signal in the window is omitted from the payload and listed in unmeasured with a reason and a to_measure_this. A client that drops the unmeasured block has reintroduced the problem this API exists to prevent.

curl -s -H "Authorization: Bearer $VIBECLI_TOKEN" \
  "http://localhost:7878/devex/scorecard?path=/src/myrepo&window=90" | jq \
  '{grade: .scorecard.delivery_grade, coverage: .scorecard.dora_coverage, missing: [.scorecard.dora.unmeasured[].metric]}'

Engagement Endpoints

The four-phase delivery spine — Discover & Assess, Prove, Build & Harden, Operate & Transfer. Full guide: Engagements.

All engagement endpoints require authentication (Authorization: Bearer $VIBECLI_TOKEN).

Method Path Description
GET /engagements/template The engagement model: phases, deliverable templates, gate templates. Answers before any engagement exists
GET /engagements List engagements
POST /engagements Create one, seeded with all four phases, every promised deliverable, and its gates
GET /engagements/{id} The engagement plus its readiness report for all four phases
PATCH /engagements/{id} Set status and/or phase
DELETE /engagements/{id} Remove it (cascades to deliverables, evidence, gates)
POST /engagements/{id}/seed Add template rows introduced since creation; never disturbs an accepted deliverable
GET /engagements/{id}/deliverables List deliverables (?phase=discover\|prove\|build\|operate)
POST /engagements/{id}/deliverables Add a deliverable outside the template
PATCH /engagements/{id}/deliverables/{did} Patch status, owner, notes; omitted fields are untouched
GET /engagements/{id}/deliverables/{did}/evidence List attached evidence
POST /engagements/{id}/deliverables/{did}/evidence Attach evidence (file, url, run, metric, note)
DELETE /engagements/{id}/evidence/{eid} Remove one evidence item
GET /engagements/{id}/gates List gates (?phase=)
POST /engagements/{id}/gates Add a gate — 400 without a measurement procedure
POST /engagements/{id}/gates/{gid}/judge Record a verdict — 400 for pass with nothing observed
DELETE /engagements/{id}/gates/{gid} Remove a gate
POST /engagements/{id}/scan Walk a workspace and propose files as evidence. {"attach": true} records them; deliverable statuses are never changed. 400 with no path and no bound workspace
POST /engagements/{id}/advance Close the current phase; {"force": true} overrides and records the override
GET /engagements/{id}/report.md Status report (text/markdown)
GET /engagements/{id}/handover.md Handover pack (text/markdown)

Gate verdicts are not_measured, pending, pass, fail, waived — five disjoint states. not_measured is never reported as fail, and a phase’s completion is null (rendered n/a, never 0%) when nothing is in scope.

An unrecognised ?phase= value returns 400, not a silent “all phases”.

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 }

Plugin commands

Back the Plugin Governance panel. Workspace plugins — signed bundles under <workspace>/.vibecli/plugins/, distinct from the user-level plugins in Plugin Development.

Command Arguments Returns
plugin_list_installed workspacePath: string InstalledPlugin[]
plugin_catalog_list workspacePath: string CatalogPlugin[] — every core plugin with installed and policy for this workspace
plugin_install_from_catalog workspacePath: string, name: string, force: boolean InstalledPlugin + signing_key_persisted
plugin_install_from_file workspacePath: string, bundlePath: string, force: boolean InstalledPlugin
plugin_install_from_url workspacePath: string, url: string, force: boolean InstalledPlugin
plugin_set_policy workspacePath, name, policy, isAdmin { ok }
plugin_uninstall workspacePath, name, isAdmin boolean

VibeCoder’s Connectors panel has no Tauri wrapper — it calls /api/vibedesk/connectors* through daemonFetch directly. The five connectors_* commands it used to call were removed: they kept connectors in a process-local Vec, recorded every one as connected without a credential, and answered connectors_test from whether the row existed.

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."
}

Office — /office/*

LibreOffice-backed conversion for Visio, legacy Office and ODF, and PDF/PNG/SVG rendering. Optional; status is in GET /health → features.soffice (state: checking · ready · missing · configured_invalid · broken). Full guide: Office & LibreOffice.

Route Purpose
GET /office/soffice Fresh status, plus import_extensions
PUT /office/soffice/path {"path": "…"} sets the override (refused with 422 if it cannot start LibreOffice); {"path": null} clears it
POST /office/convert {"path", "to", "timeout_secs"?} → {output, target, bytes, cached, engine_version, elapsed_ms}. to is pdf, pdf_sheets (spreadsheets only: one PDF page per sheet), png, svg, docx, xlsx, pptx, odt, ods, odp, odg, html, txt or csv

Errors are distinct statuses: 503 not available (body includes soffice), 404 input not found, 422 unsupported or not that format, 413 too large, 502 no usable output, 504 timed out, 403 credential-store path, 400 unknown target.

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 }.

PDF export — /api/tasks/{id}/export.pdf, /api/vibedesk/files/export.pdf

Render a chat/agent session transcript or an arbitrary workspace Markdown file to PDF. Both require the bearer token and return raw application/pdf bytes (not JSON) on success. Rendering is entirely in-process — Markdown is parsed into vibe_docfmt’s document model, converted to Typst markup, and compiled to PDF via the typst crate embedded in the daemon (vibe-pdfgen) — no external binary (pandoc or otherwise) is shelled out to.

Method Path Purpose
GET /api/tasks/{id}/export.pdf The same conversation /api/tasks/{id}/history returns as JSON, rendered to PDF. 404 if the task id doesn’t exist. A task with no linked session yet still renders (an empty transcript).
POST /api/vibedesk/files/export.pdf Body: { path, root? }. path is resolved against root (or the daemon’s workspace root when omitted) through the same path-traversal guard every other file route uses — a path reaching outside the workspace or into a credential directory is refused with 403.

Desktop-only feature (VibeCoder, VibeDesk): no Flutter/watch/VS Code/SDK client was updated for these two routes.

Plugins & connectors — /api/vibedesk/plugins/*, /api/vibedesk/connectors/*

Back the Plugins panel: what is extending the agent in a workspace, what can be installed, and which MCP servers this machine is connected to. All require the bearer token. path (query for GET, body field for POST) scopes to a project, like the other /api/vibedesk/* routes; omitted, it uses the daemon’s workspace root.

Plugins here are the workspace plugin system — signed bundles under <workspace>/.vibecli/plugins/, policed per workspace. That is a different system from the user-level ~/.vibecli/plugins/ bundles described in Plugin Development; the two do not share a manifest format or an install directory.

Method Path Purpose
GET /api/vibedesk/plugins Components live in this workspace, grouped by kind. Read-only inventory.
GET /api/vibedesk/plugins/catalog Every core plugin compiled into the daemon, with installed and policy for this workspace.
POST /api/vibedesk/plugins/install Body: { name, path?, force? }. Materialises the catalog entry, signs it, and installs it through the same verified path as a downloaded bundle. Returns signing_key_persisted — false means the publisher fingerprint will differ next install — plus included (plugins a bundle brought) and connectors (what became of each one it expects).
POST /api/vibedesk/plugins/policy Body: { name, policy, path? }. policy is "on" or "off". "required" is refused: it is an admin pin the same user could not then lower.
POST /api/vibedesk/plugins/uninstall Body: { name, path? }. Removes the install directory and the policy row. removed: false means there was nothing on disk — the policy row may still have been cleared.

Plugins, bundles and connectors

Three words, one distinction worth keeping straight:

  • A connector answers what can the agent reach — one external service, one MCP server, authenticated once. Slack, GitHub, a Postgres database.
  • A plugin answers what does the agent know how to do — skills and rules that change how it works.
  • A bundle answers what job is it set up for. It installs a set of plugins and sets up the connectors that job assumes, so a role is one click instead of six.

A bundle carries includes (other catalog plugins) and connectors (ids from the connector catalog) instead of components of its own. It references the single-purpose plugins rather than copying them: two plugins shipping a skill of the same name would collide, and a second copy of a rule is a second thing to keep true.

Installing a bundle returns what it actually managed, per connector:

state Meaning
already_configured The workspace already had it; left as it was.
added Configured by this install. It needed no credential.
needs_credentials Not configured — it cannot run without a secret only you have. fields names them.
unknown Named by the bundle but absent from the connector catalog — a bad bundle.
failed Adding it errored; error says why.

needs_credentials is the one that matters. A bundle can wire up everything that asks for nothing and it cannot invent a token; reporting that as configured would be the feature lying at the moment it is most believed. The panel prompts for those and says the setup is incomplete until they are supplied.

Removing a bundle removes the bundle, not its members: another bundle may share one, and silently uninstalling a plugin the user is still relying on is worse than leaving a row they can remove themselves. Yours lists them individually.

Bundle Installs Expects
bundle-engineering review standards, test first, debugging filesystem, git, github
bundle-on-call incident response, debugging sentry, github, slack
bundle-security-review secure defaults, dependency hygiene github, git
bundle-data review standards postgres, sqlite
bundle-research technical writing fetch, brave-search, memory

Core plugin catalog

Compiled into the binary, so there is no registry to reach and nothing to download. Each ships skills and rules only — Markdown that skill_catalog and context_assembler already load. Hooks and MCP-server components are not offered here: a hook is an executable whose exec bit does not survive the bundle round-trip, and plugin MCP servers are registered only by a module nothing currently calls.

Every entry carries a category, which is what VibeDesk’s Integrations panel (formerly Plugins) groups its marketplace sections by.

Plugin Category Ships
core-review-standards Engineering Practice Rule: what a review must check before approving.
core-test-first Engineering Practice Skill: pinning behaviour before changing it, and spotting a vacuous test.
core-commit-craft Engineering Practice Skill: commit messages that say why.
core-refactoring Engineering Practice Skill: behaviour-preserving change, and when to stop.
core-secure-defaults Security Rules: secret handling, and bounding untrusted input.
core-dependency-hygiene Security Skill: adding, upgrading and removing a dependency.
core-performance Performance Skill: measure → attribute → fix → re-measure, and the usual win order.
core-debugging Operations Skill: reproduce, bisect the search space, confirm the cause.
core-incident-response Operations Skill: stop the bleeding, keep a timeline, write the follow-up.
core-api-design Design Skill: make the wrong call hard to write.
core-technical-writing Design Skill: documentation that stays true.

The signature on a catalog install is real and verified, but it attests integrity, not provenance: the manifest is signed on this machine with a locally generated P-256 key. The embedded publisher key in a third-party bundle carries exactly the same weight — that is this format’s trust model.

Connectors

A connector is an MCP server definition plus the credentials it needs. Definitions live in the workspace store; credentials live encrypted in workspace_secrets and are never returned by any route.

Method Path Purpose
GET /api/vibedesk/connectors { connectors, catalog }. Each configured connector reports missing_credentials; each catalog entry reports runtime_available (is npx / uvx on PATH). Neither carries a health field.
POST /api/vibedesk/connectors Body: { catalog_id, credentials?, path? } for a catalog entry, or { id, title?, command, args?, credentials?, path? } for a hand-entered server. A required credential left blank is refused.
POST /api/vibedesk/connectors/toggle Body: { id, enabled, path? }. Disabled connectors are not launchable; their credentials are kept.
POST /api/vibedesk/connectors/remove Body: { id, path? }. Returns secrets_deleted so the caller can say whether the credentials went too.
POST /api/vibedesk/connectors/probe Body: { id, path? }. Actually launches the server and lists its tools. Returns { result: { state: "ok", tools } \| { state: "failed", error } \| { state: "timedout", after_secs } , checked_at }. Bounded at 45s.

probe is the only route that reports a connector as working, because it is the only one that runs it. Nothing infers “connected” from the presence of a key, and no probe result is persisted — a stored ok is a claim about the past presented as a claim about now.

Enabled connectors are merged into vibecli’s /mcp command alongside [[mcp_servers]] from config.toml, with config.toml winning a name collision. Agent runs do not consume MCP tools yet, so a connector makes tools reachable from the CLI, not from an agent turn.

Catalog entries are the reference servers from modelcontextprotocol/servers plus this binary’s own. Listing one here is not a claim that it runs on this machine — the package may have moved, the runtime may be missing. probe is the only thing that answers that.

An argument may contain {secret:NAME}, resolved from the encrypted store at launch. That is how a connection string reaches a server that takes it as an argument without ever being stored in the definition. An unresolved placeholder is passed through verbatim, so the failure names the missing secret instead of looking like a malformed argument.

Connector Category Runtime Credentials
vibecli VibeCody this binary, via --mcp-server —
filesystem Files And Code npx —
git Files And Code uvx —
github Files And Code npx GITHUB_PERSONAL_ACCESS_TOKEN
gitlab Files And Code npx GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_API_URL
fetch Web And Search uvx —
brave-search Web And Search npx BRAVE_API_KEY
puppeteer Web And Search npx —
google-maps Web And Search npx GOOGLE_MAPS_API_KEY
postgres Databases npx POSTGRES_URL (an argument secret)
sqlite Databases uvx SQLITE_DB_PATH (an argument secret)
memory Agent Memory npx —
sequential-thinking Agent Memory npx —
slack Inbox And Collaboration npx SLACK_BOT_TOKEN, SLACK_TEAM_ID
sentry Observability uvx SENTRY_AUTH_TOKEN (an argument secret)
time Utilities uvx —
everything Utilities npx —

Morning briefing — /briefing

Authenticated GET /briefing reads saved history and routine status; PUT /briefing/settings saves settings; POST /briefing/generate starts an asynchronous run with supplied settings. Poll GET for running and last_error. POST /briefing/speak takes {"text": "<markdown>"} and returns audio/wav bytes — the voice readout behind VibeDesk’s Read aloud button. See Morning briefing for fields and scheduling.