backthread 0.1.2 → 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.
- package/dist-bundle/backthread.js +132 -7
- package/hooks/hooks.json +2 -2
- package/package.json +1 -1
|
@@ -7380,6 +7380,104 @@ function sessionTimestamp(records) {
|
|
|
7380
7380
|
}
|
|
7381
7381
|
return latestIso;
|
|
7382
7382
|
}
|
|
7383
|
+
function stripLeadingSlashes(p) {
|
|
7384
|
+
let i = 0;
|
|
7385
|
+
while (i < p.length && p[i] === "/") i += 1;
|
|
7386
|
+
return p.slice(i);
|
|
7387
|
+
}
|
|
7388
|
+
function isAbsolute(p) {
|
|
7389
|
+
return p.startsWith("/");
|
|
7390
|
+
}
|
|
7391
|
+
function isForeignRelativePath(p) {
|
|
7392
|
+
if (p.startsWith("~")) return true;
|
|
7393
|
+
if (p.startsWith("\\")) return true;
|
|
7394
|
+
if (/^[A-Za-z]:[\\/]/.test(p)) return true;
|
|
7395
|
+
const stripped = p.replace(/^(?:\.[\\/])+/, "");
|
|
7396
|
+
return /^\.\.(?:[\\/]|$)/.test(stripped);
|
|
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
|
+
}
|
|
7411
|
+
function relativizeUnder(abs, root) {
|
|
7412
|
+
const trimmedRoot = root.replace(/\/+$/, "");
|
|
7413
|
+
if (trimmedRoot.length === 0) return null;
|
|
7414
|
+
if (abs === trimmedRoot) return "";
|
|
7415
|
+
const prefix = trimmedRoot + "/";
|
|
7416
|
+
if (!abs.startsWith(prefix)) return null;
|
|
7417
|
+
return stripLeadingSlashes(abs.slice(trimmedRoot.length));
|
|
7418
|
+
}
|
|
7419
|
+
function pathsFromRecord(rec) {
|
|
7420
|
+
if (!rec || typeof rec !== "object") return [];
|
|
7421
|
+
const out = [];
|
|
7422
|
+
const r = rec;
|
|
7423
|
+
const pushFromInput = (input) => {
|
|
7424
|
+
if (!input || typeof input !== "object") return;
|
|
7425
|
+
const i = input;
|
|
7426
|
+
for (const v of [i.file_path, i.path, i.notebook_path, i.cwd]) {
|
|
7427
|
+
if (typeof v === "string" && v.trim().length > 0) out.push(v.trim());
|
|
7428
|
+
}
|
|
7429
|
+
};
|
|
7430
|
+
const content = r.message?.content;
|
|
7431
|
+
if (Array.isArray(content)) {
|
|
7432
|
+
for (const raw of content) {
|
|
7433
|
+
if (!raw || typeof raw !== "object") continue;
|
|
7434
|
+
const block = raw;
|
|
7435
|
+
if (block.type === "tool_use") pushFromInput(block.input);
|
|
7436
|
+
}
|
|
7437
|
+
}
|
|
7438
|
+
if (r.payload && typeof r.payload === "object" && r.payload.type === "function_call") {
|
|
7439
|
+
const args = r.payload.arguments;
|
|
7440
|
+
if (typeof args === "string") {
|
|
7441
|
+
try {
|
|
7442
|
+
pushFromInput(JSON.parse(args));
|
|
7443
|
+
} catch {
|
|
7444
|
+
}
|
|
7445
|
+
} else {
|
|
7446
|
+
pushFromInput(args);
|
|
7447
|
+
}
|
|
7448
|
+
}
|
|
7449
|
+
return out;
|
|
7450
|
+
}
|
|
7451
|
+
function codexSessionCwd(records) {
|
|
7452
|
+
for (const raw of records) {
|
|
7453
|
+
if (!raw || typeof raw !== "object") continue;
|
|
7454
|
+
const rec = raw;
|
|
7455
|
+
if (rec.type !== "session_meta") continue;
|
|
7456
|
+
const cwd = rec.payload?.cwd;
|
|
7457
|
+
if (typeof cwd === "string" && cwd.trim().length > 0) return cwd.trim();
|
|
7458
|
+
}
|
|
7459
|
+
return null;
|
|
7460
|
+
}
|
|
7461
|
+
function sessionPaths(records, repoRoot) {
|
|
7462
|
+
const root = (repoRoot && repoRoot.trim().length > 0 ? repoRoot.trim() : codexSessionCwd(records)) ?? null;
|
|
7463
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7464
|
+
for (const rec of records) {
|
|
7465
|
+
for (const p of pathsFromRecord(rec)) {
|
|
7466
|
+
if (isAbsolute(p)) {
|
|
7467
|
+
if (root === null) continue;
|
|
7468
|
+
const rel = relativizeUnder(p, root);
|
|
7469
|
+
if (rel === null || rel.length === 0) continue;
|
|
7470
|
+
const norm = normalizeRepoRelative(rel);
|
|
7471
|
+
if (norm === null || norm.length === 0) continue;
|
|
7472
|
+
seen.add(norm);
|
|
7473
|
+
} else if (!isForeignRelativePath(p)) {
|
|
7474
|
+
const norm = normalizeRepoRelative(p.replace(/^(?:\.\/)+/, ""));
|
|
7475
|
+
if (norm !== null && norm.length > 0) seen.add(norm);
|
|
7476
|
+
}
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
return Array.from(seen).sort();
|
|
7480
|
+
}
|
|
7383
7481
|
|
|
7384
7482
|
// src/repo.ts
|
|
7385
7483
|
import { execFileSync } from "node:child_process";
|
|
@@ -7459,6 +7557,7 @@ async function serverInfer(transcript, config2, opts = {}) {
|
|
|
7459
7557
|
body.persist = true;
|
|
7460
7558
|
body.repo = { owner: opts.repo.owner, name: opts.repo.name };
|
|
7461
7559
|
if (opts.decidedAt) body.decidedAt = opts.decidedAt;
|
|
7560
|
+
if (opts.filePaths && opts.filePaths.length > 0) body.filePaths = opts.filePaths;
|
|
7462
7561
|
}
|
|
7463
7562
|
let res;
|
|
7464
7563
|
try {
|
|
@@ -7808,7 +7907,9 @@ var TRUST_COPY = [
|
|
|
7808
7907
|
" code blocks replaced with [code redacted] \u2014 to Backthread's Worker, never source.",
|
|
7809
7908
|
" Full details: https://app.backthread.dev/security"
|
|
7810
7909
|
].join("\n");
|
|
7811
|
-
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]);
|
|
7812
7913
|
async function registerHook(cwd, deps = {}) {
|
|
7813
7914
|
const doReadFile = deps.readFileImpl ?? ((p) => readFile3(p, "utf8"));
|
|
7814
7915
|
const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile3(p, d));
|
|
@@ -7854,21 +7955,43 @@ function isNotFound2(err) {
|
|
|
7854
7955
|
function mergeSessionEndHook(settings) {
|
|
7855
7956
|
const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? { ...settings.hooks } : {};
|
|
7856
7957
|
const sessionEnd = Array.isArray(hooks.SessionEnd) ? [...hooks.SessionEnd] : [];
|
|
7857
|
-
if (sessionEnd.some(
|
|
7858
|
-
|
|
7859
|
-
|
|
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;
|
|
7860
7964
|
});
|
|
7861
|
-
|
|
7965
|
+
if (!migrated) {
|
|
7966
|
+
nextSessionEnd.push({ hooks: [{ type: "command", command: HOOK_COMMAND }] });
|
|
7967
|
+
}
|
|
7968
|
+
hooks.SessionEnd = nextSessionEnd;
|
|
7862
7969
|
return { ...settings, hooks };
|
|
7863
7970
|
}
|
|
7864
|
-
function
|
|
7971
|
+
function groupHasCommand(group, command) {
|
|
7865
7972
|
if (!group || typeof group !== "object") return false;
|
|
7866
7973
|
const inner = group.hooks;
|
|
7867
7974
|
if (!Array.isArray(inner)) return false;
|
|
7868
7975
|
return inner.some(
|
|
7869
|
-
(h) => h && typeof h === "object" && h.command ===
|
|
7976
|
+
(h) => h && typeof h === "object" && h.command === command
|
|
7870
7977
|
);
|
|
7871
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
|
+
}
|
|
7872
7995
|
async function runInstall(opts = {}, deps = {}) {
|
|
7873
7996
|
const env = opts.env ?? process.env;
|
|
7874
7997
|
const log = opts.log ?? ((m) => console.error(m));
|
|
@@ -8263,6 +8386,7 @@ async function runCapture(input, deps = {}) {
|
|
|
8263
8386
|
const records = parseJsonl(rawTranscript);
|
|
8264
8387
|
const redacted = redactTranscript(records);
|
|
8265
8388
|
const decidedAt = sessionTimestamp(records) ?? void 0;
|
|
8389
|
+
const filePaths = sessionPaths(records, input.cwd);
|
|
8266
8390
|
const sessionId = redacted.sessionId ?? input.session_id ?? null;
|
|
8267
8391
|
if (redacted.turns.length === 0) {
|
|
8268
8392
|
return {
|
|
@@ -8281,6 +8405,7 @@ async function runCapture(input, deps = {}) {
|
|
|
8281
8405
|
env,
|
|
8282
8406
|
fetchImpl: deps.fetchImpl,
|
|
8283
8407
|
decidedAt,
|
|
8408
|
+
filePaths,
|
|
8284
8409
|
...repo ? { persist: true, repo } : {}
|
|
8285
8410
|
});
|
|
8286
8411
|
if (!result.ok) {
|
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.
|
|
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
|
+
"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",
|