-
Notifications
You must be signed in to change notification settings - Fork 0
Transcripts, Context Discovery & Context CLI
This layer gives agent CLIs a way to read each other's conversation history without going through the running Electron app. It has three cooperating concerns:
- Transcript reading — parse the JSONL session files that Claude Code writes.
- Local discovery state — small files in the project folder that record each node's transcript path, the explicit links between nodes, and the prompt markers that tell an agent the CLI exists.
-
A standalone CLI —
termsprawl-context, which resolves the links, loads the peer transcripts, and prints a bounded dump to stdout.
All of it is Electron-free, fail-open, and safe to run from a one-shot node process inside the project directory.
| File | Responsibility |
|---|---|
src/core/transcript.ts |
Reads session_name and printable user/assistant turns out of a Claude Code JSONL transcript; formats the linked-context dump. |
src/core/transcript-index.ts |
Persists the nodeId → transcript path mapping under <cwd>/.termsprawl/transcripts/. |
src/core/context-discovery.ts |
Plants and refreshes the project-local discovery markers (managed block in .termsprawl/AGENTS.md, skill file under .claude/skills/termsprawl-context/). |
src/core/context-links.ts |
Stores node-to-node context links as one JSON file per canonical pair; list/add/remove/peers. |
src/core/context-cli.ts |
The single CLI implementation: argv parsing, ContextCliIO abstraction, the real fs-backed IO factory, run loop. |
src/core/context-cli-entry.ts |
Thin binding of process.argv → core logic → exit code. Never imported by the app. |
scripts/build-context-cli.mjs |
Bundles the core into the committed, dependency-free scripts/termsprawl-context.mjs. |
scripts/termsprawl-context.mjs |
The shipped artifact; a self-contained plain-node ES module with all helpers inlined. |
Everything lives under the project cwd; no module in this layer writes to the user's home directory or a root AGENTS.md.
<cwd>/.termsprawl/
AGENTS.md # managed block between <!-- termsprawl:context-links --> markers
project.json # read-only here: nodes[].data.title supplies display names
transcripts/<nodeId>.json # { "version": 1, "path": "/abs/path/to/<session>.jsonl" }
links/<lo>--<hi>.json # { "version": 1, "a": "<lo>", "b": "<hi>" }
bin/termsprawl-context # the command the discovery markers point at
<cwd>/.claude/skills/termsprawl-context/SKILL.md
Link file names are canonical: a < b lexicographically, so linkFilePath(cwd, a, b) and linkFilePath(cwd, b, a) resolve to the same path and addLink is idempotent. The transcript index is keyed by node id, and recordTranscriptPath is a documented no-op for unsafe ids, an empty path, or any I/O failure.
The transcript-index header comment states that the app learns each node's transcript path from hook events, while the standalone CLI has no event history — the persisted index is the bridge between the two. That write side is not in this layer's files, but it is the contract this module assumes.
flowchart TD
subgraph Agent["Agent process (cwd = project root)"]
A["Claude session"] -->|"reads at session start"| AG[".termsprawl/AGENTS.md managed block"]
A -->|"skill discovery"| SK[".claude/skills/termsprawl-context/SKILL.md"]
A -->|"TERMSPRAWL_NODE_ID injected at spawn"| CLI["termsprawl-context --cwd . --self $TERMSPRAWL_NODE_ID"]
end
CLI --> ENTRY["context-cli-entry.ts"]
ENTRY --> PARSE["parseContextArgs"]
PARSE -->|"error"| EXIT2["stderr + exit 2"]
PARSE -->|"cwd, self"| RUN["runContextCli"]
RUN --> PEERS["io.peers(self) → peersOf → listLinks(.termsprawl/links)"]
RUN --> PATH["io.transcriptPath(peer) → readTranscriptPath(.termsprawl/transcripts)"]
RUN --> TURNS["io.turns(path) → readTranscriptTurns(session JSONL)"]
RUN --> TITLE["io.nodeTitle(peer) → .termsprawl/project.json"]
RUN --> FMT["formatLinkedContext"]
FMT --> OUT["stdout blocks joined by '\\n---\\n', exit 0"]
Key nodes. parseContextArgs accepts only --cwd <dir> and --self <nodeId>; anything else is an error answered with process.exit(2) and a termsprawl-context: prefixed message on stderr. runContextCli never fails: it returns 0 even when there are no peers, no transcript paths, or no printable turns, printing nothing in those cases. createRealContextIO(cwd) binds the pure run loop to the filesystem, reading nodeTitle out of .termsprawl/project.json and falling back to the raw node id when the title is absent or unparseable.
readTranscriptTurns(path) streams the file as text, splits on \n, and parses each non-blank line independently. Malformed lines are skipped (fail-open), and only entries whose type is exactly user or assistant are considered.
For each such entry, contentToText accepts message.content as either a plain string or an array of typed blocks; only blocks with type === 'text' survive, joined by newlines. tool_use, tool_result, and thinking blocks are dropped entirely, and a message with no printable text is skipped. Each surviving turn's text is truncated to LINKED_TURN_MAX_CHARS (2000), and the result keeps only the last MAX_LINKED_TURNS (40) turns. The caps are exported so callers and tests share the same constants.
readSessionNameFromTranscript(path) implements the same reader shape but only tracks the latest non-empty string session_name. It is the input to node-title mirroring — the comment is explicit that node titles come from the transcript rather than OSC title escapes. A missing file, unreadable file, junk lines, or absent names all yield null, so the caller keeps the existing title.
formatLinkedContext({ peerId, peerTitle, turns }) produces exactly the stdout shape the CLI emits:
# linked context from <peerTitle> (<peerId>)
## user
<text>
## assistant
<text>
context-links.ts is a pure fs helper layer with three explicit error codes: SELF, BAD_ID, and IO. addLink rejects unsafe or identical ids before touching the filesystem and returns { ok: true } only after the write succeeds; removeLink is a force: true delete that tolerates a missing file; listLinks skips directory entries that are not .json files and silently drops files that fail parseLinkFile. A parsed link must have version === 1, two distinct ids that both pass isSafeProjectId, or it is discarded.
peersOf(cwd, id) filters every valid link down to the opposite endpoint, in no particular order. The CLI therefore resolves peers in one pass and tolerates stale or corrupt link files by ignoring them rather than aborting.
Note the shared safety predicate: isSafeProjectId matches /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/ and is imported from workspace-files.ts, so the same id discipline that guards the workspace store also guards transcript-index filenames, link filenames, and the CLI arguments that reach them.
ensureContextDiscovery(cwd) writes two markers so a Claude session whose cwd is the project can find the CLI:
-
.termsprawl/AGENTS.md— a managed block delimited by<!-- termsprawl:context-links -->and<!-- /termsprawl:context-links -->, instructing the agent to run.termsprawl/bin/termsprawl-context --cwd . --self "$TERMSPRAWL_NODE_ID"and to read only what that command prints. -
.claude/skills/termsprawl-context/SKILL.md— a skill header plus the same instruction, rewritten only when the body differs.
upsertManagedAgentsBlock is idempotent: it finds the markers, keeps user prose above and below them, and re-emits before\n\n<block>\n\n<after> (trimmed, terminated by a newline). Calling it twice with no surrounding change returns the same string, so ensureContextDiscovery can skip the write. Both writes are wrapped in try/catch and are best-effort only.
The TERMSPRAWL_NODE_ID environment variable referenced by the markers is injected at spawn (documented as main/withAgentNodeIdEnv); this layer only consumes it as a shell expansion inside the marker text.
scripts/build-context-cli.mjs runs a Vite programmatic SSR library build with entry src/core/context-cli-entry.ts, format es, target node22, rollupOptions.external: [] so everything except node: builtins is inlined, then renames the emitted context-cli-entry.js to scripts/termsprawl-context.mjs. The script is invoked as pnpm run build:cli, and its output is committed so tests and packaged copies have a stable artifact. The same script additionally builds src/core/agent-tool-entry.ts into out/tools/agent-tool-entry.mjs — a separate concern from this page.
Because the shipped artifact and the TS source are the same implementation, the source of truth for CLI behavior is src/core/context-cli.ts plus the three core helpers, and scripts/termsprawl-context.mjs is a generated, readable mirror.
| Condition | Behavior |
|---|---|
Unknown argv, missing --cwd/--self value, or missing either flag |
{ error } from parseContextArgs; entry prints to stderr and exits 2. |
No peers for self
|
runContextCli returns 0 and prints nothing. |
| Peer has no indexed transcript path, or the path is unreadable | Peer is skipped; remaining peers still print. |
| Peer transcript has no printable turns | Peer is skipped. |
project.json missing or title not a string |
nodeTitle returns null; the CLI falls back to the node id. |
| Link file wrong version, junk JSON, same-id pair, unsafe id |
parseLinkFile returns null; listLinks drops it. |
addLink with identical ids |
{ error: { code: 'SELF' } }. |
addLink write failure |
{ error: { code: 'IO', message } }; no throw. |
Transcript-index file wrong version or bad path
|
readTranscriptPath returns null (treated as absent). |
| Discovery marker write fails | Swallowed; discovery is best-effort and never runs outside cwd. |
| Any transcript read error |
readSessionNameFromTranscript → null; readTranscriptTurns → []. |
-
New transcript format. Extend
contentToText/readTranscriptTurns; the caps and theTranscriptTurnshape are the stable contract the CLI and formatter depend on. -
New discovery surface. Add another marker in
ensureContextDiscovery, reusing the managed-block pattern for idempotency. -
New CLI IO behavior. Implement
ContextCliIOand inject it intorunContextCli;createRealContextIOis just the disk-backed default, which is why tests can run the CLI fully in-process. -
Layout changes.
INDEX_VERSIONandLINK_FILE_VERSIONare the migration seams: readers reject mismatched versions and degrade to "no data" instead of throwing, so a bump is safe for old files. -
Different peer semantics.
peersOfis the single resolution point; anything richer (directed links, link metadata) would changeLinkPairandparseLinkFilefirst.
Sources:
Generated from termsprawl at 0d4393be54c6200beedd91bb636e5296c30472c5.
App Shell & Platform Foundations
- Electron Main Process & Window Lifecycle
- Preload Bridge & IPC Contract
- Shared Domain Types and File/URL Helpers
- Renderer Bootstrap & App Composition
- Build Targets & TypeScript Configuration
Canvas, Nodes & Renderer State
- Infinite Canvas Surface & Viewport Interaction
- Workspace, Project & Tab State
- Node Links, Edges & Link Inspector
- Sticky, Group, Editor & Diff Nodes
- Keyboard Canvas Navigation & Cross-Panel Requests
- Theme, Accent & Visual Language
- Boot Overlay, Onboarding & Shared UI Kit
Terminals & Session Continuity
- PTY Lifecycle & Terminal Sessions
- tmux Session Naming & Reattach
- Scrollback Snapshots & Cold Replay
- Terminal Node Rendering (xterm.js)
- SSH Remote Projects, Terminals & Files
Persistence, Projects & Files
- Workspace Store & Project File Layout
- Project Scope, Deletion & Worktree Registry
- Workspace Bundle Export/Import
- File Service & File Tree UI
Agent Runtime & Tooling
- Agent Status Model & Hook Normalization
- Hook Server & CLI Hook Installers
- Agent Launch, CLI Probing & Managed Accounts
- Agent Tool Protocol & In-Process Server
- Agent Tool Client, CLI & MCP Entry
- Transcripts, Context Discovery & Context CLI
- Agent Canvas State & Status Badges
Chat Nodes & Model Providers
- Chat Runtime, Conversation & Cost
- Model Provider Adapters & Streaming
- Chat Tool Calling & Project Tools
- Chat Node UI
Git & Source Control
Embedded Browser Nodes
- Browser Manager & Guest Runtime
- CDP Facade & Browser Agent Server
- Browser Navigation Policy & Node UI
Server Edition
- Server Bootstrap & HTTP/WebSocket Entry
- RPC Dispatch, Handlers & Service Bridges
- Renderer Shim & Server Boundary
- Server Auth & Security Boundary
Relay & Remote Access
- Relay Hub & WebSocket Frame Routing
- Relay End-to-End Cryptography
- Relay Auth, Invites, Store & Admin API
- Relay Client, Pairing & Terminal Tunneling
- Relay Trust UI
Integrations & Secondary Surfaces
- Telegram Bot, Commands & Pairing
- A2A Peers: Protocol, Client & Server
- Node Link Engine, Registry & Scheduler
- Cloud Spaces, Snapshots & Sync
Settings, Updates & Maintenance