backthread 0.4.0 → 0.5.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.4.0",
5
+ "version": "0.5.0",
6
6
  "author": {
7
7
  "name": "Backthread"
8
8
  },
@@ -7139,8 +7139,13 @@ function isNotFound(err) {
7139
7139
  // src/version.ts
7140
7140
  import { readFileSync } from "node:fs";
7141
7141
  import { fileURLToPath } from "node:url";
7142
+ import { createRequire } from "node:module";
7142
7143
  import { dirname, join as join2 } from "node:path";
7143
7144
  var VERSION_HEADER = "x-backthread-version";
7145
+ var AGENT_HEADER = "x-backthread-agent";
7146
+ var REDACT_VERSION_HEADER = "x-backthread-redact-version";
7147
+ var PLATFORM_HEADER = "x-backthread-platform";
7148
+ var NODE_HEADER = "x-backthread-node";
7144
7149
  var cached = null;
7145
7150
  function cliVersion() {
7146
7151
  if (cached !== null) return cached;
@@ -7155,8 +7160,58 @@ function cliVersion() {
7155
7160
  }
7156
7161
  return cached;
7157
7162
  }
7163
+ var cachedRedact = null;
7164
+ function redactVersion() {
7165
+ if (cachedRedact !== null) return cachedRedact;
7166
+ if ("0.1.3".length > 0) {
7167
+ cachedRedact = "0.1.3";
7168
+ return cachedRedact;
7169
+ }
7170
+ cachedRedact = readRedactVersionFromDisk();
7171
+ return cachedRedact;
7172
+ }
7173
+ function readRedactVersionFromDisk() {
7174
+ try {
7175
+ const req = createRequire(import.meta.url);
7176
+ let dir = dirname(req.resolve("@backthread/redact"));
7177
+ for (let i = 0; i < 8; i++) {
7178
+ try {
7179
+ const pkg = JSON.parse(readFileSync(join2(dir, "package.json"), "utf8"));
7180
+ if (pkg.name === "@backthread/redact" && typeof pkg.version === "string" && pkg.version.length > 0) {
7181
+ return pkg.version;
7182
+ }
7183
+ } catch {
7184
+ }
7185
+ const parent = dirname(dir);
7186
+ if (parent === dir) break;
7187
+ dir = parent;
7188
+ }
7189
+ } catch {
7190
+ }
7191
+ return "0.0.0";
7192
+ }
7193
+ function platformTag() {
7194
+ return process.platform;
7195
+ }
7196
+ var cachedNodeMajor = null;
7197
+ function nodeMajor() {
7198
+ if (cachedNodeMajor !== null) return cachedNodeMajor;
7199
+ const m = /^(\d+)/.exec(process.versions.node ?? "");
7200
+ cachedNodeMajor = m ? m[1] : "";
7201
+ return cachedNodeMajor;
7202
+ }
7203
+ var requestAgent = "unknown";
7204
+ function setRequestAgent(agent) {
7205
+ if (typeof agent === "string" && agent.trim().length > 0) requestAgent = agent.trim();
7206
+ }
7158
7207
  function versionHeaders() {
7159
- return { [VERSION_HEADER]: cliVersion() };
7208
+ return {
7209
+ [VERSION_HEADER]: cliVersion(),
7210
+ [AGENT_HEADER]: requestAgent,
7211
+ [REDACT_VERSION_HEADER]: redactVersion(),
7212
+ [PLATFORM_HEADER]: platformTag(),
7213
+ [NODE_HEADER]: nodeMajor()
7214
+ };
7160
7215
  }
7161
7216
 
7162
7217
  // src/claim.ts
@@ -7293,7 +7348,7 @@ async function ensureAuth(opts = {}) {
7293
7348
  }
7294
7349
 
7295
7350
  // src/capture.ts
7296
- import { readFile as readFile8 } from "node:fs/promises";
7351
+ import { readFile as readFile9 } from "node:fs/promises";
7297
7352
 
7298
7353
  // ../packages/redact/src/index.ts
7299
7354
  var CODE_REDACTION = "[code redacted]";
@@ -7633,13 +7688,15 @@ async function serverInfer(transcript, config2, opts = {}) {
7633
7688
  const persisted = rec.persisted === true;
7634
7689
  const sessionId = typeof rec.sessionId === "string" ? rec.sessionId : transcript.sessionId ?? null;
7635
7690
  const tokensSpent = typeof rec.tokensSpent === "number" ? rec.tokensSpent : void 0;
7691
+ const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade : void 0;
7636
7692
  return {
7637
7693
  ok: true,
7638
7694
  model: "server",
7639
7695
  decisions,
7640
7696
  persisted,
7641
7697
  sessionId,
7642
- ...tokensSpent !== void 0 ? { tokensSpent } : {}
7698
+ ...tokensSpent !== void 0 ? { tokensSpent } : {},
7699
+ ...upgrade ? { upgrade } : {}
7643
7700
  };
7644
7701
  }
7645
7702
  async function inferDecisions(transcript, config2, opts = {}) {
@@ -7745,24 +7802,80 @@ async function maybeNudge(status, repo, sessionId, deps = {}) {
7745
7802
  }
7746
7803
 
7747
7804
  // src/firstRun.ts
7748
- import { join as join9 } from "node:path";
7749
- import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir6, chmod as chmod5 } from "node:fs/promises";
7805
+ import { join as join10 } from "node:path";
7806
+ import { readFile as readFile8, writeFile as writeFile7, mkdir as mkdir7, chmod as chmod6 } from "node:fs/promises";
7750
7807
 
7751
7808
  // src/install.ts
7752
- import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir5 } from "node:fs/promises";
7809
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir6 } from "node:fs/promises";
7753
7810
  import { homedir as homedir5 } from "node:os";
