opencode-capture
v0.1.0
Published
opencode plugin that captures session traces (reasoning, tool calls, text, files) as JSONL for the CMF recorder.
Maintainers
Readme
opencode-capture
An opencode plugin that captures the full turn-by-turn trace of a coding-agent session — reasoning ("thinking"), tool calls, text, files, including attempts that were started and then abandoned — while the session is running, and writes it as JSONL for the CMF recorder to pick up.
The plugin runs in-process inside opencode, so there is no separate
terminal, no opencode serve + opencode attach coordination, and no
SSE/HTTP round-trip. It captures sessions that an out-of-process capture tool
cannot reach (bare opencode TUI sessions, which start their own server on
a protected random port).
See DESIGN.md for the full design and rationale.
How it works
The plugin subscribes to opencode's event hook, which delivers the host
process's Event stream. Each event is handed to the OpencodeAdapter,
which translates it into the common trace-event schema and appends it to
<spoolDir>/<session_id>.jsonl. The CMF recorder (a separate Python process)
tails that file and records each turn into CMF with turn-to-turn lineage.
opencode (with this plugin) --JSONL--> spool/ --tail--> recorder/ --> CMF
(in-process) (Python, cmflib)Configuration
Two settings are required — the plugin throws at load if either is missing:
| Setting | Description |
|---|---|
| pipelineName | CMF label — the top-level grouping all captured turns get recorded under. Not tied to any project; use any name to query later (e.g. cmf execution list -p <name>). |
| spoolDir | Absolute path to the directory where <session_id>.jsonl files are written. Must match the recorder's spool_dir. |
Optional:
| Setting | Default | Description |
|---|---|---|
| sessions | (all) | Array of session IDs to capture; omit to capture every session. |
Settings are passed via the opencode.json plugin options tuple (see
Install). For local dev installs that don't receive config
options, environment variables are used as a fallback:
| Plugin option (opencode.json) | Environment variable (dev) |
|---|---|
| pipelineName | OPENCODE_CAPTURE_PIPELINE |
| spoolDir | OPENCODE_CAPTURE_SPOOL_DIR |
| sessions | (not available via env) |
Install
npm package (published)
opencode auto-installs npm plugins via Bun at startup. Add the plugin to the
project's opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": [["opencode-capture", {
"pipelineName": "autoresearch",
"spoolDir": "/path/to/spool"
}]]
}No environment variables needed — options come from the config tuple. Restart opencode and the plugin is active.
Local file plugin (dev)
For local development (editing the adapter, testing changes before
publishing), opencode auto-loads any .js or .ts file placed in its
plugins directory at startup. Build the plugin and copy the output there:
cd ~/opencode-capture
npm install
npm run build
cp dist/*.js ~/.config/opencode/plugins/ # global (all projects)
# or: cp dist/*.js .opencode/plugins/ # project-localLocal file plugins don't receive opencode.json options, so configuration is
via environment variables:
OPENCODE_CAPTURE_PIPELINE=autoresearch \
OPENCODE_CAPTURE_SPOOL_DIR=/path/to/spool \
opencodeRebuild after any code change:
cd ~/opencode-capture && npm run build && cp dist/*.js ~/.config/opencode/plugins/Then restart opencode.
Running
Two terminals:
Start the recorder (from the directory where
cmf initwas run):cd /path/to/working_directory source "/path/to/recorder/.venv/bin/activate" cmf-recorder --spool-dir /path/to/spoolRun opencode (bare TUI, no
serve/attachneeded):With the npm package installed via
opencode.json(options come from config):cd /path/to/working_directory opencodeOr, with the local dev plugin (configure via env vars):
cd /path/to/working_directory OPENCODE_CAPTURE_PIPELINE=autoresearch \ OPENCODE_CAPTURE_SPOOL_DIR=/path/to/spool \ opencodeSend any prompt. Within ~1 second a
<session_id>.jsonlfile appears in the spool dir.
Verify it's working
tail -f /path/to/spool/*.jsonlExpected output:
{"type":"session_start","session_id":"ses_...","pipeline_name":"autoresearch",...}
{"type":"turn_start","session_id":"ses_...","turn_id":"msg_...","turn_index":0,...}
{"type":"part","session_id":"ses_...","part_type":"text","status":"running",...}
{"type":"part","session_id":"ses_...","part_type":"text","status":"completed",...}
{"type":"turn_end","session_id":"ses_...","status":"completed",...}Then query CMF:
cmf pipeline list # should show the configured pipelineName
cmf execution list -p {pipelineName} # one row per turn
cmf artifact list -p {pipelineName} # reasoning.jsonl / tool-calls.jsonl / transcript.jsonl per turnDevelop
npm install
npm run typecheck
npm testtest/opencode-adapter.test.ts feeds a synthetic sequence of SDK Event
objects (user turn -> reasoning -> a tool call that errors out and is
abandoned -> a follow-up text part -> turn completion) through the adapter
and checks the resulting trace-event JSONL — this is what pins down that an
abandoned tool call keeps its status: "error" and the same seq across
updates, and that concurrent sessions and the sessions filter don't
cross-talk.
Publish to npm
The package has zero runtime dependencies (all SDK imports are type-only and
erase at compile time), so the published tarball contains only dist/*.js
and dist/*.d.ts.
Log in to npm (one-time):
npm loginBump the version (if needed):
npm version patch # or minor / majorBuild and publish —
prepublishOnlyrunstscautomatically before packing, so only the builtdist/is included (enforced by"files": ["dist"]inpackage.json):npm publishVerify it's installable:
npm view opencode-capture
To publish a new version, bump the version and re-run npm publish.
Architecture
The package is split into a thin plugin entry (src/plugin.ts, ~25 lines)
and the reusable adapter core (src/opencode-adapter.ts,
src/spool-writer.ts, src/schema.ts). Only the entry knows about the
opencode plugin API; the adapter is driven purely by Event objects and
writes the common trace-event schema, so it can be reused by any other
entrypoint (a CLI, a test harness, another host). See
DESIGN.md for the full rationale.
License
ISC
