@maind-dev/cli 0.6.0 → 0.6.1

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,3833 @@
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
+ function runGit(repoRoot2, args) {
1576
+ try {
1577
+ return execFileSync3("git", args, {
1578
+ cwd: repoRoot2,
1579
+ encoding: "utf8",
1580
+ stdio: ["ignore", "pipe", "ignore"],
1581
+ timeout: 15e3,
1582
+ maxBuffer: 64 * 1024 * 1024
1583
+ });
1584
+ } catch {
1585
+ return null;
1586
+ }
1587
+ }
1588
+ var git2 = runGit;
1589
+ function toPosix(p) {
1590
+ return sep === "/" ? p : p.split(sep).join("/");
1591
+ }
1592
+ function classify(paths) {
1593
+ const out = [];
1594
+ for (const raw of paths) {
1595
+ const path = toPosix(raw).trim();
1596
+ if (!path || isSkippablePath(path))
1597
+ continue;
1598
+ const language = languageForPath(path);
1599
+ if (language)
1600
+ out.push({ path, language });
1601
+ }
1602
+ return out;
1603
+ }
1604
+ function classifyPaths(paths) {
1605
+ return classify(paths);
1606
+ }
1607
+ function discoverSourceFiles(repoRoot2) {
1608
+ const tracked = git2(repoRoot2, ["ls-files", "--cached", "--others", "--exclude-standard"]);
1609
+ if (tracked !== null) {
1610
+ return classify(tracked.split("\n"));
1611
+ }
1612
+ const acc = [];
1613
+ walk(repoRoot2, "", acc);
1614
+ return classify(acc);
1615
+ }
1616
+ function walk(root, rel, acc) {
1617
+ let entries;
1618
+ try {
1619
+ entries = readdirSync(posix2.join(root, rel), { withFileTypes: true });
1620
+ } catch {
1621
+ return;
1622
+ }
1623
+ for (const entry of entries) {
1624
+ if (entry.name.startsWith(".") && entry.name !== ".") {
1625
+ if (IGNORE_DIRS.has(entry.name))
1626
+ continue;
1627
+ }
1628
+ if (IGNORE_DIRS.has(entry.name))
1629
+ continue;
1630
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
1631
+ if (entry.isDirectory())
1632
+ walk(root, childRel, acc);
1633
+ else if (entry.isFile())
1634
+ acc.push(childRel);
1635
+ }
1636
+ }
1637
+ function enumerateChangedPaths(repoRoot2, opts = {}) {
1638
+ if (headCommit(repoRoot2) === null)
1639
+ return null;
1640
+ const includeUncommitted = opts.includeUncommitted !== false;
1641
+ const ref = opts.since ?? "HEAD";
1642
+ const diffArgs = ["diff", "--name-status", "--diff-filter=d", ref];
1643
+ if (!includeUncommitted) {
1644
+ if (!opts.since) {
1645
+ throw new Error("nothing to enumerate: include_uncommitted=false requires a `since` ref");
1646
+ }
1647
+ diffArgs.push("HEAD");
1648
+ }
1649
+ const diff = git2(repoRoot2, diffArgs);
1650
+ if (diff === null) {
1651
+ throw new Error(`git diff failed \u2014 is '${ref}' a valid ref?`);
1652
+ }
1653
+ const paths = /* @__PURE__ */ new Map();
1654
+ for (const line of diff.split("\n")) {
1655
+ if (!line.trim())
1656
+ continue;
1657
+ const cols = line.split(" ");
1658
+ if (cols.length < 2)
1659
+ continue;
1660
+ const code = cols[0][0];
1661
+ const p = unquote((cols.length >= 3 ? cols[cols.length - 1] : cols[1]).trim());
1662
+ if (!p)
1663
+ continue;
1664
+ if (code === "A" || code === "C")
1665
+ paths.set(p, "added");
1666
+ else if (code === "R")
1667
+ paths.set(p, "renamed");
1668
+ else
1669
+ paths.set(p, "modified");
1670
+ }
1671
+ if (includeUncommitted) {
1672
+ const status2 = git2(repoRoot2, ["status", "--porcelain", "-uall"]) ?? "";
1673
+ for (const line of status2.split("\n")) {
1674
+ if (line.length < 4)
1675
+ continue;
1676
+ const x = line[0];
1677
+ const y = line[1];
1678
+ if (x === "D" || y === "D")
1679
+ continue;
1680
+ let p = line.slice(3);
1681
+ const arrow = p.indexOf(" -> ");
1682
+ if (arrow >= 0)
1683
+ p = p.slice(arrow + 4);
1684
+ p = unquote(p);
1685
+ if (!p || paths.has(p))
1686
+ continue;
1687
+ if (x === "?" || x === "A" || y === "A")
1688
+ paths.set(p, "added");
1689
+ else if (x === "R")
1690
+ paths.set(p, "renamed");
1691
+ else
1692
+ paths.set(p, "modified");
1693
+ }
1694
+ }
1695
+ return paths;
1696
+ }
1697
+ function unquote(p) {
1698
+ return p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
1699
+ }
1700
+ function changedSourceFiles(repoRoot2, opts = {}) {
1701
+ const paths = enumerateChangedPaths(repoRoot2, opts);
1702
+ if (paths === null)
1703
+ return null;
1704
+ return classify(paths.keys());
1705
+ }
1706
+ function statFile(repoRoot2, relPath) {
1707
+ try {
1708
+ const s = statSync(posix2.join(repoRoot2, relPath));
1709
+ return { mtimeMs: Math.floor(s.mtimeMs), size: s.size };
1710
+ } catch {
1711
+ return null;
1712
+ }
1713
+ }
1714
+ function hashContent(content) {
1715
+ return createHash3("sha256").update(content).digest("hex");
1716
+ }
1717
+ function headCommit(repoRoot2) {
1718
+ return git2(repoRoot2, ["rev-parse", "HEAD"])?.trim() ?? null;
1719
+ }
1720
+
1721
+ // ../../packages/code-index/build/incremental/hunks.js
1722
+ var MAX_DIFF_BYTES = 16 * 1024 * 1024;
1723
+ var MAX_HUNKS_PER_FILE = 500;
1724
+ var MAX_ADDED_LINES_PER_FILE = 4e3;
1725
+ var MAX_UNTRACKED_HEAD_BYTES = 256 * 1024;
1726
+ var HUNK_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
1727
+ function changedHunks(repoRoot2, opts = {}) {
1728
+ if (runGit(repoRoot2, ["rev-parse", "HEAD"]) === null)
1729
+ return null;
1730
+ const includeUncommitted = opts.includeUncommitted !== false;
1731
+ const ref = opts.since ?? "HEAD";
1732
+ const diffArgs = [
1733
+ "diff",
1734
+ "--unified=0",
1735
+ "--no-color",
1736
+ "--no-ext-diff",
1737
+ "--find-renames",
1738
+ "--diff-filter=d",
1739
+ ref
1740
+ ];
1741
+ if (!includeUncommitted) {
1742
+ if (!opts.since) {
1743
+ throw new Error("nothing to enumerate: include_uncommitted=false requires a `since` ref");
1744
+ }
1745
+ diffArgs.push("HEAD");
1746
+ }
1747
+ const diff = runGit(repoRoot2, diffArgs);
1748
+ if (diff === null) {
1749
+ throw new Error(`git diff failed \u2014 is '${ref}' a valid ref?`);
1750
+ }
1751
+ const files = /* @__PURE__ */ new Map();
1752
+ let coverage = "full";
1753
+ if (Buffer.byteLength(diff, "utf8") > MAX_DIFF_BYTES) {
1754
+ coverage = "none";
1755
+ } else {
1756
+ parseUnifiedDiff(diff, files, () => {
1757
+ coverage = "partial";
1758
+ });
1759
+ }
1760
+ if (includeUncommitted) {
1761
+ const status2 = runGit(repoRoot2, ["status", "--porcelain", "-uall"]) ?? "";
1762
+ for (const line of status2.split("\n")) {
1763
+ if (line.length < 4 || line[0] !== "?")
1764
+ continue;
1765
+ let p = line.slice(3);
1766
+ if (p.startsWith('"') && p.endsWith('"'))
1767
+ p = p.slice(1, -1);
1768
+ if (!p || files.has(p))
1769
+ continue;
1770
+ files.set(p, {
1771
+ path: p,
1772
+ status: "added",
1773
+ binary: false,
1774
+ wholeFile: true,
1775
+ degraded: false,
1776
+ addedRanges: [],
1777
+ deletionPoints: [],
1778
+ addedLines: []
1779
+ });
1780
+ }
1781
+ }
1782
+ return { files, coverage };
1783
+ }
1784
+ function parseUnifiedDiff(diff, files, onDegrade) {
1785
+ let current = null;
1786
+ let hunkCount = 0;
1787
+ let pendingRename = false;
1788
+ let pendingNewFile = false;
1789
+ const commit = () => {
1790
+ if (current)
1791
+ files.set(current.path, current);
1792
+ current = null;
1793
+ hunkCount = 0;
1794
+ };
1795
+ for (const line of diff.split("\n")) {
1796
+ if (line.startsWith("diff --git ")) {
1797
+ commit();
1798
+ pendingRename = false;
1799
+ pendingNewFile = false;
1800
+ continue;
1801
+ }
1802
+ if (line.startsWith("new file mode")) {
1803
+ pendingNewFile = true;
1804
+ continue;
1805
+ }
1806
+ if (line.startsWith("rename to ")) {
1807
+ pendingRename = true;
1808
+ continue;
1809
+ }
1810
+ if (line.startsWith("Binary files ") || line.startsWith("GIT binary patch")) {
1811
+ if (current)
1812
+ current.binary = true;
1813
+ continue;
1814
+ }
1815
+ if (line.startsWith("+++ ")) {
1816
+ let p = line.slice(4).trim();
1817
+ if (p.startsWith('"') && p.endsWith('"'))
1818
+ p = p.slice(1, -1);
1819
+ if (p.startsWith("b/"))
1820
+ p = p.slice(2);
1821
+ if (p === "/dev/null")
1822
+ continue;
1823
+ current = {
1824
+ path: p,
1825
+ status: pendingNewFile ? "added" : pendingRename ? "renamed" : "modified",
1826
+ binary: false,
1827
+ wholeFile: false,
1828
+ degraded: false,
1829
+ addedRanges: [],
1830
+ deletionPoints: [],
1831
+ addedLines: []
1832
+ };
1833
+ continue;
1834
+ }
1835
+ if (!current)
1836
+ continue;
1837
+ const m = HUNK_RE.exec(line);
1838
+ if (m) {
1839
+ hunkCount++;
1840
+ if (hunkCount > MAX_HUNKS_PER_FILE) {
1841
+ if (!current.degraded) {
1842
+ current.degraded = true;
1843
+ current.addedRanges = [];
1844
+ current.deletionPoints = [];
1845
+ current.addedLines = [];
1846
+ onDegrade();
1847
+ }
1848
+ continue;
1849
+ }
1850
+ const start = Number(m[1]);
1851
+ const count = m[2] === void 0 ? 1 : Number(m[2]);
1852
+ if (count > 0)
1853
+ current.addedRanges.push({ start, end: start + count - 1 });
1854
+ else
1855
+ current.deletionPoints.push(start);
1856
+ continue;
1857
+ }
1858
+ if (line.startsWith("+") && !line.startsWith("+++") && !current.degraded && current.addedLines.length < MAX_ADDED_LINES_PER_FILE) {
1859
+ current.addedLines.push(line.slice(1));
1860
+ }
1861
+ }
1862
+ commit();
1863
+ }
1864
+
1865
+ // ../../packages/code-index/build/review/risk-rules.js
1866
+ var RISK_RULES = [
1867
+ {
1868
+ flag: "sql_migration_added",
1869
+ path: /(^|\/)migrations\/[^/]+\.sql$/i,
1870
+ onlyWhenAdded: true
1871
+ },
1872
+ {
1873
+ flag: "security_definer_touched",
1874
+ path: /\.sql$/i,
1875
+ content: /\bSECURITY\s+DEFINER\b/i
1876
+ },
1877
+ {
1878
+ flag: "rls_policy_changed",
1879
+ path: /\.sql$/i,
1880
+ content: /\b(CREATE|ALTER|DROP)\s+POLICY\b|\bROW\s+LEVEL\s+SECURITY\b/i
1881
+ },
1882
+ {
1883
+ flag: "auth_config_changed",
1884
+ path: /(^|\/)supabase\/config\.toml$|(^|\/)(middleware|proxy)\.(ts|js)$/
1885
+ },
1886
+ {
1887
+ flag: "package_manifest_changed",
1888
+ path: /(^|\/)(package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|requirements\.txt|pyproject\.toml|Cargo\.toml|go\.mod)$/
1889
+ },
1890
+ {
1891
+ flag: "ci_workflow_changed",
1892
+ path: /^\.github\/workflows\/[^/]+\.ya?ml$|^\.gitlab-ci\.yml$/
1893
+ },
1894
+ {
1895
+ flag: "env_or_secrets_file",
1896
+ path: /(^|\/)\.env(\.[^/]+)?$|\.(pem|key)$/
1897
+ }
1898
+ ];
1899
+
1900
+ // ../../packages/code-index/build/review/risk-scan.js
1901
+ function scanRiskFlags(input) {
1902
+ const flags = /* @__PURE__ */ new Set();
1903
+ for (const [path, status2] of input.paths) {
1904
+ for (const rule of RISK_RULES) {
1905
+ if (flags.has(rule.flag))
1906
+ continue;
1907
+ if (!rule.path.test(path))
1908
+ continue;
1909
+ if (rule.onlyWhenAdded && status2 !== "added")
1910
+ continue;
1911
+ if (!rule.content) {
1912
+ flags.add(rule.flag);
1913
+ continue;
1914
+ }
1915
+ const text = contentForPath(path, input);
1916
+ if (text !== null && rule.content.test(text))
1917
+ flags.add(rule.flag);
1918
+ }
1919
+ }
1920
+ return [...flags].sort();
1921
+ }
1922
+ function contentForPath(path, input) {
1923
+ const hunks = input.hunks.get(path);
1924
+ if (hunks && !hunks.wholeFile && !hunks.degraded) {
1925
+ return hunks.addedLines.join("\n");
1926
+ }
1927
+ return input.readUntracked(path);
1928
+ }
1929
+
1930
+ // ../../packages/code-index/build/store/schema.js
1931
+ var SCHEMA_VERSION = "1";
1932
+ var SCHEMA_SQL = (
1933
+ /* sql */
1934
+ `
1935
+ CREATE TABLE IF NOT EXISTS meta (
1936
+ key TEXT PRIMARY KEY,
1937
+ value TEXT NOT NULL
1938
+ );
1939
+
1940
+ CREATE TABLE IF NOT EXISTS files (
1941
+ path TEXT PRIMARY KEY,
1942
+ language TEXT NOT NULL,
1943
+ content_hash TEXT NOT NULL,
1944
+ mtime_ms INTEGER NOT NULL,
1945
+ size INTEGER NOT NULL,
1946
+ indexed_at INTEGER NOT NULL
1947
+ );
1948
+
1949
+ CREATE TABLE IF NOT EXISTS nodes (
1950
+ node_key TEXT PRIMARY KEY,
1951
+ kind TEXT NOT NULL,
1952
+ name TEXT NOT NULL,
1953
+ qualified_name TEXT NOT NULL,
1954
+ file_path TEXT NOT NULL,
1955
+ language TEXT NOT NULL,
1956
+ start_line INTEGER NOT NULL,
1957
+ end_line INTEGER NOT NULL,
1958
+ start_byte INTEGER NOT NULL,
1959
+ end_byte INTEGER NOT NULL,
1960
+ signature TEXT,
1961
+ doc TEXT
1962
+ );
1963
+ CREATE INDEX IF NOT EXISTS nodes_file_idx ON nodes(file_path);
1964
+ CREATE INDEX IF NOT EXISTS nodes_kind_idx ON nodes(kind);
1965
+ CREATE INDEX IF NOT EXISTS nodes_name_idx ON nodes(name);
1966
+
1967
+ CREATE TABLE IF NOT EXISTS edges (
1968
+ src_key TEXT NOT NULL,
1969
+ dst_key TEXT NOT NULL,
1970
+ kind TEXT NOT NULL,
1971
+ PRIMARY KEY (src_key, dst_key, kind)
1972
+ );
1973
+ CREATE INDEX IF NOT EXISTS edges_dst_idx ON edges(dst_key, kind);
1974
+ CREATE INDEX IF NOT EXISTS edges_src_idx ON edges(src_key, kind);
1975
+
1976
+ -- Standalone (self-managed) FTS5 table \u2014 we populate/delete it in the same
1977
+ -- transaction as nodes, avoiding external-content trigger bookkeeping. The
1978
+ -- search_text column carries name + camel/snake-split tokens + doc + signature
1979
+ -- so unicode61 can match identifier fragments.
1980
+ CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
1981
+ node_key UNINDEXED,
1982
+ search_text,
1983
+ tokenize = 'unicode61'
1984
+ );
1985
+
1986
+ -- Reserved for Phase 2 (semantic). Unused in Phase 1.
1987
+ CREATE TABLE IF NOT EXISTS embeddings (
1988
+ node_key TEXT PRIMARY KEY,
1989
+ dim INTEGER NOT NULL,
1990
+ vec BLOB NOT NULL,
1991
+ content_hash TEXT NOT NULL
1992
+ );
1993
+ `
1994
+ );
1995
+ var META_SCHEMA_VERSION = "schema_version";
1996
+ var META_LAST_INDEX_COMMIT = "last_index_commit";
1997
+ var META_LAST_INDEXED_AT = "last_indexed_at";
1998
+ var META_EMBED_RECIPE_VERSION = "embed_recipe_version";
1999
+ var META_EMBED_MODEL = "embed_model";
2000
+ var META_EMBED_DIM = "embed_dim";
2001
+ var META_LAST_SYNCED_AT = "last_synced_at";
2002
+ var META_LAST_SYNCED_COMMIT = "last_synced_commit";
2003
+
2004
+ // ../../packages/code-index/build/util/identifiers.js
2005
+ function splitIdentifier(id) {
2006
+ 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);
2007
+ }
2008
+ function buildSearchText(parts) {
2009
+ const frags = /* @__PURE__ */ new Set();
2010
+ frags.add(parts.name.toLowerCase());
2011
+ frags.add(parts.qualifiedName.toLowerCase());
2012
+ for (const f of splitIdentifier(parts.name))
2013
+ frags.add(f);
2014
+ for (const f of splitIdentifier(parts.qualifiedName))
2015
+ frags.add(f);
2016
+ const extra = [parts.doc ?? "", parts.signature ?? ""].join(" ").trim();
2017
+ const base = [...frags].join(" ");
2018
+ return extra ? `${base} ${extra}` : base;
2019
+ }
2020
+ function toFtsMatchQuery(query) {
2021
+ const frags = /* @__PURE__ */ new Set();
2022
+ for (const raw of query.split(/\s+/)) {
2023
+ const cleaned = raw.replace(/[^A-Za-z0-9]+/g, "");
2024
+ if (cleaned.length === 0)
2025
+ continue;
2026
+ frags.add(cleaned.toLowerCase());
2027
+ for (const f of splitIdentifier(cleaned))
2028
+ frags.add(f);
2029
+ }
2030
+ if (frags.size === 0)
2031
+ return null;
2032
+ return [...frags].map((f) => `${f}*`).join(" OR ");
2033
+ }
2034
+
2035
+ // ../../packages/code-index/build/query/engine.js
2036
+ var CodeIndexEngine = class {
2037
+ store;
2038
+ parser;
2039
+ repoRoot;
2040
+ embedder;
2041
+ constructor(deps) {
2042
+ this.store = deps.store;
2043
+ this.parser = deps.parser;
2044
+ this.repoRoot = deps.repoRoot;
2045
+ this.embedder = deps.embedder ?? null;
2046
+ }
2047
+ async indexRepository(opts = {}) {
2048
+ const started = opts.now ?? Date.now();
2049
+ await this.store.init();
2050
+ await this.parser.init();
2051
+ const storedVersion = await this.store.getMeta(META_SCHEMA_VERSION);
2052
+ const hadIndex = await this.store.countFiles() > 0;
2053
+ if (opts.full || hadIndex && storedVersion !== SCHEMA_VERSION) {
2054
+ await this.wipe();
2055
+ }
2056
+ const discovered = discoverSourceFiles(this.repoRoot);
2057
+ const discoveredPaths = new Set(discovered.map((d) => d.path));
2058
+ const storedFiles = await this.store.getAllFiles();
2059
+ const storedByPath = new Map(storedFiles.map((f) => [f.path, f]));
2060
+ const incremental = storedFiles.length > 0 && !opts.full;
2061
+ const stats = {
2062
+ filesScanned: discovered.length,
2063
+ filesParsed: 0,
2064
+ filesSkipped: 0,
2065
+ filesDeleted: 0,
2066
+ nodeCount: 0,
2067
+ edgeCount: 0,
2068
+ durationMs: 0,
2069
+ incremental
2070
+ };
2071
+ for (const f of storedFiles) {
2072
+ if (!discoveredPaths.has(f.path)) {
2073
+ await this.store.deleteFile(f.path);
2074
+ stats.filesDeleted++;
2075
+ }
2076
+ }
2077
+ const parsedFiles = [];
2078
+ let done = 0;
2079
+ for (const d of discovered) {
2080
+ done++;
2081
+ const st = statFile(this.repoRoot, d.path);
2082
+ if (!st) {
2083
+ stats.filesSkipped++;
2084
+ continue;
2085
+ }
2086
+ const prior = storedByPath.get(d.path);
2087
+ if (prior && prior.mtimeMs === st.mtimeMs && prior.size === st.size) {
2088
+ stats.filesSkipped++;
2089
+ continue;
2090
+ }
2091
+ const content = this.readRepoFile(d.path);
2092
+ if (content === null) {
2093
+ stats.filesSkipped++;
2094
+ continue;
2095
+ }
2096
+ const contentHash = hashContent(content);
2097
+ if (prior && prior.contentHash === contentHash) {
2098
+ await this.store.upsertFile({ ...prior, mtimeMs: st.mtimeMs, indexedAt: started });
2099
+ stats.filesSkipped++;
2100
+ continue;
2101
+ }
2102
+ const parsed = await this.parser.parse(d.path, content, d.language);
2103
+ const ftsText = /* @__PURE__ */ new Map();
2104
+ for (const n of parsed.nodes)
2105
+ ftsText.set(n.nodeKey, buildSearchText(n));
2106
+ const fileRow = {
2107
+ path: d.path,
2108
+ language: d.language,
2109
+ contentHash,
2110
+ mtimeMs: st.mtimeMs,
2111
+ size: st.size,
2112
+ indexedAt: started
2113
+ };
2114
+ await this.store.replaceFileNodes(fileRow, parsed.nodes, ftsText);
2115
+ parsedFiles.push(parsed);
2116
+ stats.filesParsed++;
2117
+ opts.onProgress?.({ path: d.path, done, total: discovered.length });
2118
+ }
2119
+ if (parsedFiles.length > 0) {
2120
+ const nameIndex = await this.store.getNameIndex();
2121
+ const resIndex = buildResolutionIndex(nameIndex);
2122
+ for (const parsed of parsedFiles) {
2123
+ const resolved = resolveFileEdges(parsed, resIndex);
2124
+ await this.store.replaceOutgoingEdges(resolved.srcKeys, resolved.edges);
2125
+ }
2126
+ }
2127
+ if (this.embedder && opts.embed) {
2128
+ const embeddable = await this.store.getNodesByKinds([...EMBEDDABLE_KINDS]);
2129
+ const existing = await this.store.getEmbeddingContentHashes();
2130
+ const pending = [];
2131
+ for (const n of embeddable) {
2132
+ const text = nodeEmbedText(n);
2133
+ const hash = hashContent(`${this.embedder.recipeVersion}\0${text}`);
2134
+ if (existing.get(n.nodeKey) !== hash)
2135
+ pending.push({ nodeKey: n.nodeKey, text, hash });
2136
+ }
2137
+ const BATCH = 64;
2138
+ for (let i = 0; i < pending.length; i += BATCH) {
2139
+ const batch = pending.slice(i, i + BATCH);
2140
+ const vecs = await this.embedder.embedDocuments(batch.map((b) => b.text));
2141
+ await this.store.upsertEmbeddings(batch.map((b, j) => ({
2142
+ nodeKey: b.nodeKey,
2143
+ dim: this.embedder.dim,
2144
+ vec: vecs[j],
2145
+ contentHash: b.hash
2146
+ })));
2147
+ const done2 = Math.min(i + BATCH, pending.length);
2148
+ opts.onProgress?.({ path: `embedding symbols`, done: done2, total: pending.length });
2149
+ }
2150
+ await this.store.pruneOrphanEmbeddings();
2151
+ await this.store.setMeta(META_EMBED_RECIPE_VERSION, this.embedder.recipeVersion);
2152
+ await this.store.setMeta(META_EMBED_MODEL, this.embedder.model);
2153
+ await this.store.setMeta(META_EMBED_DIM, String(this.embedder.dim));
2154
+ }
2155
+ await this.store.setMeta(META_SCHEMA_VERSION, SCHEMA_VERSION);
2156
+ await this.store.setMeta(META_LAST_INDEXED_AT, String(started));
2157
+ const head = headCommit(this.repoRoot);
2158
+ if (head)
2159
+ await this.store.setMeta(META_LAST_INDEX_COMMIT, head);
2160
+ stats.nodeCount = await this.store.countNodes();
2161
+ stats.edgeCount = await this.store.countEdges();
2162
+ stats.durationMs = (opts.now ?? Date.now()) - started;
2163
+ return stats;
2164
+ }
2165
+ async status() {
2166
+ await this.store.init();
2167
+ const fileCount = await this.store.countFiles();
2168
+ const langs = await this.store.languageDistribution();
2169
+ const languages = {};
2170
+ for (const l of langs)
2171
+ languages[l.language] = l.count;
2172
+ const lastAt = await this.store.getMeta(META_LAST_INDEXED_AT);
2173
+ return {
2174
+ indexed: fileCount > 0,
2175
+ fileCount,
2176
+ nodeCount: await this.store.countNodes(),
2177
+ edgeCount: await this.store.countEdges(),
2178
+ embeddingCount: await this.store.countEmbeddings(),
2179
+ lastIndexedAt: lastAt ? Number(lastAt) : null,
2180
+ lastIndexCommit: await this.store.getMeta(META_LAST_INDEX_COMMIT),
2181
+ schemaVersion: await this.store.getMeta(META_SCHEMA_VERSION),
2182
+ languages
2183
+ };
2184
+ }
2185
+ async searchGraph(query, opts = {}) {
2186
+ await this.store.init();
2187
+ const limit = clampLimit(opts.limit, 20, 100);
2188
+ const hits = await this.store.searchFts(query, opts.kind ?? null, limit);
2189
+ const out = [];
2190
+ for (const h of hits) {
2191
+ const n = await this.store.getNode(h.nodeKey);
2192
+ if (n)
2193
+ out.push({ ...toHit(n), score: h.score });
2194
+ }
2195
+ return out;
2196
+ }
2197
+ async semanticQuery(query, opts = {}) {
2198
+ await this.store.init();
2199
+ if (!this.embedder)
2200
+ return [];
2201
+ const limit = clampLimit(opts.limit, 20, 100);
2202
+ const qvec = await this.embedder.embedQuery(query);
2203
+ const all = await this.store.allEmbeddings();
2204
+ const scored = all.map((e) => ({ nodeKey: e.nodeKey, score: cosineSimilarity(qvec, e.vec) })).sort((a, b) => b.score - a.score).slice(0, limit);
2205
+ const out = [];
2206
+ for (const s of scored) {
2207
+ const n = await this.store.getNode(s.nodeKey);
2208
+ if (n)
2209
+ out.push({ ...toHit(n), score: s.score });
2210
+ }
2211
+ return out;
2212
+ }
2213
+ async tracePath(nodeKey, opts = {}) {
2214
+ await this.store.init();
2215
+ const direction = opts.direction ?? "out";
2216
+ const maxDepth = clampLimit(opts.maxDepth, 3, 5);
2217
+ const limit = clampLimit(opts.limit, 100, 1e3);
2218
+ const kinds = opts.kinds ?? null;
2219
+ const steps = [];
2220
+ const seenEdge = /* @__PURE__ */ new Set();
2221
+ const visited = /* @__PURE__ */ new Set([nodeKey]);
2222
+ let frontier = [nodeKey];
2223
+ for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
2224
+ const next = [];
2225
+ for (const key of frontier) {
2226
+ const edges = await this.store.neighbors(key, direction, kinds, limit);
2227
+ for (const e of edges) {
2228
+ const id = `${e.srcKey} ${e.dstKey} ${e.kind}`;
2229
+ if (seenEdge.has(id))
2230
+ continue;
2231
+ seenEdge.add(id);
2232
+ steps.push({ srcKey: e.srcKey, dstKey: e.dstKey, kind: e.kind });
2233
+ if (steps.length >= limit)
2234
+ return steps;
2235
+ const other = direction === "out" ? e.dstKey : e.srcKey;
2236
+ if (!visited.has(other)) {
2237
+ visited.add(other);
2238
+ next.push(other);
2239
+ }
2240
+ }
2241
+ }
2242
+ frontier = next;
2243
+ }
2244
+ return steps;
2245
+ }
2246
+ async getCodeSnippet(nodeKey) {
2247
+ await this.store.init();
2248
+ const n = await this.store.getNode(nodeKey);
2249
+ if (!n)
2250
+ return null;
2251
+ let source = "";
2252
+ try {
2253
+ const full = readFileSync4(posix3.join(this.repoRoot, n.filePath), "utf8");
2254
+ source = full.slice(n.startByte, n.endByte);
2255
+ } catch {
2256
+ return null;
2257
+ }
2258
+ return {
2259
+ nodeKey: n.nodeKey,
2260
+ filePath: n.filePath,
2261
+ startLine: n.startLine,
2262
+ endLine: n.endLine,
2263
+ source
2264
+ };
2265
+ }
2266
+ async getArchitecture(opts = {}) {
2267
+ await this.store.init();
2268
+ const kinds = await this.store.kindDistribution();
2269
+ const langs = await this.store.languageDistribution();
2270
+ const kindDistribution = {};
2271
+ for (const k of kinds)
2272
+ kindDistribution[k.kind] = k.count;
2273
+ const languageDistribution = {};
2274
+ for (const l of langs)
2275
+ languageDistribution[l.language] = l.count;
2276
+ return {
2277
+ fileCount: await this.store.countFiles(),
2278
+ nodeCount: await this.store.countNodes(),
2279
+ edgeCount: await this.store.countEdges(),
2280
+ kindDistribution,
2281
+ languageDistribution,
2282
+ topModules: await this.store.topModulesByFanIn(clampLimit(opts.topN, 10, 50))
2283
+ };
2284
+ }
2285
+ async changedSymbols(opts = {}) {
2286
+ await this.store.init();
2287
+ const files = changedSourceFiles(this.repoRoot, opts);
2288
+ if (files === null) {
2289
+ throw new Error("changedSymbols requires a git repository (change detection is git-based)");
2290
+ }
2291
+ const limit = clampLimit(opts.limit, 64, 200);
2292
+ const symbols = [];
2293
+ let indexedFiles = 0;
2294
+ for (const f of files) {
2295
+ const rows = await this.store.getNodesByFile(f.path);
2296
+ if (rows.length > 0)
2297
+ indexedFiles++;
2298
+ symbols.push(...rows);
2299
+ }
2300
+ symbols.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine);
2301
+ return {
2302
+ changedFiles: files.length,
2303
+ indexedFiles,
2304
+ symbols: symbols.slice(0, limit),
2305
+ truncated: symbols.length > limit
2306
+ };
2307
+ }
2308
+ async reviewChanges(opts = {}) {
2309
+ await this.store.init();
2310
+ const rawPaths = enumerateChangedPaths(this.repoRoot, opts);
2311
+ if (rawPaths === null) {
2312
+ throw new Error("reviewChanges requires a git repository (change detection is git-based)");
2313
+ }
2314
+ const hunksRes = changedHunks(this.repoRoot, opts);
2315
+ const hunks = hunksRes?.files ?? /* @__PURE__ */ new Map();
2316
+ let hunkCoverage = hunksRes?.coverage ?? "none";
2317
+ const riskFlags = scanRiskFlags({
2318
+ paths: rawPaths,
2319
+ hunks,
2320
+ readUntracked: (p) => this.readRepoFileHead(p, MAX_UNTRACKED_HEAD_BYTES)
2321
+ });
2322
+ const files = classifyPaths(rawPaths.keys());
2323
+ const limit = clampLimit(opts.limit, 64, 200);
2324
+ const symbols = [];
2325
+ let indexedFiles = 0;
2326
+ let staleFiles = 0;
2327
+ for (const f of files) {
2328
+ const rows = await this.store.getNodesByFile(f.path);
2329
+ if (rows.length === 0)
2330
+ continue;
2331
+ indexedFiles++;
2332
+ const fileRow = await this.store.getFile(f.path);
2333
+ const content = this.readRepoFile(f.path);
2334
+ const stale = fileRow !== null && content !== null && hashContent(content) !== fileRow.contentHash;
2335
+ if (stale) {
2336
+ staleFiles++;
2337
+ if (hunkCoverage === "full")
2338
+ hunkCoverage = "partial";
2339
+ }
2340
+ const fileStatus = rawPaths.get(f.path) ?? "modified";
2341
+ const fh = hunks.get(f.path);
2342
+ for (const row of rows) {
2343
+ symbols.push(classifyReviewSymbol(row, fileStatus, fh, stale));
2344
+ }
2345
+ }
2346
+ symbols.sort((a, b) => changeRank(a.change) - changeRank(b.change) || a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine);
2347
+ return {
2348
+ changedFiles: files.length,
2349
+ indexedFiles,
2350
+ staleFiles,
2351
+ symbols: symbols.slice(0, limit),
2352
+ truncated: symbols.length > limit,
2353
+ riskFlags,
2354
+ hunkCoverage,
2355
+ baseKind: opts.since ? "ref" : "uncommitted"
2356
+ };
2357
+ }
2358
+ async close() {
2359
+ await this.store.close();
2360
+ }
2361
+ // ── internal ──
2362
+ readRepoFileHead(relPath, maxBytes) {
2363
+ const content = this.readRepoFile(relPath);
2364
+ return content === null ? null : content.slice(0, maxBytes);
2365
+ }
2366
+ readRepoFile(relPath) {
2367
+ try {
2368
+ return readFileSync4(posix3.join(this.repoRoot, relPath), "utf8");
2369
+ } catch {
2370
+ return null;
2371
+ }
2372
+ }
2373
+ async wipe() {
2374
+ for (const f of await this.store.getAllFiles()) {
2375
+ await this.store.deleteFile(f.path);
2376
+ }
2377
+ }
2378
+ };
2379
+ function toHit(n) {
2380
+ return {
2381
+ nodeKey: n.nodeKey,
2382
+ kind: n.kind,
2383
+ name: n.name,
2384
+ qualifiedName: n.qualifiedName,
2385
+ filePath: n.filePath,
2386
+ language: n.language,
2387
+ startLine: n.startLine,
2388
+ endLine: n.endLine
2389
+ };
2390
+ }
2391
+ function clampLimit(v, dflt, max) {
2392
+ if (v === void 0 || !Number.isFinite(v))
2393
+ return dflt;
2394
+ return Math.max(1, Math.min(max, Math.floor(v)));
2395
+ }
2396
+ function changeRank(c) {
2397
+ return c === "added" ? 0 : c === "modified" ? 1 : 2;
2398
+ }
2399
+ function classifyReviewSymbol(row, fileStatus, fh, stale) {
2400
+ if (stale)
2401
+ return { ...row, change: "co_located", changedLineCount: 0, staleIndex: true };
2402
+ if (fileStatus === "added" || fh?.wholeFile) {
2403
+ return {
2404
+ ...row,
2405
+ change: "added",
2406
+ changedLineCount: row.endLine - row.startLine + 1,
2407
+ staleIndex: false
2408
+ };
2409
+ }
2410
+ if (!fh || fh.degraded || fh.binary) {
2411
+ return { ...row, change: "co_located", changedLineCount: 0, staleIndex: false };
2412
+ }
2413
+ let overlap = 0;
2414
+ for (const r of fh.addedRanges) {
2415
+ const lo = Math.max(r.start, row.startLine);
2416
+ const hi = Math.min(r.end, row.endLine);
2417
+ if (hi >= lo)
2418
+ overlap += hi - lo + 1;
2419
+ }
2420
+ const deletionTouch = fh.deletionPoints.some((c) => c >= row.startLine - 1 && c <= row.endLine);
2421
+ if (overlap > 0 || deletionTouch) {
2422
+ return { ...row, change: "modified", changedLineCount: overlap, staleIndex: false };
2423
+ }
2424
+ return { ...row, change: "co_located", changedLineCount: 0, staleIndex: false };
2425
+ }
2426
+
2427
+ // ../../packages/code-index/build/store/node-sqlite.js
2428
+ import Database from "better-sqlite3";
2429
+ function toNodeRow(r) {
2430
+ return {
2431
+ nodeKey: r.node_key,
2432
+ kind: r.kind,
2433
+ name: r.name,
2434
+ qualifiedName: r.qualified_name,
2435
+ filePath: r.file_path,
2436
+ language: r.language,
2437
+ startLine: r.start_line,
2438
+ endLine: r.end_line,
2439
+ startByte: r.start_byte,
2440
+ endByte: r.end_byte,
2441
+ signature: r.signature,
2442
+ doc: r.doc
2443
+ };
2444
+ }
2445
+ var CHUNK = 400;
2446
+ var NodeSqliteStore = class {
2447
+ db;
2448
+ constructor(dbPath) {
2449
+ this.db = new Database(dbPath);
2450
+ this.db.pragma("journal_mode = WAL");
2451
+ this.db.pragma("foreign_keys = ON");
2452
+ }
2453
+ async init() {
2454
+ this.db.exec(SCHEMA_SQL);
2455
+ }
2456
+ async close() {
2457
+ this.db.close();
2458
+ }
2459
+ async getMeta(key) {
2460
+ const row = this.db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
2461
+ return row?.value ?? null;
2462
+ }
2463
+ async setMeta(key, value2) {
2464
+ this.db.prepare("INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value2);
2465
+ }
2466
+ async getAllFiles() {
2467
+ const rows = this.db.prepare("SELECT * FROM files").all();
2468
+ return rows.map((r) => ({
2469
+ path: r.path,
2470
+ language: r.language,
2471
+ contentHash: r.content_hash,
2472
+ mtimeMs: r.mtime_ms,
2473
+ size: r.size,
2474
+ indexedAt: r.indexed_at
2475
+ }));
2476
+ }
2477
+ async getFile(path) {
2478
+ const r = this.db.prepare("SELECT * FROM files WHERE path = ?").get(path);
2479
+ if (!r)
2480
+ return null;
2481
+ return {
2482
+ path: r.path,
2483
+ language: r.language,
2484
+ contentHash: r.content_hash,
2485
+ mtimeMs: r.mtime_ms,
2486
+ size: r.size,
2487
+ indexedAt: r.indexed_at
2488
+ };
2489
+ }
2490
+ async upsertFile(row) {
2491
+ this.upsertFileStmt().run(row.path, row.language, row.contentHash, row.mtimeMs, row.size, row.indexedAt);
2492
+ }
2493
+ async replaceFileNodes(file, nodes, ftsText) {
2494
+ const tx = this.db.transaction(() => {
2495
+ this.deleteFileGraph(file.path);
2496
+ const insNode = this.insertNodeStmt();
2497
+ const insFts = this.insertFtsStmt();
2498
+ for (const n of nodes) {
2499
+ 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);
2500
+ insFts.run(n.nodeKey, ftsText.get(n.nodeKey) ?? n.name);
2501
+ }
2502
+ this.upsertFileStmt().run(file.path, file.language, file.contentHash, file.mtimeMs, file.size, file.indexedAt);
2503
+ });
2504
+ tx();
2505
+ }
2506
+ async deleteFile(path) {
2507
+ const tx = this.db.transaction(() => {
2508
+ this.deleteFileGraph(path);
2509
+ this.db.prepare("DELETE FROM files WHERE path = ?").run(path);
2510
+ });
2511
+ tx();
2512
+ }
2513
+ async replaceOutgoingEdges(srcKeys, edges) {
2514
+ const tx = this.db.transaction(() => {
2515
+ const del = this.db.prepare("DELETE FROM edges WHERE src_key = ?");
2516
+ for (const k of srcKeys)
2517
+ del.run(k);
2518
+ const ins = this.db.prepare("INSERT OR IGNORE INTO edges(src_key, dst_key, kind) VALUES(?, ?, ?)");
2519
+ for (const e of edges)
2520
+ ins.run(e.srcKey, e.dstKey, e.kind);
2521
+ });
2522
+ tx();
2523
+ }
2524
+ async getNameIndex() {
2525
+ const rows = this.db.prepare("SELECT node_key, name, kind, file_path FROM nodes").all();
2526
+ return rows.map((r) => ({
2527
+ nodeKey: r.node_key,
2528
+ name: r.name,
2529
+ kind: r.kind,
2530
+ filePath: r.file_path
2531
+ }));
2532
+ }
2533
+ async getNode(nodeKey) {
2534
+ const r = this.db.prepare("SELECT * FROM nodes WHERE node_key = ?").get(nodeKey);
2535
+ return r ? toNodeRow(r) : null;
2536
+ }
2537
+ async getNodesByFile(path) {
2538
+ const rows = this.db.prepare("SELECT * FROM nodes WHERE file_path = ? ORDER BY start_line").all(path);
2539
+ return rows.map(toNodeRow);
2540
+ }
2541
+ async getNodesByKinds(kinds) {
2542
+ if (kinds.length === 0)
2543
+ return [];
2544
+ const rows = this.db.prepare(`SELECT * FROM nodes WHERE kind IN (${kinds.map(() => "?").join(",")})`).all(...kinds);
2545
+ return rows.map(toNodeRow);
2546
+ }
2547
+ async getAllNodes() {
2548
+ const rows = this.db.prepare("SELECT * FROM nodes").all();
2549
+ return rows.map(toNodeRow);
2550
+ }
2551
+ async getAllEdges() {
2552
+ const rows = this.db.prepare("SELECT src_key, dst_key, kind FROM edges").all();
2553
+ return rows.map((r) => ({
2554
+ srcKey: r.src_key,
2555
+ dstKey: r.dst_key,
2556
+ kind: r.kind
2557
+ }));
2558
+ }
2559
+ async upsertEmbeddings(rows) {
2560
+ const stmt = this.db.prepare("INSERT OR REPLACE INTO embeddings(node_key, dim, vec, content_hash) VALUES(?, ?, ?, ?)");
2561
+ const tx = this.db.transaction(() => {
2562
+ for (const r of rows) {
2563
+ const buf = Buffer.from(r.vec.buffer, r.vec.byteOffset, r.vec.byteLength);
2564
+ stmt.run(r.nodeKey, r.dim, buf, r.contentHash);
2565
+ }
2566
+ });
2567
+ tx();
2568
+ }
2569
+ async getEmbeddingContentHashes() {
2570
+ const rows = this.db.prepare("SELECT node_key, content_hash FROM embeddings").all();
2571
+ return new Map(rows.map((r) => [r.node_key, r.content_hash]));
2572
+ }
2573
+ async allEmbeddings() {
2574
+ const rows = this.db.prepare("SELECT node_key, vec FROM embeddings").all();
2575
+ return rows.map((r) => ({
2576
+ nodeKey: r.node_key,
2577
+ vec: new Float32Array(r.vec.buffer, r.vec.byteOffset, r.vec.byteLength / 4)
2578
+ }));
2579
+ }
2580
+ async pruneOrphanEmbeddings() {
2581
+ const info = this.db.prepare("DELETE FROM embeddings WHERE node_key NOT IN (SELECT node_key FROM nodes)").run();
2582
+ return info.changes;
2583
+ }
2584
+ async countEmbeddings() {
2585
+ return this.db.prepare("SELECT COUNT(*) AS c FROM embeddings").get().c;
2586
+ }
2587
+ async searchFts(query, kind, limit) {
2588
+ const match = toFtsMatchQuery(query);
2589
+ if (!match)
2590
+ return [];
2591
+ const sql = kind ? `SELECT f.node_key AS node_key, bm25(nodes_fts) AS score
2592
+ FROM nodes_fts f JOIN nodes n ON n.node_key = f.node_key
2593
+ WHERE nodes_fts MATCH ? AND n.kind = ?
2594
+ ORDER BY score LIMIT ?` : `SELECT f.node_key AS node_key, bm25(nodes_fts) AS score
2595
+ FROM nodes_fts f JOIN nodes n ON n.node_key = f.node_key
2596
+ WHERE nodes_fts MATCH ?
2597
+ ORDER BY score LIMIT ?`;
2598
+ const stmt = this.db.prepare(sql);
2599
+ const rows = kind ? stmt.all(match, kind, limit) : stmt.all(match, limit);
2600
+ return rows.map((r) => ({ nodeKey: r.node_key, score: -r.score }));
2601
+ }
2602
+ async neighbors(nodeKey, direction, kinds, limit) {
2603
+ const col = direction === "out" ? "src_key" : "dst_key";
2604
+ let sql = `SELECT src_key, dst_key, kind FROM edges WHERE ${col} = ?`;
2605
+ const params = [nodeKey];
2606
+ if (kinds && kinds.length > 0) {
2607
+ sql += ` AND kind IN (${kinds.map(() => "?").join(",")})`;
2608
+ params.push(...kinds);
2609
+ }
2610
+ sql += " LIMIT ?";
2611
+ params.push(limit);
2612
+ const rows = this.db.prepare(sql).all(...params);
2613
+ return rows.map((r) => ({
2614
+ srcKey: r.src_key,
2615
+ dstKey: r.dst_key,
2616
+ kind: r.kind
2617
+ }));
2618
+ }
2619
+ async countFiles() {
2620
+ return this.db.prepare("SELECT COUNT(*) AS c FROM files").get().c;
2621
+ }
2622
+ async countNodes() {
2623
+ return this.db.prepare("SELECT COUNT(*) AS c FROM nodes").get().c;
2624
+ }
2625
+ async countEdges() {
2626
+ return this.db.prepare("SELECT COUNT(*) AS c FROM edges").get().c;
2627
+ }
2628
+ async kindDistribution() {
2629
+ const rows = this.db.prepare("SELECT kind, COUNT(*) AS c FROM nodes GROUP BY kind ORDER BY c DESC").all();
2630
+ return rows.map((r) => ({ kind: r.kind, count: r.c }));
2631
+ }
2632
+ async languageDistribution() {
2633
+ const rows = this.db.prepare("SELECT language, COUNT(*) AS c FROM files GROUP BY language ORDER BY c DESC").all();
2634
+ return rows.map((r) => ({ language: r.language, count: r.c }));
2635
+ }
2636
+ async topModulesByFanIn(limit) {
2637
+ const rows = this.db.prepare(`SELECT n.file_path AS file_path, n.language AS language,
2638
+ (SELECT COUNT(*) FROM nodes n2 WHERE n2.file_path = n.file_path) AS symbol_count,
2639
+ (SELECT COUNT(*) FROM edges e WHERE e.src_key = n.node_key AND e.kind = 'imports') AS imports_out,
2640
+ (SELECT COUNT(*) FROM edges e WHERE e.dst_key = n.node_key AND e.kind = 'imports') AS imports_in
2641
+ FROM nodes n
2642
+ WHERE n.kind = 'module'
2643
+ ORDER BY imports_in DESC, symbol_count DESC
2644
+ LIMIT ?`).all(limit);
2645
+ return rows.map((r) => ({
2646
+ filePath: r.file_path,
2647
+ language: r.language,
2648
+ symbolCount: r.symbol_count,
2649
+ importsOut: r.imports_out,
2650
+ importsIn: r.imports_in
2651
+ }));
2652
+ }
2653
+ // ── internal helpers ──
2654
+ deleteFileGraph(path) {
2655
+ const keys = this.db.prepare("SELECT node_key FROM nodes WHERE file_path = ?").all(path).map((r) => r.node_key);
2656
+ const delEdge = this.db.prepare("DELETE FROM edges WHERE src_key = ?");
2657
+ const delFts = this.db.prepare("DELETE FROM nodes_fts WHERE node_key = ?");
2658
+ for (let i = 0; i < keys.length; i += CHUNK) {
2659
+ for (const k of keys.slice(i, i + CHUNK)) {
2660
+ delEdge.run(k);
2661
+ delFts.run(k);
2662
+ }
2663
+ }
2664
+ this.db.prepare("DELETE FROM nodes WHERE file_path = ?").run(path);
2665
+ }
2666
+ _insNode = null;
2667
+ insertNodeStmt() {
2668
+ if (!this._insNode) {
2669
+ this._insNode = this.db.prepare(`INSERT OR REPLACE INTO nodes(
2670
+ node_key, kind, name, qualified_name, file_path, language,
2671
+ start_line, end_line, start_byte, end_byte, signature, doc
2672
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
2673
+ }
2674
+ return this._insNode;
2675
+ }
2676
+ _insFts = null;
2677
+ insertFtsStmt() {
2678
+ if (!this._insFts) {
2679
+ this._insFts = this.db.prepare("INSERT INTO nodes_fts(node_key, search_text) VALUES(?, ?)");
2680
+ }
2681
+ return this._insFts;
2682
+ }
2683
+ _upsertFile = null;
2684
+ upsertFileStmt() {
2685
+ if (!this._upsertFile) {
2686
+ this._upsertFile = this.db.prepare(`INSERT INTO files(path, language, content_hash, mtime_ms, size, indexed_at)
2687
+ VALUES(?, ?, ?, ?, ?, ?)
2688
+ ON CONFLICT(path) DO UPDATE SET
2689
+ language = excluded.language,
2690
+ content_hash = excluded.content_hash,
2691
+ mtime_ms = excluded.mtime_ms,
2692
+ size = excluded.size,
2693
+ indexed_at = excluded.indexed_at`);
2694
+ }
2695
+ return this._upsertFile;
2696
+ }
2697
+ };
2698
+
2699
+ // ../../packages/code-index/build/sync/types.js
2700
+ import { createHash as createHash4 } from "node:crypto";
2701
+ function sha2562(input) {
2702
+ return createHash4("sha256").update(input, "utf8").digest("hex");
2703
+ }
2704
+ function toCloudNode(n) {
2705
+ return {
2706
+ node_key_hash: sha2562(n.nodeKey),
2707
+ name: n.name,
2708
+ qualified_name: n.qualifiedName,
2709
+ path_hash: sha2562(n.filePath),
2710
+ kind: n.kind,
2711
+ language: n.language,
2712
+ start_line: n.startLine,
2713
+ end_line: n.endLine,
2714
+ signature_hash: n.signature ? sha2562(n.signature) : null
2715
+ };
2716
+ }
2717
+ function toCloudEdge(e) {
2718
+ return {
2719
+ src_key_hash: sha2562(e.srcKey),
2720
+ dst_key_hash: sha2562(e.dstKey),
2721
+ kind: e.kind
2722
+ };
2723
+ }
2724
+ function toCloudEmbedding(v, contentHash, meta) {
2725
+ return {
2726
+ node_key_hash: sha2562(v.nodeKey),
2727
+ embedding: Array.from(v.vec),
2728
+ recipe_version: meta.recipeVersion,
2729
+ dim: meta.dim,
2730
+ model: meta.model,
2731
+ content_hash: contentHash
2732
+ };
2733
+ }
2734
+
2735
+ // ../../packages/code-index/build/sync/client.js
2736
+ var SyncError = class extends Error {
2737
+ status;
2738
+ code;
2739
+ constructor(message, status2, code) {
2740
+ super(message);
2741
+ this.status = status2;
2742
+ this.code = code;
2743
+ this.name = "SyncError";
2744
+ }
2745
+ };
2746
+ var CodeIndexSync = class {
2747
+ store;
2748
+ cfg;
2749
+ base;
2750
+ chunk;
2751
+ fetchImpl;
2752
+ constructor(store, cfg) {
2753
+ this.store = store;
2754
+ this.cfg = cfg;
2755
+ this.base = cfg.endpoint.replace(/\/+$/, "");
2756
+ this.chunk = cfg.chunkSize && cfg.chunkSize > 0 ? cfg.chunkSize : 1e3;
2757
+ this.fetchImpl = cfg.fetchImpl ?? fetch;
2758
+ }
2759
+ /** Close the underlying store handle. */
2760
+ async close() {
2761
+ await this.store.close();
2762
+ }
2763
+ /** Enable/disable the opt-in consent (CLI `maind sync enable`). */
2764
+ async setConsent(enabled, policyVersion) {
2765
+ await this.post("/api/codebase/consent", {
2766
+ enabled,
2767
+ policy_version: policyVersion ?? null
2768
+ });
2769
+ }
2770
+ /** Cloud sync status for this repo (`maind sync status`). */
2771
+ async remoteStatus() {
2772
+ const qs = new URLSearchParams({ project_key: this.cfg.projectKey });
2773
+ const json = await this.get(`/api/codebase/status?${qs.toString()}`);
2774
+ return json;
2775
+ }
2776
+ /** Local index status (for the CLI to decide whether there's anything to push). */
2777
+ async localStatus() {
2778
+ await this.store.init();
2779
+ return {
2780
+ indexed: await this.store.countFiles() > 0,
2781
+ nodeCount: await this.store.countNodes(),
2782
+ edgeCount: await this.store.countEdges(),
2783
+ embeddingCount: await this.store.countEmbeddings(),
2784
+ fileCount: await this.store.countFiles(),
2785
+ headCommit: await this.store.getMeta(META_LAST_INDEX_COMMIT),
2786
+ lastSyncedAt: numOrNull(await this.store.getMeta(META_LAST_SYNCED_AT))
2787
+ };
2788
+ }
2789
+ /**
2790
+ * Full push: upsert every local node + edge, then prune cloud rows that no
2791
+ * longer exist locally (deletions reconcile). Incremental-by-commit is a later
2792
+ * optimization; a full upsert is idempotent and fine at repo scale.
2793
+ */
2794
+ async push() {
2795
+ await this.store.init();
2796
+ const nodes = (await this.store.getAllNodes()).map(toCloudNode);
2797
+ const edges = (await this.store.getAllEdges()).map(toCloudEdge);
2798
+ const schemaVersion = await this.store.getMeta(META_SCHEMA_VERSION) ?? SCHEMA_VERSION;
2799
+ const headCommit2 = await this.store.getMeta(META_LAST_INDEX_COMMIT);
2800
+ const fileCount = await this.store.countFiles();
2801
+ const langDist = Object.fromEntries((await this.store.languageDistribution()).map((l) => [l.language, l.count]));
2802
+ const manifest = {
2803
+ project_key: this.cfg.projectKey,
2804
+ head_commit: headCommit2,
2805
+ schema_version: schemaVersion,
2806
+ file_count: fileCount,
2807
+ lang_dist: langDist
2808
+ };
2809
+ let phaseDone = 0;
2810
+ const nodeChunks = chunkArray(nodes, this.chunk);
2811
+ const edgeChunks = chunkArray(edges, this.chunk * 2);
2812
+ const totalChunks = Math.max(1, nodeChunks.length + edgeChunks.length);
2813
+ for (const chunk of nodeChunks) {
2814
+ await this.post("/api/codebase/push", { ...manifest, nodes: chunk, edges: [] });
2815
+ this.cfg.onProgress?.({ phase: "nodes", done: ++phaseDone, total: totalChunks });
2816
+ }
2817
+ for (const chunk of edgeChunks) {
2818
+ await this.post("/api/codebase/push", { ...manifest, nodes: [], edges: chunk });
2819
+ this.cfg.onProgress?.({ phase: "edges", done: ++phaseDone, total: totalChunks });
2820
+ }
2821
+ if (nodeChunks.length === 0 && edgeChunks.length === 0) {
2822
+ await this.post("/api/codebase/push", { ...manifest, nodes: [], edges: [] });
2823
+ }
2824
+ const keep = nodes.map((n) => n.node_key_hash);
2825
+ const pruneRes = await this.post("/api/codebase/prune", {
2826
+ project_key: this.cfg.projectKey,
2827
+ keep_node_key_hashes: keep
2828
+ });
2829
+ this.cfg.onProgress?.({ phase: "prune", done: totalChunks, total: totalChunks });
2830
+ const embeddings = await this.store.countEmbeddings() > 0 ? await this.pushEmbeddings() : 0;
2831
+ await this.store.setMeta(META_LAST_SYNCED_AT, String(Date.now()));
2832
+ if (headCommit2)
2833
+ await this.store.setMeta(META_LAST_SYNCED_COMMIT, headCommit2);
2834
+ return {
2835
+ nodes: nodes.length,
2836
+ edges: edges.length,
2837
+ pruned: numOr0(pruneRes.nodes_deleted),
2838
+ embeddings,
2839
+ headCommit: headCommit2
2840
+ };
2841
+ }
2842
+ /**
2843
+ * Push symbol vectors (ADR-220). Diffs local content-hashes against the
2844
+ * remote map (get_codebase_embedding_hashes) and uploads only new/changed
2845
+ * vectors — a full re-upload would be MBs on every sync. Returns the number
2846
+ * of vectors uploaded. Requires the embedder identity recorded by
2847
+ * `index --embed` (META_EMBED_*); older indexes must re-run it once.
2848
+ */
2849
+ async pushEmbeddings() {
2850
+ await this.store.init();
2851
+ const localHashes = await this.store.getEmbeddingContentHashes();
2852
+ if (localHashes.size === 0)
2853
+ return 0;
2854
+ const meta = await this.embedMeta();
2855
+ const qs = new URLSearchParams({ project_key: this.cfg.projectKey });
2856
+ const remote = await this.get(`/api/codebase/embedding-hashes?${qs.toString()}`);
2857
+ const remoteHashes = remote.hashes ?? {};
2858
+ 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);
2859
+ if (pending.length === 0)
2860
+ return 0;
2861
+ const chunks = chunkArray(pending, EMBED_CHUNK);
2862
+ let done = 0;
2863
+ for (const chunk of chunks) {
2864
+ await this.post("/api/codebase/push-embeddings", {
2865
+ project_key: this.cfg.projectKey,
2866
+ embeddings: chunk
2867
+ });
2868
+ this.cfg.onProgress?.({ phase: "embeddings", done: ++done, total: chunks.length });
2869
+ }
2870
+ return pending.length;
2871
+ }
2872
+ /** The embedder identity recorded at `index --embed` time. */
2873
+ async embedMeta() {
2874
+ const recipeVersion = await this.store.getMeta(META_EMBED_RECIPE_VERSION);
2875
+ const model = await this.store.getMeta(META_EMBED_MODEL);
2876
+ const dim = numOrNull(await this.store.getMeta(META_EMBED_DIM));
2877
+ if (!recipeVersion || !model || !dim) {
2878
+ throw new SyncError("local embeddings lack embedder metadata \u2014 re-run `maind index --embed` once to record it", 0, "missing_embed_meta");
2879
+ }
2880
+ return { recipeVersion, model, dim };
2881
+ }
2882
+ // ── HTTP ──
2883
+ headers() {
2884
+ const h = {
2885
+ "content-type": "application/json",
2886
+ authorization: `Bearer ${this.cfg.apiKey}`
2887
+ };
2888
+ if (this.cfg.deviceId)
2889
+ h["x-maind-device-id"] = this.cfg.deviceId;
2890
+ return h;
2891
+ }
2892
+ async post(path, body) {
2893
+ const res = await this.fetchImpl(`${this.base}${path}`, {
2894
+ method: "POST",
2895
+ headers: this.headers(),
2896
+ body: JSON.stringify(body)
2897
+ });
2898
+ return this.unwrap(res, path);
2899
+ }
2900
+ async get(path) {
2901
+ const res = await this.fetchImpl(`${this.base}${path}`, {
2902
+ method: "GET",
2903
+ headers: this.headers()
2904
+ });
2905
+ return this.unwrap(res, path);
2906
+ }
2907
+ async unwrap(res, path) {
2908
+ const text = await res.text().catch(() => "");
2909
+ let json = null;
2910
+ try {
2911
+ json = text ? JSON.parse(text) : null;
2912
+ } catch {
2913
+ json = null;
2914
+ }
2915
+ if (!res.ok || !json || json.ok !== true) {
2916
+ const code = json?.error;
2917
+ throw new SyncError(`${path} failed (HTTP ${res.status})${code ? `: ${code}` : ""}`, res.status, code);
2918
+ }
2919
+ return json;
2920
+ }
2921
+ };
2922
+ var EMBED_CHUNK = 250;
2923
+ function chunkArray(items, size) {
2924
+ const out = [];
2925
+ for (let i = 0; i < items.length; i += size)
2926
+ out.push(items.slice(i, i + size));
2927
+ return out;
2928
+ }
2929
+ function numOrNull(v) {
2930
+ if (v === null)
2931
+ return null;
2932
+ const n = Number(v);
2933
+ return Number.isFinite(n) ? n : null;
2934
+ }
2935
+ function numOr0(v) {
2936
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
2937
+ }
2938
+
2939
+ // ../../packages/code-index/build/node.js
2940
+ var DEFAULT_DB_RELPATH = ".maind/code-index.sqlite";
2941
+ function createNodeCodeIndex(opts) {
2942
+ const dbPath = opts.dbPath ?? posix4.join(opts.repoRoot, DEFAULT_DB_RELPATH);
2943
+ mkdirSync3(posix4.dirname(dbPath), { recursive: true });
2944
+ const store = new NodeSqliteStore(dbPath);
2945
+ const parser = new TreeSitterParser();
2946
+ return new CodeIndexEngine({
2947
+ store,
2948
+ parser,
2949
+ repoRoot: opts.repoRoot,
2950
+ embedder: opts.embedder
2951
+ });
2952
+ }
2953
+ function createCodeIndexSync(opts) {
2954
+ const dbPath = opts.dbPath ?? posix4.join(opts.repoRoot, DEFAULT_DB_RELPATH);
2955
+ const projectKey = opts.projectKey ?? computeProjectKey(opts.repoRoot) ?? void 0;
2956
+ if (!projectKey) {
2957
+ 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.");
2958
+ }
2959
+ const store = new NodeSqliteStore(dbPath);
2960
+ return new CodeIndexSync(store, {
2961
+ endpoint: opts.endpoint,
2962
+ apiKey: opts.apiKey,
2963
+ projectKey,
2964
+ deviceId: opts.deviceId,
2965
+ chunkSize: opts.chunkSize,
2966
+ onProgress: opts.onProgress
2967
+ });
2968
+ }
2969
+
2970
+ // ../../packages/embedding/build/index.js
2971
+ var EMBEDDING_MODEL = process.env.MAIND_EMBEDDING_MODEL ?? "Xenova/bge-small-en-v1.5";
2972
+ var EMBEDDING_DIM = Number(process.env.MAIND_EMBEDDING_DIM ?? 384);
2973
+ var EMBEDDING_DTYPE = process.env.MAIND_EMBEDDING_DTYPE ?? "q8";
2974
+ var EMBEDDING_QUERY_PREFIX = "Represent this sentence for searching relevant passages: ";
2975
+ var extractorPromise = null;
2976
+ async function getExtractor() {
2977
+ if (!extractorPromise) {
2978
+ extractorPromise = (async () => {
2979
+ const mod = await import("@huggingface/transformers");
2980
+ const cacheDir = process.env.MAIND_EMBEDDING_CACHE_DIR;
2981
+ if (cacheDir)
2982
+ mod.env.cacheDir = cacheDir;
2983
+ return mod.pipeline("feature-extraction", EMBEDDING_MODEL, {
2984
+ dtype: EMBEDDING_DTYPE,
2985
+ device: "cpu"
2986
+ });
2987
+ })();
2988
+ }
2989
+ return extractorPromise;
2990
+ }
2991
+ function createEmbedder() {
2992
+ const embedder = {
2993
+ async embedDocuments(texts) {
2994
+ if (texts.length === 0)
2995
+ return [];
2996
+ const extractor = await getExtractor();
2997
+ const out = await extractor(texts, { pooling: "cls", normalize: true });
2998
+ return out.tolist().map((v) => Float32Array.from(v));
2999
+ },
3000
+ async embedQuery(text) {
3001
+ const [vec] = await embedder.embedDocuments([
3002
+ EMBEDDING_QUERY_PREFIX + text
3003
+ ]);
3004
+ return vec;
3005
+ }
3006
+ };
3007
+ return embedder;
3008
+ }
3009
+
3010
+ // ../../packages/code-index/build/embed/node.js
3011
+ var NodeEmbedder = class {
3012
+ dim = EMBEDDING_DIM;
3013
+ model = EMBEDDING_MODEL;
3014
+ recipeVersion = EMBED_RECIPE_VERSION;
3015
+ inner = createEmbedder();
3016
+ embedDocuments(texts) {
3017
+ return this.inner.embedDocuments(texts);
3018
+ }
3019
+ embedQuery(text) {
3020
+ return this.inner.embedQuery(text);
3021
+ }
3022
+ };
3023
+
3024
+ // ../../packages/code-index/build/node-embed.js
3025
+ function createNodeEmbedder() {
3026
+ return new NodeEmbedder();
3027
+ }
3028
+
3029
+ // src/index-cmd.ts
3030
+ var OK_FILE_ENV = "MAIND_ORT_OK_FILE";
3031
+ var WORKER_FLAG = "--_ort-worker";
3032
+ var CLI_ENTRY = fileURLToPath(new URL("./index.js", import.meta.url));
3033
+ function isOrtWorker(argv) {
3034
+ return argv.includes(WORKER_FLAG);
3035
+ }
3036
+ function runOrtIsolated(argv) {
3037
+ const dir = mkdtempSync(join4(tmpdir(), "maind-ort-"));
3038
+ const okFile = join4(dir, "ok");
3039
+ const res = spawnSync(process.execPath, [CLI_ENTRY, ...argv, WORKER_FLAG], {
3040
+ stdio: ["ignore", "inherit", "inherit"],
3041
+ env: { ...process.env, [OK_FILE_ENV]: okFile }
3042
+ });
3043
+ const ok = existsSync2(okFile);
3044
+ try {
3045
+ rmSync(dirname3(okFile), { recursive: true, force: true });
3046
+ } catch {
3047
+ }
3048
+ if (ok) return 0;
3049
+ return res.status && res.status !== 0 ? res.status : 1;
3050
+ }
3051
+ function markOrtOk() {
3052
+ const f = process.env[OK_FILE_ENV];
3053
+ if (f) {
3054
+ try {
3055
+ writeFileSync3(f, "ok");
3056
+ } catch {
3057
+ }
3058
+ }
3059
+ }
3060
+ function flag(argv, name) {
3061
+ return argv.includes(name);
3062
+ }
3063
+ function value(argv, name) {
3064
+ const i = argv.indexOf(name);
3065
+ if (i === -1 || i + 1 >= argv.length) return void 0;
3066
+ return argv[i + 1];
3067
+ }
3068
+ function resolveRoot(explicit) {
3069
+ if (explicit) return explicit;
3070
+ try {
3071
+ return execFileSync4("git", ["rev-parse", "--show-toplevel"], {
3072
+ encoding: "utf8",
3073
+ stdio: ["ignore", "pipe", "ignore"]
3074
+ }).trim();
3075
+ } catch {
3076
+ return process.cwd();
3077
+ }
3078
+ }
3079
+ function openIndex(opts) {
3080
+ return createNodeCodeIndex({
3081
+ repoRoot: opts.root,
3082
+ dbPath: opts.db ?? void 0,
3083
+ embedder: createNodeEmbedder()
3084
+ });
3085
+ }
3086
+ function baseOpts(argv) {
3087
+ return {
3088
+ root: resolveRoot(value(argv, "--root")),
3089
+ db: value(argv, "--db") ?? null,
3090
+ full: flag(argv, "--full"),
3091
+ embed: flag(argv, "--embed"),
3092
+ json: flag(argv, "--json")
3093
+ };
3094
+ }
3095
+ function parseIndexArgs(argv) {
3096
+ return baseOpts(argv);
3097
+ }
3098
+ function parseIndexSearchArgs(argv) {
3099
+ const positional = argv.find((a) => !a.startsWith("--"));
3100
+ const limitRaw = value(argv, "--limit");
3101
+ const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 15;
3102
+ const kind = value(argv, "--kind");
3103
+ return {
3104
+ ...baseOpts(argv),
3105
+ query: positional ?? "",
3106
+ kind: kind ?? null,
3107
+ limit: Number.isFinite(limit) && limit > 0 ? limit : 15
3108
+ };
3109
+ }
3110
+ async function runIndex(opts) {
3111
+ const idx = openIndex(opts);
3112
+ try {
3113
+ const stats = await idx.indexRepository({
3114
+ full: opts.full,
3115
+ embed: opts.embed,
3116
+ onProgress: opts.json ? void 0 : (p) => {
3117
+ process.stderr.write(
3118
+ `\r ${p.done}/${p.total} ${p.path.slice(0, 60)}`.padEnd(84)
3119
+ );
3120
+ }
3121
+ });
3122
+ if (opts.json) {
3123
+ process.stdout.write(JSON.stringify(stats) + "\n");
3124
+ } else {
3125
+ process.stderr.write("\r".padEnd(84) + "\r");
3126
+ const extra = opts.embed ? `, ${(await idx.status()).embeddingCount} embeddings` : "";
3127
+ process.stdout.write(
3128
+ ` \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})
3129
+ `
3130
+ );
3131
+ }
3132
+ markOrtOk();
3133
+ return 0;
3134
+ } finally {
3135
+ await idx.close();
3136
+ }
3137
+ }
3138
+ async function runIndexStatus(opts) {
3139
+ const idx = openIndex(opts);
3140
+ try {
3141
+ const status2 = await idx.status();
3142
+ if (opts.json) {
3143
+ process.stdout.write(JSON.stringify(status2) + "\n");
3144
+ return 0;
3145
+ }
3146
+ if (!status2.indexed) {
3147
+ process.stdout.write(" not indexed \u2014 run `maind index` first.\n");
3148
+ return 0;
3149
+ }
3150
+ const langs = Object.entries(status2.languages).map(([l, c]) => `${l}:${c}`).join(" ");
3151
+ const when = status2.lastIndexedAt ? new Date(status2.lastIndexedAt).toISOString() : "\u2014";
3152
+ process.stdout.write(
3153
+ ` ${status2.fileCount} files \xB7 ${status2.nodeCount} symbols \xB7 ${status2.edgeCount} edges \xB7 ${status2.embeddingCount} embeddings
3154
+ languages: ${langs || "\u2014"}
3155
+ last indexed: ${when} (commit ${status2.lastIndexCommit?.slice(0, 8) ?? "\u2014"}, schema v${status2.schemaVersion ?? "\u2014"})
3156
+ `
3157
+ );
3158
+ return 0;
3159
+ } finally {
3160
+ await idx.close();
3161
+ }
3162
+ }
3163
+ async function runIndexSearch(opts) {
3164
+ if (!opts.query) {
3165
+ process.stderr.write("maind: index-search needs a query. Usage: maind index-search <query> [--kind <k>] [--limit <n>]\n");
3166
+ return 2;
3167
+ }
3168
+ const idx = openIndex(opts);
3169
+ try {
3170
+ const hits = await idx.searchGraph(opts.query, { kind: opts.kind ?? void 0, limit: opts.limit });
3171
+ if (opts.json) {
3172
+ process.stdout.write(JSON.stringify(hits) + "\n");
3173
+ return 0;
3174
+ }
3175
+ if (hits.length === 0) {
3176
+ process.stdout.write(" no matches.\n");
3177
+ return 0;
3178
+ }
3179
+ for (const h of hits) {
3180
+ process.stdout.write(
3181
+ ` ${h.kind.padEnd(10)} ${h.qualifiedName} ${h.filePath}:${h.startLine}
3182
+ `
3183
+ );
3184
+ }
3185
+ return 0;
3186
+ } finally {
3187
+ await idx.close();
3188
+ }
3189
+ }
3190
+ async function runIndexSemantic(opts) {
3191
+ if (!opts.query) {
3192
+ process.stderr.write("maind: index-semantic needs a query. Usage: maind index-semantic <query> [--limit <n>]\n");
3193
+ return 2;
3194
+ }
3195
+ const idx = openIndex(opts);
3196
+ try {
3197
+ const hits = await idx.semanticQuery(opts.query, { limit: opts.limit });
3198
+ if (opts.json) {
3199
+ process.stdout.write(JSON.stringify(hits) + "\n");
3200
+ } else if (hits.length === 0) {
3201
+ process.stdout.write(
3202
+ " no matches \u2014 did you build embeddings? Run 'maind index --embed' first.\n"
3203
+ );
3204
+ } else {
3205
+ for (const h of hits) {
3206
+ process.stdout.write(
3207
+ ` ${(h.score ?? 0).toFixed(3)} ${h.kind.padEnd(10)} ${h.qualifiedName} ${h.filePath}:${h.startLine}
3208
+ `
3209
+ );
3210
+ }
3211
+ }
3212
+ markOrtOk();
3213
+ return 0;
3214
+ } finally {
3215
+ await idx.close();
3216
+ }
3217
+ }
3218
+ async function runIndexArch(opts) {
3219
+ const idx = openIndex(opts);
3220
+ try {
3221
+ const arch = await idx.getArchitecture({ topN: 10 });
3222
+ if (opts.json) {
3223
+ process.stdout.write(JSON.stringify(arch) + "\n");
3224
+ return 0;
3225
+ }
3226
+ const kinds = Object.entries(arch.kindDistribution).map(([k, c]) => `${k}:${c}`).join(" ");
3227
+ process.stdout.write(
3228
+ ` ${arch.fileCount} files \xB7 ${arch.nodeCount} symbols \xB7 ${arch.edgeCount} edges
3229
+ kinds: ${kinds}
3230
+ most-imported modules:
3231
+ `
3232
+ );
3233
+ for (const m of arch.topModules) {
3234
+ process.stdout.write(
3235
+ ` ${String(m.importsIn).padStart(4)} \u2190 ${m.filePath} (${m.symbolCount} symbols)
3236
+ `
3237
+ );
3238
+ }
3239
+ return 0;
3240
+ } finally {
3241
+ await idx.close();
3242
+ }
3243
+ }
3244
+
3245
+ // src/review.ts
3246
+ import { execFileSync as execFileSync5 } from "node:child_process";
3247
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
3248
+ import { dirname as dirname4 } from "node:path";
3249
+
3250
+ // src/review-render.ts
3251
+ var COMMENT_MARKER = "<!-- maind-review -->";
3252
+ var COMMENT_FINDINGS_CAP = 30;
3253
+ var FOOTER = "maind review \xB7 deterministic, provenance-backed";
3254
+ function buildReviewManifest(data, verdict, meta) {
3255
+ const review = data.review;
3256
+ return {
3257
+ schema: "maind.review.manifest/v1",
3258
+ generated_at: meta.generatedAt,
3259
+ key_prefix: meta.keyPrefix,
3260
+ run_ref: meta.runRef,
3261
+ base: meta.base,
3262
+ diff_stats: meta.diffStats,
3263
+ symbol_count: meta.symbolCount,
3264
+ risk_flags: review?.risk_flags ?? [],
3265
+ findings: (review?.convention_findings ?? []).map((f) => ({
3266
+ convention_id: f.convention_id,
3267
+ version: f.convention_version,
3268
+ severity: f.severity,
3269
+ grade: f.grade,
3270
+ matched_symbol_count: f.matched_symbols.length,
3271
+ matched_changed_symbol_count: f.matched_changed_symbols?.length ?? 0
3272
+ })),
3273
+ served: (review?.lesson_hints ?? []).map((l) => ({
3274
+ id: l.id,
3275
+ version: typeof l.version === "number" ? l.version : null,
3276
+ tier: l.tier ?? null
3277
+ })),
3278
+ verdict: verdict?.ok === true && typeof verdict.verdict === "string" ? {
3279
+ verdict: verdict.verdict,
3280
+ counts: {
3281
+ block: verdict.counts?.block ?? 0,
3282
+ warn: verdict.counts?.warn ?? 0,
3283
+ note: verdict.counts?.note ?? 0
3284
+ },
3285
+ fail_on: meta.failOn
3286
+ } : null,
3287
+ coverage_note: review?.coverage_note ?? null
3288
+ };
3289
+ }
3290
+ function severityBadge(f) {
3291
+ return `**[${f.severity}]**`;
3292
+ }
3293
+ function findingLine(f) {
3294
+ const cite = `\`${f.convention_id}@v${f.convention_version}\``;
3295
+ const grade = f.grade === "verdict" ? " \xB7 verdict-grade" : "";
3296
+ const changed = f.matched_changed_symbols?.length ?? 0;
3297
+ const matchInfo = changed > 0 ? `matched on changed symbols: ${formatSymbols(f.matched_changed_symbols ?? [])}` : `matched (co-located): ${formatSymbols(f.matched_symbols)}`;
3298
+ const summary = f.summary?.trim() ? ` \u2014 ${f.summary.trim()}` : "";
3299
+ return `- ${severityBadge(f)} ${cite}${summary}${grade}
3300
+ ${matchInfo}`;
3301
+ }
3302
+ function formatSymbols(symbols, max = 4) {
3303
+ const shown = symbols.slice(0, max).map((s) => `\`${s}\``);
3304
+ const rest = symbols.length - shown.length;
3305
+ return shown.join(", ") + (rest > 0 ? ` (+${rest} more)` : "");
3306
+ }
3307
+ function verdictLabel(verdict) {
3308
+ const counts = verdict.counts ?? {};
3309
+ return `**Verdict: ${(verdict.verdict ?? "?").toUpperCase()}** (block: ${counts.block ?? 0} \xB7 warn: ${counts.warn ?? 0} \xB7 note: ${counts.note ?? 0})`;
3310
+ }
3311
+ function baseLabel(meta) {
3312
+ return meta.base ? `against \`${meta.base}\`` : "uncommitted work";
3313
+ }
3314
+ function renderReviewMarkdown(data, verdict, meta) {
3315
+ const review = data.review;
3316
+ const findings = review?.convention_findings ?? [];
3317
+ const hints = review?.lesson_hints ?? [];
3318
+ const riskFlags = review?.risk_flags ?? [];
3319
+ const lines = [];
3320
+ lines.push("<!-- Generated by `maind review` \u2014 do not edit by hand. -->");
3321
+ lines.push(
3322
+ `<!-- generated_at: ${meta.generatedAt} | key: ${meta.keyPrefix}${meta.runRef ? ` | run: ${meta.runRef}` : ""} -->`
3323
+ );
3324
+ lines.push("");
3325
+ lines.push("# maind review");
3326
+ lines.push("");
3327
+ lines.push(
3328
+ `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}`
3329
+ );
3330
+ lines.push("");
3331
+ if (verdict?.ok === true && verdict.verdict) {
3332
+ lines.push(verdictLabel(verdict));
3333
+ if (verdict.trust_note) {
3334
+ lines.push("");
3335
+ lines.push(`_${verdict.trust_note}_`);
3336
+ }
3337
+ lines.push("");
3338
+ }
3339
+ if (findings.length > 0) {
3340
+ lines.push("## Convention findings");
3341
+ lines.push("");
3342
+ for (const f of findings) lines.push(findingLine(f));
3343
+ lines.push("");
3344
+ } else {
3345
+ lines.push("## Convention findings");
3346
+ lines.push("");
3347
+ lines.push("_No convention findings for this change set._");
3348
+ lines.push("");
3349
+ }
3350
+ if (riskFlags.length > 0) {
3351
+ lines.push("## Risk flags");
3352
+ lines.push("");
3353
+ lines.push(
3354
+ "_Path heuristics from the local diff \u2014 informational, they never block._"
3355
+ );
3356
+ lines.push("");
3357
+ for (const flag2 of riskFlags) lines.push(`- \`${flag2}\``);
3358
+ lines.push("");
3359
+ }
3360
+ if (hints.length > 0) {
3361
+ lines.push("## Lesson hints");
3362
+ lines.push("");
3363
+ lines.push("_Semantic suggestions \u2014 advisory, never part of a verdict._");
3364
+ lines.push("");
3365
+ for (const h of hints) {
3366
+ const parts = [`\`${h.id}\``];
3367
+ if (h.tier) parts.push(h.tier);
3368
+ if (h.match_reason) parts.push(h.match_reason);
3369
+ lines.push(`- **${h.title}** (${parts.join(" \xB7 ")})`);
3370
+ }
3371
+ lines.push("");
3372
+ }
3373
+ if (review?.coverage_note) {
3374
+ lines.push("## Coverage");
3375
+ lines.push("");
3376
+ lines.push(`> ${review.coverage_note}`);
3377
+ lines.push("");
3378
+ }
3379
+ lines.push("---");
3380
+ lines.push(FOOTER);
3381
+ lines.push("");
3382
+ return lines.join("\n");
3383
+ }
3384
+ function renderReviewComment(data, verdict, meta, reportPath) {
3385
+ const review = data.review;
3386
+ const findings = review?.convention_findings ?? [];
3387
+ const riskFlags = review?.risk_flags ?? [];
3388
+ const hints = review?.lesson_hints ?? [];
3389
+ const lines = [];
3390
+ lines.push(COMMENT_MARKER);
3391
+ lines.push("## maind review");
3392
+ lines.push("");
3393
+ if (verdict?.ok === true && verdict.verdict) {
3394
+ lines.push(verdictLabel(verdict));
3395
+ lines.push("");
3396
+ }
3397
+ if (findings.length === 0) {
3398
+ lines.push(
3399
+ `No convention findings for this change set (${meta.symbolCount} changed symbol(s) reviewed ${baseLabel(meta)}).`
3400
+ );
3401
+ lines.push("");
3402
+ } else {
3403
+ const shown = findings.slice(0, COMMENT_FINDINGS_CAP);
3404
+ for (const f of shown) lines.push(findingLine(f));
3405
+ if (findings.length > shown.length) {
3406
+ lines.push("");
3407
+ lines.push(
3408
+ `\u2026and ${findings.length - shown.length} more finding(s) \u2014 see the \`${reportPath}\` build artifact.`
3409
+ );
3410
+ }
3411
+ lines.push("");
3412
+ }
3413
+ if (riskFlags.length > 0) {
3414
+ lines.push(
3415
+ `**Risk flags:** ${riskFlags.map((f) => `\`${f}\``).join(" \xB7 ")} _(heuristics, never blocking)_`
3416
+ );
3417
+ lines.push("");
3418
+ }
3419
+ if (hints.length > 0) {
3420
+ lines.push("**Related lessons:** " + hints.map((h) => `\`${h.id}\``).join(" \xB7 "));
3421
+ lines.push("");
3422
+ }
3423
+ if (review?.coverage_note) {
3424
+ lines.push(`> ${review.coverage_note}`);
3425
+ lines.push("");
3426
+ }
3427
+ lines.push("---");
3428
+ lines.push(`_${FOOTER}_`);
3429
+ lines.push("");
3430
+ return lines.join("\n");
3431
+ }
3432
+ function renderFailOpenReview(meta, reason) {
3433
+ return [
3434
+ "<!-- Generated by `maind review` (fail-open path). -->",
3435
+ `<!-- generated_at: ${meta.generatedAt} -->`,
3436
+ "",
3437
+ "# maind review",
3438
+ "",
3439
+ `_maind review was unavailable for this run: ${reason}_`,
3440
+ "",
3441
+ "The build can proceed normally \u2014 this file is informational only.",
3442
+ ""
3443
+ ].join("\n");
3444
+ }
3445
+ function renderFailOpenComment(meta, reason) {
3446
+ return [
3447
+ COMMENT_MARKER,
3448
+ "## maind review",
3449
+ "",
3450
+ `_maind review was unavailable for this run: ${reason}_`,
3451
+ "",
3452
+ "---",
3453
+ `_${FOOTER}_`,
3454
+ ""
3455
+ ].join("\n");
3456
+ }
3457
+
3458
+ // src/review.ts
3459
+ var PROJECT_KEY_RE3 = /^[a-f0-9]{64}$/;
3460
+ var CLI_CLIENT_VERSION = "0.6.1";
3461
+ var DEFAULT_COMMENT_PATH = ".maind/review-comment.md";
3462
+ var SYMBOL_CAP = 64;
3463
+ function parseReviewArgs(argv) {
3464
+ const get = (name) => {
3465
+ const i = argv.indexOf(name);
3466
+ if (i === -1 || i + 1 >= argv.length) return void 0;
3467
+ return argv[i + 1];
3468
+ };
3469
+ const has = (name) => argv.includes(name);
3470
+ let comment = null;
3471
+ const ci = argv.indexOf("--comment");
3472
+ if (ci !== -1) {
3473
+ const next = argv[ci + 1];
3474
+ comment = next && !next.startsWith("--") ? next : DEFAULT_COMMENT_PATH;
3475
+ }
3476
+ const failOnRaw = get("--fail-on");
3477
+ const failOn = failOnRaw === "block" || failOnRaw === "warn" ? failOnRaw : null;
3478
+ return {
3479
+ base: get("--base") ?? null,
3480
+ uncommitted: has("--uncommitted"),
3481
+ summary: get("--summary") ?? null,
3482
+ json: has("--json"),
3483
+ out: get("--out") ?? ".maind/review.md",
3484
+ comment,
3485
+ gate: has("--gate"),
3486
+ failOn,
3487
+ strict: has("--strict"),
3488
+ noReport: has("--no-report"),
3489
+ projectKey: get("--project-key") ?? null,
3490
+ mcpUrl: get("--mcp-url") ?? null,
3491
+ runRef: get("--run-ref") ?? process.env.GITHUB_RUN_ID ?? null,
3492
+ clientVersion: CLI_CLIENT_VERSION
3493
+ };
3494
+ }
3495
+ function nowIso3() {
3496
+ return (/* @__PURE__ */ new Date()).toISOString();
3497
+ }
3498
+ function writeArtifact3(path, content) {
3499
+ const dir = dirname4(path);
3500
+ if (dir && dir !== ".") mkdirSync4(dir, { recursive: true });
3501
+ writeFileSync4(path, content, "utf8");
3502
+ }
3503
+ function manifestPathFor(out) {
3504
+ return out.endsWith(".md") ? out.slice(0, -3) + ".json" : out + ".json";
3505
+ }
3506
+ function verdictCrossesFailOn(verdict, failOn) {
3507
+ return failOn === "block" ? verdict === "block" : verdict === "block" || verdict === "warn";
3508
+ }
3509
+ function resolveRepoRoot() {
3510
+ try {
3511
+ return execFileSync5("git", ["rev-parse", "--show-toplevel"], {
3512
+ encoding: "utf8",
3513
+ stdio: ["ignore", "pipe", "ignore"]
3514
+ }).trim();
3515
+ } catch {
3516
+ return null;
3517
+ }
3518
+ }
3519
+ async function runReview(opts) {
3520
+ const meta = {
3521
+ generatedAt: nowIso3(),
3522
+ keyPrefix: "unknown",
3523
+ runRef: opts.runRef,
3524
+ base: opts.base,
3525
+ mcpUrl: opts.mcpUrl ?? process.env.MAIND_MCP_URL ?? "https://mcp.maind.dev/mcp",
3526
+ symbolCount: 0,
3527
+ diffStats: null,
3528
+ gate: opts.gate,
3529
+ failOn: opts.gate ? opts.failOn : null
3530
+ };
3531
+ if (opts.failOn && !opts.gate) {
3532
+ warn5("--fail-on has no effect without --gate (advisory runs always exit 0).");
3533
+ }
3534
+ const failOpen = (reason) => {
3535
+ warn5(`review unavailable, ${opts.strict ? "failing (--strict)" : "continuing"}: ${reason}`);
3536
+ try {
3537
+ writeArtifact3(opts.out, renderFailOpenReview(meta, reason));
3538
+ const manifest = buildReviewManifest({}, null, meta);
3539
+ manifest.error = reason;
3540
+ writeArtifact3(manifestPathFor(opts.out), JSON.stringify(manifest, null, 2) + "\n");
3541
+ if (opts.comment) writeArtifact3(opts.comment, renderFailOpenComment(meta, reason));
3542
+ if (opts.json) process.stdout.write(JSON.stringify(manifest, null, 2) + "\n");
3543
+ } catch (writeErr) {
3544
+ warn5(`could not write fail-open artifacts: ${msg5(writeErr)}`);
3545
+ return opts.strict ? 2 : 1;
3546
+ }
3547
+ return opts.strict ? 2 : 0;
3548
+ };
3549
+ try {
3550
+ const root = resolveRepoRoot();
3551
+ if (!root) {
3552
+ return failOpen("not a git repository \u2014 review is diff-based.");
3553
+ }
3554
+ const engine = createNodeCodeIndex({ repoRoot: root });
3555
+ let result;
3556
+ try {
3557
+ const status2 = await engine.status();
3558
+ if (!status2.indexed || status2.fileCount === 0) {
3559
+ warn5("no local index found \u2014 building it now (first run in this checkout)\u2026");
3560
+ const stats = await engine.indexRepository();
3561
+ warn5(`indexed ${stats.filesParsed} file(s) in ${Math.round(stats.durationMs / 1e3)}s.`);
3562
+ }
3563
+ result = await engine.reviewChanges({
3564
+ since: opts.base ?? void 0,
3565
+ includeUncommitted: opts.base ? opts.uncommitted : true,
3566
+ limit: SYMBOL_CAP
3567
+ });
3568
+ } finally {
3569
+ await engine.close().catch(() => void 0);
3570
+ }
3571
+ const diffStats = {
3572
+ changed_file_count: result.changedFiles,
3573
+ unindexed_file_count: result.changedFiles - result.indexedFiles,
3574
+ stale_file_count: result.staleFiles,
3575
+ truncated: result.truncated,
3576
+ hunk_coverage: result.hunkCoverage,
3577
+ base_kind: result.baseKind
3578
+ };
3579
+ meta.diffStats = diffStats;
3580
+ meta.symbolCount = result.symbols.length;
3581
+ const summary = opts.summary?.trim() || void 0;
3582
+ if (result.symbols.length === 0 && result.riskFlags.length === 0 && !summary) {
3583
+ const empty = {
3584
+ ok: true,
3585
+ review: {
3586
+ convention_findings: [],
3587
+ lesson_hints: [],
3588
+ risk_flags: [],
3589
+ coverage_note: result.changedFiles === 0 ? "No changed files detected." : "Changed files found but no indexed symbols \u2014 nothing reviewable at symbol level."
3590
+ }
3591
+ };
3592
+ writeAll(empty, null, meta, opts);
3593
+ process.stderr.write(
3594
+ `maind review: no reviewable changes (${result.changedFiles} changed file(s)) \u2192 ${opts.out}
3595
+ `
3596
+ );
3597
+ return 0;
3598
+ }
3599
+ const auth = resolveAuth();
3600
+ meta.keyPrefix = auth.keyPrefix;
3601
+ const overrideKey = opts.projectKey?.trim().toLowerCase();
3602
+ const projectKey = overrideKey && PROJECT_KEY_RE3.test(overrideKey) ? overrideKey : gatherGitState().projectKey;
3603
+ const client = new McpClient({
3604
+ apiKey: auth.apiKey,
3605
+ deviceId: auth.deviceId,
3606
+ clientVersion: opts.clientVersion,
3607
+ mcpUrl: opts.mcpUrl ?? void 0,
3608
+ projectKey
3609
+ });
3610
+ const reviewArgs = {
3611
+ changed_symbols: result.symbols.map((s) => ({
3612
+ name: s.name,
3613
+ qualified_name: s.qualifiedName,
3614
+ kind: s.kind,
3615
+ language: s.language,
3616
+ change: s.change,
3617
+ changed_line_count: s.changedLineCount
3618
+ })),
3619
+ risk_flags: result.riskFlags,
3620
+ diff_stats: diffStats,
3621
+ ...summary ? { change_summary: summary } : {},
3622
+ ...projectKey ? { project_key: projectKey } : {}
3623
+ };
3624
+ const review = await client.callTool("review_changes", reviewArgs);
3625
+ if (review.ok !== true || !review.review) {
3626
+ return failOpen(
3627
+ `review_changes rejected: ${review.error ?? "unknown_error"}${review.message ? ` \u2014 ${review.message}` : ""}`
3628
+ );
3629
+ }
3630
+ let verdict = null;
3631
+ if (opts.gate) {
3632
+ const findings = review.review.convention_findings ?? [];
3633
+ try {
3634
+ verdict = await client.callTool("review_verdict", {
3635
+ findings: findings.map((f) => ({
3636
+ convention_id: f.convention_id,
3637
+ convention_version: f.convention_version,
3638
+ matched_symbols: f.matched_symbols,
3639
+ matched_changed_symbols: f.matched_changed_symbols ?? []
3640
+ })),
3641
+ risk_flags: review.review.risk_flags ?? [],
3642
+ ...projectKey ? { project_key: projectKey } : {}
3643
+ });
3644
+ if (verdict.ok !== true) {
3645
+ if (verdict.error === "requires_team" || verdict.error === "requires_enterprise") {
3646
+ warn5(
3647
+ "gate skipped: review_verdict is a Team-plan feature \u2014 findings below are advisory. " + (verdict.message ?? "Upgrade at https://maind.dev/pricing.")
3648
+ );
3649
+ verdict = null;
3650
+ } else {
3651
+ if (opts.strict) {
3652
+ return failOpen(
3653
+ `review_verdict failed: ${verdict.error ?? "unknown_error"}${verdict.message ? ` \u2014 ${verdict.message}` : ""}`
3654
+ );
3655
+ }
3656
+ warn5(
3657
+ `gate unavailable (continuing advisory): ${verdict.error ?? "unknown_error"}`
3658
+ );
3659
+ verdict = null;
3660
+ }
3661
+ }
3662
+ } catch (err2) {
3663
+ if (opts.strict) return failOpen(`review_verdict failed: ${msg5(err2)}`);
3664
+ warn5(`gate unavailable (continuing advisory): ${msg5(err2)}`);
3665
+ verdict = null;
3666
+ }
3667
+ }
3668
+ writeAll(review, verdict, meta, opts);
3669
+ const findingCount = review.review.convention_findings?.length ?? 0;
3670
+ const verdictNote = verdict?.verdict ? ` \xB7 verdict: ${verdict.verdict}` : "";
3671
+ process.stderr.write(
3672
+ `maind review: ${findingCount} finding(s), ${review.review.risk_flags?.length ?? 0} risk flag(s)${verdictNote} \u2192 ${opts.out}
3673
+ `
3674
+ );
3675
+ if (opts.gate && opts.failOn && verdict?.verdict) {
3676
+ if (verdictCrossesFailOn(verdict.verdict, opts.failOn)) {
3677
+ warn5(
3678
+ `verdict '${verdict.verdict}' crossed --fail-on ${opts.failOn} \u2014 failing the check.`
3679
+ );
3680
+ return 1;
3681
+ }
3682
+ }
3683
+ return 0;
3684
+ } catch (err2) {
3685
+ const reason = err2 instanceof MissingApiKeyError ? err2.message : msg5(err2);
3686
+ return failOpen(reason);
3687
+ }
3688
+ }
3689
+ function writeAll(review, verdict, meta, opts) {
3690
+ writeArtifact3(opts.out, renderReviewMarkdown(review, verdict, meta));
3691
+ const manifest = buildReviewManifest(review, verdict, meta);
3692
+ writeArtifact3(manifestPathFor(opts.out), JSON.stringify(manifest, null, 2) + "\n");
3693
+ if (opts.comment) {
3694
+ writeArtifact3(opts.comment, renderReviewComment(review, verdict, meta, opts.out));
3695
+ }
3696
+ if (opts.json) process.stdout.write(JSON.stringify(manifest, null, 2) + "\n");
3697
+ }
3698
+ function msg5(err2) {
3699
+ return err2 instanceof Error ? err2.message : String(err2);
3700
+ }
3701
+ function warn5(line) {
3702
+ process.stderr.write(`maind review: ${line}
3703
+ `);
3704
+ }
3705
+
3706
+ // src/sync-cmd.ts
3707
+ import { execFileSync as execFileSync6 } from "node:child_process";
3708
+ var CONSENT_POLICY_VERSION = "2026-07-05";
3709
+ var DEFAULT_DASHBOARD_URL = "https://app.maind.dev";
3710
+ function err(line) {
3711
+ process.stderr.write(`maind: ${line}
3712
+ `);
3713
+ }
3714
+ function repoRoot() {
3715
+ try {
3716
+ return execFileSync6("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
3717
+ } catch {
3718
+ return process.cwd();
3719
+ }
3720
+ }
3721
+ function dashboardUrl() {
3722
+ const raw = process.env.MAIND_DASHBOARD_URL?.trim();
3723
+ return (raw && raw.length > 0 ? raw : DEFAULT_DASHBOARD_URL).replace(/\/+$/, "");
3724
+ }
3725
+ function makeSync(json) {
3726
+ let auth;
3727
+ try {
3728
+ auth = resolveAuth();
3729
+ } catch (e) {
3730
+ if (e instanceof MissingApiKeyError) {
3731
+ err(e.message);
3732
+ return null;
3733
+ }
3734
+ throw e;
3735
+ }
3736
+ try {
3737
+ return createCodeIndexSync({
3738
+ repoRoot: repoRoot(),
3739
+ endpoint: dashboardUrl(),
3740
+ apiKey: auth.apiKey,
3741
+ deviceId: auth.deviceId,
3742
+ onProgress: json ? void 0 : (p) => process.stderr.write(` ${p.phase}: ${p.done}/${p.total}\r`)
3743
+ });
3744
+ } catch (e) {
3745
+ err(e instanceof Error ? e.message : String(e));
3746
+ return null;
3747
+ }
3748
+ }
3749
+ function msg6(e) {
3750
+ return e instanceof Error ? e.message : String(e);
3751
+ }
3752
+ function parseSyncArgs(argv) {
3753
+ const positional = argv.find((a) => !a.startsWith("-"));
3754
+ const sub = positional === "status" || positional === "enable" || positional === "disable" ? positional : "push";
3755
+ return { sub, json: argv.includes("--json") };
3756
+ }
3757
+ async function runSync(opts) {
3758
+ const sync = makeSync(opts.json);
3759
+ if (!sync) return 1;
3760
+ try {
3761
+ switch (opts.sub) {
3762
+ case "enable":
3763
+ return await enable(sync);
3764
+ case "disable":
3765
+ return await disable(sync);
3766
+ case "status":
3767
+ return await status(sync, opts.json);
3768
+ default:
3769
+ return await push(sync, opts.json);
3770
+ }
3771
+ } catch (e) {
3772
+ if (e instanceof SyncError && e.code === "consent_required") {
3773
+ err("codebase cloud-sync is not enabled \u2014 run `maind sync enable` first.");
3774
+ return 1;
3775
+ }
3776
+ err(`sync failed: ${msg6(e)}`);
3777
+ return 1;
3778
+ } finally {
3779
+ await sync.close().catch(() => {
3780
+ });
3781
+ }
3782
+ }
3783
+ async function enable(sync) {
3784
+ await sync.setConsent(true, CONSENT_POLICY_VERSION);
3785
+ process.stdout.write(
3786
+ "codebase cloud-sync enabled. Structured rows (symbol names, topology, hashes) will be pushed on `maind sync` \u2014 never source, snippets, or plaintext paths.\n"
3787
+ );
3788
+ return 0;
3789
+ }
3790
+ async function disable(sync) {
3791
+ await sync.setConsent(false);
3792
+ process.stdout.write("codebase cloud-sync disabled.\n");
3793
+ return 0;
3794
+ }
3795
+ async function status(sync, json) {
3796
+ const local = await sync.localStatus();
3797
+ const remote = await sync.remoteStatus();
3798
+ if (json) {
3799
+ process.stdout.write(JSON.stringify({ ok: true, local, remote }, null, 2) + "\n");
3800
+ return 0;
3801
+ }
3802
+ process.stdout.write(
3803
+ `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()}` : ""}
3804
+ cloud: ${remote.indexed ? `${remote.node_count ?? "?"} symbols \xB7 ${remote.edge_count ?? "?"} edges` : "not synced"}
3805
+ `
3806
+ );
3807
+ return 0;
3808
+ }
3809
+ async function push(sync, json) {
3810
+ const local = await sync.localStatus();
3811
+ if (!local.indexed || local.nodeCount === 0) {
3812
+ err("no local index found \u2014 run `maind index` first.");
3813
+ return 1;
3814
+ }
3815
+ const res = await sync.push();
3816
+ process.stderr.write("\n");
3817
+ if (json) {
3818
+ process.stdout.write(JSON.stringify({ ok: true, ...res }, null, 2) + "\n");
3819
+ return 0;
3820
+ }
3821
+ process.stdout.write(
3822
+ `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)}` : ""}.
3823
+ `
3824
+ );
3825
+ return 0;
3826
+ }
3827
+
3828
+ // src/index.ts
3829
+ var VERSION = "0.6.1";
3830
+ var HELP = `maind ${VERSION} \u2014 headless maind CLI for CI + git context
22
3831
 
23
3832
  Usage:
24
3833
  maind context [options] Pull stack-relevant lessons + conventions into the runner
@@ -38,10 +3847,10 @@ Usage:
38
3847
  maind --help Show this help
39
3848
  maind --version Print version
40
3849
 
41
- review options (advisory by default — exits 0 unless --gate + --fail-on):
3850
+ review options (advisory by default \u2014 exits 0 unless --gate + --fail-on):
42
3851
  --base <ref> Review the diff against this git ref (e.g. origin/main)
43
3852
  --uncommitted With --base: also include uncommitted changes (default without --base)
44
- --summary <text> Prose change summary — enables index-less review, sharpens hints
3853
+ --summary <text> Prose change summary \u2014 enables index-less review, sharpens hints
45
3854
  --out <path> Findings report markdown (default .maind/review.md; always written,
46
3855
  a manifest JSON lands next to it as review.json)
47
3856
  --comment [<path>] Also write PR-comment markdown (default .maind/review-comment.md);
@@ -53,32 +3862,32 @@ review options (advisory by default — exits 0 unless --gate + --fail-on):
53
3862
  --no-report Do not report run counts to maind (reserved; run reporting
54
3863
  lands with the review_runs ingest in a later release)
55
3864
  --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)
3865
+ --mcp-url <url> Override MCP endpoint (default $MAIND_MCP_URL or production)
57
3866
 
58
3867
  git-context options (run at session-start; reads local git + gh, all fail-open):
59
3868
  --out <path> Briefing markdown output (default .maind/git-context.md)
60
3869
  --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)
3870
+ --no-mcp Local git state only \u2014 skip peer-conflict + lesson enrichment
3871
+ --mcp-url <url> Override MCP endpoint (default $MAIND_MCP_URL or production)
63
3872
 
64
3873
  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
3874
+ --lesson <path> Agent-authored lesson markdown (frontmatter + body) \u2014 required
3875
+ --rationale <text> Why this is a reusable lesson \u2014 required
3876
+ --failure-type <t> e.g. tsc-error, test-failure, lint-error \u2014 required
68
3877
  --signature <s> Stable dedup key (default: failure-type + lesson hash)
69
3878
  --summary <text> Short failure description
70
3879
  --remediation <text> Suggested fix
71
3880
  --logs <path> Optional failure-log file (truncated to 8k)
72
3881
  --provider <p> CI provider (default github-actions)
73
3882
  --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)
3883
+ --report-url <url> Override ingest endpoint (default $MAIND_REPORT_URL or prod)
75
3884
 
76
3885
  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
3886
+ --outcome <s> Build outcome, e.g. \${{ job.status }} (success|failure) \u2014 required
78
3887
  --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)
3888
+ --run-ref <id> Build identifier (default manifest.run_ref or $GITHUB_RUN_ID)
80
3889
  --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)
3890
+ --result-url <url> Override ingest endpoint (default $MAIND_RESULT_URL or prod)
82
3891
 
83
3892
  context options:
84
3893
  --stack <a,b> Stack hint (seeds the platforms filter), e.g. nextjs,supabase
@@ -89,8 +3898,8 @@ context options:
89
3898
  --budget <n> Max lessons to pull (1-50, default 15; server caps search at 10)
90
3899
  --out <path> Context markdown output (default .maind/context.md)
91
3900
  --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)
3901
+ --run-ref <id> Build identifier for the manifest (default $GITHUB_RUN_ID)
3902
+ --mcp-url <url> Override MCP endpoint (default $MAIND_MCP_URL or production)
94
3903
 
95
3904
  Auth:
96
3905
  Reads MAIND_API_KEY (a read-scoped CI key from https://maind.dev/keys), or falls
@@ -98,77 +3907,68 @@ Auth:
98
3907
  error it still writes explanatory artifacts and exits 0 so the build proceeds.
99
3908
  `;