7754
- import { join as join8 } from "node:path";
7811
+ import { join as join9 } from "node:path";
7755
7812
 
7756
7813
  // src/captureCommand.ts
7757
7814
  import { stat } from "node:fs/promises";
7758
7815
  import { homedir as homedir2 } from "node:os";
7816
+ import { join as join5 } from "node:path";
7817
+
7818
+ // src/upgradeNudge.ts
7759
7819
  import { join as join4 } from "node:path";
7820
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, chmod as chmod3 } from "node:fs/promises";
7821
+ function upgradeNudgeStatePath(env = process.env) {
7822
+ return join4(configDir(env), "upgrade-nudge.json");
7823
+ }
7824
+ var UPGRADE_NUDGE_THROTTLE_MS = 24 * 60 * 60 * 1e3;
7825
+ function parseState2(raw) {
7826
+ try {
7827
+ const obj = JSON.parse(raw);
7828
+ if (obj && typeof obj === "object" && !Array.isArray(obj)) {
7829
+ const at = obj.lastUpgradeNudgeAt;
7830
+ if (typeof at === "number" && Number.isFinite(at)) return { lastUpgradeNudgeAt: at };
7831
+ }
7832
+ } catch {
7833
+ }
7834
+ return {};
7835
+ }
7836
+ async function readState2(env) {
7837
+ try {
7838
+ return parseState2(await readFile3(upgradeNudgeStatePath(env), "utf8"));
7839
+ } catch {
7840
+ return {};
7841
+ }
7842
+ }
7843
+ async function writeState2(state, env) {
7844
+ try {
7845
+ const dir = configDir(env);
7846
+ await mkdir3(dir, { recursive: true, mode: DIR_MODE });
7847
+ await chmod3(dir, DIR_MODE).catch(() => {
7848
+ });
7849
+ const path = upgradeNudgeStatePath(env);
7850
+ await writeFile3(path, JSON.stringify(state) + "\n", { mode: CONFIG_MODE });
7851
+ await chmod3(path, CONFIG_MODE).catch(() => {
7852
+ });
7853
+ } catch {
7854
+ }
7855
+ }
7856
+ async function maybeUpgradeNudge(upgrade, deps = {}) {
7857
+ try {
7858
+ if (typeof upgrade !== "string" || upgrade.trim().length === 0) return null;
7859
+ const env = deps.env ?? process.env;
7860
+ const now = deps.now ? deps.now() : Date.now();
7861
+ const state = await readState2(env);
7862
+ if (typeof state.lastUpgradeNudgeAt === "number" && now - state.lastUpgradeNudgeAt < UPGRADE_NUDGE_THROTTLE_MS) {
7863
+ return null;
7864
+ }
7865
+ await writeState2({ lastUpgradeNudgeAt: now }, env);
7866
+ return upgrade.trim();
7867
+ } catch {
7868
+ return null;
7869
+ }
7870
+ }
7871
+
7872
+ // src/captureCommand.ts
7760
7873
  function slugifyCwd(cwd) {
7761
7874
  return cwd.replace(/[^A-Za-z0-9]/g, "-");
7762
7875
  }
7763
7876
  function deriveTranscriptPath(sessionId, cwd, home) {
7764
7877
  if (!sessionId || sessionId.trim().length === 0) return null;
7765
- return join4(home, ".claude", "projects", slugifyCwd(cwd), `${sessionId}.jsonl`);
7878
+ return join5(home, ".claude", "projects", slugifyCwd(cwd), `${sessionId}.jsonl`);
7766
7879
  }
7767
7880
  async function defaultStat(path) {
7768
7881
  try {
@@ -7805,7 +7918,11 @@ async function runManualCapture(input, deps = {}) {
7805
7918
  } catch (e) {
7806
7919
  return { text: `backthread capture: error \u2014 ${e.message}`, exitCode: 1, outcome: null };
7807
7920
  }
7808
- return { text: formatManualSummary(outcome), exitCode: exitCodeFor(outcome), outcome };
7921
+ let text = formatManualSummary(outcome);
7922
+ const nudge = await (deps.upgradeNudgeImpl ?? maybeUpgradeNudge)(outcome.upgrade);
7923
+ if (nudge) text += `
7924
+ ${nudge}`;
7925
+ return { text, exitCode: exitCodeFor(outcome), outcome };
7809
7926
  }
7810
7927
  function exitCodeFor(o) {
7811
7928
  if (o.status === "infer-failed" || o.status === "persist-failed" || o.status === "error") return 1;
@@ -7862,17 +7979,17 @@ function parseManualArgs(argv) {
7862
7979
  }
7863
7980
 
7864
7981
  // src/sweep.ts
7865
- import { readFile as readFile4, stat as stat2, readdir } from "node:fs/promises";
7982
+ import { readFile as readFile5, stat as stat2, readdir } from "node:fs/promises";
7866
7983
  import { execFileSync as execFileSync2 } from "node:child_process";
7867
7984
  import { homedir as homedir3 } from "node:os";
7868
- import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join6 } from "node:path";
7985
+ import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join7 } from "node:path";
7869
7986
 
7870
7987
  // src/sweepLedger.ts
7871
- import { join as join5 } from "node:path";
7872
- import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, chmod as chmod3 } from "node:fs/promises";
7988
+ import { join as join6 } from "node:path";
7989
+ import { readFile as readFile4, writeFile as writeFile4, mkdir as mkdir4, chmod as chmod4 } from "node:fs/promises";
7873
7990
  var MAX_PROCESSED = 2e4;
