backthread 0.6.0 → 0.8.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.
@@ -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
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
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,114 +6891,11 @@ 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";
6894
6898
 
6895
- // src/loopback.ts
6896
- import { createServer } from "node:http";
6897
- import { randomBytes } from "node:crypto";
6898
- function generateState() {
6899
- return base64url(randomBytes(32));
6900
- }
6901
- function base64url(bytes) {
6902
- return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6903
- }
6904
- function validateCallback(method, rawUrl, expectedState) {
6905
- if ((method ?? "").toUpperCase() !== "GET") return { ok: false, reason: "bad_method" };
6906
- let url2;
6907
- try {
6908
- url2 = new URL(rawUrl ?? "", "http://127.0.0.1");
6909
- } catch {
6910
- return { ok: false, reason: "wrong_path" };
6911
- }
6912
- if (url2.pathname !== "/callback") return { ok: false, reason: "wrong_path" };
6913
- const errorParam = url2.searchParams.get("error");
6914
- if (errorParam) return { ok: false, reason: "error_param", error: errorParam };
6915
- const state = url2.searchParams.get("state");
6916
- if (state === null || state !== expectedState) return { ok: false, reason: "state_mismatch" };
6917
- const token = url2.searchParams.get("token");
6918
- if (!token || !/^backthread_pat_[A-Za-z0-9_-]+$/.test(token)) {
6919
- return { ok: false, reason: "missing_token" };
6920
- }
6921
- return { ok: true, token };
6922
- }
6923
- function resultPage(ok) {
6924
- const title = ok ? "You\u2019re connected" : "Something went wrong";
6925
- const body = ok ? "Backthread is now authorized on this device. You can close this tab and return to your terminal." : "Backthread couldn\u2019t finish authorizing this device. Close this tab and run <code>backthread login</code> again.";
6926
- return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>
6927
- <style>body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:18vh auto;padding:0 1.5rem;color:#18181b}
6928
- h1{font-size:1.25rem}code{background:#f4f4f5;padding:.1em .3em;border-radius:4px}</style></head>
6929
- <body><h1>${title}</h1><p>${body}</p></body></html>`;
6930
- }
6931
- function startLoopbackServer() {
6932
- return new Promise((resolveStart, rejectStart) => {
6933
- const state = generateState();
6934
- let outcome = null;
6935
- let onOutcome = null;
6936
- const deliver = (o) => {
6937
- if (outcome) return;
6938
- outcome = o;
6939
- onOutcome?.();
6940
- };
6941
- const server = createServer((req, res) => {
6942
- const result = validateCallback(req.method, req.url, state);
6943
- if (!result.ok && result.reason === "wrong_path") {
6944
- res.writeHead(404, { "Content-Type": "text/plain" });
6945
- res.end("not found");
6946
- return;
6947
- }
6948
- res.writeHead(result.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
6949
- res.end(resultPage(result.ok));
6950
- if (result.ok && result.token) {
6951
- deliver({ token: result.token });
6952
- } else {
6953
- deliver({
6954
- error: new Error(
6955
- result.reason === "error_param" ? `web app reported: ${result.error}` : `invalid callback (${result.reason})`
6956
- )
6957
- });
6958
- }
6959
- });
6960
- server.on("error", (err) => {
6961
- if (outcome === null && onOutcome === null) rejectStart(err);
6962
- else deliver({ error: err });
6963
- });
6964
- server.listen(0, "127.0.0.1", () => {
6965
- const addr = server.address();
6966
- const port = addr.port;
6967
- const close = () => {
6968
- try {
6969
- server.close();
6970
- } catch {
6971
- }
6972
- };
6973
- const waitForToken = (timeoutMs = 5 * 6e4) => new Promise((resolve, reject) => {
6974
- const finish = () => {
6975
- if (!outcome) return;
6976
- close();
6977
- if ("token" in outcome) resolve(outcome.token);
6978
- else reject(outcome.error);
6979
- };
6980
- const timer = setTimeout(() => {
6981
- close();
6982
- reject(new Error("timed out waiting for the browser to authorize this device"));
6983
- }, timeoutMs);
6984
- onOutcome = () => {
6985
- clearTimeout(timer);
6986
- finish();
6987
- };
6988
- if (outcome) {
6989
- clearTimeout(timer);
6990
- finish();
6991
- }
6992
- });
6993
- resolveStart({ port, state, waitForToken, close });
6994
- });
6995
- });
6996
- }
6997
-
6998
6899
  // src/urls.ts
6999
6900
  var DEFAULT_APP_URL = "https://app.backthread.dev";
7000
6901
  function appBaseUrl(env = process.env) {
@@ -7002,10 +6903,11 @@ function appBaseUrl(env = process.env) {
7002
6903
  if (override && override.trim().length > 0) return override.replace(/\/+$/, "");
7003
6904
  return DEFAULT_APP_URL;
7004
6905
  }
7005
- function buildCliAuthUrl(port, state, env = process.env) {
6906
+ function buildCliAuthUrl(session, clientPubKey, env = process.env, label) {
7006
6907
  const u = new URL("/cli-auth", appBaseUrl(env));
7007
- u.searchParams.set("port", String(port));
7008
- u.searchParams.set("state", state);
6908
+ u.searchParams.set("session", session);
6909
+ u.searchParams.set("k", clientPubKey);
6910
+ if (label && label.trim().length > 0) u.searchParams.set("label", label.trim());
7009
6911
  return u.toString();
7010
6912
  }
7011
6913
  var DEFAULT_WORKER_URL = "https://clew-ingest-worker.arpy-183.workers.dev";
@@ -7032,6 +6934,9 @@ function buildIngestDecisionsUrl(env = process.env) {
7032
6934
  function buildOnboardingStateUrl(env = process.env) {
7033
6935
  return new URL(`${functionsBaseUrl(env).replace(/\/+$/, "")}/onboarding-state`).toString();
7034
6936
  }
6937
+ function buildCliAuthPollUrl(env = process.env) {
6938
+ return new URL(`${functionsBaseUrl(env).replace(/\/+$/, "")}/cli-auth-poll`).toString();
6939
+ }
7035
6940
  function buildRepoDeepLink(owner, name, env = process.env) {
7036
6941
  const base = appBaseUrl(env);
7037
6942
  return `${base}/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
@@ -7044,7 +6949,7 @@ function browserCommand(platform) {
7044
6949
  case "darwin":
7045
6950
  return { cmd: "open", prefixArgs: [] };
7046
6951
  case "win32":
7047
- return { cmd: "cmd", prefixArgs: ["/c", "start", ""] };
6952
+ return { cmd: "rundll32", prefixArgs: ["url.dll,FileProtocolHandler"] };
7048
6953
  default:
7049
6954
  return { cmd: "xdg-open", prefixArgs: [] };
7050
6955
  }
@@ -7278,22 +7183,117 @@ function configLocationHint(env) {
7278
7183
  return env.BACKTHREAD_CONFIG_DIR ? `${env.BACKTHREAD_CONFIG_DIR}/config.json` : "~/.backthread/config.json";
7279
7184
  }
7280
7185
 
7186
+ // src/cliAuthCrypto.ts
7187
+ import { createECDH, hkdfSync, createDecipheriv, randomBytes } from "node:crypto";
7188
+ var HKDF_SALT = Buffer.from("backthread-cli-auth");
7189
+ var HKDF_INFO = Buffer.from("device-token-v1");
7190
+ function generateSessionId() {
7191
+ return toB64url(randomBytes(32));
7192
+ }
7193
+ function generateEphemeralKeypair() {
7194
+ const ecdh = createECDH("prime256v1");
7195
+ ecdh.generateKeys();
7196
+ return { ecdh, publicKeyB64url: toB64url(ecdh.getPublicKey()) };
7197
+ }
7198
+ function decryptToken(enc, ecdh) {
7199
+ const pagePub = fromB64url(enc.page_ephemeral_pubkey);
7200
+ const shared = ecdh.computeSecret(pagePub);
7201
+ const aesKey = Buffer.from(hkdfSync("sha256", shared, HKDF_SALT, HKDF_INFO, 32));
7202
+ const ctFull = fromB64url(enc.ciphertext);
7203
+ const tag = ctFull.subarray(ctFull.length - 16);
7204
+ const ct = ctFull.subarray(0, ctFull.length - 16);
7205
+ const iv = fromB64url(enc.iv);
7206
+ const decipher = createDecipheriv("aes-256-gcm", aesKey, iv);
7207
+ decipher.setAuthTag(tag);
7208
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
7209
+ }
7210
+ function toB64url(buf) {
7211
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
7212
+ }
7213
+ function fromB64url(s) {
7214
+ return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64");
7215
+ }
7216
+
7217
+ // src/cliAuthPoll.ts
7218
+ var TOKEN_RE = /^backthread_pat_[A-Za-z0-9_-]+$/;
7219
+ async function pollForToken(sessionId, keypair, opts = {}) {
7220
+ const env = opts.env ?? process.env;
7221
+ const doFetch = opts.fetchImpl ?? fetch;
7222
+ const interval = opts.intervalMs ?? 1500;
7223
+ const timeout = opts.timeoutMs ?? 5 * 6e4;
7224
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
7225
+ const now = opts.now ?? (() => Date.now());
7226
+ const url2 = buildCliAuthPollUrl(env);
7227
+ const deadline = now() + timeout;
7228
+ while (now() < deadline) {
7229
+ let res;
7230
+ try {
7231
+ res = await doFetch(url2, {
7232
+ method: "POST",
7233
+ headers: { "Content-Type": "application/json", ...versionHeaders() },
7234
+ // The CLI is the CONSUMING poller (default mode) — the browser peeks separately.
7235
+ body: JSON.stringify({ session_id: sessionId })
7236
+ });
7237
+ } catch {
7238
+ await sleep(interval);
7239
+ continue;
7240
+ }
7241
+ if (res.status === 429 || res.status >= 500) {
7242
+ await sleep(interval);
7243
+ continue;
7244
+ }
7245
+ const body = await res.json().catch(() => null);
7246
+ const status = typeof body?.status === "string" ? body.status : null;
7247
+ if (status === "ready") {
7248
+ const enc = extractPayload(body);
7249
+ if (!enc) return { ok: false, reason: "error", message: "incomplete token payload from the server" };
7250
+ let token;
7251
+ try {
7252
+ token = decryptToken(enc, keypair.ecdh);
7253
+ } catch {
7254
+ return { ok: false, reason: "error", message: "could not decrypt the token (key mismatch)" };
7255
+ }
7256
+ if (!TOKEN_RE.test(token)) {
7257
+ return { ok: false, reason: "error", message: "the decrypted token was malformed" };
7258
+ }
7259
+ return { ok: true, token };
7260
+ }
7261
+ if (status === "expired") {
7262
+ return { ok: false, reason: "expired", message: "the login session expired before you authorized" };
7263
+ }
7264
+ if (status === "consumed") {
7265
+ return { ok: false, reason: "error", message: "this login was already used \u2014 start a fresh `backthread login`" };
7266
+ }
7267
+ await sleep(interval);
7268
+ }
7269
+ return { ok: false, reason: "timeout", message: "timed out waiting for the browser to authorize this device" };
7270
+ }
7271
+ function extractPayload(body) {
7272
+ if (!body) return null;
7273
+ const { page_ephemeral_pubkey, iv, ciphertext } = body;
7274
+ if (typeof page_ephemeral_pubkey === "string" && typeof iv === "string" && typeof ciphertext === "string") {
7275
+ return { page_ephemeral_pubkey, iv, ciphertext };
7276
+ }
7277
+ return null;
7278
+ }
7279
+
7281
7280
  // src/login.ts
7282
7281
  async function login(opts = {}) {
7283
7282
  const env = opts.env ?? process.env;
7284
7283
  const log = opts.log ?? ((m) => console.error(m));
7285
7284
  if (opts.claim) {
7286
- const result = await exchangeClaim(opts.claim, { env, label: deviceLabel() });
7287
- log(result.message);
7288
- return { ok: result.ok, message: result.message };
7285
+ const result2 = await exchangeClaim(opts.claim, { env, label: deviceLabel() });
7286
+ log(result2.message);
7287
+ return { ok: result2.ok, message: result2.message };
7289
7288
  }
7290
7289
  if (opts.device) {
7291
7290
  return deviceLogin(log);
7292
7291
  }
7293
- const handle = await startLoopbackServer();
7294
- const authUrl = buildCliAuthUrl(handle.port, handle.state, env);
7292
+ const sessionId = generateSessionId();
7293
+ const keypair = generateEphemeralKeypair();
7294
+ const authUrl = buildCliAuthUrl(sessionId, keypair.publicKeyB64url, env, deviceLabel());
7295
7295
  log("Opening your browser to authorize this device\u2026");
7296
- log(`If it doesn't open, visit:
7296
+ log(`If it doesn't open \u2014 or you're on a remote/SSH box \u2014 open this on any device:
7297
7297
 
7298
7298
  ${authUrl}
7299
7299
  `);
@@ -7303,17 +7303,21 @@ async function login(opts = {}) {
7303
7303
  log("(Could not open a browser automatically \u2014 use the URL above.)");
7304
7304
  }
7305
7305
  }
7306
- let token;
7307
- try {
7308
- token = await handle.waitForToken();
7309
- } catch (err) {
7310
- handle.close();
7311
- return { ok: false, message: `Login failed: ${err.message}` };
7306
+ const poll = opts.pollImpl ?? pollForToken;
7307
+ const result = await poll(sessionId, keypair, { env });
7308
+ if (!result.ok) {
7309
+ return { ok: false, message: pollFailureMessage(result) };
7312
7310
  }
7313
- await updateConfig({ device_token: token }, env);
7311
+ await updateConfig({ device_token: result.token }, env);
7314
7312
  log(`Authorized. Token stored in ${configLocationHint2(env)} (chmod 0600).`);
7315
7313
  return { ok: true, message: "Device authorized and token stored." };
7316
7314
  }
7315
+ function pollFailureMessage(result) {
7316
+ if (result.reason === "expired" || result.reason === "timeout") {
7317
+ return `Login ${result.reason === "expired" ? "expired" : "timed out"}: ${result.message}. Re-run \`backthread login\` to try again.`;
7318
+ }
7319
+ return `Login failed: ${result.message}`;
7320
+ }
7317
7321
  function deviceLabel() {
7318
7322
  try {
7319
7323
  const h = hostname();
@@ -7330,10 +7334,11 @@ function deviceLogin(log) {
7330
7334
  [
7331
7335
  "Headless (--device) login is not available yet.",
7332
7336
  "",
7333
- "The device-code fallback needs a server-side device-authorization endpoint",
7334
- "that ships in a later task. For now, run `backthread login` on a machine with a",
7335
- "browser, or mint a token from the web app (Account \u2192 Connected devices) and",
7336
- 'place it in ~/.backthread/config.json under "device_token".'
7337
+ "You usually don\u2019t need it: `backthread login` prints a URL you can open on ANY",
7338
+ "device (phone, laptop) \u2014 the token is delivered by polling, so the browser doesn\u2019t",
7339
+ "have to be on this machine. For a fully browserless box, mint a token from the web",
7340
+ "app (Account \u2192 Connected devices) and place it in ~/.backthread/config.json under",
7341
+ '"device_token", or use `--claim <code>` from the web app.'
7337
7342
  ].join("\n")
7338
7343
  );
7339
7344
  return { ok: false, message: "--device fallback not implemented yet." };
@@ -7347,8 +7352,416 @@ async function ensureAuth(opts = {}) {
7347
7352
  return readConfig(env);
7348
7353
  }
7349
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
+
7350
7763
  // src/capture.ts
7351
- import { readFile as readFile9 } from "node:fs/promises";
7764
+ import { readFile as readFile10 } from "node:fs/promises";
7352
7765
 
7353
7766
  // ../packages/redact/src/index.ts
7354
7767
  var CODE_REDACTION = "[code redacted]";
@@ -7708,8 +8121,8 @@ async function inferDecisions(transcript, config2, opts = {}) {
7708
8121
  }
7709
8122
 
7710
8123
  // src/connectNudge.ts
7711
- import { join as join3 } from "node:path";
7712
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
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";
7713
8126
  function parseRepoStatus(value) {
7714
8127
  return value === "connected" || value === "not_connected" || value === "disconnected" ? value : null;
7715
8128
  }
@@ -7722,10 +8135,10 @@ function parseNextStep(value) {
7722
8135
  return "absent";
7723
8136
  }
7724
8137
  function nudgeStatePath(env = process.env) {
7725
- return join3(configDir(env), "connect-nudge.json");
8138
+ return join5(configDir(env), "connect-nudge.json");
7726
8139
  }
7727
8140
  var MAX_REMEMBERED = 50;
7728
- function parseState(raw) {
8141
+ function parseState2(raw) {
7729
8142
  try {
7730
8143
  const obj = JSON.parse(raw);
7731
8144
  if (obj && typeof obj === "object" && Array.isArray(obj.nudged)) {
@@ -7736,22 +8149,22 @@ function parseState(raw) {
7736
8149
  }
7737
8150
  return { nudged: [] };
7738
8151
  }
7739
- async function readState(env) {
8152
+ async function readState2(env) {
7740
8153
  try {
7741
- return parseState(await readFile2(nudgeStatePath(env), "utf8"));
8154
+ return parseState2(await readFile4(nudgeStatePath(env), "utf8"));
7742
8155
  } catch {
7743
8156
  return { nudged: [] };
7744
8157
  }
7745
8158
  }
7746
- async function writeState(state, env) {
8159
+ async function writeState2(state, env) {
7747
8160
  try {
7748
8161
  const dir = configDir(env);
7749
- await mkdir2(dir, { recursive: true, mode: DIR_MODE });
7750
- await chmod2(dir, DIR_MODE).catch(() => {
8162
+ await mkdir3(dir, { recursive: true, mode: DIR_MODE });
8163
+ await chmod3(dir, DIR_MODE).catch(() => {
7751
8164
  });
7752
8165
  const path = nudgeStatePath(env);
7753
- await writeFile2(path, JSON.stringify(state) + "\n", { mode: CONFIG_MODE });
7754
- await chmod2(path, CONFIG_MODE).catch(() => {
8166
+ await writeFile3(path, JSON.stringify(state) + "\n", { mode: CONFIG_MODE });
8167
+ await chmod3(path, CONFIG_MODE).catch(() => {
7755
8168
  });
7756
8169
  } catch {
7757
8170
  }
@@ -7789,12 +8202,12 @@ async function maybeNudge(status, repo, sessionId, deps = {}) {
7789
8202
  }
7790
8203
  if (line === null) return false;
7791
8204
  const log = deps.log ?? ((m) => console.error(m));
7792
- const state = await readState(env);
8205
+ const state = await readState2(env);
7793
8206
  if (state.nudged.includes(sessionId)) return false;
7794
8207
  log(line);
7795
8208
  const nudged = [...state.nudged, sessionId];
7796
8209
  if (nudged.length > MAX_REMEMBERED) nudged.splice(0, nudged.length - MAX_REMEMBERED);
7797
- await writeState({ nudged }, env);
8210
+ await writeState2({ nudged }, env);
7798
8211
  return true;
7799
8212
  } catch {
7800
8213
  return false;
@@ -7802,84 +8215,28 @@ async function maybeNudge(status, repo, sessionId, deps = {}) {
7802
8215
  }
7803
8216
 
7804
8217
  // src/firstRun.ts
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";
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";
7807
8220
 
7808
8221
  // src/install.ts
7809
- import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir6 } from "node:fs/promises";
7810
- import { homedir as homedir5 } from "node:os";
7811
- import { join as join9 } from "node:path";
7812
-
7813
- // src/captureCommand.ts
7814
- import { stat } from "node:fs/promises";
7815
- import { homedir as homedir2 } from "node:os";
7816
- import { join as join5 } from "node:path";
7817
-
7818
- // src/upgradeNudge.ts
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
- }
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";
7871
8225
 
7872
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";
7873
8230
  function slugifyCwd(cwd) {
7874
8231
  return cwd.replace(/[^A-Za-z0-9]/g, "-");
7875
8232
  }
7876
8233
  function deriveTranscriptPath(sessionId, cwd, home) {
7877
8234
  if (!sessionId || sessionId.trim().length === 0) return null;
7878
- return join5(home, ".claude", "projects", slugifyCwd(cwd), `${sessionId}.jsonl`);
8235
+ return join6(home, ".claude", "projects", slugifyCwd(cwd), `${sessionId}.jsonl`);
7879
8236
  }
7880
8237
  async function defaultStat(path) {
7881
8238
  try {
7882
- const s = await stat(path);
8239
+ const s = await stat2(path);
7883
8240
  return s.isFile();
7884
8241
  } catch {
7885
8242
  return false;
@@ -7888,7 +8245,7 @@ async function defaultStat(path) {
7888
8245
  async function resolveTranscriptPath(input, deps = {}) {
7889
8246
  const explicit = input.transcriptPath;
7890
8247
  if (explicit && explicit.trim().length > 0) return explicit;
7891
- const home = (deps.homedirImpl ?? homedir2)();
8248
+ const home = (deps.homedirImpl ?? homedir3)();
7892
8249
  const cwd = input.cwd ?? process.cwd();
7893
8250
  const derived = deriveTranscriptPath(input.sessionId, cwd, home);
7894
8251
  if (!derived) return null;
@@ -7979,17 +8336,17 @@ function parseManualArgs(argv) {
7979
8336
  }
7980
8337
 
7981
8338
  // src/sweep.ts
7982
- import { readFile as readFile5, stat as stat2, readdir } from "node:fs/promises";
8339
+ import { readFile as readFile6, stat as stat3, readdir } from "node:fs/promises";
7983
8340
  import { execFileSync as execFileSync2 } from "node:child_process";
7984
- import { homedir as homedir3 } from "node:os";
7985
- import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join7 } from "node:path";
8341
+ import { homedir as homedir4 } from "node:os";
8342
+ import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join8 } from "node:path";
7986
8343
 
7987
8344
  // src/sweepLedger.ts
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";
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";
7990
8347
  var MAX_PROCESSED = 2e4;
7991
8348
  function sweepStatePath(env = process.env) {
7992
- return join6(configDir(env), "sweep-state.json");
8349
+ return join7(configDir(env), "sweep-state.json");
7993
8350
  }
7994
8351
  function parseSweepState(raw) {
7995
8352
  try {
@@ -8014,7 +8371,7 @@ function serializeSweepState(state) {
8014
8371
  }
8015
8372
  async function readSweepState(env = process.env) {
8016
8373
  try {
8017
- return parseSweepState(await readFile4(sweepStatePath(env), "utf8"));
8374
+ return parseSweepState(await readFile5(sweepStatePath(env), "utf8"));
8018
8375
  } catch {
8019
8376
  return { processed: [], lastSweptAt: {} };
8020
8377
  }
@@ -8117,7 +8474,7 @@ async function defaultReadDir(dir) {
8117
8474
  }
8118
8475
  async function defaultPathExists(path) {
8119
8476
  try {
8120
- await stat2(path);
8477
+ await stat3(path);
8121
8478
  return true;
8122
8479
  } catch {
8123
8480
  return false;
@@ -8131,7 +8488,7 @@ function defaultMainRoot(cwd) {
8131
8488
  stdio: ["ignore", "pipe", "ignore"]
8132
8489
  }).trim();
8133
8490
  if (!out) return null;
8134
- const abs = isAbsolute2(out) ? out : join7(cwd, out);
8491
+ const abs = isAbsolute2(out) ? out : join8(cwd, out);
8135
8492
  return dirname2(abs.replace(/\/+$/, ""));
8136
8493
  } catch {
8137
8494
  return null;
@@ -8149,7 +8506,7 @@ async function runSweep(input = {}, deps = {}) {
8149
8506
  return [];
8150
8507
  }
8151
8508
  };
8152
- const baseReadFile = deps.readFileImpl ?? ((p) => readFile5(p, "utf8"));
8509
+ const baseReadFile = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
8153
8510
  const doReadFile = async (p) => {
8154
8511
  try {
8155
8512
  return await baseReadFile(p);
@@ -8178,7 +8535,7 @@ async function runSweep(input = {}, deps = {}) {
8178
8535
  const doReadState = deps.readSweepStateImpl ?? readSweepState;
8179
8536
  const doWriteState = deps.writeSweepStateImpl ?? writeSweepState;
8180
8537
  try {
8181
- const home = (deps.homedirImpl ?? homedir3)();
8538
+ const home = (deps.homedirImpl ?? homedir4)();
8182
8539
  const now = (deps.nowImpl ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
8183
8540
  const cwd = input.cwd ?? process.cwd();
8184
8541
  const target = resolveRepo(cwd, readRemote);
@@ -8206,7 +8563,7 @@ async function runSweep(input = {}, deps = {}) {
8206
8563
  }
8207
8564
  const mainRoot = doMainRoot(cwd) ?? cwd;
8208
8565
  const mainSlug = slugifyCwd(mainRoot);
8209
- const projectsRoot = join7(home, ".claude", "projects");
8566
+ const projectsRoot = join8(home, ".claude", "projects");
8210
8567
  const entries = await doReadDir(projectsRoot);
8211
8568
  const candidates = entries.filter((n) => n === mainSlug || n.startsWith(mainSlug + "-")).sort();
8212
8569
  const skip = new Set(state.processed);
@@ -8219,12 +8576,12 @@ async function runSweep(input = {}, deps = {}) {
8219
8576
  let captured = 0;
8220
8577
  let decisions = 0;
8221
8578
  for (const dirName of candidates) {
8222
- const dir = join7(projectsRoot, dirName);
8579
+ const dir = join8(projectsRoot, dirName);
8223
8580
  const files = (await doReadDir(dir)).filter((n) => n.endsWith(".jsonl")).sort();
8224
8581
  if (files.length === 0) continue;
8225
8582
  let embeddedCwd = null;
8226
8583
  for (const file2 of files) {
8227
- embeddedCwd = extractCwdFromRaw(await doReadFile(join7(dir, file2)));
8584
+ embeddedCwd = extractCwdFromRaw(await doReadFile(join8(dir, file2)));
8228
8585
  if (embeddedCwd) break;
8229
8586
  }
8230
8587
  const cwdExists = embeddedCwd ? await doPathExists(embeddedCwd) : false;
@@ -8263,7 +8620,7 @@ async function runSweep(input = {}, deps = {}) {
8263
8620
  try {
8264
8621
  outcome = await run(
8265
8622
  {
8266
- transcript_path: join7(dir, file2),
8623
+ transcript_path: join8(dir, file2),
8267
8624
  cwd: cls.cwd ?? mainRoot,
8268
8625
  session_id: sid,
8269
8626
  hook_event_name: "SessionEnd"
@@ -8324,12 +8681,12 @@ async function runBackfill(input = {}, deps = {}) {
8324
8681
  }
8325
8682
 
8326
8683
  // src/installAgent.ts
8327
- import { execFile } from "node:child_process";
8684
+ import { execFile as execFile2 } from "node:child_process";
8328
8685
  import { promisify } from "node:util";
8329
- import { homedir as homedir4 } from "node:os";
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";
8332
- var execFileP = promisify(execFile);
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);
8333
8690
  var MCP_COMMAND = "npx";
8334
8691
  var MCP_ARGS = ["-y", "backthread", "mcp"];
8335
8692
  function hookCommand(agent) {
@@ -8424,8 +8781,8 @@ async function writeJson(deps, path, obj) {
8424
8781
  await doWrite(path, JSON.stringify(obj, null, 2) + "\n");
8425
8782
  }
8426
8783
  async function installGemini(home, deps) {
8427
- const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
8428
- const path = join8(home, ".gemini", "settings.json");
8784
+ const doRead = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
8785
+ const path = join9(home, ".gemini", "settings.json");
8429
8786
  const current = await loadJsonObject(doRead, path);
8430
8787
  const a = withMcpServer(current);
8431
8788
  const b = withNestedHook(a.next, "SessionEnd", hookCommand("gemini-cli"), { name: "backthread-capture" }, [
@@ -8435,9 +8792,9 @@ async function installGemini(home, deps) {
8435
8792
  return [{ path, wrote: a.changed || b.changed }];
8436
8793
  }
8437
8794
  async function installCodex(home, deps) {
8438
- const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
8795
+ const doRead = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
8439
8796
  const writes = [];
8440
- const tomlPath = join8(home, ".codex", "config.toml");
8797
+ const tomlPath = join9(home, ".codex", "config.toml");
8441
8798
  let toml = "";
8442
8799
  try {
8443
8800
  toml = await doRead(tomlPath);
@@ -8458,7 +8815,7 @@ args = [${MCP_ARGS.map((a) => `"${a}"`).join(", ")}]
8458
8815
  await doWrite(tomlPath, toml + sep + block);
8459
8816
  writes.push({ path: tomlPath, wrote: true });
8460
8817
  }
8461
- const hooksPath = join8(home, ".codex", "hooks.json");
8818
+ const hooksPath = join9(home, ".codex", "hooks.json");
8462
8819
  const current = await loadJsonObject(doRead, hooksPath);
8463
8820
  const h = withNestedHook(current, "Stop", hookCommand("codex"), { timeout: 60 }, [legacyHookCommand("codex")]);
8464
8821
  if (h.changed) await writeJson(deps, hooksPath, h.next);
@@ -8466,12 +8823,12 @@ args = [${MCP_ARGS.map((a) => `"${a}"`).join(", ")}]
8466
8823
  return writes;
8467
8824
  }
8468
8825
  async function installCursor(home, deps) {
8469
- const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
8826
+ const doRead = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
8470
8827
  const nodeBinDir = deps.nodeBinDir ?? dirname3(process.execPath);
8471
8828
  const writes = [];
8472
- const scriptDir = join8(home, ".cursor", "hooks");
8473
- const captureScriptPath = join8(scriptDir, "backthread-capture.sh");
8474
- const mcpScriptPath = join8(scriptDir, "backthread-mcp.sh");
8829
+ const scriptDir = join9(home, ".cursor", "hooks");
8830
+ const captureScriptPath = join9(scriptDir, "backthread-capture.sh");
8831
+ const mcpScriptPath = join9(scriptDir, "backthread-mcp.sh");
8475
8832
  writes.push(
8476
8833
  await writeCursorScript(
8477
8834
  deps,
@@ -8481,12 +8838,12 @@ async function installCursor(home, deps) {
8481
8838
  )
8482
8839
  );
8483
8840
  writes.push(await writeCursorScript(deps, mcpScriptPath, cursorWrapperScript(nodeBinDir, "mcp")));
8484
- const mcpPath = join8(home, ".cursor", "mcp.json");
8841
+ const mcpPath = join9(home, ".cursor", "mcp.json");
8485
8842
  const mcpCurrent = await loadJsonObject(doRead, mcpPath);
8486
8843
  const m = withCursorMcpServer(mcpCurrent, mcpScriptPath);
8487
8844
  if (m.changed) await writeJson(deps, mcpPath, m.next);
8488
8845
  writes.push({ path: mcpPath, wrote: m.changed });
8489
- const hooksPath = join8(home, ".cursor", "hooks.json");
8846
+ const hooksPath = join9(home, ".cursor", "hooks.json");
8490
8847
  const hooksCurrent = await loadJsonObject(doRead, hooksPath);
8491
8848
  const c = withCursorStopHook(hooksCurrent, captureScriptPath);
8492
8849
  if (c.changed) await writeJson(deps, hooksPath, c.next);
@@ -8520,7 +8877,7 @@ function cursorWrapperScript(nodeBinDir, backthreadArgs, latest = false) {
8520
8877
  ].join("\n") + "\n";
8521
8878
  }
8522
8879
  async function writeCursorScript(deps, path, content) {
8523
- const doRead = deps.readFileImpl ?? ((p) => readFile6(p, "utf8"));
8880
+ const doRead = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
8524
8881
  const doChmod = deps.chmodImpl ?? ((p, mode) => chmod5(p, mode));
8525
8882
  let existing = null;
8526
8883
  try {
@@ -8594,7 +8951,7 @@ async function versionGate(agent, deps) {
8594
8951
  return null;
8595
8952
  }
8596
8953
  async function runInstallAgent(agent, deps = {}) {
8597
- const home = deps.home ?? homedir4();
8954
+ const home = deps.home ?? homedir5();
8598
8955
  const versionWarning = await versionGate(agent, deps);
8599
8956
  let writes;
8600
8957
  switch (agent) {
@@ -8636,12 +8993,12 @@ var LEGACY_HOOK_COMMANDS = [
8636
8993
  ];
8637
8994
  var OUR_HOOK_COMMANDS = /* @__PURE__ */ new Set([HOOK_COMMAND, ...LEGACY_HOOK_COMMANDS]);
8638
8995
  async function registerHook(deps = {}) {
8639
- const doReadFile = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
8996
+ const doReadFile = deps.readFileImpl ?? ((p) => readFile8(p, "utf8"));
8640
8997
  const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile6(p, d));
8641
8998
  const doMkdir = deps.mkdirImpl ?? (async (d) => void await mkdir6(d, { recursive: true }));
8642
- const home = deps.home ?? homedir5();
8643
- const settingsDir = join9(home, ".claude");
8644
- const settingsPath = join9(settingsDir, "settings.json");
8999
+ const home = deps.home ?? homedir6();
9000
+ const settingsDir = join10(home, ".claude");
9001
+ const settingsPath = join10(settingsDir, "settings.json");
8645
9002
  let settings = {};
8646
9003
  let raw = null;
8647
9004
  try {
@@ -8753,9 +9110,9 @@ function stripSessionEndHook(settings) {
8753
9110
  return next;
8754
9111
  }
8755
9112
  async function unregisterProjectHook(cwd, deps = {}) {
8756
- const doReadFile = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
9113
+ const doReadFile = deps.readFileImpl ?? ((p) => readFile8(p, "utf8"));
8757
9114
  const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile6(p, d));
8758
- const settingsPath = join9(cwd, ".claude", "settings.json");
9115
+ const settingsPath = join10(cwd, ".claude", "settings.json");
8759
9116
  let raw;
8760
9117
  try {
8761
9118
  raw = await doReadFile(settingsPath);
@@ -9020,7 +9377,7 @@ function normalizeState(raw) {
9020
9377
 
9021
9378
  // src/firstRun.ts
9022
9379
  function firstRunStatePath(env = process.env) {
9023
- return join10(configDir(env), "first-run.json");
9380
+ return join11(configDir(env), "first-run.json");
9024
9381
  }
9025
9382
  function parseFirstRunState(raw) {
9026
9383
  try {
@@ -9039,7 +9396,7 @@ function parseFirstRunState(raw) {
9039
9396
  }
9040
9397
  async function readFirstRunState(env = process.env) {
9041
9398
  try {
9042
- return parseFirstRunState(await readFile8(firstRunStatePath(env), "utf8"));
9399
+ return parseFirstRunState(await readFile9(firstRunStatePath(env), "utf8"));
9043
9400
  } catch {
9044
9401
  return {};
9045
9402
  }
@@ -9214,7 +9571,7 @@ function readStream(stream) {
9214
9571
  async function runCapture(input, deps = {}) {
9215
9572
  const env = deps.env ?? process.env;
9216
9573
  const log = deps.log ?? ((m) => console.error(m));
9217
- const doReadFile = deps.readFileImpl ?? ((p) => readFile9(p, "utf8"));
9574
+ const doReadFile = deps.readFileImpl ?? ((p) => readFile10(p, "utf8"));
9218
9575
  const doReadConfig = deps.readConfigImpl ?? readConfig;
9219
9576
  const fireEnsureAuth = deps.ensureAuthImpl ?? ((e) => {
9220
9577
  void ensureAuth({ env: e }).catch(() => {
@@ -9394,8 +9751,8 @@ async function persistDerived(decisions, repo, config2, decidedAt, ctx) {
9394
9751
 
9395
9752
  // src/fromHook.ts
9396
9753
  import { spawn as spawn2 } from "node:child_process";
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";
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";
9399
9756
  var KNOWN_AGENTS = /* @__PURE__ */ new Set([
9400
9757
  "claude-code",
9401
9758
  "codex",
@@ -9436,7 +9793,7 @@ function normalizeHookInput(payload, _agent) {
9436
9793
  return out;
9437
9794
  }
9438
9795
  function captureStatePath(env = process.env) {
9439
- return join11(configDir(env), "capture-sessions.json");
9796
+ return join12(configDir(env), "capture-sessions.json");
9440
9797
  }
9441
9798
  var MAX_REMEMBERED2 = 200;
9442
9799
  function parseState3(raw) {
@@ -9459,7 +9816,7 @@ function parseState3(raw) {
9459
9816
  }
9460
9817
  async function readState3(env) {
9461
9818
  try {
9462
- return parseState3(await readFile10(captureStatePath(env), "utf8"));
9819
+ return parseState3(await readFile11(captureStatePath(env), "utf8"));
9463
9820
  } catch {
9464
9821
  return { captured: [], watermarks: {} };
9465
9822
  }
@@ -9632,6 +9989,45 @@ function codexStdout(agent, status, outcome) {
9632
9989
  return ack;
9633
9990
  }
9634
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
+
9635
10031
  // ../node_modules/zod/v3/helpers/util.js
9636
10032
  var util;
9637
10033
  (function(util2) {
@@ -14533,8 +14929,8 @@ function uint8ArrayToBase64(bytes) {
14533
14929
  }
14534
14930
  return btoa(binaryString);
14535
14931
  }
14536
- function base64urlToUint8Array(base64url4) {
14537
- const base643 = base64url4.replace(/-/g, "+").replace(/_/g, "/");
14932
+ function base64urlToUint8Array(base64url3) {
14933
+ const base643 = base64url3.replace(/-/g, "+").replace(/_/g, "/");
14538
14934
  const padding = "=".repeat((4 - base643.length % 4) % 4);
14539
14935
  return base64ToUint8Array(base643 + padding);
14540
14936
  }
@@ -14791,7 +15187,7 @@ var safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync($ZodRealError);
14791
15187
  var regexes_exports = {};
14792
15188
  __export(regexes_exports, {
14793
15189
  base64: () => base64,
14794
- base64url: () => base64url2,
15190
+ base64url: () => base64url,
14795
15191
  bigint: () => bigint,
14796
15192
  boolean: () => boolean,
14797
15193
  browserEmail: () => browserEmail,
@@ -14886,7 +15282,7 @@ var mac = (delimiter) => {
14886
15282
  var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
14887
15283
  var cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
14888
15284
  var base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
14889
- var base64url2 = /^[A-Za-z0-9_-]*$/;
15285
+ var base64url = /^[A-Za-z0-9_-]*$/;
14890
15286
  var hostname2 = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
14891
15287
  var domain = /^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
14892
15288
  var httpProtocol = /^https?$/;
@@ -15900,14 +16296,14 @@ var $ZodBase64 = /* @__PURE__ */ $constructor("$ZodBase64", (inst, def) => {
15900
16296
  };
15901
16297
  });
15902
16298
  function isValidBase64URL(data) {
15903
- if (!base64url2.test(data))
16299
+ if (!base64url.test(data))
15904
16300
  return false;
15905
16301
  const base643 = data.replace(/[-_]/g, (c) => c === "-" ? "+" : "/");
15906
16302
  const padded = base643.padEnd(Math.ceil(base643.length / 4) * 4, "=");
15907
16303
  return isValidBase64(padded);
15908
16304
  }
15909
16305
  var $ZodBase64URL = /* @__PURE__ */ $constructor("$ZodBase64URL", (inst, def) => {
15910
- def.pattern ?? (def.pattern = base64url2);
16306
+ def.pattern ?? (def.pattern = base64url);
15911
16307
  $ZodStringFormat.init(inst, def);
15912
16308
  inst._zod.bag.contentEncoding = "base64url";
15913
16309
  inst._zod.check = (payload) => {
@@ -25936,7 +26332,7 @@ __export(external_exports, {
25936
26332
  any: () => any,
25937
26333
  array: () => array,
25938
26334
  base64: () => base642,
25939
- base64url: () => base64url3,
26335
+ base64url: () => base64url2,
25940
26336
  bigint: () => bigint2,
25941
26337
  boolean: () => boolean2,
25942
26338
  catch: () => _catch2,
@@ -26166,7 +26562,7 @@ __export(schemas_exports2, {
26166
26562
  any: () => any,
26167
26563
  array: () => array,
26168
26564
  base64: () => base642,
26169
- base64url: () => base64url3,
26565
+ base64url: () => base64url2,
26170
26566
  bigint: () => bigint2,
26171
26567
  boolean: () => boolean2,
26172
26568
  catch: () => _catch2,
@@ -26788,7 +27184,7 @@ var ZodBase64URL = /* @__PURE__ */ $constructor("ZodBase64URL", (inst, def) => {
26788
27184
  $ZodBase64URL.init(inst, def);
26789
27185
  ZodStringFormat.init(inst, def);
26790
27186
  });
26791
- function base64url3(params) {
27187
+ function base64url2(params) {
26792
27188
  return _base64url(ZodBase64URL, params);
26793
27189
  }
26794
27190
  var ZodE164 = /* @__PURE__ */ $constructor("ZodE164", (inst, def) => {
@@ -33920,15 +34316,15 @@ async function startMcpServer(deps = {}) {
33920
34316
  }
33921
34317
 
33922
34318
  // src/routingStats.ts
33923
- import { join as join12 } from "node:path";
33924
- import { readFile as readFile11, writeFile as writeFile9, mkdir as mkdir9, chmod as chmod8 } from "node:fs/promises";
34319
+ import { join as join13 } from "node:path";
34320
+ import { readFile as readFile12, writeFile as writeFile9, mkdir as mkdir9, chmod as chmod8 } from "node:fs/promises";
33925
34321
  var STATS_FILE = "routing-stats.json";
33926
34322
  function statsPath(env) {
33927
- return join12(configDir(env), STATS_FILE);
34323
+ return join13(configDir(env), STATS_FILE);
33928
34324
  }
33929
34325
  async function readRoutingStats(deps = {}) {
33930
34326
  const env = deps.env ?? process.env;
33931
- const read = deps.readFileImpl ?? readFile11;
34327
+ const read = deps.readFileImpl ?? readFile12;
33932
34328
  try {
33933
34329
  const raw = await read(statsPath(env), "utf8");
33934
34330
  const obj = JSON.parse(raw);
@@ -34001,40 +34397,60 @@ async function runSessionStart(deps = {}) {
34001
34397
  }
34002
34398
 
34003
34399
  // src/bin/backthread.ts
34004
- var USAGE = `backthread \u2014 capture the "why" of your AI-coded changes
34400
+ var USAGE = `backthread \u2014 keep the thread on what your AI agent actually shipped
34005
34401
 
34006
34402
  Usage:
34007
- backthread Set up Backthread (the unified front door \u2014 same as
34008
- \`backthread start\`): trust copy + one-tap auth + your next
34009
- step. Idempotent. [--claim <code>]
34010
- backthread start First-run setup (backs the /backthread:start slash command):
34011
- trust copy + one-tap auth + your next step. Idempotent.
34403
+ backthread [command] [flags]
34404
+
34405
+ Setup
34406
+ backthread Set up Backthread here (the front door): sign in, connect
34407
+ this repo, wire up capture. Idempotent \u2014 re-run it anytime.
34012
34408
  [--claim <code>]
34013
- backthread login Authorize this device (opens your browser)
34014
- backthread login --claim <code>
34015
- Authorize with a single-use claim code from the web app
34016
- (no browser needed \u2014 codes expire in ~10 minutes)
34017
- backthread login --device Headless / SSH login (device-code flow \u2014 coming soon)
34018
- backthread whoami Show the current device's config (token is never printed)
34019
- backthread how <question> Ask how/why something in this repo works \u2014 prints a
34020
- grounded, cited answer from your Backthread decision log
34021
- (backs the /backthread:how slash command). [--cwd <path>]
34409
+ backthread start Same as above, behind the /backthread:start slash command.
34410
+ backthread login Authorize this device (opens your browser; works over SSH \u2014
34411
+ the printed URL opens on any device) [--claim <code>] [--device]
34412
+ backthread logout Sign this device out \u2014 drop the local token, keep the repo link
34413
+ backthread whoami Show this device's config (the token is never printed)
34414
+
34415
+ Ask
34416
+ backthread how <question> Ask how/why something here works \u2014 a grounded, cited answer
34417
+ from your decision log (backs /backthread:how). [--cwd <path>]
34418
+
34419
+ Capture
34022
34420
  backthread capture Capture this session's decisions (run by the SessionEnd/Stop hook)
34023
- backthread capture --from-hook
34024
- Shared multi-agent hook entrypoint: read the hook payload off
34025
- STDIN and capture the named transcript (always exits 0)
34026
- [--agent <codex|cursor|gemini-cli>] [--detach]
34027
- backthread capture --manual Manually capture a session now (the /backthread capture slash command)
34421
+ backthread capture --manual Capture the current session now (the /backthread capture command)
34028
34422
  [--session <id>] [--transcript <path>] [--cwd <dir>]
34029
34423
  backthread mcp Start the MCP server (capture + query tools) over stdio
34030
- backthread install Set up capture for this repo (login + hook + backfill history)
34031
- [--claim <code>] [--skip-auth] [--skip-hook] [--skip-backfill]
34032
- backthread install --agent <codex|cursor|gemini>
34033
- Set up capture for another agent: write its USER-GLOBAL
34034
- MCP server config + session-end capture hook (idempotent)
34035
- backthread help Show this message
34036
34424
 
34037
- Docs: https://app.backthread.dev`;
34425
+ Manage
34426
+ backthread install Set up capture for this repo (login + hook + backfill history)
34427
+ [--claim <code>] [--agent <codex|cursor|gemini>] [--skip-auth]
34428
+ [--skip-hook] [--skip-backfill]
34429
+ backthread update Update a global install to the latest (also -u). npx is
34430
+ always latest already; the plugin updates via /plugin update.
34431
+ backthread doctor Diagnose your setup \u2014 auth, capture hook, connectivity,
34432
+ version, repo. Prints \u2713/\u2717 with fix hints; exits non-zero if broken.
34433
+ backthread version Print the installed version (also --version, -v)
34434
+ backthread help Show this message (also --help, -h)
34435
+
34436
+ Your source never leaves your machine unredacted \u2014 it's checkable in this OSS repo.
34437
+ Docs: https://app.backthread.dev
34438
+ Security: https://backthread.dev/security`;
34439
+ var KNOWN_COMMANDS = [
34440
+ "start",
34441
+ "login",
34442
+ "logout",
34443
+ "whoami",
34444
+ "how",
34445
+ "ask",
34446
+ "capture",
34447
+ "mcp",
34448
+ "install",
34449
+ "update",
34450
+ "doctor",
34451
+ "version",
34452
+ "help"
34453
+ ];
34038
34454
  function parseClaimFlag(rest) {
34039
34455
  const i = rest.indexOf("--claim");
34040
34456
  if (i === -1) return void 0;
@@ -34088,6 +34504,18 @@ async function main(argv, deps = {}) {
34088
34504
  console.log(lines.join("\n"));
34089
34505
  return cfg.device_token ? 0 : 1;
34090
34506
  }
34507
+ case "logout": {
34508
+ const logoutImpl = deps.runLogoutImpl ?? runLogout;
34509
+ const result = await logoutImpl();
34510
+ console.log(result.message);
34511
+ return result.ok ? 0 : 1;
34512
+ }
34513
+ case "doctor": {
34514
+ const doctorImpl = deps.runDoctorImpl ?? runDoctor;
34515
+ const result = await doctorImpl();
34516
+ console.log(result.text);
34517
+ return result.exitCode;
34518
+ }
34091
34519
  case "capture": {
34092
34520
  if (rest.includes("--from-hook")) {
34093
34521
  const raw = await readRawHookInput();
@@ -34164,6 +34592,19 @@ async function main(argv, deps = {}) {
34164
34592
  }
34165
34593
  case void 0:
34166
34594
  return onboarding(rest);
34595
+ case "update":
34596
+ case "--update":
34597
+ case "-u": {
34598
+ const updateImpl = deps.runUpdateImpl ?? runUpdate;
34599
+ const result = await updateImpl();
34600
+ console.log(result.message);
34601
+ return result.ok ? 0 : 1;
34602
+ }
34603
+ case "version":
34604
+ case "--version":
34605
+ case "-v":
34606
+ console.log(cliVersion());
34607
+ return 0;
34167
34608
  case "help":
34168
34609
  case "--help":
34169
34610
  case "-h":
@@ -34171,9 +34612,14 @@ async function main(argv, deps = {}) {
34171
34612
  return 0;
34172
34613
  default:
34173
34614
  if (command.startsWith("-")) return onboarding(argv);
34174
- console.error(`Unknown command: ${command}
34175
-
34176
- ${USAGE}`);
34615
+ {
34616
+ const guess = nearestCommand(command, KNOWN_COMMANDS);
34617
+ const didYouMean = guess ? ` Did you mean \`backthread ${guess}\`?` : "";
34618
+ console.error(
34619
+ `Unknown command: ${command}.${didYouMean}
34620
+ Run \`backthread help\` to see everything backthread can do.`
34621
+ );
34622
+ }
34177
34623
  return 1;
34178
34624
  }
34179
34625
  }
@@ -34184,7 +34630,7 @@ function isEntryPoint() {
34184
34630
  const self = fileURLToPath2(import.meta.url);
34185
34631
  const resolve = (p) => {
34186
34632
  try {
34187
- return realpathSync(p);
34633
+ return realpathSync2(p);
34188
34634
  } catch {
34189
34635
  return p;
34190
34636
  }