100
3909
  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);
3910
+ const argv = process.argv.slice(2);
3911
+ const cmd = argv[0];
3912
+ if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
3913
+ process.stdout.write(HELP);
3914
+ return 0;
3915
+ }
3916
+ if (cmd === "--version" || cmd === "-v") {
3917
+ process.stdout.write(VERSION + "\n");
3918
+ return 0;
3919
+ }
3920
+ if (cmd === "context") {
3921
+ return runContext(parseContextArgs(argv.slice(1)));
3922
+ }
3923
+ if (cmd === "git-context") {
3924
+ return runGitContext(parseGitContextArgs(argv.slice(1)));
3925
+ }
3926
+ if (cmd === "project-key") {
3927
+ const key = gatherGitState().projectKey;
3928
+ if (!key) {
3929
+ process.stderr.write("maind: not a git repo with a remote \u2014 no project_key.\n");
3930
+ return 1;
135
3931
  }
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;
3932
+ process.stdout.write(key + "\n");
3933
+ return 0;
3934
+ }
3935
+ const usesOrt = cmd === "index" && argv.includes("--embed") || cmd === "index-semantic";
3936
+ if (usesOrt && !isOrtWorker(argv)) {
3937
+ return runOrtIsolated(argv);
3938
+ }
3939
+ if (cmd === "index") {
3940
+ return runIndex(parseIndexArgs(argv.slice(1)));
3941
+ }
3942
+ if (cmd === "index-status") {
3943
+ return runIndexStatus(parseIndexArgs(argv.slice(1)));
3944
+ }
3945
+ if (cmd === "index-search") {
3946
+ return runIndexSearch(parseIndexSearchArgs(argv.slice(1)));
3947
+ }
3948
+ if (cmd === "index-semantic") {
3949
+ return runIndexSemantic(parseIndexSearchArgs(argv.slice(1)));
3950
+ }
3951
+ if (cmd === "index-arch") {
3952
+ return runIndexArch(parseIndexArgs(argv.slice(1)));
3953
+ }
3954
+ if (cmd === "sync") {
3955
+ return runSync(parseSyncArgs(argv.slice(1)));
3956
+ }
3957
+ if (cmd === "review") {
3958
+ return runReview(parseReviewArgs(argv.slice(1)));
3959
+ }
3960
+ if (cmd === "ci-report") {
3961
+ return runCiReport(parseCiReportArgs(argv.slice(1)));
3962
+ }
3963
+ if (cmd === "ci-result") {
3964
+ return runCiResult(parseCiResultArgs(argv.slice(1)));
3965
+ }
3966
+ process.stderr.write(`maind: unknown command '${cmd}'. Run 'maind --help'.
3967
+ `);
3968
+ return 2;
165
3969
  }
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);
3970
+ main().then((code) => process.exit(code)).catch((err2) => {
3971
+ process.stderr.write(`maind: fatal: ${err2 instanceof Error ? err2.stack ?? err2.message : String(err2)}
3972
+ `);
3973
+ process.exit(1);
173
3974
  });
174
- //# sourceMappingURL=index.js.map