Providers
Each provider is an adapter with two halves: a read-only parser over whatever that tool already wrote to disk, and a run surface that shells out to the tool’s own CLI (or, for opencode, its own HTTP server). Discovery never writes into provider storage. Runs let those CLIs create new sessions as usual. See Trust.
From a terminal with the server up: threadle services (or health) prints process + every adapter. threadle providers is the adapters alone. See CLI.
| Provider | Storage (read-only) | Run / inject | Availability signal |
|---|---|---|---|
| Claude Code | ~/.claude/projects/<slug>/<uuid>.jsonl | claude CLI | ~/.claude/projects exists |
| opencode | ~/.local/share/opencode/opencode.db (SQLite) | opencode CLI + managed opencode serve | the db file exists |
| Cursor | ~/.cursor/projects/<slug>/agent-transcripts/… | agent CLI | storage dir exists or agent --version works |
| Antigravity | ~/.gemini/antigravity-cli/brain/<uuid>/…/transcript_full.jsonl + conversation_summaries.db | agy CLI | storage dir exists or agy --version works |
| Codex | ~/.codex/sessions/YYYY/MM/DD/rollout-*-<uuid>.jsonl (+ archived) | codex CLI | ~/.codex exists or codex --version works |
| GitHub Copilot | ~/.copilot/session-store.db + session-state/ | copilot CLI | store/home exists or copilot --version works |
| Grok Build | ~/.grok/sessions/<urlencoded-cwd>/<session-id>/ | grok CLI | sessions dir exists or grok --version works |
| Muse Code | ~/.local/share/muse/sessions/<id>/session.jsonl (+ session-index.db) | muse CLI | sessions dir exists or muse --version works |
Storage roots honor CLAUDE_CONFIG_DIR, OPENCODE_DATA_DIR, CURSOR_CONFIG_DIR, ANTIGRAVITY_HOME (or JETSKI_APP_DATA_DIR), CODEX_HOME, COPILOT_HOME, GROK_HOME, MUSE_HOME, and MUSE_DATA_DIR.
Parsers skip records they do not recognize instead of throwing. Every format here is undocumented and moves. A new message kind upstream must not break your session list.
Tested against
Section titled “Tested against”These adapters are not stable contracts with Claude Code, opencode, Cursor, Antigravity, Codex, Copilot, Grok Build, or Muse Code. Storage layouts and CLI flags churn without notice. threadle is developed and smoke-tested against roughly:
| Provider | Recently exercised |
|---|---|
| Claude Code | claude 2.1.x (JSONL under ~/.claude/projects) |
| opencode | 1.18.x (SQLite opencode.db + CLI) |
| Cursor Agent | agent 2026.09.x (transcripts under ~/.cursor/projects) |
| Antigravity | agy 1.2.x (brain JSONL + conversation_summaries.db) |
| Codex | rollout JSONL under ~/.codex/sessions (wrapped session_meta / response_item) |
| GitHub Copilot | copilot session-store SQLite + session-state |
| Grok Build | grok 1.0.x (summary.json + chat_history.jsonl / updates.jsonl) |
| Muse Code | muse MSP session.jsonl under ~/.local/share/muse/sessions (+ session-index.db) |
threadle services / threadle providers print the versions on your machine. If a provider updates and sessions look wrong or empty, assume a format change first. See Troubleshooting.
Keeping adapters fresh
Section titled “Keeping adapters fresh”Formats and CLI flags are undocumented and churn. threadle does not scrape upstream APIs daily. Instead:
- Parsers skip unknown record types (never throw).
- Golden fixtures in the repo CI catch known-type regressions.
threadle check --providersprobes inject-critical strings in CLI help when the binary is installed (missing binaries are skipped). Probes may use subcommand help (e.g.opencode run --help) and accept help aliases such as--append-system-prompt[-file].- When Cursor / Claude Code / Codex / opencode /
agy/ Copilot / Grok Build / Muse Code ship a breaking change: re-run that check, then update the adapter / fixtures. Checklist in the threadle CONTRIBUTING.md.
A !! flag: row means help text no longer mentions a string threadle uses for inject. Confirm on the real CLI before changing adapters. Details: CLI → Check · app repo provider-freshness.md.
Claude Code
Section titled “Claude Code”Storage. One JSONL file per session under ~/.claude/projects/<slug>/, where the slug is the project directory with / replaced by -. threadle also reads ~/.claude/history.jsonl (prompt index) and ~/.claude/sessions/*.json (live-status registry. Each entry is confirmed with a kill(pid, 0) liveness probe, and stale files are ignored).
Parsing. Transcripts are streamed line by line. A partial or corrupt line is tolerated, because the file may be mid-append. A switch over the record type handles user, assistant, system (turn durations), ai-title, last-prompt, bridge-session, permission-mode, and file-history snapshots. Anything else falls through a default: break.
The on-disk order is not the conversation. Rewinds and compaction leave abandoned branches in the file, so the real thread is reconstructed by walking the parentUuid chain backwards from the last last-prompt leaf (with a cycle guard), falling back to file order when the leaf is missing. Blueprints and whole-session aggregation use a second path that keeps every branch.
Subagent turns (isSidechain) and meta records are excluded from the parent thread. Subagent runs live in their own files at <project>/<sessionId>/subagents/agent-*.jsonl with optional .meta.json sidecars, and appear as nodes with composite ids like <sessionId>/agent-<hex>.
Agents. Markdown files with YAML frontmatter (name, description, model): <projectDir>/.claude/agents/*.md first, then ~/.claude/agents/*.md. Plus three builtins. general-purpose, Explore, Plan.
Runs. The claude binary, resolved from PATH, always spawned with CLAUDE_CODE_ENTRYPOINT=threadle-inject so the runs are identifiable in your own logs.
| Mode | Argv shape |
|---|---|
| new session | --session-id <uuid> --append-system-prompt-file <ctx.md> -p <kickoff> --output-format json |
| resume-fork | --resume <id> --fork-session -p <prompt> --output-format json |
| agent run | --resume <id> or --session-id <uuid>, then -p <prompt>, --model, --output-format json |
| streaming | as above with --output-format stream-json --verbose |
Agent selection maps to --append-system-prompt-file (a subagent’s markdown, written to ~/.config/threadle/tmp), --permission-mode plan for Plan, or a read-only append-system-prompt for Explore. continue injection is not supported here. Fork a session instead.
Quirks. Claude’s own registry writes idle | running | waiting. threadle maps a live-pid idle → live (open terminal, not generating). running and waiting pass through. The user-message count excludes array-content messages, so tool results do not inflate it. Per-model usage tracks the 1-hour cache-write share separately, because it bills at roughly twice the input rate rather than the 5-minute rate.
opencode
Section titled “opencode”Storage. A single SQLite database at ~/.local/share/opencode/opencode.db, opened readOnly: true. Sessions are rows. Messages and parts are rows with a JSON data blob. Discovery is a SELECT, not a directory walk. Top-level sessions are WHERE parent_id IS NULL, subagent runs are the children.
Schema churn. opencode’s schema moves, so threadle gates on it. At open time it reads pragma_table_info('session') and requires id, title, directory, parent_id, agent, time_updated. If any column is missing. Or the database cannot be opened at all. It logs once and falls back to the CLI for every query:
opencode db --format json "<sql>"The direct connection is also retried up to three times on BUSY/LOCKED before dropping to the CLI. The fallback cannot bind parameters, so values are inlined with escaping. The SQL is always built internally, never from user input.
Parsing. Messages join their parts, ordered by creation time. Part types map as: text → text, reasoning → thinking, tool → a tool call plus its result, patch → a file-change marker. step-start/step-finish/compaction parts are consumed for token and cost accounting rather than rendered. Large tool outputs are spilled by opencode to tool-output/<id> files, which threadle dereferences and truncates at 64 000 characters.
Agents. Read from the managed server’s GET /agent when it is up. If that fails or is empty, opencode agent list is parsed instead.
Runs. opencode run --format json, with -s <sessionId> to continue, --agent, and -m <model>. New session ids are found by a bounded recursive scan of the JSON events for any value starting with ses_.
The managed server. For synthetic injection: writing context into a session without triggering a reply. threadle spawns opencode serve --port 0, scrapes the ephemeral URL from its output, and authenticates over HTTP Basic with a random per-process password passed via OPENCODE_SERVER_PASSWORD. The insert is POST /session/<id>/message with noReply: true and a synthetic text part. The child is single-flighted, killed on failure, killed on shutdown, and put on a 30-second cooldown after a failed start.
Quirks. Live status is a heuristic: any session row touched in the last 45 seconds is reported as running. opencode is the only provider whose reported cost is real money: it bills your own API keys, so it records what it actually spent.
Cursor
Section titled “Cursor”Storage. Transcripts at ~/.cursor/projects/<slug>/agent-transcripts/<uuid>/<uuid>.jsonl: chat metadata (title, cwd, timestamps) at ~/.cursor/chats/<hash>/<uuid>/meta.json. The metadata cwd wins over the slug when resolving the project directory. The model is not readable. Cursor keeps it in an encrypted store threadle does not touch.
Parsing. The transcript is read whole (these files are small) and parsed line by line, skipping bad lines. Records without a role are control frames like turn_ended and are skipped. Blocks of unknown type are skipped. Message ids are synthetic (<file>.jsonl:<index>) because Cursor writes none.
Subagents. Cursor stores subagent runs as sibling transcripts with no parent link on disk. threadle reconstructs the tree heuristically: it extracts Task tool calls from parent transcripts, normalizes both the spawn prompt and each candidate child’s first user message (stripping <timestamp> and <user_query> wrappers, collapsing whitespace), and matches them within a project. Matched children are tagged linkedVia: "task-prompt": a best-effort link, not a guarantee.
Agents. Three builtins, no filesystem scan: agent, plan, ask. The latter two run as agent --mode plan / --mode ask.
Runs. The binary is called agent, not cursor-agent. GUI-launched processes often lack ~/.local/bin on PATH, so threadle probes CURSOR_AGENT_PATH, then ~/.local/bin/agent, ~/.cursor/bin/agent, /usr/local/bin/agent, /opt/homebrew/bin/agent, before falling back to PATH.
agent -p --trust --workspace <projectDir> [--resume <id>] [--model <m>] <prompt>with --output-format json or stream-json appended. New sessions pin an id up front via agent create-chat and then --resume it, so the session is identifiable even if stdout parsing fails. Live status merges two signals: UUIDs from agent persist list (when tmux persist is available), and any agent transcript whose mtime is within the last ~90 seconds. So IDE Composer chats that never appear in persist still show as running.
Usage. Cursor’s public JSONL carries no list-price usage, so token counts come from three tiers in order: the CLI’s own usage object captured after each run (cached in ~/.config/threadle/cursor-usage.json), turn_ended records in the transcript, and finally a character-count estimate. Which tier produced a number is recorded, so the UI can say so.
Quirks. Subscription tier comes from agent about --format json (subscriptionTier, e.g. Pro). Not from transcript storage. Email from that command is discarded.
Antigravity
Section titled “Antigravity”Storage. Conversations live under ~/.gemini/antigravity-cli/ (override with ANTIGRAVITY_HOME or JETSKI_APP_DATA_DIR). Public transcripts are at brain/<conversation-id>/.system_generated/logs/transcript_full.jsonl (falling back to transcript.jsonl). Titles, workspace URIs, status, and parent links come from the read-only SQLite file conversation_summaries.db. Workspace can also be recovered from history.jsonl or cache/last_conversations.json when the summary row has no workspace_uris.
Parsing. Each JSONL line is a step with a type. Known types map as USER_INPUT → user and PLANNER_RESPONSE / AGENT_RESPONSE → assistant. Unknown step types are skipped. User text is stripped of <USER_REQUEST> / metadata wrappers for display. Message ids are synthetic (<file>.jsonl:<step_index>).
Subagents. When parent_conversation_id is set on a summary row, the child is tagged kind: subagent-run and hidden from the top-level session list (same pattern as other providers).
Agents. Builtins agent (default), plan (agy --mode plan), and accept-edits (agy --mode accept-edits). Custom agents under {workspace}/.agents/agents/<name>/ and ~/.agents/agents/ are listed when present.
Runs. The binary is agy. GUI-launched processes often lack ~/.local/bin on PATH, so threadle probes ANTIGRAVITY_CLI_PATH / AGY_PATH, then ~/.local/bin/agy, ~/.gemini/bin/agy, /usr/local/bin/agy, /opt/homebrew/bin/agy, before falling back to PATH.
agy -p --dangerously-skip-permissions [--conversation <id>] [--model <m>] [--mode plan|accept-edits] [--agent <name>] <prompt>with --output-format json (or stream-json when a run log sink is attached). The JSON result carries conversation_id, response, status, and usage (input_tokens, output_tokens, thinking_tokens, cache_read_tokens).
Usage. Like Cursor, public transcripts carry no authoritative list-price usage. CLI usage from threadle-initiated runs is cached in ~/.config/threadle/antigravity-usage.json. Otherwise tokens are estimated from transcript character counts (chars ÷ 4). Estimated figures are prefixed ~ in the UI.
Quirks. Auth mode (Google OAuth consumer vs API/Vertex) is read from ~/.gemini/settings.json and the onboarding cache. There is no Gemini Advanced / Google One plan tier on disk. Touched-file discovery is best-effort: public JSONL is mostly user/planner text, so threadle harvests file://… links from assistant content. Opaque conversation .db blobs are not parsed.
Storage. Rollout JSONL under ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl (override home with CODEX_HOME). Archived rollouts under archived_sessions/ are also scanned. Session id is taken from session_meta.payload.id when present, otherwise from the filename UUID.
Parsing. Prefer wrapped records (type: "response_item") with a payload. Message / reasoning / function_call / function_call_output map to normalized parts. Lines starting with <environment_context> are skipped as boilerplate. Unknown top-level types (and unknown payload types) are ignored. Legacy unwrapped message / function_call lines are still accepted. Message ids are synthetic (<file>:<index>).
Agents. One builtin: codex (the default full coding agent).
Runs. The binary is codex. GUI-launched processes often lack ~/.local/bin on PATH, so threadle probes CODEX_PATH, then ~/.local/bin/codex, ~/.codex/bin/codex, /usr/local/bin/codex, /opt/homebrew/bin/codex, before falling back to PATH.
codex exec --json --sandbox workspace-write --ask-for-approval never [--model <m>] [resume <id>] <prompt>continue inject uses resume <sessionId>. new-session omits it. Session ids are scraped from JSON event fields (session_id, thread_id, …) when present.
Quirks. Live status is not exposed yet (all sessions show as unknown/idle). Token counts are estimated from rollout transcript chars÷4 (~ in Statistics). Billing mode and ChatGPT plan (Plus/Pro/…) are read from ~/.codex/auth.json (local JWT claims only. Tokens never leave the process). Touched files come from function_call tool names (read_file, write_file, apply_patch, …) and absolute paths in their arguments.
GitHub Copilot
Section titled “GitHub Copilot”Storage. SQLite session index at ~/.copilot/session-store.db plus per-session files under session-state/ (override home with COPILOT_HOME). The database is opened readOnly: true.
Parsing. Session rows supply id, cwd, repository, branch, summary, and timestamps. Turns and touched files come from related tables. Token columns from assistant_usage_events when present. Unknown tables / columns are ignored.
Agents. One builtin: copilot (the default full coding agent).
Runs. The binary is copilot. GUI-launched processes often lack ~/.local/bin on PATH, so threadle probes COPILOT_PATH, then ~/.copilot/bin/copilot, ~/.local/bin/copilot, before falling back to PATH.
copilot -p <prompt> -s --allow-all-tools --allow-all-paths [--model <m>] [--agent <name>] [--resume <id> | --session-id <uuid>]continue inject uses --resume. new-session pins a fresh --session-id. Headless runs also set COPILOT_ALLOW_ALL=true.
Quirks. Availability is storage or CLI (same pattern as Cursor / Codex). Live status is derived from session-state when possible.
Grok Build
Section titled “Grok Build”Storage. Session directories under ~/.grok/sessions/<urlencoded-cwd>/<session-id>/ (override with GROK_HOME). Each directory has summary.json, chat_history.jsonl, updates.jsonl (ACP display stream), and usage.json.
Parsing. Discovery reads summary.json for title, model, timestamps, and message counts. Tokens and tracked cost come from usage.json (costUsdTicks / 1e10). Transcripts prefer chat_history.jsonl (user / reasoning / assistant). Harness wrappers like <user_info> are skipped. Unknown record types are ignored.
Agents. One builtin: grok.
Runs.
grok -p <prompt> --always-approve [--resume <id> | --session-id <uuid>] [-m <model>]continue inject uses --resume. new-session pins a fresh --session-id. Live status uses ~/.grok/active_sessions.json (pid still alive → running).
Quirks. Early beta. JSONL shapes churn. Actual spend is $0 under SuperGrok subscription. Tracked cost still surfaces from usage ticks.
Muse Code
Section titled “Muse Code”Storage. MSP sessions under ~/.local/share/muse/sessions/<id>/session.jsonl (override share with MUSE_DATA_DIR / XDG_DATA_HOME). Config/auth lives under ~/.muse (MUSE_HOME) and is never written by threadle. Discovery also reads the read-only SQLite index session-index.db when present. Live PIDs come from runtime/muse/sessions/.
Parsing. Each JSONL line may carry nested record_json children. Known payload types map user intents and assistant commits; unknown types are skipped. Timestamps arrive as microseconds and are normalized to ms. Subagent / child sessions link via parent ids in the index when available.
Agents. One builtin: muse. Skills under ~/.local/share/muse/skills/ (and plugins under plugins/) are listed read-only for the Skills / Plugins views.
Runs. The binary is muse. Probes MUSE_PATH, then common install locations, before PATH.
muse exec --json --prompt-file <ctx.txt> --approval-mode never [--model <m>] [--session-id <uuid>]continue inject and agent runs pass --session-id. new-session omits it and scrapes session_id from JSON events.
Models (soft catalog). There is no stable muse models command yet. threadle scrapes Spark ids from muse exec --help (then muse --help), seeds known fallbacks (muse-spark-1.3, …), and accepts MUSE_MODELS (comma/space-separated) for labs. Treat the picker list as best-effort until upstream ships a list API.
Usage. Token totals come from MSP model_completed.usage in session.jsonl (meta.tokenSource: "transcript"). Actual spend is $0 (subscription-shaped; no list-price table for Spark).
Quirks. Early Meta Muse / Spark surface. Never mutate ~/.muse or ~/.local/share/muse.
Billing honesty
Section titled “Billing honesty”Two numbers, never blended:
- Tracked cost. List price of the tokens used, from the bundled models.dev-shaped price table.
- Actual spend. What you were actually billed.
Actual spend is $0 on subscription plans (Claude Pro/Max, Cursor, Antigravity, Copilot, Grok Build / SuperGrok, Muse Code) because the tokens are already paid for. opencode reports its real key spend. Claude’s billing mode is a setting. Flip it to api and tracked cost becomes actual spend. Spend circuit breakers / ceilings use tracked session dollars where available. They are a measurement, not a forecast.
When a provider is absent
Section titled “When a provider is absent”Availability is checked per provider and nothing cascades. A missing provider simply contributes no sessions and no agents. The rest of threadle works normally, and agent-less workflows (prompts, custom nodes, merges, gates) do not need any provider at all.
Storage and CLI are checked separately. Claude Code and opencode report available purely on their storage existing, so you can browse history on a machine where the CLI is gone. Runs will then fail at spawn time with a clear error. Cursor, Antigravity, Codex, Copilot, Grok, and Muse accept either signal (storage or CLI). Version strings degrade to blank rather than failing, and a missing CLI during live-status polling yields an empty map, so sessions show as idle instead of erroring.
Related
Section titled “Related”- Agents & sessions. Wiring providers onto the canvas
- CLI.
threadle check/check --providers - CLI.
services/providers/agentsfrom the terminal - Trust model. The read-only guarantee in detail
- Install. Prerequisites per provider