backthread 0.1.3 → 0.1.4

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.
@@ -7395,6 +7395,19 @@ function isForeignRelativePath(p) {
7395
7395
  const stripped = p.replace(/^(?:\.[\\/])+/, "");
7396
7396
  return /^\.\.(?:[\\/]|$)/.test(stripped);
7397
7397
  }
7398
+ function normalizeRepoRelative(rel) {
7399
+ const out = [];
7400
+ for (const seg of rel.split(/[\\/]/)) {
7401
+ if (seg === "" || seg === ".") continue;
7402
+ if (seg === "..") {
7403
+ if (out.length === 0) return null;
7404
+ out.pop();
7405
+ continue;
7406
+ }
7407
+ out.push(seg);
7408
+ }
7409
+ return out.join("/");
7410
+ }
7398
7411
  function relativizeUnder(abs, root) {
7399
7412
  const trimmedRoot = root.replace(/\/+$/, "");
7400
7413
  if (trimmedRoot.length === 0) return null;
@@ -7454,10 +7467,12 @@ function sessionPaths(records, repoRoot) {
7454
7467
  if (root === null) continue;
7455
7468
  const rel = relativizeUnder(p, root);
7456
7469
  if (rel === null || rel.length === 0) continue;
7457
- seen.add(rel);
7470
+ const norm = normalizeRepoRelative(rel);
7471
+ if (norm === null || norm.length === 0) continue;
7472
+ seen.add(norm);
7458
7473
  } else if (!isForeignRelativePath(p)) {
7459
- const rel = p.replace(/^(?:\.\/)+/, "");
7460
- if (rel.length > 0) seen.add(rel);
7474
+ const norm = normalizeRepoRelative(p.replace(/^(?:\.\/)+/, ""));
7475
+ if (norm !== null && norm.length > 0) seen.add(norm);
7461
7476
  }
7462
7477
  }
7463
7478
  }
@@ -7892,7 +7907,9 @@ var TRUST_COPY = [
7892
7907
  " code blocks replaced with [code redacted] \u2014 to Backthread's Worker, never source.",
7893
7908
  " Full details: https://app.backthread.dev/security"
7894
7909
  ].join("\n");
7895
- var HOOK_COMMAND = "npx backthread capture";
7910
+ var HOOK_COMMAND = "npx backthread capture --from-hook --agent claude-code --detach";
7911
+ var LEGACY_HOOK_COMMANDS = ["npx backthread capture"];
7912
+ var OUR_HOOK_COMMANDS = /* @__PURE__ */ new Set([HOOK_COMMAND, ...LEGACY_HOOK_COMMANDS]);
7896
7913
  async function registerHook(cwd, deps = {}) {
7897
7914
  const doReadFile = deps.readFileImpl ?? ((p) => readFile3(p, "utf8"));
7898
7915
  const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile3(p, d));
@@ -7938,21 +7955,43 @@ function isNotFound2(err) {
7938
7955
  function mergeSessionEndHook(settings) {
7939
7956
  const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? { ...settings.hooks } : {};
7940
7957
  const sessionEnd = Array.isArray(hooks.SessionEnd) ? [...hooks.SessionEnd] : [];
7941
- if (sessionEnd.some(groupHasOurCommand)) return null;
7942
- sessionEnd.push({
7943
- hooks: [{ type: "command", command: HOOK_COMMAND }]
7958
+ if (sessionEnd.some((g) => groupHasCommand(g, HOOK_COMMAND))) return null;
7959
+ let migrated = false;
7960
+ const nextSessionEnd = sessionEnd.map((group) => {
7961
+ const rewritten = rewriteLegacyCommand(group);
7962
+ if (rewritten !== group) migrated = true;
7963
+ return rewritten;
7944
7964
  });
7945
- hooks.SessionEnd = sessionEnd;
7965
+ if (!migrated) {
7966
+ nextSessionEnd.push({ hooks: [{ type: "command", command: HOOK_COMMAND }] });
7967
+ }
7968
+ hooks.SessionEnd = nextSessionEnd;
7946
7969
  return { ...settings, hooks };
7947
7970
  }
7948
- function groupHasOurCommand(group) {
7971
+ function groupHasCommand(group, command) {
7949
7972
  if (!group || typeof group !== "object") return false;
7950
7973
  const inner = group.hooks;
7951
7974
  if (!Array.isArray(inner)) return false;
7952
7975
  return inner.some(
7953
- (h) => h && typeof h === "object" && h.command === HOOK_COMMAND
7976
+ (h) => h && typeof h === "object" && h.command === command
7954
7977
  );
7955
7978
  }
7979
+ function rewriteLegacyCommand(group) {
7980
+ if (!group || typeof group !== "object") return group;
7981
+ const inner = group.hooks;
7982
+ if (!Array.isArray(inner)) return group;
7983
+ let changed = false;
7984
+ const nextInner = inner.map((h) => {
7985
+ if (!h || typeof h !== "object") return h;
7986
+ const cmd = h.command;
7987
+ if (typeof cmd === "string" && cmd !== HOOK_COMMAND && OUR_HOOK_COMMANDS.has(cmd)) {
7988
+ changed = true;
7989
+ return { ...h, command: HOOK_COMMAND };
7990
+ }
7991
+ return h;
7992
+ });
7993
+ return changed ? { ...group, hooks: nextInner } : group;
7994
+ }
7956
7995
  async function runInstall(opts = {}, deps = {}) {
7957
7996
  const env = opts.env ?? process.env;
7958
7997
  const log = opts.log ?? ((m) => console.error(m));
package/hooks/hooks.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
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.",
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. The command routes through the shared `--from-hook` entrypoint with `--detach`: it reads the SessionEnd payload off stdin, then re-spawns a DETACHED worker that does the slow LOCALLY-redacted redact→infer→persist round-trip and returns immediately so a ≥30s inference can't be SIGTERM'd by CC's SessionEnd hook timeout (or reaped on session exit). Best-effort, never blocks/delays the session, always exits 0 (ARP-682). `--agent claude-code` selects the CC payload shape. 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
  {
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "npx backthread capture"
9
+ "command": "npx backthread capture --from-hook --agent claude-code --detach"
10
10
  }
11
11
  ]
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backthread",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
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",