backthread 0.1.1 → 0.1.3
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/README.md +13 -4
- package/dist-bundle/backthread.js +86 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ sessions, so you can ask *"how does X work?"* and stay oriented without
|
|
|
14
14
|
spelunking through PRs. The decisions become a live **"How it works"** diagram
|
|
15
15
|
and changelog at [backthread.dev](https://backthread.dev).
|
|
16
16
|
|
|
17
|
-
## Your code never leaves your machine
|
|
17
|
+
## Your source code never leaves your machine
|
|
18
18
|
|
|
19
19
|
Backthread reads your agent **transcripts**, not your repo. Before anything is
|
|
20
20
|
sent, the CLI redacts every transcript **locally**:
|
|
@@ -23,9 +23,18 @@ sent, the CLI redacts every transcript **locally**:
|
|
|
23
23
|
- **Keeps** only natural-language prompts and the agent's reasoning.
|
|
24
24
|
- **Redacts** any fenced code block to `[code redacted]`.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
So no source code and no tool I/O ever leave your machine. Because the default
|
|
27
|
+
path runs inference on our servers, what *does* leave is the **redacted
|
|
28
|
+
transcript** — natural-language prose only. The Worker re-runs the fenced-code
|
|
29
|
+
scrub server-side as a fail-closed backstop, derives the **decisions**, and
|
|
30
|
+
discards the transcript right after — processed in memory, never stored. Only
|
|
31
|
+
the decisions are persisted.
|
|
32
|
+
|
|
33
|
+
That's a weaker claim than the bring-your-own-key path — where nothing but the
|
|
34
|
+
derived decisions ever leaves your machine — which is designed and coming. We'd
|
|
35
|
+
rather say so than paper over it. The redaction fence is open source
|
|
36
|
+
([`@backthread/redact`](https://www.npmjs.com/package/@backthread/redact)) so you
|
|
37
|
+
can verify it — read more at [backthread.dev/security](https://backthread.dev/security).
|
|
29
38
|
|
|
30
39
|
## Quick start
|
|
31
40
|
|
|
@@ -7380,6 +7380,89 @@ function sessionTimestamp(records) {
|
|
|
7380
7380
|
}
|
|
7381
7381
|
return latestIso;
|
|
7382
7382
|
}
|
|
7383
|
+
function stripLeadingSlashes(p) {
|
|
7384
|
+
let i = 0;
|
|
7385
|
+
while (i < p.length && p[i] === "/") i += 1;
|
|
7386
|
+
return p.slice(i);
|
|
7387
|
+
}
|
|
7388
|
+
function isAbsolute(p) {
|
|
7389
|
+
return p.startsWith("/");
|
|
7390
|
+
}
|
|
7391
|
+
function isForeignRelativePath(p) {
|
|
7392
|
+
if (p.startsWith("~")) return true;
|
|
7393
|
+
if (p.startsWith("\\")) return true;
|
|
7394
|
+
if (/^[A-Za-z]:[\\/]/.test(p)) return true;
|
|
7395
|
+
const stripped = p.replace(/^(?:\.[\\/])+/, "");
|
|
7396
|
+
return /^\.\.(?:[\\/]|$)/.test(stripped);
|
|
7397
|
+
}
|
|
7398
|
+
function relativizeUnder(abs, root) {
|
|
7399
|
+
const trimmedRoot = root.replace(/\/+$/, "");
|
|
7400
|
+
if (trimmedRoot.length === 0) return null;
|
|
7401
|
+
if (abs === trimmedRoot) return "";
|
|
7402
|
+
const prefix = trimmedRoot + "/";
|
|
7403
|
+
if (!abs.startsWith(prefix)) return null;
|
|
7404
|
+
return stripLeadingSlashes(abs.slice(trimmedRoot.length));
|
|
7405
|
+
}
|
|
7406
|
+
function pathsFromRecord(rec) {
|
|
7407
|
+
if (!rec || typeof rec !== "object") return [];
|
|
7408
|
+
const out = [];
|
|
7409
|
+
const r = rec;
|
|
7410
|
+
const pushFromInput = (input) => {
|
|
7411
|
+
if (!input || typeof input !== "object") return;
|
|
7412
|
+
const i = input;
|
|
7413
|
+
for (const v of [i.file_path, i.path, i.notebook_path, i.cwd]) {
|
|
7414
|
+
if (typeof v === "string" && v.trim().length > 0) out.push(v.trim());
|
|
7415
|
+
}
|
|
7416
|
+
};
|
|
7417
|
+
const content = r.message?.content;
|
|
7418
|
+
if (Array.isArray(content)) {
|
|
7419
|
+
for (const raw of content) {
|
|
7420
|
+
if (!raw || typeof raw !== "object") continue;
|
|
7421
|
+
const block = raw;
|
|
7422
|
+
if (block.type === "tool_use") pushFromInput(block.input);
|
|
7423
|
+
}
|
|
7424
|
+
}
|
|
7425
|
+
if (r.payload && typeof r.payload === "object" && r.payload.type === "function_call") {
|
|
7426
|
+
const args = r.payload.arguments;
|
|
7427
|
+
if (typeof args === "string") {
|
|
7428
|
+
try {
|
|
7429
|
+
pushFromInput(JSON.parse(args));
|
|
7430
|
+
} catch {
|
|
7431
|
+
}
|
|
7432
|
+
} else {
|
|
7433
|
+
pushFromInput(args);
|
|
7434
|
+
}
|
|
7435
|
+
}
|
|
7436
|
+
return out;
|
|
7437
|
+
}
|
|
7438
|
+
function codexSessionCwd(records) {
|
|
7439
|
+
for (const raw of records) {
|
|
7440
|
+
if (!raw || typeof raw !== "object") continue;
|
|
7441
|
+
const rec = raw;
|
|
7442
|
+
if (rec.type !== "session_meta") continue;
|
|
7443
|
+
const cwd = rec.payload?.cwd;
|
|
7444
|
+
if (typeof cwd === "string" && cwd.trim().length > 0) return cwd.trim();
|
|
7445
|
+
}
|
|
7446
|
+
return null;
|
|
7447
|
+
}
|
|
7448
|
+
function sessionPaths(records, repoRoot) {
|
|
7449
|
+
const root = (repoRoot && repoRoot.trim().length > 0 ? repoRoot.trim() : codexSessionCwd(records)) ?? null;
|
|
7450
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7451
|
+
for (const rec of records) {
|
|
7452
|
+
for (const p of pathsFromRecord(rec)) {
|
|
7453
|
+
if (isAbsolute(p)) {
|
|
7454
|
+
if (root === null) continue;
|
|
7455
|
+
const rel = relativizeUnder(p, root);
|
|
7456
|
+
if (rel === null || rel.length === 0) continue;
|
|
7457
|
+
seen.add(rel);
|
|
7458
|
+
} else if (!isForeignRelativePath(p)) {
|
|
7459
|
+
const rel = p.replace(/^(?:\.\/)+/, "");
|
|
7460
|
+
if (rel.length > 0) seen.add(rel);
|
|
7461
|
+
}
|
|
7462
|
+
}
|
|
7463
|
+
}
|
|
7464
|
+
return Array.from(seen).sort();
|
|
7465
|
+
}
|
|
7383
7466
|
|
|
7384
7467
|
// src/repo.ts
|
|
7385
7468
|
import { execFileSync } from "node:child_process";
|
|
@@ -7459,6 +7542,7 @@ async function serverInfer(transcript, config2, opts = {}) {
|
|
|
7459
7542
|
body.persist = true;
|
|
7460
7543
|
body.repo = { owner: opts.repo.owner, name: opts.repo.name };
|
|
7461
7544
|
if (opts.decidedAt) body.decidedAt = opts.decidedAt;
|
|
7545
|
+
if (opts.filePaths && opts.filePaths.length > 0) body.filePaths = opts.filePaths;
|
|
7462
7546
|
}
|
|
7463
7547
|
let res;
|
|
7464
7548
|
try {
|
|
@@ -8263,6 +8347,7 @@ async function runCapture(input, deps = {}) {
|
|
|
8263
8347
|
const records = parseJsonl(rawTranscript);
|
|
8264
8348
|
const redacted = redactTranscript(records);
|
|
8265
8349
|
const decidedAt = sessionTimestamp(records) ?? void 0;
|
|
8350
|
+
const filePaths = sessionPaths(records, input.cwd);
|
|
8266
8351
|
const sessionId = redacted.sessionId ?? input.session_id ?? null;
|
|
8267
8352
|
if (redacted.turns.length === 0) {
|
|
8268
8353
|
return {
|
|
@@ -8281,6 +8366,7 @@ async function runCapture(input, deps = {}) {
|
|
|
8281
8366
|
env,
|
|
8282
8367
|
fetchImpl: deps.fetchImpl,
|
|
8283
8368
|
decidedAt,
|
|
8369
|
+
filePaths,
|
|
8284
8370
|
...repo ? { persist: true, repo } : {}
|
|
8285
8371
|
});
|
|
8286
8372
|
if (!result.ok) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backthread",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Backthread CLI — capture the
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Backthread CLI — capture the why behind your AI-coded changes from your Claude Code sessions, and ask how your codebase works without leaving the terminal. Source code and tool I/O are redacted locally before anything leaves your machine.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Backthread",
|
|
7
7
|
"homepage": "https://backthread.dev",
|