Engineering

Cursor IDE Integration#

Co-Vibe supports Cursor (1.7+, the hooks-capable releases) in three ways:

  1. covibe-local setup writes a repo-local .cursor/mcp.json entry so Cursor agents can call the Co-Vibe tools.
  2. Setup also writes a repo-local .cursor/hooks.json so the Co-Vibe hook (scripts/cursor-covibe-hook.ts) receives Cursor's session lifecycle.
  3. The hook (on stop/sessionEnd) and covibe-local watch sweep usage out of Cursor's agent transcripts (full 4-way token split) and its local sqlite store (legacy bubble tokens plus Cursor's own cost accounting).

What Setup Writes#

When setup detects Cursor — a ~/.cursor directory, or the directory named by COVIBE_CURSOR_HOME — it merges the Co-Vibe server into .cursor/mcp.json:

json
{
  "mcpServers": {
    "covibe": {
      "command": "node",
      "args": ["<package>/bin/covibe-mcp.mjs"],
      "cwd": "/path/to/repo",
      "env": {
        "COVIBE_AGENT": "cursor",
        "COVIBE_BASE_URL": "http://localhost:3000",
        "COVIBE_MCP_TOKEN": "${env:COVIBE_MCP_TOKEN}"
      }
    }
  }
}

and the hook command into .cursor/hooks.json (schema version: 1, each event maps to an array of {command} entries):

json
{
  "version": 1,
  "hooks": {
    "sessionStart": [{ "command": "node <package>/bin/covibe-cursor-hook.mjs" }],
    "afterFileEdit": [{ "command": "..." }],
    "stop": [{ "command": "..." }],
    "sessionEnd": [{ "command": "..." }]
  }
}

Merges preserve every existing server and hook entry and are idempotent; afterFileEdit is currently a no-op success reserved for per-edit overlap checks. The hook payload arrives as JSON on stdin with conversation_id (equal to the composerId in Cursor's store), generation_id, model, hook_event_name, workspace_roots, and transcript_path; Cursor also sets CURSOR_PROJECT_DIR.

Token Safety#

The raw cv_ token is never written to disk. Cursor interpolates ${env:NAME} placeholders in mcp.json command, args, env, url, and headers values for stdio servers (its own syntax — not the bare ${NAME} Claude Code expands in .mcp.json), so the config references ${env:COVIBE_MCP_TOKEN} and Cursor reads the token from your shell at spawn time. Source: the Cursor MCP reference at cursor.com/docs/mcp (config interpolation). covibe-local doctor fails if a raw token ever appears in .cursor/mcp.json.

Exporting COVIBE_MCP_TOKEN is optional since the device-flow setup: the non-secret COVIBE_AGENT: "cursor" marker lets the stdio bridge and the Cursor hook look up the typed cursor token from ~/.covibe/credentials.json when the shell variable is unset (Cursor passes the unexpanded placeholder then, which the bridge treats as absent). An exported COVIBE_MCP_TOKEN keeps precedence. See device-setup.md.

What Is Captured#

  • Per-session per-model token usage with the full 4-way split (transcript lane): agent-mode sessions write Claude-Code-shaped transcripts whose assistant messages carry message.usage with input_tokens, output_tokens, cache_read_input_tokens, and cache_creation_input_tokens; the sweep emits one event per (session, model) increment with cache_read_tokens/cache_write_tokens mapped from the cache fields, source_event_id cursor-transcript:<sessionId>:<model>:<usageLineCount>.
  • Per-bubble token counts (legacy composer fallback): each assistant message is a bubbleId:<composerId>:<bubbleId> row in the global store with tokenCount.inputTokens/outputTokens; one telemetry event per nonzero bubble, source_event_id cursor-bubble:<composerId>:<bubbleId>. When a composer id also has transcript usage, its bubble token events are suppressed — the transcript wins because it carries the full split, and emitting both would double count. Cost events are unaffected by this rule: they are Cursor's own cost accounting, stored as submitted cost rather than token-derived.
  • Per-composer per-model cost: composerData:<composerId> rows carry a usageData map of model name to {costInCents, amount}; a change emits one cost event (cursor-usage:<composerId>:<model>, cost_usd = costInCents/100, zero token counts).
  • Lines added/removed: the hook pins the session's baseline HEAD at sessionStart and submits the session-scoped git delta at stop/sessionEnd (Cursor's own totalLinesAdded/Removed composer counters stay on disk as a cross-check).
  • Session lifecycle: sessionStart/stop/sessionEnd hooks drive covibe_session start/snapshot/end, exactly like the Claude Code hook.
  • preCompact context stats: the payload carries context_tokens and context_window_size; the hook records max_context_tokens when the preCompact event is wired into hooks.json.

Provider is derived from the model name (claude* is anthropic; gpt*, codex*, o<digit>* are openai; anything else is left unset). Nothing above is captured for repos excluded with covibe-local exclude: the hook exits before any tool call and the usage sweeps are repo-scoped (see device-setup.md). For other repos, hook activity registers the repo in ~/.covibe/repos.json so the machine-level watch service picks it up automatically — no per-repo registration command.

