Claude Transcripts docs GitHub
Work in progressUnder active development — not tested as ready for use. Breaking changes land without notice, stored data may need to be discarded between revisions, and there is no auth or security model.

webapi

packages/webapi/: Bun + Hono with @hono/zod-openapi, nano for CouchDB, Bun.S3Client for S3, and Scalar at /api/docs.

It is the I/O gateway (ADR 0016): consumers read through it and write only via /api/ingest/*, /api/migrate/* and /api/search/reindex. The hook writes to CouchDB and S3 directly, which is why the webapi follows CouchDB's _changes feed. In production it also serves the SPA, the docs and the CLI binary. URL list: routes.md.

Configuration

Settings from config/config.json, else the template (CT_CONFIG_DIR overrides the directory); secrets and endpoints from the environment (configuration.md).

VariablesDefault
COUCHDB_URL, or COUCHDB_HOST + COUCHDB_PORT; COUCHDB_USER, COUCHDB_PASSWORDhttp://127.0.0.1:7652
S3_ENDPOINT, S3_REGION, S3_ACCESS_KEY, S3_SECRET_KEYhttp://127.0.0.1:7653, garage
MEILI_HOST, MEILI_API_KEY (used only when features.meilisearch is on)http://127.0.0.1:7656
WEBAPI_HOST, WEBAPI_PORT127.0.0.1:7650
CT_STATIC_DIR, CT_DOCS_DIR, CT_CLI_BINunset; each enables /app, /docs, /cli/download
CT_VERSIONbaked into the image from the git tag; reported by /health

HTTP API

The typed routes (/api/sessions*, /api/turns, /api/search*, /api/ingest/*, /api/migrate/*) are defined with createRoute + zod, so the OpenAPI spec is generated from the code. Their parameters and response shapes are documented only there: browse /api/docs, or read /api/openapi.json (committed as openapi.json, regenerated by bun run gen:clients). Validation failures return 400 { error }.

Plain Hono routes, not in the spec: /health, /, /api/blueprint*, the /api/couch/*, /api/s3/{bucketKey}/* and /api/fossil/{repoKey}/json/* proxies (GET/HEAD only), /api/docs, /api/openapi.json, /app/*, /docs/*, /cli/download.

/api/fossil/{repoKey}/json/<command> reads the repository named by fossil.repositories.<repoKey> through Fossil's JSON API, e.g. /api/fossil/sessions/json/timeline/checkin?limit=20. GET alone doesn't make it read-only — the JSON API takes parameters from the query string, so /json/user/save?… writes — so only allowlisted commands pass (version, stat, whoami, cap, resultCodes, timeline, artifact, dir, finfo, diff, branch/list, tag/list|find, wiki/list|get|diff, report/list|get|run); anything else, a bare /json?command=… or a jsonp parameter is a 403. No client header is forwarded.

Health

GET /health always returns 200 while the process is up, so probes can use the status code for liveness. The body answers whether the stores are usable:

{ ok, status, version, startedAt, stores, sessionIndex }

Boot never waits for the stores, so the webapi comes up even when they are broken and /health can say what is wrong. doctor checks it before writing anything.

Session list

Rows come from the session_index/aggregate view, one per session_id. A session with a summary doc is ended; otherwise running if its last event is within system.sessions.liveWindowMs (24 h), else incomplete. Filters (cwd, model, hostname, source exact; from/to by overlap) apply before paging, and totalCount is the filtered count. Each row carries activeMs: the session's span minus gaps longer than idleThresholdMs, from session_index/event_times.

Transcripts

GET /api/sessions/{id}/transcript reads from the CouchDB chunk docs by default (chunks/entries_by_session, paged at the view), so a running or crashed session is readable before SessionEnd. It falls back to the S3 blob when that covers more bytes: sessions recorded without full-content chunks, or a missed final flush. The response reports source (chunks or s3) and byteCoverage. Both sources are normalised to the same pruned per-turn shape, so raw JSONL is available only through /api/s3/sessions/<id>/transcript.jsonl. A 502 means CouchDB had nothing and S3 failed; a 404 means no transcript is stored.

Session detail reads summary:<id> and, when token_usage is missing but a transcript exists, computes it with sumTranscriptTokens.

Two Meilisearch indexes named in meilisearch.indexes: sessions (metadata, one doc per session) and turns (one doc per user, assistant or tool-result turn; other transcript lines aren't indexed). They are derived from CouchDB and kept current three ways:

DELETE /api/ingest/{id} also removes the session's search entries.

Meilisearch accepts only a-zA-Z0-9, - and _ in document ids, so build them with searchDocId(). It validates batches asynchronously (202 Accepted, then failure in the task queue): if an index is unexpectedly empty, check GET /tasks on Meilisearch.

Session index

session_index/aggregate has a JavaScript reduce, which CouchDB can't serve from stored btree values when grouped per session, so querying it per request cost about 20 ms per session (8 s for ~460 sessions, #107). src/storage/session-index.ts runs the grouped query once at boot, then re-reads only the sessions each _changes batch touches; the route filters, sorts and pages in memory (5–70 ms).

The change follower therefore runs even with Meilisearch off.

Storage and boot

Serving the SPA and docs

With CT_STATIC_DIR set, the SPA is served under /app/ with an index.html fallback for client-side routes (Vite builds with base: "/app/"). In development the variable is unset and Vite serves the UI, proxying /api here.

src/caching.ts:

packages/shared

Cross-cutting types (TokenUsage, SessionStatus, SessionSummary, ...) and helpers. sumTranscriptTokens(jsonl) sums token usage from a transcript, deduplicating by message.id and keeping the largest usage block per id, so streamed duplicates aren't counted twice. The webapi and the hook import the same copy. The webui and CLI take their wire types from the generated clients.