backthread 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -403
- package/dist-bundle/backthread.js +8 -8
- package/hooks/hooks.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,431 +1,93 @@
|
|
|
1
|
-
# backthread
|
|
1
|
+
# backthread
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
the `SessionEnd`/`Stop` capture hook (F7.2), the `/backthread capture` slash command
|
|
6
|
-
(F7.3), and the MCP server (F7.4) all call into it.
|
|
3
|
+
[](https://www.npmjs.com/package/backthread)
|
|
4
|
+
[](./LICENSE)
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
does **not** modify the root `package.json` and has no app dependencies. Its only
|
|
10
|
-
runtime dependency is the official MCP SDK (`@modelcontextprotocol/sdk`), pulled
|
|
11
|
-
in by the F7.4 `backthread mcp` server; everything else uses Node builtins, so `npx backthread` stays a small, audit-light download.
|
|
6
|
+
**Keep the thread on what your AI agent actually shipped.**
|
|
12
7
|
|
|
13
|
-
|
|
8
|
+
When you hand code to AI agents (Claude Code, Codex, Cursor), you stop reading
|
|
9
|
+
every change — and a few weeks later you own a codebase you never internalized.
|
|
10
|
+
Debugging slows down, refactors get scary.
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
authorizes the device, registers the SessionEnd hook, and backfills history.
|
|
12
|
+
Backthread captures the **why** behind each change straight from your agent
|
|
13
|
+
sessions, so you can ask *"how does X work?"* and stay oriented without
|
|
14
|
+
spelunking through PRs. The decisions become a live **"How it works"** diagram
|
|
15
|
+
and changelog at [backthread.dev](https://backthread.dev).
|
|
20
16
|
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
backthread start First-run setup: trust copy + one-tap auth + your next step (backs /backthread:start)
|
|
25
|
-
backthread login Authorize this device (opens your browser)
|
|
26
|
-
backthread login --device Headless / SSH login (device-code flow — STUBBED, see below)
|
|
27
|
-
backthread whoami Show the current device's config (token is never printed)
|
|
28
|
-
backthread capture Capture this session's decisions (run by the SessionEnd/Stop hook)
|
|
29
|
-
backthread capture --manual Manually capture a session now (backs the /backthread:capture slash command)
|
|
30
|
-
backthread mcp Start the MCP server (capture + query tools) over stdio
|
|
31
|
-
backthread install Set up capture for this repo: login + register hook + backfill history
|
|
32
|
-
backthread help Usage
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## `backthread login` — the loopback flow
|
|
36
|
-
|
|
37
|
-
1. Starts a localhost server on `http://127.0.0.1:<random-port>/callback`.
|
|
38
|
-
2. Opens the browser to `app.backthread.dev/cli-auth?port=<port>&state=<nonce>`.
|
|
39
|
-
3. You click **Authorize** (you're already signed into the web app). The page
|
|
40
|
-
mints a capture-scoped `backthread_pat_…` device token via the `mint-device-token`
|
|
41
|
-
Edge Function (F7.0 / ARP-448) and redirects back to the loopback with the
|
|
42
|
-
token + the same `state` nonce.
|
|
43
|
-
4. The CLI validates the `state` nonce (CSRF guard) and writes the token to
|
|
44
|
-
`~/.backthread/config.json` at **chmod `0600`**.
|
|
45
|
-
|
|
46
|
-
One browser click, zero copy-paste. **The token never touches the clipboard or
|
|
47
|
-
terminal scrollback** — it goes straight from the loopback query string into the
|
|
48
|
-
0600 config file and is never printed or logged.
|
|
49
|
-
|
|
50
|
-
## Inference router — `infer.ts` (F7.IR / ARP-450)
|
|
51
|
-
|
|
52
|
-
`inferDecisions(transcript, config, opts)` decides **how** decisions get derived
|
|
53
|
-
from a normalized + redacted transcript and **whose** credentials pay, then
|
|
54
|
-
returns the derived decisions for the caller (F7.2 hook / F7.4 MCP) to POST to
|
|
55
|
-
`ingest-decisions` — **unless** the result says the server already persisted them.
|
|
56
|
-
|
|
57
|
-
- **Model 2 (server-side, our keys) — the DEFAULT.** POSTs the redacted
|
|
58
|
-
transcript to the Worker's F7.SF `POST /infer-decisions` (auth: the F7.0
|
|
59
|
-
`backthread_pat_` device token), which runs the tuned Gemini-bulk → Sonnet-tiebreak
|
|
60
|
-
pipeline with our keys. Pass `persist: true` + `repo` to have the server also
|
|
61
|
-
write the decisions (membership-gated) — then `result.persisted` is `true` and
|
|
62
|
-
the caller MUST NOT re-POST to `ingest-decisions` (double-write). Default is
|
|
63
|
-
derive-only: the caller persists.
|
|
64
|
-
- **Model 3 (BYO API key) — a power-user override, SEAM ONLY.** `localByokInfer`
|
|
65
|
-
is a stub marked `// TODO(F7.19 / ARP-463)` that always reports "no BYOK
|
|
66
|
-
configured", so the router falls through to Model 2. Local BYOK execution +
|
|
67
|
-
key storage/validation/settings UX ships in F7.18/19 (ARP-462/463).
|
|
68
|
-
- **Model 4 — parked, not modelled here** (post-MVP, opt-in; ToS-blocked variants
|
|
69
|
-
are off the table per the F7.IV spike, ARP-449).
|
|
70
|
-
|
|
71
|
-
**Trust boundary (be honest — Model 2 is the weaker claim).** Because the
|
|
72
|
-
default path runs inference on our servers, the thing that leaves your machine
|
|
73
|
-
on Model 2 is **the redacted transcript** — natural-language prose only. The
|
|
74
|
-
plugin redacts LOCALLY first (drops every tool-use / tool-result record and
|
|
75
|
-
redacts fenced code to `[code redacted]`), so **no source code and no tool I/O
|
|
76
|
-
ever leave the machine**; the Worker re-runs the fenced-code scrub server-side
|
|
77
|
-
as a fail-closed backstop against a plugin redaction bug, derives the decisions,
|
|
78
|
-
and **discards the transcript right after extraction — processed in memory,
|
|
79
|
-
never stored.** That is a *weaker* claim than the BYOK/Model-3 path, where
|
|
80
|
-
nothing but the derived decisions ever leaves the machine — and we say so out
|
|
81
|
-
loud rather than paper over it. (Model 3 is still a SEAM only; until F7.19/20
|
|
82
|
-
ship, every run takes the Model-2 server path.) This is distinct from the
|
|
83
|
-
hosted **repo-ingestion** pipeline (the diagram), which clones your code into a
|
|
84
|
-
destroy-on-exit sandbox and never stores source either — two separate paths,
|
|
85
|
-
same never-store-source posture for actual code. The canonical statement
|
|
86
|
-
lives at [`/security`](https://backthread.dev/security).
|
|
87
|
-
|
|
88
|
-
Dependency-free (global `fetch`); a `fetchImpl` seam lets tests run without a
|
|
89
|
-
network or Worker. Worker origin is overridable via `BACKTHREAD_WORKER_URL`
|
|
90
|
-
(e.g. `http://localhost:8787` for `wrangler dev`).
|
|
91
|
-
|
|
92
|
-
## `backthread capture` — the SessionEnd/Stop hook (F7.2 / ARP-433)
|
|
93
|
-
|
|
94
|
-
The self-maintaining moat: at the end of every agent session, derive the
|
|
95
|
-
session's **decisions** (the "why") and land them in the hosted log, so it stays
|
|
96
|
-
current after the one-time F5 backfill. `backthread capture` is the headless hook
|
|
97
|
-
handler. Pipeline (all LOCAL until the last network hop):
|
|
98
|
-
|
|
99
|
-
1. Read the hook input (JSON on **stdin**; `transcript_path` + `cwd` +
|
|
100
|
-
`session_id`). A `BACKTHREAD_HOOK_INPUT` env var is accepted as a dev/test fallback.
|
|
101
|
-
2. Read the `.jsonl` transcript off disk.
|
|
102
|
-
3. **Redact LOCALLY** (`redact.ts` — the security fence): drop every
|
|
103
|
-
tool-use / tool-result record, keep only natural-language prose, redact fenced
|
|
104
|
-
code blocks to `[code redacted]`. **No source code or tool I/O ever leaves the
|
|
105
|
-
machine.**
|
|
106
|
-
4. Derive decisions via the **F7.IR router** (`inferDecisions`). When a repo
|
|
107
|
-
resolves from `cwd`, we pass `persist: true` so the server (membership-gated)
|
|
108
|
-
also writes them — then `result.persisted` is `true`.
|
|
109
|
-
5. Persist:
|
|
110
|
-
- `result.persisted` → **done** (re-POSTing would double-write).
|
|
111
|
-
- else POST the **derived** decisions to `ingest-decisions`, which routes
|
|
112
|
-
connected vs **repo-less** (F7.8) server-side.
|
|
113
|
-
|
|
114
|
-
**Best-effort + non-blocking (the load-bearing contract):** `runCapture` never
|
|
115
|
-
throws — every failure mode resolves a structured outcome — and `backthread capture`
|
|
116
|
-
**always exits 0**. A capture hiccup can never disrupt or delay the user's Claude
|
|
117
|
-
Code session. If there's no device token, it kicks off `backthread login`
|
|
118
|
-
fire-and-forget (never awaited) and **skips this capture** — the next session
|
|
119
|
-
captures once a token exists.
|
|
120
|
-
|
|
121
|
-
### Registering the hook — TWO mechanisms (F7.7 / ARP-438)
|
|
122
|
-
|
|
123
|
-
Claude Code runs `SessionEnd` hooks, passing the session JSON on the hook
|
|
124
|
-
process's **stdin**. `npx backthread capture` reads that payload, derives the session's
|
|
125
|
-
decisions (locally-redacted), and persists them best-effort — always exiting 0, so
|
|
126
|
-
the hook host never blocks on it and never sees a non-zero exit. There are two ways
|
|
127
|
-
the hook gets registered, and Backthread uses both depending on how it was installed:
|
|
128
|
-
|
|
129
|
-
1. **PRIMARY — the plugin manifest.** When Backthread is installed as a Claude Code
|
|
130
|
-
plugin, the hook is declared in **`hooks/hooks.json`** (referenced by the
|
|
131
|
-
`hooks` field of `.claude-plugin/plugin.json`) and Claude Code registers it
|
|
132
|
-
**automatically on install** — the user's `.claude/settings.json` is never
|
|
133
|
-
touched. This is the right home: the hook ships + versions with the plugin and
|
|
134
|
-
is removed cleanly on uninstall. (Schema confirmed against the current Claude
|
|
135
|
-
Code plugin docs: `hooks/hooks.json`, `command` type.)
|
|
136
|
-
2. **FALLBACK — `backthread install` writes `.claude/settings.json`.** For the bare
|
|
137
|
-
`npx backthread` (non-plugin) path there is no manifest doing the wiring, so
|
|
138
|
-
`backthread install` merges this entry into the project's `.claude/settings.json`
|
|
139
|
-
itself (idempotent — re-running never duplicates it; a strict merge that never
|
|
140
|
-
clobbers other settings/hooks):
|
|
141
|
-
|
|
142
|
-
```jsonc
|
|
143
|
-
{
|
|
144
|
-
"hooks": {
|
|
145
|
-
"SessionEnd": [
|
|
146
|
-
{ "hooks": [ { "type": "command", "command": "npx backthread capture" } ] }
|
|
147
|
-
]
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## `backthread start` — the plugin first-run (8B.5 / ARP-486)
|
|
153
|
-
|
|
154
|
-
The **in-agent** first-run for the plugin half of the both-equal front door (ARP-482),
|
|
155
|
-
behind the **`/backthread:start`** slash command. Where `backthread install` wires the
|
|
156
|
-
bare-`npx` (non-plugin) path (hook registration + backfill), `start` is the experience
|
|
157
|
-
a founder gets after a **marketplace 1-click** install — the SessionEnd hook is already
|
|
158
|
-
armed by the manifest, so `start` only does the *human* part:
|
|
159
|
-
|
|
160
|
-
1. **Idempotence gate.** Reads `~/.backthread/first-run.json`. A returning user
|
|
161
|
-
(`onboarded` flag set) is **never re-onboarded** — it just confirms they're set up.
|
|
162
|
-
2. **Trust gate.** Prints the **never-store-source** trust copy (the `TRUST_COPY` from
|
|
163
|
-
`install.ts`, consistent with [`/security`](https://backthread.dev/security)) **before
|
|
164
|
-
anything else** — the AC requires the claim to precede any transcript processing.
|
|
165
|
-
3. **One-tap auth.** `--claim <code>` (8B.9) exchanges a web-app-minted claim code for a
|
|
166
|
-
device token — no browser, the web-initiated door. With no claim code it runs the
|
|
167
|
-
browser **loopback** (`ensureAuth`); an already-authed device short-circuits.
|
|
168
|
-
`--device` (headless device-code, F7.23) is **out of scope** (→ ARP-467) and is
|
|
169
|
-
**refused with the loud stub** rather than silently falling back to the loopback
|
|
170
|
-
(which would hang a headless box).
|
|
171
|
-
4. **State-driven next step.** Reads the **unified onboarding state** (`fetchOnboardingState`,
|
|
172
|
-
8B.6 — the SAME backend signal the web wizard reads) and renders its canonical next
|
|
173
|
-
step. When the repo isn't connected this IS the **F7.10 connect nudge** copy
|
|
174
|
-
(server-driven); a terminal state renders cleanly with the diagram deep-link.
|
|
175
|
-
5. **Mark onboarded.** Persists the flag so step 1 short-circuits next time.
|
|
176
|
-
|
|
177
|
-
Exits non-zero **only** on a genuine auth failure (capture won't run until you act);
|
|
178
|
-
the auth failure does **not** mark onboarded, so re-running retries.
|
|
179
|
-
|
|
180
|
-
**"Onboarded" is an explicit flag**, not the derived "token present + ≥1 capture": the
|
|
181
|
-
trust gate must precede the very first capture (so we can't key onboarding off having
|
|
182
|
-
already captured), and a token rotation must not re-trigger the wizard. The derived
|
|
183
|
-
onboarding *state* still drives the *next-step* copy — it's just not the onboarding gate.
|
|
184
|
-
|
|
185
|
-
### The two confirmation surfaces in the capture path
|
|
17
|
+
## Your source code never leaves your machine
|
|
186
18
|
|
|
187
|
-
|
|
19
|
+
Backthread reads your agent **transcripts**, not your repo. Before anything is
|
|
20
|
+
sent, the CLI redacts every transcript **locally**:
|
|
188
21
|
|
|
189
|
-
- **
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
prints the trust copy **once, before reading the transcript or firing login** — the
|
|
193
|
-
never-store-source claim holds on both entrypoints. Best-effort + never-throws +
|
|
194
|
-
never-blocks: it can't break the always-exit-0 capture contract.
|
|
195
|
-
- **First-capture confirmation** (`maybeFirstCaptureConfirm`): after the **first** capture
|
|
196
|
-
that lands decisions against a **connected** repo, a once-only **"captured N — view them
|
|
197
|
-
in your "How it works" diagram: <deep-link>"** line. Mutually exclusive with the connect
|
|
198
|
-
nudge (which owns the not-connected case). Throttled via `firstCaptureShown` in the
|
|
199
|
-
same `first-run.json`. Best-effort.
|
|
22
|
+
- **Drops** every tool call and tool result — where source code and command output live.
|
|
23
|
+
- **Keeps** only natural-language prompts and the agent's reasoning.
|
|
24
|
+
- **Redacts** any fenced code block to `[code redacted]`.
|
|
200
25
|
|
|
201
|
-
|
|
26
|
+
So no source code and no tool I/O ever leave your machine. Because the default
|
|
27
|
+
path runs inference on our servers, what *does* leave is the **redacted
|
|
28
|
+
transcript** — natural-language prose only. The Worker re-runs the fenced-code
|
|
29
|
+
scrub server-side as a fail-closed backstop, derives the **decisions**, and
|
|
30
|
+
discards the transcript right after — processed in memory, never stored. Only
|
|
31
|
+
the decisions are persisted.
|
|
202
32
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
33
|
+
That's a weaker claim than the bring-your-own-key path — where nothing but the
|
|
34
|
+
derived decisions ever leaves your machine — which is designed and coming. We'd
|
|
35
|
+
rather say so than paper over it. The redaction fence is open source
|
|
36
|
+
([`@backthread/redact`](https://www.npmjs.com/package/@backthread/redact)) so you
|
|
37
|
+
can verify it — read more at [backthread.dev/security](https://backthread.dev/security).
|
|
206
38
|
|
|
207
|
-
|
|
208
|
-
token already exists it's reused; otherwise the browser OAuth-loopback runs once.
|
|
209
|
-
2. **Register the SessionEnd hook** — writes the `.claude/settings.json` fallback
|
|
210
|
-
above (skip with `--skip-hook` when installed as a plugin, where the manifest
|
|
211
|
-
already registers it).
|
|
212
|
-
3. **Chain the backfill** — runs `backthread`'s **cli-native backfill** (below) so the
|
|
213
|
-
decision log is **non-empty at the aha moment**, then self-maintaining via the
|
|
214
|
-
live hook. Best-effort; skipped automatically when not yet authorized.
|
|
39
|
+
## Quick start
|
|
215
40
|
|
|
216
|
-
|
|
41
|
+
In your project:
|
|
217
42
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
redaction happens locally; source code + tool I/O never leave the machine; only
|
|
221
|
-
the **derived decisions** are stored. On the default server-inference path a
|
|
222
|
-
*redacted, natural-language-only* transcript does leave the machine — never
|
|
223
|
-
source, never tool output — and it's **discarded right after extraction, never
|
|
224
|
-
stored** (the server re-redacts as a fail-closed backstop). The copy states this
|
|
225
|
-
weaker claim plainly rather than hiding it behind "only decisions leave."
|
|
226
|
-
|
|
227
|
-
### Cli-native backfill — `backfill.ts` (the architecture decision, FLAGGED)
|
|
228
|
-
|
|
229
|
-
The original F5 backfill is `scripts/ingest/decisions/backfill-cli.ts`, but the
|
|
230
|
-
`npx backthread` plugin **cannot ship `scripts/`** (separate dependency-light bundle,
|
|
231
|
-
`rootDir: src`). So `backthread install` chains a **cli-native** backfill instead: it
|
|
232
|
-
enumerates this repo's existing Claude Code transcripts at
|
|
233
|
-
`~/.claude/projects/<encoded-cwd>/*.jsonl` (the layout F7.3 established) and runs
|
|
234
|
-
each one through the **same `runCapture` pipeline** as the live hook. It does not
|
|
235
|
-
reimplement the redact/derive/persist fence — the never-store-source posture is
|
|
236
|
-
inherited verbatim. It is sequential (a one-shot seed has no latency budget),
|
|
237
|
-
best-effort (a missing dir / unreadable file / per-transcript failure is tallied,
|
|
238
|
-
never fatal), and idempotent (the pipeline's dedupe key makes re-running safe).
|
|
239
|
-
|
|
240
|
-
> **SCOPE (flagged):** this cli-native backfill is **Claude-Code-only**. Claude
|
|
241
|
-
> Code's per-repo transcript layout is enumerable with zero dependencies; the
|
|
242
|
-
> **multi-agent** backfill (Codex / Cursor / Gemini CLI via the F7.26 provider
|
|
243
|
-
> registry) stays the **dogfood / server path** in `scripts/`, whose adapters the
|
|
244
|
-
> plugin must not depend on. A founder using other agents still gets *live* capture
|
|
245
|
-
> for them via the per-agent surfaces; only the one-shot history seed is
|
|
246
|
-
> Claude-Code-only here. A shared backfill layer is a follow-up.
|
|
247
|
-
|
|
248
|
-
## `/backthread:capture` — the manual slash command (F7.3 / ARP-434)
|
|
249
|
-
|
|
250
|
-
The explicit/manual counterpart to the automatic SessionEnd hook: the founder who
|
|
251
|
-
wants to capture **mid-session** ("capture what we just decided, now") or re-run a
|
|
252
|
-
session runs it. It drives the **same `runCapture` pipeline** as F7.2 (local-redact
|
|
253
|
-
→ F7.IR router-derive → hosted-POST) — the redact fence is never reimplemented —
|
|
254
|
-
but differs from the hook in two ways:
|
|
255
|
-
|
|
256
|
-
1. **It resolves the transcript itself.** The hook is fed `transcript_path` on
|
|
257
|
-
STDIN by Claude Code; a slash command is **not** (Claude Code exposes
|
|
258
|
-
`${CLAUDE_SESSION_ID}` and the cwd to a command, but not the transcript path).
|
|
259
|
-
So the manual path derives it from Claude Code's on-disk layout:
|
|
260
|
-
`~/.claude/projects/<slugified-cwd>/<session_id>.jsonl` (the slug replaces every
|
|
261
|
-
non-alphanumeric char in the absolute cwd with `-`). An explicit
|
|
262
|
-
`--transcript <path>` always wins. If neither resolves a readable file, it
|
|
263
|
-
prints an **actionable hint** (run with `--transcript <path>`) — never a silent
|
|
264
|
-
no-op, and never a browser pop.
|
|
265
|
-
2. **It surfaces a per-run summary to STDOUT** (status + decision count +
|
|
266
|
-
repo-connected state) and exits non-zero on a genuine failure or when not logged
|
|
267
|
-
in — the manual analogue of the local pipeline's `summarize()`. (The hook stays
|
|
268
|
-
silent-to-stderr and always exits 0.) A missing device token surfaces as a
|
|
269
|
-
`run backthread login` hint; manual mode injects a **no-op `ensureAuth`** so — unlike
|
|
270
|
-
the best-effort hook — it never kicks off a background browser login.
|
|
271
|
-
|
|
272
|
-
### The slash-command mechanism (FLAGGED design choice)
|
|
273
|
-
|
|
274
|
-
Claude Code custom commands are merged into **skills**: a file at
|
|
275
|
-
`<plugin>/commands/capture.md` in a plugin named `backthread` becomes **`/backthread:capture`**
|
|
276
|
-
(plugin-namespaced — the faithful realization of the ticket's `/backthread capture`). The
|
|
277
|
-
command file uses Claude Code's **`!`shell injection`**` to run
|
|
278
|
-
`npx backthread capture --manual --session "${CLAUDE_SESSION_ID}" --cwd "$(pwd)"` at
|
|
279
|
-
render time; the bin's summary is inlined into the prompt and the agent relays it.
|
|
280
|
-
|
|
281
|
-
This package therefore ships a minimal plugin manifest (`.claude-plugin/plugin.json`)
|
|
282
|
-
+ the command file (`commands/capture.md`), in addition to the `backthread` bin. Both are
|
|
283
|
-
in package.json `files` so they ship via npm. The same files load standalone for
|
|
284
|
-
local dev with `claude --plugin-dir ./cli`. (The hook + MCP-server wiring into a
|
|
285
|
-
user's settings is install-time automation owned by F7.7 / ARP-438, sequenced after
|
|
286
|
-
this task; the command file is the slash-command half.)
|
|
287
|
-
|
|
288
|
-
## `backthread mcp` — the MCP server (F7.4 / ARP-435)
|
|
289
|
-
|
|
290
|
-
The in-Claude-Code surface for both halves of HMW #2 ("CC = capture/queries
|
|
291
|
-
entry"). `backthread mcp` starts a long-running MCP server over **stdio** (stdout is the
|
|
292
|
-
JSON-RPC channel; all diagnostics go to stderr) exposing two tools:
|
|
293
|
-
|
|
294
|
-
- **`capture`** — capture the current/given session's DECISIONS (the "why"). It
|
|
295
|
-
reuses the **F7.2 `runCapture` pipeline verbatim** (local-redact → derive →
|
|
296
|
-
hosted-POST) — the redact fence is never reimplemented. Best-effort: a hiccup
|
|
297
|
-
never disrupts the session. Args: `transcript_path` (**required** — unlike the
|
|
298
|
-
hook, the MCP tool has no STDIN, so the host must supply the transcript path;
|
|
299
|
-
without it the tool returns an actionable hint and captures nothing), plus
|
|
300
|
-
optional `cwd` and `session_id`.
|
|
301
|
-
- **`query`** ("how does X work?") — reads the **salience-ranked Flows +
|
|
302
|
-
Decisions** for the configured repo via the F7.5 `read-decisions` endpoint
|
|
303
|
-
(authenticated with the `backthread_pat_` device token), and returns them **plus a
|
|
304
|
-
deep-link** into the web-app "How it works" diagram
|
|
305
|
-
(`https://app.backthread.dev/<owner>/<repo>`) so the founder can jump from CC to the
|
|
306
|
-
visual. Read-only. Args (all optional): `question` (narrated against the returned
|
|
307
|
-
log, not a server-side filter), `repo` (`owner/name` override; else `config.repo`,
|
|
308
|
-
else the cwd git remote), `cwd`. Unauthed → it reports "run `backthread login`" — it
|
|
309
|
-
**never** triggers a browser login itself (unlike the best-effort capture hook).
|
|
310
|
-
|
|
311
|
-
Register it in `.claude/settings.json` (install-time wiring is F7.7 / ARP-438):
|
|
312
|
-
|
|
313
|
-
```jsonc
|
|
314
|
-
{
|
|
315
|
-
"mcpServers": {
|
|
316
|
-
"backthread": { "command": "npx", "args": ["backthread", "mcp"] }
|
|
317
|
-
}
|
|
318
|
-
}
|
|
43
|
+
```bash
|
|
44
|
+
npx backthread install
|
|
319
45
|
```
|
|
320
46
|
|
|
321
|
-
|
|
322
|
-
`runCapture` / `queryDecisions` (their own modules + tests). Both handlers and the
|
|
323
|
-
full tool wiring are unit-tested over the SDK's in-memory transport with mocked
|
|
324
|
-
impls — no live network, no browser, no real auth.
|
|
325
|
-
|
|
326
|
-
### How `cli/` accesses the redact/parse fence (architecture decision)
|
|
327
|
-
|
|
328
|
-
The redaction fence is owned by `scripts/ingest/decisions/transcript.ts` (the
|
|
329
|
-
F3/ARP-427 fence, behind the F7.11 provider seam). The `cli/` package, however,
|
|
330
|
-
is a **separate, dependency-light bundle** whose distribution target is `npx backthread`
|
|
331
|
-
— where **`scripts/` does not ship** — and its tsconfig pins `rootDir: src`, so a
|
|
332
|
-
`../scripts/...` import would also break the build layout. So the fence is
|
|
333
|
-
**vendored** into `cli/src/redact.ts` (+ the git-remote→repo parser into
|
|
334
|
-
`cli/src/repo.ts`) as pure, zero-dependency copies. Parity with the canonical
|
|
335
|
-
implementations is enforced by golden cases in `redact.test.ts` / `repo.test.ts`,
|
|
336
|
-
so the two cannot silently drift.
|
|
337
|
-
|
|
338
|
-
> **Distribution follow-up (flagged on ARP-433):** the vendored copy is the
|
|
339
|
-
> pragmatic dogfood call, **not** the end state. The end state is a tiny shared
|
|
340
|
-
> package (e.g. `@backthread/redact`) that both `scripts/ingest/decisions` and `cli/`
|
|
341
|
-
> depend on, so the security fence has exactly **one** implementation. Until then,
|
|
342
|
-
> a change to one fence must be mirrored in the other (the parity tests will fail
|
|
343
|
-
> loudly if not). This is a follow-up ticket, not in-scope here.
|
|
47
|
+
That's the whole setup. `install`:
|
|
344
48
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
"repo": "owner/name",
|
|
353
|
-
"device_token": "backthread_pat_…"
|
|
354
|
-
}
|
|
355
|
-
```
|
|
49
|
+
1. **Signs you in** — opens your browser for one click (you'll need a free
|
|
50
|
+
[Backthread](https://backthread.dev) account; the CLI never sees a password,
|
|
51
|
+
and your device token is never printed or copied to the clipboard).
|
|
52
|
+
2. **Wires up capture** — registers a hook so each Claude Code session is
|
|
53
|
+
captured automatically when it ends.
|
|
54
|
+
3. **Backfills history** — replays your recent Claude Code sessions in this repo
|
|
55
|
+
so your "How it works" log isn't empty on day one.
|
|
356
56
|
|
|
357
|
-
|
|
57
|
+
Already added Backthread as a Claude Code plugin? The hook is wired for you —
|
|
58
|
+
run `/backthread:start` (or `npx backthread start`) just to sign in.
|
|
358
59
|
|
|
359
|
-
##
|
|
60
|
+
## Onboard yourself in 3 steps
|
|
360
61
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
`http://localhost:54321/functions/v1`) instead of production.
|
|
368
|
-
- `BACKTHREAD_CONFIG_DIR` — point the config at a temp dir instead of `~/.backthread`
|
|
369
|
-
(used by the unit tests; no real `$HOME` is touched).
|
|
62
|
+
1. **Install** — `npx backthread install` in your repo. One browser click to authorize.
|
|
63
|
+
2. **Keep coding** — at the end of every Claude Code session, Backthread captures
|
|
64
|
+
the decisions automatically. Nothing to remember.
|
|
65
|
+
3. **Ask "how does X work?"** — query your decision log right inside Claude Code
|
|
66
|
+
(the `backthread` MCP server exposes a `query` tool), or open the live diagram
|
|
67
|
+
at [app.backthread.dev](https://app.backthread.dev).
|
|
370
68
|
|
|
371
|
-
##
|
|
372
|
-
|
|
373
|
-
**Default: `npx backthread`** — a standalone npm package with a `backthread` bin. Rationale:
|
|
374
|
-
it's the most portable spine — the F7.2 hook, F7.3 slash command, and F7.4 MCP
|
|
375
|
-
all shell out to / import the same bin regardless of which agent host invokes
|
|
376
|
-
them, and `npx backthread login` works on any machine with Node. A **Claude Code
|
|
377
|
-
plugin manifest** can wrap this bin later (it would just declare the hook +
|
|
378
|
-
slash command and call `backthread`), so picking `npx backthread` now does not foreclose the
|
|
379
|
-
plugin-manifest path — it's the lower layer underneath it. Per-agent adapters
|
|
380
|
-
(Cursor/Codex) are explicitly later (Thread B / ARP-455→458).
|
|
381
|
-
|
|
382
|
-
## `--device` fallback — STUBBED in F7.1
|
|
383
|
-
|
|
384
|
-
The headless device-code flow (`gh`-style: "go to <url>, enter code ABCD-1234")
|
|
385
|
-
needs a **server-side device-authorization endpoint** (a `/device/code` +
|
|
386
|
-
`/device/token` pair) that does not exist yet — the F7.0 primitive (ARP-448)
|
|
387
|
-
only ships the session-path mint. `backthread login --device` currently prints clear
|
|
388
|
-
guidance (run `backthread login` with a browser, or mint from Account → Connected
|
|
389
|
-
devices) and exits non-zero. Full impl is a follow-up.
|
|
390
|
-
|
|
391
|
-
## Build / test
|
|
69
|
+
## Commands
|
|
392
70
|
|
|
393
71
|
```
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
72
|
+
backthread install Set up capture for this repo (sign in + hook + backfill)
|
|
73
|
+
backthread start First-run for the Claude Code plugin (sign in + your next step)
|
|
74
|
+
backthread login Authorize this device (opens your browser)
|
|
75
|
+
backthread whoami Show this device's config (your token is never printed)
|
|
76
|
+
backthread capture Capture a session's decisions (run automatically by the hook)
|
|
77
|
+
backthread mcp Start the MCP server — the capture + "how does X work?" query tools
|
|
78
|
+
backthread help Show usage
|
|
399
79
|
```
|
|
400
80
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
## Two build paths (8A.1 / ARP-474)
|
|
81
|
+
## Requirements
|
|
404
82
|
|
|
405
|
-
|
|
83
|
+
- **Node.js ≥ 22.18**
|
|
406
84
|
|
|
407
|
-
|
|
408
|
-
multi-file `dist/` and resolves the one runtime dep
|
|
409
|
-
(`@modelcontextprotocol/sdk`) from `node_modules` at runtime. This is what
|
|
410
|
-
`npx backthread` uses (npm installs the dep tree), and what `dist/bin/backthread.js` — the
|
|
411
|
-
`package.json` `bin` target — points at.
|
|
412
|
-
- **`npm run bundle` (esbuild) → `dist-bundle/backthread.js`** — the **self-contained
|
|
413
|
-
distribution path**. Bundles the bin **and** that runtime dep into a single
|
|
414
|
-
executable ESM file (`esbuild.config.mjs`: `--bundle --platform=node
|
|
415
|
-
--format=esm --target=node22`, tree-shaken so only the `server/mcp.js` +
|
|
416
|
-
`server/stdio.js` subpaths of the SDK are inlined). It runs with **no
|
|
417
|
-
`npm install`** — drop the one file anywhere with Node 22 and run it.
|
|
85
|
+
## Learn more
|
|
418
86
|
|
|
419
|
-
|
|
87
|
+
- **Live app** — [backthread.dev](https://backthread.dev)
|
|
88
|
+
- **How your data is handled** — [backthread.dev/security](https://backthread.dev/security)
|
|
89
|
+
- **Source & internals** — [github.com/backthread/backthread](https://github.com/backthread/backthread)
|
|
420
90
|
|
|
421
|
-
|
|
422
|
-
`${CLAUDE_PLUGIN_ROOT}/cli/dist-bundle/backthread.js` (so the plugin ships a runnable
|
|
423
|
-
bin without vendoring `node_modules`), and
|
|
424
|
-
- a **future standalone binary** (Node SEA / `pkg`) wraps.
|
|
91
|
+
## License
|
|
425
92
|
|
|
426
|
-
|
|
427
|
-
are listed in `package.json` `files` so they ship via npm/`npm pack`. A
|
|
428
|
-
`prepack` script (`build && bundle`) regenerates both from current source
|
|
429
|
-
whenever the package is packed, so a tarball never ships stale/empty `dist*`.
|
|
430
|
-
The bundle does not replace the tsc build — they coexist for their two targets.
|
|
431
|
-
esbuild is the only new devDep.
|
|
93
|
+
[MIT](./LICENSE) © Backthread
|
|
@@ -7272,12 +7272,12 @@ function deviceLogin(log) {
|
|
|
7272
7272
|
"Headless (--device) login is not available yet.",
|
|
7273
7273
|
"",
|
|
7274
7274
|
"The device-code fallback needs a server-side device-authorization endpoint",
|
|
7275
|
-
"that ships in a later
|
|
7275
|
+
"that ships in a later task. For now, run `backthread login` on a machine with a",
|
|
7276
7276
|
"browser, or mint a token from the web app (Account \u2192 Connected devices) and",
|
|
7277
7277
|
'place it in ~/.backthread/config.json under "device_token".'
|
|
7278
7278
|
].join("\n")
|
|
7279
7279
|
);
|
|
7280
|
-
return { ok: false, message: "--device fallback not implemented yet
|
|
7280
|
+
return { ok: false, message: "--device fallback not implemented yet." };
|
|
7281
7281
|
}
|
|
7282
7282
|
async function ensureAuth(opts = {}) {
|
|
7283
7283
|
const env = opts.env ?? process.env;
|
|
@@ -7469,7 +7469,7 @@ async function serverInfer(transcript, config2, opts = {}) {
|
|
|
7469
7469
|
Authorization: `Bearer ${token}`,
|
|
7470
7470
|
"Content-Type": "application/json",
|
|
7471
7471
|
...versionHeaders()
|
|
7472
|
-
// x-backthread-version — server-side compat guard
|
|
7472
|
+
// x-backthread-version — server-side compat guard
|
|
7473
7473
|
},
|
|
7474
7474
|
body: JSON.stringify(body)
|
|
7475
7475
|
});
|
|
@@ -7974,7 +7974,7 @@ async function fetchOnboardingState(input = {}, deps = {}) {
|
|
|
7974
7974
|
// never logged
|
|
7975
7975
|
"Content-Type": "application/json",
|
|
7976
7976
|
...versionHeaders()
|
|
7977
|
-
// x-backthread-version — server-side compat guard
|
|
7977
|
+
// x-backthread-version — server-side compat guard
|
|
7978
7978
|
},
|
|
7979
7979
|
// CLI shape: repo_slug = "owner/name". Omitted when no repo resolved.
|
|
7980
7980
|
body: JSON.stringify(repo ? { repo_slug: `${repo.owner}/${repo.name}` } : {})
|
|
@@ -8314,10 +8314,10 @@ async function runCapture(input, deps = {}) {
|
|
|
8314
8314
|
env,
|
|
8315
8315
|
fetchImpl: deps.fetchImpl,
|
|
8316
8316
|
log,
|
|
8317
|
-
// Carry the session id so the connect-nudge
|
|
8317
|
+
// Carry the session id so the connect-nudge can throttle once-per-session
|
|
8318
8318
|
// — the SessionEnd hook fires once, but manual/MCP captures fire many times.
|
|
8319
8319
|
sessionId,
|
|
8320
|
-
//
|
|
8320
|
+
// first-capture confirmation seam (threaded so tests can stub it).
|
|
8321
8321
|
firstCaptureConfirmImpl: deps.firstCaptureConfirmImpl
|
|
8322
8322
|
});
|
|
8323
8323
|
} catch (e) {
|
|
@@ -8346,7 +8346,7 @@ async function persistDerived(decisions, repo, config2, decidedAt, ctx) {
|
|
|
8346
8346
|
// device token — never logged
|
|
8347
8347
|
"Content-Type": "application/json",
|
|
8348
8348
|
...versionHeaders()
|
|
8349
|
-
// x-backthread-version — server-side compat guard
|
|
8349
|
+
// x-backthread-version — server-side compat guard
|
|
8350
8350
|
},
|
|
8351
8351
|
body: JSON.stringify(body)
|
|
8352
8352
|
});
|
|
@@ -32696,7 +32696,7 @@ async function queryDecisions(input, deps = {}) {
|
|
|
32696
32696
|
Authorization: `Bearer ${config2.device_token}`,
|
|
32697
32697
|
"Content-Type": "application/json",
|
|
32698
32698
|
...versionHeaders()
|
|
32699
|
-
// x-backthread-version — server-side compat guard
|
|
32699
|
+
// x-backthread-version — server-side compat guard
|
|
32700
32700
|
},
|
|
32701
32701
|
body: JSON.stringify({ repo: { owner: repo.owner, name: repo.name } })
|
|
32702
32702
|
});
|
package/hooks/hooks.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "
|
|
2
|
+
"$comment": "The SessionEnd capture hook, declared in the PLUGIN MANIFEST (referenced from .claude-plugin/plugin.json). When Backthread is installed as a Claude Code plugin, this registers the hook automatically — no mutation of the user's .claude/settings.json. `npx backthread capture` reads the SessionEnd payload off stdin, derives this session's decisions LOCALLY-redacted, and persists them best-effort; it always exits 0 so a capture hiccup can never disrupt the session. Mirrored by the .claude/settings.json fallback that `backthread install` writes for the bare-npx (non-plugin) path. We register ONLY SessionEnd (once per session) on purpose — `runCapture` also handles a Stop payload, but Stop fires on every turn-end, which would capture far too aggressively, so Stop is intentionally NOT registered here.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionEnd": [
|
|
5
5
|
{
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backthread",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Backthread CLI — capture the
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Backthread CLI — capture the why behind your AI-coded changes from your Claude Code sessions, and ask how your codebase works without leaving the terminal. Source code and tool I/O are redacted locally before anything leaves your machine.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Backthread",
|
|
7
7
|
"homepage": "https://backthread.dev",
|