The Transcript Lane#

Current Cursor versions write agent-mode session transcripts at ~/.cursor/projects/<project-key>/agent-transcripts/<session_id>/<session_id>.jsonl plus subagent files under .../<session_id>/subagents/agent-<id>.jsonl (COVIBE_CURSOR_PROJECTS overrides the projects directory). The lines are Claude-Code shaped — {"role": ..., "message": {"usage": ..., "model": ...}} with a top-level role instead of type — so the sweep reuses the Claude Code transcript parser (scripts/local-runtime/claude-usage.ts).

The <project-key> is the workspace path with path separators replaced by -, colons dropped, and the leading - stripped (/Users/alice/proj -> Users-alice-proj). Derivation varies slightly by Cursor version (e.g. the drive-letter case on Windows), so when the derived key's directory is absent the sweep scans every project directory and keeps the ones whose key still prefix-matches case-insensitively. As a second net, the hook records each payload's transcript_path (keyed by conversation_id) into the usage state so the sweep reads exactly that file even when the key derivation misses.

The transcript session_id equals the hook's conversation_id and the composerId used by the bubble lane, which is what makes the double-count suppression above possible. Per-file line offsets and a per-session running count of usage-bearing lines live in the usage state (the count keeps source_event_ids stable across re-reads and monotonic as transcripts grow); the transcript lane primes on its own first run, so installs upgrading from the bubble lane do not flood historical transcript usage. The hook's stop payload is officially only {status, loop_count} and is never read for tokens.

How The Store Is Read#

The usage sweep reads ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb (COVIBE_CURSOR_DB overrides; Linux ~/.config/Cursor/..., Windows %APPDATA%\Cursor\...). The database is WAL and locked while the IDE runs, so every read opens an immutable=1 file: URI snapshot — never a lock. Composers are scoped to the repo via the workspace join: each workspaceStorage/<md5>/workspace.json names the opened folder, and that directory's own state.vscdb lists the folder's composers under the ItemTable key composer.composerData (subdirectory cwds are accepted). Dedupe state lives in .covibe/cursor-usage-state.json (version 1): the first sweep primes existing rows without submitting, and every event is keyed by its source_event_id so re-runs never double-count.

Auto Model Resolution#

When the user runs Auto, the disk rows store the literal model "default". The hook payload's model field is the only reliable resolved value, so the hook records a conversation_id to model map into the usage state file and the sweep uses it as the final fallback before "unknown". Sessions that ran before the hook was installed cannot be resolved retroactively.

What Is Provably Uncapturable#

  • The full 4-way token split for legacy composer history: transcripts capture the complete split for new agent-mode sessions, but sessions that predate the transcript layout only have bubbles, and roughly 88 percent of bubbles carry no tokenCount at all (so the remaining ~12 percent limitation applies to that history alone; the Cursor team Admin API is the only other source of complete splits for it).
  • Retroactive Auto-model resolution: disk stores "default"; only the live hook payload knows the resolved model.
  • Per-request API latency: the store keeps timestamps per bubble, not per upstream request.
  • Tab/autocomplete tokens: never written to the local store.
  • Rate-limit/quota state: server-side only, not mirrored locally.

Doctor Check#

covibe-local doctor prints one cursor check:

  • INFO when Cursor is not installed (nothing to do)
  • WARN with a setup hint when Cursor is installed but .cursor/mcp.json is missing, missing the Co-Vibe server, or stale
  • FAIL when .cursor/mcp.json stores a raw token
  • PASS when the config is wired (plus hook wiring via hasCoVibeCursorHooks and whether the global state.vscdb exists yet for the usage sweep)

Verification#

bash
npx vitest run tests/unit/local-companion-cursor-setup.test.ts --no-file-parallelism
npx vitest run tests/unit/local-companion-cursor-doctor.test.ts --no-file-parallelism
npx vitest run tests/unit/local-companion-cursor-usage.test.ts --no-file-parallelism
npx vitest run tests/unit/local-companion-cursor-transcripts.test.ts --no-file-parallelism
npx vitest run tests/unit/cursor-hook.test.ts --no-file-parallelism

File-Overlap Visibility#

Cursor sessions get conflict warnings the same two ways as Claude Code minus mid-edit context injection: the hook submits repo snapshots at sessionStart/stop so teammates see overlapping uncommitted edits, and agents pull covibe_team operation: "state" -> repo_snapshot_conflicts plus covibe_task operation: "check" file_conflicts before editing, as instructed by the covibe-local agent-rules snippet, installed in AGENTS.md and as a native always-on rule at .cursor/rules/covibe.mdc so the protocol loads even on Cursor versions without AGENTS.md support. The reserved afterFileEdit hook is the future slot for pushing per-edit overlap warnings into a running session.

View as .md