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.
@@ -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.2.2",
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
- 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).
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
- ## Quick start
52
+ ## What `npx backthread` does
40
53
 
41
- ### Claude Code install the plugin (one click, recommended)
54
+ The bare command is the unified front door. Under the hood it:
42
55
 
43
- In Claude Code:
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. That's the whole setup.
56
-
57
- ### Any agent (or bare terminal) → `npx backthread install`
83
+ just signs you in.
58
84
 
59
- In your project:
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
- if (redacted.turns.length === 0) {
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: redacted.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
- return persistDerived(result.decisions, repo, config2, decidedAt, {
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.captured)) {
9069
- const captured = obj.captured.filter((s) => typeof s === "string");
9070
- return { captured };
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
- await writeState2({ captured }, env);
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
- const mark = deps.markCapturedImpl ?? markSessionCaptured;
9178
- await mark(input.session_id, env).catch(() => {
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 main(argv) {
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
- const result = await runStart({
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
- main(process.argv.slice(2)).then((code) => {
33668
- if (code === null) return;
33669
- process.exit(code);
33670
- }).catch((err) => {
33671
- console.error(`backthread: ${err.message ?? err}`);
33672
- process.exit(1);
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.2.2",
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",