# 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_id`s 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.
