backthread 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist-bundle/backthread.js +89 -25
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "backthread",
|
|
3
3
|
"displayName": "Backthread",
|
|
4
4
|
"description": "Backthread helps you understand your codebase while AI ships features. It captures the why behind every Claude Code session so you can ask \"how does X work?\" without digging through PRs.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.9.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Backthread"
|
|
8
8
|
},
|
|
@@ -34092,8 +34092,49 @@ var StdioServerTransport = class {
|
|
|
34092
34092
|
};
|
|
34093
34093
|
|
|
34094
34094
|
// src/query.ts
|
|
34095
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
34095
34096
|
var DEFAULT_QUESTION = "How does this project work?";
|
|
34096
|
-
var GROUNDED_ASK_TIMEOUT_MS =
|
|
34097
|
+
var GROUNDED_ASK_TIMEOUT_MS = 45e3;
|
|
34098
|
+
var GROUNDED_ASK_ATTEMPTS = 2;
|
|
34099
|
+
var defaultGitRunner2 = (args, cwd) => {
|
|
34100
|
+
try {
|
|
34101
|
+
execFileSync3("git", args, { cwd, stdio: ["ignore", "ignore", "ignore"], timeout: 3e3 });
|
|
34102
|
+
return 0;
|
|
34103
|
+
} catch (e) {
|
|
34104
|
+
const status = e.status;
|
|
34105
|
+
return typeof status === "number" ? status : 128;
|
|
34106
|
+
}
|
|
34107
|
+
};
|
|
34108
|
+
var SHA_RE = /^[0-9a-f]{7,40}$/i;
|
|
34109
|
+
function countCitationsAfterCheckout(citations, cwd, runGit = defaultGitRunner2) {
|
|
34110
|
+
const stale = /* @__PURE__ */ new Map();
|
|
34111
|
+
for (const c of citations) {
|
|
34112
|
+
if (c.anchorSha && SHA_RE.test(c.anchorSha)) stale.set(c.anchorSha, false);
|
|
34113
|
+
}
|
|
34114
|
+
if (stale.size === 0) return 0;
|
|
34115
|
+
for (const sha of stale.keys()) {
|
|
34116
|
+
const exists = runGit(["rev-parse", "--quiet", "--verify", `${sha}^{commit}`], cwd);
|
|
34117
|
+
if (exists === 1) {
|
|
34118
|
+
stale.set(sha, true);
|
|
34119
|
+
continue;
|
|
34120
|
+
}
|
|
34121
|
+
if (exists !== 0) return 0;
|
|
34122
|
+
const contained = runGit(["merge-base", "--is-ancestor", sha, "HEAD"], cwd);
|
|
34123
|
+
if (contained === 1) {
|
|
34124
|
+
stale.set(sha, true);
|
|
34125
|
+
continue;
|
|
34126
|
+
}
|
|
34127
|
+
if (contained !== 0) return 0;
|
|
34128
|
+
}
|
|
34129
|
+
let n = 0;
|
|
34130
|
+
for (const c of citations) {
|
|
34131
|
+
if (c.anchorSha && stale.get(c.anchorSha)) n += 1;
|
|
34132
|
+
}
|
|
34133
|
+
return n;
|
|
34134
|
+
}
|
|
34135
|
+
function stalenessNote(n) {
|
|
34136
|
+
return `Note: ${n} of the decisions cited above landed after your checkout \u2014 this answer reflects the tracked branch.`;
|
|
34137
|
+
}
|
|
34097
34138
|
function parseSlug2(slug) {
|
|
34098
34139
|
const parts = slug.trim().replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
34099
34140
|
if (parts.length !== 2) return null;
|
|
@@ -34140,33 +34181,46 @@ async function queryDecisions(input, deps = {}) {
|
|
|
34140
34181
|
}
|
|
34141
34182
|
const deepLink = buildRepoDeepLink(repo.owner, repo.name, env);
|
|
34142
34183
|
const question = typeof input.question === "string" && input.question.trim().length > 0 ? input.question.trim() : DEFAULT_QUESTION;
|
|
34143
|
-
const ac = new AbortController();
|
|
34144
|
-
const timer = setTimeout(() => ac.abort(), GROUNDED_ASK_TIMEOUT_MS);
|
|
34145
34184
|
let res;
|
|
34146
|
-
|
|
34147
|
-
|
|
34148
|
-
|
|
34149
|
-
|
|
34150
|
-
|
|
34151
|
-
|
|
34152
|
-
|
|
34153
|
-
|
|
34154
|
-
|
|
34155
|
-
|
|
34156
|
-
|
|
34157
|
-
|
|
34158
|
-
|
|
34159
|
-
|
|
34160
|
-
|
|
34161
|
-
|
|
34185
|
+
let failDetail = "";
|
|
34186
|
+
for (let attempt = 1; attempt <= GROUNDED_ASK_ATTEMPTS; attempt++) {
|
|
34187
|
+
const ac = new AbortController();
|
|
34188
|
+
const timer = setTimeout(() => ac.abort(), GROUNDED_ASK_TIMEOUT_MS);
|
|
34189
|
+
try {
|
|
34190
|
+
res = await doFetch(buildGroundedAskUrl(env), {
|
|
34191
|
+
method: "POST",
|
|
34192
|
+
headers: {
|
|
34193
|
+
// Bearer device token — never logged.
|
|
34194
|
+
Authorization: `Bearer ${config2.device_token}`,
|
|
34195
|
+
"Content-Type": "application/json",
|
|
34196
|
+
...versionHeaders()
|
|
34197
|
+
// x-backthread-version — server-side compat guard
|
|
34198
|
+
},
|
|
34199
|
+
// The server accepts `repo` as an "owner/name" slug (it re-resolves + gates).
|
|
34200
|
+
body: JSON.stringify({ question, repo: `${repo.owner}/${repo.name}` }),
|
|
34201
|
+
signal: ac.signal
|
|
34202
|
+
});
|
|
34203
|
+
if (res.status >= 500 && attempt < GROUNDED_ASK_ATTEMPTS) {
|
|
34204
|
+
failDetail = `grounded-ask rejected (${res.status})`;
|
|
34205
|
+
res = void 0;
|
|
34206
|
+
continue;
|
|
34207
|
+
}
|
|
34208
|
+
break;
|
|
34209
|
+
} catch (e) {
|
|
34210
|
+
const aborted2 = e.name === "AbortError";
|
|
34211
|
+
failDetail = aborted2 ? `grounded-ask timed out after ${GROUNDED_ASK_TIMEOUT_MS / 1e3}s` : `grounded-ask request failed: ${e.message}`;
|
|
34212
|
+
res = void 0;
|
|
34213
|
+
} finally {
|
|
34214
|
+
clearTimeout(timer);
|
|
34215
|
+
}
|
|
34216
|
+
}
|
|
34217
|
+
if (!res) {
|
|
34162
34218
|
return {
|
|
34163
34219
|
status: "read-failed",
|
|
34164
|
-
detail:
|
|
34220
|
+
detail: `${failDetail} (after ${GROUNDED_ASK_ATTEMPTS} attempts) \u2014 try again.`,
|
|
34165
34221
|
repo,
|
|
34166
34222
|
deepLink
|
|
34167
34223
|
};
|
|
34168
|
-
} finally {
|
|
34169
|
-
clearTimeout(timer);
|
|
34170
34224
|
}
|
|
34171
34225
|
let payload;
|
|
34172
34226
|
try {
|
|
@@ -34194,13 +34248,22 @@ async function queryDecisions(input, deps = {}) {
|
|
|
34194
34248
|
};
|
|
34195
34249
|
}
|
|
34196
34250
|
const upgrade = typeof rec.upgrade === "string" && rec.upgrade.length > 0 ? rec.upgrade : void 0;
|
|
34251
|
+
const citations = normalizeCitations(rec.citations);
|
|
34252
|
+
let renderedAnswer = answer;
|
|
34253
|
+
try {
|
|
34254
|
+
const n = countCitationsAfterCheckout(citations, input.cwd ?? process.cwd(), deps.runGitImpl);
|
|
34255
|
+
if (n > 0) renderedAnswer = `${answer}
|
|
34256
|
+
|
|
34257
|
+
${stalenessNote(n)}`;
|
|
34258
|
+
} catch {
|
|
34259
|
+
}
|
|
34197
34260
|
return {
|
|
34198
34261
|
status: "ok",
|
|
34199
34262
|
detail: `grounded answer (${typeof rec.coverage === "string" ? rec.coverage : "partial"} coverage)`,
|
|
34200
34263
|
repo,
|
|
34201
|
-
answer,
|
|
34264
|
+
answer: renderedAnswer,
|
|
34202
34265
|
coverage: typeof rec.coverage === "string" ? rec.coverage : void 0,
|
|
34203
|
-
citations
|
|
34266
|
+
citations,
|
|
34204
34267
|
inferredSpans: Array.isArray(rec.inferredSpans) ? rec.inferredSpans.map(String) : [],
|
|
34205
34268
|
// Prefer the server's deepLink; fall back to the locally-built one.
|
|
34206
34269
|
deepLink: typeof rec.deepLink === "string" && rec.deepLink.length > 0 ? rec.deepLink : deepLink,
|
|
@@ -34220,7 +34283,8 @@ function normalizeCitations(raw) {
|
|
|
34220
34283
|
title: String(r.title ?? ""),
|
|
34221
34284
|
url: String(r.url ?? ""),
|
|
34222
34285
|
moduleIds: Array.isArray(r.moduleIds) ? r.moduleIds.map(String) : [],
|
|
34223
|
-
decidedAt: typeof r.decidedAt === "string" ? r.decidedAt : null
|
|
34286
|
+
decidedAt: typeof r.decidedAt === "string" ? r.decidedAt : null,
|
|
34287
|
+
anchorSha: typeof r.anchorSha === "string" ? r.anchorSha : null
|
|
34224
34288
|
};
|
|
34225
34289
|
});
|
|
34226
34290
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backthread",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Backthread keeps the thread on what your AI coding agent ships — it captures the why behind every change and turns it into a living 'How it works' view of your codebase you can actually query.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Backthread",
|