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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -12
- package/dist-bundle/backthread.js +743 -297
- 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,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(
|
|
6906
|
+
function buildCliAuthUrl(session, clientPubKey, env = process.env, label) {
|
|
7006
6907
|
const u = new URL("/cli-auth", appBaseUrl(env));
|
|
7007
|
-
u.searchParams.set("
|
|
7008
|
-
u.searchParams.set("
|
|
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: "
|
|
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
|
|
7287
|
-
log(
|
|
7288
|
-
return { ok:
|
|
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
|
|
7294
|
-
const
|
|
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
|
|
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
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
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
|
-
"
|
|
7334
|
-
"
|
|
7335
|
-
"
|
|
7336
|
-
|
|
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
|
|
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
|
|
7712
|
-
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";
|
|
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
|
|
8138
|
+
return join5(configDir(env), "connect-nudge.json");
|
|
7726
8139
|
}
|
|
7727
8140
|
var MAX_REMEMBERED = 50;
|
|
7728
|
-
function
|
|
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
|
|
8152
|
+
async function readState2(env) {
|
|
7740
8153
|
try {
|
|
7741
|
-
return
|
|
8154
|
+
return parseState2(await readFile4(nudgeStatePath(env), "utf8"));
|
|
7742
8155
|
} catch {
|
|
7743
8156
|
return { nudged: [] };
|
|
7744
8157
|
}
|
|
7745
8158
|
}
|
|
7746
|
-
async function
|
|
8159
|
+
async function writeState2(state, env) {
|
|
7747
8160
|
try {
|
|
7748
8161
|
const dir = configDir(env);
|
|
7749
|
-
await
|
|
7750
|
-
await
|
|
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
|
|
7754
|
-
await
|
|
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
|
|
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
|
|
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
|
|
7806
|
-
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";
|
|
7807
8220
|
|
|
7808
8221
|
// src/install.ts
|
|
7809
|
-
import { readFile as
|
|
7810
|
-
import { homedir as
|
|
7811
|
-
import { join as
|
|
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
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
|
7985
|
-
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";
|
|
7986
8343
|
|
|
7987
8344
|
// src/sweepLedger.ts
|
|
7988
|
-
import { join as
|
|
7989
|
-
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";
|
|
7990
8347
|
var MAX_PROCESSED = 2e4;
|
|
7991
8348
|
function sweepStatePath(env = process.env) {
|
|
7992
|
-
return
|
|
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
|
|
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
|
|
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 :
|
|
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) =>
|
|
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 ??
|
|
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 =
|
|
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 =
|
|
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(
|
|
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:
|
|
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
|
|
8330
|
-
import { join as
|
|
8331
|
-
import { readFile as
|
|
8332
|
-
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);
|
|
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) =>
|
|
8428
|
-
const path =
|
|
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) =>
|
|
8795
|
+
const doRead = deps.readFileImpl ?? ((p) => readFile7(p, "utf8"));
|
|
8439
8796
|
const writes = [];
|
|
8440
|
-
const tomlPath =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
8473
|
-
const captureScriptPath =
|
|
8474
|
-
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");
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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 ??
|
|
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) =>
|
|
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 ??
|
|
8643
|
-
const settingsDir =
|
|
8644
|
-
const settingsPath =
|
|
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) =>
|
|
9113
|
+
const doReadFile = deps.readFileImpl ?? ((p) => readFile8(p, "utf8"));
|
|
8757
9114
|
const doWriteFile = deps.writeFileImpl ?? ((p, d) => writeFile6(p, d));
|
|
8758
|
-
const settingsPath =
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
9398
|
-
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";
|
|
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
|
|
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
|
|
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(
|
|
14537
|
-
const base643 =
|
|
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: () =>
|
|
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
|
|
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 (!
|
|
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 =
|
|
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: () =>
|
|
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: () =>
|
|
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
|
|
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
|
|
33924
|
-
import { readFile as
|
|
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
|
|
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 ??
|
|
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
|
|
34400
|
+
var USAGE = `backthread \u2014 keep the thread on what your AI agent actually shipped
|
|
34005
34401
|
|
|
34006
34402
|
Usage:
|
|
34007
|
-
backthread
|
|
34008
|
-
|
|
34009
|
-
|
|
34010
|
-
backthread
|
|
34011
|
-
|
|
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
|
|
34014
|
-
backthread login
|
|
34015
|
-
|
|
34016
|
-
|
|
34017
|
-
backthread
|
|
34018
|
-
|
|
34019
|
-
|
|
34020
|
-
|
|
34021
|
-
(backs
|
|
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 --
|
|
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
|
-
|
|
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
|
-
|
|
34175
|
-
|
|
34176
|
-
${
|
|
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
|
|
34633
|
+
return realpathSync2(p);
|
|
34188
34634
|
} catch {
|
|
34189
34635
|
return p;
|
|
34190
34636
|
}
|