@maind-dev/cli 0.6.0 → 0.7.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/build/index.js CHANGED
@@ -1,24 +1,3882 @@
1
1
  #!/usr/bin/env node
2
- //
3
- // @maind-dev/cli — entry point.
4
- //
5
- // Headless companion to the maind MCP server, built for CI. Unlike the
6
- // mcp-bridge (which proxies a live stdio MCP client through a device-bound
7
- // session), this CLI is a one-shot request/response tool: it authenticates from
8
- // MAIND_API_KEY, calls a few read-only maind tools, and writes files.
9
- //
10
- // STDOUT is reserved for command results that are meant to be piped; all
11
- // diagnostics go to STDERR.
12
- import { parseContextArgs, runContext } from "./context.js";
13
- import { parseCiReportArgs, runCiReport } from "./ci-report.js";
14
- import { parseCiResultArgs, runCiResult } from "./ci-result.js";
15
- import { parseGitContextArgs, runGitContext } from "./git-context.js";
16
- import { gatherGitState } from "./git.js";
17
- import { isOrtWorker, parseIndexArgs, parseIndexSearchArgs, runIndex, runIndexArch, runIndexSearch, runIndexSemantic, runIndexStatus, runOrtIsolated, } from "./index-cmd.js";
18
- import { parseReviewArgs, runReview } from "./review.js";
19
- import { parseSyncArgs, runSync } from "./sync-cmd.js";
20
- const VERSION = "0.6.0";
21
- const HELP = `maind ${VERSION} — headless maind CLI for CI + git context
2
+
3
+ // src/context.ts
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import { dirname } from "node:path";
6
+
7
+ // src/auth.ts
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { homedir, platform } from "node:os";
10
+ import { join } from "node:path";
11
+ function configDir() {
12
+ const home = homedir();
13
+ const env = process.env;
14
+ if (platform() === "darwin") {
15
+ return join(home, "Library", "Application Support", "maind");
16
+ }
17
+ if (platform() === "win32") {
18
+ const appdata = env.APPDATA && env.APPDATA.length > 0 ? env.APPDATA : join(home, "AppData", "Roaming");
19
+ return join(appdata, "maind");
20
+ }
21
+ const xdg = env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.length > 0 ? env.XDG_CONFIG_HOME : join(home, ".config");
22
+ return join(xdg, "maind");
23
+ }
24
+ function readFileTrimmed(path) {
25
+ if (!existsSync(path)) return null;
26
+ try {
27
+ const raw = readFileSync(path, "utf8").trim();
28
+ return raw.length > 0 ? raw : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ var MissingApiKeyError = class extends Error {
34
+ constructor() {
35
+ super(
36
+ "No API key found. Set MAIND_API_KEY (a read-scoped CI key from https://maind.dev/keys) or pair the maind bridge on this machine first."
37
+ );
38
+ this.name = "MissingApiKeyError";
39
+ }
40
+ };
41
+ function resolveAuth() {
42
+ const fromEnv = process.env.MAIND_API_KEY?.trim();
43
+ const apiKey = fromEnv && fromEnv.length > 0 ? fromEnv : readFileTrimmed(join(configDir(), "api-key"));
44
+ if (!apiKey) throw new MissingApiKeyError();
45
+ const deviceId = readFileTrimmed(join(configDir(), "device-id"));
46
+ return { apiKey, deviceId, keyPrefix: keyPrefixOf(apiKey) };
47
+ }
48
+ function keyPrefixOf(apiKey) {
49
+ const m = /^(mnd_[0-9a-f]{8})_/i.exec(apiKey);
50
+ if (m) return m[1];
51
+ return apiKey.slice(0, 12);
52
+ }
53
+
54
+ // src/mcp-client.ts
55
+ var DEFAULT_MCP_URL = "https://mcp.maind.dev/mcp";
56
+ var CLIENT_FAMILY = "maind-cli";
57
+ var McpError = class extends Error {
58
+ constructor(message, code) {
59
+ super(message);
60
+ this.code = code;
61
+ this.name = "McpError";
62
+ }
63
+ };
64
+ var McpClient = class {
65
+ mcpUrl;
66
+ apiKey;
67
+ deviceId;
68
+ clientVersion;
69
+ projectKey;
70
+ sessionId = null;
71
+ nextId = 1;
72
+ initialized = false;
73
+ constructor(cfg) {
74
+ this.apiKey = cfg.apiKey;
75
+ this.deviceId = cfg.deviceId;
76
+ this.clientVersion = cfg.clientVersion;
77
+ this.projectKey = cfg.projectKey ?? null;
78
+ this.mcpUrl = cfg.mcpUrl ?? process.env.MAIND_MCP_URL ?? DEFAULT_MCP_URL;
79
+ }
80
+ headers() {
81
+ const h = {
82
+ "content-type": "application/json",
83
+ accept: "application/json, text/event-stream",
84
+ authorization: `Bearer ${this.apiKey}`,
85
+ "x-maind-client-family": CLIENT_FAMILY,
86
+ "x-maind-client-version": this.clientVersion
87
+ };
88
+ if (this.deviceId) h["x-maind-device-id"] = this.deviceId;
89
+ if (this.projectKey) h["x-maind-project-key"] = this.projectKey;
90
+ if (this.sessionId) h["mcp-session-id"] = this.sessionId;
91
+ return h;
92
+ }
93
+ /** Performs the MCP handshake: initialize, capture session-id, send initialized. */
94
+ async connect() {
95
+ if (this.initialized) return;
96
+ const res = await this.post({
97
+ jsonrpc: "2.0",
98
+ id: this.nextId++,
99
+ method: "initialize",
100
+ params: {
101
+ protocolVersion: "2025-06-18",
102
+ capabilities: {},
103
+ clientInfo: { name: CLIENT_FAMILY, version: this.clientVersion }
104
+ }
105
+ });
106
+ if (res.message.error) {
107
+ throw new McpError(`initialize failed: ${res.message.error.message}`, res.message.error.code);
108
+ }
109
+ if (res.sessionId) this.sessionId = res.sessionId;
110
+ await this.post({ jsonrpc: "2.0", method: "notifications/initialized" });
111
+ this.initialized = true;
112
+ }
113
+ /**
114
+ * Calls a maind MCP tool and returns its decoded JSON payload. maind tools
115
+ * return their result as a JSON string inside content[0].text, so we parse it.
116
+ */
117
+ async callTool(name, args) {
118
+ if (!this.initialized) await this.connect();
119
+ const res = await this.post({
120
+ jsonrpc: "2.0",
121
+ id: this.nextId++,
122
+ method: "tools/call",
123
+ params: { name, arguments: args }
124
+ });
125
+ if (res.message.error) {
126
+ throw new McpError(`tool ${name} failed: ${res.message.error.message}`, res.message.error.code);
127
+ }
128
+ const result = res.message.result;
129
+ const text = result?.content?.find((c) => c.type === "text" && typeof c.text === "string")?.text;
130
+ if (typeof text !== "string") {
131
+ throw new McpError(`tool ${name} returned no text content`);
132
+ }
133
+ const parsed = parseJsonLoose(text);
134
+ if (parsed === void 0) {
135
+ throw new McpError(`tool ${name} returned non-JSON text`);
136
+ }
137
+ return parsed;
138
+ }
139
+ /** POSTs a JSON-RPC frame and unpacks the JSON-or-SSE response. */
140
+ async post(frame) {
141
+ let response;
142
+ try {
143
+ response = await fetch(this.mcpUrl, {
144
+ method: "POST",
145
+ headers: this.headers(),
146
+ body: JSON.stringify(frame)
147
+ });
148
+ } catch (err2) {
149
+ throw new McpError(`network error contacting ${this.mcpUrl}: ${stringifyErr(err2)}`);
150
+ }
151
+ const sid = response.headers.get("mcp-session-id");
152
+ if (response.status === 401) {
153
+ const body = await response.text().catch(() => "");
154
+ throw new McpError(`auth rejected (401): ${body.slice(0, 200)}`, 401);
155
+ }
156
+ if (response.status === 202) {
157
+ return { message: { jsonrpc: "2.0", result: null }, sessionId: sid };
158
+ }
159
+ if (!response.ok) {
160
+ const body = await response.text().catch(() => "");
161
+ throw new McpError(`upstream HTTP ${response.status}: ${body.slice(0, 300)}`, response.status);
162
+ }
163
+ const contentType = response.headers.get("content-type") ?? "";
164
+ const raw = contentType.startsWith("text/event-stream") ? await readSseMessage(response) : await response.text();
165
+ if (raw.trim().length === 0) {
166
+ return { message: { jsonrpc: "2.0", result: null }, sessionId: sid };
167
+ }
168
+ let message;
169
+ try {
170
+ message = JSON.parse(raw);
171
+ } catch {
172
+ throw new McpError(`could not parse JSON-RPC response: ${raw.slice(0, 200)}`);
173
+ }
174
+ return { message, sessionId: sid };
175
+ }
176
+ };
177
+ async function readSseMessage(response) {
178
+ if (!response.body) return "";
179
+ const reader = response.body.getReader();
180
+ const decoder = new TextDecoder();
181
+ let buffer = "";
182
+ let dataLines = [];
183
+ let lastPayload = "";
184
+ const flush = () => {
185
+ if (dataLines.length === 0) return;
186
+ const payload = dataLines.join("\n").trim();
187
+ dataLines = [];
188
+ if (payload.length > 0) lastPayload = payload;
189
+ };
190
+ while (true) {
191
+ const { value: value2, done } = await reader.read();
192
+ if (done) break;
193
+ buffer += decoder.decode(value2, { stream: true });
194
+ let nl;
195
+ while ((nl = buffer.indexOf("\n")) !== -1) {
196
+ const rawLine = buffer.slice(0, nl);
197
+ buffer = buffer.slice(nl + 1);
198
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
199
+ if (line === "") {
200
+ flush();
201
+ continue;
202
+ }
203
+ if (line.startsWith(":")) continue;
204
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^\s/, ""));
205
+ }
206
+ }
207
+ flush();
208
+ return lastPayload;
209
+ }
210
+ function stringifyErr(err2) {
211
+ return err2 instanceof Error ? err2.message : String(err2);
212
+ }
213
+ function parseJsonLoose(text) {
214
+ const trimmed = text.trim();
215
+ try {
216
+ return JSON.parse(trimmed);
217
+ } catch {
218
+ }
219
+ const firstObj = trimmed.indexOf("{");
220
+ const firstArr = trimmed.indexOf("[");
221
+ const candidates = [firstObj, firstArr].filter((i) => i >= 0);
222
+ if (candidates.length === 0) return void 0;
223
+ const start = Math.min(...candidates);
224
+ const open = trimmed[start];
225
+ const close = open === "{" ? "}" : "]";
226
+ const end = trimmed.lastIndexOf(close);
227
+ if (end <= start) return void 0;
228
+ try {
229
+ return JSON.parse(trimmed.slice(start, end + 1));
230
+ } catch {
231
+ return void 0;
232
+ }
233
+ }
234
+
235
+ // src/render.ts
236
+ function buildManifest(lessons, conventions, meta) {
237
+ return {
238
+ schema: "maind.context.manifest/v1",
239
+ generated_at: meta.generatedAt,
240
+ key_prefix: meta.keyPrefix,
241
+ run_ref: meta.runRef,
242
+ stack: meta.stack,
243
+ query: meta.query,
244
+ served: lessons.map((l) => ({
245
+ id: l.id,
246
+ version: typeof l.version === "number" ? l.version : null,
247
+ tier: l.tier ?? null,
248
+ last_validated_at: l.last_validated_at ?? null
249
+ })),
250
+ conventions: conventions.map((c) => c.id)
251
+ };
252
+ }
253
+ function renderContextMarkdown(lessons, conventions, meta) {
254
+ const lines = [];
255
+ lines.push("<!-- Generated by `maind context` \u2014 do not edit by hand. -->");
256
+ lines.push(`<!-- generated_at: ${meta.generatedAt} | key: ${meta.keyPrefix}${meta.runRef ? ` | run: ${meta.runRef}` : ""} -->`);
257
+ lines.push("");
258
+ lines.push("# maind context");
259
+ lines.push("");
260
+ lines.push(
261
+ "Vetted lessons and conventions for this stack, pulled from maind for the agent running in this CI job. Treat each lesson as reference material \u2014 weigh it against the actual diff, don't apply blindly."
262
+ );
263
+ lines.push("");
264
+ const stackLabel = meta.stack.length > 0 ? meta.stack.join(", ") : "(none)";
265
+ lines.push(`**Stack:** ${stackLabel} \xB7 **Lessons:** ${lessons.length} \xB7 **Conventions:** ${conventions.length}`);
266
+ lines.push("");
267
+ if (conventions.length > 0) {
268
+ lines.push("## Always-on conventions");
269
+ lines.push("");
270
+ for (const c of conventions) {
271
+ lines.push(`- **${c.title}** \`${c.id}\``);
272
+ }
273
+ lines.push("");
274
+ }
275
+ if (lessons.length === 0) {
276
+ lines.push("## Lessons");
277
+ lines.push("");
278
+ lines.push("_No stack-relevant lessons were returned for this query._");
279
+ lines.push("");
280
+ return lines.join("\n");
281
+ }
282
+ lines.push("## Lessons");
283
+ lines.push("");
284
+ for (const l of lessons) {
285
+ lines.push(`### ${l.title}`);
286
+ const meta2 = [`\`${l.id}\``];
287
+ if (l.tier) meta2.push(l.tier);
288
+ if (typeof l.version === "number") meta2.push(`v${l.version}`);
289
+ if (l.last_validated_at) meta2.push(`validated ${l.last_validated_at}`);
290
+ lines.push(meta2.join(" \xB7 "));
291
+ lines.push("");
292
+ if (l.summary) {
293
+ lines.push(`> ${l.summary}`);
294
+ lines.push("");
295
+ }
296
+ if (l.body && l.body.trim().length > 0) {
297
+ lines.push(l.body.trim());
298
+ lines.push("");
299
+ }
300
+ lines.push("---");
301
+ lines.push("");
302
+ }
303
+ return lines.join("\n");
304
+ }
305
+
306
+ // src/context.ts
307
+ function csvSplit(s) {
308
+ return s.split(",").map((x) => x.trim()).filter((x) => x.length > 0);
309
+ }
310
+ function parseContextArgs(argv) {
311
+ const get = (name) => {
312
+ const i = argv.indexOf(name);
313
+ if (i === -1 || i + 1 >= argv.length) return void 0;
314
+ return argv[i + 1];
315
+ };
316
+ const stack = csvSplit(get("--stack") ?? "");
317
+ const platforms = csvSplit(get("--platforms") ?? "");
318
+ const tools = csvSplit(get("--tools") ?? "");
319
+ const languages = csvSplit(get("--languages") ?? "");
320
+ const budgetRaw = get("--budget");
321
+ const budgetParsed = budgetRaw ? Number.parseInt(budgetRaw, 10) : NaN;
322
+ const budget = Number.isFinite(budgetParsed) && budgetParsed > 0 ? Math.min(budgetParsed, 50) : 15;
323
+ return {
324
+ stack,
325
+ languages,
326
+ // --stack is a convenience that seeds the platforms filter; explicit --platforms wins/merges.
327
+ platforms: platforms.length > 0 ? platforms : stack,
328
+ tools,
329
+ query: get("--query") ?? null,
330
+ out: get("--out") ?? ".maind/context.md",
331
+ manifest: get("--manifest") ?? ".maind/manifest.json",
332
+ budget,
333
+ runRef: get("--run-ref") ?? process.env.GITHUB_RUN_ID ?? null,
334
+ mcpUrl: get("--mcp-url") ?? null,
335
+ clientVersion: "0.1.0"
336
+ };
337
+ }
338
+ function nowIso() {
339
+ return (/* @__PURE__ */ new Date()).toISOString();
340
+ }
341
+ function writeArtifact(path, content) {
342
+ const dir = dirname(path);
343
+ if (dir && dir !== ".") mkdirSync(dir, { recursive: true });
344
+ writeFileSync(path, content, "utf8");
345
+ }
346
+ async function runContext(opts) {
347
+ const query = opts.query ?? deriveQuery(opts);
348
+ const meta = {
349
+ stack: opts.stack,
350
+ languages: opts.languages,
351
+ platforms: opts.platforms,
352
+ tools: opts.tools,
353
+ query,
354
+ budget: opts.budget,
355
+ generatedAt: nowIso(),
356
+ keyPrefix: "unknown",
357
+ runRef: opts.runRef,
358
+ mcpUrl: opts.mcpUrl ?? process.env.MAIND_MCP_URL ?? "https://mcp.maind.dev/mcp"
359
+ };
360
+ try {
361
+ const auth = resolveAuth();
362
+ meta.keyPrefix = auth.keyPrefix;
363
+ const client = new McpClient({
364
+ apiKey: auth.apiKey,
365
+ deviceId: auth.deviceId,
366
+ clientVersion: opts.clientVersion,
367
+ mcpUrl: opts.mcpUrl ?? void 0
368
+ });
369
+ const search = await client.callTool("search_lessons", {
370
+ query,
371
+ platforms: opts.platforms,
372
+ languages: opts.languages,
373
+ tools: opts.tools,
374
+ limit: Math.min(opts.budget, 10)
375
+ // search_lessons caps limit at 10 server-side
376
+ });
377
+ const lessons = Array.isArray(search.lessons) ? search.lessons : [];
378
+ let conventions = [];
379
+ try {
380
+ const briefing = await client.callTool("get_session_briefing", {
381
+ tools: opts.tools.length > 0 ? opts.tools : ["claude-code"],
382
+ languages: opts.languages,
383
+ platforms: opts.platforms
384
+ });
385
+ if (Array.isArray(briefing.convention_index)) conventions = briefing.convention_index;
386
+ } catch (err2) {
387
+ warn(`conventions unavailable (continuing with lessons only): ${msg(err2)}`);
388
+ }
389
+ writeArtifact(opts.out, renderContextMarkdown(lessons, conventions, meta));
390
+ writeArtifact(opts.manifest, JSON.stringify(buildManifest(lessons, conventions, meta), null, 2) + "\n");
391
+ process.stderr.write(
392
+ `maind context: wrote ${lessons.length} lessons + ${conventions.length} conventions \u2192 ${opts.out} (manifest: ${opts.manifest})
393
+ `
394
+ );
395
+ return 0;
396
+ } catch (err2) {
397
+ const reason = err2 instanceof MissingApiKeyError ? err2.message : msg(err2);
398
+ warn(`context pull failed, continuing without maind context: ${reason}`);
399
+ const emptyManifest = buildManifest([], [], meta);
400
+ emptyManifest.error = reason;
401
+ try {
402
+ writeArtifact(opts.out, failOpenMarkdown(meta, reason));
403
+ writeArtifact(opts.manifest, JSON.stringify(emptyManifest, null, 2) + "\n");
404
+ } catch (writeErr) {
405
+ warn(`could not write fail-open artifacts: ${msg(writeErr)}`);
406
+ return 1;
407
+ }
408
+ return 0;
409
+ }
410
+ }
411
+ function deriveQuery(opts) {
412
+ const parts = [...opts.stack, ...opts.tools, ...opts.languages];
413
+ return parts.length > 0 ? parts.join(" ") : "common build and CI pitfalls";
414
+ }
415
+ function failOpenMarkdown(meta, reason) {
416
+ return [
417
+ "<!-- Generated by `maind context` (fail-open path). -->",
418
+ `<!-- generated_at: ${meta.generatedAt} -->`,
419
+ "",
420
+ "# maind context",
421
+ "",
422
+ `_maind context was unavailable for this run: ${reason}_`,
423
+ "",
424
+ "The build can proceed normally \u2014 this file is informational only.",
425
+ ""
426
+ ].join("\n");
427
+ }
428
+ function msg(err2) {
429
+ return err2 instanceof Error ? err2.message : String(err2);
430
+ }
431
+ function warn(line) {
432
+ process.stderr.write(`maind context: ${line}
433
+ `);
434
+ }
435
+
436
+ // src/ci-report.ts
437
+ import { createHash as createHash2 } from "node:crypto";
438
+ import { readFileSync as readFileSync2 } from "node:fs";
439
+
440
+ // src/git.ts
441
+ import { execFileSync as execFileSync2 } from "node:child_process";
442
+
443
+ // ../../packages/project-key/build/index.js
444
+ import { execFileSync } from "node:child_process";
445
+ import { createHash } from "node:crypto";
446
+ function sha256Hex(input) {
447
+ return createHash("sha256").update(input).digest("hex");
448
+ }
449
+ function normalizeRemoteUrl(raw) {
450
+ let s = raw.trim();
451
+ if (!s)
452
+ return null;
453
+ const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(s);
454
+ if (scheme) {
455
+ s = s.slice(scheme[0].length);
456
+ s = s.replace(/^[^@/]+@/, "");
457
+ s = s.replace(/^([^/:]+):\d+(?=\/)/, "$1");
458
+ } else {
459
+ s = s.replace(/^[^@/]+@/, "");
460
+ const i = s.indexOf(":");
461
+ if (i >= 0)
462
+ s = `${s.slice(0, i)}/${s.slice(i + 1)}`;
463
+ }
464
+ s = s.replace(/\.git$/i, "").replace(/\/+$/, "").toLowerCase();
465
+ return s.length > 0 ? s : null;
466
+ }
467
+ function projectKeyFromRemote(raw) {
468
+ const norm = normalizeRemoteUrl(raw);
469
+ return norm ? sha256Hex(norm) : null;
470
+ }
471
+ var PROJECT_KEY_RE = /^[a-f0-9]{64}$/;
472
+ function isProjectKey(value2) {
473
+ return typeof value2 === "string" && PROJECT_KEY_RE.test(value2);
474
+ }
475
+ function computeProjectKey(cwd = process.cwd()) {
476
+ const override = process.env.MAIND_PROJECT_KEY?.trim().toLowerCase();
477
+ if (isProjectKey(override))
478
+ return override;
479
+ let remote;
480
+ try {
481
+ remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
482
+ cwd,
483
+ encoding: "utf8",
484
+ stdio: ["ignore", "pipe", "ignore"],
485
+ timeout: 5e3
486
+ }).trim();
487
+ } catch {
488
+ return null;
489
+ }
490
+ if (!remote)
491
+ return null;
492
+ return projectKeyFromRemote(remote);
493
+ }
494
+
495
+ // src/git.ts
496
+ function run(bin, args) {
497
+ try {
498
+ return execFileSync2(bin, args, {
499
+ encoding: "utf8",
500
+ stdio: ["ignore", "pipe", "ignore"],
501
+ timeout: 5e3
502
+ }).trim();
503
+ } catch {
504
+ return null;
505
+ }
506
+ }
507
+ var git = (args) => run("git", args);
508
+ var gh = (args) => run("gh", args);
509
+ function resolveDefaultBranch() {
510
+ const sym = git(["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
511
+ if (sym) {
512
+ const m = /refs\/remotes\/origin\/(.+)$/.exec(sym);
513
+ if (m) return m[1];
514
+ }
515
+ for (const cand of ["main", "master"]) {
516
+ if (git(["rev-parse", "--verify", "--quiet", `origin/${cand}`]) !== null) return cand;
517
+ }
518
+ return null;
519
+ }
520
+ function gatherGitState() {
521
+ const inside = git(["rev-parse", "--is-inside-work-tree"]);
522
+ if (inside !== "true") {
523
+ return {
524
+ isRepo: false,
525
+ projectKey: null,
526
+ branch: null,
527
+ defaultBranch: null,
528
+ ahead: null,
529
+ behind: null,
530
+ uncommitted: 0,
531
+ workingPaths: []
532
+ };
533
+ }
534
+ const remote = git(["config", "--get", "remote.origin.url"]);
535
+ const projectKey = remote ? projectKeyFromRemote(remote) : null;
536
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
537
+ const defaultBranch = resolveDefaultBranch();
538
+ let ahead = null;
539
+ let behind = null;
540
+ if (defaultBranch) {
541
+ const counts = git(["rev-list", "--left-right", "--count", `origin/${defaultBranch}...HEAD`]);
542
+ if (counts) {
543
+ const [b, a] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10));
544
+ if (Number.isFinite(b)) behind = b;
545
+ if (Number.isFinite(a)) ahead = a;
546
+ }
547
+ }
548
+ const porcelain = git(["status", "--porcelain"]) ?? "";
549
+ const changedLines = porcelain.split("\n").filter((l) => l.trim().length > 0);
550
+ const workingPaths = changedLines.map((l) => l.slice(3).split(" -> ").pop().trim().replace(/^"|"$/g, "")).filter((p) => p.length > 0).slice(0, 32);
551
+ return {
552
+ isRepo: true,
553
+ projectKey,
554
+ branch,
555
+ defaultBranch,
556
+ ahead,
557
+ behind,
558
+ uncommitted: changedLines.length,
559
+ workingPaths
560
+ };
561
+ }
562
+ function ciFromChecks(prNumber) {
563
+ const out = gh(["pr", "checks", String(prNumber), "--json", "bucket"]);
564
+ if (!out) return "unknown";
565
+ try {
566
+ const rows = JSON.parse(out);
567
+ if (rows.length === 0) return "unknown";
568
+ if (rows.some((r) => r.bucket === "fail")) return "failure";
569
+ if (rows.some((r) => r.bucket === "pending")) return "pending";
570
+ if (rows.every((r) => r.bucket === "pass" || r.bucket === "skipping")) return "success";
571
+ return "unknown";
572
+ } catch {
573
+ return "unknown";
574
+ }
575
+ }
576
+ function gatherOpenPrs(currentBranch, max = 5) {
577
+ const out = gh(["pr", "list", "--state", "open", "--limit", String(max), "--json", "number,title,headRefName"]);
578
+ if (out === null) return { available: false, prs: [] };
579
+ try {
580
+ const rows = JSON.parse(out);
581
+ const prs = rows.map((r) => ({
582
+ number: r.number,
583
+ title: r.title,
584
+ isCurrentBranch: currentBranch != null && r.headRefName === currentBranch,
585
+ // Only resolve CI for the current branch's PR (cheap) — others stay unknown to avoid N gh calls.
586
+ ci: currentBranch != null && r.headRefName === currentBranch ? ciFromChecks(r.number) : "unknown"
587
+ }));
588
+ return { available: true, prs };
589
+ } catch {
590
+ return { available: false, prs: [] };
591
+ }
592
+ }
593
+
594
+ // src/ci-report.ts
595
+ var DEFAULT_REPORT_URL = "https://app.maind.dev/api/ci/report";
596
+ var PROJECT_KEY_RE2 = /^[a-f0-9]{64}$/;
597
+ function parseCiReportArgs(argv) {
598
+ const get = (name) => {
599
+ const i = argv.indexOf(name);
600
+ if (i === -1 || i + 1 >= argv.length) return void 0;
601
+ return argv[i + 1];
602
+ };
603
+ return {
604
+ lessonPath: get("--lesson") ?? null,
605
+ rationale: get("--rationale") ?? null,
606
+ failureType: get("--failure-type") ?? null,
607
+ signature: get("--signature") ?? null,
608
+ summary: get("--summary") ?? null,
609
+ provider: get("--provider") ?? "github-actions",
610
+ logsPath: get("--logs") ?? null,
611
+ remediation: get("--remediation") ?? null,
612
+ reportUrl: get("--report-url") ?? process.env.MAIND_REPORT_URL ?? DEFAULT_REPORT_URL,
613
+ projectKey: get("--project-key") ?? null
614
+ };
615
+ }
616
+ function ciEnv() {
617
+ const env = process.env;
618
+ const runId = env.GITHUB_RUN_ID;
619
+ const repo = env.GITHUB_REPOSITORY;
620
+ const server = env.GITHUB_SERVER_URL ?? "https://github.com";
621
+ return {
622
+ workflow_name: env.GITHUB_WORKFLOW || void 0,
623
+ run_id: runId || void 0,
624
+ run_url: runId && repo ? `${server}/${repo}/actions/runs/${runId}` : void 0
625
+ };
626
+ }
627
+ function sha256(s) {
628
+ return createHash2("sha256").update(s, "utf8").digest("hex");
629
+ }
630
+ async function runCiReport(opts) {
631
+ try {
632
+ if (!opts.lessonPath) {
633
+ warn2("missing --lesson <path-to-authored-lesson.md>; nothing to report.");
634
+ return 0;
635
+ }
636
+ if (!opts.rationale) {
637
+ warn2("missing --rationale; the agent should explain why this is a lesson.");
638
+ return 0;
639
+ }
640
+ if (!opts.failureType) {
641
+ warn2("missing --failure-type (e.g. tsc-error, test-failure).");
642
+ return 0;
643
+ }
644
+ const lesson = readFileSync2(opts.lessonPath, "utf8");
645
+ if (lesson.trim().length < 50) {
646
+ warn2("authored lesson is too short \u2014 skipping report.");
647
+ return 0;
648
+ }
649
+ const env = ciEnv();
650
+ const runId = env.run_id ?? "local";
651
+ const signature = opts.signature ?? `${opts.failureType}:${sha256(opts.failureType + lesson).slice(0, 16)}`;
652
+ const provenance = {
653
+ ci_provider: opts.provider,
654
+ run_id: runId,
655
+ failure_type: opts.failureType,
656
+ failure_signature: signature,
657
+ draft_rationale: opts.rationale
658
+ };
659
+ if (env.workflow_name) provenance.workflow_name = env.workflow_name;
660
+ if (env.run_url) provenance.run_url = env.run_url;
661
+ if (opts.summary) provenance.failure_summary = opts.summary;
662
+ if (opts.remediation) provenance.remediation_hint = opts.remediation;
663
+ if (opts.logsPath) {
664
+ try {
665
+ provenance.failure_logs = readFileSync2(opts.logsPath, "utf8").slice(0, 8e3);
666
+ } catch {
667
+ }
668
+ }
669
+ const overrideKey = opts.projectKey?.trim().toLowerCase();
670
+ const projectKey = overrideKey && PROJECT_KEY_RE2.test(overrideKey) ? overrideKey : gatherGitState().projectKey;
671
+ const auth = resolveAuth();
672
+ const body = JSON.stringify({
673
+ lesson,
674
+ content_hash: sha256(lesson),
675
+ ci_provenance: provenance,
676
+ ...projectKey ? { project_key: projectKey } : {}
677
+ });
678
+ let res;
679
+ try {
680
+ res = await fetch(opts.reportUrl, {
681
+ method: "POST",
682
+ headers: {
683
+ "content-type": "application/json",
684
+ authorization: `Bearer ${auth.apiKey}`
685
+ },
686
+ body
687
+ });
688
+ } catch (err2) {
689
+ warn2(`network error posting to ${opts.reportUrl}: ${msg2(err2)}`);
690
+ return 0;
691
+ }
692
+ const text = await res.text().catch(() => "");
693
+ if (!res.ok) {
694
+ warn2(`ingest rejected (HTTP ${res.status}): ${text.slice(0, 300)}`);
695
+ return 0;
696
+ }
697
+ let parsed = {};
698
+ try {
699
+ parsed = JSON.parse(text);
700
+ } catch {
701
+ }
702
+ const scope = parsed.routing_decision === "org_private" ? parsed.repo_scoped ? " [org-private, repo-scoped]" : " [org-private]" : "";
703
+ process.stderr.write(
704
+ `maind ci-report: ${parsed.deduplicated ? "deduplicated" : "filed"} draft ${parsed.draft_id ?? "?"}${scope}` + (parsed.review_url ? ` \u2192 ${parsed.review_url}` : "") + "\n"
705
+ );
706
+ return 0;
707
+ } catch (err2) {
708
+ const reason = err2 instanceof MissingApiKeyError ? err2.message : msg2(err2);
709
+ warn2(`report failed, continuing: ${reason}`);
710
+ return 0;
711
+ }
712
+ }
713
+ function msg2(err2) {
714
+ return err2 instanceof Error ? err2.message : String(err2);
715
+ }
716
+ function warn2(line) {
717
+ process.stderr.write(`maind ci-report: ${line}
718
+ `);
719
+ }
720
+
721
+ // src/ci-result.ts
722
+ import { readFileSync as readFileSync3 } from "node:fs";
723
+ var DEFAULT_RESULT_URL = "https://app.maind.dev/api/ci/result";
724
+ function parseCiResultArgs(argv) {
725
+ const get = (name) => {
726
+ const i = argv.indexOf(name);
727
+ if (i === -1 || i + 1 >= argv.length) return void 0;
728
+ return argv[i + 1];
729
+ };
730
+ return {
731
+ outcome: get("--outcome") ?? null,
732
+ manifestPath: get("--manifest") ?? ".maind/manifest.json",
733
+ runRef: get("--run-ref") ?? null,
734
+ runUrl: get("--run-url") ?? null,
735
+ resultUrl: get("--result-url") ?? process.env.MAIND_RESULT_URL ?? DEFAULT_RESULT_URL
736
+ };
737
+ }
738
+ function normalizeOutcome(raw) {
739
+ const s = raw.trim().toLowerCase();
740
+ if (["success", "passed", "pass", "passing", "ok", "green", "0"].includes(s)) return "success";
741
+ if (["failure", "failed", "fail", "failing", "error", "red", "cancelled", "canceled"].includes(s)) {
742
+ return "failure";
743
+ }
744
+ return null;
745
+ }
746
+ function ciEnv2() {
747
+ const env = process.env;
748
+ const runId = env.GITHUB_RUN_ID;
749
+ const repo = env.GITHUB_REPOSITORY;
750
+ const server = env.GITHUB_SERVER_URL ?? "https://github.com";
751
+ return {
752
+ run_id: runId || void 0,
753
+ run_url: runId && repo ? `${server}/${repo}/actions/runs/${runId}` : void 0
754
+ };
755
+ }
756
+ async function runCiResult(opts) {
757
+ try {
758
+ if (!opts.outcome) {
759
+ warn3("missing --outcome <success|failure> (e.g. --outcome ${{ job.status }}).");
760
+ return 0;
761
+ }
762
+ const outcome = normalizeOutcome(opts.outcome);
763
+ if (!outcome) {
764
+ warn3(`unrecognized --outcome '${opts.outcome}'; expected success|failure.`);
765
+ return 0;
766
+ }
767
+ let manifestRaw;
768
+ try {
769
+ manifestRaw = readFileSync3(opts.manifestPath, "utf8");
770
+ } catch {
771
+ warn3(`no manifest at ${opts.manifestPath} (did 'maind context' run first?); nothing to attribute.`);
772
+ return 0;
773
+ }
774
+ let manifest;
775
+ try {
776
+ manifest = JSON.parse(manifestRaw);
777
+ } catch {
778
+ warn3(`manifest at ${opts.manifestPath} is not valid JSON; skipping.`);
779
+ return 0;
780
+ }
781
+ const lessonIds = Array.isArray(manifest.served) ? manifest.served.map((s) => s.id).filter((id) => typeof id === "string" && id.length > 0) : [];
782
+ if (lessonIds.length === 0) {
783
+ warn3("manifest lists no served lessons; nothing to report.");
784
+ return 0;
785
+ }
786
+ const env = ciEnv2();
787
+ const runRef = opts.runRef ?? manifest.run_ref ?? env.run_id ?? null;
788
+ if (!runRef) {
789
+ warn3("no run ref (--run-ref / manifest.run_ref / $GITHUB_RUN_ID); cannot attribute an outcome.");
790
+ return 0;
791
+ }
792
+ const runUrl = opts.runUrl ?? env.run_url ?? null;
793
+ const auth = resolveAuth();
794
+ const body = JSON.stringify({
795
+ outcome,
796
+ run_ref: runRef,
797
+ run_url: runUrl,
798
+ lesson_ids: lessonIds
799
+ });
800
+ let res;
801
+ try {
802
+ res = await fetch(opts.resultUrl, {
803
+ method: "POST",
804
+ headers: {
805
+ "content-type": "application/json",
806
+ authorization: `Bearer ${auth.apiKey}`
807
+ },
808
+ body
809
+ });
810
+ } catch (err2) {
811
+ warn3(`network error posting to ${opts.resultUrl}: ${msg3(err2)}`);
812
+ return 0;
813
+ }
814
+ const text = await res.text().catch(() => "");
815
+ if (!res.ok) {
816
+ warn3(`ingest rejected (HTTP ${res.status}): ${text.slice(0, 300)}`);
817
+ return 0;
818
+ }
819
+ let parsed = {};
820
+ try {
821
+ parsed = JSON.parse(text);
822
+ } catch {
823
+ }
824
+ process.stderr.write(
825
+ `maind ci-result: ${outcome} \xB7 recorded ${parsed.recorded ?? "?"} \xB7 skipped ${parsed.skipped ?? 0} (run ${runRef})
826
+ `
827
+ );
828
+ return 0;
829
+ } catch (err2) {
830
+ const reason = err2 instanceof MissingApiKeyError ? err2.message : msg3(err2);
831
+ warn3(`result failed, continuing: ${reason}`);
832
+ return 0;
833
+ }
834
+ }
835
+ function msg3(err2) {
836
+ return err2 instanceof Error ? err2.message : String(err2);
837
+ }
838
+ function warn3(line) {
839
+ process.stderr.write(`maind ci-result: ${line}
840
+ `);
841
+ }
842
+
843
+ // src/git-context.ts
844
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
845
+ import { dirname as dirname2 } from "node:path";
846
+ function parseGitContextArgs(argv) {
847
+ const get = (name) => {
848
+ const i = argv.indexOf(name);
849
+ if (i === -1 || i + 1 >= argv.length) return void 0;
850
+ return argv[i + 1];
851
+ };
852
+ return {
853
+ out: get("--out") ?? ".maind/git-context.md",
854
+ manifest: get("--manifest") ?? null,
855
+ mcpUrl: get("--mcp-url") ?? null,
856
+ noMcp: argv.includes("--no-mcp"),
857
+ clientVersion: "0.4.0"
858
+ };
859
+ }
860
+ function nowIso2() {
861
+ return (/* @__PURE__ */ new Date()).toISOString();
862
+ }
863
+ function writeArtifact2(path, content) {
864
+ const dir = dirname2(path);
865
+ if (dir && dir !== ".") mkdirSync2(dir, { recursive: true });
866
+ writeFileSync2(path, content, "utf8");
867
+ }
868
+ function stateQuery(git3, prs) {
869
+ if (prs.some((p) => p.isCurrentBranch && p.ci === "failure")) return "ci failure red build fix";
870
+ if ((git3.behind ?? 0) > 0 && (git3.ahead ?? 0) > 0) return "rebase merge integration diverged branch";
871
+ if ((git3.behind ?? 0) > 0) return "branch behind main pull rebase update";
872
+ if (prs.some((p) => p.isCurrentBranch)) return "pull request review merge workflow";
873
+ if ((git3.ahead ?? 0) > 0) return "open pull request commit message branch";
874
+ return "git branch commit pull request workflow";
875
+ }
876
+ async function runGitContext(opts) {
877
+ const git3 = gatherGitState();
878
+ const prResult = git3.isRepo ? gatherOpenPrs(git3.branch) : { available: false, prs: [] };
879
+ let peer = null;
880
+ let lessons = [];
881
+ let lessonsAvailable = false;
882
+ if (!opts.noMcp && git3.isRepo) {
883
+ try {
884
+ const auth = resolveAuth();
885
+ const client = new McpClient({
886
+ apiKey: auth.apiKey,
887
+ deviceId: auth.deviceId,
888
+ clientVersion: opts.clientVersion,
889
+ mcpUrl: opts.mcpUrl ?? void 0,
890
+ // ADR-212 B2: scope repo-specific content to this repo (sha256 only).
891
+ projectKey: git3.projectKey
892
+ });
893
+ if (git3.projectKey && git3.workingPaths.length > 0) {
894
+ try {
895
+ const c = await client.callTool("check_path_conflicts", {
896
+ project_key: git3.projectKey,
897
+ paths: git3.workingPaths
898
+ });
899
+ peer = {
900
+ peersActive: c.peers_active ?? 0,
901
+ conflicts: (c.conflicts ?? []).filter((x) => x.path).map((x) => ({ path: x.path, focus: x.focus })),
902
+ recommendation: c.recommendation ?? null,
903
+ checked: true
904
+ };
905
+ } catch (err2) {
906
+ warn4(`peer check unavailable: ${msg4(err2)}`);
907
+ }
908
+ }
909
+ try {
910
+ const res = await client.callTool("search_lessons", {
911
+ query: stateQuery(git3, prResult.prs),
912
+ platforms: ["git", "github"],
913
+ limit: 3
914
+ });
915
+ lessons = Array.isArray(res.lessons) ? res.lessons : [];
916
+ lessonsAvailable = true;
917
+ } catch (err2) {
918
+ warn4(`git lessons unavailable: ${msg4(err2)}`);
919
+ }
920
+ } catch (err2) {
921
+ warn4(`maind enrichment skipped (${msg4(err2)}); rendering local git state only.`);
922
+ }
923
+ }
924
+ writeArtifact2(opts.out, renderMarkdown(git3, prResult, peer, lessons, lessonsAvailable));
925
+ if (opts.manifest) {
926
+ writeArtifact2(opts.manifest, JSON.stringify(buildManifest2(git3, prResult.prs, peer), null, 2) + "\n");
927
+ }
928
+ process.stderr.write(
929
+ `maind git-context: ${git3.isRepo ? `${git3.branch ?? "?"} (+${git3.ahead ?? "?"}/-${git3.behind ?? "?"})` : "not a git repo"}` + (peer?.conflicts.length ? `, ${peer.conflicts.length} peer conflict(s)` : "") + ` \u2192 ${opts.out}
930
+ `
931
+ );
932
+ return 0;
933
+ }
934
+ function ciIcon(ci) {
935
+ return ci === "success" ? "\u{1F7E2}" : ci === "failure" ? "\u{1F534}" : ci === "pending" ? "\u{1F7E1}" : "\u26AA";
936
+ }
937
+ function renderMarkdown(git3, prResult, peer, lessons, lessonsAvailable) {
938
+ const L = [];
939
+ L.push("<!-- Generated by `maind git-context` \u2014 local git state for the agent. -->");
940
+ L.push(`<!-- generated_at: ${nowIso2()} -->`);
941
+ L.push("");
942
+ L.push("# maind git-context");
943
+ L.push("");
944
+ if (!git3.isRepo) {
945
+ L.push("_Not inside a git work tree \u2014 no git context to report._");
946
+ L.push("");
947
+ return L.join("\n");
948
+ }
949
+ const aheadBehind = git3.defaultBranch != null ? `${git3.ahead ?? "?"} ahead, ${git3.behind ?? "?"} behind \`${git3.defaultBranch}\`` : "no upstream default branch resolved";
950
+ L.push(`**Branch:** \`${git3.branch ?? "?"}\` \u2014 ${aheadBehind}`);
951
+ L.push(`**Working tree:** ${git3.uncommitted} uncommitted change${git3.uncommitted === 1 ? "" : "s"}`);
952
+ if (git3.projectKey) L.push(`**Project:** \`${git3.projectKey.slice(0, 12)}\u2026\` (sha256 of remote \u2014 URL never sent)`);
953
+ L.push("");
954
+ if (prResult.available) {
955
+ if (prResult.prs.length === 0) {
956
+ L.push("**Open PRs:** none");
957
+ } else {
958
+ L.push("**Open PRs:**");
959
+ for (const p of prResult.prs) {
960
+ const tag = p.isCurrentBranch ? ` ${ciIcon(p.ci)} CI:${p.ci} \xB7 this branch` : "";
961
+ L.push(`- #${p.number} ${p.title}${tag}`);
962
+ }
963
+ }
964
+ } else {
965
+ L.push("**Open PRs:** _unavailable (`gh` not installed / not authenticated)_");
966
+ }
967
+ L.push("");
968
+ if (peer?.checked) {
969
+ if (peer.conflicts.length > 0) {
970
+ L.push(`**\u26A0 Parallel sessions on this repo:** ${peer.peersActive} active \u2014 path conflicts:`);
971
+ for (const c of peer.conflicts) {
972
+ L.push(`- \`${c.path}\`${c.focus ? ` \u2014 peer focus: ${c.focus}` : ""}`);
973
+ }
974
+ if (peer.recommendation) L.push(`- _${peer.recommendation}_`);
975
+ } else if (peer.peersActive > 0) {
976
+ L.push(`**Parallel sessions on this repo:** ${peer.peersActive} active \u2014 no conflict on your changed paths.`);
977
+ } else {
978
+ L.push("**Parallel sessions on this repo:** none.");
979
+ }
980
+ } else {
981
+ L.push("**Parallel sessions:** _not checked (no key / no changed paths / offline)._");
982
+ }
983
+ L.push("");
984
+ if (lessonsAvailable && lessons.length > 0) {
985
+ L.push("## Relevant git lessons (maind)");
986
+ for (const l of lessons) {
987
+ L.push(`- **${l.title ?? l.id}**${l.id ? ` \`${l.id}\`` : ""}`);
988
+ }
989
+ L.push("");
990
+ }
991
+ L.push('_Full PR workflow: `activate_skill({id:"skill_pr_lifecycle"})`._');
992
+ L.push("");
993
+ return L.join("\n");
994
+ }
995
+ function buildManifest2(git3, prs, peer) {
996
+ return {
997
+ schema: "maind.git-context/v1",
998
+ generated_at: nowIso2(),
999
+ project_key: git3.projectKey,
1000
+ branch: git3.branch,
1001
+ default_branch: git3.defaultBranch,
1002
+ ahead: git3.ahead,
1003
+ behind: git3.behind,
1004
+ uncommitted: git3.uncommitted,
1005
+ open_prs: prs.map((p) => ({ number: p.number, ci: p.ci, current_branch: p.isCurrentBranch })),
1006
+ peers_active: peer?.checked ? peer.peersActive : null,
1007
+ path_conflicts: peer?.conflicts.map((c) => c.path) ?? []
1008
+ };
1009
+ }
1010
+ function msg4(err2) {
1011
+ return err2 instanceof Error ? err2.message : String(err2);
1012
+ }
1013
+ function warn4(line) {
1014
+ process.stderr.write(`maind git-context: ${line}
1015
+ `);
1016
+ }
1017
+
1018
+ // src/index-cmd.ts
1019
+ import { execFileSync as execFileSync4, spawnSync } from "node:child_process";
1020
+ import { existsSync as existsSync2, mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
1021
+ import { tmpdir } from "node:os";
1022
+ import { dirname as dirname3, join as join4 } from "node:path";
1023
+ import { fileURLToPath } from "node:url";
1024
+
1025
+ // ../../packages/code-index/build/node.js
1026
+ import { mkdirSync as mkdirSync3 } from "node:fs";
1027
+ import { posix as posix4 } from "node:path";
1028
+
1029
+ // ../../packages/code-index/build/parse/tree-sitter/parser.js
1030
+ import { join as join3 } from "node:path";
1031
+ import Parser from "web-tree-sitter";
1032
+
1033
+ // ../../packages/code-index/build/parse/keys.js
1034
+ function moduleKey(filePath) {
1035
+ return `${filePath}#<module>`;
1036
+ }
1037
+ function symbolKey(filePath, qualifiedName, kind) {
1038
+ return `${filePath}#${qualifiedName}:${kind}`;
1039
+ }
1040
+
1041
+ // ../../packages/code-index/build/parse/types.js
1042
+ function languageForPath(filePath) {
1043
+ const lower = filePath.toLowerCase();
1044
+ if (lower.endsWith(".tsx"))
1045
+ return "tsx";
1046
+ if (lower.endsWith(".ts") || lower.endsWith(".mts") || lower.endsWith(".cts"))
1047
+ return "typescript";
1048
+ if (lower.endsWith(".js") || lower.endsWith(".jsx") || lower.endsWith(".mjs") || lower.endsWith(".cjs"))
1049
+ return "javascript";
1050
+ if (lower.endsWith(".py") || lower.endsWith(".pyi"))
1051
+ return "python";
1052
+ return null;
1053
+ }
1054
+ function isSkippablePath(filePath) {
1055
+ return filePath.toLowerCase().endsWith(".d.ts");
1056
+ }
1057
+
1058
+ // ../../packages/code-index/build/parse/lang/shared.js
1059
+ function newResult() {
1060
+ return { nodes: [], edges: [], refs: [], imports: [] };
1061
+ }
1062
+ function line1(row) {
1063
+ return row + 1;
1064
+ }
1065
+ function signatureOf(node) {
1066
+ const firstLine = node.text.split("\n", 1)[0] ?? "";
1067
+ return firstLine.trim().slice(0, 200);
1068
+ }
1069
+ function calleeName(callee) {
1070
+ if (!callee)
1071
+ return null;
1072
+ switch (callee.type) {
1073
+ case "identifier":
1074
+ case "property_identifier":
1075
+ case "type_identifier":
1076
+ return callee.text;
1077
+ case "member_expression":
1078
+ return callee.childForFieldName("property")?.text ?? null;
1079
+ case "attribute":
1080
+ return callee.childForFieldName("attribute")?.text ?? null;
1081
+ default:
1082
+ return null;
1083
+ }
1084
+ }
1085
+ function collectCallTargets(body, callType) {
1086
+ const names = /* @__PURE__ */ new Set();
1087
+ for (const call of body.descendantsOfType(callType)) {
1088
+ const name = calleeName(call.childForFieldName("function"));
1089
+ if (name)
1090
+ names.add(name);
1091
+ }
1092
+ return [...names];
1093
+ }
1094
+ function addCallRefs(res, fromKey, body, callType) {
1095
+ for (const target of collectCallTargets(body, callType)) {
1096
+ res.refs.push({ fromKey, kind: "calls", targetName: target });
1097
+ }
1098
+ }
1099
+
1100
+ // ../../packages/code-index/build/parse/lang/python.js
1101
+ var CALL_TYPE = "call";
1102
+ function extractPython(root, ctx) {
1103
+ const res = newResult();
1104
+ for (const stmt of root.namedChildren) {
1105
+ handleTopLevel(stmt, ctx, res);
1106
+ }
1107
+ return res;
1108
+ }
1109
+ function unwrapDecorated(node) {
1110
+ if (node.type === "decorated_definition") {
1111
+ return node.childForFieldName("definition") ?? node;
1112
+ }
1113
+ return node;
1114
+ }
1115
+ function handleTopLevel(rawStmt, ctx, res) {
1116
+ const stmt = unwrapDecorated(rawStmt);
1117
+ switch (stmt.type) {
1118
+ case "import_statement":
1119
+ case "import_from_statement": {
1120
+ const mod = stmt.childForFieldName("module_name") ?? stmt.childForFieldName("name");
1121
+ const spec = mod?.text ?? firstDottedName(stmt);
1122
+ if (spec)
1123
+ res.imports.push({ fromModuleKey: ctx.moduleKey, specifier: spec });
1124
+ return;
1125
+ }
1126
+ case "function_definition": {
1127
+ const name = stmt.childForFieldName("name")?.text;
1128
+ if (!name)
1129
+ return;
1130
+ const node = makeNode(stmt, ctx, name, name, "function");
1131
+ res.nodes.push(node);
1132
+ res.edges.push({ srcKey: ctx.moduleKey, dstKey: node.nodeKey, kind: "contains" });
1133
+ const body = stmt.childForFieldName("body");
1134
+ if (body)
1135
+ addCallRefs(res, node.nodeKey, body, CALL_TYPE);
1136
+ return;
1137
+ }
1138
+ case "class_definition":
1139
+ addClass(stmt, ctx, res);
1140
+ return;
1141
+ case "expression_statement": {
1142
+ const assign = stmt.namedChildren.find((c) => c.type === "assignment");
1143
+ const left = assign?.childForFieldName("left");
1144
+ if (left && left.type === "identifier") {
1145
+ const node = makeNode(stmt, ctx, left.text, left.text, "variable");
1146
+ res.nodes.push(node);
1147
+ res.edges.push({ srcKey: ctx.moduleKey, dstKey: node.nodeKey, kind: "contains" });
1148
+ }
1149
+ return;
1150
+ }
1151
+ default:
1152
+ return;
1153
+ }
1154
+ }
1155
+ function addClass(stmt, ctx, res) {
1156
+ const name = stmt.childForFieldName("name")?.text;
1157
+ if (!name)
1158
+ return;
1159
+ const classNode = makeNode(stmt, ctx, name, name, "class");
1160
+ res.nodes.push(classNode);
1161
+ res.edges.push({ srcKey: ctx.moduleKey, dstKey: classNode.nodeKey, kind: "contains" });
1162
+ const supers = stmt.childForFieldName("superclasses");
1163
+ if (supers) {
1164
+ for (const arg of supers.namedChildren) {
1165
+ if (arg.type === "identifier") {
1166
+ res.refs.push({ fromKey: classNode.nodeKey, kind: "extends", targetName: arg.text });
1167
+ } else if (arg.type === "attribute") {
1168
+ const attr = arg.childForFieldName("attribute")?.text;
1169
+ if (attr)
1170
+ res.refs.push({ fromKey: classNode.nodeKey, kind: "extends", targetName: attr });
1171
+ }
1172
+ }
1173
+ }
1174
+ const body = stmt.childForFieldName("body");
1175
+ if (!body)
1176
+ return;
1177
+ for (const member of body.namedChildren) {
1178
+ const def = unwrapDecorated(member);
1179
+ if (def.type !== "function_definition")
1180
+ continue;
1181
+ const mName = def.childForFieldName("name")?.text;
1182
+ if (!mName)
1183
+ continue;
1184
+ const qn = `${name}.${mName}`;
1185
+ const methodNode = makeNode(def, ctx, mName, qn, "method");
1186
+ res.nodes.push(methodNode);
1187
+ res.edges.push({ srcKey: classNode.nodeKey, dstKey: methodNode.nodeKey, kind: "contains" });
1188
+ const mBody = def.childForFieldName("body");
1189
+ if (mBody)
1190
+ addCallRefs(res, methodNode.nodeKey, mBody, CALL_TYPE);
1191
+ }
1192
+ }
1193
+ function makeNode(node, ctx, name, qualifiedName, kind) {
1194
+ return {
1195
+ nodeKey: symbolKey(ctx.filePath, qualifiedName, kind),
1196
+ kind,
1197
+ name,
1198
+ qualifiedName,
1199
+ filePath: ctx.filePath,
1200
+ language: ctx.language,
1201
+ startLine: line1(node.startPosition.row),
1202
+ endLine: line1(node.endPosition.row),
1203
+ startByte: node.startIndex,
1204
+ endByte: node.endIndex,
1205
+ signature: signatureOf(node),
1206
+ doc: null
1207
+ };
1208
+ }
1209
+ function firstDottedName(stmt) {
1210
+ return stmt.descendantsOfType("dotted_name")[0]?.text ?? null;
1211
+ }
1212
+
1213
+ // ../../packages/code-index/build/parse/lang/typescript.js
1214
+ var CALL_TYPE2 = "call_expression";
1215
+ function extractTypescript(root, ctx) {
1216
+ const res = newResult();
1217
+ for (const stmt of root.namedChildren) {
1218
+ handleTopLevel2(stmt, ctx, res);
1219
+ }
1220
+ return res;
1221
+ }
1222
+ function unwrapExport(node) {
1223
+ if (node.type === "export_statement") {
1224
+ return node.childForFieldName("declaration") ?? node;
1225
+ }
1226
+ return node;
1227
+ }
1228
+ function handleTopLevel2(rawStmt, ctx, res) {
1229
+ const stmt = unwrapExport(rawStmt);
1230
+ switch (stmt.type) {
1231
+ case "import_statement": {
1232
+ const spec = stmt.childForFieldName("source")?.text;
1233
+ if (spec)
1234
+ res.imports.push({ fromModuleKey: ctx.moduleKey, specifier: stripQuotes(spec) });
1235
+ return;
1236
+ }
1237
+ case "function_declaration":
1238
+ case "generator_function_declaration": {
1239
+ addFunctionLike(stmt, ctx, res, "function");
1240
+ return;
1241
+ }
1242
+ case "class_declaration":
1243
+ case "abstract_class_declaration": {
1244
+ addClass2(stmt, ctx, res);
1245
+ return;
1246
+ }
1247
+ case "interface_declaration":
1248
+ addSimpleNamed(stmt, ctx, res, "interface");
1249
+ return;
1250
+ case "type_alias_declaration":
1251
+ addSimpleNamed(stmt, ctx, res, "type_alias");
1252
+ return;
1253
+ case "enum_declaration":
1254
+ addSimpleNamed(stmt, ctx, res, "enum");
1255
+ return;
1256
+ case "lexical_declaration":
1257
+ case "variable_declaration": {
1258
+ for (const decl of stmt.namedChildren) {
1259
+ if (decl.type !== "variable_declarator")
1260
+ continue;
1261
+ const nameNode = decl.childForFieldName("name");
1262
+ if (!nameNode || nameNode.type !== "identifier")
1263
+ continue;
1264
+ const value2 = decl.childForFieldName("value");
1265
+ const isFn = value2?.type === "arrow_function" || value2?.type === "function_expression" || value2?.type === "function";
1266
+ addFunctionLike(decl, ctx, res, isFn ? "function" : "variable", nameNode, value2);
1267
+ }
1268
+ return;
1269
+ }
1270
+ default:
1271
+ return;
1272
+ }
1273
+ }
1274
+ function addFunctionLike(declNode, ctx, res, kind, nameOverride, bodyOverride) {
1275
+ const nameNode = nameOverride ?? declNode.childForFieldName("name");
1276
+ const name = nameNode?.text;
1277
+ if (!name)
1278
+ return;
1279
+ const node = makeNode2(declNode, ctx, name, name, kind);
1280
+ res.nodes.push(node);
1281
+ res.edges.push({ srcKey: ctx.moduleKey, dstKey: node.nodeKey, kind: "contains" });
1282
+ const body = bodyOverride ?? declNode.childForFieldName("body") ?? declNode;
1283
+ if (kind === "function" && body)
1284
+ addCallRefs(res, node.nodeKey, body, CALL_TYPE2);
1285
+ }
1286
+ function addSimpleNamed(declNode, ctx, res, kind) {
1287
+ const name = declNode.childForFieldName("name")?.text;
1288
+ if (!name)
1289
+ return;
1290
+ const node = makeNode2(declNode, ctx, name, name, kind);
1291
+ res.nodes.push(node);
1292
+ res.edges.push({ srcKey: ctx.moduleKey, dstKey: node.nodeKey, kind: "contains" });
1293
+ }
1294
+ function addClass2(declNode, ctx, res) {
1295
+ const name = declNode.childForFieldName("name")?.text;
1296
+ if (!name)
1297
+ return;
1298
+ const classNode = makeNode2(declNode, ctx, name, name, "class");
1299
+ res.nodes.push(classNode);
1300
+ res.edges.push({ srcKey: ctx.moduleKey, dstKey: classNode.nodeKey, kind: "contains" });
1301
+ for (const heritage of declNode.descendantsOfType("class_heritage")) {
1302
+ for (const clause of heritage.namedChildren) {
1303
+ const kind = clause.type === "implements_clause" ? "implements" : "extends";
1304
+ for (const id of clause.descendantsOfType("type_identifier")) {
1305
+ res.refs.push({ fromKey: classNode.nodeKey, kind, targetName: id.text });
1306
+ }
1307
+ for (const id of clause.descendantsOfType("identifier")) {
1308
+ res.refs.push({ fromKey: classNode.nodeKey, kind, targetName: id.text });
1309
+ }
1310
+ }
1311
+ }
1312
+ const body = declNode.childForFieldName("body");
1313
+ if (!body)
1314
+ return;
1315
+ for (const member of body.namedChildren) {
1316
+ if (member.type !== "method_definition")
1317
+ continue;
1318
+ const mName = member.childForFieldName("name")?.text;
1319
+ if (!mName)
1320
+ continue;
1321
+ const qn = `${name}.${mName}`;
1322
+ const methodNode = makeNode2(member, ctx, mName, qn, "method");
1323
+ res.nodes.push(methodNode);
1324
+ res.edges.push({ srcKey: classNode.nodeKey, dstKey: methodNode.nodeKey, kind: "contains" });
1325
+ const mBody = member.childForFieldName("body");
1326
+ if (mBody)
1327
+ addCallRefs(res, methodNode.nodeKey, mBody, CALL_TYPE2);
1328
+ }
1329
+ }
1330
+ function makeNode2(node, ctx, name, qualifiedName, kind) {
1331
+ return {
1332
+ nodeKey: symbolKey(ctx.filePath, qualifiedName, kind),
1333
+ kind,
1334
+ name,
1335
+ qualifiedName,
1336
+ filePath: ctx.filePath,
1337
+ language: ctx.language,
1338
+ startLine: line1(node.startPosition.row),
1339
+ endLine: line1(node.endPosition.row),
1340
+ startByte: node.startIndex,
1341
+ endByte: node.endIndex,
1342
+ signature: signatureOf(node),
1343
+ doc: null
1344
+ };
1345
+ }
1346
+ function stripQuotes(s) {
1347
+ return s.replace(/^["'`]|["'`]$/g, "");
1348
+ }
1349
+
1350
+ // ../../packages/code-index/build/parse/tree-sitter/grammars.js
1351
+ import { createRequire } from "node:module";
1352
+ import { join as join2 } from "node:path";
1353
+ var GRAMMAR_FILE = {
1354
+ typescript: "tree-sitter-typescript.wasm",
1355
+ tsx: "tree-sitter-tsx.wasm",
1356
+ javascript: "tree-sitter-javascript.wasm",
1357
+ python: "tree-sitter-python.wasm"
1358
+ };
1359
+ var CORE_WASM_FILE = "tree-sitter.wasm";
1360
+ function grammarPath(language, assetDir) {
1361
+ if (assetDir)
1362
+ return join2(assetDir, GRAMMAR_FILE[language]);
1363
+ return createRequire(import.meta.url).resolve(`tree-sitter-wasms/out/${GRAMMAR_FILE[language]}`);
1364
+ }
1365
+
1366
+ // ../../packages/code-index/build/parse/tree-sitter/parser.js
1367
+ var SUPPORTED = ["typescript", "tsx", "javascript", "python"];
1368
+ var TreeSitterParser = class {
1369
+ initialized = false;
1370
+ parser = null;
1371
+ grammars = /* @__PURE__ */ new Map();
1372
+ assetDir;
1373
+ constructor(opts = {}) {
1374
+ this.assetDir = opts.assetDir;
1375
+ }
1376
+ async init() {
1377
+ if (this.initialized)
1378
+ return;
1379
+ await Parser.init(this.assetDir ? { locateFile: (file) => join3(this.assetDir, file || CORE_WASM_FILE) } : void 0);
1380
+ this.parser = new Parser();
1381
+ this.initialized = true;
1382
+ }
1383
+ supports(language) {
1384
+ return SUPPORTED.includes(language);
1385
+ }
1386
+ async parse(filePath, source, language) {
1387
+ await this.init();
1388
+ const parser = this.parser;
1389
+ parser.setLanguage(await this.grammar(language));
1390
+ const tree = parser.parse(source);
1391
+ if (!tree) {
1392
+ return { filePath, language, nodes: [], edges: [], refs: [], imports: [] };
1393
+ }
1394
+ const root = tree.rootNode;
1395
+ const mKey = moduleKey(filePath);
1396
+ const moduleNode = {
1397
+ nodeKey: mKey,
1398
+ kind: "module",
1399
+ name: filePath.split("/").pop() ?? filePath,
1400
+ qualifiedName: "<module>",
1401
+ filePath,
1402
+ language,
1403
+ startLine: 1,
1404
+ endLine: line1(root.endPosition.row),
1405
+ startByte: 0,
1406
+ // UTF-16 code-unit length (matches tree-sitter's startIndex/endIndex units).
1407
+ endByte: source.length,
1408
+ signature: null,
1409
+ doc: null
1410
+ };
1411
+ const ctx = { filePath, language, source, moduleKey: mKey };
1412
+ const extracted = language === "python" ? extractPython(root, ctx) : extractTypescript(root, ctx);
1413
+ tree.delete();
1414
+ return {
1415
+ filePath,
1416
+ language,
1417
+ nodes: [moduleNode, ...extracted.nodes],
1418
+ edges: extracted.edges,
1419
+ refs: extracted.refs,
1420
+ imports: extracted.imports
1421
+ };
1422
+ }
1423
+ async grammar(language) {
1424
+ let g = this.grammars.get(language);
1425
+ if (!g) {
1426
+ g = await Parser.Language.load(grammarPath(language, this.assetDir));
1427
+ this.grammars.set(language, g);
1428
+ }
1429
+ return g;
1430
+ }
1431
+ };
1432
+
1433
+ // ../../packages/code-index/build/query/engine.js
1434
+ import { readFileSync as readFileSync4 } from "node:fs";
1435
+ import { posix as posix3 } from "node:path";
1436
+
1437
+ // ../../packages/code-index/build/embed/recipe.js
1438
+ var EMBED_RECIPE_VERSION = "1";
1439
+ var MAX_CHARS = 1e3;
1440
+ function nodeEmbedText(n) {
1441
+ return [`${n.kind} ${n.qualifiedName}`, n.signature ?? "", n.doc ?? "", n.filePath].filter((s) => s.length > 0).join("\n").slice(0, MAX_CHARS);
1442
+ }
1443
+
1444
+ // ../../packages/code-index/build/embed/types.js
1445
+ var EMBEDDABLE_KINDS = [
1446
+ "function",
1447
+ "method",
1448
+ "class",
1449
+ "interface",
1450
+ "enum",
1451
+ "type_alias"
1452
+ ];
1453
+
1454
+ // ../../packages/code-index/build/util/vector.js
1455
+ function cosineSimilarity(a, b) {
1456
+ const n = Math.min(a.length, b.length);
1457
+ let dot = 0;
1458
+ let na = 0;
1459
+ let nb = 0;
1460
+ for (let i = 0; i < n; i++) {
1461
+ const x = a[i];
1462
+ const y = b[i];
1463
+ dot += x * y;
1464
+ na += x * x;
1465
+ nb += y * y;
1466
+ }
1467
+ if (na === 0 || nb === 0)
1468
+ return 0;
1469
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
1470
+ }
1471
+
1472
+ // ../../packages/code-index/build/graph/resolve.js
1473
+ import { posix } from "node:path";
1474
+ var RESOLVABLE_KINDS = /* @__PURE__ */ new Set([
1475
+ "function",
1476
+ "method",
1477
+ "class",
1478
+ "interface",
1479
+ "enum",
1480
+ "type_alias"
1481
+ ]);
1482
+ var SOURCE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".py"];
1483
+ function buildResolutionIndex(entries) {
1484
+ const nameToKeys = /* @__PURE__ */ new Map();
1485
+ const files = /* @__PURE__ */ new Set();
1486
+ for (const e of entries) {
1487
+ files.add(e.filePath);
1488
+ if (!RESOLVABLE_KINDS.has(e.kind))
1489
+ continue;
1490
+ const list = nameToKeys.get(e.name);
1491
+ if (list)
1492
+ list.push(e.nodeKey);
1493
+ else
1494
+ nameToKeys.set(e.name, [e.nodeKey]);
1495
+ }
1496
+ return { nameToKeys, files };
1497
+ }
1498
+ function resolveFileEdges(parsed, index) {
1499
+ const seen = /* @__PURE__ */ new Set();
1500
+ const edges = [];
1501
+ const srcKeys = /* @__PURE__ */ new Set();
1502
+ const push2 = (srcKey, dstKey, kind) => {
1503
+ if (srcKey === dstKey)
1504
+ return;
1505
+ const dedup = `${srcKey}\0${dstKey}\0${kind}`;
1506
+ if (seen.has(dedup))
1507
+ return;
1508
+ seen.add(dedup);
1509
+ edges.push({ srcKey, dstKey, kind });
1510
+ srcKeys.add(srcKey);
1511
+ };
1512
+ for (const e of parsed.edges)
1513
+ push2(e.srcKey, e.dstKey, e.kind);
1514
+ for (const n of parsed.nodes)
1515
+ srcKeys.add(n.nodeKey);
1516
+ for (const ref of parsed.refs) {
1517
+ const candidates = index.nameToKeys.get(ref.targetName);
1518
+ if (!candidates || candidates.length !== 1)
1519
+ continue;
1520
+ push2(ref.fromKey, candidates[0], ref.kind);
1521
+ }
1522
+ for (const imp of parsed.imports) {
1523
+ const target = resolveImportTarget(parsed.filePath, imp.specifier, index.files);
1524
+ if (target)
1525
+ push2(imp.fromModuleKey, moduleKey(target), "imports");
1526
+ }
1527
+ return { filePath: parsed.filePath, srcKeys: [...srcKeys], edges };
1528
+ }
1529
+ function resolveImportTarget(fromFile, specifier, files) {
1530
+ if (!specifier.startsWith("."))
1531
+ return null;
1532
+ const base = posix.normalize(posix.join(posix.dirname(fromFile), specifier));
1533
+ const candidates = [];
1534
+ const hasSourceExt = SOURCE_EXTS.some((e) => base.endsWith(e));
1535
+ if (hasSourceExt)
1536
+ candidates.push(base);
1537
+ const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
1538
+ if (jsLike) {
1539
+ const stem = base.slice(0, -jsLike[0].length);
1540
+ for (const e of SOURCE_EXTS)
1541
+ candidates.push(stem + e);
1542
+ }
1543
+ if (!hasSourceExt) {
1544
+ for (const e of SOURCE_EXTS)
1545
+ candidates.push(base + e);
1546
+ for (const e of SOURCE_EXTS)
1547
+ candidates.push(`${base}/index${e}`);
1548
+ }
1549
+ for (const c of candidates) {
1550
+ if (files.has(c))
1551
+ return c;
1552
+ }
1553
+ return null;
1554
+ }
1555
+
1556
+ // ../../packages/code-index/build/incremental/discover.js
1557
+ import { execFileSync as execFileSync3 } from "node:child_process";
1558
+ import { createHash as createHash3 } from "node:crypto";
1559
+ import { readdirSync, statSync } from "node:fs";
1560
+ import { posix as posix2, sep } from "node:path";
1561
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
1562
+ ".git",
1563
+ "node_modules",
1564
+ "build",
1565
+ "dist",
1566
+ ".next",
1567
+ "out",
1568
+ "coverage",
1569
+ ".turbo",
1570
+ ".cache",
1571
+ "__pycache__",
1572
+ ".venv",
1573
+ "venv"
1574
+ ]);
1575
+ var GIT_CONFIG_PIN = ["-c", "diff.renames=true", "-c", "core.quotePath=false"];
1576
+ function runGit(repoRoot2, args) {
1577
+ try {
1578
+ return execFileSync3("git", [...GIT_CONFIG_PIN, ...args], {
1579
+ cwd: repoRoot2,
1580
+ encoding: "utf8",
1581
+ stdio: ["ignore", "pipe", "ignore"],
1582
+ timeout: 15e3,
1583
+ maxBuffer: 64 * 1024 * 1024
1584
+ });
1585
+ } catch {
1586
+ return null;
1587
+ }
1588
+ }
1589
+ var git2 = runGit;
1590
+ function toPosix(p) {
1591
+ return sep === "/" ? p : p.split(sep).join("/");
1592
+ }
1593
+ function classify(paths) {
1594
+ const out = [];
1595
+ for (const raw of paths) {
1596
+ const path = toPosix(raw).trim();
1597
+ if (!path || isSkippablePath(path))
1598
+ continue;
1599
+ const language = languageForPath(path);
1600
+ if (language)
1601
+ out.push({ path, language });
1602
+ }
1603
+ return out;
1604
+ }
1605
+ function classifyPaths(paths) {
1606
+ return classify(paths);
1607
+ }
1608
+ function discoverSourceFiles(repoRoot2) {
1609
+ const tracked = git2(repoRoot2, ["ls-files", "--cached", "--others", "--exclude-standard"]);
1610
+ if (tracked !== null) {
1611
+ return classify(tracked.split("\n"));
1612
+ }
1613
+ const acc = [];
1614
+ walk(repoRoot2, "", acc);
1615
+ return classify(acc);
1616
+ }
1617
+ function walk(root, rel, acc) {
1618
+ let entries;
1619
+ try {
1620
+ entries = readdirSync(posix2.join(root, rel), { withFileTypes: true });
1621
+ } catch {
1622
+ return;
1623
+ }
1624
+ for (const entry of entries) {
1625
+ if (entry.name.startsWith(".") && entry.name !== ".") {
1626
+ if (IGNORE_DIRS.has(entry.name))
1627
+ continue;
1628
+ }
1629
+ if (IGNORE_DIRS.has(entry.name))
1630
+ continue;
1631
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
1632
+ if (entry.isDirectory())
1633
+ walk(root, childRel, acc);
1634
+ else if (entry.isFile())
1635
+ acc.push(childRel);
1636
+ }
1637
+ }
1638
+ function enumerateChangedPaths(repoRoot2, opts = {}) {
1639
+ if (headCommit(repoRoot2) === null)
1640
+ return null;
1641
+ const includeUncommitted = opts.includeUncommitted !== false;
1642
+ const ref = opts.since ?? "HEAD";
1643
+ const diffArgs = ["diff", "--name-status", "--diff-filter=d", ref];
1644
+ if (!includeUncommitted) {
1645
+ if (!opts.since) {
1646
+ throw new Error("nothing to enumerate: include_uncommitted=false requires a `since` ref");
1647
+ }
1648
+ diffArgs.push("HEAD");
1649
+ }
1650
+ const diff = git2(repoRoot2, diffArgs);
1651
+ if (diff === null) {
1652
+ throw new Error(`git diff failed \u2014 is '${ref}' a valid ref?`);
1653
+ }
1654
+ const paths = /* @__PURE__ */ new Map();
1655
+ for (const line of diff.split("\n")) {
1656
+ if (!line.trim())
1657
+ continue;
1658
+ const cols = line.split(" ");
1659
+ if (cols.length < 2)
1660
+ continue;
1661
+ const code = cols[0][0];
1662
+ const p = unquote((cols.length >= 3 ? cols[cols.length - 1] : cols[1]).trim());
1663
+ if (!p)
1664
+ continue;
1665
+ if (code === "A" || code === "C")
1666
+ paths.set(p, "added");
1667
+ else if (code === "R")
1668
+ paths.set(p, "renamed");
1669
+ else
1670
+ paths.set(p, "modified");
1671
+ }
1672
+ if (includeUncommitted) {
1673
+ const status2 = git2(repoRoot2, ["status", "--porcelain", "-uall"]) ?? "";
1674
+ for (const line of status2.split("\n")) {
1675
+ if (line.length < 4)
1676
+ continue;
1677
+ const x = line[0];
1678
+ const y = line[1];
1679
+ if (x === "D" || y === "D")
1680
+ continue;
1681
+ let p = line.slice(3);
1682
+ const arrow = p.indexOf(" -> ");
1683
+ if (arrow >= 0)
1684
+ p = p.slice(arrow + 4);
1685
+ p = unquote(p);
1686
+ if (!p || paths.has(p))
1687
+ continue;
1688
+ if (x === "?" || x === "A" || y === "A")
1689
+ paths.set(p, "added");
1690
+ else if (x === "R")
1691
+ paths.set(p, "renamed");
1692
+ else
1693
+ paths.set(p, "modified");
1694
+ }
1695
+ }
1696
+ return paths;
1697
+ }
1698
+ function unquote(p) {
1699
+ return p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
1700
+ }
1701
+ function changedSourceFiles(repoRoot2, opts = {}) {
1702
+ const paths = enumerateChangedPaths(repoRoot2, opts);
1703
+ if (paths === null)
1704
+ return null;
1705
+ return classify(paths.keys());
1706
+ }
1707
+ function statFile(repoRoot2, relPath) {
1708
+ try {
1709
+ const s = statSync(posix2.join(repoRoot2, relPath));
1710
+ return { mtimeMs: Math.floor(s.mtimeMs), size: s.size };
1711
+ } catch {
1712
+ return null;
1713
+ }
1714
+ }
1715
+ function hashContent(content) {
1716
+ return createHash3("sha256").update(content).digest("hex");
1717
+ }
1718
+ function headCommit(repoRoot2) {
1719
+ return git2(repoRoot2, ["rev-parse", "HEAD"])?.trim() ?? null;
1720
+ }
1721
+ function resolveCommitSha(repoRoot2, ref) {
1722
+ return git2(repoRoot2, ["rev-parse", "--verify", `${ref}^{commit}`])?.trim() ?? null;
1723
+ }
1724
+ function isWorktreeDirty(repoRoot2) {
1725
+ const status2 = git2(repoRoot2, ["status", "--porcelain", "-uall"]);
1726
+ return status2 !== null && status2.trim().length > 0;
1727
+ }
1728
+
1729
+ // ../../packages/code-index/build/incremental/hunks.js
1730
+ var MAX_DIFF_BYTES = 16 * 1024 * 1024;
1731
+ var MAX_HUNKS_PER_FILE = 500;
1732
+ var MAX_ADDED_LINES_PER_FILE = 4e3;
1733
+ var MAX_UNTRACKED_HEAD_BYTES = 256 * 1024;
1734
+ var HUNK_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
1735
+ function changedHunks(repoRoot2, opts = {}) {
1736
+ if (runGit(repoRoot2, ["rev-parse", "HEAD"]) === null)
1737
+ return null;
1738
+ const includeUncommitted = opts.includeUncommitted !== false;
1739
+ const ref = opts.since ?? "HEAD";
1740
+ const diffArgs = [
1741
+ "diff",
1742
+ "--unified=0",
1743
+ "--no-color",
1744
+ "--no-ext-diff",
1745
+ "--diff-filter=d",
1746
+ ref
1747
+ ];
1748
+ if (!includeUncommitted) {
1749
+ if (!opts.since) {
1750
+ throw new Error("nothing to enumerate: include_uncommitted=false requires a `since` ref");
1751
+ }
1752
+ diffArgs.push("HEAD");
1753
+ }
1754
+ const diff = runGit(repoRoot2, diffArgs);
1755
+ if (diff === null) {
1756
+ throw new Error(`git diff failed \u2014 is '${ref}' a valid ref?`);
1757
+ }
1758
+ const files = /* @__PURE__ */ new Map();
1759
+ let coverage = "full";
1760
+ if (Buffer.byteLength(diff, "utf8") > MAX_DIFF_BYTES) {
1761
+ coverage = "none";
1762
+ } else {
1763
+ parseUnifiedDiff(diff, files, () => {
1764
+ coverage = "partial";
1765
+ });
1766
+ }
1767
+ if (includeUncommitted) {
1768
+ const status2 = runGit(repoRoot2, ["status", "--porcelain", "-uall"]) ?? "";
1769
+ for (const line of status2.split("\n")) {
1770
+ if (line.length < 4 || line[0] !== "?")
1771
+ continue;
1772
+ let p = line.slice(3);
1773
+ if (p.startsWith('"') && p.endsWith('"'))
1774
+ p = p.slice(1, -1);
1775
+ if (!p || files.has(p))
1776
+ continue;
1777
+ files.set(p, {
1778
+ path: p,
1779
+ status: "added",
1780
+ binary: false,
1781
+ wholeFile: true,
1782
+ degraded: false,
1783
+ addedRanges: [],
1784
+ deletionPoints: [],
1785
+ addedLines: []
1786
+ });
1787
+ }
1788
+ }
1789
+ return { files, coverage };
1790
+ }
1791
+ function parseUnifiedDiff(diff, files, onDegrade) {
1792
+ let current = null;
1793
+ let hunkCount = 0;
1794
+ let pendingRename = false;
1795
+ let pendingNewFile = false;
1796
+ const commit = () => {
1797
+ if (current)
1798
+ files.set(current.path, current);
1799
+ current = null;
1800
+ hunkCount = 0;
1801
+ };
1802
+ for (const line of diff.split("\n")) {
1803
+ if (line.startsWith("diff --git ")) {
1804
+ commit();
1805
+ pendingRename = false;
1806
+ pendingNewFile = false;
1807
+ continue;
1808
+ }
1809
+ if (line.startsWith("new file mode")) {
1810
+ pendingNewFile = true;
1811
+ continue;
1812
+ }
1813
+ if (line.startsWith("rename to ")) {
1814
+ pendingRename = true;
1815
+ continue;
1816
+ }
1817
+ if (line.startsWith("Binary files ") || line.startsWith("GIT binary patch")) {
1818
+ if (current)
1819
+ current.binary = true;
1820
+ continue;
1821
+ }
1822
+ if (line.startsWith("+++ ")) {
1823
+ let p = line.slice(4).trim();
1824
+ if (p.startsWith('"') && p.endsWith('"'))
1825
+ p = p.slice(1, -1);
1826
+ if (p.startsWith("b/"))
1827
+ p = p.slice(2);
1828
+ if (p === "/dev/null")
1829
+ continue;
1830
+ current = {
1831
+ path: p,
1832
+ status: pendingNewFile ? "added" : pendingRename ? "renamed" : "modified",
1833
+ binary: false,
1834
+ wholeFile: false,
1835
+ degraded: false,
1836
+ addedRanges: [],
1837
+ deletionPoints: [],
1838
+ addedLines: []
1839
+ };
1840
+ continue;
1841
+ }
1842
+ if (!current)
1843
+ continue;
1844
+ const m = HUNK_RE.exec(line);
1845
+ if (m) {
1846
+ hunkCount++;
1847
+ if (hunkCount > MAX_HUNKS_PER_FILE) {
1848
+ if (!current.degraded) {
1849
+ current.degraded = true;
1850
+ current.addedRanges = [];
1851
+ current.deletionPoints = [];
1852
+ current.addedLines = [];
1853
+ onDegrade();
1854
+ }
1855
+ continue;
1856
+ }
1857
+ const start = Number(m[1]);
1858
+ const count = m[2] === void 0 ? 1 : Number(m[2]);
1859
+ if (count > 0)
1860
+ current.addedRanges.push({ start, end: start + count - 1 });
1861
+ else
1862
+ current.deletionPoints.push(start);
1863
+ continue;
1864
+ }
1865
+ if (line.startsWith("+") && !line.startsWith("+++") && !current.degraded && current.addedLines.length < MAX_ADDED_LINES_PER_FILE) {
1866
+ current.addedLines.push(line.slice(1));
1867
+ }
1868
+ }
1869
+ commit();
1870
+ }
1871
+
1872
+ // ../../packages/code-index/build/review/risk-rules.js
1873
+ var RISK_RULES = [
1874
+ {
1875
+ flag: "sql_migration_added",
1876
+ path: /(^|\/)migrations\/[^/]+\.sql$/i,
1877
+ onlyWhenAdded: true
1878
+ },
1879
+ {
1880
+ flag: "security_definer_touched",
1881
+ path: /\.sql$/i,
1882
+ content: /\bSECURITY\s+DEFINER\b/i
1883
+ },
1884
+ {
1885
+ flag: "rls_policy_changed",
1886
+ path: /\.sql$/i,
1887
+ content: /\b(CREATE|ALTER|DROP)\s+POLICY\b|\bROW\s+LEVEL\s+SECURITY\b/i
1888
+ },
1889
+ {
1890
+ flag: "auth_config_changed",
1891
+ path: /(^|\/)supabase\/config\.toml$|(^|\/)(middleware|proxy)\.(ts|js)$/
1892
+ },
1893
+ {
1894
+ flag: "package_manifest_changed",
1895
+ path: /(^|\/)(package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|requirements\.txt|pyproject\.toml|Cargo\.toml|go\.mod)$/
1896
+ },
1897
+ {
1898
+ flag: "ci_workflow_changed",
1899
+ path: /^\.github\/workflows\/[^/]+\.ya?ml$|^\.gitlab-ci\.yml$/
1900
+ },
1901
+ {
1902
+ flag: "env_or_secrets_file",
1903
+ path: /(^|\/)\.env(\.[^/]+)?$|\.(pem|key)$/
1904
+ }
1905
+ ];
1906
+
1907
+ // ../../packages/code-index/build/review/risk-scan.js
1908
+ function scanRiskFlags(input) {
1909
+ const flags = /* @__PURE__ */ new Set();
1910
+ for (const [path, status2] of input.paths) {
1911
+ for (const rule of RISK_RULES) {
1912
+ if (flags.has(rule.flag))
1913
+ continue;
1914
+ if (!rule.path.test(path))
1915
+ continue;
1916
+ if (rule.onlyWhenAdded && status2 !== "added")
1917
+ continue;
1918
+ if (!rule.content) {
1919
+ flags.add(rule.flag);
1920
+ continue;
1921
+ }
1922
+ const text = contentForPath(path, input);
1923
+ if (text !== null && rule.content.test(text))
1924
+ flags.add(rule.flag);
1925
+ }
1926
+ }
1927
+ return [...flags].sort();
1928
+ }
1929
+ function contentForPath(path, input) {
1930
+ const hunks = input.hunks.get(path);
1931
+ if (hunks && !hunks.wholeFile && !hunks.degraded) {
1932
+ return hunks.addedLines.join("\n");
1933
+ }
1934
+ return input.readUntracked(path);
1935
+ }
1936
+
1937
+ // ../../packages/code-index/build/store/schema.js
1938
+ var SCHEMA_VERSION = "1";
1939
+ var SCHEMA_SQL = (
1940
+ /* sql */
1941
+ `
1942
+ CREATE TABLE IF NOT EXISTS meta (
1943
+ key TEXT PRIMARY KEY,
1944
+ value TEXT NOT NULL
1945
+ );
1946
+
1947
+ CREATE TABLE IF NOT EXISTS files (
1948
+ path TEXT PRIMARY KEY,
1949
+ language TEXT NOT NULL,
1950
+ content_hash TEXT NOT NULL,
1951
+ mtime_ms INTEGER NOT NULL,
1952
+ size INTEGER NOT NULL,
1953
+ indexed_at INTEGER NOT NULL
1954
+ );
1955
+
1956
+ CREATE TABLE IF NOT EXISTS nodes (
1957
+ node_key TEXT PRIMARY KEY,
1958
+ kind TEXT NOT NULL,
1959
+ name TEXT NOT NULL,
1960
+ qualified_name TEXT NOT NULL,
1961
+ file_path TEXT NOT NULL,
1962
+ language TEXT NOT NULL,
1963
+ start_line INTEGER NOT NULL,
1964
+ end_line INTEGER NOT NULL,
1965
+ start_byte INTEGER NOT NULL,
1966
+ end_byte INTEGER NOT NULL,
1967
+ signature TEXT,
1968
+ doc TEXT
1969
+ );
1970
+ CREATE INDEX IF NOT EXISTS nodes_file_idx ON nodes(file_path);
1971
+ CREATE INDEX IF NOT EXISTS nodes_kind_idx ON nodes(kind);
1972
+ CREATE INDEX IF NOT EXISTS nodes_name_idx ON nodes(name);
1973
+
1974
+ CREATE TABLE IF NOT EXISTS edges (
1975
+ src_key TEXT NOT NULL,
1976
+ dst_key TEXT NOT NULL,
1977
+ kind TEXT NOT NULL,
1978
+ PRIMARY KEY (src_key, dst_key, kind)
1979
+ );
1980
+ CREATE INDEX IF NOT EXISTS edges_dst_idx ON edges(dst_key, kind);
1981
+ CREATE INDEX IF NOT EXISTS edges_src_idx ON edges(src_key, kind);
1982
+
1983
+ -- Standalone (self-managed) FTS5 table \u2014 we populate/delete it in the same
1984
+ -- transaction as nodes, avoiding external-content trigger bookkeeping. The
1985
+ -- search_text column carries name + camel/snake-split tokens + doc + signature
1986
+ -- so unicode61 can match identifier fragments.
1987
+ CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
1988
+ node_key UNINDEXED,
1989
+ search_text,
1990
+ tokenize = 'unicode61'
1991
+ );
1992
+
1993
+ -- Reserved for Phase 2 (semantic). Unused in Phase 1.
1994
+ CREATE TABLE IF NOT EXISTS embeddings (
1995
+ node_key TEXT PRIMARY KEY,
1996
+ dim INTEGER NOT NULL,
1997
+ vec BLOB NOT NULL,
1998
+ content_hash TEXT NOT NULL
1999
+ );
2000
+ `
2001
+ );
2002
+ var META_SCHEMA_VERSION = "schema_version";
2003
+ var META_LAST_INDEX_COMMIT = "last_index_commit";
2004
+ var META_LAST_INDEXED_AT = "last_indexed_at";
2005
+ var META_EMBED_RECIPE_VERSION = "embed_recipe_version";
2006
+ var META_EMBED_MODEL = "embed_model";
2007
+ var META_EMBED_DIM = "embed_dim";
2008
+ var META_LAST_SYNCED_AT = "last_synced_at";
2009
+ var META_LAST_SYNCED_COMMIT = "last_synced_commit";
2010
+
2011
+ // ../../packages/code-index/build/util/identifiers.js
2012
+ function splitIdentifier(id) {
2013
+ return id.replace(/[_\-./]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/\s+/).filter((t) => t.length > 0);
2014
+ }
2015
+ function buildSearchText(parts) {
2016
+ const frags = /* @__PURE__ */ new Set();
2017
+ frags.add(parts.name.toLowerCase());
2018
+ frags.add(parts.qualifiedName.toLowerCase());
2019
+ for (const f of splitIdentifier(parts.name))
2020
+ frags.add(f);
2021
+ for (const f of splitIdentifier(parts.qualifiedName))
2022
+ frags.add(f);
2023
+ const extra = [parts.doc ?? "", parts.signature ?? ""].join(" ").trim();
2024
+ const base = [...frags].join(" ");
2025
+ return extra ? `${base} ${extra}` : base;
2026
+ }
2027
+ function toFtsMatchQuery(query) {
2028
+ const frags = /* @__PURE__ */ new Set();
2029
+ for (const raw of query.split(/\s+/)) {
2030
+ const cleaned = raw.replace(/[^A-Za-z0-9]+/g, "");
2031
+ if (cleaned.length === 0)
2032
+ continue;
2033
+ frags.add(cleaned.toLowerCase());
2034
+ for (const f of splitIdentifier(cleaned))
2035
+ frags.add(f);
2036
+ }
2037
+ if (frags.size === 0)
2038
+ return null;
2039
+ return [...frags].map((f) => `${f}*`).join(" OR ");
2040
+ }
2041
+
2042
+ // ../../packages/code-index/build/query/engine.js
2043
+ var CodeIndexEngine = class {
2044
+ store;
2045
+ parser;
2046
+ repoRoot;
2047
+ embedder;
2048
+ constructor(deps) {
2049
+ this.store = deps.store;
2050
+ this.parser = deps.parser;
2051
+ this.repoRoot = deps.repoRoot;
2052
+ this.embedder = deps.embedder ?? null;
2053
+ }
2054
+ async indexRepository(opts = {}) {
2055
+ const started = opts.now ?? Date.now();
2056
+ await this.store.init();
2057
+ await this.parser.init();
2058
+ const storedVersion = await this.store.getMeta(META_SCHEMA_VERSION);
2059
+ const hadIndex = await this.store.countFiles() > 0;
2060
+ if (opts.full || hadIndex && storedVersion !== SCHEMA_VERSION) {
2061
+ await this.wipe();
2062
+ }
2063
+ const discovered = discoverSourceFiles(this.repoRoot);
2064
+ const discoveredPaths = new Set(discovered.map((d) => d.path));
2065
+ const storedFiles = await this.store.getAllFiles();
2066
+ const storedByPath = new Map(storedFiles.map((f) => [f.path, f]));
2067
+ const incremental = storedFiles.length > 0 && !opts.full;
2068
+ const stats = {
2069
+ filesScanned: discovered.length,
2070
+ filesParsed: 0,
2071
+ filesSkipped: 0,
2072
+ filesDeleted: 0,
2073
+ nodeCount: 0,
2074
+ edgeCount: 0,
2075
+ durationMs: 0,
2076
+ incremental
2077
+ };
2078
+ for (const f of storedFiles) {
2079
+ if (!discoveredPaths.has(f.path)) {
2080
+ await this.store.deleteFile(f.path);
2081
+ stats.filesDeleted++;
2082
+ }
2083
+ }
2084
+ const parsedFiles = [];
2085
+ let done = 0;
2086
+ for (const d of discovered) {
2087
+ done++;
2088
+ const st = statFile(this.repoRoot, d.path);
2089
+ if (!st) {
2090
+ stats.filesSkipped++;
2091
+ continue;
2092
+ }
2093
+ const prior = storedByPath.get(d.path);
2094
+ if (prior && prior.mtimeMs === st.mtimeMs && prior.size === st.size) {
2095
+ stats.filesSkipped++;
2096
+ continue;
2097
+ }
2098
+ const content = this.readRepoFile(d.path);
2099
+ if (content === null) {
2100
+ stats.filesSkipped++;
2101
+ continue;
2102
+ }
2103
+ const contentHash = hashContent(content);
2104
+ if (prior && prior.contentHash === contentHash) {
2105
+ await this.store.upsertFile({ ...prior, mtimeMs: st.mtimeMs, indexedAt: started });
2106
+ stats.filesSkipped++;
2107
+ continue;
2108
+ }
2109
+ const parsed = await this.parser.parse(d.path, content, d.language);
2110
+ const ftsText = /* @__PURE__ */ new Map();
2111
+ for (const n of parsed.nodes)
2112
+ ftsText.set(n.nodeKey, buildSearchText(n));
2113
+ const fileRow = {
2114
+ path: d.path,
2115
+ language: d.language,
2116
+ contentHash,
2117
+ mtimeMs: st.mtimeMs,
2118
+ size: st.size,
2119
+ indexedAt: started
2120
+ };
2121
+ await this.store.replaceFileNodes(fileRow, parsed.nodes, ftsText);
2122
+ parsedFiles.push(parsed);
2123
+ stats.filesParsed++;
2124
+ opts.onProgress?.({ path: d.path, done, total: discovered.length });
2125
+ }
2126
+ if (parsedFiles.length > 0) {
2127
+ const nameIndex = await this.store.getNameIndex();
2128
+ const resIndex = buildResolutionIndex(nameIndex);
2129
+ for (const parsed of parsedFiles) {
2130
+ const resolved = resolveFileEdges(parsed, resIndex);
2131
+ await this.store.replaceOutgoingEdges(resolved.srcKeys, resolved.edges);
2132
+ }
2133
+ }
2134
+ if (this.embedder && opts.embed) {
2135
+ const embeddable = await this.store.getNodesByKinds([...EMBEDDABLE_KINDS]);
2136
+ const existing = await this.store.getEmbeddingContentHashes();
2137
+ const pending = [];
2138
+ for (const n of embeddable) {
2139
+ const text = nodeEmbedText(n);
2140
+ const hash = hashContent(`${this.embedder.recipeVersion}\0${text}`);
2141
+ if (existing.get(n.nodeKey) !== hash)
2142
+ pending.push({ nodeKey: n.nodeKey, text, hash });
2143
+ }
2144
+ const BATCH = 64;
2145
+ for (let i = 0; i < pending.length; i += BATCH) {
2146
+ const batch = pending.slice(i, i + BATCH);
2147
+ const vecs = await this.embedder.embedDocuments(batch.map((b) => b.text));
2148
+ await this.store.upsertEmbeddings(batch.map((b, j) => ({
2149
+ nodeKey: b.nodeKey,
2150
+ dim: this.embedder.dim,
2151
+ vec: vecs[j],
2152
+ contentHash: b.hash
2153
+ })));
2154
+ const done2 = Math.min(i + BATCH, pending.length);
2155
+ opts.onProgress?.({ path: `embedding symbols`, done: done2, total: pending.length });
2156
+ }
2157
+ await this.store.pruneOrphanEmbeddings();
2158
+ await this.store.setMeta(META_EMBED_RECIPE_VERSION, this.embedder.recipeVersion);
2159
+ await this.store.setMeta(META_EMBED_MODEL, this.embedder.model);
2160
+ await this.store.setMeta(META_EMBED_DIM, String(this.embedder.dim));
2161
+ }
2162
+ await this.store.setMeta(META_SCHEMA_VERSION, SCHEMA_VERSION);
2163
+ await this.store.setMeta(META_LAST_INDEXED_AT, String(started));
2164
+ const head = headCommit(this.repoRoot);
2165
+ if (head)
2166
+ await this.store.setMeta(META_LAST_INDEX_COMMIT, head);
2167
+ stats.nodeCount = await this.store.countNodes();
2168
+ stats.edgeCount = await this.store.countEdges();
2169
+ stats.durationMs = (opts.now ?? Date.now()) - started;
2170
+ return stats;
2171
+ }
2172
+ async status() {
2173
+ await this.store.init();
2174
+ const fileCount = await this.store.countFiles();
2175
+ const langs = await this.store.languageDistribution();
2176
+ const languages = {};
2177
+ for (const l of langs)
2178
+ languages[l.language] = l.count;
2179
+ const lastAt = await this.store.getMeta(META_LAST_INDEXED_AT);
2180
+ return {
2181
+ indexed: fileCount > 0,
2182
+ fileCount,
2183
+ nodeCount: await this.store.countNodes(),
2184
+ edgeCount: await this.store.countEdges(),
2185
+ embeddingCount: await this.store.countEmbeddings(),
2186
+ lastIndexedAt: lastAt ? Number(lastAt) : null,
2187
+ lastIndexCommit: await this.store.getMeta(META_LAST_INDEX_COMMIT),
2188
+ schemaVersion: await this.store.getMeta(META_SCHEMA_VERSION),
2189
+ languages
2190
+ };
2191
+ }
2192
+ async searchGraph(query, opts = {}) {
2193
+ await this.store.init();
2194
+ const limit = clampLimit(opts.limit, 20, 100);
2195
+ const hits = await this.store.searchFts(query, opts.kind ?? null, limit);
2196
+ const out = [];
2197
+ for (const h of hits) {
2198
+ const n = await this.store.getNode(h.nodeKey);
2199
+ if (n)
2200
+ out.push({ ...toHit(n), score: h.score });
2201
+ }
2202
+ return out;
2203
+ }
2204
+ async semanticQuery(query, opts = {}) {
2205
+ await this.store.init();
2206
+ if (!this.embedder)
2207
+ return [];
2208
+ const limit = clampLimit(opts.limit, 20, 100);
2209
+ const qvec = await this.embedder.embedQuery(query);
2210
+ const all = await this.store.allEmbeddings();
2211
+ const scored = all.map((e) => ({ nodeKey: e.nodeKey, score: cosineSimilarity(qvec, e.vec) })).sort((a, b) => b.score - a.score).slice(0, limit);
2212
+ const out = [];
2213
+ for (const s of scored) {
2214
+ const n = await this.store.getNode(s.nodeKey);
2215
+ if (n)
2216
+ out.push({ ...toHit(n), score: s.score });
2217
+ }
2218
+ return out;
2219
+ }
2220
+ async tracePath(nodeKey, opts = {}) {
2221
+ await this.store.init();
2222
+ const direction = opts.direction ?? "out";
2223
+ const maxDepth = clampLimit(opts.maxDepth, 3, 5);
2224
+ const limit = clampLimit(opts.limit, 100, 1e3);
2225
+ const kinds = opts.kinds ?? null;
2226
+ const steps = [];
2227
+ const seenEdge = /* @__PURE__ */ new Set();
2228
+ const visited = /* @__PURE__ */ new Set([nodeKey]);
2229
+ let frontier = [nodeKey];
2230
+ for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
2231
+ const next = [];
2232
+ for (const key of frontier) {
2233
+ const edges = await this.store.neighbors(key, direction, kinds, limit);
2234
+ for (const e of edges) {
2235
+ const id = `${e.srcKey} ${e.dstKey} ${e.kind}`;
2236
+ if (seenEdge.has(id))
2237
+ continue;
2238
+ seenEdge.add(id);
2239
+ steps.push({ srcKey: e.srcKey, dstKey: e.dstKey, kind: e.kind });
2240
+ if (steps.length >= limit)
2241
+ return steps;
2242
+ const other = direction === "out" ? e.dstKey : e.srcKey;
2243
+ if (!visited.has(other)) {
2244
+ visited.add(other);
2245
+ next.push(other);
2246
+ }
2247
+ }
2248
+ }
2249
+ frontier = next;
2250
+ }
2251
+ return steps;
2252
+ }
2253
+ async getCodeSnippet(nodeKey) {
2254
+ await this.store.init();
2255
+ const n = await this.store.getNode(nodeKey);
2256
+ if (!n)
2257
+ return null;
2258
+ let source = "";
2259
+ try {
2260
+ const full = readFileSync4(posix3.join(this.repoRoot, n.filePath), "utf8");
2261
+ source = full.slice(n.startByte, n.endByte);
2262
+ } catch {
2263
+ return null;
2264
+ }
2265
+ return {
2266
+ nodeKey: n.nodeKey,
2267
+ filePath: n.filePath,
2268
+ startLine: n.startLine,
2269
+ endLine: n.endLine,
2270
+ source
2271
+ };
2272
+ }
2273
+ async getArchitecture(opts = {}) {
2274
+ await this.store.init();
2275
+ const kinds = await this.store.kindDistribution();
2276
+ const langs = await this.store.languageDistribution();
2277
+ const kindDistribution = {};
2278
+ for (const k of kinds)
2279
+ kindDistribution[k.kind] = k.count;
2280
+ const languageDistribution = {};
2281
+ for (const l of langs)
2282
+ languageDistribution[l.language] = l.count;
2283
+ return {
2284
+ fileCount: await this.store.countFiles(),
2285
+ nodeCount: await this.store.countNodes(),
2286
+ edgeCount: await this.store.countEdges(),
2287
+ kindDistribution,
2288
+ languageDistribution,
2289
+ topModules: await this.store.topModulesByFanIn(clampLimit(opts.topN, 10, 50))
2290
+ };
2291
+ }
2292
+ async changedSymbols(opts = {}) {
2293
+ await this.store.init();
2294
+ const files = changedSourceFiles(this.repoRoot, opts);
2295
+ if (files === null) {
2296
+ throw new Error("changedSymbols requires a git repository (change detection is git-based)");
2297
+ }
2298
+ const limit = clampLimit(opts.limit, 64, 200);
2299
+ const symbols = [];
2300
+ let indexedFiles = 0;
2301
+ for (const f of files) {
2302
+ const rows = await this.store.getNodesByFile(f.path);
2303
+ if (rows.length > 0)
2304
+ indexedFiles++;
2305
+ symbols.push(...rows);
2306
+ }
2307
+ symbols.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine);
2308
+ return {
2309
+ changedFiles: files.length,
2310
+ indexedFiles,
2311
+ symbols: symbols.slice(0, limit),
2312
+ truncated: symbols.length > limit
2313
+ };
2314
+ }
2315
+ async reviewChanges(opts = {}) {
2316
+ await this.store.init();
2317
+ const rawPaths = enumerateChangedPaths(this.repoRoot, opts);
2318
+ if (rawPaths === null) {
2319
+ throw new Error("reviewChanges requires a git repository (change detection is git-based)");
2320
+ }
2321
+ const hunksRes = changedHunks(this.repoRoot, opts);
2322
+ const hunks = hunksRes?.files ?? /* @__PURE__ */ new Map();
2323
+ let hunkCoverage = hunksRes?.coverage ?? "none";
2324
+ const riskFlags = scanRiskFlags({
2325
+ paths: rawPaths,
2326
+ hunks,
2327
+ readUntracked: (p) => this.readRepoFileHead(p, MAX_UNTRACKED_HEAD_BYTES)
2328
+ });
2329
+ const files = classifyPaths(rawPaths.keys());
2330
+ const limit = clampLimit(opts.limit, 64, 200);
2331
+ const symbols = [];
2332
+ let indexedFiles = 0;
2333
+ let staleFiles = 0;
2334
+ for (const f of files) {
2335
+ const rows = await this.store.getNodesByFile(f.path);
2336
+ if (rows.length === 0)
2337
+ continue;
2338
+ indexedFiles++;
2339
+ const fileRow = await this.store.getFile(f.path);
2340
+ const content = this.readRepoFile(f.path);
2341
+ const stale = fileRow !== null && content !== null && hashContent(content) !== fileRow.contentHash;
2342
+ if (stale) {
2343
+ staleFiles++;
2344
+ if (hunkCoverage === "full")
2345
+ hunkCoverage = "partial";
2346
+ }
2347
+ const fileStatus = rawPaths.get(f.path) ?? "modified";
2348
+ const fh = hunks.get(f.path);
2349
+ for (const row of rows) {
2350
+ symbols.push(classifyReviewSymbol(row, fileStatus, fh, stale));
2351
+ }
2352
+ }
2353
+ symbols.sort((a, b) => changeRank(a.change) - changeRank(b.change) || a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine);
2354
+ return {
2355
+ changedFiles: files.length,
2356
+ indexedFiles,
2357
+ staleFiles,
2358
+ symbols: symbols.slice(0, limit),
2359
+ truncated: symbols.length > limit,
2360
+ riskFlags,
2361
+ hunkCoverage,
2362
+ baseKind: opts.since ? "ref" : "uncommitted",
2363
+ headSha: headCommit(this.repoRoot),
2364
+ dirty: isWorktreeDirty(this.repoRoot),
2365
+ baseSha: opts.since ? resolveCommitSha(this.repoRoot, opts.since) : null
2366
+ };
2367
+ }
2368
+ async close() {
2369
+ await this.store.close();
2370
+ }
2371
+ // ── internal ──
2372
+ readRepoFileHead(relPath, maxBytes) {
2373
+ const content = this.readRepoFile(relPath);
2374
+ return content === null ? null : content.slice(0, maxBytes);
2375
+ }
2376
+ readRepoFile(relPath) {
2377
+ try {
2378
+ return readFileSync4(posix3.join(this.repoRoot, relPath), "utf8");
2379
+ } catch {
2380
+ return null;
2381
+ }
2382
+ }
2383
+ async wipe() {
2384
+ for (const f of await this.store.getAllFiles()) {
2385
+ await this.store.deleteFile(f.path);
2386
+ }
2387
+ }
2388
+ };
2389
+ function toHit(n) {
2390
+ return {
2391
+ nodeKey: n.nodeKey,
2392
+ kind: n.kind,
2393
+ name: n.name,
2394
+ qualifiedName: n.qualifiedName,
2395
+ filePath: n.filePath,
2396
+ language: n.language,
2397
+ startLine: n.startLine,
2398
+ endLine: n.endLine
2399
+ };
2400
+ }
2401
+ function clampLimit(v, dflt, max) {
2402
+ if (v === void 0 || !Number.isFinite(v))
2403
+ return dflt;
2404
+ return Math.max(1, Math.min(max, Math.floor(v)));
2405
+ }
2406
+ function changeRank(c) {
2407
+ return c === "added" ? 0 : c === "modified" ? 1 : 2;
2408
+ }
2409
+ function classifyReviewSymbol(row, fileStatus, fh, stale) {
2410
+ if (stale)
2411
+ return { ...row, change: "co_located", changedLineCount: 0, staleIndex: true };
2412
+ if (fileStatus === "added" || fh?.wholeFile) {
2413
+ return {
2414
+ ...row,
2415
+ change: "added",
2416
+ changedLineCount: row.endLine - row.startLine + 1,
2417
+ staleIndex: false
2418
+ };
2419
+ }
2420
+ if (!fh || fh.degraded || fh.binary) {
2421
+ return { ...row, change: "co_located", changedLineCount: 0, staleIndex: false };
2422
+ }
2423
+ let overlap = 0;
2424
+ for (const r of fh.addedRanges) {
2425
+ const lo = Math.max(r.start, row.startLine);
2426
+ const hi = Math.min(r.end, row.endLine);
2427
+ if (hi >= lo)
2428
+ overlap += hi - lo + 1;
2429
+ }
2430
+ const deletionTouch = fh.deletionPoints.some((c) => c >= row.startLine - 1 && c <= row.endLine);
2431
+ if (overlap > 0 || deletionTouch) {
2432
+ return { ...row, change: "modified", changedLineCount: overlap, staleIndex: false };
2433
+ }
2434
+ return { ...row, change: "co_located", changedLineCount: 0, staleIndex: false };
2435
+ }
2436
+
2437
+ // ../../packages/code-index/build/store/node-sqlite.js
2438
+ import Database from "better-sqlite3";
2439
+ function toNodeRow(r) {
2440
+ return {
2441
+ nodeKey: r.node_key,
2442
+ kind: r.kind,
2443
+ name: r.name,
2444
+ qualifiedName: r.qualified_name,
2445
+ filePath: r.file_path,
2446
+ language: r.language,
2447
+ startLine: r.start_line,
2448
+ endLine: r.end_line,
2449
+ startByte: r.start_byte,
2450
+ endByte: r.end_byte,
2451
+ signature: r.signature,
2452
+ doc: r.doc
2453
+ };
2454
+ }
2455
+ var CHUNK = 400;
2456
+ var NodeSqliteStore = class {
2457
+ db;
2458
+ constructor(dbPath) {
2459
+ this.db = new Database(dbPath);
2460
+ this.db.pragma("journal_mode = WAL");
2461
+ this.db.pragma("foreign_keys = ON");
2462
+ }
2463
+ async init() {
2464
+ this.db.exec(SCHEMA_SQL);
2465
+ }
2466
+ async close() {
2467
+ this.db.close();
2468
+ }
2469
+ async getMeta(key) {
2470
+ const row = this.db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
2471
+ return row?.value ?? null;
2472
+ }
2473
+ async setMeta(key, value2) {
2474
+ this.db.prepare("INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value2);
2475
+ }
2476
+ async getAllFiles() {
2477
+ const rows = this.db.prepare("SELECT * FROM files").all();
2478
+ return rows.map((r) => ({
2479
+ path: r.path,
2480
+ language: r.language,
2481
+ contentHash: r.content_hash,
2482
+ mtimeMs: r.mtime_ms,
2483
+ size: r.size,
2484
+ indexedAt: r.indexed_at
2485
+ }));
2486
+ }
2487
+ async getFile(path) {
2488
+ const r = this.db.prepare("SELECT * FROM files WHERE path = ?").get(path);
2489
+ if (!r)
2490
+ return null;
2491
+ return {
2492
+ path: r.path,
2493
+ language: r.language,
2494
+ contentHash: r.content_hash,
2495
+ mtimeMs: r.mtime_ms,
2496
+ size: r.size,
2497
+ indexedAt: r.indexed_at
2498
+ };
2499
+ }
2500
+ async upsertFile(row) {
2501
+ this.upsertFileStmt().run(row.path, row.language, row.contentHash, row.mtimeMs, row.size, row.indexedAt);
2502
+ }
2503
+ async replaceFileNodes(file, nodes, ftsText) {
2504
+ const tx = this.db.transaction(() => {
2505
+ this.deleteFileGraph(file.path);
2506
+ const insNode = this.insertNodeStmt();
2507
+ const insFts = this.insertFtsStmt();
2508
+ for (const n of nodes) {
2509
+ insNode.run(n.nodeKey, n.kind, n.name, n.qualifiedName, n.filePath, n.language, n.startLine, n.endLine, n.startByte, n.endByte, n.signature, n.doc);
2510
+ insFts.run(n.nodeKey, ftsText.get(n.nodeKey) ?? n.name);
2511
+ }
2512
+ this.upsertFileStmt().run(file.path, file.language, file.contentHash, file.mtimeMs, file.size, file.indexedAt);
2513
+ });
2514
+ tx();
2515
+ }
2516
+ async deleteFile(path) {
2517
+ const tx = this.db.transaction(() => {
2518
+ this.deleteFileGraph(path);
2519
+ this.db.prepare("DELETE FROM files WHERE path = ?").run(path);
2520
+ });
2521
+ tx();
2522
+ }
2523
+ async replaceOutgoingEdges(srcKeys, edges) {
2524
+ const tx = this.db.transaction(() => {
2525
+ const del = this.db.prepare("DELETE FROM edges WHERE src_key = ?");
2526
+ for (const k of srcKeys)
2527
+ del.run(k);
2528
+ const ins = this.db.prepare("INSERT OR IGNORE INTO edges(src_key, dst_key, kind) VALUES(?, ?, ?)");
2529
+ for (const e of edges)
2530
+ ins.run(e.srcKey, e.dstKey, e.kind);
2531
+ });
2532
+ tx();
2533
+ }
2534
+ async getNameIndex() {
2535
+ const rows = this.db.prepare("SELECT node_key, name, kind, file_path FROM nodes").all();
2536
+ return rows.map((r) => ({
2537
+ nodeKey: r.node_key,
2538
+ name: r.name,
2539
+ kind: r.kind,
2540
+ filePath: r.file_path
2541
+ }));
2542
+ }
2543
+ async getNode(nodeKey) {
2544
+ const r = this.db.prepare("SELECT * FROM nodes WHERE node_key = ?").get(nodeKey);
2545
+ return r ? toNodeRow(r) : null;
2546
+ }
2547
+ async getNodesByFile(path) {
2548
+ const rows = this.db.prepare("SELECT * FROM nodes WHERE file_path = ? ORDER BY start_line").all(path);
2549
+ return rows.map(toNodeRow);
2550
+ }
2551
+ async getNodesByKinds(kinds) {
2552
+ if (kinds.length === 0)
2553
+ return [];
2554
+ const rows = this.db.prepare(`SELECT * FROM nodes WHERE kind IN (${kinds.map(() => "?").join(",")})`).all(...kinds);
2555
+ return rows.map(toNodeRow);
2556
+ }
2557
+ async getAllNodes() {
2558
+ const rows = this.db.prepare("SELECT * FROM nodes").all();
2559
+ return rows.map(toNodeRow);
2560
+ }
2561
+ async getAllEdges() {
2562
+ const rows = this.db.prepare("SELECT src_key, dst_key, kind FROM edges").all();
2563
+ return rows.map((r) => ({
2564
+ srcKey: r.src_key,
2565
+ dstKey: r.dst_key,
2566
+ kind: r.kind
2567
+ }));
2568
+ }
2569
+ async upsertEmbeddings(rows) {
2570
+ const stmt = this.db.prepare("INSERT OR REPLACE INTO embeddings(node_key, dim, vec, content_hash) VALUES(?, ?, ?, ?)");
2571
+ const tx = this.db.transaction(() => {
2572
+ for (const r of rows) {
2573
+ const buf = Buffer.from(r.vec.buffer, r.vec.byteOffset, r.vec.byteLength);
2574
+ stmt.run(r.nodeKey, r.dim, buf, r.contentHash);
2575
+ }
2576
+ });
2577
+ tx();
2578
+ }
2579
+ async getEmbeddingContentHashes() {
2580
+ const rows = this.db.prepare("SELECT node_key, content_hash FROM embeddings").all();
2581
+ return new Map(rows.map((r) => [r.node_key, r.content_hash]));
2582
+ }
2583
+ async allEmbeddings() {
2584
+ const rows = this.db.prepare("SELECT node_key, vec FROM embeddings").all();
2585
+ return rows.map((r) => ({
2586
+ nodeKey: r.node_key,
2587
+ vec: new Float32Array(r.vec.buffer, r.vec.byteOffset, r.vec.byteLength / 4)
2588
+ }));
2589
+ }
2590
+ async pruneOrphanEmbeddings() {
2591
+ const info = this.db.prepare("DELETE FROM embeddings WHERE node_key NOT IN (SELECT node_key FROM nodes)").run();
2592
+ return info.changes;
2593
+ }
2594
+ async countEmbeddings() {
2595
+ return this.db.prepare("SELECT COUNT(*) AS c FROM embeddings").get().c;
2596
+ }
2597
+ async searchFts(query, kind, limit) {
2598
+ const match = toFtsMatchQuery(query);
2599
+ if (!match)
2600
+ return [];
2601
+ const sql = kind ? `SELECT f.node_key AS node_key, bm25(nodes_fts) AS score
2602
+ FROM nodes_fts f JOIN nodes n ON n.node_key = f.node_key
2603
+ WHERE nodes_fts MATCH ? AND n.kind = ?
2604
+ ORDER BY score LIMIT ?` : `SELECT f.node_key AS node_key, bm25(nodes_fts) AS score
2605
+ FROM nodes_fts f JOIN nodes n ON n.node_key = f.node_key
2606
+ WHERE nodes_fts MATCH ?
2607
+ ORDER BY score LIMIT ?`;
2608
+ const stmt = this.db.prepare(sql);
2609
+ const rows = kind ? stmt.all(match, kind, limit) : stmt.all(match, limit);
2610
+ return rows.map((r) => ({ nodeKey: r.node_key, score: -r.score }));
2611
+ }
2612
+ async neighbors(nodeKey, direction, kinds, limit) {
2613
+ const col = direction === "out" ? "src_key" : "dst_key";
2614
+ let sql = `SELECT src_key, dst_key, kind FROM edges WHERE ${col} = ?`;
2615
+ const params = [nodeKey];
2616
+ if (kinds && kinds.length > 0) {
2617
+ sql += ` AND kind IN (${kinds.map(() => "?").join(",")})`;
2618
+ params.push(...kinds);
2619
+ }
2620
+ sql += " LIMIT ?";
2621
+ params.push(limit);
2622
+ const rows = this.db.prepare(sql).all(...params);
2623
+ return rows.map((r) => ({
2624
+ srcKey: r.src_key,
2625
+ dstKey: r.dst_key,
2626
+ kind: r.kind
2627
+ }));
2628
+ }
2629
+ async countFiles() {
2630
+ return this.db.prepare("SELECT COUNT(*) AS c FROM files").get().c;
2631
+ }
2632
+ async countNodes() {
2633
+ return this.db.prepare("SELECT COUNT(*) AS c FROM nodes").get().c;
2634
+ }
2635
+ async countEdges() {
2636
+ return this.db.prepare("SELECT COUNT(*) AS c FROM edges").get().c;
2637
+ }
2638
+ async kindDistribution() {
2639
+ const rows = this.db.prepare("SELECT kind, COUNT(*) AS c FROM nodes GROUP BY kind ORDER BY c DESC").all();
2640
+ return rows.map((r) => ({ kind: r.kind, count: r.c }));
2641
+ }
2642
+ async languageDistribution() {
2643
+ const rows = this.db.prepare("SELECT language, COUNT(*) AS c FROM files GROUP BY language ORDER BY c DESC").all();
2644
+ return rows.map((r) => ({ language: r.language, count: r.c }));
2645
+ }
2646
+ async topModulesByFanIn(limit) {
2647
+ const rows = this.db.prepare(`SELECT n.file_path AS file_path, n.language AS language,
2648
+ (SELECT COUNT(*) FROM nodes n2 WHERE n2.file_path = n.file_path) AS symbol_count,
2649
+ (SELECT COUNT(*) FROM edges e WHERE e.src_key = n.node_key AND e.kind = 'imports') AS imports_out,
2650
+ (SELECT COUNT(*) FROM edges e WHERE e.dst_key = n.node_key AND e.kind = 'imports') AS imports_in
2651
+ FROM nodes n
2652
+ WHERE n.kind = 'module'
2653
+ ORDER BY imports_in DESC, symbol_count DESC
2654
+ LIMIT ?`).all(limit);
2655
+ return rows.map((r) => ({
2656
+ filePath: r.file_path,
2657
+ language: r.language,
2658
+ symbolCount: r.symbol_count,
2659
+ importsOut: r.imports_out,
2660
+ importsIn: r.imports_in
2661
+ }));
2662
+ }
2663
+ // ── internal helpers ──
2664
+ deleteFileGraph(path) {
2665
+ const keys = this.db.prepare("SELECT node_key FROM nodes WHERE file_path = ?").all(path).map((r) => r.node_key);
2666
+ const delEdge = this.db.prepare("DELETE FROM edges WHERE src_key = ?");
2667
+ const delFts = this.db.prepare("DELETE FROM nodes_fts WHERE node_key = ?");
2668
+ for (let i = 0; i < keys.length; i += CHUNK) {
2669
+ for (const k of keys.slice(i, i + CHUNK)) {
2670
+ delEdge.run(k);
2671
+ delFts.run(k);
2672
+ }
2673
+ }
2674
+ this.db.prepare("DELETE FROM nodes WHERE file_path = ?").run(path);
2675
+ }
2676
+ _insNode = null;
2677
+ insertNodeStmt() {
2678
+ if (!this._insNode) {
2679
+ this._insNode = this.db.prepare(`INSERT OR REPLACE INTO nodes(
2680
+ node_key, kind, name, qualified_name, file_path, language,
2681
+ start_line, end_line, start_byte, end_byte, signature, doc
2682
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
2683
+ }
2684
+ return this._insNode;
2685
+ }
2686
+ _insFts = null;
2687
+ insertFtsStmt() {
2688
+ if (!this._insFts) {
2689
+ this._insFts = this.db.prepare("INSERT INTO nodes_fts(node_key, search_text) VALUES(?, ?)");
2690
+ }
2691
+ return this._insFts;
2692
+ }
2693
+ _upsertFile = null;
2694
+ upsertFileStmt() {
2695
+ if (!this._upsertFile) {
2696
+ this._upsertFile = this.db.prepare(`INSERT INTO files(path, language, content_hash, mtime_ms, size, indexed_at)
2697
+ VALUES(?, ?, ?, ?, ?, ?)
2698
+ ON CONFLICT(path) DO UPDATE SET
2699
+ language = excluded.language,
2700
+ content_hash = excluded.content_hash,
2701
+ mtime_ms = excluded.mtime_ms,
2702
+ size = excluded.size,
2703
+ indexed_at = excluded.indexed_at`);
2704
+ }
2705
+ return this._upsertFile;
2706
+ }
2707
+ };
2708
+
2709
+ // ../../packages/code-index/build/sync/types.js
2710
+ import { createHash as createHash4 } from "node:crypto";
2711
+ function sha2562(input) {
2712
+ return createHash4("sha256").update(input, "utf8").digest("hex");
2713
+ }
2714
+ function toCloudNode(n) {
2715
+ return {
2716
+ node_key_hash: sha2562(n.nodeKey),
2717
+ name: n.name,
2718
+ qualified_name: n.qualifiedName,
2719
+ path_hash: sha2562(n.filePath),
2720
+ kind: n.kind,
2721
+ language: n.language,
2722
+ start_line: n.startLine,
2723
+ end_line: n.endLine,
2724
+ signature_hash: n.signature ? sha2562(n.signature) : null
2725
+ };
2726
+ }
2727
+ function toCloudEdge(e) {
2728
+ return {
2729
+ src_key_hash: sha2562(e.srcKey),
2730
+ dst_key_hash: sha2562(e.dstKey),
2731
+ kind: e.kind
2732
+ };
2733
+ }
2734
+ function toCloudEmbedding(v, contentHash, meta) {
2735
+ return {
2736
+ node_key_hash: sha2562(v.nodeKey),
2737
+ embedding: Array.from(v.vec),
2738
+ recipe_version: meta.recipeVersion,
2739
+ dim: meta.dim,
2740
+ model: meta.model,
2741
+ content_hash: contentHash
2742
+ };
2743
+ }
2744
+
2745
+ // ../../packages/code-index/build/sync/client.js
2746
+ var SyncError = class extends Error {
2747
+ status;
2748
+ code;
2749
+ constructor(message, status2, code) {
2750
+ super(message);
2751
+ this.status = status2;
2752
+ this.code = code;
2753
+ this.name = "SyncError";
2754
+ }
2755
+ };
2756
+ var CodeIndexSync = class {
2757
+ store;
2758
+ cfg;
2759
+ base;
2760
+ chunk;
2761
+ fetchImpl;
2762
+ constructor(store, cfg) {
2763
+ this.store = store;
2764
+ this.cfg = cfg;
2765
+ this.base = cfg.endpoint.replace(/\/+$/, "");
2766
+ this.chunk = cfg.chunkSize && cfg.chunkSize > 0 ? cfg.chunkSize : 1e3;
2767
+ this.fetchImpl = cfg.fetchImpl ?? fetch;
2768
+ }
2769
+ /** Close the underlying store handle. */
2770
+ async close() {
2771
+ await this.store.close();
2772
+ }
2773
+ /** Enable/disable the opt-in consent (CLI `maind sync enable`). */
2774
+ async setConsent(enabled, policyVersion) {
2775
+ await this.post("/api/codebase/consent", {
2776
+ enabled,
2777
+ policy_version: policyVersion ?? null
2778
+ });
2779
+ }
2780
+ /** Cloud sync status for this repo (`maind sync status`). */
2781
+ async remoteStatus() {
2782
+ const qs = new URLSearchParams({ project_key: this.cfg.projectKey });
2783
+ const json = await this.get(`/api/codebase/status?${qs.toString()}`);
2784
+ return json;
2785
+ }
2786
+ /** Local index status (for the CLI to decide whether there's anything to push). */
2787
+ async localStatus() {
2788
+ await this.store.init();
2789
+ return {
2790
+ indexed: await this.store.countFiles() > 0,
2791
+ nodeCount: await this.store.countNodes(),
2792
+ edgeCount: await this.store.countEdges(),
2793
+ embeddingCount: await this.store.countEmbeddings(),
2794
+ fileCount: await this.store.countFiles(),
2795
+ headCommit: await this.store.getMeta(META_LAST_INDEX_COMMIT),
2796
+ lastSyncedAt: numOrNull(await this.store.getMeta(META_LAST_SYNCED_AT))
2797
+ };
2798
+ }
2799
+ /**
2800
+ * Full push: upsert every local node + edge, then prune cloud rows that no
2801
+ * longer exist locally (deletions reconcile). Incremental-by-commit is a later
2802
+ * optimization; a full upsert is idempotent and fine at repo scale.
2803
+ */
2804
+ async push() {
2805
+ await this.store.init();
2806
+ const nodes = (await this.store.getAllNodes()).map(toCloudNode);
2807
+ const edges = (await this.store.getAllEdges()).map(toCloudEdge);
2808
+ const schemaVersion = await this.store.getMeta(META_SCHEMA_VERSION) ?? SCHEMA_VERSION;
2809
+ const headCommit2 = await this.store.getMeta(META_LAST_INDEX_COMMIT);
2810
+ const fileCount = await this.store.countFiles();
2811
+ const langDist = Object.fromEntries((await this.store.languageDistribution()).map((l) => [l.language, l.count]));
2812
+ const manifest = {
2813
+ project_key: this.cfg.projectKey,
2814
+ head_commit: headCommit2,
2815
+ schema_version: schemaVersion,
2816
+ file_count: fileCount,
2817
+ lang_dist: langDist
2818
+ };
2819
+ let phaseDone = 0;
2820
+ const nodeChunks = chunkArray(nodes, this.chunk);
2821
+ const edgeChunks = chunkArray(edges, this.chunk * 2);
2822
+ const totalChunks = Math.max(1, nodeChunks.length + edgeChunks.length);
2823
+ for (const chunk of nodeChunks) {
2824
+ await this.post("/api/codebase/push", { ...manifest, nodes: chunk, edges: [] });
2825
+ this.cfg.onProgress?.({ phase: "nodes", done: ++phaseDone, total: totalChunks });
2826
+ }
2827
+ for (const chunk of edgeChunks) {
2828
+ await this.post("/api/codebase/push", { ...manifest, nodes: [], edges: chunk });
2829
+ this.cfg.onProgress?.({ phase: "edges", done: ++phaseDone, total: totalChunks });
2830
+ }
2831
+ if (nodeChunks.length === 0 && edgeChunks.length === 0) {
2832
+ await this.post("/api/codebase/push", { ...manifest, nodes: [], edges: [] });
2833
+ }
2834
+ const keep = nodes.map((n) => n.node_key_hash);
2835
+ const pruneRes = await this.post("/api/codebase/prune", {
2836
+ project_key: this.cfg.projectKey,
2837
+ keep_node_key_hashes: keep
2838
+ });
2839
+ this.cfg.onProgress?.({ phase: "prune", done: totalChunks, total: totalChunks });
2840
+ const embeddings = await this.store.countEmbeddings() > 0 ? await this.pushEmbeddings() : 0;
2841
+ await this.store.setMeta(META_LAST_SYNCED_AT, String(Date.now()));
2842
+ if (headCommit2)
2843
+ await this.store.setMeta(META_LAST_SYNCED_COMMIT, headCommit2);
2844
+ return {
2845
+ nodes: nodes.length,
2846
+ edges: edges.length,
2847
+ pruned: numOr0(pruneRes.nodes_deleted),
2848
+ embeddings,
2849
+ headCommit: headCommit2
2850
+ };
2851
+ }
2852
+ /**
2853
+ * Push symbol vectors (ADR-220). Diffs local content-hashes against the
2854
+ * remote map (get_codebase_embedding_hashes) and uploads only new/changed
2855
+ * vectors — a full re-upload would be MBs on every sync. Returns the number
2856
+ * of vectors uploaded. Requires the embedder identity recorded by
2857
+ * `index --embed` (META_EMBED_*); older indexes must re-run it once.
2858
+ */
2859
+ async pushEmbeddings() {
2860
+ await this.store.init();
2861
+ const localHashes = await this.store.getEmbeddingContentHashes();
2862
+ if (localHashes.size === 0)
2863
+ return 0;
2864
+ const meta = await this.embedMeta();
2865
+ const qs = new URLSearchParams({ project_key: this.cfg.projectKey });
2866
+ const remote = await this.get(`/api/codebase/embedding-hashes?${qs.toString()}`);
2867
+ const remoteHashes = remote.hashes ?? {};
2868
+ const pending = (await this.store.allEmbeddings()).map((v) => toCloudEmbedding(v, localHashes.get(v.nodeKey) ?? "", meta)).filter((c) => c.content_hash !== "" && remoteHashes[c.node_key_hash] !== c.content_hash);
2869
+ if (pending.length === 0)
2870
+ return 0;
2871
+ const chunks = chunkArray(pending, EMBED_CHUNK);
2872
+ let done = 0;
2873
+ for (const chunk of chunks) {
2874
+ await this.post("/api/codebase/push-embeddings", {
2875
+ project_key: this.cfg.projectKey,
2876
+ embeddings: chunk
2877
+ });
2878
+ this.cfg.onProgress?.({ phase: "embeddings", done: ++done, total: chunks.length });
2879
+ }
2880
+ return pending.length;
2881
+ }
2882
+ /** The embedder identity recorded at `index --embed` time. */
2883
+ async embedMeta() {
2884
+ const recipeVersion = await this.store.getMeta(META_EMBED_RECIPE_VERSION);
2885
+ const model = await this.store.getMeta(META_EMBED_MODEL);
2886
+ const dim = numOrNull(await this.store.getMeta(META_EMBED_DIM));
2887
+ if (!recipeVersion || !model || !dim) {
2888
+ throw new SyncError("local embeddings lack embedder metadata \u2014 re-run `maind index --embed` once to record it", 0, "missing_embed_meta");
2889
+ }
2890
+ return { recipeVersion, model, dim };
2891
+ }
2892
+ // ── HTTP ──
2893
+ headers() {
2894
+ const h = {
2895
+ "content-type": "application/json",
2896
+ authorization: `Bearer ${this.cfg.apiKey}`
2897
+ };
2898
+ if (this.cfg.deviceId)
2899
+ h["x-maind-device-id"] = this.cfg.deviceId;
2900
+ return h;
2901
+ }
2902
+ async post(path, body) {
2903
+ const res = await this.fetchImpl(`${this.base}${path}`, {
2904
+ method: "POST",
2905
+ headers: this.headers(),
2906
+ body: JSON.stringify(body)
2907
+ });
2908
+ return this.unwrap(res, path);
2909
+ }
2910
+ async get(path) {
2911
+ const res = await this.fetchImpl(`${this.base}${path}`, {
2912
+ method: "GET",
2913
+ headers: this.headers()
2914
+ });
2915
+ return this.unwrap(res, path);
2916
+ }
2917
+ async unwrap(res, path) {
2918
+ const text = await res.text().catch(() => "");
2919
+ let json = null;
2920
+ try {
2921
+ json = text ? JSON.parse(text) : null;
2922
+ } catch {
2923
+ json = null;
2924
+ }
2925
+ if (!res.ok || !json || json.ok !== true) {
2926
+ const code = json?.error;
2927
+ throw new SyncError(`${path} failed (HTTP ${res.status})${code ? `: ${code}` : ""}`, res.status, code);
2928
+ }
2929
+ return json;
2930
+ }
2931
+ };
2932
+ var EMBED_CHUNK = 250;
2933
+ function chunkArray(items, size) {
2934
+ const out = [];
2935
+ for (let i = 0; i < items.length; i += size)
2936
+ out.push(items.slice(i, i + size));
2937
+ return out;
2938
+ }
2939
+ function numOrNull(v) {
2940
+ if (v === null)
2941
+ return null;
2942
+ const n = Number(v);
2943
+ return Number.isFinite(n) ? n : null;
2944
+ }
2945
+ function numOr0(v) {
2946
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
2947
+ }
2948
+
2949
+ // ../../packages/code-index/build/node.js
2950
+ var DEFAULT_DB_RELPATH = ".maind/code-index.sqlite";
2951
+ function createNodeCodeIndex(opts) {
2952
+ const dbPath = opts.dbPath ?? posix4.join(opts.repoRoot, DEFAULT_DB_RELPATH);
2953
+ mkdirSync3(posix4.dirname(dbPath), { recursive: true });
2954
+ const store = new NodeSqliteStore(dbPath);
2955
+ const parser = new TreeSitterParser();
2956
+ return new CodeIndexEngine({
2957
+ store,
2958
+ parser,
2959
+ repoRoot: opts.repoRoot,
2960
+ embedder: opts.embedder
2961
+ });
2962
+ }
2963
+ function createCodeIndexSync(opts) {
2964
+ const dbPath = opts.dbPath ?? posix4.join(opts.repoRoot, DEFAULT_DB_RELPATH);
2965
+ const projectKey = opts.projectKey ?? computeProjectKey(opts.repoRoot) ?? void 0;
2966
+ if (!projectKey) {
2967
+ throw new Error("No project_key \u2014 the repo has no git remote. Cloud sync scopes by sha256(remote); commit a remote or pass an explicit key.");
2968
+ }
2969
+ const store = new NodeSqliteStore(dbPath);
2970
+ return new CodeIndexSync(store, {
2971
+ endpoint: opts.endpoint,
2972
+ apiKey: opts.apiKey,
2973
+ projectKey,
2974
+ deviceId: opts.deviceId,
2975
+ chunkSize: opts.chunkSize,
2976
+ onProgress: opts.onProgress
2977
+ });
2978
+ }
2979
+
2980
+ // ../../packages/embedding/build/index.js
2981
+ var EMBEDDING_MODEL = process.env.MAIND_EMBEDDING_MODEL ?? "Xenova/bge-small-en-v1.5";
2982
+ var EMBEDDING_DIM = Number(process.env.MAIND_EMBEDDING_DIM ?? 384);
2983
+ var EMBEDDING_DTYPE = process.env.MAIND_EMBEDDING_DTYPE ?? "q8";
2984
+ var EMBEDDING_QUERY_PREFIX = "Represent this sentence for searching relevant passages: ";
2985
+ var extractorPromise = null;
2986
+ async function getExtractor() {
2987
+ if (!extractorPromise) {
2988
+ extractorPromise = (async () => {
2989
+ const mod = await import("@huggingface/transformers");
2990
+ const cacheDir = process.env.MAIND_EMBEDDING_CACHE_DIR;
2991
+ if (cacheDir)
2992
+ mod.env.cacheDir = cacheDir;
2993
+ return mod.pipeline("feature-extraction", EMBEDDING_MODEL, {
2994
+ dtype: EMBEDDING_DTYPE,
2995
+ device: "cpu"
2996
+ });
2997
+ })();
2998
+ }
2999
+ return extractorPromise;
3000
+ }
3001
+ function createEmbedder() {
3002
+ const embedder = {
3003
+ async embedDocuments(texts) {
3004
+ if (texts.length === 0)
3005
+ return [];
3006
+ const extractor = await getExtractor();
3007
+ const out = await extractor(texts, { pooling: "cls", normalize: true });
3008
+ return out.tolist().map((v) => Float32Array.from(v));
3009
+ },
3010
+ async embedQuery(text) {
3011
+ const [vec] = await embedder.embedDocuments([
3012
+ EMBEDDING_QUERY_PREFIX + text
3013
+ ]);
3014
+ return vec;
3015
+ }
3016
+ };
3017
+ return embedder;
3018
+ }
3019
+
3020
+ // ../../packages/code-index/build/embed/node.js
3021
+ var NodeEmbedder = class {
3022
+ dim = EMBEDDING_DIM;
3023
+ model = EMBEDDING_MODEL;
3024
+ recipeVersion = EMBED_RECIPE_VERSION;
3025
+ inner = createEmbedder();
3026
+ embedDocuments(texts) {
3027
+ return this.inner.embedDocuments(texts);
3028
+ }
3029
+ embedQuery(text) {
3030
+ return this.inner.embedQuery(text);
3031
+ }
3032
+ };
3033
+
3034
+ // ../../packages/code-index/build/node-embed.js
3035
+ function createNodeEmbedder() {
3036
+ return new NodeEmbedder();
3037
+ }
3038
+
3039
+ // src/index-cmd.ts
3040
+ var OK_FILE_ENV = "MAIND_ORT_OK_FILE";
3041
+ var WORKER_FLAG = "--_ort-worker";
3042
+ var CLI_ENTRY = fileURLToPath(new URL("./index.js", import.meta.url));
3043
+ function isOrtWorker(argv) {
3044
+ return argv.includes(WORKER_FLAG);
3045
+ }
3046
+ function runOrtIsolated(argv) {
3047
+ const dir = mkdtempSync(join4(tmpdir(), "maind-ort-"));
3048
+ const okFile = join4(dir, "ok");
3049
+ const res = spawnSync(process.execPath, [CLI_ENTRY, ...argv, WORKER_FLAG], {
3050
+ stdio: ["ignore", "inherit", "inherit"],
3051
+ env: { ...process.env, [OK_FILE_ENV]: okFile }
3052
+ });
3053
+ const ok = existsSync2(okFile);
3054
+ try {
3055
+ rmSync(dirname3(okFile), { recursive: true, force: true });
3056
+ } catch {
3057
+ }
3058
+ if (ok) return 0;
3059
+ return res.status && res.status !== 0 ? res.status : 1;
3060
+ }
3061
+ function markOrtOk() {
3062
+ const f = process.env[OK_FILE_ENV];
3063
+ if (f) {
3064
+ try {
3065
+ writeFileSync3(f, "ok");
3066
+ } catch {
3067
+ }
3068
+ }
3069
+ }
3070
+ function flag(argv, name) {
3071
+ return argv.includes(name);
3072
+ }
3073
+ function value(argv, name) {
3074
+ const i = argv.indexOf(name);
3075
+ if (i === -1 || i + 1 >= argv.length) return void 0;
3076
+ return argv[i + 1];
3077
+ }
3078
+ function resolveRoot(explicit) {
3079
+ if (explicit) return explicit;
3080
+ try {
3081
+ return execFileSync4("git", ["rev-parse", "--show-toplevel"], {
3082
+ encoding: "utf8",
3083
+ stdio: ["ignore", "pipe", "ignore"]
3084
+ }).trim();
3085
+ } catch {
3086
+ return process.cwd();
3087
+ }
3088
+ }
3089
+ function openIndex(opts) {
3090
+ return createNodeCodeIndex({
3091
+ repoRoot: opts.root,
3092
+ dbPath: opts.db ?? void 0,
3093
+ embedder: createNodeEmbedder()
3094
+ });
3095
+ }
3096
+ function baseOpts(argv) {
3097
+ return {
3098
+ root: resolveRoot(value(argv, "--root")),
3099
+ db: value(argv, "--db") ?? null,
3100
+ full: flag(argv, "--full"),
3101
+ embed: flag(argv, "--embed"),
3102
+ json: flag(argv, "--json")
3103
+ };
3104
+ }
3105
+ function parseIndexArgs(argv) {
3106
+ return baseOpts(argv);
3107
+ }
3108
+ function parseIndexSearchArgs(argv) {
3109
+ const positional = argv.find((a) => !a.startsWith("--"));
3110
+ const limitRaw = value(argv, "--limit");
3111
+ const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 15;
3112
+ const kind = value(argv, "--kind");
3113
+ return {
3114
+ ...baseOpts(argv),
3115
+ query: positional ?? "",
3116
+ kind: kind ?? null,
3117
+ limit: Number.isFinite(limit) && limit > 0 ? limit : 15
3118
+ };
3119
+ }
3120
+ async function runIndex(opts) {
3121
+ const idx = openIndex(opts);
3122
+ try {
3123
+ const stats = await idx.indexRepository({
3124
+ full: opts.full,
3125
+ embed: opts.embed,
3126
+ onProgress: opts.json ? void 0 : (p) => {
3127
+ process.stderr.write(
3128
+ `\r ${p.done}/${p.total} ${p.path.slice(0, 60)}`.padEnd(84)
3129
+ );
3130
+ }
3131
+ });
3132
+ if (opts.json) {
3133
+ process.stdout.write(JSON.stringify(stats) + "\n");
3134
+ } else {
3135
+ process.stderr.write("\r".padEnd(84) + "\r");
3136
+ const extra = opts.embed ? `, ${(await idx.status()).embeddingCount} embeddings` : "";
3137
+ process.stdout.write(
3138
+ ` \u2713 ${stats.filesParsed} parsed \xB7 ${stats.filesSkipped} unchanged \xB7 ${stats.filesDeleted} removed \u2192 ${stats.nodeCount} symbols, ${stats.edgeCount} edges (${stats.durationMs}ms${stats.incremental ? ", incremental" : ""}${extra})
3139
+ `
3140
+ );
3141
+ }
3142
+ markOrtOk();
3143
+ return 0;
3144
+ } finally {
3145
+ await idx.close();
3146
+ }
3147
+ }
3148
+ async function runIndexStatus(opts) {
3149
+ const idx = openIndex(opts);
3150
+ try {
3151
+ const status2 = await idx.status();
3152
+ if (opts.json) {
3153
+ process.stdout.write(JSON.stringify(status2) + "\n");
3154
+ return 0;
3155
+ }
3156
+ if (!status2.indexed) {
3157
+ process.stdout.write(" not indexed \u2014 run `maind index` first.\n");
3158
+ return 0;
3159
+ }
3160
+ const langs = Object.entries(status2.languages).map(([l, c]) => `${l}:${c}`).join(" ");
3161
+ const when = status2.lastIndexedAt ? new Date(status2.lastIndexedAt).toISOString() : "\u2014";
3162
+ process.stdout.write(
3163
+ ` ${status2.fileCount} files \xB7 ${status2.nodeCount} symbols \xB7 ${status2.edgeCount} edges \xB7 ${status2.embeddingCount} embeddings
3164
+ languages: ${langs || "\u2014"}
3165
+ last indexed: ${when} (commit ${status2.lastIndexCommit?.slice(0, 8) ?? "\u2014"}, schema v${status2.schemaVersion ?? "\u2014"})
3166
+ `
3167
+ );
3168
+ return 0;
3169
+ } finally {
3170
+ await idx.close();
3171
+ }
3172
+ }
3173
+ async function runIndexSearch(opts) {
3174
+ if (!opts.query) {
3175
+ process.stderr.write("maind: index-search needs a query. Usage: maind index-search <query> [--kind <k>] [--limit <n>]\n");
3176
+ return 2;
3177
+ }
3178
+ const idx = openIndex(opts);
3179
+ try {
3180
+ const hits = await idx.searchGraph(opts.query, { kind: opts.kind ?? void 0, limit: opts.limit });
3181
+ if (opts.json) {
3182
+ process.stdout.write(JSON.stringify(hits) + "\n");
3183
+ return 0;
3184
+ }
3185
+ if (hits.length === 0) {
3186
+ process.stdout.write(" no matches.\n");
3187
+ return 0;
3188
+ }
3189
+ for (const h of hits) {
3190
+ process.stdout.write(
3191
+ ` ${h.kind.padEnd(10)} ${h.qualifiedName} ${h.filePath}:${h.startLine}
3192
+ `
3193
+ );
3194
+ }
3195
+ return 0;
3196
+ } finally {
3197
+ await idx.close();
3198
+ }
3199
+ }
3200
+ async function runIndexSemantic(opts) {
3201
+ if (!opts.query) {
3202
+ process.stderr.write("maind: index-semantic needs a query. Usage: maind index-semantic <query> [--limit <n>]\n");
3203
+ return 2;
3204
+ }
3205
+ const idx = openIndex(opts);
3206
+ try {
3207
+ const hits = await idx.semanticQuery(opts.query, { limit: opts.limit });
3208
+ if (opts.json) {
3209
+ process.stdout.write(JSON.stringify(hits) + "\n");
3210
+ } else if (hits.length === 0) {
3211
+ process.stdout.write(
3212
+ " no matches \u2014 did you build embeddings? Run 'maind index --embed' first.\n"
3213
+ );
3214
+ } else {
3215
+ for (const h of hits) {
3216
+ process.stdout.write(
3217
+ ` ${(h.score ?? 0).toFixed(3)} ${h.kind.padEnd(10)} ${h.qualifiedName} ${h.filePath}:${h.startLine}
3218
+ `
3219
+ );
3220
+ }
3221
+ }
3222
+ markOrtOk();
3223
+ return 0;
3224
+ } finally {
3225
+ await idx.close();
3226
+ }
3227
+ }
3228
+ async function runIndexArch(opts) {
3229
+ const idx = openIndex(opts);
3230
+ try {
3231
+ const arch = await idx.getArchitecture({ topN: 10 });
3232
+ if (opts.json) {
3233
+ process.stdout.write(JSON.stringify(arch) + "\n");
3234
+ return 0;
3235
+ }
3236
+ const kinds = Object.entries(arch.kindDistribution).map(([k, c]) => `${k}:${c}`).join(" ");
3237
+ process.stdout.write(
3238
+ ` ${arch.fileCount} files \xB7 ${arch.nodeCount} symbols \xB7 ${arch.edgeCount} edges
3239
+ kinds: ${kinds}
3240
+ most-imported modules:
3241
+ `
3242
+ );
3243
+ for (const m of arch.topModules) {
3244
+ process.stdout.write(
3245
+ ` ${String(m.importsIn).padStart(4)} \u2190 ${m.filePath} (${m.symbolCount} symbols)
3246
+ `
3247
+ );
3248
+ }
3249
+ return 0;
3250
+ } finally {
3251
+ await idx.close();
3252
+ }
3253
+ }
3254
+
3255
+ // src/review.ts
3256
+ import { execFileSync as execFileSync5 } from "node:child_process";
3257
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
3258
+ import { dirname as dirname4 } from "node:path";
3259
+
3260
+ // ../../packages/code-index/build/index.js
3261
+ var CODE_INDEX_VERSION = "0.1.0";
3262
+
3263
+ // src/review-render.ts
3264
+ var COMMENT_MARKER = "<!-- maind-review -->";
3265
+ var COMMENT_FINDINGS_CAP = 30;
3266
+ var FOOTER = "maind review \xB7 deterministic, provenance-backed";
3267
+ function buildReviewManifest(data, verdict, meta) {
3268
+ const review = data.review;
3269
+ return {
3270
+ schema: "maind.review.manifest/v1",
3271
+ generated_at: meta.generatedAt,
3272
+ key_prefix: meta.keyPrefix,
3273
+ run_ref: meta.runRef,
3274
+ base: meta.base,
3275
+ diff_stats: meta.diffStats,
3276
+ symbol_count: meta.symbolCount,
3277
+ risk_flags: review?.risk_flags ?? [],
3278
+ findings: (review?.convention_findings ?? []).map((f) => ({
3279
+ convention_id: f.convention_id,
3280
+ version: f.convention_version,
3281
+ severity: f.severity,
3282
+ grade: f.grade,
3283
+ matched_symbol_count: f.matched_symbols.length,
3284
+ matched_changed_symbol_count: f.matched_changed_symbols?.length ?? 0
3285
+ })),
3286
+ served: (review?.lesson_hints ?? []).map((l) => ({
3287
+ id: l.id,
3288
+ version: typeof l.version === "number" ? l.version : null,
3289
+ tier: l.tier ?? null
3290
+ })),
3291
+ verdict: verdict?.ok === true && typeof verdict.verdict === "string" ? {
3292
+ verdict: verdict.verdict,
3293
+ counts: {
3294
+ block: verdict.counts?.block ?? 0,
3295
+ warn: verdict.counts?.warn ?? 0,
3296
+ note: verdict.counts?.note ?? 0
3297
+ },
3298
+ fail_on: meta.failOn
3299
+ } : null,
3300
+ run: verdict?.ok === true ? verdict.run ?? null : null,
3301
+ ...verdict?.ok === true && verdict.run_note ? { run_note: verdict.run_note } : {},
3302
+ coverage_note: review?.coverage_note ?? null
3303
+ };
3304
+ }
3305
+ function severityBadge(f) {
3306
+ return `**[${f.severity}]**`;
3307
+ }
3308
+ function findingLine(f) {
3309
+ const cite = `\`${f.convention_id}@v${f.convention_version}\``;
3310
+ const grade = f.grade === "verdict" ? " \xB7 verdict-grade" : "";
3311
+ const changed = f.matched_changed_symbols?.length ?? 0;
3312
+ const matchInfo = changed > 0 ? `matched on changed symbols: ${formatSymbols(f.matched_changed_symbols ?? [])}` : `matched (co-located): ${formatSymbols(f.matched_symbols)}`;
3313
+ const summary = f.summary?.trim() ? ` \u2014 ${f.summary.trim()}` : "";
3314
+ return `- ${severityBadge(f)} ${cite}${summary}${grade}
3315
+ ${matchInfo}`;
3316
+ }
3317
+ function formatSymbols(symbols, max = 4) {
3318
+ const shown = symbols.slice(0, max).map((s) => `\`${s}\``);
3319
+ const rest = symbols.length - shown.length;
3320
+ return shown.join(", ") + (rest > 0 ? ` (+${rest} more)` : "");
3321
+ }
3322
+ function verdictLabel(verdict) {
3323
+ const counts = verdict.counts ?? {};
3324
+ return `**Verdict: ${(verdict.verdict ?? "?").toUpperCase()}** (block: ${counts.block ?? 0} \xB7 warn: ${counts.warn ?? 0} \xB7 note: ${counts.note ?? 0})`;
3325
+ }
3326
+ function attestationLine(verdict) {
3327
+ const r = verdict.run;
3328
+ if (r) {
3329
+ const grade = r.attestation_grade ? "attestation-grade" : "recorded, not attestation-grade";
3330
+ return `Attestation: run \`${r.id.slice(0, 8)}\` (${grade}) \xB7 commit \`${r.commit_sha.slice(0, 12)}\` \xB7 input \`${r.input_hash.slice(0, 12)}\` \xB7 policy \`${r.policy_hash.slice(0, 12)}\``;
3331
+ }
3332
+ if (verdict.run_note) return `Attestation: none \u2014 ${verdict.run_note}`;
3333
+ return null;
3334
+ }
3335
+ function baseLabel(meta) {
3336
+ return meta.base ? `against \`${meta.base}\`` : "uncommitted work";
3337
+ }
3338
+ function renderReviewMarkdown(data, verdict, meta) {
3339
+ const review = data.review;
3340
+ const findings = review?.convention_findings ?? [];
3341
+ const hints = review?.lesson_hints ?? [];
3342
+ const riskFlags = review?.risk_flags ?? [];
3343
+ const lines = [];
3344
+ lines.push("<!-- Generated by `maind review` \u2014 do not edit by hand. -->");
3345
+ lines.push(
3346
+ `<!-- generated_at: ${meta.generatedAt} | key: ${meta.keyPrefix}${meta.runRef ? ` | run: ${meta.runRef}` : ""} -->`
3347
+ );
3348
+ lines.push("");
3349
+ lines.push("# maind review");
3350
+ lines.push("");
3351
+ lines.push(
3352
+ `Reviewed ${baseLabel(meta)}: ${meta.symbolCount} changed symbol(s) across ${meta.diffStats?.changed_file_count ?? 0} file(s) \xB7 findings: ${findings.length} \xB7 risk flags: ${riskFlags.length} \xB7 lesson hints: ${hints.length}`
3353
+ );
3354
+ lines.push("");
3355
+ if (verdict?.ok === true && verdict.verdict) {
3356
+ lines.push(verdictLabel(verdict));
3357
+ const attest = attestationLine(verdict);
3358
+ if (attest) {
3359
+ lines.push("");
3360
+ lines.push(attest);
3361
+ }
3362
+ if (verdict.trust_note) {
3363
+ lines.push("");
3364
+ lines.push(`_${verdict.trust_note}_`);
3365
+ }
3366
+ lines.push("");
3367
+ }
3368
+ if (findings.length > 0) {
3369
+ lines.push("## Convention findings");
3370
+ lines.push("");
3371
+ for (const f of findings) lines.push(findingLine(f));
3372
+ lines.push("");
3373
+ } else {
3374
+ lines.push("## Convention findings");
3375
+ lines.push("");
3376
+ lines.push("_No convention findings for this change set._");
3377
+ lines.push("");
3378
+ }
3379
+ if (riskFlags.length > 0) {
3380
+ lines.push("## Risk flags");
3381
+ lines.push("");
3382
+ lines.push(
3383
+ "_Path heuristics from the local diff \u2014 informational, they never block._"
3384
+ );
3385
+ lines.push("");
3386
+ for (const flag2 of riskFlags) lines.push(`- \`${flag2}\``);
3387
+ lines.push("");
3388
+ }
3389
+ if (hints.length > 0) {
3390
+ lines.push("## Lesson hints");
3391
+ lines.push("");
3392
+ lines.push("_Semantic suggestions \u2014 advisory, never part of a verdict._");
3393
+ lines.push("");
3394
+ for (const h of hints) {
3395
+ const parts = [`\`${h.id}\``];
3396
+ if (h.tier) parts.push(h.tier);
3397
+ if (h.match_reason) parts.push(h.match_reason);
3398
+ lines.push(`- **${h.title}** (${parts.join(" \xB7 ")})`);
3399
+ }
3400
+ lines.push("");
3401
+ }
3402
+ if (review?.coverage_note) {
3403
+ lines.push("## Coverage");
3404
+ lines.push("");
3405
+ lines.push(`> ${review.coverage_note}`);
3406
+ lines.push("");
3407
+ }
3408
+ lines.push("---");
3409
+ lines.push(FOOTER);
3410
+ lines.push("");
3411
+ return lines.join("\n");
3412
+ }
3413
+ function renderReviewComment(data, verdict, meta, reportPath) {
3414
+ const review = data.review;
3415
+ const findings = review?.convention_findings ?? [];
3416
+ const riskFlags = review?.risk_flags ?? [];
3417
+ const hints = review?.lesson_hints ?? [];
3418
+ const lines = [];
3419
+ lines.push(COMMENT_MARKER);
3420
+ lines.push("## maind review");
3421
+ lines.push("");
3422
+ if (verdict?.ok === true && verdict.verdict) {
3423
+ lines.push(verdictLabel(verdict));
3424
+ const attest = attestationLine(verdict);
3425
+ if (attest) lines.push(attest);
3426
+ lines.push("");
3427
+ }
3428
+ if (findings.length === 0) {
3429
+ lines.push(
3430
+ `No convention findings for this change set (${meta.symbolCount} changed symbol(s) reviewed ${baseLabel(meta)}).`
3431
+ );
3432
+ lines.push("");
3433
+ } else {
3434
+ const shown = findings.slice(0, COMMENT_FINDINGS_CAP);
3435
+ for (const f of shown) lines.push(findingLine(f));
3436
+ if (findings.length > shown.length) {
3437
+ lines.push("");
3438
+ lines.push(
3439
+ `\u2026and ${findings.length - shown.length} more finding(s) \u2014 see the \`${reportPath}\` build artifact.`
3440
+ );
3441
+ }
3442
+ lines.push("");
3443
+ }
3444
+ if (riskFlags.length > 0) {
3445
+ lines.push(
3446
+ `**Risk flags:** ${riskFlags.map((f) => `\`${f}\``).join(" \xB7 ")} _(heuristics, never blocking)_`
3447
+ );
3448
+ lines.push("");
3449
+ }
3450
+ if (hints.length > 0) {
3451
+ lines.push("**Related lessons:** " + hints.map((h) => `\`${h.id}\``).join(" \xB7 "));
3452
+ lines.push("");
3453
+ }
3454
+ if (review?.coverage_note) {
3455
+ lines.push(`> ${review.coverage_note}`);
3456
+ lines.push("");
3457
+ }
3458
+ lines.push("---");
3459
+ lines.push(`_${FOOTER}_`);
3460
+ lines.push("");
3461
+ return lines.join("\n");
3462
+ }
3463
+ function renderFailOpenReview(meta, reason) {
3464
+ return [
3465
+ "<!-- Generated by `maind review` (fail-open path). -->",
3466
+ `<!-- generated_at: ${meta.generatedAt} -->`,
3467
+ "",
3468
+ "# maind review",
3469
+ "",
3470
+ `_maind review was unavailable for this run: ${reason}_`,
3471
+ "",
3472
+ "The build can proceed normally \u2014 this file is informational only.",
3473
+ ""
3474
+ ].join("\n");
3475
+ }
3476
+ function renderFailOpenComment(meta, reason) {
3477
+ return [
3478
+ COMMENT_MARKER,
3479
+ "## maind review",
3480
+ "",
3481
+ `_maind review was unavailable for this run: ${reason}_`,
3482
+ "",
3483
+ "---",
3484
+ `_${FOOTER}_`,
3485
+ ""
3486
+ ].join("\n");
3487
+ }
3488
+
3489
+ // src/review.ts
3490
+ var PROJECT_KEY_RE3 = /^[a-f0-9]{64}$/;
3491
+ var CLI_CLIENT_VERSION = "0.7.0";
3492
+ var DEFAULT_COMMENT_PATH = ".maind/review-comment.md";
3493
+ var SYMBOL_CAP = 64;
3494
+ function parseReviewArgs(argv) {
3495
+ const get = (name) => {
3496
+ const i = argv.indexOf(name);
3497
+ if (i === -1 || i + 1 >= argv.length) return void 0;
3498
+ return argv[i + 1];
3499
+ };
3500
+ const has = (name) => argv.includes(name);
3501
+ let comment = null;
3502
+ const ci = argv.indexOf("--comment");
3503
+ if (ci !== -1) {
3504
+ const next = argv[ci + 1];
3505
+ comment = next && !next.startsWith("--") ? next : DEFAULT_COMMENT_PATH;
3506
+ }
3507
+ const failOnRaw = get("--fail-on");
3508
+ const failOn = failOnRaw === "block" || failOnRaw === "warn" ? failOnRaw : null;
3509
+ return {
3510
+ base: get("--base") ?? null,
3511
+ uncommitted: has("--uncommitted"),
3512
+ summary: get("--summary") ?? null,
3513
+ json: has("--json"),
3514
+ out: get("--out") ?? ".maind/review.md",
3515
+ comment,
3516
+ gate: has("--gate"),
3517
+ failOn,
3518
+ strict: has("--strict"),
3519
+ noReport: has("--no-report"),
3520
+ projectKey: get("--project-key") ?? null,
3521
+ mcpUrl: get("--mcp-url") ?? null,
3522
+ runRef: get("--run-ref") ?? process.env.GITHUB_RUN_ID ?? null,
3523
+ clientVersion: CLI_CLIENT_VERSION
3524
+ };
3525
+ }
3526
+ function nowIso3() {
3527
+ return (/* @__PURE__ */ new Date()).toISOString();
3528
+ }
3529
+ function writeArtifact3(path, content) {
3530
+ const dir = dirname4(path);
3531
+ if (dir && dir !== ".") mkdirSync4(dir, { recursive: true });
3532
+ writeFileSync4(path, content, "utf8");
3533
+ }
3534
+ function manifestPathFor(out) {
3535
+ return out.endsWith(".md") ? out.slice(0, -3) + ".json" : out + ".json";
3536
+ }
3537
+ function verdictCrossesFailOn(verdict, failOn) {
3538
+ return failOn === "block" ? verdict === "block" : verdict === "block" || verdict === "warn";
3539
+ }
3540
+ function resolveRepoRoot() {
3541
+ try {
3542
+ return execFileSync5("git", ["rev-parse", "--show-toplevel"], {
3543
+ encoding: "utf8",
3544
+ stdio: ["ignore", "pipe", "ignore"]
3545
+ }).trim();
3546
+ } catch {
3547
+ return null;
3548
+ }
3549
+ }
3550
+ async function runReview(opts) {
3551
+ const meta = {
3552
+ generatedAt: nowIso3(),
3553
+ keyPrefix: "unknown",
3554
+ runRef: opts.runRef,
3555
+ base: opts.base,
3556
+ mcpUrl: opts.mcpUrl ?? process.env.MAIND_MCP_URL ?? "https://mcp.maind.dev/mcp",
3557
+ symbolCount: 0,
3558
+ diffStats: null,
3559
+ gate: opts.gate,
3560
+ failOn: opts.gate ? opts.failOn : null
3561
+ };
3562
+ if (opts.failOn && !opts.gate) {
3563
+ warn5("--fail-on has no effect without --gate (advisory runs always exit 0).");
3564
+ }
3565
+ const failOpen = (reason) => {
3566
+ warn5(`review unavailable, ${opts.strict ? "failing (--strict)" : "continuing"}: ${reason}`);
3567
+ try {
3568
+ writeArtifact3(opts.out, renderFailOpenReview(meta, reason));
3569
+ const manifest = buildReviewManifest({}, null, meta);
3570
+ manifest.error = reason;
3571
+ writeArtifact3(manifestPathFor(opts.out), JSON.stringify(manifest, null, 2) + "\n");
3572
+ if (opts.comment) writeArtifact3(opts.comment, renderFailOpenComment(meta, reason));
3573
+ if (opts.json) process.stdout.write(JSON.stringify(manifest, null, 2) + "\n");
3574
+ } catch (writeErr) {
3575
+ warn5(`could not write fail-open artifacts: ${msg5(writeErr)}`);
3576
+ return opts.strict ? 2 : 1;
3577
+ }
3578
+ return opts.strict ? 2 : 0;
3579
+ };
3580
+ try {
3581
+ const root = resolveRepoRoot();
3582
+ if (!root) {
3583
+ return failOpen("not a git repository \u2014 review is diff-based.");
3584
+ }
3585
+ const engine = createNodeCodeIndex({ repoRoot: root });
3586
+ let result;
3587
+ try {
3588
+ const status2 = await engine.status();
3589
+ if (!status2.indexed || status2.fileCount === 0) {
3590
+ warn5("no local index found \u2014 building it now (first run in this checkout)\u2026");
3591
+ const stats = await engine.indexRepository();
3592
+ warn5(`indexed ${stats.filesParsed} file(s) in ${Math.round(stats.durationMs / 1e3)}s.`);
3593
+ }
3594
+ result = await engine.reviewChanges({
3595
+ since: opts.base ?? void 0,
3596
+ includeUncommitted: opts.base ? opts.uncommitted : true,
3597
+ limit: SYMBOL_CAP
3598
+ });
3599
+ } finally {
3600
+ await engine.close().catch(() => void 0);
3601
+ }
3602
+ const diffStats = {
3603
+ changed_file_count: result.changedFiles,
3604
+ unindexed_file_count: result.changedFiles - result.indexedFiles,
3605
+ stale_file_count: result.staleFiles,
3606
+ truncated: result.truncated,
3607
+ hunk_coverage: result.hunkCoverage,
3608
+ base_kind: result.baseKind,
3609
+ // Attest-Anker (ADR-230 §3): Commit-Objekt-Hashes + dirty — kein Source.
3610
+ head_sha: result.headSha,
3611
+ dirty: result.dirty,
3612
+ base_sha: result.baseSha,
3613
+ code_index_version: CODE_INDEX_VERSION
3614
+ };
3615
+ meta.diffStats = diffStats;
3616
+ meta.symbolCount = result.symbols.length;
3617
+ const summary = opts.summary?.trim() || void 0;
3618
+ if (result.symbols.length === 0 && !summary) {
3619
+ const flags = result.riskFlags;
3620
+ const empty = {
3621
+ ok: true,
3622
+ review: {
3623
+ convention_findings: [],
3624
+ lesson_hints: [],
3625
+ risk_flags: flags,
3626
+ // changed_file_count counts INDEXABLE files; risk flags come from the
3627
+ // raw path list. Both can be non-zero independently — say which it is
3628
+ // instead of claiming "no changes" while a flag is showing.
3629
+ coverage_note: flags.length > 0 ? result.changedFiles === 0 ? "No indexable changed files \u2014 the risk flags below come from non-indexable paths (migrations, workflows, manifests, env files), which the symbol index never sees." : "Changed files found but no indexed symbols \u2014 no symbol-level review. The risk flags below come from the raw path list; pass --summary for an index-less review." : result.changedFiles === 0 ? "No changed files detected." : "Changed files found but no indexed symbols \u2014 nothing reviewable at symbol level."
3630
+ }
3631
+ };
3632
+ writeAll(empty, null, meta, opts);
3633
+ process.stderr.write(
3634
+ `maind review: no reviewable symbols (${result.changedFiles} changed file(s), ${flags.length} risk flag(s)) \u2192 ${opts.out}
3635
+ `
3636
+ );
3637
+ return 0;
3638
+ }
3639
+ const auth = resolveAuth();
3640
+ meta.keyPrefix = auth.keyPrefix;
3641
+ const overrideKey = opts.projectKey?.trim().toLowerCase();
3642
+ const projectKey = overrideKey && PROJECT_KEY_RE3.test(overrideKey) ? overrideKey : gatherGitState().projectKey;
3643
+ const client = new McpClient({
3644
+ apiKey: auth.apiKey,
3645
+ deviceId: auth.deviceId,
3646
+ clientVersion: opts.clientVersion,
3647
+ mcpUrl: opts.mcpUrl ?? void 0,
3648
+ projectKey
3649
+ });
3650
+ const reviewArgs = {
3651
+ changed_symbols: result.symbols.map((s) => ({
3652
+ name: s.name,
3653
+ qualified_name: s.qualifiedName,
3654
+ kind: s.kind,
3655
+ language: s.language,
3656
+ change: s.change,
3657
+ changed_line_count: s.changedLineCount
3658
+ })),
3659
+ risk_flags: result.riskFlags,
3660
+ diff_stats: diffStats,
3661
+ ...summary ? { change_summary: summary } : {},
3662
+ ...projectKey ? { project_key: projectKey } : {}
3663
+ };
3664
+ const review = await client.callTool("review_changes", reviewArgs);
3665
+ if (review.ok !== true || !review.review) {
3666
+ return failOpen(
3667
+ `review_changes rejected: ${review.error ?? "unknown_error"}${review.message ? ` \u2014 ${review.message}` : ""}`
3668
+ );
3669
+ }
3670
+ let verdict = null;
3671
+ if (opts.gate) {
3672
+ const findings = review.review.convention_findings ?? [];
3673
+ const attestArgs = !opts.noReport && result.headSha ? {
3674
+ commit_sha: result.headSha,
3675
+ ...result.baseSha ? { base_sha: result.baseSha } : {},
3676
+ dirty: result.dirty,
3677
+ diff_stats: diffStats,
3678
+ code_index_version: CODE_INDEX_VERSION
3679
+ } : {};
3680
+ try {
3681
+ verdict = await client.callTool("review_verdict", {
3682
+ findings: findings.map((f) => ({
3683
+ convention_id: f.convention_id,
3684
+ convention_version: f.convention_version,
3685
+ matched_symbols: f.matched_symbols,
3686
+ matched_changed_symbols: f.matched_changed_symbols ?? []
3687
+ })),
3688
+ risk_flags: review.review.risk_flags ?? [],
3689
+ ...projectKey ? { project_key: projectKey } : {},
3690
+ ...attestArgs
3691
+ });
3692
+ if (verdict.ok !== true) {
3693
+ if (verdict.error === "requires_team" || verdict.error === "requires_enterprise") {
3694
+ warn5(
3695
+ "gate skipped: review_verdict is a Team-plan feature \u2014 findings below are advisory. " + (verdict.message ?? "Upgrade at https://maind.dev/pricing.")
3696
+ );
3697
+ verdict = null;
3698
+ } else {
3699
+ if (opts.strict) {
3700
+ return failOpen(
3701
+ `review_verdict failed: ${verdict.error ?? "unknown_error"}${verdict.message ? ` \u2014 ${verdict.message}` : ""}`
3702
+ );
3703
+ }
3704
+ warn5(
3705
+ `gate unavailable (continuing advisory): ${verdict.error ?? "unknown_error"}`
3706
+ );
3707
+ verdict = null;
3708
+ }
3709
+ }
3710
+ } catch (err2) {
3711
+ if (opts.strict) return failOpen(`review_verdict failed: ${msg5(err2)}`);
3712
+ warn5(`gate unavailable (continuing advisory): ${msg5(err2)}`);
3713
+ verdict = null;
3714
+ }
3715
+ }
3716
+ writeAll(review, verdict, meta, opts);
3717
+ const findingCount = review.review.convention_findings?.length ?? 0;
3718
+ const runNote = verdict?.run ? ` \xB7 run: ${verdict.run.id.slice(0, 8)}${verdict.run.attestation_grade ? " (attestation-grade)" : ""}` : verdict?.run_note ? ` \xB7 run: none (${verdict.run_note})` : "";
3719
+ const verdictNote = verdict?.verdict ? ` \xB7 verdict: ${verdict.verdict}${runNote}` : "";
3720
+ process.stderr.write(
3721
+ `maind review: ${findingCount} finding(s), ${review.review.risk_flags?.length ?? 0} risk flag(s)${verdictNote} \u2192 ${opts.out}
3722
+ `
3723
+ );
3724
+ if (opts.gate && opts.failOn && verdict?.verdict) {
3725
+ if (verdictCrossesFailOn(verdict.verdict, opts.failOn)) {
3726
+ warn5(
3727
+ `verdict '${verdict.verdict}' crossed --fail-on ${opts.failOn} \u2014 failing the check.`
3728
+ );
3729
+ return 1;
3730
+ }
3731
+ }
3732
+ return 0;
3733
+ } catch (err2) {
3734
+ const reason = err2 instanceof MissingApiKeyError ? err2.message : msg5(err2);
3735
+ return failOpen(reason);
3736
+ }
3737
+ }
3738
+ function writeAll(review, verdict, meta, opts) {
3739
+ writeArtifact3(opts.out, renderReviewMarkdown(review, verdict, meta));
3740
+ const manifest = buildReviewManifest(review, verdict, meta);
3741
+ writeArtifact3(manifestPathFor(opts.out), JSON.stringify(manifest, null, 2) + "\n");
3742
+ if (opts.comment) {
3743
+ writeArtifact3(opts.comment, renderReviewComment(review, verdict, meta, opts.out));
3744
+ }
3745
+ if (opts.json) process.stdout.write(JSON.stringify(manifest, null, 2) + "\n");
3746
+ }
3747
+ function msg5(err2) {
3748
+ return err2 instanceof Error ? err2.message : String(err2);
3749
+ }
3750
+ function warn5(line) {
3751
+ process.stderr.write(`maind review: ${line}
3752
+ `);
3753
+ }
3754
+
3755
+ // src/sync-cmd.ts
3756
+ import { execFileSync as execFileSync6 } from "node:child_process";
3757
+ var CONSENT_POLICY_VERSION = "2026-07-05";
3758
+ var DEFAULT_DASHBOARD_URL = "https://app.maind.dev";
3759
+ function err(line) {
3760
+ process.stderr.write(`maind: ${line}
3761
+ `);
3762
+ }
3763
+ function repoRoot() {
3764
+ try {
3765
+ return execFileSync6("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
3766
+ } catch {
3767
+ return process.cwd();
3768
+ }
3769
+ }
3770
+ function dashboardUrl() {
3771
+ const raw = process.env.MAIND_DASHBOARD_URL?.trim();
3772
+ return (raw && raw.length > 0 ? raw : DEFAULT_DASHBOARD_URL).replace(/\/+$/, "");
3773
+ }
3774
+ function makeSync(json) {
3775
+ let auth;
3776
+ try {
3777
+ auth = resolveAuth();
3778
+ } catch (e) {
3779
+ if (e instanceof MissingApiKeyError) {
3780
+ err(e.message);
3781
+ return null;
3782
+ }
3783
+ throw e;
3784
+ }
3785
+ try {
3786
+ return createCodeIndexSync({
3787
+ repoRoot: repoRoot(),
3788
+ endpoint: dashboardUrl(),
3789
+ apiKey: auth.apiKey,
3790
+ deviceId: auth.deviceId,
3791
+ onProgress: json ? void 0 : (p) => process.stderr.write(` ${p.phase}: ${p.done}/${p.total}\r`)
3792
+ });
3793
+ } catch (e) {
3794
+ err(e instanceof Error ? e.message : String(e));
3795
+ return null;
3796
+ }
3797
+ }
3798
+ function msg6(e) {
3799
+ return e instanceof Error ? e.message : String(e);
3800
+ }
3801
+ function parseSyncArgs(argv) {
3802
+ const positional = argv.find((a) => !a.startsWith("-"));
3803
+ const sub = positional === "status" || positional === "enable" || positional === "disable" ? positional : "push";
3804
+ return { sub, json: argv.includes("--json") };
3805
+ }
3806
+ async function runSync(opts) {
3807
+ const sync = makeSync(opts.json);
3808
+ if (!sync) return 1;
3809
+ try {
3810
+ switch (opts.sub) {
3811
+ case "enable":
3812
+ return await enable(sync);
3813
+ case "disable":
3814
+ return await disable(sync);
3815
+ case "status":
3816
+ return await status(sync, opts.json);
3817
+ default:
3818
+ return await push(sync, opts.json);
3819
+ }
3820
+ } catch (e) {
3821
+ if (e instanceof SyncError && e.code === "consent_required") {
3822
+ err("codebase cloud-sync is not enabled \u2014 run `maind sync enable` first.");
3823
+ return 1;
3824
+ }
3825
+ err(`sync failed: ${msg6(e)}`);
3826
+ return 1;
3827
+ } finally {
3828
+ await sync.close().catch(() => {
3829
+ });
3830
+ }
3831
+ }
3832
+ async function enable(sync) {
3833
+ await sync.setConsent(true, CONSENT_POLICY_VERSION);
3834
+ process.stdout.write(
3835
+ "codebase cloud-sync enabled. Structured rows (symbol names, topology, hashes) will be pushed on `maind sync` \u2014 never source, snippets, or plaintext paths.\n"
3836
+ );
3837
+ return 0;
3838
+ }
3839
+ async function disable(sync) {
3840
+ await sync.setConsent(false);
3841
+ process.stdout.write("codebase cloud-sync disabled.\n");
3842
+ return 0;
3843
+ }
3844
+ async function status(sync, json) {
3845
+ const local = await sync.localStatus();
3846
+ const remote = await sync.remoteStatus();
3847
+ if (json) {
3848
+ process.stdout.write(JSON.stringify({ ok: true, local, remote }, null, 2) + "\n");
3849
+ return 0;
3850
+ }
3851
+ process.stdout.write(
3852
+ `local: ${local.indexed ? `${local.nodeCount} symbols \xB7 ${local.edgeCount} edges \xB7 ${local.fileCount} files` : "not indexed"}${local.lastSyncedAt ? ` \xB7 last synced ${new Date(local.lastSyncedAt).toISOString()}` : ""}
3853
+ cloud: ${remote.indexed ? `${remote.node_count ?? "?"} symbols \xB7 ${remote.edge_count ?? "?"} edges` : "not synced"}
3854
+ `
3855
+ );
3856
+ return 0;
3857
+ }
3858
+ async function push(sync, json) {
3859
+ const local = await sync.localStatus();
3860
+ if (!local.indexed || local.nodeCount === 0) {
3861
+ err("no local index found \u2014 run `maind index` first.");
3862
+ return 1;
3863
+ }
3864
+ const res = await sync.push();
3865
+ process.stderr.write("\n");
3866
+ if (json) {
3867
+ process.stdout.write(JSON.stringify({ ok: true, ...res }, null, 2) + "\n");
3868
+ return 0;
3869
+ }
3870
+ process.stdout.write(
3871
+ `synced ${res.nodes} symbols + ${res.edges} edges to the cloud${res.embeddings ? ` + ${res.embeddings} vectors` : ""}${res.pruned ? ` (pruned ${res.pruned} stale)` : ""}${res.headCommit ? ` @ ${res.headCommit.slice(0, 8)}` : ""}.
3872
+ `
3873
+ );
3874
+ return 0;
3875
+ }
3876
+
3877
+ // src/index.ts
3878
+ var VERSION = "0.7.0";
3879
+ var HELP = `maind ${VERSION} \u2014 headless maind CLI for CI + git context
22
3880
 
23
3881
  Usage:
24
3882
  maind context [options] Pull stack-relevant lessons + conventions into the runner
@@ -38,10 +3896,10 @@ Usage:
38
3896
  maind --help Show this help
39
3897
  maind --version Print version
40
3898
 
41
- review options (advisory by default — exits 0 unless --gate + --fail-on):
3899
+ review options (advisory by default \u2014 exits 0 unless --gate + --fail-on):
42
3900
  --base <ref> Review the diff against this git ref (e.g. origin/main)
43
3901
  --uncommitted With --base: also include uncommitted changes (default without --base)
44
- --summary <text> Prose change summary — enables index-less review, sharpens hints
3902
+ --summary <text> Prose change summary \u2014 enables index-less review, sharpens hints
45
3903
  --out <path> Findings report markdown (default .maind/review.md; always written,
46
3904
  a manifest JSON lands next to it as review.json)
47
3905
  --comment [<path>] Also write PR-comment markdown (default .maind/review-comment.md);
@@ -50,35 +3908,36 @@ review options (advisory by default — exits 0 unless --gate + --fail-on):
50
3908
  --gate Aggregate findings into a server-side pass|warn|block verdict (Team plan)
51
3909
  --fail-on block|warn With --gate: exit 1 when the verdict crosses this threshold
52
3910
  --strict Infra errors exit 2 instead of failing open with exit 0
53
- --no-report Do not report run counts to maind (reserved; run reporting
54
- lands with the review_runs ingest in a later release)
3911
+ --no-report With --gate: omit the commit anchors so the server does NOT
3912
+ persist a review_runs attestation record (ADR-230) \u2014 the
3913
+ verdict is then advisory-only, without a replayable proof
55
3914
  --project-key <hex> Repo project_key override (default: auto-detect from git)
56
- --mcp-url <url> Override MCP endpoint (default \$MAIND_MCP_URL or production)
3915
+ --mcp-url <url> Override MCP endpoint (default $MAIND_MCP_URL or production)
57
3916
 
58
3917
  git-context options (run at session-start; reads local git + gh, all fail-open):
59
3918
  --out <path> Briefing markdown output (default .maind/git-context.md)
60
3919
  --manifest <path> Optional structured JSON output (git_context manifest)
61
- --no-mcp Local git state only — skip peer-conflict + lesson enrichment
62
- --mcp-url <url> Override MCP endpoint (default \$MAIND_MCP_URL or production)
3920
+ --no-mcp Local git state only \u2014 skip peer-conflict + lesson enrichment
3921
+ --mcp-url <url> Override MCP endpoint (default $MAIND_MCP_URL or production)
63
3922
 
64
3923
  ci-report options (run in an on-failure step; the agent authors the lesson first):
65
- --lesson <path> Agent-authored lesson markdown (frontmatter + body) — required
66
- --rationale <text> Why this is a reusable lesson — required
67
- --failure-type <t> e.g. tsc-error, test-failure, lint-error — required
3924
+ --lesson <path> Agent-authored lesson markdown (frontmatter + body) \u2014 required
3925
+ --rationale <text> Why this is a reusable lesson \u2014 required
3926
+ --failure-type <t> e.g. tsc-error, test-failure, lint-error \u2014 required
68
3927
  --signature <s> Stable dedup key (default: failure-type + lesson hash)
69
3928
  --summary <text> Short failure description
70
3929
  --remediation <text> Suggested fix
71
3930
  --logs <path> Optional failure-log file (truncated to 8k)
72
3931
  --provider <p> CI provider (default github-actions)
73
3932
  --project-key <hex> Repo project_key for org-private repo-scoping (default: auto-detect from git)
74
- --report-url <url> Override ingest endpoint (default \$MAIND_REPORT_URL or prod)
3933
+ --report-url <url> Override ingest endpoint (default $MAIND_REPORT_URL or prod)
75
3934
 
76
3935
  ci-result options (run at the end of a job, success OR failure):
77
- --outcome <s> Build outcome, e.g. \${{ job.status }} (success|failure) — required
3936
+ --outcome <s> Build outcome, e.g. \${{ job.status }} (success|failure) \u2014 required
78
3937
  --manifest <path> Served-lesson manifest from 'maind context' (default .maind/manifest.json)
79
- --run-ref <id> Build identifier (default manifest.run_ref or \$GITHUB_RUN_ID)
3938
+ --run-ref <id> Build identifier (default manifest.run_ref or $GITHUB_RUN_ID)
80
3939
  --run-url <url> Link to the CI run (default derived from GitHub env)
81
- --result-url <url> Override ingest endpoint (default \$MAIND_RESULT_URL or prod)
3940
+ --result-url <url> Override ingest endpoint (default $MAIND_RESULT_URL or prod)
82
3941
 
83
3942
  context options:
84
3943
  --stack <a,b> Stack hint (seeds the platforms filter), e.g. nextjs,supabase
@@ -89,8 +3948,8 @@ context options:
89
3948
  --budget <n> Max lessons to pull (1-50, default 15; server caps search at 10)
90
3949
  --out <path> Context markdown output (default .maind/context.md)
91
3950
  --manifest <path> Manifest JSON output (default .maind/manifest.json)
92
- --run-ref <id> Build identifier for the manifest (default \$GITHUB_RUN_ID)
93
- --mcp-url <url> Override MCP endpoint (default \$MAIND_MCP_URL or production)
3951
+ --run-ref <id> Build identifier for the manifest (default $GITHUB_RUN_ID)
3952
+ --mcp-url <url> Override MCP endpoint (default $MAIND_MCP_URL or production)
94
3953
 
95
3954
  Auth:
96
3955
  Reads MAIND_API_KEY (a read-scoped CI key from https://maind.dev/keys), or falls
@@ -98,77 +3957,68 @@ Auth:
98
3957
  error it still writes explanatory artifacts and exits 0 so the build proceeds.
99
3958
  `;
100
3959
  async function main() {
101
- const argv = process.argv.slice(2);
102
- const cmd = argv[0];
103
- if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
104
- process.stdout.write(HELP);
105
- return 0;
106
- }
107
- if (cmd === "--version" || cmd === "-v") {
108
- process.stdout.write(VERSION + "\n");
109
- return 0;
110
- }
111
- if (cmd === "context") {
112
- return runContext(parseContextArgs(argv.slice(1)));
113
- }
114
- if (cmd === "git-context") {
115
- return runGitContext(parseGitContextArgs(argv.slice(1)));
116
- }
117
- if (cmd === "project-key") {
118
- // Prints the repo's project_key (sha256 of the normalized remote) for use in
119
- // repo_keys when authoring repo-scoped org-private content. The raw URL is
120
- // never printed — only the hash. STDOUT so it can be piped/captured.
121
- const key = gatherGitState().projectKey;
122
- if (!key) {
123
- process.stderr.write("maind: not a git repo with a remote — no project_key.\n");
124
- return 1;
125
- }
126
- process.stdout.write(key + "\n");
127
- return 0;
128
- }
129
- // ONNX-runtime teardown isolation: `index --embed` and `index-semantic` use the
130
- // embedder, which aborts the process on teardown (see index-cmd.ts). Re-exec
131
- // them in a child so this process exits cleanly — unless we ARE that child.
132
- const usesOrt = (cmd === "index" && argv.includes("--embed")) || cmd === "index-semantic";
133
- if (usesOrt && !isOrtWorker(argv)) {
134
- return runOrtIsolated(argv);
3960
+ const argv = process.argv.slice(2);
3961
+ const cmd = argv[0];
3962
+ if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
3963
+ process.stdout.write(HELP);
3964
+ return 0;
3965
+ }
3966
+ if (cmd === "--version" || cmd === "-v") {
3967
+ process.stdout.write(VERSION + "\n");
3968
+ return 0;
3969
+ }
3970
+ if (cmd === "context") {
3971
+ return runContext(parseContextArgs(argv.slice(1)));
3972
+ }
3973
+ if (cmd === "git-context") {
3974
+ return runGitContext(parseGitContextArgs(argv.slice(1)));
3975
+ }
3976
+ if (cmd === "project-key") {
3977
+ const key = gatherGitState().projectKey;
3978
+ if (!key) {
3979
+ process.stderr.write("maind: not a git repo with a remote \u2014 no project_key.\n");
3980
+ return 1;
135
3981
  }
136
- if (cmd === "index") {
137
- return runIndex(parseIndexArgs(argv.slice(1)));
138
- }
139
- if (cmd === "index-status") {
140
- return runIndexStatus(parseIndexArgs(argv.slice(1)));
141
- }
142
- if (cmd === "index-search") {
143
- return runIndexSearch(parseIndexSearchArgs(argv.slice(1)));
144
- }
145
- if (cmd === "index-semantic") {
146
- return runIndexSemantic(parseIndexSearchArgs(argv.slice(1)));
147
- }
148
- if (cmd === "index-arch") {
149
- return runIndexArch(parseIndexArgs(argv.slice(1)));
150
- }
151
- if (cmd === "sync") {
152
- return runSync(parseSyncArgs(argv.slice(1)));
153
- }
154
- if (cmd === "review") {
155
- return runReview(parseReviewArgs(argv.slice(1)));
156
- }
157
- if (cmd === "ci-report") {
158
- return runCiReport(parseCiReportArgs(argv.slice(1)));
159
- }
160
- if (cmd === "ci-result") {
161
- return runCiResult(parseCiResultArgs(argv.slice(1)));
162
- }
163
- process.stderr.write(`maind: unknown command '${cmd}'. Run 'maind --help'.\n`);
164
- return 2;
3982
+ process.stdout.write(key + "\n");
3983
+ return 0;
3984
+ }
3985
+ const usesOrt = cmd === "index" && argv.includes("--embed") || cmd === "index-semantic";
3986
+ if (usesOrt && !isOrtWorker(argv)) {
3987
+ return runOrtIsolated(argv);
3988
+ }
3989
+ if (cmd === "index") {
3990
+ return runIndex(parseIndexArgs(argv.slice(1)));
3991
+ }
3992
+ if (cmd === "index-status") {
3993
+ return runIndexStatus(parseIndexArgs(argv.slice(1)));
3994
+ }
3995
+ if (cmd === "index-search") {
3996
+ return runIndexSearch(parseIndexSearchArgs(argv.slice(1)));
3997
+ }
3998
+ if (cmd === "index-semantic") {
3999
+ return runIndexSemantic(parseIndexSearchArgs(argv.slice(1)));
4000
+ }
4001
+ if (cmd === "index-arch") {
4002
+ return runIndexArch(parseIndexArgs(argv.slice(1)));
4003
+ }
4004
+ if (cmd === "sync") {
4005
+ return runSync(parseSyncArgs(argv.slice(1)));
4006
+ }
4007
+ if (cmd === "review") {
4008
+ return runReview(parseReviewArgs(argv.slice(1)));
4009
+ }
4010
+ if (cmd === "ci-report") {
4011
+ return runCiReport(parseCiReportArgs(argv.slice(1)));
4012
+ }
4013
+ if (cmd === "ci-result") {
4014
+ return runCiResult(parseCiResultArgs(argv.slice(1)));
4015
+ }
4016
+ process.stderr.write(`maind: unknown command '${cmd}'. Run 'maind --help'.
4017
+ `);
4018
+ return 2;
165
4019
  }
166
- main()
167
- .then((code) => process.exit(code))
168
- .catch((err) => {
169
- // Defensive backstop — runContext is already fail-open, so reaching here means
170
- // an unexpected programming error. Surface it on stderr and exit non-zero.
171
- process.stderr.write(`maind: fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
172
- process.exit(1);
4020
+ main().then((code) => process.exit(code)).catch((err2) => {
4021
+ process.stderr.write(`maind: fatal: ${err2 instanceof Error ? err2.stack ?? err2.message : String(err2)}
4022
+ `);
4023
+ process.exit(1);
173
4024
  });
174
- //# sourceMappingURL=index.js.map