7874
7991
  function sweepStatePath(env = process.env) {
7875
- return join5(configDir(env), "sweep-state.json");
7992
+ return join6(configDir(env), "sweep-state.json");
7876
7993
  }
7877
7994
  function parseSweepState(raw) {
7878
7995
  try {
@@ -7897,7 +8014,7 @@ function serializeSweepState(state) {
7897
8014
  }
7898
8015
  async function readSweepState(env = process.env) {
7899
8016
  try {
7900
- return parseSweepState(await readFile3(sweepStatePath(env), "utf8"));
8017
+ return parseSweepState(await readFile4(sweepStatePath(env), "utf8"));
7901
8018
  } catch {
7902
8019
  return { processed: [], lastSweptAt: {} };
7903
8020
  }
@@ -7905,12 +8022,12 @@ async function readSweepState(env = process.env) {
7905
8022
  async function writeSweepState(state, env = process.env) {
7906
8023
  try {
7907
8024
  const dir = configDir(env);
7908
- await mkdir3(dir, { recursive: true, mode: DIR_MODE });
7909
- await chmod3(dir, DIR_MODE).catch(() => {
8025
+ await mkdir4(dir, { recursive: true, mode: DIR_MODE });
8026
+ await chmod4(dir, DIR_MODE).catch(() => {
7910
8027
  });
7911
8028
  const path = sweepStatePath(env);
7912
- await writeFile3(path, serializeSweepState(state), { mode: CONFIG_MODE });
7913
- await chmod3(path, CONFIG_MODE).catch(() => {
8029
+ await writeFile4(path, serializeSweepState(state), { mode: CONFIG_MODE });
8030
+ await chmod4(path, CONFIG_MODE).catch(() => {
7914
8031
  });
7915
8032
  } catch {
7916
8033
  }
@@ -8014,7 +8131,7 @@ function defaultMainRoot(cwd) {
8014
8131
  stdio: ["ignore", "pipe", "ignore"]
8015
8132
  }).trim();
8016
8133
  if (!out) return null;
8017
- const abs = isAbsolute2(out) ? out : join6(cwd, out);
8134
+ const abs = isAbsolute2(out) ? out : join7(cwd, out);
8018
8135
  return dirname2(abs.replace(/\/+$/, ""));
8019
8136
  } catch {
8020
8137
  return null;
@@ -8032,7 +8149,7 @@ async function runSweep(input = {}, deps = {}) {
8032
8149
  return [];
8033
8150
  }
8034
8151
  };
8035
- const baseReadFile = deps.readFileImpl ?? ((p) => readFile4(p, "utf8"));
8152
+ const baseReadFile = deps.readFileImpl ?? ((p) => readFile5(p, "utf8"));
8036
8153
  const doReadFile = async (p) => {
8037
8154
  try {
8038
8155
  return await baseReadFile(p);
@@ -8089,7 +8206,7 @@ async function runSweep(input = {}, deps = {}) {
8089
8206
  }
8090
8207
  const mainRoot = doMainRoot(cwd) ?? cwd;
8091
8208
  const mainSlug = slugifyCwd(mainRoot);
8092
- const projectsRoot = join6(home, ".claude", "projects");
8209
+ const projectsRoot = join7(home, ".claude", "projects");
8093
8210
  const entries = await doReadDir(projectsRoot);
8094
8211
  const candidates = entries.filter((n) => n === mainSlug || n.startsWith(mainSlug + "-")).sort();
8095
8212
  const skip = new Set(state.processed);
@@ -8102,12 +8219,12 @@ async function runSweep(input = {}, deps = {}) {
8102
8219
  let captured = 0;
8103
8220
  let decisions = 0;
8104
8221
  for (const dirName of candidates) {
8105
- const dir = join6(projectsRoot, dirName);
8222
+ const dir = join7(projectsRoot, dirName);
8106
8223
  const files = (await doReadDir(dir)).filter((n) => n.endsWith(".jsonl")).sort();
8107
8224
  if (files.length === 0) continue;
8108
8225
  let embeddedCwd = null;
8109
8226
  for (const file2 of files) {
8110
- embeddedCwd = extractCwdFromRaw(await doReadFile(join6(dir, file2)));
8227
+ embeddedCwd = extractCwdFromRaw(await doReadFile(join7(dir, file2)));
8111
8228
  if (embeddedCwd) break;
8112
8229
  }
8113
8230
  const cwdExists = embeddedCwd ? await doPathExists(embeddedCwd) : false;
@@ -8146,7 +8263,7 @@ async function runSweep(input = {}, deps = {}) {
8146
8263
  try {
8147
8264
  outcome = await run(
8148
8265
  {
8149
- transcript_path: join6(dir, file2),
8266
+ transcript_path: join7(dir, file2),
8150
8267
  cwd: cls.cwd ?? mainRoot,
8151
8268
  session_id: sid,
8152
8269
  hook_event_name: "SessionEnd"
@@ -8210,8 +8327,8 @@ async function runBackfill(input = {}, deps = {}) {
8210
8327
  import { execFile } from "node:child_process";
8211
8328
  import { promisify } from "node:util";
8212
8329
  import { homedir as homedir4 } from "node:os";
8213
- import { join as join7, dirname as dirname3 } from "node:path";
8214
- import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4, chmod as chmod4 } from "node:fs/promises";
8330
+ import { join as join8, dirname as dirname3 } from "node:path";
8331
+ import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir5, chmod as chmod5 } from "node:fs/promises";
8215
8332
  var execFileP = promisify(execFile);
8216
8333
  var MCP_COMMAND = "npx";
8217
8334
  var MCP_ARGS = ["-y", "backthread", "mcp"];
@@ -8278,14 +8395,14 @@ function withNestedHook(settings, event, command, extra = {}) {
8278
8395
  return { next: { ...settings, hooks }, changed: true };
8279
8396
  }
8280
8397
  async function writeJson(deps, path, obj) {
8281
- const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir4(d, { recursive: true }));
8282
- const doWrite = deps.writeFileImpl ?? ((p, d) => writeFile4(p, d));
8398
+ const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir5(d, { recursive: true }));
8399
+ const doWrite = deps.writeFileImpl ?? ((p, d) => writeFile5(p, d));
8283
8400
  await doMkdir(dirname3(path));
8284
8401
  await doWrite(path, JSON.stringify(obj, null, 2) + "\n");
8285
8402
  }
8286
8403
  async function installGemini(home, deps) {
8287
- const doRead = deps.readFileImpl ?? ((p) => readFile5(p, "utf8"));
8288
- const path = join7(home, ".gemini", "settings.json");
8404
+ const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
8405
+ const path = join8(home, ".gemini", "settings.json");
8289
8406
  const current = await loadJsonObject(doRead, path);
8290
8407
  const a = withMcpServer(current);
8291
8408
  const b = withNestedHook(a.next, "SessionEnd", hookCommand("gemini-cli"), { name: "backthread-capture" });
@@ -8293,9 +8410,9 @@ async function installGemini(home, deps) {
8293
8410
  return [{ path, wrote: a.changed || b.changed }];
8294
8411
  }
8295
8412
  async function installCodex(home, deps) {
8296
- const doRead = deps.readFileImpl ?? ((p) => readFile5(p, "utf8"));
8413
+ const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
8297
8414
  const writes = [];
8298
- const tomlPath = join7(home, ".codex", "config.toml");
8415
+ const tomlPath = join8(home, ".codex", "config.toml");
8299
8416
  let toml = "";
8300
8417
  try {
8301
8418
  toml = await doRead(tomlPath);
@@ -8310,13 +8427,13 @@ command = "${MCP_COMMAND}"
8310
8427
  args = [${MCP_ARGS.map((a) => `"${a}"`).join(", ")}]
8311
8428
  `;
8312
8429
  const sep = toml.length === 0 ? "" : toml.endsWith("\n") ? "\n" : "\n\n";
8313
- const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir4(d, { recursive: true }));
8314
- const doWrite = deps.writeFileImpl ?? ((p, d) => writeFile4(p, d));
8430
+ const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir5(d, { recursive: true }));
8431
+ const doWrite = deps.writeFileImpl ?? ((p, d) => writeFile5(p, d));
8315
8432
  await doMkdir(dirname3(tomlPath));
8316
8433
  await doWrite(tomlPath, toml + sep + block);
8317
8434
  writes.push({ path: tomlPath, wrote: true });
8318
8435
  }
8319
- const hooksPath = join7(home, ".codex", "hooks.json");
8436
+ const hooksPath = join8(home, ".codex", "hooks.json");
8320
8437
  const current = await loadJsonObject(doRead, hooksPath);
8321
8438
  const h = withNestedHook(current, "Stop", hookCommand("codex"), { timeout: 60 });
8322
8439
  if (h.changed) await writeJson(deps, hooksPath, h.next);
@@ -8324,12 +8441,12 @@ args = [${MCP_ARGS.map((a) => `"${a}"`).join(", ")}]
8324
8441
  return writes;
8325
8442
  }
8326
8443
  async function installCursor(home, deps) {
8327
- const doRead = deps.readFileImpl ?? ((p) => readFile5(p, "utf8"));
8444
+ const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
8328
8445
  const nodeBinDir = deps.nodeBinDir ?? dirname3(process.execPath);
8329
8446
  const writes = [];
8330
- const scriptDir = join7(home, ".cursor", "hooks");
8331
- const captureScriptPath = join7(scriptDir, "backthread-capture.sh");
8332
- const mcpScriptPath = join7(scriptDir, "backthread-mcp.sh");
8447
+ const scriptDir = join8(home, ".cursor", "hooks");
8448
+ const captureScriptPath = join8(scriptDir, "backthread-capture.sh");
8449
+ const mcpScriptPath = join8(scriptDir, "backthread-mcp.sh");
8333
8450
  writes.push(
8334
8451
  await writeCursorScript(
8335
8452
  deps,
@@ -8338,12 +8455,12 @@ async function installCursor(home, deps) {
8338
8455
  )
8339
8456
  );
8340
8457
  writes.push(await writeCursorScript(deps, mcpScriptPath, cursorWrapperScript(nodeBinDir, "mcp")));
8341
- const mcpPath = join7(home, ".cursor", "mcp.json");
8458
+ const mcpPath = join8(home, ".cursor", "mcp.json");
8342
8459
  const mcpCurrent = await loadJsonObject(doRead, mcpPath);
8343
8460
  const m = withCursorMcpServer(mcpCurrent, mcpScriptPath);
8344
8461
  if (m.changed) await writeJson(deps, mcpPath, m.next);
8345
8462
  writes.push({ path: mcpPath, wrote: m.changed });
8346
- const hooksPath = join7(home, ".cursor", "hooks.json");
8463
+ const hooksPath = join8(home, ".cursor", "hooks.json");
8347
8464
  const hooksCurrent = await loadJsonObject(doRead, hooksPath);
8348
8465
  const c = withCursorStopHook(hooksCurrent, captureScriptPath);
8349
8466
  if (c.changed) await writeJson(deps, hooksPath, c.next);
@@ -8376,8 +8493,8 @@ function cursorWrapperScript(nodeBinDir, backthreadArgs) {
8376
8493
  ].join("\n") + "\n";
8377
8494
  }
8378
8495
  async function writeCursorScript(deps, path, content) {
8379
- const doRead = deps.readFileImpl ?? ((p) => readFile5(p, "utf8"));
8380
- const doChmod = deps.chmodImpl ?? ((p, mode) => chmod4(p, mode));
8496
+ const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
8497
+ const doChmod = deps.chmodImpl ?? ((p, mode) => chmod5(p, mode));
8381
8498
  let existing = null;
8382
8499
  try {
8383
8500
  existing = await doRead(path);
@@ -8389,8 +8506,8 @@ async function writeCursorScript(deps, path, content) {
8389
8506
  });
8390
8507
  return { path, wrote: false };
8391
8508
  }
8392
- const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir4(d, { recursive: true }));
8393
- const doWrite = deps.writeFileImpl ?? ((p, d) => writeFile4(p, d));
8509
+ const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir5(d, { recursive: true }));
8510
+ const doWrite = deps.writeFileImpl ?? ((p, d) => writeFile5(p, d));
8394
8511
  await doMkdir(dirname3(path));
8395
8512
  await doWrite(path, content);
8396
8513
  await doChmod(path, 493);
@@ -8485,16 +8602,19 @@ var TRUST_COPY = [
8485
8602
  " code blocks replaced with [code redacted] \u2014 to Backthread's Worker, never source.",
8486
8603
  " Full details: https://app.backthread.dev/security"
8487
8604
  ].join("\n");
8488
- var HOOK_COMMAND = "npx backthread capture --from-hook --agent claude-code --detach";
8489
- var LEGACY_HOOK_COMMANDS = ["npx backthread capture"];
8605
+ var HOOK_COMMAND = "npx backthread@latest capture --from-hook --agent claude-code --detach";
8606
+ var LEGACY_HOOK_COMMANDS = [
8607
+ "npx backthread capture",
8608
+ "npx backthread capture --from-hook --agent claude-code --detach"
8609
+ ];
8490
8610
  var OUR_HOOK_COMMANDS = /* @__PURE__ */ new Set([HOOK_COMMAND, ...LEGACY_HOOK_COMMANDS]);
8491
8611
  async function registerHook(deps = {}) {
8492
- const doReadFile = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
8493
- const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile5(p, d));
8494
- const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir5(d, { recursive: true }));
8612
+ const doReadFile = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
8613
+ const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile6(p, d));
8614
+ const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir6(d, { recursive: true }));
8495
8615
  const home = deps.home ?? homedir5();
8496
- const settingsDir = join8(home, ".claude");
8497
- const settingsPath = join8(settingsDir, "settings.json");
8616
+ const settingsDir = join9(home, ".claude");
8617
+ const settingsPath = join9(settingsDir, "settings.json");
8498
8618
  let settings = {};
8499
8619
  let raw = null;
8500
8620
  try {
@@ -8606,9 +8726,9 @@ function stripSessionEndHook(settings) {
8606
8726
  return next;
8607
8727
  }
8608
8728
  async function unregisterProjectHook(cwd, deps = {}) {
8609
- const doReadFile = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
8610
- const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile5(p, d));
8611
- const settingsPath = join8(cwd, ".claude", "settings.json");
8729
+ const doReadFile = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
8730
+ const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile6(p, d));
8731
+ const settingsPath = join9(cwd, ".claude", "settings.json");
8612
8732
  let raw;
8613
8733
  try {
8614
8734
  raw = await doReadFile(settingsPath);
@@ -8873,7 +8993,7 @@ function normalizeState(raw) {
8873
8993
 
8874
8994
  // src/firstRun.ts
8875
8995
  function firstRunStatePath(env = process.env) {
8876
- return join9(configDir(env), "first-run.json");
8996
+ return join10(configDir(env), "first-run.json");
8877
8997
  }
8878
8998
  function parseFirstRunState(raw) {
8879
8999
  try {
@@ -8892,7 +9012,7 @@ function parseFirstRunState(raw) {
8892
9012
  }
8893
9013
  async function readFirstRunState(env = process.env) {
8894
9014
  try {
8895
- return parseFirstRunState(await readFile7(firstRunStatePath(env), "utf8"));
9015
+ return parseFirstRunState(await readFile8(firstRunStatePath(env), "utf8"));
8896
9016
  } catch {
8897
9017
  return {};
8898
9018
  }
@@ -8902,12 +9022,12 @@ async function updateFirstRunState(patch, env = process.env) {
8902
9022
  const current = await readFirstRunState(env);
8903
9023
  const next = { ...current, ...patch };
8904
9024
  const dir = configDir(env);
8905
- await mkdir6(dir, { recursive: true, mode: DIR_MODE });
8906
- await chmod5(dir, DIR_MODE).catch(() => {
9025
+ await mkdir7(dir, { recursive: true, mode: DIR_MODE });
9026
+ await chmod6(dir, DIR_MODE).catch(() => {
8907
9027
  });
8908
9028
  const path = firstRunStatePath(env);
8909
- await writeFile6(path, JSON.stringify(next) + "\n", { mode: CONFIG_MODE });
8910
- await chmod5(path, CONFIG_MODE).catch(() => {
9029
+ await writeFile7(path, JSON.stringify(next) + "\n", { mode: CONFIG_MODE });
9030
+ await chmod6(path, CONFIG_MODE).catch(() => {
8911
9031
  });
8912
9032
  } catch {
8913
9033
  }
@@ -8915,13 +9035,13 @@ async function updateFirstRunState(patch, env = process.env) {
8915
9035
  async function maybeShowTrustGate(deps = {}) {
8916
9036
  try {
8917
9037
  const env = deps.env ?? process.env;
8918
- const readState3 = deps.readStateImpl ?? readFirstRunState;
8919
- const state = await readState3(env);
9038
+ const readState4 = deps.readStateImpl ?? readFirstRunState;
9039
+ const state = await readState4(env);
8920
9040
  if (state.onboarded === true || state.trustShown === true) return false;
8921
9041
  const log = deps.log ?? ((m) => console.error(m));
8922
9042
  log(TRUST_COPY);
8923
- const writeState3 = deps.writeStateImpl ?? updateFirstRunState;
8924
- await writeState3({ trustShown: true }, env);
9043
+ const writeState4 = deps.writeStateImpl ?? updateFirstRunState;
9044
+ await writeState4({ trustShown: true }, env);
8925
9045
  return true;
8926
9046
  } catch {
8927
9047
  return false;
@@ -8932,8 +9052,8 @@ async function runStart(opts = {}, deps = {}) {
8932
9052
  const log = opts.log ?? ((m) => console.error(m));
8933
9053
  const cwd = opts.cwd ?? process.cwd();
8934
9054
  const entry = opts.entry ?? detectEntry({ claim: opts.claim, env });
8935
- const readState3 = deps.readStateImpl ?? readFirstRunState;
8936
- const existingState = await readState3(env).catch(() => ({}));
9055
+ const readState4 = deps.readStateImpl ?? readFirstRunState;
9056
+ const existingState = await readState4(env).catch(() => ({}));
8937
9057
  if (existingState.onboarded === true) {
8938
9058
  const readCfg = deps.readConfigImpl ?? readConfig;
8939
9059
  const cfg = await readCfg(env).catch(() => ({}));
@@ -8982,8 +9102,8 @@ async function runStart(opts = {}, deps = {}) {
8982
9102
  () => ({ status: "error", detail: "state fetch failed (swallowed)" })
8983
9103
  );
8984
9104
  log("\n" + renderNextStep(stateOut, env));
8985
- const writeState3 = deps.writeStateImpl ?? updateFirstRunState;
8986
- await writeState3({ onboarded: true, trustShown: true }, env);
9105
+ const writeState4 = deps.writeStateImpl ?? updateFirstRunState;
9106
+ await writeState4({ onboarded: true, trustShown: true }, env);
8987
9107
  return { exitCode: 0, status: "onboarded", authed: true };
8988
9108
  }
8989
9109
  function renderNextStep(out, env = process.env) {
@@ -9024,13 +9144,13 @@ async function maybeFirstCaptureConfirm(count, repoConnected, repo, deps = {}) {
9024
9144
  if (!repoConnected || !repo) return false;
9025
9145
  if (!(count > 0)) return false;
9026
9146
  const env = deps.env ?? process.env;
9027
- const readState3 = deps.readStateImpl ?? readFirstRunState;
9028
- const state = await readState3(env);
9147
+ const readState4 = deps.readStateImpl ?? readFirstRunState;
9148
+ const state = await readState4(env);
9029
9149
  if (state.firstCaptureShown === true) return false;
9030
9150
  const log = deps.log ?? ((m) => console.error(m));
9031
9151
  log(firstCaptureMessage(count, repo, env));
9032
- const writeState3 = deps.writeStateImpl ?? updateFirstRunState;
9033
- await writeState3({ firstCaptureShown: true }, env);
9152
+ const writeState4 = deps.writeStateImpl ?? updateFirstRunState;
9153
+ await writeState4({ firstCaptureShown: true }, env);
9034
9154
  return true;
9035
9155
  } catch {
9036
9156
  return false;
@@ -9067,7 +9187,7 @@ function readStream(stream) {
9067
9187
  async function runCapture(input, deps = {}) {
9068
9188
  const env = deps.env ?? process.env;
9069
9189
  const log = deps.log ?? ((m) => console.error(m));
9070
- const doReadFile = deps.readFileImpl ?? ((p) => readFile8(p, "utf8"));
9190
+ const doReadFile = deps.readFileImpl ?? ((p) => readFile9(p, "utf8"));
9071
9191
  const doReadConfig = deps.readConfigImpl ?? readConfig;
9072
9192
  const fireEnsureAuth = deps.ensureAuthImpl ?? ((e) => {
9073
9193
  void ensureAuth({ env: e }).catch(() => {
@@ -9129,6 +9249,7 @@ async function runCapture(input, deps = {}) {
9129
9249
  if (!result.ok) {
9130
9250
  return { status: "infer-failed", detail: result.error ?? "inference failed (no detail)." };
9131
9251
  }
9252
+ const inferUpgrade = result.upgrade;
9132
9253
  if (result.persisted) {
9133
9254
  const confirm = deps.firstCaptureConfirmImpl ?? maybeFirstCaptureConfirm;
9134
9255
  await confirm(result.decisions.length, true, repo, { env, log }).catch(() => false);
@@ -9137,7 +9258,8 @@ async function runCapture(input, deps = {}) {
9137
9258
  detail: `inference router persisted ${result.decisions.length} decision(s) server-side.`,
9138
9259
  count: result.decisions.length,
9139
9260
  repoConnected: true,
9140
- turnCount
9261
+ turnCount,
9262
+ ...inferUpgrade ? { upgrade: inferUpgrade } : {}
9141
9263
  };
9142
9264
  }
9143
9265
  if (result.decisions.length === 0) {
@@ -9145,7 +9267,8 @@ async function runCapture(input, deps = {}) {
9145
9267
  status: "nothing-to-capture",
9146
9268
  detail: "inference returned no decisions for this session.",
9147
9269
  count: 0,
9148
- turnCount
9270
+ turnCount,
9271
+ ...inferUpgrade ? { upgrade: inferUpgrade } : {}
9149
9272
  };
9150
9273
  }
9151
9274
  if (!repo) {
@@ -9153,7 +9276,8 @@ async function runCapture(input, deps = {}) {
9153
9276
  status: "nothing-to-capture",
9154
9277
  detail: "derived decisions but could not resolve a repo from cwd (no git remote) \u2014 nothing to claim them under; skipped.",
9155
9278
  count: 0,
9156
- turnCount
9279
+ turnCount,
9280
+ ...inferUpgrade ? { upgrade: inferUpgrade } : {}
9157
9281
  };
9158
9282
  }
9159
9283
  const out = await persistDerived(result.decisions, repo, config2, decidedAt, {
@@ -9223,7 +9347,7 @@ async function persistDerived(decisions, repo, config2, decidedAt, ctx) {
9223
9347
  const rec = payload && typeof payload === "object" ? payload : {};
9224
9348
  const count = typeof rec.count === "number" ? rec.count : decisions.length;
9225
9349
  const repoConnected = rec.repoConnected === true;
9226
- const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade : null;
9350
+ const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade : void 0;
9227
9351
  const base = repoConnected ? `captured ${count} decision(s) to ${repo.owner}/${repo.name}.` : `captured ${count} decision(s) (repo not yet connected \u2014 held as pending).`;
9228
9352
  await maybeNudge(parseRepoStatus(rec.repoStatus), repo, ctx.sessionId, {
9229
9353
  env: ctx.env,
@@ -9234,16 +9358,17 @@ async function persistDerived(decisions, repo, config2, decidedAt, ctx) {
9234
9358
  await confirm(count, repoConnected, repo, { env: ctx.env, log: ctx.log }).catch(() => false);
9235
9359
  return {
9236
9360
  status: "persisted",
9237
- detail: upgrade ? `${base} ${upgrade}` : base,
9361
+ detail: base,
9238
9362
  count,
9239
- repoConnected
9363
+ repoConnected,
9364
+ ...upgrade ? { upgrade } : {}
9240
9365
  };
9241
9366
  }
9242
9367
 
9243
9368
  // src/fromHook.ts
9244
9369
  import { spawn as spawn2 } from "node:child_process";
9245
- import { join as join10 } from "node:path";
9246
- import { readFile as readFile9, writeFile as writeFile7, mkdir as mkdir7, chmod as chmod6 } from "node:fs/promises";
9370
+ import { join as join11 } from "node:path";
9371
+ import { readFile as readFile10, writeFile as writeFile8, mkdir as mkdir8, chmod as chmod7 } from "node:fs/promises";
9247
9372
  var KNOWN_AGENTS = /* @__PURE__ */ new Set([
9248
9373
  "claude-code",
9249
9374
  "codex",
@@ -9284,10 +9409,10 @@ function normalizeHookInput(payload, _agent) {
9284
9409
  return out;
9285
9410
  }
9286
9411
  function captureStatePath(env = process.env) {
9287
- return join10(configDir(env), "capture-sessions.json");
9412
+ return join11(configDir(env), "capture-sessions.json");
9288
9413
  }
9289
9414
  var MAX_REMEMBERED2 = 200;
9290
- function parseState2(raw) {
9415
+ function parseState3(raw) {
9291
9416
  try {
9292
9417
  const obj = JSON.parse(raw);
9293
9418
  if (obj && typeof obj === "object" && !Array.isArray(obj)) {
@@ -9305,49 +9430,49 @@ function parseState2(raw) {
9305
9430
  }
9306
9431
  return { captured: [], watermarks: {} };
9307
9432
  }
9308
- async function readState2(env) {
9433
+ async function readState3(env) {
9309
9434
  try {
9310
- return parseState2(await readFile9(captureStatePath(env), "utf8"));
9435
+ return parseState3(await readFile10(captureStatePath(env), "utf8"));
9311
9436
  } catch {
9312
9437
  return { captured: [], watermarks: {} };
9313
9438
  }
9314
9439
  }
9315
- async function writeState2(state, env) {
9440
+ async function writeState3(state, env) {
9316
9441
  try {
9317
9442
  const dir = configDir(env);
9318
- await mkdir7(dir, { recursive: true, mode: DIR_MODE });
9319
- await chmod6(dir, DIR_MODE).catch(() => {
9443
+ await mkdir8(dir, { recursive: true, mode: DIR_MODE });
9444
+ await chmod7(dir, DIR_MODE).catch(() => {
9320
9445
  });
9321
9446
  const path = captureStatePath(env);
9322
- await writeFile7(path, JSON.stringify(state) + "\n", { mode: CONFIG_MODE });
9323
- await chmod6(path, CONFIG_MODE).catch(() => {
9447
+ await writeFile8(path, JSON.stringify(state) + "\n", { mode: CONFIG_MODE });
9448
+ await chmod7(path, CONFIG_MODE).catch(() => {
9324
9449
  });
9325
9450
  } catch {
9326
9451
  }
9327
9452
  }
9328
9453
  async function wasSessionCaptured(sessionId, env = process.env) {
9329
9454
  if (!sessionId || sessionId.trim().length === 0) return false;
9330
- const state = await readState2(env);
9455
+ const state = await readState3(env);
9331
9456
  return state.captured.includes(sessionId);
9332
9457
  }
9333
9458
  async function markSessionCaptured(sessionId, env = process.env) {
9334
9459
  if (!sessionId || sessionId.trim().length === 0) return;
9335
- const state = await readState2(env);
9460
+ const state = await readState3(env);
9336
9461
  if (state.captured.includes(sessionId)) return;
9337
9462
  const captured = [...state.captured, sessionId];
9338
9463
  if (captured.length > MAX_REMEMBERED2) captured.splice(0, captured.length - MAX_REMEMBERED2);
9339
9464
  const { [sessionId]: _dropped, ...watermarks } = state.watermarks;
9340
- await writeState2({ captured, watermarks }, env);
9465
+ await writeState3({ captured, watermarks }, env);
9341
9466
  }
9342
9467
  async function captureWatermark(sessionId, env = process.env) {
9343
9468
  if (!sessionId || sessionId.trim().length === 0) return 0;
9344
- const state = await readState2(env);
9469
+ const state = await readState3(env);
9345
9470
  return state.watermarks[sessionId] ?? 0;
9346
9471
  }
9347
9472
  async function setCaptureWatermark(sessionId, turnCount, env = process.env) {
9348
9473
  if (!sessionId || sessionId.trim().length === 0) return;
9349
9474
  if (typeof turnCount !== "number" || !Number.isFinite(turnCount) || turnCount < 0) return;
9350
- const state = await readState2(env);
9475
+ const state = await readState3(env);
9351
9476
  const prev = state.watermarks[sessionId] ?? 0;
9352
9477
  if (turnCount <= prev) return;
9353
9478
  const { [sessionId]: _old, ...rest } = state.watermarks;
@@ -9356,7 +9481,7 @@ async function setCaptureWatermark(sessionId, turnCount, env = process.env) {
9356
9481
  if (keys.length > MAX_REMEMBERED2) {
9357
9482
  for (const k of keys.slice(0, keys.length - MAX_REMEMBERED2)) delete watermarks[k];
9358
9483
  }
9359
- await writeState2({ captured: state.captured, watermarks }, env);
9484
+ await writeState3({ captured: state.captured, watermarks }, env);
9360
9485
  }
9361
9486
  function spawnDetached(rawPayload, agent, deps = {}) {
9362
9487
  const doSpawn = deps.spawnImpl ?? spawn2;
@@ -33629,15 +33754,16 @@ async function queryDecisions(input, deps = {}) {
33629
33754
  const rec = payload && typeof payload === "object" ? payload : {};
33630
33755
  const flows = normalizeFlows(rec.flows);
33631
33756
  const decisions = normalizeDecisions(rec.decisions);
33632
- const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade : null;
33757
+ const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade : void 0;
33633
33758
  const base = `${flows.length} flow(s), ${decisions.length} decision(s) for ${repo.owner}/${repo.name}.`;
33634
33759
  return {
33635
33760
  status: "ok",
33636
- detail: upgrade ? `${base} ${upgrade}` : base,
33761
+ detail: base,
33637
33762
  repo,
33638
33763
  flows,
33639
33764
  decisions,
33640
- deepLink
33765
+ deepLink,
33766
+ ...upgrade ? { upgrade } : {}
33641
33767
  };
33642
33768
  } catch (e) {
33643
33769
  return { status: "error", detail: `query failed (swallowed): ${e.message}` };
@@ -33705,7 +33831,12 @@ async function handleQueryTool(args = {}, deps = {}) {
33705
33831
  const run = deps.queryDecisionsImpl ?? queryDecisions;
33706
33832
  try {
33707
33833
  const outcome = await run({ repo: args.repo, cwd: args.cwd }, deps.queryDeps);
33708
- return textResult(formatQueryOutcome(outcome, args.question), outcome.status !== "ok");
33834
+ let text = formatQueryOutcome(outcome, args.question);
33835
+ const nudge = await (deps.upgradeNudgeImpl ?? maybeUpgradeNudge)(outcome.upgrade);
33836
+ if (nudge) text += `
33837
+
33838
+ ${nudge}`;
33839
+ return textResult(text, outcome.status !== "ok");
33709
33840
  } catch (e) {
33710
33841
  return textResult(`query: error \u2014 ${e.message}`, true);
33711
33842
  }
@@ -33872,9 +34003,11 @@ async function main(argv, deps = {}) {
33872
34003
  if (rest.includes("--from-hook")) {
33873
34004
  const raw = await readRawHookInput();
33874
34005
  const detach = rest.includes("--detach") && !rest.includes("--no-detach");
34006
+ const agent = parseAgent(flagValue(rest, "--agent"));
34007
+ setRequestAgent(agent);
33875
34008
  const result = await runFromHook({
33876
34009
  rawPayload: raw,
33877
- agent: parseAgent(flagValue(rest, "--agent")),
34010
+ agent,
33878
34011
  detach
33879
34012
  });
33880
34013
  if (result.stdout) console.log(JSON.stringify(result.stdout));
@@ -33891,6 +34024,7 @@ async function main(argv, deps = {}) {
33891
34024
  return result.exitCode;
33892
34025
  }
33893
34026
  try {
34027
+ setRequestAgent("claude-code");
33894
34028
  const hookInput = await readHookInput();
33895
34029
  const outcome = await runCapture(hookInput);
33896
34030
  console.error(`backthread capture: ${outcome.status} \u2014 ${outcome.detail}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backthread",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",