backthread 0.4.0 → 0.5.1
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/dist-bundle/backthread.js +277 -116
- 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.5.1",
|
|
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 {
|
|
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
|
|
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
|
|
7749
|
-
import { readFile as
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
7872
|
-
import { readFile as
|
|
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
|
|
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
|
|
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
|
|
7909
|
-
await
|
|
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
|
|
7913
|
-
await
|
|
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 :
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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(
|
|
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:
|
|
8266
|
+
transcript_path: join7(dir, file2),
|
|
8150
8267
|
cwd: cls.cwd ?? mainRoot,
|
|
8151
8268
|
session_id: sid,
|
|
8152
8269
|
hook_event_name: "SessionEnd"
|
|
@@ -8210,12 +8327,15 @@ 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
|
|
8214
|
-
import { readFile as
|
|
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"];
|
|
8218
8335
|
function hookCommand(agent) {
|
|
8336
|
+
return `npx -y backthread@latest capture --from-hook --agent ${agent} --detach`;
|
|
8337
|
+
}
|
|
8338
|
+
function legacyHookCommand(agent) {
|
|
8219
8339
|
return `npx -y backthread capture --from-hook --agent ${agent} --detach`;
|
|
8220
8340
|
}
|
|
8221
8341
|
var MIN_VERSION = {
|
|
@@ -8265,37 +8385,59 @@ function withMcpServer(settings) {
|
|
|
8265
8385
|
mcpServers.backthread = desired;
|
|
8266
8386
|
return { next: { ...settings, mcpServers }, changed: true };
|
|
8267
8387
|
}
|
|
8268
|
-
function
|
|
8388
|
+
function groupRunsCommand(group, command) {
|
|
8389
|
+
const inner = group?.hooks;
|
|
8390
|
+
return Array.isArray(inner) && inner.some((h) => h?.command === command);
|
|
8391
|
+
}
|
|
8392
|
+
function rewriteLegacyInGroup(group, legacyCommands, command) {
|
|
8393
|
+
const inner = group?.hooks;
|
|
8394
|
+
if (!Array.isArray(inner)) return group;
|
|
8395
|
+
let changed = false;
|
|
8396
|
+
const nextInner = inner.map((h) => {
|
|
8397
|
+
const cmd = h?.command;
|
|
8398
|
+
if (typeof cmd === "string" && cmd !== command && legacyCommands.includes(cmd)) {
|
|
8399
|
+
changed = true;
|
|
8400
|
+
return { ...h, command };
|
|
8401
|
+
}
|
|
8402
|
+
return h;
|
|
8403
|
+
});
|
|
8404
|
+
return changed ? { ...group, hooks: nextInner } : group;
|
|
8405
|
+
}
|
|
8406
|
+
function withNestedHook(settings, event, command, extra = {}, legacyCommands = []) {
|
|
8269
8407
|
const hooks = asObject(settings.hooks);
|
|
8270
8408
|
const list = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8409
|
+
if (list.some((g) => groupRunsCommand(g, command))) return { next: settings, changed: false };
|
|
8410
|
+
let migrated = false;
|
|
8411
|
+
const nextList = list.map((g) => {
|
|
8412
|
+
const rewritten = rewriteLegacyInGroup(g, legacyCommands, command);
|
|
8413
|
+
if (rewritten !== g) migrated = true;
|
|
8414
|
+
return rewritten;
|
|
8274
8415
|
});
|
|
8275
|
-
if (
|
|
8276
|
-
|
|
8277
|
-
hooks[event] = list;
|
|
8416
|
+
if (!migrated) nextList.push({ hooks: [{ type: "command", command, ...extra }] });
|
|
8417
|
+
hooks[event] = nextList;
|
|
8278
8418
|
return { next: { ...settings, hooks }, changed: true };
|
|
8279
8419
|
}
|
|
8280
8420
|
async function writeJson(deps, path, obj) {
|
|
8281
|
-
const doMkdir = deps.mkdirImpl ?? (async (d) => void await
|
|
8282
|
-
const doWrite = deps.writeFileImpl ?? ((p, d) =>
|
|
8421
|
+
const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir5(d, { recursive: true }));
|
|
8422
|
+
const doWrite = deps.writeFileImpl ?? ((p, d) => writeFile5(p, d));
|
|
8283
8423
|
await doMkdir(dirname3(path));
|
|
8284
8424
|
await doWrite(path, JSON.stringify(obj, null, 2) + "\n");
|
|
8285
8425
|
}
|
|
8286
8426
|
async function installGemini(home, deps) {
|
|
8287
|
-
const doRead = deps.readFileImpl ?? ((p) =>
|
|
8288
|
-
const path =
|
|
8427
|
+
const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
|
|
8428
|
+
const path = join8(home, ".gemini", "settings.json");
|
|
8289
8429
|
const current = await loadJsonObject(doRead, path);
|
|
8290
8430
|
const a = withMcpServer(current);
|
|
8291
|
-
const b = withNestedHook(a.next, "SessionEnd", hookCommand("gemini-cli"), { name: "backthread-capture" }
|
|
8431
|
+
const b = withNestedHook(a.next, "SessionEnd", hookCommand("gemini-cli"), { name: "backthread-capture" }, [
|
|
8432
|
+
legacyHookCommand("gemini-cli")
|
|
8433
|
+
]);
|
|
8292
8434
|
if (a.changed || b.changed) await writeJson(deps, path, b.next);
|
|
8293
8435
|
return [{ path, wrote: a.changed || b.changed }];
|
|
8294
8436
|
}
|
|
8295
8437
|
async function installCodex(home, deps) {
|
|
8296
|
-
const doRead = deps.readFileImpl ?? ((p) =>
|
|
8438
|
+
const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
|
|
8297
8439
|
const writes = [];
|
|
8298
|
-
const tomlPath =
|
|
8440
|
+
const tomlPath = join8(home, ".codex", "config.toml");
|
|
8299
8441
|
let toml = "";
|
|
8300
8442
|
try {
|
|
8301
8443
|
toml = await doRead(tomlPath);
|
|
@@ -8310,40 +8452,41 @@ command = "${MCP_COMMAND}"
|
|
|
8310
8452
|
args = [${MCP_ARGS.map((a) => `"${a}"`).join(", ")}]
|
|
8311
8453
|
`;
|
|
8312
8454
|
const sep = toml.length === 0 ? "" : toml.endsWith("\n") ? "\n" : "\n\n";
|
|
8313
|
-
const doMkdir = deps.mkdirImpl ?? (async (d) => void await
|
|
8314
|
-
const doWrite = deps.writeFileImpl ?? ((p, d) =>
|
|
8455
|
+
const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir5(d, { recursive: true }));
|
|
8456
|
+
const doWrite = deps.writeFileImpl ?? ((p, d) => writeFile5(p, d));
|
|
8315
8457
|
await doMkdir(dirname3(tomlPath));
|
|
8316
8458
|
await doWrite(tomlPath, toml + sep + block);
|
|
8317
8459
|
writes.push({ path: tomlPath, wrote: true });
|
|
8318
8460
|
}
|
|
8319
|
-
const hooksPath =
|
|
8461
|
+
const hooksPath = join8(home, ".codex", "hooks.json");
|
|
8320
8462
|
const current = await loadJsonObject(doRead, hooksPath);
|
|
8321
|
-
const h = withNestedHook(current, "Stop", hookCommand("codex"), { timeout: 60 });
|
|
8463
|
+
const h = withNestedHook(current, "Stop", hookCommand("codex"), { timeout: 60 }, [legacyHookCommand("codex")]);
|
|
8322
8464
|
if (h.changed) await writeJson(deps, hooksPath, h.next);
|
|
8323
8465
|
writes.push({ path: hooksPath, wrote: h.changed });
|
|
8324
8466
|
return writes;
|
|
8325
8467
|
}
|
|
8326
8468
|
async function installCursor(home, deps) {
|
|
8327
|
-
const doRead = deps.readFileImpl ?? ((p) =>
|
|
8469
|
+
const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
|
|
8328
8470
|
const nodeBinDir = deps.nodeBinDir ?? dirname3(process.execPath);
|
|
8329
8471
|
const writes = [];
|
|
8330
|
-
const scriptDir =
|
|
8331
|
-
const captureScriptPath =
|
|
8332
|
-
const mcpScriptPath =
|
|
8472
|
+
const scriptDir = join8(home, ".cursor", "hooks");
|
|
8473
|
+
const captureScriptPath = join8(scriptDir, "backthread-capture.sh");
|
|
8474
|
+
const mcpScriptPath = join8(scriptDir, "backthread-mcp.sh");
|
|
8333
8475
|
writes.push(
|
|
8334
8476
|
await writeCursorScript(
|
|
8335
8477
|
deps,
|
|
8336
8478
|
captureScriptPath,
|
|
8337
|
-
|
|
8479
|
+
// capture hook → self-updating (@latest), like the other agents' hooks (ARP-739).
|
|
8480
|
+
cursorWrapperScript(nodeBinDir, "capture --from-hook --agent cursor --detach", true)
|
|
8338
8481
|
)
|
|
8339
8482
|
);
|
|
8340
8483
|
writes.push(await writeCursorScript(deps, mcpScriptPath, cursorWrapperScript(nodeBinDir, "mcp")));
|
|
8341
|
-
const mcpPath =
|
|
8484
|
+
const mcpPath = join8(home, ".cursor", "mcp.json");
|
|
8342
8485
|
const mcpCurrent = await loadJsonObject(doRead, mcpPath);
|
|
8343
8486
|
const m = withCursorMcpServer(mcpCurrent, mcpScriptPath);
|
|
8344
8487
|
if (m.changed) await writeJson(deps, mcpPath, m.next);
|
|
8345
8488
|
writes.push({ path: mcpPath, wrote: m.changed });
|
|
8346
|
-
const hooksPath =
|
|
8489
|
+
const hooksPath = join8(home, ".cursor", "hooks.json");
|
|
8347
8490
|
const hooksCurrent = await loadJsonObject(doRead, hooksPath);
|
|
8348
8491
|
const c = withCursorStopHook(hooksCurrent, captureScriptPath);
|
|
8349
8492
|
if (c.changed) await writeJson(deps, hooksPath, c.next);
|
|
@@ -8353,7 +8496,8 @@ async function installCursor(home, deps) {
|
|
|
8353
8496
|
function shSingleQuote(s) {
|
|
8354
8497
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
8355
8498
|
}
|
|
8356
|
-
function cursorWrapperScript(nodeBinDir, backthreadArgs) {
|
|
8499
|
+
function cursorWrapperScript(nodeBinDir, backthreadArgs, latest = false) {
|
|
8500
|
+
const pkg = latest ? "backthread@latest" : "backthread";
|
|
8357
8501
|
return [
|
|
8358
8502
|
"#!/bin/sh",
|
|
8359
8503
|
"# Backthread wrapper for Cursor \u2014 generated by `backthread install --agent cursor` (ARP-692).",
|
|
@@ -8372,12 +8516,12 @@ function cursorWrapperScript(nodeBinDir, backthreadArgs) {
|
|
|
8372
8516
|
' PATH="$NODE_BIN_DIR:$PATH"',
|
|
8373
8517
|
" export PATH",
|
|
8374
8518
|
"fi",
|
|
8375
|
-
`exec npx -y
|
|
8519
|
+
`exec npx -y ${pkg} ${backthreadArgs}`
|
|
8376
8520
|
].join("\n") + "\n";
|
|
8377
8521
|
}
|
|
8378
8522
|
async function writeCursorScript(deps, path, content) {
|
|
8379
|
-
const doRead = deps.readFileImpl ?? ((p) =>
|
|
8380
|
-
const doChmod = deps.chmodImpl ?? ((p, mode) =>
|
|
8523
|
+
const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
|
|
8524
|
+
const doChmod = deps.chmodImpl ?? ((p, mode) => chmod5(p, mode));
|
|
8381
8525
|
let existing = null;
|
|
8382
8526
|
try {
|
|
8383
8527
|
existing = await doRead(path);
|
|
@@ -8389,8 +8533,8 @@ async function writeCursorScript(deps, path, content) {
|
|
|
8389
8533
|
});
|
|
8390
8534
|
return { path, wrote: false };
|
|
8391
8535
|
}
|
|
8392
|
-
const doMkdir = deps.mkdirImpl ?? (async (d) => void await
|
|
8393
|
-
const doWrite = deps.writeFileImpl ?? ((p, d) =>
|
|
8536
|
+
const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir5(d, { recursive: true }));
|
|
8537
|
+
const doWrite = deps.writeFileImpl ?? ((p, d) => writeFile5(p, d));
|
|
8394
8538
|
await doMkdir(dirname3(path));
|
|
8395
8539
|
await doWrite(path, content);
|
|
8396
8540
|
await doChmod(path, 493);
|
|
@@ -8406,7 +8550,7 @@ function withCursorMcpServer(settings, mcpScriptPath) {
|
|
|
8406
8550
|
return { next: { ...settings, mcpServers }, changed: true };
|
|
8407
8551
|
}
|
|
8408
8552
|
function withCursorStopHook(settings, captureScriptPath) {
|
|
8409
|
-
const legacyInline =
|
|
8553
|
+
const legacyInline = legacyHookCommand("cursor");
|
|
8410
8554
|
const hooks = asObject(settings.hooks);
|
|
8411
8555
|
const stop = Array.isArray(hooks.stop) ? [...hooks.stop] : [];
|
|
8412
8556
|
const hadDesired = stop.some((h) => h?.command === captureScriptPath);
|
|
@@ -8485,16 +8629,19 @@ var TRUST_COPY = [
|
|
|
8485
8629
|
" code blocks replaced with [code redacted] \u2014 to Backthread's Worker, never source.",
|
|
8486
8630
|
" Full details: https://app.backthread.dev/security"
|
|
8487
8631
|
].join("\n");
|
|
8488
|
-
var HOOK_COMMAND = "npx backthread capture --from-hook --agent claude-code --detach";
|
|
8489
|
-
var LEGACY_HOOK_COMMANDS = [
|
|
8632
|
+
var HOOK_COMMAND = "npx backthread@latest capture --from-hook --agent claude-code --detach";
|
|
8633
|
+
var LEGACY_HOOK_COMMANDS = [
|
|
8634
|
+
"npx backthread capture",
|
|
8635
|
+
"npx backthread capture --from-hook --agent claude-code --detach"
|
|
8636
|
+
];
|
|
8490
8637
|
var OUR_HOOK_COMMANDS = /* @__PURE__ */ new Set([HOOK_COMMAND, ...LEGACY_HOOK_COMMANDS]);
|
|
8491
8638
|
async function registerHook(deps = {}) {
|
|
8492
|
-
const doReadFile = deps.readFileImpl ?? ((p) =>
|
|
8493
|
-
const doWriteFile = deps.writeFileImpl ?? ((p, d) =>
|
|
8494
|
-
const doMkdir = deps.mkdirImpl ?? (async (d) => void await
|
|
8639
|
+
const doReadFile = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
|
|
8640
|
+
const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile6(p, d));
|
|
8641
|
+
const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir6(d, { recursive: true }));
|
|
8495
8642
|
const home = deps.home ?? homedir5();
|
|
8496
|
-
const settingsDir =
|
|
8497
|
-
const settingsPath =
|
|
8643
|
+
const settingsDir = join9(home, ".claude");
|
|
8644
|
+
const settingsPath = join9(settingsDir, "settings.json");
|
|
8498
8645
|
let settings = {};
|
|
8499
8646
|
let raw = null;
|
|
8500
8647
|
try {
|
|
@@ -8606,9 +8753,9 @@ function stripSessionEndHook(settings) {
|
|
|
8606
8753
|
return next;
|
|
8607
8754
|
}
|
|
8608
8755
|
async function unregisterProjectHook(cwd, deps = {}) {
|
|
8609
|
-
const doReadFile = deps.readFileImpl ?? ((p) =>
|
|
8610
|
-
const doWriteFile = deps.writeFileImpl ?? ((p, d) =>
|
|
8611
|
-
const settingsPath =
|
|
8756
|
+
const doReadFile = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
|
|
8757
|
+
const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile6(p, d));
|
|
8758
|
+
const settingsPath = join9(cwd, ".claude", "settings.json");
|
|
8612
8759
|
let raw;
|
|
8613
8760
|
try {
|
|
8614
8761
|
raw = await doReadFile(settingsPath);
|
|
@@ -8873,7 +9020,7 @@ function normalizeState(raw) {
|
|
|
8873
9020
|
|
|
8874
9021
|
// src/firstRun.ts
|
|
8875
9022
|
function firstRunStatePath(env = process.env) {
|
|
8876
|
-
return
|
|
9023
|
+
return join10(configDir(env), "first-run.json");
|
|
8877
9024
|
}
|
|
8878
9025
|
function parseFirstRunState(raw) {
|
|
8879
9026
|
try {
|
|
@@ -8892,7 +9039,7 @@ function parseFirstRunState(raw) {
|
|
|
8892
9039
|
}
|
|
8893
9040
|
async function readFirstRunState(env = process.env) {
|
|
8894
9041
|
try {
|
|
8895
|
-
return parseFirstRunState(await
|
|
9042
|
+
return parseFirstRunState(await readFile8(firstRunStatePath(env), "utf8"));
|
|
8896
9043
|
} catch {
|
|
8897
9044
|
return {};
|
|
8898
9045
|
}
|
|
@@ -8902,12 +9049,12 @@ async function updateFirstRunState(patch, env = process.env) {
|
|
|
8902
9049
|
const current = await readFirstRunState(env);
|
|
8903
9050
|
const next = { ...current, ...patch };
|
|
8904
9051
|
const dir = configDir(env);
|
|
8905
|
-
await
|
|
8906
|
-
await
|
|
9052
|
+
await mkdir7(dir, { recursive: true, mode: DIR_MODE });
|
|
9053
|
+
await chmod6(dir, DIR_MODE).catch(() => {
|
|
8907
9054
|
});
|
|
8908
9055
|
const path = firstRunStatePath(env);
|
|
8909
|
-
await
|
|
8910
|
-
await
|
|
9056
|
+
await writeFile7(path, JSON.stringify(next) + "\n", { mode: CONFIG_MODE });
|
|
9057
|
+
await chmod6(path, CONFIG_MODE).catch(() => {
|
|
8911
9058
|
});
|
|
8912
9059
|
} catch {
|
|
8913
9060
|
}
|
|
@@ -8915,13 +9062,13 @@ async function updateFirstRunState(patch, env = process.env) {
|
|
|
8915
9062
|
async function maybeShowTrustGate(deps = {}) {
|
|
8916
9063
|
try {
|
|
8917
9064
|
const env = deps.env ?? process.env;
|
|
8918
|
-
const
|
|
8919
|
-
const state = await
|
|
9065
|
+
const readState4 = deps.readStateImpl ?? readFirstRunState;
|
|
9066
|
+
const state = await readState4(env);
|
|
8920
9067
|
if (state.onboarded === true || state.trustShown === true) return false;
|
|
8921
9068
|
const log = deps.log ?? ((m) => console.error(m));
|
|
8922
9069
|
log(TRUST_COPY);
|
|
8923
|
-
const
|
|
8924
|
-
await
|
|
9070
|
+
const writeState4 = deps.writeStateImpl ?? updateFirstRunState;
|
|
9071
|
+
await writeState4({ trustShown: true }, env);
|
|
8925
9072
|
return true;
|
|
8926
9073
|
} catch {
|
|
8927
9074
|
return false;
|
|
@@ -8932,8 +9079,8 @@ async function runStart(opts = {}, deps = {}) {
|
|
|
8932
9079
|
const log = opts.log ?? ((m) => console.error(m));
|
|
8933
9080
|
const cwd = opts.cwd ?? process.cwd();
|
|
8934
9081
|
const entry = opts.entry ?? detectEntry({ claim: opts.claim, env });
|
|
8935
|
-
const
|
|
8936
|
-
const existingState = await
|
|
9082
|
+
const readState4 = deps.readStateImpl ?? readFirstRunState;
|
|
9083
|
+
const existingState = await readState4(env).catch(() => ({}));
|
|
8937
9084
|
if (existingState.onboarded === true) {
|
|
8938
9085
|
const readCfg = deps.readConfigImpl ?? readConfig;
|
|
8939
9086
|
const cfg = await readCfg(env).catch(() => ({}));
|
|
@@ -8982,8 +9129,8 @@ async function runStart(opts = {}, deps = {}) {
|
|
|
8982
9129
|
() => ({ status: "error", detail: "state fetch failed (swallowed)" })
|
|
8983
9130
|
);
|
|
8984
9131
|
log("\n" + renderNextStep(stateOut, env));
|
|
8985
|
-
const
|
|
8986
|
-
await
|
|
9132
|
+
const writeState4 = deps.writeStateImpl ?? updateFirstRunState;
|
|
9133
|
+
await writeState4({ onboarded: true, trustShown: true }, env);
|
|
8987
9134
|
return { exitCode: 0, status: "onboarded", authed: true };
|
|
8988
9135
|
}
|
|
8989
9136
|
function renderNextStep(out, env = process.env) {
|
|
@@ -9024,13 +9171,13 @@ async function maybeFirstCaptureConfirm(count, repoConnected, repo, deps = {}) {
|
|
|
9024
9171
|
if (!repoConnected || !repo) return false;
|
|
9025
9172
|
if (!(count > 0)) return false;
|
|
9026
9173
|
const env = deps.env ?? process.env;
|
|
9027
|
-
const
|
|
9028
|
-
const state = await
|
|
9174
|
+
const readState4 = deps.readStateImpl ?? readFirstRunState;
|
|
9175
|
+
const state = await readState4(env);
|
|
9029
9176
|
if (state.firstCaptureShown === true) return false;
|
|
9030
9177
|
const log = deps.log ?? ((m) => console.error(m));
|
|
9031
9178
|
log(firstCaptureMessage(count, repo, env));
|
|
9032
|
-
const
|
|
9033
|
-
await
|
|
9179
|
+
const writeState4 = deps.writeStateImpl ?? updateFirstRunState;
|
|
9180
|
+
await writeState4({ firstCaptureShown: true }, env);
|
|
9034
9181
|
return true;
|
|
9035
9182
|
} catch {
|
|
9036
9183
|
return false;
|
|
@@ -9067,7 +9214,7 @@ function readStream(stream) {
|
|
|
9067
9214
|
async function runCapture(input, deps = {}) {
|
|
9068
9215
|
const env = deps.env ?? process.env;
|
|
9069
9216
|
const log = deps.log ?? ((m) => console.error(m));
|
|
9070
|
-
const doReadFile = deps.readFileImpl ?? ((p) =>
|
|
9217
|
+
const doReadFile = deps.readFileImpl ?? ((p) => readFile9(p, "utf8"));
|
|
9071
9218
|
const doReadConfig = deps.readConfigImpl ?? readConfig;
|
|
9072
9219
|
const fireEnsureAuth = deps.ensureAuthImpl ?? ((e) => {
|
|
9073
9220
|
void ensureAuth({ env: e }).catch(() => {
|
|
@@ -9129,6 +9276,7 @@ async function runCapture(input, deps = {}) {
|
|
|
9129
9276
|
if (!result.ok) {
|
|
9130
9277
|
return { status: "infer-failed", detail: result.error ?? "inference failed (no detail)." };
|
|
9131
9278
|
}
|
|
9279
|
+
const inferUpgrade = result.upgrade;
|
|
9132
9280
|
if (result.persisted) {
|
|
9133
9281
|
const confirm = deps.firstCaptureConfirmImpl ?? maybeFirstCaptureConfirm;
|
|
9134
9282
|
await confirm(result.decisions.length, true, repo, { env, log }).catch(() => false);
|
|
@@ -9137,7 +9285,8 @@ async function runCapture(input, deps = {}) {
|
|
|
9137
9285
|
detail: `inference router persisted ${result.decisions.length} decision(s) server-side.`,
|
|
9138
9286
|
count: result.decisions.length,
|
|
9139
9287
|
repoConnected: true,
|
|
9140
|
-
turnCount
|
|
9288
|
+
turnCount,
|
|
9289
|
+
...inferUpgrade ? { upgrade: inferUpgrade } : {}
|
|
9141
9290
|
};
|
|
9142
9291
|
}
|
|
9143
9292
|
if (result.decisions.length === 0) {
|
|
@@ -9145,7 +9294,8 @@ async function runCapture(input, deps = {}) {
|
|
|
9145
9294
|
status: "nothing-to-capture",
|
|
9146
9295
|
detail: "inference returned no decisions for this session.",
|
|
9147
9296
|
count: 0,
|
|
9148
|
-
turnCount
|
|
9297
|
+
turnCount,
|
|
9298
|
+
...inferUpgrade ? { upgrade: inferUpgrade } : {}
|
|
9149
9299
|
};
|
|
9150
9300
|
}
|
|
9151
9301
|
if (!repo) {
|
|
@@ -9153,7 +9303,8 @@ async function runCapture(input, deps = {}) {
|
|
|
9153
9303
|
status: "nothing-to-capture",
|
|
9154
9304
|
detail: "derived decisions but could not resolve a repo from cwd (no git remote) \u2014 nothing to claim them under; skipped.",
|
|
9155
9305
|
count: 0,
|
|
9156
|
-
turnCount
|
|
9306
|
+
turnCount,
|
|
9307
|
+
...inferUpgrade ? { upgrade: inferUpgrade } : {}
|
|
9157
9308
|
};
|
|
9158
9309
|
}
|
|
9159
9310
|
const out = await persistDerived(result.decisions, repo, config2, decidedAt, {
|
|
@@ -9223,7 +9374,7 @@ async function persistDerived(decisions, repo, config2, decidedAt, ctx) {
|
|
|
9223
9374
|
const rec = payload && typeof payload === "object" ? payload : {};
|
|
9224
9375
|
const count = typeof rec.count === "number" ? rec.count : decisions.length;
|
|
9225
9376
|
const repoConnected = rec.repoConnected === true;
|
|
9226
|
-
const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade :
|
|
9377
|
+
const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade : void 0;
|
|
9227
9378
|
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
9379
|
await maybeNudge(parseRepoStatus(rec.repoStatus), repo, ctx.sessionId, {
|
|
9229
9380
|
env: ctx.env,
|
|
@@ -9234,16 +9385,17 @@ async function persistDerived(decisions, repo, config2, decidedAt, ctx) {
|
|
|
9234
9385
|
await confirm(count, repoConnected, repo, { env: ctx.env, log: ctx.log }).catch(() => false);
|
|
9235
9386
|
return {
|
|
9236
9387
|
status: "persisted",
|
|
9237
|
-
detail:
|
|
9388
|
+
detail: base,
|
|
9238
9389
|
count,
|
|
9239
|
-
repoConnected
|
|
9390
|
+
repoConnected,
|
|
9391
|
+
...upgrade ? { upgrade } : {}
|
|
9240
9392
|
};
|
|
9241
9393
|
}
|
|
9242
9394
|
|
|
9243
9395
|
// src/fromHook.ts
|
|
9244
9396
|
import { spawn as spawn2 } from "node:child_process";
|
|
9245
|
-
import { join as
|
|
9246
|
-
import { readFile as
|
|
9397
|
+
import { join as join11 } from "node:path";
|
|
9398
|
+
import { readFile as readFile10, writeFile as writeFile8, mkdir as mkdir8, chmod as chmod7 } from "node:fs/promises";
|
|
9247
9399
|
var KNOWN_AGENTS = /* @__PURE__ */ new Set([
|
|
9248
9400
|
"claude-code",
|
|
9249
9401
|
"codex",
|
|
@@ -9284,10 +9436,10 @@ function normalizeHookInput(payload, _agent) {
|
|
|
9284
9436
|
return out;
|
|
9285
9437
|
}
|
|
9286
9438
|
function captureStatePath(env = process.env) {
|
|
9287
|
-
return
|
|
9439
|
+
return join11(configDir(env), "capture-sessions.json");
|
|
9288
9440
|
}
|
|
9289
9441
|
var MAX_REMEMBERED2 = 200;
|
|
9290
|
-
function
|
|
9442
|
+
function parseState3(raw) {
|
|
9291
9443
|
try {
|
|
9292
9444
|
const obj = JSON.parse(raw);
|
|
9293
9445
|
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
@@ -9305,49 +9457,49 @@ function parseState2(raw) {
|
|
|
9305
9457
|
}
|
|
9306
9458
|
return { captured: [], watermarks: {} };
|
|
9307
9459
|
}
|
|
9308
|
-
async function
|
|
9460
|
+
async function readState3(env) {
|
|
9309
9461
|
try {
|
|
9310
|
-
return
|
|
9462
|
+
return parseState3(await readFile10(captureStatePath(env), "utf8"));
|
|
9311
9463
|
} catch {
|
|
9312
9464
|
return { captured: [], watermarks: {} };
|
|
9313
9465
|
}
|
|
9314
9466
|
}
|
|
9315
|
-
async function
|
|
9467
|
+
async function writeState3(state, env) {
|
|
9316
9468
|
try {
|
|
9317
9469
|
const dir = configDir(env);
|
|
9318
|
-
await
|
|
9319
|
-
await
|
|
9470
|
+
await mkdir8(dir, { recursive: true, mode: DIR_MODE });
|
|
9471
|
+
await chmod7(dir, DIR_MODE).catch(() => {
|
|
9320
9472
|
});
|
|
9321
9473
|
const path = captureStatePath(env);
|
|
9322
|
-
await
|
|
9323
|
-
await
|
|
9474
|
+
await writeFile8(path, JSON.stringify(state) + "\n", { mode: CONFIG_MODE });
|
|
9475
|
+
await chmod7(path, CONFIG_MODE).catch(() => {
|
|
9324
9476
|
});
|
|
9325
9477
|
} catch {
|
|
9326
9478
|
}
|
|
9327
9479
|
}
|
|
9328
9480
|
async function wasSessionCaptured(sessionId, env = process.env) {
|
|
9329
9481
|
if (!sessionId || sessionId.trim().length === 0) return false;
|
|
9330
|
-
const state = await
|
|
9482
|
+
const state = await readState3(env);
|
|
9331
9483
|
return state.captured.includes(sessionId);
|
|
9332
9484
|
}
|
|
9333
9485
|
async function markSessionCaptured(sessionId, env = process.env) {
|
|
9334
9486
|
if (!sessionId || sessionId.trim().length === 0) return;
|
|
9335
|
-
const state = await
|
|
9487
|
+
const state = await readState3(env);
|
|
9336
9488
|
if (state.captured.includes(sessionId)) return;
|
|
9337
9489
|
const captured = [...state.captured, sessionId];
|
|
9338
9490
|
if (captured.length > MAX_REMEMBERED2) captured.splice(0, captured.length - MAX_REMEMBERED2);
|
|
9339
9491
|
const { [sessionId]: _dropped, ...watermarks } = state.watermarks;
|
|
9340
|
-
await
|
|
9492
|
+
await writeState3({ captured, watermarks }, env);
|
|
9341
9493
|
}
|
|
9342
9494
|
async function captureWatermark(sessionId, env = process.env) {
|
|
9343
9495
|
if (!sessionId || sessionId.trim().length === 0) return 0;
|
|
9344
|
-
const state = await
|
|
9496
|
+
const state = await readState3(env);
|
|
9345
9497
|
return state.watermarks[sessionId] ?? 0;
|
|
9346
9498
|
}
|
|
9347
9499
|
async function setCaptureWatermark(sessionId, turnCount, env = process.env) {
|
|
9348
9500
|
if (!sessionId || sessionId.trim().length === 0) return;
|
|
9349
9501
|
if (typeof turnCount !== "number" || !Number.isFinite(turnCount) || turnCount < 0) return;
|
|
9350
|
-
const state = await
|
|
9502
|
+
const state = await readState3(env);
|
|
9351
9503
|
const prev = state.watermarks[sessionId] ?? 0;
|
|
9352
9504
|
if (turnCount <= prev) return;
|
|
9353
9505
|
const { [sessionId]: _old, ...rest } = state.watermarks;
|
|
@@ -9356,7 +9508,7 @@ async function setCaptureWatermark(sessionId, turnCount, env = process.env) {
|
|
|
9356
9508
|
if (keys.length > MAX_REMEMBERED2) {
|
|
9357
9509
|
for (const k of keys.slice(0, keys.length - MAX_REMEMBERED2)) delete watermarks[k];
|
|
9358
9510
|
}
|
|
9359
|
-
await
|
|
9511
|
+
await writeState3({ captured: state.captured, watermarks }, env);
|
|
9360
9512
|
}
|
|
9361
9513
|
function spawnDetached(rawPayload, agent, deps = {}) {
|
|
9362
9514
|
const doSpawn = deps.spawnImpl ?? spawn2;
|
|
@@ -33629,15 +33781,16 @@ async function queryDecisions(input, deps = {}) {
|
|
|
33629
33781
|
const rec = payload && typeof payload === "object" ? payload : {};
|
|
33630
33782
|
const flows = normalizeFlows(rec.flows);
|
|
33631
33783
|
const decisions = normalizeDecisions(rec.decisions);
|
|
33632
|
-
const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade :
|
|
33784
|
+
const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade : void 0;
|
|
33633
33785
|
const base = `${flows.length} flow(s), ${decisions.length} decision(s) for ${repo.owner}/${repo.name}.`;
|
|
33634
33786
|
return {
|
|
33635
33787
|
status: "ok",
|
|
33636
|
-
detail:
|
|
33788
|
+
detail: base,
|
|
33637
33789
|
repo,
|
|
33638
33790
|
flows,
|
|
33639
33791
|
decisions,
|
|
33640
|
-
deepLink
|
|
33792
|
+
deepLink,
|
|
33793
|
+
...upgrade ? { upgrade } : {}
|
|
33641
33794
|
};
|
|
33642
33795
|
} catch (e) {
|
|
33643
33796
|
return { status: "error", detail: `query failed (swallowed): ${e.message}` };
|
|
@@ -33705,7 +33858,12 @@ async function handleQueryTool(args = {}, deps = {}) {
|
|
|
33705
33858
|
const run = deps.queryDecisionsImpl ?? queryDecisions;
|
|
33706
33859
|
try {
|
|
33707
33860
|
const outcome = await run({ repo: args.repo, cwd: args.cwd }, deps.queryDeps);
|
|
33708
|
-
|
|
33861
|
+
let text = formatQueryOutcome(outcome, args.question);
|
|
33862
|
+
const nudge = await (deps.upgradeNudgeImpl ?? maybeUpgradeNudge)(outcome.upgrade);
|
|
33863
|
+
if (nudge) text += `
|
|
33864
|
+
|
|
33865
|
+
${nudge}`;
|
|
33866
|
+
return textResult(text, outcome.status !== "ok");
|
|
33709
33867
|
} catch (e) {
|
|
33710
33868
|
return textResult(`query: error \u2014 ${e.message}`, true);
|
|
33711
33869
|
}
|
|
@@ -33872,9 +34030,11 @@ async function main(argv, deps = {}) {
|
|
|
33872
34030
|
if (rest.includes("--from-hook")) {
|
|
33873
34031
|
const raw = await readRawHookInput();
|
|
33874
34032
|
const detach = rest.includes("--detach") && !rest.includes("--no-detach");
|
|
34033
|
+
const agent = parseAgent(flagValue(rest, "--agent"));
|
|
34034
|
+
setRequestAgent(agent);
|
|
33875
34035
|
const result = await runFromHook({
|
|
33876
34036
|
rawPayload: raw,
|
|
33877
|
-
agent
|
|
34037
|
+
agent,
|
|
33878
34038
|
detach
|
|
33879
34039
|
});
|
|
33880
34040
|
if (result.stdout) console.log(JSON.stringify(result.stdout));
|
|
@@ -33891,6 +34051,7 @@ async function main(argv, deps = {}) {
|
|
|
33891
34051
|
return result.exitCode;
|
|
33892
34052
|
}
|
|
33893
34053
|
try {
|
|
34054
|
+
setRequestAgent("claude-code");
|
|
33894
34055
|
const hookInput = await readHookInput();
|
|
33895
34056
|
const outcome = await runCapture(hookInput);
|
|
33896
34057
|
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.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|