StackUnderflow ingests session data from more than one coding agent. Seventeen adapters are registered: four ship default-on (Claude Code, Codex, Cursor, Cline) and thirteen are opt-in beta (KiloCode, Roo Code, OpenCode, Cursor Agent, Qwen, Gemini, Copilot, Codeium, Continue, Droid, Kiro, OpenClaw, Pi + OMP).
| Provider | Status | Source format | Default state |
|---|---|---|---|
| Claude Code | stable | per-project JSONL under ~/.claude/projects/<slug>/ (+ legacy ~/.claude/history.jsonl) |
on |
| Codex | stable | rollout JSONL under ~/.codex/sessions/ |
on |
| Cursor | stable | SQLite state.vscdb at ~/Library/Application Support/Cursor/User/globalStorage/ |
on |
| Cline | stable | per-task JSON in ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/tasks/ |
on |
| KiloCode | beta | per-task JSON in …/kilocode.kilo-code/tasks/ (Cline parser) |
off |
| Roo Code | beta | per-task JSON in …/rooveterinaryinc.roo-cline/tasks/ (Cline parser) |
off |
| OpenCode | beta | SQLite under $XDG_DATA_HOME/opencode/ (or ~/.local/share/opencode/) |
off |
| Cursor Agent | beta | text/JSONL transcripts in ~/.cursor/projects/{p}/agent-transcripts/ (+ ~/.cursor/ai-tracking/ai-code-tracking.db) |
off |
| Qwen | beta | JSONL in $QWEN_DATA_DIR/projects/{p}/chats/*.jsonl (default ~/.qwen/) |
off |
| Gemini | beta | JSON / JSONL in ~/.gemini/tmp/{p}/chats/session-*.{json,jsonl} |
off |
| Copilot | beta | ~/.copilot/session-state/{sid}/events.jsonl + VS Code workspaceStorage/{h}/GitHub.copilot-chat/transcripts/ |
off |
| Codeium | beta | ~/.codeium/ (discovery stub — protobuf decoding deferred; yields nothing today) |
off |
| Continue | beta | ~/.continue/*.{db,sqlite,sqlite3} (defensive SQLite parser) |
off |
| Droid | beta | $FACTORY_DIR (or ~/.factory/sessions/{projectHash}/) |
off |
| Kiro | beta | ~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent/*.chat |
off |
| OpenClaw | beta | first existing of ~/.openclaw/, ~/.clawdbot/, ~/.moltbot/, ~/.moldbot/ (agents/) |
off |
| Pi + OMP | beta | ~/.pi/agent/sessions/ and ~/.omp/agent/sessions/ (one adapter, both roots) |
off |
Cursor and Cline shipped as opt-in beta in v0.4.0, behind STACKUNDERFLOW_BETA_CURSOR=1 / STACKUNDERFLOW_BETA_CLINE=1. They were promoted to default-on in v0.6.0: both have test coverage, fingerprint-based caching for the Cursor vscdb (stackunderflow/infra/cursor_cache.py), and have run against real local user data across several releases. Existing installs that already exported the beta env vars need no change — the env vars are a no-op for the two promoted adapters.
The remaining 13 beta adapters are gated by environment variables in stackunderflow/adapters/__init__.py:
STACKUNDERFLOW_BETA_KILOCODE=1 stackunderflow start
STACKUNDERFLOW_BETA_ROOCODE=1 stackunderflow start
STACKUNDERFLOW_BETA_OPENCODE=1 stackunderflow start
STACKUNDERFLOW_BETA_CURSOR_AGENT=1 stackunderflow start
STACKUNDERFLOW_BETA_QWEN=1 stackunderflow start
STACKUNDERFLOW_BETA_GEMINI=1 stackunderflow start
STACKUNDERFLOW_BETA_COPILOT=1 stackunderflow start
STACKUNDERFLOW_BETA_CODEIUM=1 stackunderflow start
STACKUNDERFLOW_BETA_CONTINUE=1 stackunderflow start
STACKUNDERFLOW_BETA_DROID=1 stackunderflow start
STACKUNDERFLOW_BETA_KIRO=1 stackunderflow start
STACKUNDERFLOW_BETA_OPENCLAW=1 stackunderflow start
STACKUNDERFLOW_BETA_PI=1 stackunderflow start # toggles both Pi + OMPCombine them in one invocation:
STACKUNDERFLOW_BETA_QWEN=1 STACKUNDERFLOW_BETA_GEMINI=1 stackunderflow startTo make the opt-in persistent, export the variables from your shell rc (~/.zshrc, ~/.bashrc):
export STACKUNDERFLOW_BETA_QWEN=1
export STACKUNDERFLOW_BETA_GEMINI=1The flag parser accepts 1, true, yes, on (case-insensitive); anything else leaves the adapter unregistered.
Cursor. Reads the cursorDiskKV table in ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb (opened via SQLite read-only URI). Two key prefixes are walked: bubbleId:% for chat bubbles (with text, modelInfo.modelName, tokenCount, createdAt) and agentKv:blob:% for agent KV blobs (with content, providerOptions.cursor.modelName). One SessionRef is yielded per conversationId; source_kind="database" and seq is the SQLite rowid so resumable reads use the rowid as a high-water mark. macOS only in v1 — Linux and Windows path constants are present in stackunderflow/adapters/cursor.py but untested.
Cline. Reads each task directory ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/tasks/{taskId}/. Two files per task: ui_messages.json (a flat array of UI events — api_req_started events become assistant records and carry tokensIn / tokensOut / cacheWrites / cacheReads) and api_conversation_history.json (Anthropic-shape messages whose first user message embeds <model>...</model>). One SessionRef per task; seq is the event index in ui_messages.json rather than a byte offset, so resume means "skip first N events." macOS only in v1.
Adapter source: stackunderflow/adapters/cursor.py, stackunderflow/adapters/cline.py. See docs/adapters.md for the contract these adapters implement.
Provider chips render on the session table and project cards, color-coded per provider. When the same project slug is ingested through more than one adapter (the schema's UNIQUE(provider, slug) constraint allows one row per provider), the project card renders one chip per provider. Implementation: stackunderflow-ui/src/components/common/ProviderChip.tsx. The API surfaces the field on /api/projects (as provider plus a providers array) and on /api/jsonl-files (as provider).
Cursor publishes no general per-token rate card — users pay a flat subscription and the IDE multiplexes Anthropic / OpenAI / Google / Cursor-trained models behind the scenes. CursorPricer (stackunderflow/infra/providers/cursor.py) prices each cursor model id one of four ways:
| Class | Examples | Rate source |
|---|---|---|
| Vendor-prefixed | claude-4.5-sonnet-thinking, claude-4.6-sonnet, gpt-5-codex, gpt-4o, gemini-2.5-pro-preview-05-06, gemini-3-pro |
Delegates to AnthropicPricer / OpenAIPricer / GeminiPricer. A Claude-via-Cursor record costs the same as a native Claude record. Gemini ids with -preview-MM-DD or -experimental suffixes strip back to the base id and retry. |
composer-1 |
composer-1 |
Cursor's published rate: input $1.25/M, output $10.00/M, cache-write $1.5625/M, cache-read $0.125/M (cursor.com/docs/models-and-pricing). |
composer-2 |
composer-2 |
ESTIMATED at Anthropic Sonnet 4.x rates: input $3/M, output $15/M, cache-write $3.75/M, cache-read $0.30/M. Cursor publishes no per-token rate for composer-2; Sonnet-tier is the closest acknowledged analogue for an agentic Sonnet-class model. |
| Autoselectors | cursor-auto, cursor-fast |
Same ESTIMATED Sonnet-tier rates. When Cursor picks the model the actual engine is unknown, and the estimate keeps these records out of $0 territory in compare / cost reports. |
An id no delegate recognizes falls back to the same Sonnet-tier estimate rather than returning None, so a record with non-zero token counts always contributes a real dollar figure. The source comments flag every estimated rate.
When a record's record.raw["cost_source"] == "estimated", the UI prefixes its cost with ≈ and exposes a tooltip ("estimated cost — provider does not surface per-message tokens"). The Cursor adapter sets this flag whenever it falls back to the len(text) // 4 heuristic because the bubble has zero tokenCount.{inputTokens, outputTokens} (Cursor v3 returns zero counts on every bubble; see stackunderflow/adapters/cursor.py).
The flag is set on the adapter Record and persists in messages.raw_json. The ETL usage_events pipeline carries cost_source as a first-class column: the normalizer layer (stackunderflow/etl/normalize/) stamps every event with one of live, rate_card, estimated, or unknown (the COST_SOURCE_* constants in etl/normalize/base.py). The ≈ marker on the Sessions table reads the messages-level flag.
flowchart LR
user[User: stackunderflow start] --> ingest[run_ingest]
ingest --> claude[ClaudeAdapter]
ingest --> codex[CodexAdapter]
ingest --> cursor[CursorAdapter]
ingest --> cline[ClineAdapter]
ingest --> betas[13 opt-in beta adapters]
claude --> store[(SQLite store.db)]
codex --> store
cursor --> store
cline --> store
store --> api[/REST API/]
api --> ui[React dashboard]
Cursor / Cline show no data. Check that the source file exists. Cursor: ls ~/Library/Application\ Support/Cursor/User/globalStorage/state.vscdb. Cline: ls ~/Library/Application\ Support/Code/User/globalStorage/saoudrizwan.claude-dev/tasks/. If either path is missing the adapter exits cleanly without logging an error — the tool is not installed or has not been used on this machine. After confirming the path, re-run stackunderflow reindex.
I enabled a beta adapter but no data shows up. Same pattern: confirm the on-disk source exists for the adapter you opted into (paths are listed in the table above), then re-run stackunderflow reindex with the env var set.
My Cursor sessions show $0 (or a ≈ marker). As of the cursor-pricing fix, cursor records with non-zero token counts always price at a real dollar figure: vendor-prefixed ids (claude-*, gpt-*, gemini-*) delegate to the upstream pricer; composer-1 prices at Cursor's published rate; composer-2 and the cursor-auto / cursor-fast autoselectors use ESTIMATED Anthropic Sonnet 4.x rates (see "Cursor pricing" above). A record still showing $0 means its token counts are zero — Cursor v3 stores zero tokenCount.{inputTokens, outputTokens} on every bubble, and the adapter's len(text) // 4 fallback estimate also returns 0 when the bubble's text payload is empty (the v3 bubble shape stores rich JSON with diffs and code chunks instead of a top-level text field, so some assistant bubbles legitimately have no estimable text). The ≈ marker on Sessions table rows reflects the estimated-tokens flag; cost is estimated even when the dollar figure is non-zero.
How do I disable a beta adapter? Unset the env var (unset STACKUNDERFLOW_BETA_QWEN) and restart the server. The adapter is no longer registered and any existing rows in the store stay put — running stackunderflow reindex again only refreshes whatever the registered adapters can see. Cursor and Cline can no longer be disabled via env var (they're default-on as of v0.7.0); to skip them, comment out the register(_CursorAdapter()) / register(_ClineAdapter()) calls in stackunderflow/adapters/__init__.py or run a custom build.