brain0 0.1.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/README.md +36 -0
- package/bin/brain0.js +269 -0
- package/package.json +53 -0
- package/src/lib.mjs +72 -0
- package/vendor/gui/assets/BufferResource-VGpCfGWQ.js +186 -0
- package/vendor/gui/assets/BufferResource-VGpCfGWQ.js.map +1 -0
- package/vendor/gui/assets/CanvasRenderer-fCJxZSuL.js +2 -0
- package/vendor/gui/assets/CanvasRenderer-fCJxZSuL.js.map +1 -0
- package/vendor/gui/assets/Filter-DBNO82oR.js +2 -0
- package/vendor/gui/assets/Filter-DBNO82oR.js.map +1 -0
- package/vendor/gui/assets/RenderTargetSystem-DxpnxrEz.js +185 -0
- package/vendor/gui/assets/RenderTargetSystem-DxpnxrEz.js.map +1 -0
- package/vendor/gui/assets/WebGLRenderer-DyKdZImF.js +157 -0
- package/vendor/gui/assets/WebGLRenderer-DyKdZImF.js.map +1 -0
- package/vendor/gui/assets/WebGPURenderer-DkU0IC2y.js +42 -0
- package/vendor/gui/assets/WebGPURenderer-DkU0IC2y.js.map +1 -0
- package/vendor/gui/assets/browserAll-Cx9RpU85.js +15 -0
- package/vendor/gui/assets/browserAll-Cx9RpU85.js.map +1 -0
- package/vendor/gui/assets/index-QopGLZI_.js +256 -0
- package/vendor/gui/assets/index-QopGLZI_.js.map +1 -0
- package/vendor/gui/assets/webworkerAll-DTnGa_dQ.js +84 -0
- package/vendor/gui/assets/webworkerAll-DTnGa_dQ.js.map +1 -0
- package/vendor/gui/brain0-logo.svg +1 -0
- package/vendor/gui/brain0-wordmark.svg +1 -0
- package/vendor/gui/index.html +506 -0
- package/vendor/server/server.js +1907 -0
|
@@ -0,0 +1,1907 @@
|
|
|
1
|
+
// packages/server/dist/server.js
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { resolve as resolve2 } from "node:path";
|
|
8
|
+
|
|
9
|
+
// packages/server/dist/askcache.js
|
|
10
|
+
function normalizeQuery(q) {
|
|
11
|
+
return q.trim().toLowerCase().replace(/\s+/g, " ");
|
|
12
|
+
}
|
|
13
|
+
var BoundedCache = class {
|
|
14
|
+
max;
|
|
15
|
+
map = /* @__PURE__ */ new Map();
|
|
16
|
+
constructor(max = 100) {
|
|
17
|
+
this.max = max;
|
|
18
|
+
}
|
|
19
|
+
get(key) {
|
|
20
|
+
return this.map.get(key);
|
|
21
|
+
}
|
|
22
|
+
set(key, value) {
|
|
23
|
+
if (this.map.has(key))
|
|
24
|
+
this.map.delete(key);
|
|
25
|
+
this.map.set(key, value);
|
|
26
|
+
if (this.map.size > this.max) {
|
|
27
|
+
const oldest = this.map.keys().next().value;
|
|
28
|
+
if (oldest !== void 0)
|
|
29
|
+
this.map.delete(oldest);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
get size() {
|
|
33
|
+
return this.map.size;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// packages/server/dist/staticfiles.js
|
|
38
|
+
import { normalize, resolve, sep } from "node:path";
|
|
39
|
+
var CONTENT_TYPES = {
|
|
40
|
+
".html": "text/html; charset=utf-8",
|
|
41
|
+
".js": "text/javascript; charset=utf-8",
|
|
42
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
43
|
+
".css": "text/css; charset=utf-8",
|
|
44
|
+
".svg": "image/svg+xml",
|
|
45
|
+
".json": "application/json",
|
|
46
|
+
".map": "application/json",
|
|
47
|
+
".png": "image/png",
|
|
48
|
+
".ico": "image/x-icon",
|
|
49
|
+
".woff": "font/woff",
|
|
50
|
+
".woff2": "font/woff2",
|
|
51
|
+
".txt": "text/plain; charset=utf-8"
|
|
52
|
+
};
|
|
53
|
+
function contentTypeFor(path) {
|
|
54
|
+
const dot = path.lastIndexOf(".");
|
|
55
|
+
const ext = dot === -1 ? "" : path.slice(dot).toLowerCase();
|
|
56
|
+
return CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
57
|
+
}
|
|
58
|
+
function safeJoin(root, urlPathname) {
|
|
59
|
+
let decoded;
|
|
60
|
+
try {
|
|
61
|
+
decoded = decodeURIComponent(urlPathname);
|
|
62
|
+
} catch {
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
if (decoded.includes("\0"))
|
|
66
|
+
return void 0;
|
|
67
|
+
if (decoded.split("/").some((seg) => seg === ".."))
|
|
68
|
+
return void 0;
|
|
69
|
+
const rootAbs = resolve(root);
|
|
70
|
+
const joined = resolve(rootAbs, "." + normalize("/" + decoded));
|
|
71
|
+
return joined === rootAbs || joined.startsWith(rootAbs + sep) ? joined : void 0;
|
|
72
|
+
}
|
|
73
|
+
function cacheControlFor(urlPathname) {
|
|
74
|
+
return urlPathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// packages/shared/dist/risk.js
|
|
78
|
+
var LOW = 0.34;
|
|
79
|
+
var HIGH = 0.66;
|
|
80
|
+
var APOSTERIORI_EVIDENCE = 0.05;
|
|
81
|
+
function clampUnit(value) {
|
|
82
|
+
return Math.max(0, Math.min(1, value));
|
|
83
|
+
}
|
|
84
|
+
function fusedScore(risk) {
|
|
85
|
+
const a = clampUnit(risk.apriori);
|
|
86
|
+
const p = clampUnit(risk.aposteriori);
|
|
87
|
+
return 1 - (1 - a) * (1 - p);
|
|
88
|
+
}
|
|
89
|
+
function riskTransition(risk) {
|
|
90
|
+
const a = clampUnit(risk.apriori);
|
|
91
|
+
const p = clampUnit(risk.aposteriori);
|
|
92
|
+
if (p < APOSTERIORI_EVIDENCE)
|
|
93
|
+
return "pending";
|
|
94
|
+
const lookedSafe = a < LOW;
|
|
95
|
+
const lookedRisky = a >= HIGH;
|
|
96
|
+
const provedDangerous = p >= HIGH;
|
|
97
|
+
const provedSafe = p < LOW;
|
|
98
|
+
if (lookedSafe && provedDangerous)
|
|
99
|
+
return "safe_to_dangerous";
|
|
100
|
+
if (lookedRisky && provedDangerous)
|
|
101
|
+
return "confirmed_dangerous";
|
|
102
|
+
if (lookedRisky && provedSafe)
|
|
103
|
+
return "overestimated_risk";
|
|
104
|
+
if (lookedSafe && provedSafe)
|
|
105
|
+
return "stable";
|
|
106
|
+
return provedDangerous ? "confirmed_dangerous" : "stable";
|
|
107
|
+
}
|
|
108
|
+
function hslToRgb(hDeg, s, l) {
|
|
109
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
110
|
+
const h = hDeg / 60;
|
|
111
|
+
const x = c * (1 - Math.abs(h % 2 - 1));
|
|
112
|
+
let r1 = 0;
|
|
113
|
+
let g1 = 0;
|
|
114
|
+
let b1 = 0;
|
|
115
|
+
const sextant = Math.floor(h);
|
|
116
|
+
if (sextant === 0)
|
|
117
|
+
[r1, g1, b1] = [c, x, 0];
|
|
118
|
+
else if (sextant === 1)
|
|
119
|
+
[r1, g1, b1] = [x, c, 0];
|
|
120
|
+
else if (sextant === 2)
|
|
121
|
+
[r1, g1, b1] = [0, c, x];
|
|
122
|
+
else if (sextant === 3)
|
|
123
|
+
[r1, g1, b1] = [0, x, c];
|
|
124
|
+
else if (sextant === 4)
|
|
125
|
+
[r1, g1, b1] = [x, 0, c];
|
|
126
|
+
else
|
|
127
|
+
[r1, g1, b1] = [c, 0, x];
|
|
128
|
+
const m = l - c / 2;
|
|
129
|
+
return {
|
|
130
|
+
r: Math.round((r1 + m) * 255),
|
|
131
|
+
g: Math.round((g1 + m) * 255),
|
|
132
|
+
b: Math.round((b1 + m) * 255)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function hueSweepRgb(t) {
|
|
136
|
+
const clamped = clampUnit(t);
|
|
137
|
+
return hslToRgb(120 * (1 - clamped), 1, 0.5);
|
|
138
|
+
}
|
|
139
|
+
function toHex({ r, g, b }) {
|
|
140
|
+
const h = (n) => n.toString(16).padStart(2, "0");
|
|
141
|
+
return `#${h(r)}${h(g)}${h(b)}`;
|
|
142
|
+
}
|
|
143
|
+
function riskColor(risk) {
|
|
144
|
+
const fused = fusedScore(risk);
|
|
145
|
+
const rgb = hueSweepRgb(fused);
|
|
146
|
+
return { fused, rgb, hex: toHex(rgb), transition: riskTransition(risk) };
|
|
147
|
+
}
|
|
148
|
+
function isGoldSignal(risk) {
|
|
149
|
+
return riskTransition(risk) === "safe_to_dangerous";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// packages/shared/dist/payload.js
|
|
153
|
+
import { readFile } from "node:fs/promises";
|
|
154
|
+
import { join } from "node:path";
|
|
155
|
+
var REF_SCHEME = "blake3:";
|
|
156
|
+
var FsPayloadReader = class {
|
|
157
|
+
root;
|
|
158
|
+
constructor(root) {
|
|
159
|
+
this.root = root;
|
|
160
|
+
}
|
|
161
|
+
pathFor(ref) {
|
|
162
|
+
const hex = ref.startsWith(REF_SCHEME) ? ref.slice(REF_SCHEME.length) : ref;
|
|
163
|
+
const shard = hex.slice(0, 2);
|
|
164
|
+
const rest = hex.slice(2);
|
|
165
|
+
return join(this.root, shard, `${rest}.blob`);
|
|
166
|
+
}
|
|
167
|
+
/** Fetch a payload as text, or `undefined` if it is not present. */
|
|
168
|
+
async getText(ref) {
|
|
169
|
+
try {
|
|
170
|
+
return await readFile(this.pathFor(ref), "utf8");
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (err.code === "ENOENT")
|
|
173
|
+
return void 0;
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// packages/shared/dist/graph.js
|
|
180
|
+
var LEVELS = ["repo", "module", "file", "symbol"];
|
|
181
|
+
function buildGraphSnapshot(store2, repo2) {
|
|
182
|
+
const nodes = [];
|
|
183
|
+
const edges = [];
|
|
184
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
185
|
+
const commitTaskIds = /* @__PURE__ */ new Set();
|
|
186
|
+
const taskCache = /* @__PURE__ */ new Map();
|
|
187
|
+
const getTask = (id) => {
|
|
188
|
+
if (!taskCache.has(id))
|
|
189
|
+
taskCache.set(id, store2.getTask(id));
|
|
190
|
+
return taskCache.get(id);
|
|
191
|
+
};
|
|
192
|
+
const isCommitTask = (task) => task.sourceAdapter === void 0;
|
|
193
|
+
const pushEdge = (edge) => {
|
|
194
|
+
const key = `${edge.kind}|${edge.src}|${edge.dst}`;
|
|
195
|
+
if (!edgeKeys.has(key)) {
|
|
196
|
+
edgeKeys.add(key);
|
|
197
|
+
edges.push(edge);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
for (const level of LEVELS) {
|
|
201
|
+
for (const artifact of store2.listArtifacts(repo2, level)) {
|
|
202
|
+
nodes.push({
|
|
203
|
+
id: artifact.id,
|
|
204
|
+
kind: "artifact",
|
|
205
|
+
level: artifact.level,
|
|
206
|
+
label: artifact.qualifiedPath,
|
|
207
|
+
risk: artifact.risk,
|
|
208
|
+
colorHex: riskColor(artifact.risk).hex
|
|
209
|
+
});
|
|
210
|
+
for (const edge of store2.outEdges("artifact_contains", artifact.id)) {
|
|
211
|
+
pushEdge({
|
|
212
|
+
kind: "artifact_contains",
|
|
213
|
+
src: String(edge.parent ?? ""),
|
|
214
|
+
dst: String(edge.child ?? "")
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
for (const edge of store2.inEdges("task_modifies_artifact", artifact.id)) {
|
|
218
|
+
const taskId = String(edge.task ?? "");
|
|
219
|
+
if (!taskId)
|
|
220
|
+
continue;
|
|
221
|
+
const task = getTask(taskId);
|
|
222
|
+
if (!task || !isCommitTask(task))
|
|
223
|
+
continue;
|
|
224
|
+
commitTaskIds.add(taskId);
|
|
225
|
+
pushEdge({ kind: "task_modifies_artifact", src: taskId, dst: artifact.id });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const taskId of commitTaskIds) {
|
|
230
|
+
const task = getTask(taskId);
|
|
231
|
+
if (task) {
|
|
232
|
+
nodes.push({
|
|
233
|
+
id: taskId,
|
|
234
|
+
kind: "task",
|
|
235
|
+
label: taskId,
|
|
236
|
+
ref: task.sessionId,
|
|
237
|
+
// observer/commit tasks carry the git commit SHA as their session id
|
|
238
|
+
timestamp: task.createdAt,
|
|
239
|
+
author: task.author.name,
|
|
240
|
+
agent: task.agent.name
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { repo: repo2, nodes, edges };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// packages/shared/dist/detail.js
|
|
248
|
+
function readablePayload(text) {
|
|
249
|
+
if (text === void 0)
|
|
250
|
+
return void 0;
|
|
251
|
+
let bad = 0;
|
|
252
|
+
for (const ch of text) {
|
|
253
|
+
const c = ch.codePointAt(0) ?? 0;
|
|
254
|
+
if (c === 65533 || c < 9 || c > 126 && c < 160)
|
|
255
|
+
bad++;
|
|
256
|
+
}
|
|
257
|
+
return bad / Math.max(text.length, 1) > 0.1 ? void 0 : text;
|
|
258
|
+
}
|
|
259
|
+
async function buildNodeDetail(store2, getText2, id) {
|
|
260
|
+
const artifact = store2.getArtifact(id);
|
|
261
|
+
if (artifact) {
|
|
262
|
+
const parent = artifact.parentId ? store2.getArtifact(artifact.parentId) : void 0;
|
|
263
|
+
const parentDiffByRef = /* @__PURE__ */ new Map();
|
|
264
|
+
let parentLatestDiff;
|
|
265
|
+
if (parent) {
|
|
266
|
+
for (const pv of store2.artifactVersions(parent.id)) {
|
|
267
|
+
if (pv.diffRef) {
|
|
268
|
+
parentDiffByRef.set(pv.source.ref, pv.diffRef);
|
|
269
|
+
parentLatestDiff = pv.diffRef;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const versions = store2.artifactVersions(id).map((v) => {
|
|
274
|
+
let diffRef = v.diffRef;
|
|
275
|
+
let diffOfPath;
|
|
276
|
+
if (!diffRef && parent) {
|
|
277
|
+
diffRef = parentDiffByRef.get(v.source.ref) ?? parentLatestDiff;
|
|
278
|
+
if (diffRef)
|
|
279
|
+
diffOfPath = parent.qualifiedPath;
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
timestamp: v.timestamp,
|
|
283
|
+
committer: v.author?.name,
|
|
284
|
+
changeKind: v.changeKind,
|
|
285
|
+
linesAdded: v.linesAdded,
|
|
286
|
+
linesRemoved: v.linesRemoved,
|
|
287
|
+
source: `${v.source.kind}:${v.source.ref.slice(0, 10)}`,
|
|
288
|
+
diffRef,
|
|
289
|
+
diffOfPath
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
const intents = [];
|
|
293
|
+
for (const edge of store2.inEdges("task_modifies_artifact", id)) {
|
|
294
|
+
const taskId = String(edge.task ?? "");
|
|
295
|
+
const task2 = taskId ? store2.getTask(taskId) : void 0;
|
|
296
|
+
if (task2 && task2.sourceAdapter !== void 0) {
|
|
297
|
+
intents.push({
|
|
298
|
+
taskId,
|
|
299
|
+
agent: task2.agent?.name ?? "",
|
|
300
|
+
author: task2.author?.name ?? "",
|
|
301
|
+
when: task2.createdAt
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
id,
|
|
307
|
+
kind: "artifact",
|
|
308
|
+
label: artifact.qualifiedPath,
|
|
309
|
+
level: artifact.level,
|
|
310
|
+
risk: artifact.risk,
|
|
311
|
+
path: artifact.qualifiedPath,
|
|
312
|
+
childCount: store2.children(id).length,
|
|
313
|
+
intents,
|
|
314
|
+
versions
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const task = store2.getTask(id);
|
|
318
|
+
if (task) {
|
|
319
|
+
if (task.sourceAdapter === void 0) {
|
|
320
|
+
let encrypted2 = false;
|
|
321
|
+
let message;
|
|
322
|
+
for (const v of store2.taskVersions(id)) {
|
|
323
|
+
if (!v.decisionSummaryRef)
|
|
324
|
+
continue;
|
|
325
|
+
const raw = await getText2(v.decisionSummaryRef);
|
|
326
|
+
const text = readablePayload(raw);
|
|
327
|
+
if (text)
|
|
328
|
+
message = text;
|
|
329
|
+
else if (raw !== void 0)
|
|
330
|
+
encrypted2 = true;
|
|
331
|
+
}
|
|
332
|
+
const changedFiles = [];
|
|
333
|
+
const touched = /* @__PURE__ */ new Set();
|
|
334
|
+
const touchedVersions = /* @__PURE__ */ new Set();
|
|
335
|
+
for (const edge of store2.outEdges("task_modifies_artifact", id)) {
|
|
336
|
+
const artifactId = String(edge.artifact ?? "");
|
|
337
|
+
if (!artifactId)
|
|
338
|
+
continue;
|
|
339
|
+
touched.add(artifactId);
|
|
340
|
+
const versionId = String(edge.version ?? "");
|
|
341
|
+
if (versionId)
|
|
342
|
+
touchedVersions.add(versionId);
|
|
343
|
+
const art = store2.getArtifact(artifactId);
|
|
344
|
+
if (!art || art.level !== "file")
|
|
345
|
+
continue;
|
|
346
|
+
const avs = store2.artifactVersions(artifactId);
|
|
347
|
+
const v = avs.find((x) => x.id === versionId) ?? avs[avs.length - 1];
|
|
348
|
+
changedFiles.push({
|
|
349
|
+
artifactId,
|
|
350
|
+
path: art.qualifiedPath,
|
|
351
|
+
changeKind: v?.changeKind,
|
|
352
|
+
linesAdded: v?.linesAdded,
|
|
353
|
+
linesRemoved: v?.linesRemoved,
|
|
354
|
+
diffRef: v?.diffRef
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
changedFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
358
|
+
const promptIds = /* @__PURE__ */ new Set();
|
|
359
|
+
for (const artifactId of touched) {
|
|
360
|
+
for (const edge of store2.inEdges("task_modifies_artifact", artifactId)) {
|
|
361
|
+
const other = String(edge.task ?? "");
|
|
362
|
+
const ev = String(edge.version ?? "");
|
|
363
|
+
if (other && other !== id && ev && touchedVersions.has(ev))
|
|
364
|
+
promptIds.add(other);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const prompts = [];
|
|
368
|
+
const readsSet = /* @__PURE__ */ new Set();
|
|
369
|
+
const secretKinds = /* @__PURE__ */ new Map();
|
|
370
|
+
for (const promptId of promptIds) {
|
|
371
|
+
const t = store2.getTask(promptId);
|
|
372
|
+
if (!t || t.sourceAdapter === void 0)
|
|
373
|
+
continue;
|
|
374
|
+
let summary;
|
|
375
|
+
for (const v of store2.taskVersions(promptId)) {
|
|
376
|
+
for (const r of v.reads ?? [])
|
|
377
|
+
readsSet.add(r);
|
|
378
|
+
for (const rs of v.readSecrets ?? []) {
|
|
379
|
+
const set = secretKinds.get(rs.path) ?? /* @__PURE__ */ new Set();
|
|
380
|
+
for (const k of rs.kinds)
|
|
381
|
+
set.add(k);
|
|
382
|
+
secretKinds.set(rs.path, set);
|
|
383
|
+
}
|
|
384
|
+
if (v.decisionSummaryRef) {
|
|
385
|
+
const text = readablePayload(await getText2(v.decisionSummaryRef));
|
|
386
|
+
if (text)
|
|
387
|
+
summary = text;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
prompts.push({
|
|
391
|
+
taskId: promptId,
|
|
392
|
+
summary,
|
|
393
|
+
agent: t.agent?.name ?? "",
|
|
394
|
+
author: t.author?.name ?? "",
|
|
395
|
+
when: t.createdAt
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
prompts.sort((a, b) => a.when.localeCompare(b.when));
|
|
399
|
+
const reads = [...readsSet].sort();
|
|
400
|
+
const readSecrets = [...secretKinds.entries()].map(([path, kinds]) => ({ path, kinds: [...kinds].sort() })).sort((a, b) => a.path.localeCompare(b.path));
|
|
401
|
+
return {
|
|
402
|
+
id,
|
|
403
|
+
kind: "commit",
|
|
404
|
+
label: id,
|
|
405
|
+
agent: task.agent?.name,
|
|
406
|
+
author: task.author?.name,
|
|
407
|
+
createdAt: task.createdAt,
|
|
408
|
+
message,
|
|
409
|
+
source: `git:${task.sessionId.slice(0, 10)}`,
|
|
410
|
+
changedFiles,
|
|
411
|
+
prompts,
|
|
412
|
+
reads,
|
|
413
|
+
readSecrets,
|
|
414
|
+
versions: [],
|
|
415
|
+
note: encrypted2 ? "payload encrypted \u2014 re-run observe with --no-encrypt-payload to preview the message" : void 0
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
let encrypted = false;
|
|
419
|
+
const versions = [];
|
|
420
|
+
for (const v of store2.taskVersions(id)) {
|
|
421
|
+
let summary;
|
|
422
|
+
if (v.decisionSummaryRef) {
|
|
423
|
+
const raw = await getText2(v.decisionSummaryRef);
|
|
424
|
+
summary = readablePayload(raw);
|
|
425
|
+
if (raw !== void 0 && summary === void 0)
|
|
426
|
+
encrypted = true;
|
|
427
|
+
}
|
|
428
|
+
versions.push({
|
|
429
|
+
timestamp: v.timestamp,
|
|
430
|
+
declared: v.declared.map((d) => d.path),
|
|
431
|
+
driftUndeclared: v.drift?.undeclared,
|
|
432
|
+
driftPhantom: v.drift?.phantom,
|
|
433
|
+
summary
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
id,
|
|
438
|
+
kind: "task",
|
|
439
|
+
label: id,
|
|
440
|
+
agent: task.agent?.name,
|
|
441
|
+
author: task.author?.name,
|
|
442
|
+
createdAt: task.createdAt,
|
|
443
|
+
versions,
|
|
444
|
+
note: encrypted ? "payload encrypted \u2014 re-run observe with --no-encrypt-payload to preview summaries" : void 0
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
return void 0;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// packages/shared/dist/store.js
|
|
451
|
+
import { DatabaseSync } from "node:sqlite";
|
|
452
|
+
import { readFileSync } from "node:fs";
|
|
453
|
+
import { fileURLToPath } from "node:url";
|
|
454
|
+
var ARTIFACT_COLS = "id, level, repo, qualified_path, lang, parent_id, current_version, risk_apriori, risk_aposteriori";
|
|
455
|
+
var TASK_COLS = "id, session_id, agent_name, agent_version, author_name, author_email, created_at, current_version, source_adapter, session_cwd";
|
|
456
|
+
var AV_COLS = "id, artifact_id, timestamp, author_name, author_email, agent_name, agent_version, source_kind, source_ref, qualified_path, fingerprint, change_kind, change_from, lines_added, lines_removed, diff_ref";
|
|
457
|
+
var TV_COLS = "id, task_id, timestamp, prompt_ref, decision_summary_ref, declared_json, drift_json, reads_json, read_secrets_json";
|
|
458
|
+
function str(value) {
|
|
459
|
+
return value == null ? "" : String(value);
|
|
460
|
+
}
|
|
461
|
+
function optStr(value) {
|
|
462
|
+
return value == null ? void 0 : String(value);
|
|
463
|
+
}
|
|
464
|
+
function num(value) {
|
|
465
|
+
return typeof value === "number" ? value : Number(value ?? 0);
|
|
466
|
+
}
|
|
467
|
+
function encodeVector(vector) {
|
|
468
|
+
const buf = new Uint8Array(vector.length * 4);
|
|
469
|
+
const view = new DataView(buf.buffer);
|
|
470
|
+
vector.forEach((v, i) => view.setFloat32(i * 4, v, true));
|
|
471
|
+
return buf;
|
|
472
|
+
}
|
|
473
|
+
function decodeVector(bytes) {
|
|
474
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
475
|
+
const out = [];
|
|
476
|
+
for (let i = 0; i + 4 <= bytes.byteLength; i += 4)
|
|
477
|
+
out.push(view.getFloat32(i, true));
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
function cosine(a, b) {
|
|
481
|
+
if (a.length !== b.length)
|
|
482
|
+
return 0;
|
|
483
|
+
let dot = 0;
|
|
484
|
+
let na = 0;
|
|
485
|
+
let nb = 0;
|
|
486
|
+
for (let i = 0; i < a.length; i++) {
|
|
487
|
+
const x = a[i] ?? 0;
|
|
488
|
+
const y = b[i] ?? 0;
|
|
489
|
+
dot += x * y;
|
|
490
|
+
na += x * x;
|
|
491
|
+
nb += y * y;
|
|
492
|
+
}
|
|
493
|
+
if (na === 0 || nb === 0)
|
|
494
|
+
return 0;
|
|
495
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
496
|
+
}
|
|
497
|
+
function mapArtifact(row) {
|
|
498
|
+
return {
|
|
499
|
+
id: str(row.id),
|
|
500
|
+
level: str(row.level),
|
|
501
|
+
repo: str(row.repo),
|
|
502
|
+
qualifiedPath: str(row.qualified_path),
|
|
503
|
+
lang: optStr(row.lang),
|
|
504
|
+
parentId: optStr(row.parent_id),
|
|
505
|
+
currentVersion: str(row.current_version),
|
|
506
|
+
risk: { apriori: num(row.risk_apriori), aposteriori: num(row.risk_aposteriori) }
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function mapTask(row) {
|
|
510
|
+
return {
|
|
511
|
+
id: str(row.id),
|
|
512
|
+
sessionId: str(row.session_id),
|
|
513
|
+
agent: { name: str(row.agent_name), version: optStr(row.agent_version) },
|
|
514
|
+
author: { name: str(row.author_name), email: optStr(row.author_email) },
|
|
515
|
+
createdAt: str(row.created_at),
|
|
516
|
+
currentVersion: str(row.current_version),
|
|
517
|
+
sourceAdapter: optStr(row.source_adapter),
|
|
518
|
+
sessionCwd: optStr(row.session_cwd)
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function mapArtifactVersion(row) {
|
|
522
|
+
const source = {
|
|
523
|
+
kind: str(row.source_kind) === "git" ? "git" : "checkpoint",
|
|
524
|
+
ref: str(row.source_ref)
|
|
525
|
+
};
|
|
526
|
+
return {
|
|
527
|
+
id: str(row.id),
|
|
528
|
+
artifactId: str(row.artifact_id),
|
|
529
|
+
timestamp: str(row.timestamp),
|
|
530
|
+
author: { name: str(row.author_name), email: optStr(row.author_email) },
|
|
531
|
+
agent: { name: str(row.agent_name), version: optStr(row.agent_version) },
|
|
532
|
+
source,
|
|
533
|
+
qualifiedPath: str(row.qualified_path),
|
|
534
|
+
fingerprint: str(row.fingerprint),
|
|
535
|
+
changeKind: str(row.change_kind),
|
|
536
|
+
changeFrom: optStr(row.change_from),
|
|
537
|
+
linesAdded: num(row.lines_added),
|
|
538
|
+
linesRemoved: num(row.lines_removed),
|
|
539
|
+
diffRef: optStr(row.diff_ref)
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function mapTaskVersion(row) {
|
|
543
|
+
return {
|
|
544
|
+
id: str(row.id),
|
|
545
|
+
taskId: str(row.task_id),
|
|
546
|
+
timestamp: str(row.timestamp),
|
|
547
|
+
promptRef: optStr(row.prompt_ref),
|
|
548
|
+
decisionSummaryRef: optStr(row.decision_summary_ref),
|
|
549
|
+
declared: row.declared_json ? JSON.parse(str(row.declared_json)) : [],
|
|
550
|
+
drift: row.drift_json ? JSON.parse(str(row.drift_json)) : void 0,
|
|
551
|
+
reads: row.reads_json ? JSON.parse(str(row.reads_json)) : [],
|
|
552
|
+
readSecrets: row.read_secrets_json ? JSON.parse(str(row.read_secrets_json)) : []
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function schemaPath() {
|
|
556
|
+
return fileURLToPath(new URL("../../../schema/sqlite.sql", import.meta.url));
|
|
557
|
+
}
|
|
558
|
+
var Brain0Store = class {
|
|
559
|
+
db;
|
|
560
|
+
constructor(dbPath) {
|
|
561
|
+
this.db = new DatabaseSync(dbPath);
|
|
562
|
+
this.db.exec("PRAGMA busy_timeout=5000");
|
|
563
|
+
}
|
|
564
|
+
/** Apply the shared schema (idempotent) — mainly for tests; the core normally migrates. */
|
|
565
|
+
migrate() {
|
|
566
|
+
this.db.exec(readFileSync(schemaPath(), "utf8"));
|
|
567
|
+
}
|
|
568
|
+
/** Direct access to the underlying connection (e.g. to seed test data). */
|
|
569
|
+
raw() {
|
|
570
|
+
return this.db;
|
|
571
|
+
}
|
|
572
|
+
close() {
|
|
573
|
+
this.db.close();
|
|
574
|
+
}
|
|
575
|
+
getTask(id) {
|
|
576
|
+
const row = this.db.prepare(`SELECT ${TASK_COLS} FROM task_nodes WHERE id=?`).get(id);
|
|
577
|
+
return row ? mapTask(row) : void 0;
|
|
578
|
+
}
|
|
579
|
+
/** Commit task ids whose git SHA (stored as `session_id`) starts with `prefix` — for resolving
|
|
580
|
+
* an explicit commit referenced by SHA in a query. Commit tasks have no source adapter. */
|
|
581
|
+
commitTaskIdsByShaPrefix(prefix) {
|
|
582
|
+
const rows = this.db.prepare("SELECT id FROM task_nodes WHERE source_adapter IS NULL AND session_id LIKE ? ORDER BY created_at").all(`${prefix}%`);
|
|
583
|
+
return rows.map((r) => str(r.id));
|
|
584
|
+
}
|
|
585
|
+
taskVersions(taskId) {
|
|
586
|
+
const rows = this.db.prepare(`SELECT ${TV_COLS} FROM task_versions WHERE task_id=? ORDER BY timestamp, id`).all(taskId);
|
|
587
|
+
return rows.map(mapTaskVersion);
|
|
588
|
+
}
|
|
589
|
+
getArtifact(id) {
|
|
590
|
+
const row = this.db.prepare(`SELECT ${ARTIFACT_COLS} FROM artifact_nodes WHERE id=?`).get(id);
|
|
591
|
+
return row ? mapArtifact(row) : void 0;
|
|
592
|
+
}
|
|
593
|
+
/** All artifacts in a repo at a given level (e.g. all symbols), for audit/aggregation. */
|
|
594
|
+
listArtifacts(repo2, level) {
|
|
595
|
+
const rows = this.db.prepare(`SELECT ${ARTIFACT_COLS} FROM artifact_nodes WHERE repo=? AND level=? ORDER BY qualified_path`).all(repo2, level);
|
|
596
|
+
return rows.map(mapArtifact);
|
|
597
|
+
}
|
|
598
|
+
children(parentId) {
|
|
599
|
+
const rows = this.db.prepare(`SELECT ${ARTIFACT_COLS} FROM artifact_nodes WHERE parent_id=? ORDER BY qualified_path`).all(parentId);
|
|
600
|
+
return rows.map(mapArtifact);
|
|
601
|
+
}
|
|
602
|
+
/** Version chain for an artifact, oldest first (the timeline axis). */
|
|
603
|
+
artifactVersions(artifactId) {
|
|
604
|
+
const rows = this.db.prepare(`SELECT ${AV_COLS} FROM artifact_versions WHERE artifact_id=? ORDER BY timestamp, id`).all(artifactId);
|
|
605
|
+
return rows.map(mapArtifactVersion);
|
|
606
|
+
}
|
|
607
|
+
outEdges(kind, src) {
|
|
608
|
+
const rows = this.db.prepare("SELECT attrs_json FROM edges WHERE kind=? AND src=?").all(kind, src);
|
|
609
|
+
return rows.map((r) => JSON.parse(str(r.attrs_json)));
|
|
610
|
+
}
|
|
611
|
+
inEdges(kind, dst) {
|
|
612
|
+
const rows = this.db.prepare("SELECT attrs_json FROM edges WHERE kind=? AND dst=?").all(kind, dst);
|
|
613
|
+
return rows.map((r) => JSON.parse(str(r.attrs_json)));
|
|
614
|
+
}
|
|
615
|
+
/** Task ids that have no embedding yet (the indexer's work-list). */
|
|
616
|
+
tasksMissingEmbeddings() {
|
|
617
|
+
const rows = this.db.prepare("SELECT t.id FROM task_nodes t LEFT JOIN task_embeddings e ON e.task_id=t.id WHERE e.task_id IS NULL").all();
|
|
618
|
+
return rows.map((r) => str(r.id));
|
|
619
|
+
}
|
|
620
|
+
putTaskEmbedding(taskId, vector) {
|
|
621
|
+
this.db.prepare("INSERT OR REPLACE INTO task_embeddings (task_id, dim, vec) VALUES (?,?,?)").run(taskId, vector.length, encodeVector(vector));
|
|
622
|
+
}
|
|
623
|
+
/** Top-k task nodes by cosine similarity to the query embedding. */
|
|
624
|
+
searchTasksByVector(query, k) {
|
|
625
|
+
const rows = this.db.prepare("SELECT te.task_id AS task_id, te.vec AS vec, tn.created_at AS created_at FROM task_embeddings te JOIN task_nodes tn ON tn.id = te.task_id").all();
|
|
626
|
+
const hits = rows.map((r) => ({
|
|
627
|
+
taskId: str(r.task_id),
|
|
628
|
+
cosine: cosine(query, decodeVector(r.vec)),
|
|
629
|
+
createdAt: str(r.created_at)
|
|
630
|
+
}));
|
|
631
|
+
hits.sort((a, b) => b.cosine - a.cosine);
|
|
632
|
+
return hits.slice(0, k);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// packages/agent/dist/embeddings.js
|
|
637
|
+
function fnv1a(text) {
|
|
638
|
+
let hash = 2166136261;
|
|
639
|
+
for (let i = 0; i < text.length; i++) {
|
|
640
|
+
hash ^= text.charCodeAt(i);
|
|
641
|
+
hash = Math.imul(hash, 16777619);
|
|
642
|
+
}
|
|
643
|
+
return hash >>> 0;
|
|
644
|
+
}
|
|
645
|
+
function tokenize(text) {
|
|
646
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
|
|
647
|
+
}
|
|
648
|
+
function localEmbed(text, dimension) {
|
|
649
|
+
const vec = new Array(dimension).fill(0);
|
|
650
|
+
for (const token of tokenize(text)) {
|
|
651
|
+
const bucket = fnv1a(token) % dimension;
|
|
652
|
+
const sign = (fnv1a(`sign:${token}`) & 1) === 1 ? 1 : -1;
|
|
653
|
+
vec[bucket] = (vec[bucket] ?? 0) + sign;
|
|
654
|
+
}
|
|
655
|
+
let norm = 0;
|
|
656
|
+
for (const x of vec)
|
|
657
|
+
norm += x * x;
|
|
658
|
+
norm = Math.sqrt(norm);
|
|
659
|
+
if (norm === 0)
|
|
660
|
+
return vec;
|
|
661
|
+
return vec.map((x) => x / norm);
|
|
662
|
+
}
|
|
663
|
+
var LocalEmbeddingProvider = class {
|
|
664
|
+
dimension;
|
|
665
|
+
descriptor = { name: "local", remote: false };
|
|
666
|
+
constructor(dimension = 256) {
|
|
667
|
+
this.dimension = dimension;
|
|
668
|
+
}
|
|
669
|
+
embed(text) {
|
|
670
|
+
return Promise.resolve(localEmbed(text, this.dimension));
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
var OpenAIEmbeddingProvider = class {
|
|
674
|
+
apiKey;
|
|
675
|
+
model;
|
|
676
|
+
baseUrl;
|
|
677
|
+
dimension;
|
|
678
|
+
descriptor = { name: "openai", remote: true };
|
|
679
|
+
constructor(apiKey, model = "text-embedding-3-small", dimension = 1536, baseUrl = "https://api.openai.com/v1") {
|
|
680
|
+
this.apiKey = apiKey;
|
|
681
|
+
this.model = model;
|
|
682
|
+
this.baseUrl = baseUrl;
|
|
683
|
+
this.dimension = dimension;
|
|
684
|
+
}
|
|
685
|
+
async embed(text) {
|
|
686
|
+
const res = await fetch(`${this.baseUrl}/embeddings`, {
|
|
687
|
+
method: "POST",
|
|
688
|
+
headers: {
|
|
689
|
+
"content-type": "application/json",
|
|
690
|
+
authorization: `Bearer ${this.apiKey}`
|
|
691
|
+
},
|
|
692
|
+
body: JSON.stringify({ model: this.model, input: text })
|
|
693
|
+
});
|
|
694
|
+
if (!res.ok) {
|
|
695
|
+
throw new Error(`OpenAI embeddings failed: ${res.status} ${await res.text()}`);
|
|
696
|
+
}
|
|
697
|
+
const json2 = await res.json();
|
|
698
|
+
const embedding = json2.data[0]?.embedding;
|
|
699
|
+
if (!embedding)
|
|
700
|
+
throw new Error("OpenAI embeddings: empty response");
|
|
701
|
+
return embedding;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// packages/agent/dist/ranking.js
|
|
706
|
+
var DEFAULT_WEIGHTS = { semantic: 0.6, recency: 0.3, risk: 0.1 };
|
|
707
|
+
var DEFAULT_HALF_LIFE_DAYS = 30;
|
|
708
|
+
var MS_PER_DAY = 864e5;
|
|
709
|
+
function recencyDecay(createdAt, now, halfLifeDays) {
|
|
710
|
+
const ts = Date.parse(createdAt);
|
|
711
|
+
if (Number.isNaN(ts))
|
|
712
|
+
return 0;
|
|
713
|
+
const ageDays = Math.max(0, (now.getTime() - ts) / MS_PER_DAY);
|
|
714
|
+
return Math.pow(0.5, ageDays / halfLifeDays);
|
|
715
|
+
}
|
|
716
|
+
function recencyAwareRank(candidates, options = {}) {
|
|
717
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
718
|
+
const halfLife = options.halfLifeDays ?? DEFAULT_HALF_LIFE_DAYS;
|
|
719
|
+
const w = { ...DEFAULT_WEIGHTS, ...options.weights };
|
|
720
|
+
const ranked = candidates.map((c) => {
|
|
721
|
+
const recency = recencyDecay(c.createdAt, now, halfLife);
|
|
722
|
+
const score2 = w.semantic * c.cosine + w.recency * recency + w.risk * (c.risk ?? 0);
|
|
723
|
+
return { ...c, recency, score: score2 };
|
|
724
|
+
});
|
|
725
|
+
ranked.sort((a, b) => b.score - a.score || b.recency - a.recency);
|
|
726
|
+
return ranked;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// packages/agent/dist/indexer.js
|
|
730
|
+
async function taskEmbeddingText(store2, taskId, getText2) {
|
|
731
|
+
const parts = [];
|
|
732
|
+
for (const version of store2.taskVersions(taskId)) {
|
|
733
|
+
if (version.promptRef) {
|
|
734
|
+
const text = await getText2(version.promptRef);
|
|
735
|
+
if (text)
|
|
736
|
+
parts.push(text);
|
|
737
|
+
}
|
|
738
|
+
if (version.decisionSummaryRef) {
|
|
739
|
+
const text = await getText2(version.decisionSummaryRef);
|
|
740
|
+
if (text)
|
|
741
|
+
parts.push(text);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return parts.join("\n");
|
|
745
|
+
}
|
|
746
|
+
async function backfillEmbeddings(store2, getText2, provider) {
|
|
747
|
+
let indexed = 0;
|
|
748
|
+
for (const taskId of store2.tasksMissingEmbeddings()) {
|
|
749
|
+
const text = await taskEmbeddingText(store2, taskId, getText2);
|
|
750
|
+
if (text.trim().length === 0)
|
|
751
|
+
continue;
|
|
752
|
+
const embedding = await provider.embed(text);
|
|
753
|
+
store2.putTaskEmbedding(taskId, embedding);
|
|
754
|
+
indexed += 1;
|
|
755
|
+
}
|
|
756
|
+
return indexed;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// packages/agent/dist/llm.js
|
|
760
|
+
var NoLlmError = class extends Error {
|
|
761
|
+
constructor(message = "no LLM configured or reachable") {
|
|
762
|
+
super(message);
|
|
763
|
+
this.name = "NoLlmError";
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
var EchoLLM = class {
|
|
767
|
+
complete(messages) {
|
|
768
|
+
const user = messages.find((m) => m.role === "user")?.content ?? "";
|
|
769
|
+
const firstLine = user.split("\n").find((l) => l.trim().length > 0) ?? "";
|
|
770
|
+
return Promise.resolve(`[offline-llm] ${firstLine.slice(0, 200)}`);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
async function withTimeout(p, ms) {
|
|
774
|
+
return Promise.race([
|
|
775
|
+
p,
|
|
776
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms))
|
|
777
|
+
]);
|
|
778
|
+
}
|
|
779
|
+
var OpenAICompatProvider = class {
|
|
780
|
+
apiKey;
|
|
781
|
+
model;
|
|
782
|
+
baseUrl;
|
|
783
|
+
maxTokens;
|
|
784
|
+
descriptor;
|
|
785
|
+
constructor(apiKey, model = "gpt-4o-mini", baseUrl = "https://api.openai.com/v1", descriptor, maxTokens = 4096) {
|
|
786
|
+
this.apiKey = apiKey;
|
|
787
|
+
this.model = model;
|
|
788
|
+
this.baseUrl = baseUrl;
|
|
789
|
+
this.maxTokens = maxTokens;
|
|
790
|
+
this.descriptor = {
|
|
791
|
+
name: descriptor?.name ?? "openai",
|
|
792
|
+
model,
|
|
793
|
+
endpoint: baseUrl,
|
|
794
|
+
remote: descriptor?.remote ?? true,
|
|
795
|
+
ok: descriptor?.ok
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
headers() {
|
|
799
|
+
const h = { "content-type": "application/json" };
|
|
800
|
+
if (this.apiKey)
|
|
801
|
+
h.authorization = `Bearer ${this.apiKey}`;
|
|
802
|
+
return h;
|
|
803
|
+
}
|
|
804
|
+
async chat(body) {
|
|
805
|
+
const res = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
806
|
+
method: "POST",
|
|
807
|
+
headers: this.headers(),
|
|
808
|
+
body: JSON.stringify({ model: this.model, max_tokens: this.maxTokens, ...body })
|
|
809
|
+
});
|
|
810
|
+
if (!res.ok)
|
|
811
|
+
throw new Error(`${this.descriptor.name} failed: ${res.status} ${await res.text()}`);
|
|
812
|
+
const json2 = await res.json();
|
|
813
|
+
return json2.choices[0]?.message?.content ?? "";
|
|
814
|
+
}
|
|
815
|
+
complete(messages) {
|
|
816
|
+
return this.chat({ messages });
|
|
817
|
+
}
|
|
818
|
+
// Best-effort native JSON mode (broadly supported by OpenAI and recent Ollama). The caller still
|
|
819
|
+
// validates with parseStructured and falls back to `complete()` if this errors.
|
|
820
|
+
completeStructured(messages, _schema) {
|
|
821
|
+
return this.chat({ messages, response_format: { type: "json_object" } });
|
|
822
|
+
}
|
|
823
|
+
// Cheap reachability: GET /models (no model load), so a cold Ollama still reports reachable.
|
|
824
|
+
async probe() {
|
|
825
|
+
try {
|
|
826
|
+
const res = await withTimeout(fetch(`${this.baseUrl}/models`, { headers: this.headers() }), 4e3);
|
|
827
|
+
return res.ok;
|
|
828
|
+
} catch {
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
var ClaudeProvider = class {
|
|
834
|
+
apiKey;
|
|
835
|
+
model;
|
|
836
|
+
maxTokens;
|
|
837
|
+
baseUrl;
|
|
838
|
+
descriptor;
|
|
839
|
+
constructor(apiKey, model = "claude-sonnet-4-6", maxTokens = 4096, baseUrl = "https://api.anthropic.com/v1") {
|
|
840
|
+
this.apiKey = apiKey;
|
|
841
|
+
this.model = model;
|
|
842
|
+
this.maxTokens = maxTokens;
|
|
843
|
+
this.baseUrl = baseUrl;
|
|
844
|
+
this.descriptor = { name: "anthropic", model, endpoint: baseUrl, remote: true };
|
|
845
|
+
}
|
|
846
|
+
async complete(messages) {
|
|
847
|
+
const system = messages.filter((m) => m.role === "system").map((m) => m.content).join("\n\n");
|
|
848
|
+
const rest = messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content }));
|
|
849
|
+
const res = await fetch(`${this.baseUrl}/messages`, {
|
|
850
|
+
method: "POST",
|
|
851
|
+
headers: {
|
|
852
|
+
"content-type": "application/json",
|
|
853
|
+
"x-api-key": this.apiKey,
|
|
854
|
+
"anthropic-version": "2023-06-01"
|
|
855
|
+
},
|
|
856
|
+
body: JSON.stringify({
|
|
857
|
+
model: this.model,
|
|
858
|
+
max_tokens: this.maxTokens,
|
|
859
|
+
system: system || void 0,
|
|
860
|
+
messages: rest
|
|
861
|
+
})
|
|
862
|
+
});
|
|
863
|
+
if (!res.ok)
|
|
864
|
+
throw new Error(`Claude failed: ${res.status} ${await res.text()}`);
|
|
865
|
+
const json2 = await res.json();
|
|
866
|
+
return json2.content.map((c) => c.text ?? "").join("");
|
|
867
|
+
}
|
|
868
|
+
async probe() {
|
|
869
|
+
try {
|
|
870
|
+
await withTimeout(this.complete([{ role: "user", content: "ping" }]), 6e3);
|
|
871
|
+
return true;
|
|
872
|
+
} catch {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// packages/agent/dist/structured.js
|
|
879
|
+
var SEVERITIES = ["info", "warn", "critical", "gold"];
|
|
880
|
+
var KINDS = /* @__PURE__ */ new Set(["task", "artifact", "version"]);
|
|
881
|
+
var STRUCTURED_SCHEMA = {
|
|
882
|
+
type: "object",
|
|
883
|
+
additionalProperties: false,
|
|
884
|
+
properties: {
|
|
885
|
+
intent: { type: "string", enum: ["debug", "audit"] },
|
|
886
|
+
highlights: {
|
|
887
|
+
type: "array",
|
|
888
|
+
items: {
|
|
889
|
+
type: "object",
|
|
890
|
+
additionalProperties: false,
|
|
891
|
+
properties: {
|
|
892
|
+
id: { type: "string" },
|
|
893
|
+
kind: { type: "string", enum: ["task", "artifact", "version"] },
|
|
894
|
+
reason: { type: "string" },
|
|
895
|
+
severity: { type: "string", enum: ["info", "warn", "critical", "gold"] },
|
|
896
|
+
verdict: { type: ["string", "null"] }
|
|
897
|
+
},
|
|
898
|
+
required: ["id", "kind", "reason", "severity", "verdict"]
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
explanation: { type: "string" }
|
|
902
|
+
},
|
|
903
|
+
required: ["intent", "highlights", "explanation"]
|
|
904
|
+
};
|
|
905
|
+
function extractJson(raw) {
|
|
906
|
+
let s = raw.trim();
|
|
907
|
+
const fence = s.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
908
|
+
if (fence?.[1])
|
|
909
|
+
s = fence[1].trim();
|
|
910
|
+
const start = s.indexOf("{");
|
|
911
|
+
const end = s.lastIndexOf("}");
|
|
912
|
+
if (start === -1 || end === -1 || end < start)
|
|
913
|
+
return void 0;
|
|
914
|
+
return s.slice(start, end + 1);
|
|
915
|
+
}
|
|
916
|
+
function clampSeverity(value) {
|
|
917
|
+
return SEVERITIES.includes(value) ? value : "info";
|
|
918
|
+
}
|
|
919
|
+
function validateHighlight(h, knownIds) {
|
|
920
|
+
if (typeof h !== "object" || h === null)
|
|
921
|
+
return void 0;
|
|
922
|
+
const hi = h;
|
|
923
|
+
const id = typeof hi.id === "string" ? hi.id : "";
|
|
924
|
+
const kind = typeof hi.kind === "string" ? hi.kind : "";
|
|
925
|
+
if (!id || !knownIds.has(id) || !KINDS.has(kind))
|
|
926
|
+
return void 0;
|
|
927
|
+
const verdict = typeof hi.verdict === "string" && hi.verdict.trim() ? hi.verdict.trim() : void 0;
|
|
928
|
+
return {
|
|
929
|
+
id,
|
|
930
|
+
kind,
|
|
931
|
+
reason: typeof hi.reason === "string" ? hi.reason : "",
|
|
932
|
+
severity: clampSeverity(hi.severity),
|
|
933
|
+
verdict
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function validateHighlights(arr, knownIds) {
|
|
937
|
+
const out = [];
|
|
938
|
+
for (const h of arr) {
|
|
939
|
+
const f = validateHighlight(h, knownIds);
|
|
940
|
+
if (f)
|
|
941
|
+
out.push(f);
|
|
942
|
+
}
|
|
943
|
+
return out;
|
|
944
|
+
}
|
|
945
|
+
function recoverHighlightObjects(text) {
|
|
946
|
+
const key = text.indexOf('"highlights"');
|
|
947
|
+
if (key < 0)
|
|
948
|
+
return [];
|
|
949
|
+
const open = text.indexOf("[", key);
|
|
950
|
+
if (open < 0)
|
|
951
|
+
return [];
|
|
952
|
+
const out = [];
|
|
953
|
+
let depth = 0;
|
|
954
|
+
let objStart = -1;
|
|
955
|
+
let inStr = false;
|
|
956
|
+
let esc = false;
|
|
957
|
+
for (let i = open + 1; i < text.length; i++) {
|
|
958
|
+
const ch = text[i];
|
|
959
|
+
if (inStr) {
|
|
960
|
+
if (esc)
|
|
961
|
+
esc = false;
|
|
962
|
+
else if (ch === "\\")
|
|
963
|
+
esc = true;
|
|
964
|
+
else if (ch === '"')
|
|
965
|
+
inStr = false;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (ch === '"')
|
|
969
|
+
inStr = true;
|
|
970
|
+
else if (ch === "{") {
|
|
971
|
+
if (depth === 0)
|
|
972
|
+
objStart = i;
|
|
973
|
+
depth += 1;
|
|
974
|
+
} else if (ch === "}") {
|
|
975
|
+
depth -= 1;
|
|
976
|
+
if (depth === 0 && objStart >= 0) {
|
|
977
|
+
try {
|
|
978
|
+
out.push(JSON.parse(text.slice(objStart, i + 1)));
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
objStart = -1;
|
|
982
|
+
}
|
|
983
|
+
} else if (ch === "]" && depth === 0) {
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return out;
|
|
988
|
+
}
|
|
989
|
+
function parseStructured(raw, knownIds, fallbackIntent) {
|
|
990
|
+
const trimmed = raw.trim();
|
|
991
|
+
const looksJson = trimmed.startsWith("{") || /^```(?:json)?/i.test(trimmed);
|
|
992
|
+
const intentOf = (v) => v === "audit" ? "audit" : v === "debug" ? "debug" : fallbackIntent;
|
|
993
|
+
const recover = () => {
|
|
994
|
+
const recovered = validateHighlights(recoverHighlightObjects(raw), knownIds);
|
|
995
|
+
if (recovered.length > 0) {
|
|
996
|
+
const im = raw.match(/"intent"\s*:\s*"(debug|audit)"/);
|
|
997
|
+
return {
|
|
998
|
+
intent: intentOf(im?.[1]),
|
|
999
|
+
highlights: recovered,
|
|
1000
|
+
explanation: "The model's answer was cut off (too long) \u2014 showing the findings recovered before the cut."
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
intent: fallbackIntent,
|
|
1005
|
+
highlights: [],
|
|
1006
|
+
explanation: "The model's structured answer was malformed or cut off. Try a more specific question, or a stronger / higher-token model."
|
|
1007
|
+
};
|
|
1008
|
+
};
|
|
1009
|
+
const jsonText = extractJson(raw);
|
|
1010
|
+
if (!jsonText) {
|
|
1011
|
+
if (!looksJson)
|
|
1012
|
+
return { intent: fallbackIntent, highlights: [], explanation: trimmed };
|
|
1013
|
+
return recover();
|
|
1014
|
+
}
|
|
1015
|
+
let parsed;
|
|
1016
|
+
try {
|
|
1017
|
+
parsed = JSON.parse(jsonText);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return recover();
|
|
1020
|
+
}
|
|
1021
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1022
|
+
return recover();
|
|
1023
|
+
const obj = parsed;
|
|
1024
|
+
return {
|
|
1025
|
+
intent: intentOf(obj.intent),
|
|
1026
|
+
highlights: Array.isArray(obj.highlights) ? validateHighlights(obj.highlights, knownIds) : [],
|
|
1027
|
+
explanation: typeof obj.explanation === "string" ? obj.explanation : ""
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// packages/agent/dist/intent.js
|
|
1032
|
+
var AUDIT_TERMS = [
|
|
1033
|
+
"audit",
|
|
1034
|
+
"secret",
|
|
1035
|
+
"secrets",
|
|
1036
|
+
"credential",
|
|
1037
|
+
"credentials",
|
|
1038
|
+
"leak",
|
|
1039
|
+
"leaked",
|
|
1040
|
+
"exposed",
|
|
1041
|
+
"exfiltrat",
|
|
1042
|
+
"egress",
|
|
1043
|
+
"privacy",
|
|
1044
|
+
"sensitive",
|
|
1045
|
+
"password",
|
|
1046
|
+
"token",
|
|
1047
|
+
"api key",
|
|
1048
|
+
"apikey",
|
|
1049
|
+
".env",
|
|
1050
|
+
"read",
|
|
1051
|
+
"reads",
|
|
1052
|
+
"reached the model",
|
|
1053
|
+
"sent to",
|
|
1054
|
+
"compliance",
|
|
1055
|
+
"pii"
|
|
1056
|
+
];
|
|
1057
|
+
var DEBUG_TERMS = [
|
|
1058
|
+
"debug",
|
|
1059
|
+
"bug",
|
|
1060
|
+
"error",
|
|
1061
|
+
"fail",
|
|
1062
|
+
"failing",
|
|
1063
|
+
"broke",
|
|
1064
|
+
"broken",
|
|
1065
|
+
"crash",
|
|
1066
|
+
"regression",
|
|
1067
|
+
"root cause",
|
|
1068
|
+
"introduced",
|
|
1069
|
+
"stack trace",
|
|
1070
|
+
"exception",
|
|
1071
|
+
"why",
|
|
1072
|
+
"wrong",
|
|
1073
|
+
"issue",
|
|
1074
|
+
"fix",
|
|
1075
|
+
"not working"
|
|
1076
|
+
];
|
|
1077
|
+
function score(haystack, terms) {
|
|
1078
|
+
let n = 0;
|
|
1079
|
+
for (const t of terms)
|
|
1080
|
+
if (haystack.includes(t))
|
|
1081
|
+
n += 1;
|
|
1082
|
+
return n;
|
|
1083
|
+
}
|
|
1084
|
+
function classifyIntent(query) {
|
|
1085
|
+
const q = query.toLowerCase();
|
|
1086
|
+
const audit = score(q, AUDIT_TERMS);
|
|
1087
|
+
const debug = score(q, DEBUG_TERMS);
|
|
1088
|
+
if (audit === 0 && debug === 0)
|
|
1089
|
+
return { intent: "debug", confidence: 0 };
|
|
1090
|
+
const intent = audit > debug ? "audit" : "debug";
|
|
1091
|
+
const total = audit + debug;
|
|
1092
|
+
const confidence = total === 0 ? 0 : Math.abs(audit - debug) / total;
|
|
1093
|
+
return { intent, confidence };
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// packages/agent/dist/redact.js
|
|
1097
|
+
function isExternal(path) {
|
|
1098
|
+
return path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path);
|
|
1099
|
+
}
|
|
1100
|
+
function entropy(s) {
|
|
1101
|
+
if (!s)
|
|
1102
|
+
return 0;
|
|
1103
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1104
|
+
for (const ch of s)
|
|
1105
|
+
counts.set(ch, (counts.get(ch) ?? 0) + 1);
|
|
1106
|
+
let e = 0;
|
|
1107
|
+
for (const n of counts.values()) {
|
|
1108
|
+
const p = n / s.length;
|
|
1109
|
+
e -= p * Math.log2(p);
|
|
1110
|
+
}
|
|
1111
|
+
return e;
|
|
1112
|
+
}
|
|
1113
|
+
function isHighEntropy(token) {
|
|
1114
|
+
return token.length >= 32 && /[0-9]/.test(token) && /[A-Za-z]/.test(token) && entropy(token) >= 4;
|
|
1115
|
+
}
|
|
1116
|
+
var WHOLE = [
|
|
1117
|
+
{ kind: "private_key", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g },
|
|
1118
|
+
{ kind: "jwt", re: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
1119
|
+
{ kind: "anthropic_key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
|
|
1120
|
+
{ kind: "openai_key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
|
|
1121
|
+
{ kind: "aws_access_key", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
1122
|
+
{ kind: "gcp_api_key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
1123
|
+
{ kind: "github_token", re: /\b(?:gh[pousr]_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,})\b/g },
|
|
1124
|
+
{ kind: "slack_token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }
|
|
1125
|
+
];
|
|
1126
|
+
function secretScrub(text) {
|
|
1127
|
+
let out = text;
|
|
1128
|
+
for (const { kind, re } of WHOLE)
|
|
1129
|
+
out = out.replace(re, `[REDACTED:${kind}]`);
|
|
1130
|
+
out = out.replace(/(:\/\/[^:@/\s]+:)([^@/\s]+)(@)/g, (_m, a, _pw, c) => `${a}[REDACTED:url_credentials]${c}`);
|
|
1131
|
+
out = out.replace(/\b([a-z0-9_]*(?:key|token|secret|password|passwd|pwd|credential)[a-z0-9_]*)\s*([:=])\s*["']?([^"'\s]{6,})["']?/gi, (_m, k, sep2, _v) => `${k}${sep2}[REDACTED:env_secret]`);
|
|
1132
|
+
return out;
|
|
1133
|
+
}
|
|
1134
|
+
function redactPaths(text) {
|
|
1135
|
+
return text.replace(/(?<![:\w])\/(?:[A-Za-z0-9._-]+\/)+[A-Za-z0-9._-]+/g, "[REDACTED:path]").replace(/\b[A-Za-z]:\\(?:[A-Za-z0-9._-]+\\)*[A-Za-z0-9._-]+/g, "[REDACTED:path]");
|
|
1136
|
+
}
|
|
1137
|
+
function redactEntropy(text) {
|
|
1138
|
+
return text.replace(/[A-Za-z0-9+/=_-]{32,}/g, (m) => isHighEntropy(m) ? "[REDACTED:high_entropy]" : m);
|
|
1139
|
+
}
|
|
1140
|
+
function redactFreeText(text) {
|
|
1141
|
+
return redactPaths(redactEntropy(secretScrub(text)));
|
|
1142
|
+
}
|
|
1143
|
+
function redactStructural(text) {
|
|
1144
|
+
return redactPaths(secretScrub(text));
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// packages/agent/dist/llm-config.js
|
|
1148
|
+
var OLLAMA_ENDPOINT = "http://localhost:11434/v1";
|
|
1149
|
+
var DEFAULT_OLLAMA_MODEL = "qwen3:4b";
|
|
1150
|
+
function resolveLlmConfig(env) {
|
|
1151
|
+
const model = env.BRAIN0_LLM_MODEL;
|
|
1152
|
+
const endpoint = env.BRAIN0_LLM_ENDPOINT;
|
|
1153
|
+
const ollama = () => ({
|
|
1154
|
+
provider: "ollama",
|
|
1155
|
+
model: model ?? DEFAULT_OLLAMA_MODEL,
|
|
1156
|
+
endpoint: endpoint ?? OLLAMA_ENDPOINT,
|
|
1157
|
+
apiKey: "",
|
|
1158
|
+
remote: false
|
|
1159
|
+
});
|
|
1160
|
+
const anthropic = () => ({
|
|
1161
|
+
provider: "anthropic",
|
|
1162
|
+
model: model ?? "claude-sonnet-4-6",
|
|
1163
|
+
endpoint: endpoint ?? "https://api.anthropic.com/v1",
|
|
1164
|
+
apiKey: env.ANTHROPIC_API_KEY ?? "",
|
|
1165
|
+
remote: true
|
|
1166
|
+
});
|
|
1167
|
+
const openai = () => ({
|
|
1168
|
+
provider: "openai",
|
|
1169
|
+
model: model ?? "gpt-4o-mini",
|
|
1170
|
+
endpoint: endpoint ?? "https://api.openai.com/v1",
|
|
1171
|
+
apiKey: env.OPENAI_API_KEY ?? "",
|
|
1172
|
+
remote: true
|
|
1173
|
+
});
|
|
1174
|
+
switch (env.BRAIN0_LLM_PROVIDER?.toLowerCase()) {
|
|
1175
|
+
case "ollama":
|
|
1176
|
+
return ollama();
|
|
1177
|
+
case "anthropic":
|
|
1178
|
+
return anthropic();
|
|
1179
|
+
case "openai":
|
|
1180
|
+
return openai();
|
|
1181
|
+
case "echo":
|
|
1182
|
+
return { provider: "echo", model: "", endpoint: "", apiKey: "", remote: false };
|
|
1183
|
+
default:
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
if (env.ANTHROPIC_API_KEY)
|
|
1187
|
+
return anthropic();
|
|
1188
|
+
if (env.OPENAI_API_KEY)
|
|
1189
|
+
return openai();
|
|
1190
|
+
return ollama();
|
|
1191
|
+
}
|
|
1192
|
+
function resolveEmbedderConfig(env) {
|
|
1193
|
+
const dim = Number(env.BRAIN0_EMBED_DIM ?? "256") || 256;
|
|
1194
|
+
if (env.BRAIN0_EMBED_PROVIDER?.toLowerCase() === "openai") {
|
|
1195
|
+
return {
|
|
1196
|
+
provider: "openai",
|
|
1197
|
+
model: env.BRAIN0_EMBED_MODEL ?? "text-embedding-3-small",
|
|
1198
|
+
endpoint: env.BRAIN0_EMBED_ENDPOINT ?? "https://api.openai.com/v1",
|
|
1199
|
+
apiKey: env.OPENAI_API_KEY ?? "",
|
|
1200
|
+
dim,
|
|
1201
|
+
remote: true
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
return { provider: "local", model: "local-feature-hash", endpoint: "", apiKey: "", dim, remote: false };
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// packages/agent/dist/agent.js
|
|
1208
|
+
var DEBUG_SYSTEM = "You are brain0's root-cause debugging assistant. Given an issue and the candidate intents (prompts/decisions) plus the code they changed \u2014 ordered most-relevant and most-recent first \u2014 identify the change most likely to have introduced the issue. Cite task and artifact ids. Be concise.";
|
|
1209
|
+
var AUDIT_SYSTEM = "You are brain0's audit assistant. Summarize what changed, where the risk concentrates, and call out any 'looked safe but proved dangerous' (gold-signal) artifacts. Be concise.";
|
|
1210
|
+
var ASK_SYSTEM = 'You are brain0\'s debug+audit assistant. From the user\'s question, decide the INTENT: \'debug\' (find the change/root cause of an issue) or \'audit\' (what reached the model \u2014 files read, secrets, risk concentration). Use ONLY the evidence provided. Reply with a single JSON object: {"intent":"debug"|"audit", "highlights":[{"id":"<copied from evidence>","kind":"task"|"artifact"|"version","reason":"why it matters","severity":"info"|"warn"|"critical"|"gold","verdict":"short label or null"}],"explanation":"concise prose"}. Every id MUST be copied verbatim from the evidence. Highlight only the few nodes that matter (the root-cause chain for debug; the risky / secret-touching nodes for audit), MOST IMPORTANT FIRST: AT MOST 8 highlights, each `reason` <= 12 words and `verdict` a few words. Keep `explanation` to 1\u20132 sentences. Keep the whole answer compact so the JSON stays complete and valid. Output JSON only, no prose outside it.';
|
|
1211
|
+
var Brain0Agent = class {
|
|
1212
|
+
store;
|
|
1213
|
+
embedder;
|
|
1214
|
+
llm;
|
|
1215
|
+
getText;
|
|
1216
|
+
maxHydrate;
|
|
1217
|
+
weights;
|
|
1218
|
+
repo;
|
|
1219
|
+
level;
|
|
1220
|
+
constructor(opts) {
|
|
1221
|
+
this.store = opts.store;
|
|
1222
|
+
this.embedder = opts.embedder;
|
|
1223
|
+
this.llm = opts.llm;
|
|
1224
|
+
this.getText = opts.getText ?? (() => Promise.resolve(void 0));
|
|
1225
|
+
this.maxHydrate = opts.maxHydrate ?? 8;
|
|
1226
|
+
this.weights = opts.weights;
|
|
1227
|
+
this.repo = opts.repo;
|
|
1228
|
+
this.level = opts.level;
|
|
1229
|
+
}
|
|
1230
|
+
/** Max fused risk across the artifacts a task modified (for ranking). */
|
|
1231
|
+
taskRisk(taskId) {
|
|
1232
|
+
let max = 0;
|
|
1233
|
+
for (const edge of this.store.outEdges("task_modifies_artifact", taskId)) {
|
|
1234
|
+
const artId = String(edge.artifact ?? "");
|
|
1235
|
+
const art = this.store.getArtifact(artId);
|
|
1236
|
+
if (art)
|
|
1237
|
+
max = Math.max(max, fusedScore(art.risk));
|
|
1238
|
+
}
|
|
1239
|
+
return max;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Root-cause debug: from the issue text, find the relevant intents, rank recency-aware,
|
|
1243
|
+
* traverse to the code + its version chains, and explain — operating by reference.
|
|
1244
|
+
*/
|
|
1245
|
+
async debug(query) {
|
|
1246
|
+
const qvec = await this.embedder.embed(query);
|
|
1247
|
+
const pool = this.store.searchTasksByVector(qvec, Math.max(this.maxHydrate * 3, this.maxHydrate));
|
|
1248
|
+
const candidates = pool.map((h) => ({
|
|
1249
|
+
taskId: h.taskId,
|
|
1250
|
+
cosine: h.cosine,
|
|
1251
|
+
createdAt: h.createdAt,
|
|
1252
|
+
risk: this.taskRisk(h.taskId)
|
|
1253
|
+
}));
|
|
1254
|
+
const ranked = recencyAwareRank(candidates, { weights: this.weights });
|
|
1255
|
+
const top = ranked.slice(0, this.maxHydrate);
|
|
1256
|
+
const artifacts = /* @__PURE__ */ new Set();
|
|
1257
|
+
const versions = /* @__PURE__ */ new Set();
|
|
1258
|
+
const evidence = [];
|
|
1259
|
+
for (const cand of top) {
|
|
1260
|
+
const text = await taskEmbeddingText(this.store, cand.taskId, this.getText);
|
|
1261
|
+
const artifactLines = [];
|
|
1262
|
+
for (const edge of this.store.outEdges("task_modifies_artifact", cand.taskId)) {
|
|
1263
|
+
const artId = String(edge.artifact ?? "");
|
|
1264
|
+
const art = this.store.getArtifact(artId);
|
|
1265
|
+
if (!art)
|
|
1266
|
+
continue;
|
|
1267
|
+
artifacts.add(artId);
|
|
1268
|
+
const chain = this.store.artifactVersions(artId);
|
|
1269
|
+
for (const v of chain)
|
|
1270
|
+
versions.add(v.id);
|
|
1271
|
+
const last = chain[chain.length - 1];
|
|
1272
|
+
const color = riskColor(art.risk);
|
|
1273
|
+
artifactLines.push(` - ${artId} ${art.qualifiedPath} risk=${color.fused.toFixed(2)} (${color.transition}) lastChange=${last?.changeKind ?? "?"} by ${last?.author.name ?? "?"} @ ${last?.timestamp ?? "?"}`);
|
|
1274
|
+
}
|
|
1275
|
+
evidence.push(`Task ${cand.taskId} [relevance=${cand.cosine.toFixed(2)} recency=${cand.recency.toFixed(2)} risk=${(cand.risk ?? 0).toFixed(2)}]
|
|
1276
|
+
${text || "(no payload)"}
|
|
1277
|
+
Changed:
|
|
1278
|
+
${artifactLines.join("\n") || " (none)"}`);
|
|
1279
|
+
}
|
|
1280
|
+
const explanation = await this.llm.complete([
|
|
1281
|
+
{ role: "system", content: DEBUG_SYSTEM },
|
|
1282
|
+
{
|
|
1283
|
+
role: "user",
|
|
1284
|
+
content: `Issue: ${query}
|
|
1285
|
+
|
|
1286
|
+
Candidate intents (most relevant first):
|
|
1287
|
+
|
|
1288
|
+
${evidence.join("\n\n")}`
|
|
1289
|
+
}
|
|
1290
|
+
]);
|
|
1291
|
+
return {
|
|
1292
|
+
query,
|
|
1293
|
+
highlights: {
|
|
1294
|
+
tasks: top.map((t) => t.taskId),
|
|
1295
|
+
artifacts: [...artifacts],
|
|
1296
|
+
versions: [...versions]
|
|
1297
|
+
},
|
|
1298
|
+
ranked: top,
|
|
1299
|
+
explanation
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Big-picture audit over a repo: risk distribution, gold-signal artifacts, and the
|
|
1304
|
+
* riskiest nodes — by reference (no payload dump).
|
|
1305
|
+
*/
|
|
1306
|
+
async audit(opts) {
|
|
1307
|
+
const level = opts.level ?? "symbol";
|
|
1308
|
+
const artifacts = this.store.listArtifacts(opts.repo, level);
|
|
1309
|
+
const scored = artifacts.map((a) => ({ a, fused: fusedScore(a.risk) }));
|
|
1310
|
+
let green = 0;
|
|
1311
|
+
let yellow = 0;
|
|
1312
|
+
let red = 0;
|
|
1313
|
+
const goldSignals = [];
|
|
1314
|
+
for (const { a, fused } of scored) {
|
|
1315
|
+
if (fused < 0.34)
|
|
1316
|
+
green += 1;
|
|
1317
|
+
else if (fused < 0.66)
|
|
1318
|
+
yellow += 1;
|
|
1319
|
+
else
|
|
1320
|
+
red += 1;
|
|
1321
|
+
if (isGoldSignal(a.risk))
|
|
1322
|
+
goldSignals.push(a.id);
|
|
1323
|
+
}
|
|
1324
|
+
const topRisky = [...scored].sort((x, y) => y.fused - x.fused).slice(0, this.maxHydrate).map(({ a, fused }) => ({ id: a.id, path: a.qualifiedPath, fused }));
|
|
1325
|
+
const summary = `Repo ${opts.repo}: ${artifacts.length} ${level}(s). Risk distribution green=${green} yellow=${yellow} red=${red}. Gold-signal (looked safe \u2192 proved dangerous): ${goldSignals.length}.`;
|
|
1326
|
+
const explanation = await this.llm.complete([
|
|
1327
|
+
{ role: "system", content: AUDIT_SYSTEM },
|
|
1328
|
+
{
|
|
1329
|
+
role: "user",
|
|
1330
|
+
content: `${summary}
|
|
1331
|
+
Top risky:
|
|
1332
|
+
` + topRisky.map((t) => `- ${t.id} ${t.path} ${t.fused.toFixed(2)}`).join("\n")
|
|
1333
|
+
}
|
|
1334
|
+
]);
|
|
1335
|
+
return { repo: opts.repo, level, distribution: { green, yellow, red }, goldSignals, topRisky, explanation };
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Assemble the enriched, reference-preserving evidence for the top tasks: hydrated summary,
|
|
1339
|
+
* files read (with out-of-repo flag), declared↔done drift, and each changed artifact's risk +
|
|
1340
|
+
* coupling (centrality/blast via the co-change `artifact_depends_on` edges, omitted when absent).
|
|
1341
|
+
* Returns the prompt block, the set of valid ids (to reject hallucinated ones), the retrieval
|
|
1342
|
+
* highlight floor, and which tasks read an out-of-repo file (for the audit card flag).
|
|
1343
|
+
*/
|
|
1344
|
+
async buildBundle(top) {
|
|
1345
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1346
|
+
const tasks = [];
|
|
1347
|
+
const artifacts = /* @__PURE__ */ new Set();
|
|
1348
|
+
const versions = /* @__PURE__ */ new Set();
|
|
1349
|
+
const taskExternalRead = /* @__PURE__ */ new Map();
|
|
1350
|
+
const blocks = [];
|
|
1351
|
+
for (const cand of top) {
|
|
1352
|
+
tasks.push(cand.taskId);
|
|
1353
|
+
ids.add(cand.taskId);
|
|
1354
|
+
const reads = /* @__PURE__ */ new Set();
|
|
1355
|
+
let undeclared = 0;
|
|
1356
|
+
let phantom = 0;
|
|
1357
|
+
for (const v of this.store.taskVersions(cand.taskId)) {
|
|
1358
|
+
for (const r of v.reads ?? [])
|
|
1359
|
+
reads.add(r);
|
|
1360
|
+
undeclared += v.drift?.undeclared?.length ?? 0;
|
|
1361
|
+
phantom += v.drift?.phantom?.length ?? 0;
|
|
1362
|
+
}
|
|
1363
|
+
const readList = [...reads];
|
|
1364
|
+
taskExternalRead.set(cand.taskId, readList.some(isExternal));
|
|
1365
|
+
const artLines = [];
|
|
1366
|
+
for (const edge of this.store.outEdges("task_modifies_artifact", cand.taskId)) {
|
|
1367
|
+
const artId = String(edge.artifact ?? "");
|
|
1368
|
+
const art = this.store.getArtifact(artId);
|
|
1369
|
+
if (!art)
|
|
1370
|
+
continue;
|
|
1371
|
+
artifacts.add(artId);
|
|
1372
|
+
ids.add(artId);
|
|
1373
|
+
for (const v of this.store.artifactVersions(artId)) {
|
|
1374
|
+
versions.add(v.id);
|
|
1375
|
+
ids.add(v.id);
|
|
1376
|
+
}
|
|
1377
|
+
const color = riskColor(art.risk);
|
|
1378
|
+
const coupled = this.store.inEdges("artifact_depends_on", artId).length;
|
|
1379
|
+
const gold = isGoldSignal(art.risk) ? " GOLD(safe\u2192dangerous)" : "";
|
|
1380
|
+
artLines.push(` - ${artId} ${art.qualifiedPath} risk=${color.fused.toFixed(2)} (${color.transition})${gold}` + (coupled ? ` coupledTo=${coupled}` : ""));
|
|
1381
|
+
}
|
|
1382
|
+
const fullSummary = await taskEmbeddingText(this.store, cand.taskId, this.getText);
|
|
1383
|
+
const summary = fullSummary.length > 480 ? `${fullSummary.slice(0, 480)}\u2026 (truncated)` : fullSummary;
|
|
1384
|
+
const readsLine = readList.length ? readList.slice(0, 8).map((r) => isExternal(r) ? `${r} (EXTERNAL)` : r).join(", ") : "(none)";
|
|
1385
|
+
blocks.push(`Task ${cand.taskId} [rel=${cand.cosine.toFixed(2)} rec=${cand.recency.toFixed(2)} risk=${(cand.risk ?? 0).toFixed(2)}]
|
|
1386
|
+
${summary || "(no payload)"}
|
|
1387
|
+
reads: ${readsLine}
|
|
1388
|
+
drift: undeclared=${undeclared} phantom=${phantom}
|
|
1389
|
+
changed:
|
|
1390
|
+
${capped(artLines, 6).join("\n") || " (none)"}`);
|
|
1391
|
+
}
|
|
1392
|
+
return {
|
|
1393
|
+
text: blocks.join("\n\n"),
|
|
1394
|
+
ids,
|
|
1395
|
+
retrieval: { tasks, artifacts: [...artifacts], versions: [...versions] },
|
|
1396
|
+
taskExternalRead
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
/** Repo-wide audit numbers (distribution / gold signals / top risky) — the misclassification-safe
|
|
1400
|
+
* floor, computed from the full level scan, not the ~8 retrieved tasks. */
|
|
1401
|
+
auditNumbers() {
|
|
1402
|
+
if (!this.repo)
|
|
1403
|
+
return void 0;
|
|
1404
|
+
const level = this.level ?? "symbol";
|
|
1405
|
+
const scored = this.store.listArtifacts(this.repo, level).map((a) => ({ a, fused: fusedScore(a.risk) }));
|
|
1406
|
+
let green = 0;
|
|
1407
|
+
let yellow = 0;
|
|
1408
|
+
let red = 0;
|
|
1409
|
+
const goldSignals = [];
|
|
1410
|
+
for (const { a, fused } of scored) {
|
|
1411
|
+
if (fused < 0.34)
|
|
1412
|
+
green += 1;
|
|
1413
|
+
else if (fused < 0.66)
|
|
1414
|
+
yellow += 1;
|
|
1415
|
+
else
|
|
1416
|
+
red += 1;
|
|
1417
|
+
if (isGoldSignal(a.risk))
|
|
1418
|
+
goldSignals.push(a.id);
|
|
1419
|
+
}
|
|
1420
|
+
const topRisky = [...scored].sort((x, y) => y.fused - x.fused).slice(0, this.maxHydrate).map(({ a, fused }) => ({ id: a.id, path: a.qualifiedPath, fused }));
|
|
1421
|
+
return { distribution: { green, yellow, red }, goldSignals, topRisky };
|
|
1422
|
+
}
|
|
1423
|
+
/** Artifacts (and their linked tasks) named by the query — the lexical channel. */
|
|
1424
|
+
resolveEntityArtifacts(query) {
|
|
1425
|
+
const artifacts = /* @__PURE__ */ new Set();
|
|
1426
|
+
const tasks = /* @__PURE__ */ new Set();
|
|
1427
|
+
if (!this.repo)
|
|
1428
|
+
return { artifacts: [], tasks: [] };
|
|
1429
|
+
const tokens = extractEntityTokens(query).map((t) => t.toLowerCase());
|
|
1430
|
+
if (tokens.length === 0)
|
|
1431
|
+
return { artifacts: [], tasks: [] };
|
|
1432
|
+
for (const level of ["file", "symbol"]) {
|
|
1433
|
+
for (const art of this.store.listArtifacts(this.repo, level)) {
|
|
1434
|
+
const path = art.qualifiedPath.toLowerCase();
|
|
1435
|
+
if (tokens.some((t) => path.includes(t))) {
|
|
1436
|
+
artifacts.add(art.id);
|
|
1437
|
+
for (const edge of this.store.inEdges("task_modifies_artifact", art.id)) {
|
|
1438
|
+
const task = String(edge.task ?? "");
|
|
1439
|
+
if (task)
|
|
1440
|
+
tasks.add(task);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return { artifacts: [...artifacts], tasks: [...tasks] };
|
|
1446
|
+
}
|
|
1447
|
+
/** Commit task ids referenced by a git SHA in the query (hex tokens of 7–40 chars matched as a
|
|
1448
|
+
* commit's session_id prefix). Empty when the query names no known commit. */
|
|
1449
|
+
resolveCommitTasks(query) {
|
|
1450
|
+
const out = /* @__PURE__ */ new Set();
|
|
1451
|
+
const tokens = query.toLowerCase().match(/\b[0-9a-f]{7,40}\b/g) ?? [];
|
|
1452
|
+
for (const token of tokens) {
|
|
1453
|
+
for (const id of this.store.commitTaskIdsByShaPrefix(token))
|
|
1454
|
+
out.add(id);
|
|
1455
|
+
}
|
|
1456
|
+
return [...out];
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Single auto-intent entry point for the smart chat. Retrieval is ALWAYS intent-agnostic; the
|
|
1460
|
+
* LLM self-classifies debug vs audit and returns structured findings (validated against the
|
|
1461
|
+
* bundle ids) plus prose. Egress is redacted per-channel when the provider is remote; the result
|
|
1462
|
+
* carries the truthful egress state. The LLM call failing never blanks the graph — the retrieval
|
|
1463
|
+
* floor still highlights — and never throws: it returns `error` instead.
|
|
1464
|
+
*/
|
|
1465
|
+
/** The shared retrieval stage (no LLM): classify, resolve entities, rank, build the bundle. */
|
|
1466
|
+
async gather(query) {
|
|
1467
|
+
const cls = classifyIntent(query);
|
|
1468
|
+
const llmDesc = this.llm.descriptor ?? {
|
|
1469
|
+
name: "echo",
|
|
1470
|
+
model: "",
|
|
1471
|
+
endpoint: "",
|
|
1472
|
+
remote: false
|
|
1473
|
+
};
|
|
1474
|
+
const embDesc = this.embedder.descriptor ?? { name: "local", remote: false };
|
|
1475
|
+
const resolved = this.resolveCommitTasks(query);
|
|
1476
|
+
const entity = this.resolveEntityArtifacts(query);
|
|
1477
|
+
let top;
|
|
1478
|
+
if (resolved.length > 0) {
|
|
1479
|
+
top = resolved.slice(0, this.maxHydrate).map((taskId) => ({
|
|
1480
|
+
taskId,
|
|
1481
|
+
cosine: 1,
|
|
1482
|
+
createdAt: this.store.getTask(taskId)?.createdAt ?? "",
|
|
1483
|
+
risk: this.taskRisk(taskId),
|
|
1484
|
+
recency: 1,
|
|
1485
|
+
score: 1
|
|
1486
|
+
}));
|
|
1487
|
+
} else {
|
|
1488
|
+
const embedInput = embDesc.remote ? redactFreeText(query) : query;
|
|
1489
|
+
const qvec = await this.embedder.embed(embedInput);
|
|
1490
|
+
const pool = this.store.searchTasksByVector(qvec, Math.max(this.maxHydrate * 3, this.maxHydrate));
|
|
1491
|
+
const candidates = pool.map((h) => ({
|
|
1492
|
+
taskId: h.taskId,
|
|
1493
|
+
cosine: h.cosine,
|
|
1494
|
+
createdAt: h.createdAt,
|
|
1495
|
+
risk: this.taskRisk(h.taskId)
|
|
1496
|
+
}));
|
|
1497
|
+
const ranked = recencyAwareRank(candidates, { weights: this.weights });
|
|
1498
|
+
const lex = entity.tasks.map((taskId) => ({
|
|
1499
|
+
taskId,
|
|
1500
|
+
cosine: 0.95,
|
|
1501
|
+
createdAt: this.store.getTask(taskId)?.createdAt ?? "",
|
|
1502
|
+
risk: this.taskRisk(taskId),
|
|
1503
|
+
recency: 1,
|
|
1504
|
+
score: 0.95
|
|
1505
|
+
})).sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, Math.ceil(this.maxHydrate / 2));
|
|
1506
|
+
const lexIds = new Set(lex.map((t) => t.taskId));
|
|
1507
|
+
top = [...lex, ...ranked.filter((t) => !lexIds.has(t.taskId))].slice(0, this.maxHydrate);
|
|
1508
|
+
}
|
|
1509
|
+
const bundle = await this.buildBundle(top);
|
|
1510
|
+
for (const id of entity.artifacts) {
|
|
1511
|
+
bundle.ids.add(id);
|
|
1512
|
+
if (!bundle.retrieval.artifacts.includes(id))
|
|
1513
|
+
bundle.retrieval.artifacts.push(id);
|
|
1514
|
+
}
|
|
1515
|
+
return { cls, llmDesc, embDesc, top, bundle };
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Phase 1 of the two-phase smart chat: retrieval only — highlights, ranked candidates, and
|
|
1519
|
+
* (for audit-shaped queries) the deterministic numbers. Milliseconds, zero LLM tokens; the
|
|
1520
|
+
* GUI paints this immediately while `ask()` produces the explanation.
|
|
1521
|
+
*/
|
|
1522
|
+
async retrieve(query) {
|
|
1523
|
+
const { cls, llmDesc, embDesc, top, bundle } = await this.gather(query);
|
|
1524
|
+
const audit = cls.intent === "audit" ? this.auditNumbers() : void 0;
|
|
1525
|
+
return {
|
|
1526
|
+
query,
|
|
1527
|
+
intent: cls.intent,
|
|
1528
|
+
confidence: cls.confidence,
|
|
1529
|
+
highlights: bundle.retrieval,
|
|
1530
|
+
findings: [],
|
|
1531
|
+
ranked: top,
|
|
1532
|
+
distribution: audit?.distribution,
|
|
1533
|
+
goldSignals: audit?.goldSignals,
|
|
1534
|
+
topRisky: audit?.topRisky,
|
|
1535
|
+
explanation: "",
|
|
1536
|
+
egress: {
|
|
1537
|
+
llm: llmDesc,
|
|
1538
|
+
embedder: embDesc,
|
|
1539
|
+
redacted: llmDesc.remote,
|
|
1540
|
+
zeroEgress: !llmDesc.remote && !embDesc.remote
|
|
1541
|
+
},
|
|
1542
|
+
error: void 0
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
async ask(query) {
|
|
1546
|
+
const { cls, llmDesc, embDesc, top, bundle } = await this.gather(query);
|
|
1547
|
+
const queryOut = llmDesc.remote ? redactFreeText(query) : query;
|
|
1548
|
+
const structuralOut = llmDesc.remote ? redactStructural(bundle.text) : bundle.text;
|
|
1549
|
+
const userMsg = `Question: ${queryOut}
|
|
1550
|
+
|
|
1551
|
+
Evidence (choose ids ONLY from here):
|
|
1552
|
+
|
|
1553
|
+
${structuralOut}`;
|
|
1554
|
+
const messages = [
|
|
1555
|
+
{ role: "system", content: ASK_SYSTEM },
|
|
1556
|
+
{ role: "user", content: userMsg }
|
|
1557
|
+
];
|
|
1558
|
+
let raw = "";
|
|
1559
|
+
let error;
|
|
1560
|
+
try {
|
|
1561
|
+
if (this.llm.completeStructured) {
|
|
1562
|
+
try {
|
|
1563
|
+
raw = await this.llm.completeStructured(messages, STRUCTURED_SCHEMA);
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
if (err instanceof NoLlmError)
|
|
1566
|
+
throw err;
|
|
1567
|
+
raw = await this.llm.complete(messages);
|
|
1568
|
+
}
|
|
1569
|
+
} else {
|
|
1570
|
+
raw = await this.llm.complete(messages);
|
|
1571
|
+
}
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
error = err instanceof NoLlmError ? "no-llm" : "llm-unreachable";
|
|
1574
|
+
}
|
|
1575
|
+
const structured = error ? { intent: cls.intent, highlights: [], explanation: "" } : parseStructured(raw, bundle.ids, cls.intent);
|
|
1576
|
+
const union = (floor, add) => [.../* @__PURE__ */ new Set([...floor, ...add])];
|
|
1577
|
+
const llmByKind = (kind) => structured.highlights.filter((h) => h.kind === kind).map((h) => h.id);
|
|
1578
|
+
const highlights = {
|
|
1579
|
+
tasks: union(bundle.retrieval.tasks, llmByKind("task")),
|
|
1580
|
+
artifacts: union(bundle.retrieval.artifacts, llmByKind("artifact")),
|
|
1581
|
+
versions: union(bundle.retrieval.versions, llmByKind("version"))
|
|
1582
|
+
};
|
|
1583
|
+
const findings = structured.highlights.map((h) => {
|
|
1584
|
+
const art = h.kind === "artifact" ? this.store.getArtifact(h.id) : void 0;
|
|
1585
|
+
const external = h.kind === "artifact" ? art ? isExternal(art.qualifiedPath) : false : bundle.taskExternalRead.get(h.id) ?? false;
|
|
1586
|
+
return { ...h, path: art?.qualifiedPath, external };
|
|
1587
|
+
});
|
|
1588
|
+
const audit = structured.intent === "audit" ? this.auditNumbers() : void 0;
|
|
1589
|
+
return {
|
|
1590
|
+
query,
|
|
1591
|
+
intent: structured.intent,
|
|
1592
|
+
confidence: cls.confidence,
|
|
1593
|
+
highlights,
|
|
1594
|
+
findings,
|
|
1595
|
+
ranked: top,
|
|
1596
|
+
distribution: audit?.distribution,
|
|
1597
|
+
goldSignals: audit?.goldSignals,
|
|
1598
|
+
topRisky: audit?.topRisky,
|
|
1599
|
+
explanation: structured.explanation,
|
|
1600
|
+
egress: { llm: llmDesc, embedder: embDesc, redacted: llmDesc.remote, zeroEgress: !llmDesc.remote && !embDesc.remote },
|
|
1601
|
+
error
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
function extractEntityTokens(query) {
|
|
1606
|
+
const out = /* @__PURE__ */ new Set();
|
|
1607
|
+
for (const m of query.matchAll(/`([^`]+)`/g)) {
|
|
1608
|
+
const t = m[1]?.trim();
|
|
1609
|
+
if (t && t.length >= 3)
|
|
1610
|
+
out.add(t);
|
|
1611
|
+
}
|
|
1612
|
+
for (const raw of query.split(/\s+/)) {
|
|
1613
|
+
const w = raw.replace(/^[('"«]+|[)'"».,;:!?]+$/g, "");
|
|
1614
|
+
if (w.length < 3)
|
|
1615
|
+
continue;
|
|
1616
|
+
if (w.includes("/") || w.includes("::") || /\.[a-z]{1,4}$/i.test(w))
|
|
1617
|
+
out.add(w);
|
|
1618
|
+
}
|
|
1619
|
+
return [...out];
|
|
1620
|
+
}
|
|
1621
|
+
function capped(lines, keep) {
|
|
1622
|
+
if (lines.length <= keep)
|
|
1623
|
+
return lines;
|
|
1624
|
+
return [...lines.slice(0, keep), ` \u2026 +${lines.length - keep} more`];
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// packages/server/dist/server.js
|
|
1628
|
+
function buildEmbedder() {
|
|
1629
|
+
const c = resolveEmbedderConfig(process.env);
|
|
1630
|
+
if (c.provider === "openai")
|
|
1631
|
+
return new OpenAIEmbeddingProvider(c.apiKey, c.model, 1536, c.endpoint);
|
|
1632
|
+
return new LocalEmbeddingProvider(c.dim);
|
|
1633
|
+
}
|
|
1634
|
+
async function buildLlm() {
|
|
1635
|
+
const c = resolveLlmConfig(process.env);
|
|
1636
|
+
let provider;
|
|
1637
|
+
switch (c.provider) {
|
|
1638
|
+
case "anthropic":
|
|
1639
|
+
provider = new ClaudeProvider(c.apiKey, c.model, 4096, c.endpoint);
|
|
1640
|
+
break;
|
|
1641
|
+
case "openai":
|
|
1642
|
+
provider = new OpenAICompatProvider(c.apiKey, c.model, c.endpoint, { name: "openai", remote: true });
|
|
1643
|
+
break;
|
|
1644
|
+
case "echo":
|
|
1645
|
+
provider = new EchoLLM();
|
|
1646
|
+
break;
|
|
1647
|
+
default:
|
|
1648
|
+
provider = new OpenAICompatProvider("", c.model, c.endpoint, { name: "ollama", remote: false });
|
|
1649
|
+
break;
|
|
1650
|
+
}
|
|
1651
|
+
const ok = provider.probe ? await provider.probe() : true;
|
|
1652
|
+
if (provider.descriptor)
|
|
1653
|
+
provider.descriptor.ok = ok;
|
|
1654
|
+
return provider;
|
|
1655
|
+
}
|
|
1656
|
+
function json(res, status, body) {
|
|
1657
|
+
res.writeHead(status, {
|
|
1658
|
+
"content-type": "application/json",
|
|
1659
|
+
"access-control-allow-origin": "*"
|
|
1660
|
+
});
|
|
1661
|
+
res.end(JSON.stringify(body));
|
|
1662
|
+
}
|
|
1663
|
+
var db = resolve2(process.env.BRAIN0_DB ?? ".brain0/index.db");
|
|
1664
|
+
var payloadDir = resolve2(process.env.BRAIN0_PAYLOAD ?? ".brain0/payload");
|
|
1665
|
+
var repo = process.env.BRAIN0_REPO ?? "";
|
|
1666
|
+
var port = Number(process.env.PORT ?? 8787);
|
|
1667
|
+
if (!existsSync(db)) {
|
|
1668
|
+
console.error(`brain0-server: index database not found at ${db}
|
|
1669
|
+
(cwd is ${process.cwd()}; pnpm runs scripts from the package directory)
|
|
1670
|
+
Pass an ABSOLUTE path, run from the repo root, e.g.:
|
|
1671
|
+
BRAIN0_REPO=${repo || "myorg/myrepo"} \\
|
|
1672
|
+
BRAIN0_DB="$PWD/.brain0/index.db" BRAIN0_PAYLOAD="$PWD/.brain0/payload" \\
|
|
1673
|
+
pnpm --filter @brain0/server start
|
|
1674
|
+
Create the index first with: brain0 observe (and/or brain0 ingest).`);
|
|
1675
|
+
process.exit(1);
|
|
1676
|
+
}
|
|
1677
|
+
var store = new Brain0Store(db);
|
|
1678
|
+
var payload = new FsPayloadReader(payloadDir);
|
|
1679
|
+
var getText = (ref) => payload.getText(ref);
|
|
1680
|
+
var embedder = buildEmbedder();
|
|
1681
|
+
var llm = await buildLlm();
|
|
1682
|
+
var agent = new Brain0Agent({ store, embedder, llm, getText, repo: repo || void 0 });
|
|
1683
|
+
var repoRoot = resolve2(import.meta.dirname, "../../..");
|
|
1684
|
+
var guiDir = process.env.BRAIN0_GUI_DIR ? resolve2(process.env.BRAIN0_GUI_DIR) : resolve2(repoRoot, "packages/gui/build");
|
|
1685
|
+
var guiAvailable = existsSync(resolve2(guiDir, "index.html"));
|
|
1686
|
+
async function serveStatic(pathname, res) {
|
|
1687
|
+
if (!guiAvailable) {
|
|
1688
|
+
json(res, 404, {
|
|
1689
|
+
error: "not found",
|
|
1690
|
+
hint: "GUI build not present \u2014 run `pnpm --filter @brain0/gui build` (or set BRAIN0_GUI_DIR)"
|
|
1691
|
+
});
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
const wanted = pathname === "/" ? "/index.html" : pathname;
|
|
1695
|
+
const file = safeJoin(guiDir, wanted);
|
|
1696
|
+
if (!file) {
|
|
1697
|
+
json(res, 403, { error: "forbidden" });
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
try {
|
|
1701
|
+
const body = await readFile2(file);
|
|
1702
|
+
res.writeHead(200, {
|
|
1703
|
+
"content-type": contentTypeFor(file),
|
|
1704
|
+
"cache-control": cacheControlFor(wanted)
|
|
1705
|
+
});
|
|
1706
|
+
res.end(body);
|
|
1707
|
+
} catch {
|
|
1708
|
+
try {
|
|
1709
|
+
const body = await readFile2(resolve2(guiDir, "index.html"));
|
|
1710
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" });
|
|
1711
|
+
res.end(body);
|
|
1712
|
+
} catch {
|
|
1713
|
+
json(res, 404, { error: "not found" });
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
var repoPath = process.env.BRAIN0_REPO_PATH;
|
|
1718
|
+
var REPO_RE = /^[A-Za-z0-9._/-]+$/;
|
|
1719
|
+
function resolveBin() {
|
|
1720
|
+
if (process.env.BRAIN0_BIN && existsSync(process.env.BRAIN0_BIN))
|
|
1721
|
+
return process.env.BRAIN0_BIN;
|
|
1722
|
+
for (const p of ["target/release/brain0", "target/debug/brain0"]) {
|
|
1723
|
+
const abs = resolve2(repoRoot, p);
|
|
1724
|
+
if (existsSync(abs))
|
|
1725
|
+
return abs;
|
|
1726
|
+
}
|
|
1727
|
+
return void 0;
|
|
1728
|
+
}
|
|
1729
|
+
function payloadIsEncrypted() {
|
|
1730
|
+
try {
|
|
1731
|
+
const row = store.raw().prepare("SELECT value FROM meta WHERE key='payload_encryption'").get();
|
|
1732
|
+
return row?.value !== "plaintext";
|
|
1733
|
+
} catch {
|
|
1734
|
+
return true;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
var refresh = { state: "idle", phase: "", jobId: "", startedAt: 0, finishedAt: 0, error: "", lines: [] };
|
|
1738
|
+
function pushLine(s) {
|
|
1739
|
+
const last = refresh.lines.at(-1);
|
|
1740
|
+
if (s && s !== last) {
|
|
1741
|
+
refresh.lines.push(s);
|
|
1742
|
+
if (refresh.lines.length > 50)
|
|
1743
|
+
refresh.lines.shift();
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
function spawnStep(bin, args) {
|
|
1747
|
+
return new Promise((res, rej) => {
|
|
1748
|
+
const child = spawn(bin, args, { cwd: repoPath, env: process.env });
|
|
1749
|
+
const capture = (buf) => {
|
|
1750
|
+
for (const raw of buf.toString().split(/[\r\n]+/)) {
|
|
1751
|
+
const l = raw.trim();
|
|
1752
|
+
if (l)
|
|
1753
|
+
pushLine(l);
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
child.stdout.on("data", capture);
|
|
1757
|
+
child.stderr.on("data", capture);
|
|
1758
|
+
child.on("exit", (c) => c === 0 ? res() : rej(new Error(`${args[0]} exited with code ${c}`)));
|
|
1759
|
+
child.on("error", rej);
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
async function runRefresh() {
|
|
1763
|
+
const bin = resolveBin();
|
|
1764
|
+
refresh.state = "running";
|
|
1765
|
+
refresh.jobId = randomUUID();
|
|
1766
|
+
refresh.startedAt = Date.now();
|
|
1767
|
+
refresh.finishedAt = 0;
|
|
1768
|
+
refresh.error = "";
|
|
1769
|
+
refresh.lines = [];
|
|
1770
|
+
const enc = payloadIsEncrypted() ? [] : ["--no-encrypt-payload"];
|
|
1771
|
+
const common = ["--repo", repo, "--path", repoPath, "--db", db, "--payload", payloadDir, ...enc];
|
|
1772
|
+
try {
|
|
1773
|
+
refresh.phase = "ingest";
|
|
1774
|
+
await spawnStep(bin, ["ingest", ...common]);
|
|
1775
|
+
refresh.phase = "observe";
|
|
1776
|
+
await spawnStep(bin, ["observe", ...common]);
|
|
1777
|
+
refresh.phase = "embed";
|
|
1778
|
+
await backfillEmbeddings(store, getText, embedder);
|
|
1779
|
+
refresh.state = "done";
|
|
1780
|
+
} catch (err) {
|
|
1781
|
+
refresh.state = "error";
|
|
1782
|
+
refresh.error = String(err);
|
|
1783
|
+
} finally {
|
|
1784
|
+
refresh.finishedAt = Date.now();
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
await backfillEmbeddings(store, getText, embedder);
|
|
1788
|
+
function shapeAsk(result) {
|
|
1789
|
+
return {
|
|
1790
|
+
tasks: result.highlights.tasks,
|
|
1791
|
+
// legacy
|
|
1792
|
+
artifacts: result.highlights.artifacts,
|
|
1793
|
+
// legacy
|
|
1794
|
+
explanation: result.explanation,
|
|
1795
|
+
// legacy
|
|
1796
|
+
intent: result.intent,
|
|
1797
|
+
confidence: result.confidence,
|
|
1798
|
+
findings: result.findings,
|
|
1799
|
+
distribution: result.distribution,
|
|
1800
|
+
goldSignals: result.goldSignals,
|
|
1801
|
+
topRisky: result.topRisky,
|
|
1802
|
+
provider: {
|
|
1803
|
+
name: result.egress.llm.name,
|
|
1804
|
+
remote: result.egress.llm.remote,
|
|
1805
|
+
ok: result.egress.llm.ok,
|
|
1806
|
+
embedder: result.egress.embedder,
|
|
1807
|
+
redacted: result.egress.redacted,
|
|
1808
|
+
zeroEgress: result.egress.zeroEgress
|
|
1809
|
+
},
|
|
1810
|
+
error: result.error
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
var askCache = new BoundedCache(100);
|
|
1814
|
+
function indexGeneration() {
|
|
1815
|
+
try {
|
|
1816
|
+
const row = store.raw().prepare("SELECT (SELECT COUNT(*) FROM task_versions) tv, (SELECT COUNT(*) FROM artifact_versions) av, (SELECT COALESCE(MAX(timestamp),'') FROM artifact_versions) m").get();
|
|
1817
|
+
return `${row?.tv ?? 0}:${row?.av ?? 0}:${row?.m ?? ""}`;
|
|
1818
|
+
} catch {
|
|
1819
|
+
return String(Date.now());
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
var server = createServer((req, res) => {
|
|
1823
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1824
|
+
void (async () => {
|
|
1825
|
+
try {
|
|
1826
|
+
if (url.pathname === "/graph.json") {
|
|
1827
|
+
json(res, 200, buildGraphSnapshot(store, repo));
|
|
1828
|
+
} else if (url.pathname === "/api/node") {
|
|
1829
|
+
const detail = await buildNodeDetail(store, getText, url.searchParams.get("id") ?? "");
|
|
1830
|
+
if (detail)
|
|
1831
|
+
json(res, 200, detail);
|
|
1832
|
+
else
|
|
1833
|
+
json(res, 404, { error: "node not found" });
|
|
1834
|
+
} else if (url.pathname === "/api/diff") {
|
|
1835
|
+
const raw = await getText(url.searchParams.get("ref") ?? "");
|
|
1836
|
+
if (raw === void 0) {
|
|
1837
|
+
json(res, 404, { error: "diff not found" });
|
|
1838
|
+
} else {
|
|
1839
|
+
const text = readablePayload(raw);
|
|
1840
|
+
json(res, 200, text === void 0 ? { encrypted: true } : { diff: text });
|
|
1841
|
+
}
|
|
1842
|
+
} else if (url.pathname === "/api/debug") {
|
|
1843
|
+
const q = url.searchParams.get("q") ?? "";
|
|
1844
|
+
if (url.searchParams.get("phase") === "retrieve") {
|
|
1845
|
+
json(res, 200, shapeAsk(await agent.retrieve(q)));
|
|
1846
|
+
} else {
|
|
1847
|
+
const key = `${indexGeneration()}|${normalizeQuery(q)}`;
|
|
1848
|
+
const hit = askCache.get(key);
|
|
1849
|
+
if (hit) {
|
|
1850
|
+
json(res, 200, { ...hit, cached: true });
|
|
1851
|
+
} else {
|
|
1852
|
+
const result = await agent.ask(q);
|
|
1853
|
+
const shaped = shapeAsk(result);
|
|
1854
|
+
if (!result.error)
|
|
1855
|
+
askCache.set(key, shaped);
|
|
1856
|
+
json(res, 200, shaped);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
} else if (url.pathname === "/health") {
|
|
1860
|
+
json(res, 200, {
|
|
1861
|
+
ok: true,
|
|
1862
|
+
repo,
|
|
1863
|
+
db,
|
|
1864
|
+
llm: { name: llm.descriptor?.name ?? "echo", remote: llm.descriptor?.remote ?? false, ok: llm.descriptor?.ok },
|
|
1865
|
+
embedder: { name: embedder.descriptor?.name ?? "local", remote: embedder.descriptor?.remote ?? false },
|
|
1866
|
+
zeroEgress: !(llm.descriptor?.remote ?? false) && !(embedder.descriptor?.remote ?? false)
|
|
1867
|
+
});
|
|
1868
|
+
} else if (url.pathname === "/api/refresh" && req.method === "POST") {
|
|
1869
|
+
const sameSite = req.headers["sec-fetch-site"] ?? "same-origin";
|
|
1870
|
+
if (req.headers["x-brain0-refresh"] !== "1" || sameSite !== "same-origin" && sameSite !== "none") {
|
|
1871
|
+
json(res, 403, { error: "forbidden: refresh is same-origin only" });
|
|
1872
|
+
} else if (!repo || !REPO_RE.test(repo) || repo.startsWith("-")) {
|
|
1873
|
+
json(res, 503, {
|
|
1874
|
+
error: "BRAIN0_REPO unset or invalid",
|
|
1875
|
+
hint: "start the server with a valid BRAIN0_REPO=<id>"
|
|
1876
|
+
});
|
|
1877
|
+
} else if (!repoPath || !existsSync(repoPath) || repoPath.startsWith("-")) {
|
|
1878
|
+
json(res, 503, {
|
|
1879
|
+
error: "BRAIN0_REPO_PATH unset or not a directory",
|
|
1880
|
+
hint: "set BRAIN0_REPO_PATH to the observed repo working tree (dev.mjs sets it)"
|
|
1881
|
+
});
|
|
1882
|
+
} else if (!resolveBin()) {
|
|
1883
|
+
json(res, 503, { error: "brain0 binary not found", hint: "run: cargo build -p brain0-cli" });
|
|
1884
|
+
} else if (refresh.state === "running") {
|
|
1885
|
+
json(res, 409, { error: "refresh already running", jobId: refresh.jobId });
|
|
1886
|
+
} else {
|
|
1887
|
+
void runRefresh();
|
|
1888
|
+
json(res, 202, { jobId: refresh.jobId, state: "running" });
|
|
1889
|
+
}
|
|
1890
|
+
} else if (url.pathname === "/api/refresh/status") {
|
|
1891
|
+
json(res, 200, refresh);
|
|
1892
|
+
} else if (req.method === "GET" || req.method === "HEAD") {
|
|
1893
|
+
await serveStatic(url.pathname, res);
|
|
1894
|
+
} else {
|
|
1895
|
+
json(res, 404, { error: "not found" });
|
|
1896
|
+
}
|
|
1897
|
+
} catch (err) {
|
|
1898
|
+
json(res, 500, { error: String(err) });
|
|
1899
|
+
}
|
|
1900
|
+
})();
|
|
1901
|
+
});
|
|
1902
|
+
var zeroEgress = !(llm.descriptor?.remote ?? false) && !(embedder.descriptor?.remote ?? false);
|
|
1903
|
+
server.listen(port, "127.0.0.1", () => {
|
|
1904
|
+
console.log(`brain0-server listening on :${port} (repo=${repo || "<unset>"} db=${db})`);
|
|
1905
|
+
console.log(guiAvailable ? ` gui: http://localhost:${port}/ (serving ${guiDir})` : ` gui: not built \u2014 API only (run \`pnpm --filter @brain0/gui build\` or set BRAIN0_GUI_DIR)`);
|
|
1906
|
+
console.log(` llm: ${llm.descriptor?.name ?? "echo"}${llm.descriptor?.ok === false ? " (UNREACHABLE \u2014 run `ollama serve` or set a key)" : ""} \xB7 embeddings: ${embedder.descriptor?.name ?? "local"} \xB7 ${zeroEgress ? "zero egress (all local)" : "REMOTE \u2014 context redacted before egress"}`);
|
|
1907
|
+
});
|