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 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 /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
i16PCM. - 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
u32little-endian sample rate followed byf32samples. 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.