backthread 0.7.0 → 0.9.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -12
- package/dist-bundle/backthread.js +700 -191
- package/package.json +13 -7
|
@@ -6,7 +6,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
8
|
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
-
|
|
9
|
+
try {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
throw mod = 0, e;
|
|
13
|
+
}
|
|
10
14
|
};
|
|
11
15
|
var __export = (target, all) => {
|
|
12
16
|
for (var name in all)
|
|
@@ -6887,7 +6891,7 @@ var require_dist = __commonJS({
|
|
|
6887
6891
|
|
|
6888
6892
|
// src/bin/backthread.ts
|
|
6889
6893
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6890
|
-
import { realpathSync } from "node:fs";
|
|
6894
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
6891
6895
|
|
|
6892
6896
|
// src/login.ts
|
|
6893
6897
|
import { hostname } from "node:os";
|
|
@@ -6945,7 +6949,7 @@ function browserCommand(platform) {
|
|
|
6945
6949
|
case "darwin":
|
|
6946
6950
|
return { cmd: "open", prefixArgs: [] };
|
|
6947
6951
|
case "win32":
|
|
6948
|
-
return { cmd: "
|
|
6952
|
+
return { cmd: "rundll32", prefixArgs: ["url.dll,FileProtocolHandler"] };
|
|
6949
6953
|
default:
|
|
6950
6954
|
return { cmd: "xdg-open", prefixArgs: [] };
|
|
6951
6955
|
}
|
|
@@ -7258,7 +7262,7 @@ async function pollForToken(sessionId, keypair, opts = {}) {
|
|
|
7258
7262
|
return { ok: false, reason: "expired", message: "the login session expired before you authorized" };
|
|
7259
7263
|
}
|
|
7260
7264
|
if (status === "consumed") {
|
|
7261
|
-
return { ok: false, reason: "error", message: "this login was already used \u2014 start a fresh `
|
|
7265
|
+
return { ok: false, reason: "error", message: "this login was already used \u2014 start a fresh `backthread login`" };
|
|
7262
7266
|
}
|
|
7263
7267
|
await sleep(interval);
|
|
7264
7268
|
}
|
|
@@ -7348,8 +7352,416 @@ async function ensureAuth(opts = {}) {
|
|
|
7348
7352
|
return readConfig(env);
|
|
7349
7353
|
}
|
|
7350
7354
|
|
|
7355
|
+
// src/logout.ts
|
|
7356
|
+
async function runLogout(env = process.env) {
|
|
7357
|
+
const where = configLocationHint3(env);
|
|
7358
|
+
let cfg;
|
|
7359
|
+
try {
|
|
7360
|
+
cfg = await readConfig(env);
|
|
7361
|
+
} catch (err) {
|
|
7362
|
+
return { ok: false, cleared: false, message: `Couldn't read ${where} to sign out (${err.message ?? err}). Check its permissions and retry.` };
|
|
7363
|
+
}
|
|
7364
|
+
if (!cfg.device_token) {
|
|
7365
|
+
return { ok: true, cleared: false, message: `Already signed out \u2014 no device token in ${where}.` };
|
|
7366
|
+
}
|
|
7367
|
+
const next = {};
|
|
7368
|
+
if (cfg.account !== void 0) next.account = cfg.account;
|
|
7369
|
+
if (cfg.repo !== void 0) next.repo = cfg.repo;
|
|
7370
|
+
await writeConfig(next, env);
|
|
7371
|
+
const kept = cfg.repo ? ` (kept your ${cfg.repo} link)` : "";
|
|
7372
|
+
return {
|
|
7373
|
+
ok: true,
|
|
7374
|
+
cleared: true,
|
|
7375
|
+
message: `Signed out. Removed this device's token from ${where}${kept}.
|
|
7376
|
+
Revoke it server-side under Account \u2192 Connected devices; \`backthread login\` re-authorizes.`
|
|
7377
|
+
};
|
|
7378
|
+
}
|
|
7379
|
+
function configLocationHint3(env) {
|
|
7380
|
+
return env.BACKTHREAD_CONFIG_DIR ? configPath(env) : "~/.backthread/config.json";
|
|
7381
|
+
}
|
|
7382
|
+
|
|
7383
|
+
// src/update.ts
|
|
7384
|
+
import { realpathSync } from "node:fs";
|
|
7385
|
+
|
|
7386
|
+
// src/upgradeNudge.ts
|
|
7387
|
+
import { join as join3 } from "node:path";
|
|
7388
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
|
|
7389
|
+
function upgradeNudgeStatePath(env = process.env) {
|
|
7390
|
+
return join3(configDir(env), "upgrade-nudge.json");
|
|
7391
|
+
}
|
|
7392
|
+
var UPGRADE_NUDGE_THROTTLE_MS = 24 * 60 * 60 * 1e3;
|
|
7393
|
+
function parseState(raw) {
|
|
7394
|
+
try {
|
|
7395
|
+
const obj = JSON.parse(raw);
|
|
7396
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
7397
|
+
const at = obj.lastUpgradeNudgeAt;
|
|
7398
|
+
if (typeof at === "number" && Number.isFinite(at)) return { lastUpgradeNudgeAt: at };
|
|
7399
|
+
}
|
|
7400
|
+
} catch {
|
|
7401
|
+
}
|
|
7402
|
+
return {};
|
|
7403
|
+
}
|
|
7404
|
+
async function readState(env) {
|
|
7405
|
+
try {
|
|
7406
|
+
return parseState(await readFile2(upgradeNudgeStatePath(env), "utf8"));
|
|
7407
|
+
} catch {
|
|
7408
|
+
return {};
|
|
7409
|
+
}
|
|
7410
|
+
}
|
|
7411
|
+
async function writeState(state, env) {
|
|
7412
|
+
try {
|
|
7413
|
+
const dir = configDir(env);
|
|
7414
|
+
await mkdir2(dir, { recursive: true, mode: DIR_MODE });
|
|
7415
|
+
await chmod2(dir, DIR_MODE).catch(() => {
|
|
7416
|
+
});
|
|
7417
|
+
const path = upgradeNudgeStatePath(env);
|
|
7418
|
+
await writeFile2(path, JSON.stringify(state) + "\n", { mode: CONFIG_MODE });
|
|
7419
|
+
await chmod2(path, CONFIG_MODE).catch(() => {
|
|
7420
|
+
});
|
|
7421
|
+
} catch {
|
|
7422
|
+
}
|
|
7423
|
+
}
|
|
7424
|
+
async function maybeUpgradeNudge(upgrade, deps = {}) {
|
|
7425
|
+
try {
|
|
7426
|
+
if (typeof upgrade !== "string" || upgrade.trim().length === 0) return null;
|
|
7427
|
+
const env = deps.env ?? process.env;
|
|
7428
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
7429
|
+
const state = await readState(env);
|
|
7430
|
+
if (typeof state.lastUpgradeNudgeAt === "number" && now - state.lastUpgradeNudgeAt < UPGRADE_NUDGE_THROTTLE_MS) {
|
|
7431
|
+
return null;
|
|
7432
|
+
}
|
|
7433
|
+
await writeState({ lastUpgradeNudgeAt: now }, env);
|
|
7434
|
+
return upgrade.trim();
|
|
7435
|
+
} catch {
|
|
7436
|
+
return null;
|
|
7437
|
+
}
|
|
7438
|
+
}
|
|
7439
|
+
async function resetUpgradeNudge(deps = {}) {
|
|
7440
|
+
try {
|
|
7441
|
+
const env = deps.env ?? process.env;
|
|
7442
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
7443
|
+
await writeState({ lastUpgradeNudgeAt: now }, env);
|
|
7444
|
+
} catch {
|
|
7445
|
+
}
|
|
7446
|
+
}
|
|
7447
|
+
|
|
7448
|
+
// src/npm.ts
|
|
7449
|
+
import { execFile } from "node:child_process";
|
|
7450
|
+
function runNpm(args) {
|
|
7451
|
+
const isWin = process.platform === "win32";
|
|
7452
|
+
const npm = isWin ? "npm.cmd" : "npm";
|
|
7453
|
+
return new Promise((resolve) => {
|
|
7454
|
+
try {
|
|
7455
|
+
execFile(
|
|
7456
|
+
npm,
|
|
7457
|
+
args,
|
|
7458
|
+
{ timeout: 12e4, windowsHide: true, shell: isWin, maxBuffer: 8 * 1024 * 1024 },
|
|
7459
|
+
(err, stdout, stderr) => {
|
|
7460
|
+
resolve({
|
|
7461
|
+
ok: !err,
|
|
7462
|
+
stdout: (stdout ?? "").toString().trim(),
|
|
7463
|
+
stderr: (stderr ?? "").toString().trim()
|
|
7464
|
+
});
|
|
7465
|
+
}
|
|
7466
|
+
);
|
|
7467
|
+
} catch (e) {
|
|
7468
|
+
resolve({ ok: false, stdout: "", stderr: e.message ?? String(e) });
|
|
7469
|
+
}
|
|
7470
|
+
});
|
|
7471
|
+
}
|
|
7472
|
+
|
|
7473
|
+
// src/update.ts
|
|
7474
|
+
var SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
7475
|
+
var NPX_SEGMENT_RE = /(?:^|[\\/])_npx[\\/]/;
|
|
7476
|
+
function detectInstallContext(env, scriptPath) {
|
|
7477
|
+
if (typeof env.CLAUDE_PLUGIN_ROOT === "string" && env.CLAUDE_PLUGIN_ROOT.trim().length > 0) return "plugin";
|
|
7478
|
+
if (scriptPath && NPX_SEGMENT_RE.test(scriptPath)) return "npx";
|
|
7479
|
+
return "global";
|
|
7480
|
+
}
|
|
7481
|
+
function resolveScriptPath() {
|
|
7482
|
+
const raw = process.argv[1] ?? "";
|
|
7483
|
+
if (!raw) return "";
|
|
7484
|
+
if (NPX_SEGMENT_RE.test(raw)) return raw;
|
|
7485
|
+
try {
|
|
7486
|
+
return realpathSync(raw);
|
|
7487
|
+
} catch {
|
|
7488
|
+
return raw;
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
function firstLine(s) {
|
|
7492
|
+
const line = s.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
7493
|
+
return line ?? "unknown npm error";
|
|
7494
|
+
}
|
|
7495
|
+
async function runUpdate(deps = {}) {
|
|
7496
|
+
const env = deps.env ?? process.env;
|
|
7497
|
+
const log = deps.log ?? ((m) => console.error(m));
|
|
7498
|
+
const current = (deps.currentVersion ?? cliVersion)();
|
|
7499
|
+
const runNpm2 = deps.runNpm ?? runNpm;
|
|
7500
|
+
const resetNudge = deps.resetNudge ?? ((e) => resetUpgradeNudge({ env: e }));
|
|
7501
|
+
const scriptPath = deps.scriptPath ?? resolveScriptPath();
|
|
7502
|
+
const context = detectInstallContext(env, scriptPath);
|
|
7503
|
+
if (context === "npx") {
|
|
7504
|
+
return {
|
|
7505
|
+
ok: true,
|
|
7506
|
+
context,
|
|
7507
|
+
updated: false,
|
|
7508
|
+
message: "You're running Backthread via `npx`, which already fetches the latest published version\non every run \u2014 nothing to update. Want a pinned, always-available binary?\n npm i -g backthread\nThen `backthread update` pulls new releases on demand."
|
|
7509
|
+
};
|
|
7510
|
+
}
|
|
7511
|
+
if (context === "plugin") {
|
|
7512
|
+
return {
|
|
7513
|
+
ok: true,
|
|
7514
|
+
context,
|
|
7515
|
+
updated: false,
|
|
7516
|
+
message: "This is the Claude Code plugin's bundled copy of Backthread \u2014 the plugin manages it,\nnot npm. Update it from Claude Code:\n /plugin update backthread\nFor a standalone terminal CLI too: `npm i -g backthread`."
|
|
7517
|
+
};
|
|
7518
|
+
}
|
|
7519
|
+
log("Checking npm for the latest backthread\u2026");
|
|
7520
|
+
const view = await runNpm2(["view", "backthread", "version"]);
|
|
7521
|
+
if (!view.ok || !SEMVER_RE.test(view.stdout)) {
|
|
7522
|
+
const why = view.ok ? `unexpected npm output "${view.stdout}"` : firstLine(view.stderr);
|
|
7523
|
+
return {
|
|
7524
|
+
ok: false,
|
|
7525
|
+
context,
|
|
7526
|
+
updated: false,
|
|
7527
|
+
message: `Couldn't check npm for the latest version (${why}). Are you online? Your current install (${current}) is untouched.`
|
|
7528
|
+
};
|
|
7529
|
+
}
|
|
7530
|
+
const latest = view.stdout;
|
|
7531
|
+
if (current === latest) {
|
|
7532
|
+
await resetNudge(env);
|
|
7533
|
+
return { ok: true, context, updated: false, message: `Backthread is already up to date (${current} is the latest).` };
|
|
7534
|
+
}
|
|
7535
|
+
log(`Updating backthread ${current} \u2192 ${latest} (npm i -g backthread@latest)\u2026`);
|
|
7536
|
+
const install = await runNpm2(["install", "-g", "backthread@latest"]);
|
|
7537
|
+
if (!install.ok) {
|
|
7538
|
+
return {
|
|
7539
|
+
ok: false,
|
|
7540
|
+
context,
|
|
7541
|
+
updated: false,
|
|
7542
|
+
message: `npm couldn't install backthread@latest: ${firstLine(install.stderr)}
|
|
7543
|
+
Your current install (${current}) is untouched. If this is a permissions error, retry with your global-install method (e.g. a Node version manager, or sudo).`
|
|
7544
|
+
};
|
|
7545
|
+
}
|
|
7546
|
+
await resetNudge(env);
|
|
7547
|
+
return {
|
|
7548
|
+
ok: true,
|
|
7549
|
+
context,
|
|
7550
|
+
updated: true,
|
|
7551
|
+
message: `Updated Backthread ${current} \u2192 ${latest}. Restart any long-running sessions to pick it up.`
|
|
7552
|
+
};
|
|
7553
|
+
}
|
|
7554
|
+
|
|
7555
|
+
// src/doctor.ts
|
|
7556
|
+
import { homedir as homedir2 } from "node:os";
|
|
7557
|
+
import { join as join4 } from "node:path";
|
|
7558
|
+
import { readFile as readFile3, stat } from "node:fs/promises";
|
|
7559
|
+
var SEMVER_RE2 = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
7560
|
+
var REPO_SLUG_RE = /^[^/\s]+\/[^/\s]+$/;
|
|
7561
|
+
async function loadConfig(env) {
|
|
7562
|
+
try {
|
|
7563
|
+
return { config: await readConfig(env), error: null };
|
|
7564
|
+
} catch (e) {
|
|
7565
|
+
return { config: null, error: e };
|
|
7566
|
+
}
|
|
7567
|
+
}
|
|
7568
|
+
function authCheck(loaded, env) {
|
|
7569
|
+
if (loaded.error) {
|
|
7570
|
+
return {
|
|
7571
|
+
key: "auth",
|
|
7572
|
+
label: "Auth",
|
|
7573
|
+
status: "fail",
|
|
7574
|
+
critical: true,
|
|
7575
|
+
detail: `couldn't read ${configHint(env)} (${loaded.error.message ?? loaded.error}) \u2014 check its permissions`
|
|
7576
|
+
};
|
|
7577
|
+
}
|
|
7578
|
+
if (loaded.config?.device_token) {
|
|
7579
|
+
return { key: "auth", label: "Auth", status: "ok", detail: "signed in (device token present)" };
|
|
7580
|
+
}
|
|
7581
|
+
return { key: "auth", label: "Auth", status: "fail", critical: true, detail: "not signed in \u2014 run `backthread login`" };
|
|
7582
|
+
}
|
|
7583
|
+
async function permsCheck(deps, env) {
|
|
7584
|
+
if (process.platform === "win32") {
|
|
7585
|
+
return { key: "perms", label: "Config perms", status: "info", detail: "n/a on Windows (POSIX modes not enforced)" };
|
|
7586
|
+
}
|
|
7587
|
+
const doStat = deps.statImpl ?? ((p) => stat(p));
|
|
7588
|
+
const filePath = configPath(env);
|
|
7589
|
+
const dirPath = configDir(env);
|
|
7590
|
+
let fileMode = null;
|
|
7591
|
+
let dirMode = null;
|
|
7592
|
+
try {
|
|
7593
|
+
fileMode = (await doStat(filePath)).mode & 511;
|
|
7594
|
+
} catch {
|
|
7595
|
+
return { key: "perms", label: "Config perms", status: "info", detail: "no config file yet (run `backthread login`)" };
|
|
7596
|
+
}
|
|
7597
|
+
try {
|
|
7598
|
+
dirMode = (await doStat(dirPath)).mode & 511;
|
|
7599
|
+
} catch {
|
|
7600
|
+
dirMode = null;
|
|
7601
|
+
}
|
|
7602
|
+
const fileLoose = (fileMode & 63) !== 0;
|
|
7603
|
+
const dirLoose = dirMode !== null && (dirMode & 63) !== 0;
|
|
7604
|
+
if (fileLoose || dirLoose) {
|
|
7605
|
+
return {
|
|
7606
|
+
key: "perms",
|
|
7607
|
+
label: "Config perms",
|
|
7608
|
+
status: "warn",
|
|
7609
|
+
detail: `too open (config ${octal(fileMode)}${dirMode !== null ? `, dir ${octal(dirMode)}` : ""}) \u2014 run \`chmod 600 ${configHint(env)}\` (dir 700)`
|
|
7610
|
+
};
|
|
7611
|
+
}
|
|
7612
|
+
return { key: "perms", label: "Config perms", status: "ok", detail: `config 0600${dirMode !== null ? ", dir 0700" : ""}` };
|
|
7613
|
+
}
|
|
7614
|
+
function repoCheck(loaded) {
|
|
7615
|
+
if (loaded.error) {
|
|
7616
|
+
return { key: "repo", label: "Repo", status: "warn", detail: "could not read the connected repo" };
|
|
7617
|
+
}
|
|
7618
|
+
const repo = loaded.config?.repo;
|
|
7619
|
+
if (repo && REPO_SLUG_RE.test(repo)) {
|
|
7620
|
+
return { key: "repo", label: "Repo", status: "ok", detail: repo };
|
|
7621
|
+
}
|
|
7622
|
+
if (repo) {
|
|
7623
|
+
return { key: "repo", label: "Repo", status: "warn", detail: `connected slug "${repo}" is not owner/name \u2014 reconnect in the web app` };
|
|
7624
|
+
}
|
|
7625
|
+
return { key: "repo", label: "Repo", status: "warn", detail: "no repo connected \u2014 run `backthread install` (or connect it in the web app)" };
|
|
7626
|
+
}
|
|
7627
|
+
var AGENT_HOOK_FILES = [
|
|
7628
|
+
{ agent: "claude-code", files: (h) => [join4(h, ".claude", "settings.json")] },
|
|
7629
|
+
{ agent: "gemini", files: (h) => [join4(h, ".gemini", "settings.json")] },
|
|
7630
|
+
{ agent: "codex", files: (h) => [join4(h, ".codex", "hooks.json"), join4(h, ".codex", "config.toml")] },
|
|
7631
|
+
{ agent: "cursor", files: (h) => [join4(h, ".cursor", "hooks.json"), join4(h, ".cursor", "mcp.json")] }
|
|
7632
|
+
];
|
|
7633
|
+
async function hookCheck(deps, env) {
|
|
7634
|
+
const home = deps.home ?? homedir2();
|
|
7635
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
7636
|
+
const doRead = deps.readFileImpl ?? ((p) => readFile3(p, "utf8"));
|
|
7637
|
+
const mentions = async (path) => {
|
|
7638
|
+
try {
|
|
7639
|
+
return (await doRead(path)).includes("backthread");
|
|
7640
|
+
} catch {
|
|
7641
|
+
return false;
|
|
7642
|
+
}
|
|
7643
|
+
};
|
|
7644
|
+
const asPlugin = typeof env.CLAUDE_PLUGIN_ROOT === "string" && env.CLAUDE_PLUGIN_ROOT.trim().length > 0;
|
|
7645
|
+
const wired = [];
|
|
7646
|
+
if (asPlugin) wired.push("claude-code (plugin)");
|
|
7647
|
+
for (const { agent, files } of AGENT_HOOK_FILES) {
|
|
7648
|
+
for (const f of files(home)) {
|
|
7649
|
+
if (await mentions(f)) {
|
|
7650
|
+
wired.push(agent);
|
|
7651
|
+
break;
|
|
7652
|
+
}
|
|
7653
|
+
}
|
|
7654
|
+
}
|
|
7655
|
+
const projectScoped = await mentions(join4(cwd, ".claude", "settings.json")) || await mentions(join4(cwd, ".claude", "settings.local.json"));
|
|
7656
|
+
const userScopedCC = asPlugin || await mentions(join4(home, ".claude", "settings.json"));
|
|
7657
|
+
const uniqueWired = Array.from(new Set(wired));
|
|
7658
|
+
if (projectScoped && !userScopedCC) {
|
|
7659
|
+
return {
|
|
7660
|
+
key: "hook",
|
|
7661
|
+
label: "Capture hook",
|
|
7662
|
+
status: "warn",
|
|
7663
|
+
detail: "PROJECT-scoped only \u2014 blind in git worktrees + other repos (ARP-680). Re-run `backthread install` for the user-scope hook."
|
|
7664
|
+
};
|
|
7665
|
+
}
|
|
7666
|
+
if (uniqueWired.length > 0) {
|
|
7667
|
+
return { key: "hook", label: "Capture hook", status: "ok", detail: `wired for ${uniqueWired.join(", ")}` };
|
|
7668
|
+
}
|
|
7669
|
+
return {
|
|
7670
|
+
key: "hook",
|
|
7671
|
+
label: "Capture hook",
|
|
7672
|
+
status: "warn",
|
|
7673
|
+
detail: "not detected \u2014 run `backthread install` here (or `backthread install --agent <codex|cursor|gemini>`)"
|
|
7674
|
+
};
|
|
7675
|
+
}
|
|
7676
|
+
async function connectivityCheck(deps, env) {
|
|
7677
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
7678
|
+
const timeout = deps.connectTimeoutMs ?? 5e3;
|
|
7679
|
+
const targets = [
|
|
7680
|
+
{ name: "worker", url: workerBaseUrl(env) },
|
|
7681
|
+
{ name: "functions", url: functionsBaseUrl(env) }
|
|
7682
|
+
];
|
|
7683
|
+
const results = await Promise.all(
|
|
7684
|
+
targets.map(async ({ name, url: url2 }) => {
|
|
7685
|
+
const controller = new AbortController();
|
|
7686
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
7687
|
+
try {
|
|
7688
|
+
const res = await doFetch(url2, { method: "GET", signal: controller.signal });
|
|
7689
|
+
await res.body?.cancel?.().catch(() => {
|
|
7690
|
+
});
|
|
7691
|
+
return { name, reachable: true };
|
|
7692
|
+
} catch {
|
|
7693
|
+
return { name, reachable: false };
|
|
7694
|
+
} finally {
|
|
7695
|
+
clearTimeout(timer);
|
|
7696
|
+
}
|
|
7697
|
+
})
|
|
7698
|
+
);
|
|
7699
|
+
const down = results.filter((r) => !r.reachable).map((r) => r.name);
|
|
7700
|
+
if (down.length === 0) {
|
|
7701
|
+
return { key: "connectivity", label: "Connectivity", status: "ok", detail: "worker + functions reachable" };
|
|
7702
|
+
}
|
|
7703
|
+
return {
|
|
7704
|
+
key: "connectivity",
|
|
7705
|
+
label: "Connectivity",
|
|
7706
|
+
status: "warn",
|
|
7707
|
+
detail: `couldn't reach ${down.join(" + ")} (offline, or blocked by a proxy/firewall?)`
|
|
7708
|
+
};
|
|
7709
|
+
}
|
|
7710
|
+
async function versionCheck(deps) {
|
|
7711
|
+
const current = cliVersion();
|
|
7712
|
+
const redact = redactVersion();
|
|
7713
|
+
const base = `backthread ${current} \xB7 redact ${redact}`;
|
|
7714
|
+
const runNpm2 = deps.runNpm ?? runNpm;
|
|
7715
|
+
const view = await runNpm2(["view", "backthread", "version"]);
|
|
7716
|
+
if (!view.ok || !SEMVER_RE2.test(view.stdout)) {
|
|
7717
|
+
return { key: "version", label: "Version", status: "info", detail: `${base} (couldn't check npm for the latest \u2014 offline?)` };
|
|
7718
|
+
}
|
|
7719
|
+
const latest = view.stdout;
|
|
7720
|
+
if (current === latest) {
|
|
7721
|
+
return { key: "version", label: "Version", status: "ok", detail: `${base} (latest)` };
|
|
7722
|
+
}
|
|
7723
|
+
return { key: "version", label: "Version", status: "info", detail: `${base} \u2014 update available (${latest}): \`backthread update\`` };
|
|
7724
|
+
}
|
|
7725
|
+
async function collectChecks(deps = {}) {
|
|
7726
|
+
const env = deps.env ?? process.env;
|
|
7727
|
+
const loaded = await loadConfig(env);
|
|
7728
|
+
const [perms, hook, connectivity, version2] = await Promise.all([
|
|
7729
|
+
permsCheck(deps, env),
|
|
7730
|
+
hookCheck(deps, env),
|
|
7731
|
+
connectivityCheck(deps, env),
|
|
7732
|
+
versionCheck(deps)
|
|
7733
|
+
]);
|
|
7734
|
+
return [authCheck(loaded, env), perms, repoCheck(loaded), hook, connectivity, version2];
|
|
7735
|
+
}
|
|
7736
|
+
var GLYPH = { ok: "\u2713", fail: "\u2717", warn: "\u26A0", info: "\u2139" };
|
|
7737
|
+
function formatReport(checks) {
|
|
7738
|
+
const width = Math.max(...checks.map((c) => c.label.length));
|
|
7739
|
+
const lines = checks.map((c) => `${GLYPH[c.status]} ${c.label.padEnd(width)} ${c.detail}`);
|
|
7740
|
+
const fails = checks.filter((c) => c.status === "fail").length;
|
|
7741
|
+
const warns = checks.filter((c) => c.status === "warn").length;
|
|
7742
|
+
let summary;
|
|
7743
|
+
if (fails > 0) summary = `
|
|
7744
|
+
${fails} issue${fails === 1 ? "" : "s"} to fix \u2014 see the \u2717 above, then re-run \`backthread doctor\`.`;
|
|
7745
|
+
else if (warns > 0) summary = `
|
|
7746
|
+
Mostly good \u2014 the \u26A0 above are worth a look but capture can still run.`;
|
|
7747
|
+
else summary = `
|
|
7748
|
+
All good \u2014 Backthread is set up. \u{1F9F5}`;
|
|
7749
|
+
return ["backthread doctor\n", ...lines, summary].join("\n");
|
|
7750
|
+
}
|
|
7751
|
+
async function runDoctor(deps = {}) {
|
|
7752
|
+
const checks = await collectChecks(deps);
|
|
7753
|
+
const exitCode = checks.some((c) => c.status === "fail" && c.critical) ? 1 : 0;
|
|
7754
|
+
return { text: formatReport(checks), exitCode, checks };
|
|
7755
|
+
}
|
|
7756
|
+
function octal(mode) {
|
|
7757
|
+
return "0" + mode.toString(8).padStart(3, "0");
|
|
7758
|
+
}
|
|
7759
|
+
function configHint(env) {
|
|
7760
|
+
return env.BACKTHREAD_CONFIG_DIR ? configPath(env) : "~/.backthread/config.json";
|
|
7761
|
+
}
|
|
7762
|
+
|
|
7351
7763
|
// src/capture.ts
|
|
7352
|
-
import { readFile as
|
|
7764
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
7353
7765
|
|
|
7354
7766
|
// ../packages/redact/src/index.ts
|
|
7355
7767
|
var CODE_REDACTION = "[code redacted]";
|
|
@@ -7709,8 +8121,8 @@ async function inferDecisions(transcript, config2, opts = {}) {
|
|
|
7709
8121
|
}
|
|
7710
8122
|
|
|
7711
8123
|
// src/connectNudge.ts
|
|
7712
|
-
import { join as
|
|
7713
|
-
import { readFile as
|
|
8124
|
+
import { join as join5 } from "node:path";
|
|
8125
|
+
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3, chmod as chmod3 } from "node:fs/promises";
|
|
7714
8126
|
function parseRepoStatus(value) {
|
|
7715
8127
|
return value === "connected" || value === "not_connected" || value === "disconnected" ? value : null;
|
|
7716
8128
|
}
|
|
@@ -7723,10 +8135,10 @@ function parseNextStep(value) {
|
|
|
7723
8135
|
return "absent";
|
|
7724
8136
|
}
|
|
7725
8137
|
function nudgeStatePath(env = process.env) {
|
|
7726
|
-
return
|
|
8138
|
+
return join5(configDir(env), "connect-nudge.json");
|
|
7727
8139
|
}
|
|
7728
8140
|
var MAX_REMEMBERED = 50;
|
|
7729
|
-
function
|
|
8141
|
+
function parseState2(raw) {
|
|
7730
8142
|
try {
|
|
7731
8143
|
const obj = JSON.parse(raw);
|
|
7732
8144
|
if (obj && typeof obj === "object" && Array.isArray(obj.nudged)) {
|
|
@@ -7737,22 +8149,22 @@ function parseState(raw) {
|
|
|
7737
8149
|
}
|
|
7738
8150
|
return { nudged: [] };
|
|
7739
8151
|
}
|
|
7740
|
-
async function
|
|
8152
|
+
async function readState2(env) {
|
|
7741
8153
|
try {
|
|
7742
|
-
return
|
|
8154
|
+
return parseState2(await readFile4(nudgeStatePath(env), "utf8"));
|
|
7743
8155
|
} catch {
|
|
7744
8156
|
return { nudged: [] };
|
|
7745
8157
|
}
|
|
7746
8158
|
}
|
|
7747
|
-
async function
|
|
8159
|
+
async function writeState2(state, env) {
|
|
7748
8160
|
try {
|
|
7749
8161
|
const dir = configDir(env);
|
|
7750
|
-
await
|
|
7751
|
-
await
|
|
8162
|
+
await mkdir3(dir, { recursive: true, mode: DIR_MODE });
|
|
8163
|
+
await chmod3(dir, DIR_MODE).catch(() => {
|
|
7752
8164
|
});
|
|
7753
8165
|
const path = nudgeStatePath(env);
|
|
7754
|
-
await
|
|
7755
|
-
await
|
|
8166
|
+
await writeFile3(path, JSON.stringify(state) + "\n", { mode: CONFIG_MODE });
|
|
8167
|
+
await chmod3(path, CONFIG_MODE).catch(() => {
|
|
7756
8168
|
});
|
|
7757
8169
|
} catch {
|
|
7758
8170
|
}
|
|
@@ -7790,12 +8202,12 @@ async function maybeNudge(status, repo, sessionId, deps = {}) {
|
|
|
7790
8202
|
}
|
|
7791
8203
|
if (line === null) return false;
|
|
7792
8204
|
const log = deps.log ?? ((m) => console.error(m));
|
|
7793
|
-
const state = await
|
|
8205
|
+
const state = await readState2(env);
|
|
7794
8206
|
if (state.nudged.includes(sessionId)) return false;
|
|
7795
8207
|
log(line);
|
|
7796
8208
|
const nudged = [...state.nudged, sessionId];
|
|
7797
8209
|
if (nudged.length > MAX_REMEMBERED) nudged.splice(0, nudged.length - MAX_REMEMBERED);
|
|
7798
|
-
await
|
|
8210
|
+
await writeState2({ nudged }, env);
|
|
7799
8211
|
return true;
|
|
7800
8212
|
} catch {
|
|
7801
8213
|
return false;
|
|
@@ -7803,84 +8215,28 @@ async function maybeNudge(status, repo, sessionId, deps = {}) {
|
|
|
7803
8215
|
}
|
|
7804
8216
|
|
|
7805
8217
|
// src/firstRun.ts
|
|
7806
|
-
import { join as
|
|
7807
|
-
import { readFile as
|
|
8218
|
+
import { join as join11 } from "node:path";
|
|
8219
|
+
import { readFile as readFile9, writeFile as writeFile7, mkdir as mkdir7, chmod as chmod6 } from "node:fs/promises";
|
|
7808
8220
|
|
|
7809
8221
|
// src/install.ts
|
|
7810
|
-
import { readFile as
|
|
7811
|
-
import { homedir as
|
|
7812
|
-
import { join as
|
|
7813
|
-
|
|
7814
|
-
// src/captureCommand.ts
|
|
7815
|
-
import { stat } from "node:fs/promises";
|
|
7816
|
-
import { homedir as homedir2 } from "node:os";
|
|
7817
|
-
import { join as join5 } from "node:path";
|
|
7818
|
-
|
|
7819
|
-
// src/upgradeNudge.ts
|
|
7820
|
-
import { join as join4 } from "node:path";
|
|
7821
|
-
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, chmod as chmod3 } from "node:fs/promises";
|
|
7822
|
-
function upgradeNudgeStatePath(env = process.env) {
|
|
7823
|
-
return join4(configDir(env), "upgrade-nudge.json");
|
|
7824
|
-
}
|
|
7825
|
-
var UPGRADE_NUDGE_THROTTLE_MS = 24 * 60 * 60 * 1e3;
|
|
7826
|
-
function parseState2(raw) {
|
|
7827
|
-
try {
|
|
7828
|
-
const obj = JSON.parse(raw);
|
|
7829
|
-
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
7830
|
-
const at = obj.lastUpgradeNudgeAt;
|
|
7831
|
-
if (typeof at === "number" && Number.isFinite(at)) return { lastUpgradeNudgeAt: at };
|
|
7832
|
-
}
|
|
7833
|
-
} catch {
|
|
7834
|
-
}
|
|
7835
|
-
return {};
|
|
7836
|
-
}
|
|
7837
|
-
async function readState2(env) {
|
|
7838
|
-
try {
|
|
7839
|
-
return parseState2(await readFile3(upgradeNudgeStatePath(env), "utf8"));
|
|
7840
|
-
} catch {
|
|
7841
|
-
return {};
|
|
7842
|
-
}
|
|
7843
|
-
}
|
|
7844
|
-
async function writeState2(state, env) {
|
|
7845
|
-
try {
|
|
7846
|
-
const dir = configDir(env);
|
|
7847
|
-
await mkdir3(dir, { recursive: true, mode: DIR_MODE });
|
|
7848
|
-
await chmod3(dir, DIR_MODE).catch(() => {
|
|
7849
|
-
});
|
|
7850
|
-
const path = upgradeNudgeStatePath(env);
|
|
7851
|
-
await writeFile3(path, JSON.stringify(state) + "\n", { mode: CONFIG_MODE });
|
|
7852
|
-
await chmod3(path, CONFIG_MODE).catch(() => {
|
|
7853
|
-
});
|
|
7854
|
-
} catch {
|
|
7855
|
-
}
|
|
7856
|
-
}
|
|
7857
|
-
async function maybeUpgradeNudge(upgrade, deps = {}) {
|
|
7858
|
-
try {
|
|
7859
|
-
if (typeof upgrade !== "string" || upgrade.trim().length === 0) return null;
|
|
7860
|
-
const env = deps.env ?? process.env;
|
|
7861
|
-
const now = deps.now ? deps.now() : Date.now();
|
|
7862
|
-
const state = await readState2(env);
|
|
7863
|
-
if (typeof state.lastUpgradeNudgeAt === "number" && now - state.lastUpgradeNudgeAt < UPGRADE_NUDGE_THROTTLE_MS) {
|
|
7864
|
-
return null;
|
|
7865
|
-
}
|
|
7866
|
-
await writeState2({ lastUpgradeNudgeAt: now }, env);
|
|
7867
|
-
return upgrade.trim();
|
|
7868
|
-
} catch {
|
|
7869
|
-
return null;
|
|
7870
|
-
}
|
|
7871
|
-
}
|
|
8222
|
+
import { readFile as readFile8, writeFile as writeFile6, mkdir as mkdir6 } from "node:fs/promises";
|
|
8223
|
+
import { homedir as homedir6 } from "node:os";
|
|
8224
|
+
import { join as join10 } from "node:path";
|
|
7872
8225
|
|
|
7873
8226
|
// src/captureCommand.ts
|
|
8227
|
+
import { stat as stat2 } from "node:fs/promises";
|
|
8228
|
+
import { homedir as homedir3 } from "node:os";
|
|
8229
|
+
import { join as join6 } from "node:path";
|
|
7874
8230
|
function slugifyCwd(cwd) {
|
|
7875
8231
|
return cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
7876
8232
|
}
|
|
7877
8233
|
function deriveTranscriptPath(sessionId, cwd, home) {
|
|
7878
8234
|
if (!sessionId || sessionId.trim().length === 0) return null;
|
|
7879
|
-
return
|
|
8235
|
+
return join6(home, ".claude", "projects", slugifyCwd(cwd), `${sessionId}.jsonl`);
|
|
7880
8236
|
}
|
|
7881
8237
|
async function defaultStat(path) {
|
|
7882
8238
|
try {
|
|
7883
|
-
const s = await
|
|
8239
|
+
const s = await stat2(path);
|
|
7884
8240
|
return s.isFile();
|
|
7885
8241
|
} catch {
|
|
7886
8242
|
return false;
|
|
@@ -7889,7 +8245,7 @@ async function defaultStat(path) {
|
|
|
7889
8245
|
async function resolveTranscriptPath(input, deps = {}) {
|
|
7890
8246
|
const explicit = input.transcriptPath;
|
|
7891
8247
|
if (explicit && explicit.trim().length > 0) return explicit;
|
|
7892
|
-
const home = (deps.homedirImpl ??
|
|
8248
|
+
const home = (deps.homedirImpl ?? homedir3)();
|
|
7893
8249
|
const cwd = input.cwd ?? process.cwd();
|
|
7894
8250
|
const derived = deriveTranscriptPath(input.sessionId, cwd, home);
|
|
7895
8251
|
if (!derived) return null;
|
|
@@ -7980,17 +8336,17 @@ function parseManualArgs(argv) {
|
|
|
7980
8336
|
}
|
|
7981
8337
|
|
|
7982
8338
|
// src/sweep.ts
|
|
7983
|
-
import { readFile as
|
|
8339
|
+
import { readFile as readFile6, stat as stat3, readdir } from "node:fs/promises";
|
|
7984
8340
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
7985
|
-
import { homedir as
|
|
7986
|
-
import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as
|
|
8341
|
+
import { homedir as homedir4 } from "node:os";
|
|
8342
|
+
import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join8 } from "node:path";
|
|
7987
8343
|
|
|
7988
8344
|
// src/sweepLedger.ts
|
|
7989
|
-
import { join as
|
|
7990
|
-
import { readFile as
|
|
8345
|
+
import { join as join7 } from "node:path";
|
|
8346
|
+
import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir4, chmod as chmod4 } from "node:fs/promises";
|
|
7991
8347
|
var MAX_PROCESSED = 2e4;
|
|
7992
8348
|
function sweepStatePath(env = process.env) {
|
|
7993
|
-
return
|
|
8349
|
+
return join7(configDir(env), "sweep-state.json");
|
|
7994
8350
|
}
|
|
7995
8351
|
function parseSweepState(raw) {
|
|
7996
8352
|
try {
|
|
@@ -8015,7 +8371,7 @@ function serializeSweepState(state) {
|
|
|
8015
8371
|
}
|
|
8016
8372
|
async function readSweepState(env = process.env) {
|
|
8017
8373
|
try {
|
|
8018
|
-
return parseSweepState(await
|
|
8374
|
+
return parseSweepState(await readFile5(sweepStatePath(env), "utf8"));
|
|
8019
8375
|
} catch {
|
|
8020
8376
|
return { processed: [], lastSweptAt: {} };
|
|
8021
8377
|
}
|
|
@@ -8118,7 +8474,7 @@ async function defaultReadDir(dir) {
|
|
|
8118
8474
|
}
|
|
8119
8475
|
async function defaultPathExists(path) {
|
|
8120
8476
|
try {
|
|
8121
|
-
await
|
|
8477
|
+
await stat3(path);
|
|
8122
8478
|
return true;
|
|
8123
8479
|
} catch {
|
|
8124
8480
|
return false;
|
|
@@ -8132,7 +8488,7 @@ function defaultMainRoot(cwd) {
|
|
|
8132
8488
|
stdio: ["ignore", "pipe", "ignore"]
|
|
8133
8489
|
}).trim();
|
|
8134
8490
|
if (!out) return null;
|
|
8135
|
-
const abs = isAbsolute2(out) ? out :
|
|
8491
|
+
const abs = isAbsolute2(out) ? out : join8(cwd, out);
|
|
8136
8492
|
return dirname2(abs.replace(/\/+$/, ""));
|
|
8137
8493
|
} catch {
|
|
8138
8494
|
return null;
|
|
@@ -8150,7 +8506,7 @@ async function runSweep(input = {}, deps = {}) {
|
|
|
8150
8506
|
return [];
|
|
8151
8507
|
}
|
|
8152
8508
|
};
|
|
8153
|
-
const baseReadFile = deps.readFileImpl ?? ((p) =>
|
|
8509
|
+
const baseReadFile = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
|
|
8154
8510
|
const doReadFile = async (p) => {
|
|
8155
8511
|
try {
|
|
8156
8512
|
return await baseReadFile(p);
|
|
@@ -8179,7 +8535,7 @@ async function runSweep(input = {}, deps = {}) {
|
|
|
8179
8535
|
const doReadState = deps.readSweepStateImpl ?? readSweepState;
|
|
8180
8536
|
const doWriteState = deps.writeSweepStateImpl ?? writeSweepState;
|
|
8181
8537
|
try {
|
|
8182
|
-
const home = (deps.homedirImpl ??
|
|
8538
|
+
const home = (deps.homedirImpl ?? homedir4)();
|
|
8183
8539
|
const now = (deps.nowImpl ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
|
|
8184
8540
|
const cwd = input.cwd ?? process.cwd();
|
|
8185
8541
|
const target = resolveRepo(cwd, readRemote);
|
|
@@ -8207,7 +8563,7 @@ async function runSweep(input = {}, deps = {}) {
|
|
|
8207
8563
|
}
|
|
8208
8564
|
const mainRoot = doMainRoot(cwd) ?? cwd;
|
|
8209
8565
|
const mainSlug = slugifyCwd(mainRoot);
|
|
8210
|
-
const projectsRoot =
|
|
8566
|
+
const projectsRoot = join8(home, ".claude", "projects");
|
|
8211
8567
|
const entries = await doReadDir(projectsRoot);
|
|
8212
8568
|
const candidates = entries.filter((n) => n === mainSlug || n.startsWith(mainSlug + "-")).sort();
|
|
8213
8569
|
const skip = new Set(state.processed);
|
|
@@ -8220,12 +8576,12 @@ async function runSweep(input = {}, deps = {}) {
|
|
|
8220
8576
|
let captured = 0;
|
|
8221
8577
|
let decisions = 0;
|
|
8222
8578
|
for (const dirName of candidates) {
|
|
8223
|
-
const dir =
|
|
8579
|
+
const dir = join8(projectsRoot, dirName);
|
|
8224
8580
|
const files = (await doReadDir(dir)).filter((n) => n.endsWith(".jsonl")).sort();
|
|
8225
8581
|
if (files.length === 0) continue;
|
|
8226
8582
|
let embeddedCwd = null;
|
|
8227
8583
|
for (const file2 of files) {
|
|
8228
|
-
embeddedCwd = extractCwdFromRaw(await doReadFile(
|
|
8584
|
+
embeddedCwd = extractCwdFromRaw(await doReadFile(join8(dir, file2)));
|
|
8229
8585
|
if (embeddedCwd) break;
|
|
8230
8586
|
}
|
|
8231
8587
|
const cwdExists = embeddedCwd ? await doPathExists(embeddedCwd) : false;
|
|
@@ -8264,7 +8620,7 @@ async function runSweep(input = {}, deps = {}) {
|
|
|
8264
8620
|
try {
|
|
8265
8621
|
outcome = await run(
|
|
8266
8622
|
{
|
|
8267
|
-
transcript_path:
|
|
8623
|
+
transcript_path: join8(dir, file2),
|
|
8268
8624
|
cwd: cls.cwd ?? mainRoot,
|
|
8269
8625
|
session_id: sid,
|
|
8270
8626
|
hook_event_name: "SessionEnd"
|
|
@@ -8325,12 +8681,12 @@ async function runBackfill(input = {}, deps = {}) {
|
|
|
8325
8681
|
}
|
|
8326
8682
|
|
|
8327
8683
|
// src/installAgent.ts
|
|
8328
|
-
import { execFile } from "node:child_process";
|
|
8684
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
8329
8685
|
import { promisify } from "node:util";
|
|
8330
|
-
import { homedir as
|
|
8331
|
-
import { join as
|
|
8332
|
-
import { readFile as
|
|
8333
|
-
var execFileP = promisify(
|
|
8686
|
+
import { homedir as homedir5 } from "node:os";
|
|
8687
|
+
import { join as join9, dirname as dirname3 } from "node:path";
|
|
8688
|
+
import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5, chmod as chmod5 } from "node:fs/promises";
|
|
8689
|
+
var execFileP = promisify(execFile2);
|
|
8334
8690
|
var MCP_COMMAND = "npx";
|
|
8335
8691
|
var MCP_ARGS = ["-y", "backthread", "mcp"];
|
|
8336
8692
|
function hookCommand(agent) {
|
|
@@ -8425,8 +8781,8 @@ async function writeJson(deps, path, obj) {
|
|
|
8425
8781
|
await doWrite(path, JSON.stringify(obj, null, 2) + "\n");
|
|
8426
8782
|
}
|
|
8427
8783
|
async function installGemini(home, deps) {
|
|
8428
|
-
const doRead = deps.readFileImpl ?? ((p) =>
|
|
8429
|
-
const path =
|
|
8784
|
+
const doRead = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
|
|
8785
|
+
const path = join9(home, ".gemini", "settings.json");
|
|
8430
8786
|
const current = await loadJsonObject(doRead, path);
|
|
8431
8787
|
const a = withMcpServer(current);
|
|
8432
8788
|
const b = withNestedHook(a.next, "SessionEnd", hookCommand("gemini-cli"), { name: "backthread-capture" }, [
|
|
@@ -8436,9 +8792,9 @@ async function installGemini(home, deps) {
|
|
|
8436
8792
|
return [{ path, wrote: a.changed || b.changed }];
|
|
8437
8793
|
}
|
|
8438
8794
|
async function installCodex(home, deps) {
|
|
8439
|
-
const doRead = deps.readFileImpl ?? ((p) =>
|
|
8795
|
+
const doRead = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
|
|
8440
8796
|
const writes = [];
|
|
8441
|
-
const tomlPath =
|
|
8797
|
+
const tomlPath = join9(home, ".codex", "config.toml");
|
|
8442
8798
|
let toml = "";
|
|
8443
8799
|
try {
|
|
8444
8800
|
toml = await doRead(tomlPath);
|
|
@@ -8459,7 +8815,7 @@ args = [${MCP_ARGS.map((a) => `"${a}"`).join(", ")}]
|
|
|
8459
8815
|
await doWrite(tomlPath, toml + sep + block);
|
|
8460
8816
|
writes.push({ path: tomlPath, wrote: true });
|
|
8461
8817
|
}
|
|
8462
|
-
const hooksPath =
|
|
8818
|
+
const hooksPath = join9(home, ".codex", "hooks.json");
|
|
8463
8819
|
const current = await loadJsonObject(doRead, hooksPath);
|
|
8464
8820
|
const h = withNestedHook(current, "Stop", hookCommand("codex"), { timeout: 60 }, [legacyHookCommand("codex")]);
|
|
8465
8821
|
if (h.changed) await writeJson(deps, hooksPath, h.next);
|
|
@@ -8467,12 +8823,12 @@ args = [${MCP_ARGS.map((a) => `"${a}"`).join(", ")}]
|
|
|
8467
8823
|
return writes;
|
|
8468
8824
|
}
|
|
8469
8825
|
async function installCursor(home, deps) {
|
|
8470
|
-
const doRead = deps.readFileImpl ?? ((p) =>
|
|
8826
|
+
const doRead = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
|
|
8471
8827
|
const nodeBinDir = deps.nodeBinDir ?? dirname3(process.execPath);
|
|
8472
8828
|
const writes = [];
|
|
8473
|
-
const scriptDir =
|
|
8474
|
-
const captureScriptPath =
|
|
8475
|
-
const mcpScriptPath =
|
|
8829
|
+
const scriptDir = join9(home, ".cursor", "hooks");
|
|
8830
|
+
const captureScriptPath = join9(scriptDir, "backthread-capture.sh");
|
|
8831
|
+
const mcpScriptPath = join9(scriptDir, "backthread-mcp.sh");
|
|
8476
8832
|
writes.push(
|
|
8477
8833
|
await writeCursorScript(
|
|
8478
8834
|
deps,
|
|
@@ -8482,12 +8838,12 @@ async function installCursor(home, deps) {
|
|
|
8482
8838
|
)
|
|
8483
8839
|
);
|
|
8484
8840
|
writes.push(await writeCursorScript(deps, mcpScriptPath, cursorWrapperScript(nodeBinDir, "mcp")));
|
|
8485
|
-
const mcpPath =
|
|
8841
|
+
const mcpPath = join9(home, ".cursor", "mcp.json");
|
|
8486
8842
|
const mcpCurrent = await loadJsonObject(doRead, mcpPath);
|
|
8487
8843
|
const m = withCursorMcpServer(mcpCurrent, mcpScriptPath);
|
|
8488
8844
|
if (m.changed) await writeJson(deps, mcpPath, m.next);
|
|
8489
8845
|
writes.push({ path: mcpPath, wrote: m.changed });
|
|
8490
|
-
const hooksPath =
|
|
8846
|
+
const hooksPath = join9(home, ".cursor", "hooks.json");
|
|
8491
8847
|
const hooksCurrent = await loadJsonObject(doRead, hooksPath);
|
|
8492
8848
|
const c = withCursorStopHook(hooksCurrent, captureScriptPath);
|
|
8493
8849
|
if (c.changed) await writeJson(deps, hooksPath, c.next);
|
|
@@ -8521,7 +8877,7 @@ function cursorWrapperScript(nodeBinDir, backthreadArgs, latest = false) {
|
|
|
8521
8877
|
].join("\n") + "\n";
|
|
8522
8878
|
}
|
|
8523
8879
|
async function writeCursorScript(deps, path, content) {
|
|
8524
|
-
const doRead = deps.readFileImpl ?? ((p) =>
|
|
8880
|
+
const doRead = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
|
|
8525
8881
|
const doChmod = deps.chmodImpl ?? ((p, mode) => chmod5(p, mode));
|
|
8526
8882
|
let existing = null;
|
|
8527
8883
|
try {
|
|
@@ -8595,7 +8951,7 @@ async function versionGate(agent, deps) {
|
|
|
8595
8951
|
return null;
|
|
8596
8952
|
}
|
|
8597
8953
|
async function runInstallAgent(agent, deps = {}) {
|
|
8598
|
-
const home = deps.home ??
|
|
8954
|
+
const home = deps.home ?? homedir5();
|
|
8599
8955
|
const versionWarning = await versionGate(agent, deps);
|
|
8600
8956
|
let writes;
|
|
8601
8957
|
switch (agent) {
|
|
@@ -8637,12 +8993,12 @@ var LEGACY_HOOK_COMMANDS = [
|
|
|
8637
8993
|
];
|
|
8638
8994
|
var OUR_HOOK_COMMANDS = /* @__PURE__ */ new Set([HOOK_COMMAND, ...LEGACY_HOOK_COMMANDS]);
|
|
8639
8995
|
async function registerHook(deps = {}) {
|
|
8640
|
-
const doReadFile = deps.readFileImpl ?? ((p) =>
|
|
8996
|
+
const doReadFile = deps.readFileImpl ?? ((p) => readFile8(p, "utf8"));
|
|
8641
8997
|
const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile6(p, d));
|
|
8642
8998
|
const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir6(d, { recursive: true }));
|
|
8643
|
-
const home = deps.home ??
|
|
8644
|
-
const settingsDir =
|
|
8645
|
-
const settingsPath =
|
|
8999
|
+
const home = deps.home ?? homedir6();
|
|
9000
|
+
const settingsDir = join10(home, ".claude");
|
|
9001
|
+
const settingsPath = join10(settingsDir, "settings.json");
|
|
8646
9002
|
let settings = {};
|
|
8647
9003
|
let raw = null;
|
|
8648
9004
|
try {
|
|
@@ -8754,9 +9110,9 @@ function stripSessionEndHook(settings) {
|
|
|
8754
9110
|
return next;
|
|
8755
9111
|
}
|
|
8756
9112
|
async function unregisterProjectHook(cwd, deps = {}) {
|
|
8757
|
-
const doReadFile = deps.readFileImpl ?? ((p) =>
|
|
9113
|
+
const doReadFile = deps.readFileImpl ?? ((p) => readFile8(p, "utf8"));
|
|
8758
9114
|
const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile6(p, d));
|
|
8759
|
-
const settingsPath =
|
|
9115
|
+
const settingsPath = join10(cwd, ".claude", "settings.json");
|
|
8760
9116
|
let raw;
|
|
8761
9117
|
try {
|
|
8762
9118
|
raw = await doReadFile(settingsPath);
|
|
@@ -9021,7 +9377,7 @@ function normalizeState(raw) {
|
|
|
9021
9377
|
|
|
9022
9378
|
// src/firstRun.ts
|
|
9023
9379
|
function firstRunStatePath(env = process.env) {
|
|
9024
|
-
return
|
|
9380
|
+
return join11(configDir(env), "first-run.json");
|
|
9025
9381
|
}
|
|
9026
9382
|
function parseFirstRunState(raw) {
|
|
9027
9383
|
try {
|
|
@@ -9040,7 +9396,7 @@ function parseFirstRunState(raw) {
|
|
|
9040
9396
|
}
|
|
9041
9397
|
async function readFirstRunState(env = process.env) {
|
|
9042
9398
|
try {
|
|
9043
|
-
return parseFirstRunState(await
|
|
9399
|
+
return parseFirstRunState(await readFile9(firstRunStatePath(env), "utf8"));
|
|
9044
9400
|
} catch {
|
|
9045
9401
|
return {};
|
|
9046
9402
|
}
|
|
@@ -9215,7 +9571,7 @@ function readStream(stream) {
|
|
|
9215
9571
|
async function runCapture(input, deps = {}) {
|
|
9216
9572
|
const env = deps.env ?? process.env;
|
|
9217
9573
|
const log = deps.log ?? ((m) => console.error(m));
|
|
9218
|
-
const doReadFile = deps.readFileImpl ?? ((p) =>
|
|
9574
|
+
const doReadFile = deps.readFileImpl ?? ((p) => readFile10(p, "utf8"));
|
|
9219
9575
|
const doReadConfig = deps.readConfigImpl ?? readConfig;
|
|
9220
9576
|
const fireEnsureAuth = deps.ensureAuthImpl ?? ((e) => {
|
|
9221
9577
|
void ensureAuth({ env: e }).catch(() => {
|
|
@@ -9395,8 +9751,8 @@ async function persistDerived(decisions, repo, config2, decidedAt, ctx) {
|
|
|
9395
9751
|
|
|
9396
9752
|
// src/fromHook.ts
|
|
9397
9753
|
import { spawn as spawn2 } from "node:child_process";
|
|
9398
|
-
import { join as
|
|
9399
|
-
import { readFile as
|
|
9754
|
+
import { join as join12 } from "node:path";
|
|
9755
|
+
import { readFile as readFile11, writeFile as writeFile8, mkdir as mkdir8, chmod as chmod7 } from "node:fs/promises";
|
|
9400
9756
|
var KNOWN_AGENTS = /* @__PURE__ */ new Set([
|
|
9401
9757
|
"claude-code",
|
|
9402
9758
|
"codex",
|
|
@@ -9437,7 +9793,7 @@ function normalizeHookInput(payload, _agent) {
|
|
|
9437
9793
|
return out;
|
|
9438
9794
|
}
|
|
9439
9795
|
function captureStatePath(env = process.env) {
|
|
9440
|
-
return
|
|
9796
|
+
return join12(configDir(env), "capture-sessions.json");
|
|
9441
9797
|
}
|
|
9442
9798
|
var MAX_REMEMBERED2 = 200;
|
|
9443
9799
|
function parseState3(raw) {
|
|
@@ -9460,7 +9816,7 @@ function parseState3(raw) {
|
|
|
9460
9816
|
}
|
|
9461
9817
|
async function readState3(env) {
|
|
9462
9818
|
try {
|
|
9463
|
-
return parseState3(await
|
|
9819
|
+
return parseState3(await readFile11(captureStatePath(env), "utf8"));
|
|
9464
9820
|
} catch {
|
|
9465
9821
|
return { captured: [], watermarks: {} };
|
|
9466
9822
|
}
|
|
@@ -9633,6 +9989,45 @@ function codexStdout(agent, status, outcome) {
|
|
|
9633
9989
|
return ack;
|
|
9634
9990
|
}
|
|
9635
9991
|
|
|
9992
|
+
// src/suggest.ts
|
|
9993
|
+
var MAX_DISTANCE = 2;
|
|
9994
|
+
function editDistance(a, b) {
|
|
9995
|
+
const m = a.length;
|
|
9996
|
+
const n = b.length;
|
|
9997
|
+
if (m === 0) return n;
|
|
9998
|
+
if (n === 0) return m;
|
|
9999
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
10000
|
+
let curr = new Array(n + 1);
|
|
10001
|
+
for (let i = 1; i <= m; i++) {
|
|
10002
|
+
curr[0] = i;
|
|
10003
|
+
for (let j = 1; j <= n; j++) {
|
|
10004
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
10005
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
10006
|
+
}
|
|
10007
|
+
[prev, curr] = [curr, prev];
|
|
10008
|
+
}
|
|
10009
|
+
return prev[n];
|
|
10010
|
+
}
|
|
10011
|
+
function preferable(candidate, incumbent) {
|
|
10012
|
+
if (candidate.length !== incumbent.length) return candidate.length < incumbent.length;
|
|
10013
|
+
return candidate < incumbent;
|
|
10014
|
+
}
|
|
10015
|
+
function nearestCommand(input, commands) {
|
|
10016
|
+
const needle = input.toLowerCase();
|
|
10017
|
+
if (needle.length === 0) return null;
|
|
10018
|
+
let best = null;
|
|
10019
|
+
let bestDist = Infinity;
|
|
10020
|
+
for (const cmd of commands) {
|
|
10021
|
+
const d = editDistance(needle, cmd.toLowerCase());
|
|
10022
|
+
if (d > MAX_DISTANCE || d >= needle.length) continue;
|
|
10023
|
+
if (d < bestDist || d === bestDist && best !== null && preferable(cmd, best)) {
|
|
10024
|
+
best = cmd;
|
|
10025
|
+
bestDist = d;
|
|
10026
|
+
}
|
|
10027
|
+
}
|
|
10028
|
+
return best;
|
|
10029
|
+
}
|
|
10030
|
+
|
|
9636
10031
|
// ../node_modules/zod/v3/helpers/util.js
|
|
9637
10032
|
var util;
|
|
9638
10033
|
(function(util2) {
|
|
@@ -33697,8 +34092,49 @@ var StdioServerTransport = class {
|
|
|
33697
34092
|
};
|
|
33698
34093
|
|
|
33699
34094
|
// src/query.ts
|
|
34095
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
33700
34096
|
var DEFAULT_QUESTION = "How does this project work?";
|
|
33701
|
-
var GROUNDED_ASK_TIMEOUT_MS =
|
|
34097
|
+
var GROUNDED_ASK_TIMEOUT_MS = 45e3;
|
|
34098
|
+
var GROUNDED_ASK_ATTEMPTS = 2;
|
|
34099
|
+
var defaultGitRunner2 = (args, cwd) => {
|
|
34100
|
+
try {
|
|
34101
|
+
execFileSync3("git", args, { cwd, stdio: ["ignore", "ignore", "ignore"], timeout: 3e3 });
|
|
34102
|
+
return 0;
|
|
34103
|
+
} catch (e) {
|
|
34104
|
+
const status = e.status;
|
|
34105
|
+
return typeof status === "number" ? status : 128;
|
|
34106
|
+
}
|
|
34107
|
+
};
|
|
34108
|
+
var SHA_RE = /^[0-9a-f]{7,40}$/i;
|
|
34109
|
+
function countCitationsAfterCheckout(citations, cwd, runGit = defaultGitRunner2) {
|
|
34110
|
+
const stale = /* @__PURE__ */ new Map();
|
|
34111
|
+
for (const c of citations) {
|
|
34112
|
+
if (c.anchorSha && SHA_RE.test(c.anchorSha)) stale.set(c.anchorSha, false);
|
|
34113
|
+
}
|
|
34114
|
+
if (stale.size === 0) return 0;
|
|
34115
|
+
for (const sha of stale.keys()) {
|
|
34116
|
+
const exists = runGit(["rev-parse", "--quiet", "--verify", `${sha}^{commit}`], cwd);
|
|
34117
|
+
if (exists === 1) {
|
|
34118
|
+
stale.set(sha, true);
|
|
34119
|
+
continue;
|
|
34120
|
+
}
|
|
34121
|
+
if (exists !== 0) return 0;
|
|
34122
|
+
const contained = runGit(["merge-base", "--is-ancestor", sha, "HEAD"], cwd);
|
|
34123
|
+
if (contained === 1) {
|
|
34124
|
+
stale.set(sha, true);
|
|
34125
|
+
continue;
|
|
34126
|
+
}
|
|
34127
|
+
if (contained !== 0) return 0;
|
|
34128
|
+
}
|
|
34129
|
+
let n = 0;
|
|
34130
|
+
for (const c of citations) {
|
|
34131
|
+
if (c.anchorSha && stale.get(c.anchorSha)) n += 1;
|
|
34132
|
+
}
|
|
34133
|
+
return n;
|
|
34134
|
+
}
|
|
34135
|
+
function stalenessNote(n) {
|
|
34136
|
+
return `Note: ${n} of the decisions cited above landed after your checkout \u2014 this answer reflects the tracked branch.`;
|
|
34137
|
+
}
|
|
33702
34138
|
function parseSlug2(slug) {
|
|
33703
34139
|
const parts = slug.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
33704
34140
|
if (parts.length !== 2) return null;
|
|
@@ -33745,33 +34181,46 @@ async function queryDecisions(input, deps = {}) {
|
|
|
33745
34181
|
}
|
|
33746
34182
|
const deepLink = buildRepoDeepLink(repo.owner, repo.name, env);
|
|
33747
34183
|
const question = typeof input.question === "string" && input.question.trim().length > 0 ? input.question.trim() : DEFAULT_QUESTION;
|
|
33748
|
-
const ac = new AbortController();
|
|
33749
|
-
const timer = setTimeout(() => ac.abort(), GROUNDED_ASK_TIMEOUT_MS);
|
|
33750
34184
|
let res;
|
|
33751
|
-
|
|
33752
|
-
|
|
33753
|
-
|
|
33754
|
-
|
|
33755
|
-
|
|
33756
|
-
|
|
33757
|
-
|
|
33758
|
-
|
|
33759
|
-
|
|
33760
|
-
|
|
33761
|
-
|
|
33762
|
-
|
|
33763
|
-
|
|
33764
|
-
|
|
33765
|
-
|
|
33766
|
-
|
|
34185
|
+
let failDetail = "";
|
|
34186
|
+
for (let attempt = 1; attempt <= GROUNDED_ASK_ATTEMPTS; attempt++) {
|
|
34187
|
+
const ac = new AbortController();
|
|
34188
|
+
const timer = setTimeout(() => ac.abort(), GROUNDED_ASK_TIMEOUT_MS);
|
|
34189
|
+
try {
|
|
34190
|
+
res = await doFetch(buildGroundedAskUrl(env), {
|
|
34191
|
+
method: "POST",
|
|
34192
|
+
headers: {
|
|
34193
|
+
// Bearer device token — never logged.
|
|
34194
|
+
Authorization: `Bearer ${config2.device_token}`,
|
|
34195
|
+
"Content-Type": "application/json",
|
|
34196
|
+
...versionHeaders()
|
|
34197
|
+
// x-backthread-version — server-side compat guard
|
|
34198
|
+
},
|
|
34199
|
+
// The server accepts `repo` as an "owner/name" slug (it re-resolves + gates).
|
|
34200
|
+
body: JSON.stringify({ question, repo: `${repo.owner}/${repo.name}` }),
|
|
34201
|
+
signal: ac.signal
|
|
34202
|
+
});
|
|
34203
|
+
if (res.status >= 500 && attempt < GROUNDED_ASK_ATTEMPTS) {
|
|
34204
|
+
failDetail = `grounded-ask rejected (${res.status})`;
|
|
34205
|
+
res = void 0;
|
|
34206
|
+
continue;
|
|
34207
|
+
}
|
|
34208
|
+
break;
|
|
34209
|
+
} catch (e) {
|
|
34210
|
+
const aborted2 = e.name === "AbortError";
|
|
34211
|
+
failDetail = aborted2 ? `grounded-ask timed out after ${GROUNDED_ASK_TIMEOUT_MS / 1e3}s` : `grounded-ask request failed: ${e.message}`;
|
|
34212
|
+
res = void 0;
|
|
34213
|
+
} finally {
|
|
34214
|
+
clearTimeout(timer);
|
|
34215
|
+
}
|
|
34216
|
+
}
|
|
34217
|
+
if (!res) {
|
|
33767
34218
|
return {
|
|
33768
34219
|
status: "read-failed",
|
|
33769
|
-
detail:
|
|
34220
|
+
detail: `${failDetail} (after ${GROUNDED_ASK_ATTEMPTS} attempts) \u2014 try again.`,
|
|
33770
34221
|
repo,
|
|
33771
34222
|
deepLink
|
|
33772
34223
|
};
|
|
33773
|
-
} finally {
|
|
33774
|
-
clearTimeout(timer);
|
|
33775
34224
|
}
|
|
33776
34225
|
let payload;
|
|
33777
34226
|
try {
|
|
@@ -33799,13 +34248,22 @@ async function queryDecisions(input, deps = {}) {
|
|
|
33799
34248
|
};
|
|
33800
34249
|
}
|
|
33801
34250
|
const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade : void 0;
|
|
34251
|
+
const citations = normalizeCitations(rec.citations);
|
|
34252
|
+
let renderedAnswer = answer;
|
|
34253
|
+
try {
|
|
34254
|
+
const n = countCitationsAfterCheckout(citations, input.cwd ?? process.cwd(), deps.runGitImpl);
|
|
34255
|
+
if (n > 0) renderedAnswer = `${answer}
|
|
34256
|
+
|
|
34257
|
+
${stalenessNote(n)}`;
|
|
34258
|
+
} catch {
|
|
34259
|
+
}
|
|
33802
34260
|
return {
|
|
33803
34261
|
status: "ok",
|
|
33804
34262
|
detail: `grounded answer (${typeof rec.coverage === "string" ? rec.coverage : "partial"} coverage)`,
|
|
33805
34263
|
repo,
|
|
33806
|
-
answer,
|
|
34264
|
+
answer: renderedAnswer,
|
|
33807
34265
|
coverage: typeof rec.coverage === "string" ? rec.coverage : void 0,
|
|
33808
|
-
citations
|
|
34266
|
+
citations,
|
|
33809
34267
|
inferredSpans: Array.isArray(rec.inferredSpans) ? rec.inferredSpans.map(String) : [],
|
|
33810
34268
|
// Prefer the server's deepLink; fall back to the locally-built one.
|
|
33811
34269
|
deepLink: typeof rec.deepLink === "string" && rec.deepLink.length > 0 ? rec.deepLink : deepLink,
|
|
@@ -33825,7 +34283,8 @@ function normalizeCitations(raw) {
|
|
|
33825
34283
|
title: String(r.title ?? ""),
|
|
33826
34284
|
url: String(r.url ?? ""),
|
|
33827
34285
|
moduleIds: Array.isArray(r.moduleIds) ? r.moduleIds.map(String) : [],
|
|
33828
|
-
decidedAt: typeof r.decidedAt === "string" ? r.decidedAt : null
|
|
34286
|
+
decidedAt: typeof r.decidedAt === "string" ? r.decidedAt : null,
|
|
34287
|
+
anchorSha: typeof r.anchorSha === "string" ? r.anchorSha : null
|
|
33829
34288
|
};
|
|
33830
34289
|
});
|
|
33831
34290
|
}
|
|
@@ -33921,15 +34380,15 @@ async function startMcpServer(deps = {}) {
|
|
|
33921
34380
|
}
|
|
33922
34381
|
|
|
33923
34382
|
// src/routingStats.ts
|
|
33924
|
-
import { join as
|
|
33925
|
-
import { readFile as
|
|
34383
|
+
import { join as join13 } from "node:path";
|
|
34384
|
+
import { readFile as readFile12, writeFile as writeFile9, mkdir as mkdir9, chmod as chmod8 } from "node:fs/promises";
|
|
33926
34385
|
var STATS_FILE = "routing-stats.json";
|
|
33927
34386
|
function statsPath(env) {
|
|
33928
|
-
return
|
|
34387
|
+
return join13(configDir(env), STATS_FILE);
|
|
33929
34388
|
}
|
|
33930
34389
|
async function readRoutingStats(deps = {}) {
|
|
33931
34390
|
const env = deps.env ?? process.env;
|
|
33932
|
-
const read = deps.readFileImpl ??
|
|
34391
|
+
const read = deps.readFileImpl ?? readFile12;
|
|
33933
34392
|
try {
|
|
33934
34393
|
const raw = await read(statsPath(env), "utf8");
|
|
33935
34394
|
const obj = JSON.parse(raw);
|
|
@@ -34002,40 +34461,60 @@ async function runSessionStart(deps = {}) {
|
|
|
34002
34461
|
}
|
|
34003
34462
|
|
|
34004
34463
|
// src/bin/backthread.ts
|
|
34005
|
-
var USAGE = `backthread \u2014
|
|
34464
|
+
var USAGE = `backthread \u2014 keep the thread on what your AI agent actually shipped
|
|
34006
34465
|
|
|
34007
34466
|
Usage:
|
|
34008
|
-
backthread
|
|
34009
|
-
|
|
34010
|
-
|
|
34011
|
-
backthread
|
|
34012
|
-
|
|
34467
|
+
backthread [command] [flags]
|
|
34468
|
+
|
|
34469
|
+
Setup
|
|
34470
|
+
backthread Set up Backthread here (the front door): sign in, connect
|
|
34471
|
+
this repo, wire up capture. Idempotent \u2014 re-run it anytime.
|
|
34013
34472
|
[--claim <code>]
|
|
34014
|
-
backthread
|
|
34015
|
-
backthread login
|
|
34016
|
-
|
|
34017
|
-
|
|
34018
|
-
backthread
|
|
34019
|
-
|
|
34020
|
-
|
|
34021
|
-
|
|
34022
|
-
(backs
|
|
34473
|
+
backthread start Same as above, behind the /backthread:start slash command.
|
|
34474
|
+
backthread login Authorize this device (opens your browser; works over SSH \u2014
|
|
34475
|
+
the printed URL opens on any device) [--claim <code>] [--device]
|
|
34476
|
+
backthread logout Sign this device out \u2014 drop the local token, keep the repo link
|
|
34477
|
+
backthread whoami Show this device's config (the token is never printed)
|
|
34478
|
+
|
|
34479
|
+
Ask
|
|
34480
|
+
backthread how <question> Ask how/why something here works \u2014 a grounded, cited answer
|
|
34481
|
+
from your decision log (backs /backthread:how). [--cwd <path>]
|
|
34482
|
+
|
|
34483
|
+
Capture
|
|
34023
34484
|
backthread capture Capture this session's decisions (run by the SessionEnd/Stop hook)
|
|
34024
|
-
backthread capture --
|
|
34025
|
-
Shared multi-agent hook entrypoint: read the hook payload off
|
|
34026
|
-
STDIN and capture the named transcript (always exits 0)
|
|
34027
|
-
[--agent <codex|cursor|gemini-cli>] [--detach]
|
|
34028
|
-
backthread capture --manual Manually capture a session now (the /backthread capture slash command)
|
|
34485
|
+
backthread capture --manual Capture the current session now (the /backthread capture command)
|
|
34029
34486
|
[--session <id>] [--transcript <path>] [--cwd <dir>]
|
|
34030
34487
|
backthread mcp Start the MCP server (capture + query tools) over stdio
|
|
34031
|
-
backthread install Set up capture for this repo (login + hook + backfill history)
|
|
34032
|
-
[--claim <code>] [--skip-auth] [--skip-hook] [--skip-backfill]
|
|
34033
|
-
backthread install --agent <codex|cursor|gemini>
|
|
34034
|
-
Set up capture for another agent: write its USER-GLOBAL
|
|
34035
|
-
MCP server config + session-end capture hook (idempotent)
|
|
34036
|
-
backthread help Show this message
|
|
34037
34488
|
|
|
34038
|
-
|
|
34489
|
+
Manage
|
|
34490
|
+
backthread install Set up capture for this repo (login + hook + backfill history)
|
|
34491
|
+
[--claim <code>] [--agent <codex|cursor|gemini>] [--skip-auth]
|
|
34492
|
+
[--skip-hook] [--skip-backfill]
|
|
34493
|
+
backthread update Update a global install to the latest (also -u). npx is
|
|
34494
|
+
always latest already; the plugin updates via /plugin update.
|
|
34495
|
+
backthread doctor Diagnose your setup \u2014 auth, capture hook, connectivity,
|
|
34496
|
+
version, repo. Prints \u2713/\u2717 with fix hints; exits non-zero if broken.
|
|
34497
|
+
backthread version Print the installed version (also --version, -v)
|
|
34498
|
+
backthread help Show this message (also --help, -h)
|
|
34499
|
+
|
|
34500
|
+
Your source never leaves your machine unredacted \u2014 it's checkable in this OSS repo.
|
|
34501
|
+
Docs: https://app.backthread.dev
|
|
34502
|
+
Security: https://backthread.dev/security`;
|
|
34503
|
+
var KNOWN_COMMANDS = [
|
|
34504
|
+
"start",
|
|
34505
|
+
"login",
|
|
34506
|
+
"logout",
|
|
34507
|
+
"whoami",
|
|
34508
|
+
"how",
|
|
34509
|
+
"ask",
|
|
34510
|
+
"capture",
|
|
34511
|
+
"mcp",
|
|
34512
|
+
"install",
|
|
34513
|
+
"update",
|
|
34514
|
+
"doctor",
|
|
34515
|
+
"version",
|
|
34516
|
+
"help"
|
|
34517
|
+
];
|
|
34039
34518
|
function parseClaimFlag(rest) {
|
|
34040
34519
|
const i = rest.indexOf("--claim");
|
|
34041
34520
|
if (i === -1) return void 0;
|
|
@@ -34089,6 +34568,18 @@ async function main(argv, deps = {}) {
|
|
|
34089
34568
|
console.log(lines.join("\n"));
|
|
34090
34569
|
return cfg.device_token ? 0 : 1;
|
|
34091
34570
|
}
|
|
34571
|
+
case "logout": {
|
|
34572
|
+
const logoutImpl = deps.runLogoutImpl ?? runLogout;
|
|
34573
|
+
const result = await logoutImpl();
|
|
34574
|
+
console.log(result.message);
|
|
34575
|
+
return result.ok ? 0 : 1;
|
|
34576
|
+
}
|
|
34577
|
+
case "doctor": {
|
|
34578
|
+
const doctorImpl = deps.runDoctorImpl ?? runDoctor;
|
|
34579
|
+
const result = await doctorImpl();
|
|
34580
|
+
console.log(result.text);
|
|
34581
|
+
return result.exitCode;
|
|
34582
|
+
}
|
|
34092
34583
|
case "capture": {
|
|
34093
34584
|
if (rest.includes("--from-hook")) {
|
|
34094
34585
|
const raw = await readRawHookInput();
|
|
@@ -34165,6 +34656,19 @@ async function main(argv, deps = {}) {
|
|
|
34165
34656
|
}
|
|
34166
34657
|
case void 0:
|
|
34167
34658
|
return onboarding(rest);
|
|
34659
|
+
case "update":
|
|
34660
|
+
case "--update":
|
|
34661
|
+
case "-u": {
|
|
34662
|
+
const updateImpl = deps.runUpdateImpl ?? runUpdate;
|
|
34663
|
+
const result = await updateImpl();
|
|
34664
|
+
console.log(result.message);
|
|
34665
|
+
return result.ok ? 0 : 1;
|
|
34666
|
+
}
|
|
34667
|
+
case "version":
|
|
34668
|
+
case "--version":
|
|
34669
|
+
case "-v":
|
|
34670
|
+
console.log(cliVersion());
|
|
34671
|
+
return 0;
|
|
34168
34672
|
case "help":
|
|
34169
34673
|
case "--help":
|
|
34170
34674
|
case "-h":
|
|
@@ -34172,9 +34676,14 @@ async function main(argv, deps = {}) {
|
|
|
34172
34676
|
return 0;
|
|
34173
34677
|
default:
|
|
34174
34678
|
if (command.startsWith("-")) return onboarding(argv);
|
|
34175
|
-
|
|
34176
|
-
|
|
34177
|
-
${
|
|
34679
|
+
{
|
|
34680
|
+
const guess = nearestCommand(command, KNOWN_COMMANDS);
|
|
34681
|
+
const didYouMean = guess ? ` Did you mean \`backthread ${guess}\`?` : "";
|
|
34682
|
+
console.error(
|
|
34683
|
+
`Unknown command: ${command}.${didYouMean}
|
|
34684
|
+
Run \`backthread help\` to see everything backthread can do.`
|
|
34685
|
+
);
|
|
34686
|
+
}
|
|
34178
34687
|
return 1;
|
|
34179
34688
|
}
|
|
34180
34689
|
}
|
|
@@ -34185,7 +34694,7 @@ function isEntryPoint() {
|
|
|
34185
34694
|
const self = fileURLToPath2(import.meta.url);
|
|
34186
34695
|
const resolve = (p) => {
|
|
34187
34696
|
try {
|
|
34188
|
-
return
|
|
34697
|
+
return realpathSync2(p);
|
|
34189
34698
|
} catch {
|
|
34190
34699
|
return p;
|
|
34191
34700
|
}
|