backthread 0.2.2 → 0.3.0
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/.claude-plugin/plugin.json +1 -1
- package/README.md +43 -42
- package/dist-bundle/backthread.js +186 -32
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "backthread",
|
|
3
3
|
"displayName": "Backthread",
|
|
4
4
|
"description": "Backthread helps you understand your codebase while AI ships features. It captures the why behind every Claude Code session so you can ask \"how does X work?\" without digging through PRs.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.3.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Backthread"
|
|
8
8
|
},
|
package/README.md
CHANGED
|
@@ -5,14 +5,27 @@
|
|
|
5
5
|
|
|
6
6
|
**Keep the thread on what your AI agent actually shipped.**
|
|
7
7
|
|
|
8
|
+
```bash
|
|
9
|
+
npx backthread
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
One command, the whole setup: signs you in (one browser click), connects this
|
|
13
|
+
repo, wires up automatic capture, and hands you the link to your live **"How it
|
|
14
|
+
works"** diagram. Re-run it any time — it's idempotent, so a returning user just
|
|
15
|
+
gets told they're good to go.
|
|
16
|
+
|
|
17
|
+
> **In Claude Code?** `/plugin marketplace add backthread/backthread` →
|
|
18
|
+
> `/plugin install backthread@backthread` → `/backthread:start`. The plugin
|
|
19
|
+
> bundles the CLI, so there's no separate npm step.
|
|
20
|
+
|
|
8
21
|
When you hand code to AI agents (Claude Code, Codex, Cursor), you stop reading
|
|
9
22
|
every change — and a few weeks later you own a codebase you never internalized.
|
|
10
|
-
Debugging slows down, refactors get scary.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
Debugging slows down, refactors get scary. Backthread captures the **why** behind
|
|
24
|
+
each change straight from your agent sessions, so you can ask *"how does X work?"*
|
|
25
|
+
and stay oriented without spelunking through PRs. The decisions become a live
|
|
26
|
+
**"How it works"** diagram and changelog at
|
|
27
|
+
[app.backthread.dev](https://app.backthread.dev) — see the
|
|
28
|
+
[live demo](https://app.backthread.dev/demo).
|
|
16
29
|
|
|
17
30
|
## Your source code never leaves your machine
|
|
18
31
|
|
|
@@ -36,11 +49,26 @@ rather say so than paper over it. The redaction fence is open source
|
|
|
36
49
|
([`@backthread/redact`](https://www.npmjs.com/package/@backthread/redact)) so you
|
|
37
50
|
can verify it — read more at [backthread.dev/security](https://backthread.dev/security).
|
|
38
51
|
|
|
39
|
-
##
|
|
52
|
+
## What `npx backthread` does
|
|
40
53
|
|
|
41
|
-
|
|
54
|
+
The bare command is the unified front door. Under the hood it:
|
|
42
55
|
|
|
43
|
-
|
|
56
|
+
1. **Signs you in** — opens your browser for one click (you'll need a free
|
|
57
|
+
[Backthread](https://backthread.dev) account; the CLI never sees a password,
|
|
58
|
+
and your device token is never printed or copied to the clipboard).
|
|
59
|
+
2. **Wires up capture** — registers a hook so each Claude Code session is
|
|
60
|
+
captured automatically when it ends.
|
|
61
|
+
3. **Backfills history** — replays your recent Claude Code sessions in this repo
|
|
62
|
+
so your "How it works" log isn't empty on day one.
|
|
63
|
+
|
|
64
|
+
Then keep coding. At the end of every Claude Code session, Backthread captures
|
|
65
|
+
the decisions automatically — nothing to remember. Ask *"how does X work?"* right
|
|
66
|
+
inside Claude Code (the `backthread` MCP server exposes a `query` tool), or open
|
|
67
|
+
the live diagram at [app.backthread.dev](https://app.backthread.dev).
|
|
68
|
+
|
|
69
|
+
### Claude Code plugin (alternative)
|
|
70
|
+
|
|
71
|
+
Prefer the marketplace? In Claude Code:
|
|
44
72
|
|
|
45
73
|
```
|
|
46
74
|
/plugin marketplace add backthread/backthread
|
|
@@ -52,30 +80,9 @@ Installing the plugin bundles the CLI — no separate npm step — and registers
|
|
|
52
80
|
**user/global scope** (so it works across every repo and git worktree), the
|
|
53
81
|
SessionEnd **capture hook**, the `/backthread:capture` & `/backthread:start`
|
|
54
82
|
commands, and the **backthread MCP server** (capture + `query`). `/backthread:start`
|
|
55
|
-
just signs you in.
|
|
56
|
-
|
|
57
|
-
### Any agent (or bare terminal) → `npx backthread install`
|
|
83
|
+
just signs you in.
|
|
58
84
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
npx backthread install
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
That's the whole setup. `install`:
|
|
66
|
-
|
|
67
|
-
1. **Signs you in** — opens your browser for one click (you'll need a free
|
|
68
|
-
[Backthread](https://backthread.dev) account; the CLI never sees a password,
|
|
69
|
-
and your device token is never printed or copied to the clipboard).
|
|
70
|
-
2. **Wires up capture** — registers a hook so each Claude Code session is
|
|
71
|
-
captured automatically when it ends.
|
|
72
|
-
3. **Backfills history** — replays your recent Claude Code sessions in this repo
|
|
73
|
-
so your "How it works" log isn't empty on day one.
|
|
74
|
-
|
|
75
|
-
Already added Backthread as a Claude Code plugin? The hook is wired for you —
|
|
76
|
-
run `/backthread:start` (or `npx backthread start`) just to sign in.
|
|
77
|
-
|
|
78
|
-
### Codex / Cursor / Gemini CLI → `npx backthread install --agent <agent>`
|
|
85
|
+
### Codex / Cursor / Gemini CLI
|
|
79
86
|
|
|
80
87
|
Use another coding agent? One command wires up its **MCP server** (the `query`
|
|
81
88
|
tool) **and** an automatic capture hook — written to that agent's **user-global**
|
|
@@ -92,18 +99,11 @@ clobbers your other config). Then `npx backthread login` once to authorize. Gemi
|
|
|
92
99
|
users can also install the [one-command extension](https://github.com/backthread/backthread/tree/main/extensions/gemini)
|
|
93
100
|
instead, and Codex users the [plugin](https://github.com/backthread/backthread/tree/main/extensions/codex).
|
|
94
101
|
|
|
95
|
-
## Onboard yourself in 3 steps
|
|
96
|
-
|
|
97
|
-
1. **Install** — `npx backthread install` in your repo. One browser click to authorize.
|
|
98
|
-
2. **Keep coding** — at the end of every Claude Code session, Backthread captures
|
|
99
|
-
the decisions automatically. Nothing to remember.
|
|
100
|
-
3. **Ask "how does X work?"** — query your decision log right inside Claude Code
|
|
101
|
-
(the `backthread` MCP server exposes a `query` tool), or open the live diagram
|
|
102
|
-
at [app.backthread.dev](https://app.backthread.dev).
|
|
103
|
-
|
|
104
102
|
## Commands
|
|
105
103
|
|
|
106
104
|
```
|
|
105
|
+
backthread Set up Backthread — the unified front door (sign in + connect + capture).
|
|
106
|
+
Idempotent: a returning user is told they're good to go.
|
|
107
107
|
backthread install Set up capture for this repo (sign in + hook + backfill)
|
|
108
108
|
backthread start First-run for the Claude Code plugin (sign in + your next step)
|
|
109
109
|
backthread login Authorize this device (opens your browser)
|
|
@@ -119,7 +119,8 @@ backthread help Show usage
|
|
|
119
119
|
|
|
120
120
|
## Learn more
|
|
121
121
|
|
|
122
|
-
- **Live app** — [backthread.dev](https://backthread.dev)
|
|
122
|
+
- **Live app & demo** — [app.backthread.dev](https://app.backthread.dev) · [app.backthread.dev/demo](https://app.backthread.dev/demo)
|
|
123
|
+
- **Marketing site** — [backthread.dev](https://backthread.dev)
|
|
123
124
|
- **How your data is handled** — [backthread.dev/security](https://backthread.dev/security)
|
|
124
125
|
- **Source & internals** — [github.com/backthread/backthread](https://github.com/backthread/backthread)
|
|
125
126
|
|
|
@@ -6885,6 +6885,10 @@ var require_dist = __commonJS({
|
|
|
6885
6885
|
}
|
|
6886
6886
|
});
|
|
6887
6887
|
|
|
6888
|
+
// src/bin/backthread.ts
|
|
6889
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6890
|
+
import { realpathSync } from "node:fs";
|
|
6891
|
+
|
|
6888
6892
|
// src/login.ts
|
|
6889
6893
|
import { hostname } from "node:os";
|
|
6890
6894
|
|
|
@@ -7516,6 +7520,27 @@ function resolveRepo(cwd, readRemote = defaultRemoteReader) {
|
|
|
7516
7520
|
const remote = readRemote(cwd);
|
|
7517
7521
|
return remote ? parseRepoFromRemote(remote) : null;
|
|
7518
7522
|
}
|
|
7523
|
+
var defaultGitRunner = (cwd, args) => {
|
|
7524
|
+
try {
|
|
7525
|
+
return execFileSync("git", args, {
|
|
7526
|
+
cwd,
|
|
7527
|
+
encoding: "utf8",
|
|
7528
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
7529
|
+
});
|
|
7530
|
+
} catch {
|
|
7531
|
+
return null;
|
|
7532
|
+
}
|
|
7533
|
+
};
|
|
7534
|
+
function resolveGitContext(cwd, run = defaultGitRunner) {
|
|
7535
|
+
const rawBranch = run(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
7536
|
+
const branch = rawBranch ? rawBranch.trim() : "";
|
|
7537
|
+
const rawSha = run(cwd, ["rev-parse", "HEAD"]);
|
|
7538
|
+
const sha = rawSha ? rawSha.trim() : "";
|
|
7539
|
+
return {
|
|
7540
|
+
branch: branch && branch !== "HEAD" ? branch : null,
|
|
7541
|
+
headSha: sha || null
|
|
7542
|
+
};
|
|
7543
|
+
}
|
|
7519
7544
|
|
|
7520
7545
|
// src/infer.ts
|
|
7521
7546
|
async function localByokInfer(_transcript, _config, _opts) {
|
|
@@ -7558,6 +7583,9 @@ async function serverInfer(transcript, config2, opts = {}) {
|
|
|
7558
7583
|
body.repo = { owner: opts.repo.owner, name: opts.repo.name };
|
|
7559
7584
|
if (opts.decidedAt) body.decidedAt = opts.decidedAt;
|
|
7560
7585
|
if (opts.filePaths && opts.filePaths.length > 0) body.filePaths = opts.filePaths;
|
|
7586
|
+
if (opts.captured?.branch != null) body.capturedBranch = opts.captured.branch;
|
|
7587
|
+
if (opts.captured?.headSha != null) body.capturedHeadSha = opts.captured.headSha;
|
|
7588
|
+
if (opts.captured?.at != null) body.capturedAt = opts.captured.at;
|
|
7561
7589
|
}
|
|
7562
7590
|
let res;
|
|
7563
7591
|
try {
|
|
@@ -8555,6 +8583,33 @@ Backthread is set up for ${targetAgent}. New sessions are captured automatically
|
|
|
8555
8583
|
return { exitCode, authed, hookRegistered, backfill, agentResult: null };
|
|
8556
8584
|
}
|
|
8557
8585
|
|
|
8586
|
+
// src/entry.ts
|
|
8587
|
+
var PLUGIN_MARKETPLACE = "backthread/backthread";
|
|
8588
|
+
var PLUGIN_INSTALL = "backthread@backthread";
|
|
8589
|
+
function detectEntry(input = {}) {
|
|
8590
|
+
if (input.claim && input.claim.trim().length > 0) return "web";
|
|
8591
|
+
return "terminal";
|
|
8592
|
+
}
|
|
8593
|
+
function isInsideClaudeCode(env = process.env) {
|
|
8594
|
+
return env.CLAUDECODE === "1";
|
|
8595
|
+
}
|
|
8596
|
+
function captureGuidance(env = process.env) {
|
|
8597
|
+
if (isInsideClaudeCode(env)) {
|
|
8598
|
+
return [
|
|
8599
|
+
`Capture (the "why"): you're in Claude Code \u2014 install the plugin so every`,
|
|
8600
|
+
" session is captured automatically (it wires the hook + MCP across all your",
|
|
8601
|
+
" repos and worktrees):",
|
|
8602
|
+
` /plugin marketplace add ${PLUGIN_MARKETPLACE}`,
|
|
8603
|
+
` /plugin install ${PLUGIN_INSTALL}`
|
|
8604
|
+
].join("\n");
|
|
8605
|
+
}
|
|
8606
|
+
return [
|
|
8607
|
+
'Capture (the "why"): run `npx backthread install` here to wire the capture hook',
|
|
8608
|
+
" so each Claude Code session is captured automatically when it ends.",
|
|
8609
|
+
" Using Codex / Cursor / Gemini? `npx backthread install --agent <codex|cursor|gemini>`."
|
|
8610
|
+
].join("\n");
|
|
8611
|
+
}
|
|
8612
|
+
|
|
8558
8613
|
// src/onboardingState.ts
|
|
8559
8614
|
function parseSlug(slug) {
|
|
8560
8615
|
const parts = slug.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
@@ -8734,6 +8789,7 @@ async function runStart(opts = {}, deps = {}) {
|
|
|
8734
8789
|
const env = opts.env ?? process.env;
|
|
8735
8790
|
const log = opts.log ?? ((m) => console.error(m));
|
|
8736
8791
|
const cwd = opts.cwd ?? process.cwd();
|
|
8792
|
+
const entry = opts.entry ?? detectEntry({ claim: opts.claim, env });
|
|
8737
8793
|
const readState3 = deps.readStateImpl ?? readFirstRunState;
|
|
8738
8794
|
const existingState = await readState3(env).catch(() => ({}));
|
|
8739
8795
|
if (existingState.onboarded === true) {
|
|
@@ -8742,6 +8798,10 @@ async function runStart(opts = {}, deps = {}) {
|
|
|
8742
8798
|
if (cfg.device_token) {
|
|
8743
8799
|
log("Backthread is already set up on this machine \u2014 you're good to go.");
|
|
8744
8800
|
log(" New sessions are captured automatically when they end.");
|
|
8801
|
+
const repo = resolveOnboardingRepo({ cwd }, cfg, deps.onboardingDeps?.readRemoteImpl);
|
|
8802
|
+
if (repo) {
|
|
8803
|
+
log(` View your "How it works" diagram: ${buildRepoDeepLink(repo.owner, repo.name, env)}`);
|
|
8804
|
+
}
|
|
8745
8805
|
log(" Run `/backthread:capture` to capture the current session now.");
|
|
8746
8806
|
return { exitCode: 0, status: "already-onboarded", authed: true };
|
|
8747
8807
|
}
|
|
@@ -8772,6 +8832,9 @@ async function runStart(opts = {}, deps = {}) {
|
|
|
8772
8832
|
log(" Run `/backthread:start` again to retry authorizing this device.");
|
|
8773
8833
|
return { exitCode: 1, status: "auth-failed", authed: false };
|
|
8774
8834
|
}
|
|
8835
|
+
if (entry === "terminal") {
|
|
8836
|
+
log("\n" + captureGuidance(env));
|
|
8837
|
+
}
|
|
8775
8838
|
const fetchState = deps.fetchStateImpl ?? fetchOnboardingState;
|
|
8776
8839
|
const stateOut = await fetchState({ cwd }, { env, ...deps.onboardingDeps }).catch(
|
|
8777
8840
|
() => ({ status: "error", detail: "state fetch failed (swallowed)" })
|
|
@@ -8894,24 +8957,31 @@ async function runCapture(input, deps = {}) {
|
|
|
8894
8957
|
const decidedAt = sessionTimestamp(records) ?? void 0;
|
|
8895
8958
|
const filePaths = sessionPaths(records, input.cwd);
|
|
8896
8959
|
const sessionId = redacted.sessionId ?? input.session_id ?? null;
|
|
8897
|
-
|
|
8960
|
+
const turnCount = redacted.turns.length;
|
|
8961
|
+
const fromTurn = deps.fromTurnIndex ?? 0;
|
|
8962
|
+
const newTurns = fromTurn > 0 ? redacted.turns.slice(fromTurn) : redacted.turns;
|
|
8963
|
+
if (newTurns.length === 0) {
|
|
8898
8964
|
return {
|
|
8899
8965
|
status: "nothing-to-capture",
|
|
8900
|
-
detail: "redaction left no natural-language turns (session was all code / tool I/O)."
|
|
8901
|
-
count: 0
|
|
8966
|
+
detail: turnCount === 0 ? "redaction left no natural-language turns (session was all code / tool I/O)." : `no new turns since the last capture (watermark ${fromTurn} of ${turnCount}).`,
|
|
8967
|
+
count: 0,
|
|
8968
|
+
turnCount
|
|
8902
8969
|
};
|
|
8903
8970
|
}
|
|
8904
8971
|
const transcript = {
|
|
8905
8972
|
sessionId,
|
|
8906
|
-
turns:
|
|
8973
|
+
turns: newTurns,
|
|
8907
8974
|
stats: redacted.stats
|
|
8908
8975
|
};
|
|
8909
8976
|
const repo = input.cwd ? resolveRepo(input.cwd, deps.readRemoteImpl) : null;
|
|
8977
|
+
const gitContext = input.cwd ? resolveGitContext(input.cwd, deps.readGitImpl) : { branch: null, headSha: null };
|
|
8978
|
+
const captured = { branch: gitContext.branch, headSha: gitContext.headSha, at: decidedAt ?? null };
|
|
8910
8979
|
const result = await inferDecisions(transcript, config2, {
|
|
8911
8980
|
env,
|
|
8912
8981
|
fetchImpl: deps.fetchImpl,
|
|
8913
8982
|
decidedAt,
|
|
8914
8983
|
filePaths,
|
|
8984
|
+
captured,
|
|
8915
8985
|
...repo ? { persist: true, repo } : {}
|
|
8916
8986
|
});
|
|
8917
8987
|
if (!result.ok) {
|
|
@@ -8924,33 +8994,39 @@ async function runCapture(input, deps = {}) {
|
|
|
8924
8994
|
status: "persisted-by-server",
|
|
8925
8995
|
detail: `inference router persisted ${result.decisions.length} decision(s) server-side.`,
|
|
8926
8996
|
count: result.decisions.length,
|
|
8927
|
-
repoConnected: true
|
|
8997
|
+
repoConnected: true,
|
|
8998
|
+
turnCount
|
|
8928
8999
|
};
|
|
8929
9000
|
}
|
|
8930
9001
|
if (result.decisions.length === 0) {
|
|
8931
9002
|
return {
|
|
8932
9003
|
status: "nothing-to-capture",
|
|
8933
9004
|
detail: "inference returned no decisions for this session.",
|
|
8934
|
-
count: 0
|
|
9005
|
+
count: 0,
|
|
9006
|
+
turnCount
|
|
8935
9007
|
};
|
|
8936
9008
|
}
|
|
8937
9009
|
if (!repo) {
|
|
8938
9010
|
return {
|
|
8939
9011
|
status: "nothing-to-capture",
|
|
8940
9012
|
detail: "derived decisions but could not resolve a repo from cwd (no git remote) \u2014 nothing to claim them under; skipped.",
|
|
8941
|
-
count: 0
|
|
9013
|
+
count: 0,
|
|
9014
|
+
turnCount
|
|
8942
9015
|
};
|
|
8943
9016
|
}
|
|
8944
|
-
|
|
9017
|
+
const out = await persistDerived(result.decisions, repo, config2, decidedAt, {
|
|
8945
9018
|
env,
|
|
8946
9019
|
fetchImpl: deps.fetchImpl,
|
|
8947
9020
|
log,
|
|
8948
9021
|
// Carry the session id so the connect-nudge can throttle once-per-session
|
|
8949
9022
|
// — the SessionEnd hook fires once, but manual/MCP captures fire many times.
|
|
8950
9023
|
sessionId,
|
|
9024
|
+
// ARP-696 — the session's git context, for the held-state decision server-side.
|
|
9025
|
+
captured,
|
|
8951
9026
|
// first-capture confirmation seam (threaded so tests can stub it).
|
|
8952
9027
|
firstCaptureConfirmImpl: deps.firstCaptureConfirmImpl
|
|
8953
9028
|
});
|
|
9029
|
+
return { ...out, turnCount };
|
|
8954
9030
|
} catch (e) {
|
|
8955
9031
|
return { status: "error", detail: `capture failed (swallowed): ${e.message}` };
|
|
8956
9032
|
}
|
|
@@ -8963,6 +9039,13 @@ async function persistDerived(decisions, repo, config2, decidedAt, ctx) {
|
|
|
8963
9039
|
}
|
|
8964
9040
|
const body = {
|
|
8965
9041
|
repo: { owner: repo.owner, name: repo.name },
|
|
9042
|
+
// ARP-696 — session-level git context (the ingest-decisions validator reads it
|
|
9043
|
+
// body-level and stamps each decision). Each field only when present; absent →
|
|
9044
|
+
// the server keeps the decision merged (back-compat). It's the repo-less /
|
|
9045
|
+
// self-persist path, so a held decision waits for the repo to connect + reconcile.
|
|
9046
|
+
...ctx.captured?.branch != null ? { capturedBranch: ctx.captured.branch } : {},
|
|
9047
|
+
...ctx.captured?.headSha != null ? { capturedHeadSha: ctx.captured.headSha } : {},
|
|
9048
|
+
...ctx.captured?.at != null ? { capturedAt: ctx.captured.at } : {},
|
|
8966
9049
|
decisions: decisions.map((d) => ({
|
|
8967
9050
|
...d,
|
|
8968
9051
|
...decidedAt && d.decidedAt === void 0 ? { decidedAt } : {}
|
|
@@ -9065,19 +9148,26 @@ var MAX_REMEMBERED2 = 200;
|
|
|
9065
9148
|
function parseState2(raw) {
|
|
9066
9149
|
try {
|
|
9067
9150
|
const obj = JSON.parse(raw);
|
|
9068
|
-
if (obj && typeof obj === "object" && Array.isArray(obj
|
|
9069
|
-
const
|
|
9070
|
-
|
|
9151
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
9152
|
+
const o = obj;
|
|
9153
|
+
const captured = Array.isArray(o.captured) ? o.captured.filter((s) => typeof s === "string") : [];
|
|
9154
|
+
const watermarks = {};
|
|
9155
|
+
if (o.watermarks && typeof o.watermarks === "object" && !Array.isArray(o.watermarks)) {
|
|
9156
|
+
for (const [k, v] of Object.entries(o.watermarks)) {
|
|
9157
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 0) watermarks[k] = v;
|
|
9158
|
+
}
|
|
9159
|
+
}
|
|
9160
|
+
return { captured, watermarks };
|
|
9071
9161
|
}
|
|
9072
9162
|
} catch {
|
|
9073
9163
|
}
|
|
9074
|
-
return { captured: [] };
|
|
9164
|
+
return { captured: [], watermarks: {} };
|
|
9075
9165
|
}
|
|
9076
9166
|
async function readState2(env) {
|
|
9077
9167
|
try {
|
|
9078
9168
|
return parseState2(await readFile9(captureStatePath(env), "utf8"));
|
|
9079
9169
|
} catch {
|
|
9080
|
-
return { captured: [] };
|
|
9170
|
+
return { captured: [], watermarks: {} };
|
|
9081
9171
|
}
|
|
9082
9172
|
}
|
|
9083
9173
|
async function writeState2(state, env) {
|
|
@@ -9104,7 +9194,27 @@ async function markSessionCaptured(sessionId, env = process.env) {
|
|
|
9104
9194
|
if (state.captured.includes(sessionId)) return;
|
|
9105
9195
|
const captured = [...state.captured, sessionId];
|
|
9106
9196
|
if (captured.length > MAX_REMEMBERED2) captured.splice(0, captured.length - MAX_REMEMBERED2);
|
|
9107
|
-
|
|
9197
|
+
const { [sessionId]: _dropped, ...watermarks } = state.watermarks;
|
|
9198
|
+
await writeState2({ captured, watermarks }, env);
|
|
9199
|
+
}
|
|
9200
|
+
async function captureWatermark(sessionId, env = process.env) {
|
|
9201
|
+
if (!sessionId || sessionId.trim().length === 0) return 0;
|
|
9202
|
+
const state = await readState2(env);
|
|
9203
|
+
return state.watermarks[sessionId] ?? 0;
|
|
9204
|
+
}
|
|
9205
|
+
async function setCaptureWatermark(sessionId, turnCount, env = process.env) {
|
|
9206
|
+
if (!sessionId || sessionId.trim().length === 0) return;
|
|
9207
|
+
if (typeof turnCount !== "number" || !Number.isFinite(turnCount) || turnCount < 0) return;
|
|
9208
|
+
const state = await readState2(env);
|
|
9209
|
+
const prev = state.watermarks[sessionId] ?? 0;
|
|
9210
|
+
if (turnCount <= prev) return;
|
|
9211
|
+
const { [sessionId]: _old, ...rest } = state.watermarks;
|
|
9212
|
+
const watermarks = { ...rest, [sessionId]: turnCount };
|
|
9213
|
+
const keys = Object.keys(watermarks);
|
|
9214
|
+
if (keys.length > MAX_REMEMBERED2) {
|
|
9215
|
+
for (const k of keys.slice(0, keys.length - MAX_REMEMBERED2)) delete watermarks[k];
|
|
9216
|
+
}
|
|
9217
|
+
await writeState2({ captured: state.captured, watermarks }, env);
|
|
9108
9218
|
}
|
|
9109
9219
|
function spawnDetached(rawPayload, agent, deps = {}) {
|
|
9110
9220
|
const doSpawn = deps.spawnImpl ?? spawn2;
|
|
@@ -9171,12 +9281,22 @@ async function runFromHook(deps = {}) {
|
|
|
9171
9281
|
stdout: codexStdout(agent, "duplicate-session")
|
|
9172
9282
|
};
|
|
9173
9283
|
}
|
|
9284
|
+
const readWatermark = deps.watermarkImpl ?? captureWatermark;
|
|
9285
|
+
const fromTurnIndex = await readWatermark(input.session_id, env).catch(() => 0);
|
|
9174
9286
|
const run = deps.runCaptureImpl ?? runCapture;
|
|
9175
|
-
const outcome = await run(input, deps.captureDeps);
|
|
9287
|
+
const outcome = await run(input, { ...deps.captureDeps, fromTurnIndex });
|
|
9176
9288
|
if (!isTransient(outcome)) {
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9289
|
+
if (outcome.status === "no-auth") {
|
|
9290
|
+
await (deps.markCapturedImpl ?? markSessionCaptured)(input.session_id, env).catch(() => {
|
|
9291
|
+
});
|
|
9292
|
+
} else if (typeof outcome.turnCount === "number") {
|
|
9293
|
+
await (deps.setWatermarkImpl ?? setCaptureWatermark)(
|
|
9294
|
+
input.session_id,
|
|
9295
|
+
outcome.turnCount,
|
|
9296
|
+
env
|
|
9297
|
+
).catch(() => {
|
|
9298
|
+
});
|
|
9299
|
+
}
|
|
9180
9300
|
if (isTerminallyProcessed(outcome)) {
|
|
9181
9301
|
await (deps.markSweepProcessedImpl ?? markSweepProcessed)(input.session_id, env).catch(() => {
|
|
9182
9302
|
});
|
|
@@ -33531,6 +33651,9 @@ async function startMcpServer(deps = {}) {
|
|
|
33531
33651
|
var USAGE = `backthread \u2014 capture the "why" of your AI-coded changes
|
|
33532
33652
|
|
|
33533
33653
|
Usage:
|
|
33654
|
+
backthread Set up Backthread (the unified front door \u2014 same as
|
|
33655
|
+
\`backthread start\`): trust copy + one-tap auth + your next
|
|
33656
|
+
step. Idempotent. [--claim <code>]
|
|
33534
33657
|
backthread start First-run setup (backs the /backthread:start slash command):
|
|
33535
33658
|
trust copy + one-tap auth + your next step. Idempotent.
|
|
33536
33659
|
[--claim <code>]
|
|
@@ -33574,8 +33697,18 @@ function flagValue(rest, flag) {
|
|
|
33574
33697
|
if (!value || value.startsWith("--")) return void 0;
|
|
33575
33698
|
return value;
|
|
33576
33699
|
}
|
|
33577
|
-
async function
|
|
33700
|
+
async function runOnboarding(rest) {
|
|
33701
|
+
const claim = parseClaimFlag(rest);
|
|
33702
|
+
const result = await runStart({
|
|
33703
|
+
claim,
|
|
33704
|
+
device: rest.includes("--device"),
|
|
33705
|
+
entry: detectEntry({ claim })
|
|
33706
|
+
});
|
|
33707
|
+
return result.exitCode;
|
|
33708
|
+
}
|
|
33709
|
+
async function main(argv, deps = {}) {
|
|
33578
33710
|
const [command, ...rest] = argv;
|
|
33711
|
+
const onboarding = deps.runOnboardingImpl ?? runOnboarding;
|
|
33579
33712
|
switch (command) {
|
|
33580
33713
|
case "login": {
|
|
33581
33714
|
const device = rest.includes("--device");
|
|
@@ -33629,11 +33762,7 @@ async function main(argv) {
|
|
|
33629
33762
|
return null;
|
|
33630
33763
|
}
|
|
33631
33764
|
case "start": {
|
|
33632
|
-
|
|
33633
|
-
claim: parseClaimFlag(rest),
|
|
33634
|
-
device: rest.includes("--device")
|
|
33635
|
-
});
|
|
33636
|
-
return result.exitCode;
|
|
33765
|
+
return onboarding(rest);
|
|
33637
33766
|
}
|
|
33638
33767
|
case "install": {
|
|
33639
33768
|
const agentFlag = flagValue(rest, "--agent");
|
|
@@ -33651,23 +33780,48 @@ async function main(argv) {
|
|
|
33651
33780
|
});
|
|
33652
33781
|
return result.exitCode;
|
|
33653
33782
|
}
|
|
33783
|
+
case void 0:
|
|
33784
|
+
return onboarding(rest);
|
|
33654
33785
|
case "help":
|
|
33655
33786
|
case "--help":
|
|
33656
33787
|
case "-h":
|
|
33657
|
-
case void 0:
|
|
33658
33788
|
console.log(USAGE);
|
|
33659
33789
|
return 0;
|
|
33660
33790
|
default:
|
|
33791
|
+
if (command.startsWith("-")) return onboarding(argv);
|
|
33661
33792
|
console.error(`Unknown command: ${command}
|
|
33662
33793
|
|
|
33663
33794
|
${USAGE}`);
|
|
33664
33795
|
return 1;
|
|
33665
33796
|
}
|
|
33666
33797
|
}
|
|
33667
|
-
|
|
33668
|
-
|
|
33669
|
-
|
|
33670
|
-
|
|
33671
|
-
|
|
33672
|
-
|
|
33673
|
-
|
|
33798
|
+
function isEntryPoint() {
|
|
33799
|
+
try {
|
|
33800
|
+
const entry = process.argv[1];
|
|
33801
|
+
if (!entry) return false;
|
|
33802
|
+
const self = fileURLToPath2(import.meta.url);
|
|
33803
|
+
const resolve = (p) => {
|
|
33804
|
+
try {
|
|
33805
|
+
return realpathSync(p);
|
|
33806
|
+
} catch {
|
|
33807
|
+
return p;
|
|
33808
|
+
}
|
|
33809
|
+
};
|
|
33810
|
+
return resolve(self) === resolve(entry);
|
|
33811
|
+
} catch {
|
|
33812
|
+
return true;
|
|
33813
|
+
}
|
|
33814
|
+
}
|
|
33815
|
+
if (isEntryPoint()) {
|
|
33816
|
+
main(process.argv.slice(2)).then((code) => {
|
|
33817
|
+
if (code === null) return;
|
|
33818
|
+
process.exit(code);
|
|
33819
|
+
}).catch((err) => {
|
|
33820
|
+
console.error(`backthread: ${err.message ?? err}`);
|
|
33821
|
+
process.exit(1);
|
|
33822
|
+
});
|
|
33823
|
+
}
|
|
33824
|
+
export {
|
|
33825
|
+
main,
|
|
33826
|
+
runOnboarding
|
|
33827
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backthread",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Backthread helps you understand your codebase while AI ships features. The CLI captures the why behind every AI session and lets you ask how your codebase works, right from the terminal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Backthread",
|