@swarmvaultai/engine 0.1.19 → 0.1.21

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/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  uniqueBy,
22
22
  writeFileIfChanged,
23
23
  writeJsonFile
24
- } from "./chunk-NCSZ4AKP.js";
24
+ } from "./chunk-QMW7OISM.js";
25
25
 
26
26
  // src/agents.ts
27
27
  import fs from "fs/promises";
@@ -39,6 +39,7 @@ function buildManagedBlock(target) {
39
39
  "- Treat `raw/` as immutable source input.",
40
40
  "- Treat `wiki/` as generated markdown owned by the agent and compiler workflow.",
41
41
  "- Read `wiki/graph/report.md` before broad file searching when it exists; otherwise start with `wiki/index.md`.",
42
+ "- For graph questions, prefer `swarmvault graph query`, `swarmvault graph path`, and `swarmvault graph explain` before broad grep/glob searching.",
42
43
  "- Preserve frontmatter fields including `page_id`, `source_ids`, `node_ids`, `freshness`, and `source_hashes`.",
43
44
  "- Save high-value answers back into `wiki/outputs/` instead of leaving them only in chat.",
44
45
  "- Prefer `swarmvault ingest`, `swarmvault compile`, `swarmvault query`, and `swarmvault lint` for SwarmVault maintenance tasks.",
@@ -50,6 +51,33 @@ function buildManagedBlock(target) {
50
51
  }
51
52
  return body;
52
53
  }
54
+ var claudeHookMatcher = "Glob|Grep";
55
+ var claudeHookCommand = "if [ -f wiki/graph/report.md ]; then echo 'swarmvault: Graph report exists. Read wiki/graph/report.md before broad raw-file searching.'; fi";
56
+ async function installClaudeHook(rootDir) {
57
+ const settingsPath = path.join(rootDir, ".claude", "settings.json");
58
+ await ensureDir(path.dirname(settingsPath));
59
+ let settings = {};
60
+ if (await fileExists(settingsPath)) {
61
+ try {
62
+ settings = JSON.parse(await fs.readFile(settingsPath, "utf8"));
63
+ } catch {
64
+ settings = {};
65
+ }
66
+ }
67
+ const hooks = settings.hooks ?? {};
68
+ const preToolUse = hooks.PreToolUse ?? [];
69
+ const exists = preToolUse.some((entry) => entry.matcher === claudeHookMatcher && JSON.stringify(entry).includes("swarmvault:"));
70
+ if (!exists) {
71
+ preToolUse.push({
72
+ matcher: claudeHookMatcher,
73
+ hooks: [{ type: "command", command: claudeHookCommand }]
74
+ });
75
+ }
76
+ settings.hooks = { ...hooks, PreToolUse: preToolUse };
77
+ await fs.writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
78
+ `, "utf8");
79
+ return settingsPath;
80
+ }
53
81
  function targetPathForAgent(rootDir, agent) {
54
82
  switch (agent) {
55
83
  case "codex":
@@ -87,7 +115,7 @@ async function upsertManagedBlock(filePath, block) {
87
115
  ${block}
88
116
  `, "utf8");
89
117
  }
90
- async function installAgent(rootDir, agent) {
118
+ async function installAgent(rootDir, agent, options = {}) {
91
119
  await initWorkspace(rootDir);
92
120
  const target = targetPathForAgent(rootDir, agent);
93
121
  switch (agent) {
@@ -99,6 +127,9 @@ async function installAgent(rootDir, agent) {
99
127
  return target;
100
128
  case "claude": {
101
129
  await upsertManagedBlock(target, buildManagedBlock("claude"));
130
+ if (options.claudeHook) {
131
+ await installClaudeHook(rootDir);
132
+ }
102
133
  return target;
103
134
  }
104
135
  case "gemini": {
@@ -125,12 +156,445 @@ async function installConfiguredAgents(rootDir) {
125
156
  dedupedTargets.set(target, agent);
126
157
  }
127
158
  }
128
- return Promise.all([...dedupedTargets.values()].map((agent) => installAgent(rootDir, agent)));
159
+ return Promise.all(
160
+ [...dedupedTargets.values()].map(
161
+ (agent) => installAgent(rootDir, agent, {
162
+ claudeHook: agent === "claude"
163
+ })
164
+ )
165
+ );
166
+ }
167
+
168
+ // src/graph-export.ts
169
+ import fs2 from "fs/promises";
170
+ import path2 from "path";
171
+ var NODE_COLORS = {
172
+ source: "#f59e0b",
173
+ module: "#fb7185",
174
+ symbol: "#8b5cf6",
175
+ rationale: "#14b8a6",
176
+ concept: "#0ea5e9",
177
+ entity: "#22c55e"
178
+ };
179
+ function xmlEscape(value) {
180
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
181
+ }
182
+ function cypherEscape(value) {
183
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
184
+ }
185
+ function relationType(relation) {
186
+ const normalized = relation.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
187
+ return normalized || "RELATED_TO";
188
+ }
189
+ function graphPageById(graph) {
190
+ return new Map(graph.pages.map((page) => [page.id, page]));
191
+ }
192
+ function graphNodeById(graph) {
193
+ return new Map(graph.nodes.map((node) => [node.id, node]));
194
+ }
195
+ function sortedCommunities(graph) {
196
+ const known = (graph.communities ?? []).map((community) => ({
197
+ ...community,
198
+ nodeIds: [...community.nodeIds].sort((left, right) => left.localeCompare(right))
199
+ }));
200
+ const knownIds = new Set(known.flatMap((community) => community.nodeIds));
201
+ const unassigned = graph.nodes.filter((node) => !knownIds.has(node.id)).sort((left, right) => left.label.localeCompare(right.label) || left.id.localeCompare(right.id)).map((node) => node.id);
202
+ if (unassigned.length) {
203
+ known.push({
204
+ id: "community:unassigned",
205
+ label: "Unassigned",
206
+ nodeIds: unassigned
207
+ });
208
+ }
209
+ return known.sort((left, right) => left.label.localeCompare(right.label) || left.id.localeCompare(right.id));
210
+ }
211
+ function layoutGraph(graph) {
212
+ const communities = sortedCommunities(graph);
213
+ const width = 1600;
214
+ const height = Math.max(900, 420 * Math.max(1, Math.ceil(communities.length / 3)));
215
+ const columns = Math.max(1, Math.ceil(Math.sqrt(Math.max(1, communities.length))));
216
+ const nodesById = graphNodeById(graph);
217
+ const positioned = [];
218
+ communities.forEach((community, index) => {
219
+ const col = index % columns;
220
+ const row = Math.floor(index / columns);
221
+ const centerX = 240 + col * 460;
222
+ const centerY = 220 + row * 360;
223
+ const members = community.nodeIds.map((nodeId) => nodesById.get(nodeId)).filter((node) => Boolean(node)).sort((left, right) => left.label.localeCompare(right.label) || left.id.localeCompare(right.id));
224
+ const radius = Math.max(40, 36 * Math.sqrt(members.length));
225
+ members.forEach((node, memberIndex) => {
226
+ const angle = Math.PI * 2 * memberIndex / Math.max(1, members.length);
227
+ positioned.push({
228
+ node,
229
+ x: centerX + Math.cos(angle) * radius,
230
+ y: centerY + Math.sin(angle) * radius
231
+ });
232
+ });
233
+ });
234
+ return { width, height, nodes: positioned };
235
+ }
236
+ function nodeShape(positioned) {
237
+ const { node, x, y } = positioned;
238
+ const fill = NODE_COLORS[node.type] ?? "#94a3b8";
239
+ if (node.type === "module") {
240
+ return `<rect x="${(x - 32).toFixed(1)}" y="${(y - 18).toFixed(1)}" width="64" height="36" rx="10" fill="${fill}" stroke="#0f172a" stroke-width="2" />`;
241
+ }
242
+ if (node.type === "symbol") {
243
+ const points = [
244
+ `${x.toFixed(1)},${(y - 18).toFixed(1)}`,
245
+ `${(x + 18).toFixed(1)},${y.toFixed(1)}`,
246
+ `${x.toFixed(1)},${(y + 18).toFixed(1)}`,
247
+ `${(x - 18).toFixed(1)},${y.toFixed(1)}`
248
+ ].join(" ");
249
+ return `<polygon points="${points}" fill="${fill}" stroke="#0f172a" stroke-width="2" />`;
250
+ }
251
+ if (node.type === "rationale") {
252
+ const points = [
253
+ `${(x - 18).toFixed(1)},${(y - 10).toFixed(1)}`,
254
+ `${x.toFixed(1)},${(y - 20).toFixed(1)}`,
255
+ `${(x + 18).toFixed(1)},${(y - 10).toFixed(1)}`,
256
+ `${(x + 18).toFixed(1)},${(y + 10).toFixed(1)}`,
257
+ `${x.toFixed(1)},${(y + 20).toFixed(1)}`,
258
+ `${(x - 18).toFixed(1)},${(y + 10).toFixed(1)}`
259
+ ].join(" ");
260
+ return `<polygon points="${points}" fill="${fill}" stroke="#0f172a" stroke-width="2" />`;
261
+ }
262
+ return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${node.isGodNode ? 24 : 18}" fill="${fill}" stroke="#0f172a" stroke-width="${node.isGodNode ? 3 : 2}" />`;
263
+ }
264
+ function nodeTitle(node, page) {
265
+ return [
266
+ node.label,
267
+ `id=${node.id}`,
268
+ `type=${node.type}`,
269
+ node.communityId ? `community=${node.communityId}` : "",
270
+ page ? `page=${page.path}` : "",
271
+ node.degree !== void 0 ? `degree=${node.degree}` : "",
272
+ node.bridgeScore !== void 0 ? `bridge=${node.bridgeScore}` : ""
273
+ ].filter(Boolean).join("\n");
274
+ }
275
+ function renderSvg(graph) {
276
+ const layout = layoutGraph(graph);
277
+ const pageById2 = graphPageById(graph);
278
+ const positionedById = new Map(layout.nodes.map((item) => [item.node.id, item]));
279
+ const communityLabels = sortedCommunities(graph).map((community, index) => {
280
+ const col = index % Math.max(1, Math.ceil(Math.sqrt(Math.max(1, sortedCommunities(graph).length))));
281
+ const row = Math.floor(index / Math.max(1, Math.ceil(Math.sqrt(Math.max(1, sortedCommunities(graph).length)))));
282
+ return {
283
+ label: community.label,
284
+ x: 240 + col * 460,
285
+ y: 90 + row * 360
286
+ };
287
+ });
288
+ const lines = [
289
+ '<?xml version="1.0" encoding="UTF-8"?>',
290
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${layout.width}" height="${layout.height}" viewBox="0 0 ${layout.width} ${layout.height}" role="img" aria-labelledby="title desc">`,
291
+ ' <title id="title">SwarmVault Graph Export</title>',
292
+ ` <desc id="desc">Nodes=${graph.nodes.length}, edges=${graph.edges.length}, communities=${graph.communities?.length ?? 0}</desc>`,
293
+ " <defs>",
294
+ ' <marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">',
295
+ ' <path d="M 0 0 L 10 5 L 0 10 z" fill="#64748b" />',
296
+ " </marker>",
297
+ " </defs>",
298
+ ' <rect width="100%" height="100%" fill="#020617" />'
299
+ ];
300
+ for (const community of communityLabels) {
301
+ lines.push(
302
+ ` <text x="${community.x.toFixed(1)}" y="${community.y.toFixed(1)}" fill="#cbd5e1" font-family="Avenir Next, Segoe UI, sans-serif" font-size="16" text-anchor="middle">${xmlEscape(community.label)}</text>`
303
+ );
304
+ }
305
+ for (const edge of [...graph.edges].sort((left, right) => left.id.localeCompare(right.id))) {
306
+ const source = positionedById.get(edge.source);
307
+ const target = positionedById.get(edge.target);
308
+ if (!source || !target) {
309
+ continue;
310
+ }
311
+ lines.push(
312
+ ` <g data-edge-id="${xmlEscape(edge.id)}" data-relation="${xmlEscape(edge.relation)}" data-evidence-class="${xmlEscape(edge.evidenceClass)}">`,
313
+ ` <title>${xmlEscape(
314
+ `${source.node.label} --${edge.relation}/${edge.evidenceClass}/${edge.confidence.toFixed(2)}--> ${target.node.label}`
315
+ )}</title>`,
316
+ ` <line x1="${source.x.toFixed(1)}" y1="${source.y.toFixed(1)}" x2="${target.x.toFixed(1)}" y2="${target.y.toFixed(1)}" stroke="#64748b" stroke-opacity="0.55" stroke-width="${Math.max(
317
+ 1.5,
318
+ Math.min(4, edge.confidence * 3)
319
+ ).toFixed(1)}" marker-end="url(#arrow)" />`,
320
+ " </g>"
321
+ );
322
+ }
323
+ for (const positioned of layout.nodes) {
324
+ const page = positioned.node.pageId ? pageById2.get(positioned.node.pageId) : void 0;
325
+ lines.push(
326
+ ` <g data-node-id="${xmlEscape(positioned.node.id)}" data-node-type="${xmlEscape(positioned.node.type)}" data-community-id="${xmlEscape(positioned.node.communityId ?? "")}">`,
327
+ ` <title>${xmlEscape(nodeTitle(positioned.node, page))}</title>`,
328
+ ` ${nodeShape(positioned)}`,
329
+ ` <text x="${positioned.x.toFixed(1)}" y="${(positioned.y + 34).toFixed(1)}" fill="#e2e8f0" font-family="Avenir Next, Segoe UI, sans-serif" font-size="11" text-anchor="middle">${xmlEscape(positioned.node.label)}</text>`,
330
+ " </g>"
331
+ );
332
+ }
333
+ lines.push("</svg>", "");
334
+ return lines.join("\n");
335
+ }
336
+ function graphMlData(value) {
337
+ if (Array.isArray(value)) {
338
+ return JSON.stringify(value);
339
+ }
340
+ if (value === void 0 || value === null) {
341
+ return "";
342
+ }
343
+ return String(value);
344
+ }
345
+ function renderGraphMl(graph) {
346
+ const pageById2 = graphPageById(graph);
347
+ const keys = [
348
+ { id: "n_label", for: "node", name: "label", type: "string" },
349
+ { id: "n_type", for: "node", name: "type", type: "string" },
350
+ { id: "n_page", for: "node", name: "pageId", type: "string" },
351
+ { id: "n_page_path", for: "node", name: "pagePath", type: "string" },
352
+ { id: "n_language", for: "node", name: "language", type: "string" },
353
+ { id: "n_symbol_kind", for: "node", name: "symbolKind", type: "string" },
354
+ { id: "n_project_ids", for: "node", name: "projectIds", type: "string" },
355
+ { id: "n_source_ids", for: "node", name: "sourceIds", type: "string" },
356
+ { id: "n_community", for: "node", name: "communityId", type: "string" },
357
+ { id: "n_degree", for: "node", name: "degree", type: "double" },
358
+ { id: "n_bridge", for: "node", name: "bridgeScore", type: "double" },
359
+ { id: "e_relation", for: "edge", name: "relation", type: "string" },
360
+ { id: "e_status", for: "edge", name: "status", type: "string" },
361
+ { id: "e_evidence", for: "edge", name: "evidenceClass", type: "string" },
362
+ { id: "e_confidence", for: "edge", name: "confidence", type: "double" },
363
+ { id: "e_provenance", for: "edge", name: "provenance", type: "string" }
364
+ ];
365
+ const lines = [
366
+ '<?xml version="1.0" encoding="UTF-8"?>',
367
+ '<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">'
368
+ ];
369
+ for (const key of keys) {
370
+ lines.push(` <key id="${key.id}" for="${key.for}" attr.name="${key.name}" attr.type="${key.type}" />`);
371
+ }
372
+ lines.push(' <graph id="swarmvault" edgedefault="directed">');
373
+ for (const node of [...graph.nodes].sort((left, right) => left.id.localeCompare(right.id))) {
374
+ const page = node.pageId ? pageById2.get(node.pageId) : void 0;
375
+ lines.push(` <node id="${xmlEscape(node.id)}">`);
376
+ const dataEntries = [
377
+ ["n_label", node.label],
378
+ ["n_type", node.type],
379
+ ["n_page", node.pageId],
380
+ ["n_page_path", page?.path],
381
+ ["n_language", node.language],
382
+ ["n_symbol_kind", node.symbolKind],
383
+ ["n_project_ids", node.projectIds],
384
+ ["n_source_ids", node.sourceIds],
385
+ ["n_community", node.communityId],
386
+ ["n_degree", node.degree],
387
+ ["n_bridge", node.bridgeScore]
388
+ ];
389
+ for (const [key, value] of dataEntries) {
390
+ if (value === void 0) {
391
+ continue;
392
+ }
393
+ lines.push(` <data key="${key}">${xmlEscape(graphMlData(value))}</data>`);
394
+ }
395
+ lines.push(" </node>");
396
+ }
397
+ for (const edge of [...graph.edges].sort((left, right) => left.id.localeCompare(right.id))) {
398
+ lines.push(` <edge id="${xmlEscape(edge.id)}" source="${xmlEscape(edge.source)}" target="${xmlEscape(edge.target)}">`);
399
+ for (const [key, value] of [
400
+ ["e_relation", edge.relation],
401
+ ["e_status", edge.status],
402
+ ["e_evidence", edge.evidenceClass],
403
+ ["e_confidence", edge.confidence],
404
+ ["e_provenance", edge.provenance]
405
+ ]) {
406
+ lines.push(` <data key="${key}">${xmlEscape(graphMlData(value))}</data>`);
407
+ }
408
+ lines.push(" </edge>");
409
+ }
410
+ lines.push(" </graph>", "</graphml>", "");
411
+ return lines.join("\n");
412
+ }
413
+ function renderCypher(graph) {
414
+ const pageById2 = graphPageById(graph);
415
+ const lines = ["// Neo4j Cypher import generated by SwarmVault", ""];
416
+ for (const node of [...graph.nodes].sort((left, right) => left.id.localeCompare(right.id))) {
417
+ const page = node.pageId ? pageById2.get(node.pageId) : void 0;
418
+ const props = [
419
+ `id: '${cypherEscape(node.id)}'`,
420
+ `label: '${cypherEscape(node.label)}'`,
421
+ `type: '${cypherEscape(node.type)}'`,
422
+ `sourceIds: '${cypherEscape(JSON.stringify(node.sourceIds))}'`,
423
+ `projectIds: '${cypherEscape(JSON.stringify(node.projectIds))}'`,
424
+ node.pageId ? `pageId: '${cypherEscape(node.pageId)}'` : "",
425
+ page?.path ? `pagePath: '${cypherEscape(page.path)}'` : "",
426
+ node.language ? `language: '${cypherEscape(node.language)}'` : "",
427
+ node.symbolKind ? `symbolKind: '${cypherEscape(node.symbolKind)}'` : "",
428
+ node.communityId ? `communityId: '${cypherEscape(node.communityId)}'` : "",
429
+ node.degree !== void 0 ? `degree: ${node.degree}` : "",
430
+ node.bridgeScore !== void 0 ? `bridgeScore: ${node.bridgeScore}` : "",
431
+ node.isGodNode !== void 0 ? `isGodNode: ${node.isGodNode}` : ""
432
+ ].filter(Boolean).join(", ");
433
+ lines.push(`MERGE (n:SwarmNode {id: '${cypherEscape(node.id)}'}) SET n += { ${props} };`);
434
+ }
435
+ lines.push("");
436
+ for (const edge of [...graph.edges].sort((left, right) => left.id.localeCompare(right.id))) {
437
+ lines.push(
438
+ `MATCH (a:SwarmNode {id: '${cypherEscape(edge.source)}'}), (b:SwarmNode {id: '${cypherEscape(edge.target)}'})`,
439
+ `MERGE (a)-[r:${relationType(edge.relation)} {id: '${cypherEscape(edge.id)}'}]->(b)`,
440
+ `SET r += { relation: '${cypherEscape(edge.relation)}', status: '${cypherEscape(edge.status)}', evidenceClass: '${cypherEscape(
441
+ edge.evidenceClass
442
+ )}', confidence: ${edge.confidence}, provenance: '${cypherEscape(JSON.stringify(edge.provenance))}' };`
443
+ );
444
+ }
445
+ lines.push("");
446
+ return lines.join("\n");
447
+ }
448
+ async function loadGraph(rootDir) {
449
+ const { paths } = await loadVaultConfig(rootDir);
450
+ const graph = await readJsonFile(paths.graphPath);
451
+ if (!graph) {
452
+ throw new Error("Graph artifact not found. Run `swarmvault compile` first.");
453
+ }
454
+ return graph;
455
+ }
456
+ async function writeGraphExport(outputPath, content) {
457
+ await ensureDir(path2.dirname(outputPath));
458
+ await fs2.writeFile(outputPath, content, "utf8");
459
+ return path2.resolve(outputPath);
460
+ }
461
+ async function exportGraphFormat(rootDir, format, outputPath) {
462
+ const graph = await loadGraph(rootDir);
463
+ const rendered = format === "svg" ? renderSvg(graph) : format === "graphml" ? renderGraphMl(graph) : renderCypher(graph);
464
+ const resolvedPath = await writeGraphExport(outputPath, rendered);
465
+ return { format, outputPath: resolvedPath };
466
+ }
467
+
468
+ // src/hooks.ts
469
+ import fs3 from "fs/promises";
470
+ import path3 from "path";
471
+ var hookStart = "# >>> swarmvault hook >>>";
472
+ var hookEnd = "# <<< swarmvault hook <<<";
473
+ async function findNearestGitRoot(startPath) {
474
+ let current = path3.resolve(startPath);
475
+ try {
476
+ const stat = await fs3.stat(current);
477
+ if (!stat.isDirectory()) {
478
+ current = path3.dirname(current);
479
+ }
480
+ } catch {
481
+ current = path3.dirname(current);
482
+ }
483
+ while (true) {
484
+ if (await fileExists(path3.join(current, ".git"))) {
485
+ return current;
486
+ }
487
+ const parent = path3.dirname(current);
488
+ if (parent === current) {
489
+ return null;
490
+ }
491
+ current = parent;
492
+ }
493
+ }
494
+ function shellQuote(value) {
495
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
496
+ }
497
+ function managedHookBlock(vaultRoot) {
498
+ return [
499
+ hookStart,
500
+ `cd ${shellQuote(vaultRoot)} || exit 0`,
501
+ "if command -v swarmvault >/dev/null 2>&1; then",
502
+ " swarmvault watch --repo --once >/dev/null 2>&1 || printf '[swarmvault hook] refresh failed\\n' >&2",
503
+ "fi",
504
+ hookEnd,
505
+ ""
506
+ ].join("\n");
507
+ }
508
+ function hookPath(repoRoot, hookName) {
509
+ return path3.join(repoRoot, ".git", "hooks", hookName);
510
+ }
511
+ async function readHookStatus(filePath) {
512
+ if (!await fileExists(filePath)) {
513
+ return "not_installed";
514
+ }
515
+ const content = await fs3.readFile(filePath, "utf8");
516
+ return content.includes(hookStart) && content.includes(hookEnd) ? "installed" : "other_content";
517
+ }
518
+ async function upsertHookFile(filePath, block) {
519
+ const existing = await fileExists(filePath) ? await fs3.readFile(filePath, "utf8") : "";
520
+ let next;
521
+ const startIndex = existing.indexOf(hookStart);
522
+ const endIndex = existing.indexOf(hookEnd);
523
+ if (startIndex !== -1 && endIndex !== -1) {
524
+ next = `${existing.slice(0, startIndex)}${block}${existing.slice(endIndex + hookEnd.length)}`.trimEnd();
525
+ } else if (existing.trim().length > 0) {
526
+ next = `${existing.trimEnd()}
527
+
528
+ ${block}`.trimEnd();
529
+ } else {
530
+ next = `#!/bin/sh
531
+ ${block}`.trimEnd();
532
+ }
533
+ await ensureDir(path3.dirname(filePath));
534
+ await fs3.writeFile(filePath, `${next}
535
+ `, { mode: 493, encoding: "utf8" });
536
+ await fs3.chmod(filePath, 493);
537
+ }
538
+ async function removeHookBlock(filePath) {
539
+ if (!await fileExists(filePath)) {
540
+ return;
541
+ }
542
+ const existing = await fs3.readFile(filePath, "utf8");
543
+ const startIndex = existing.indexOf(hookStart);
544
+ const endIndex = existing.indexOf(hookEnd);
545
+ if (startIndex === -1 || endIndex === -1) {
546
+ return;
547
+ }
548
+ const next = `${existing.slice(0, startIndex)}${existing.slice(endIndex + hookEnd.length)}`.trim();
549
+ if (!next || next === "#!/bin/sh") {
550
+ await fs3.rm(filePath, { force: true });
551
+ return;
552
+ }
553
+ await fs3.writeFile(filePath, `${next}
554
+ `, "utf8");
555
+ }
556
+ async function getGitHookStatus(rootDir) {
557
+ const repoRoot = await findNearestGitRoot(rootDir);
558
+ if (!repoRoot) {
559
+ return {
560
+ repoRoot: null,
561
+ postCommit: "not_installed",
562
+ postCheckout: "not_installed"
563
+ };
564
+ }
565
+ return {
566
+ repoRoot,
567
+ postCommit: await readHookStatus(hookPath(repoRoot, "post-commit")),
568
+ postCheckout: await readHookStatus(hookPath(repoRoot, "post-checkout"))
569
+ };
570
+ }
571
+ async function installGitHooks(rootDir) {
572
+ const repoRoot = await findNearestGitRoot(rootDir);
573
+ if (!repoRoot) {
574
+ throw new Error("No git repository found above the current vault.");
575
+ }
576
+ const block = managedHookBlock(path3.resolve(rootDir));
577
+ await upsertHookFile(hookPath(repoRoot, "post-commit"), block);
578
+ await upsertHookFile(hookPath(repoRoot, "post-checkout"), block);
579
+ return getGitHookStatus(rootDir);
580
+ }
581
+ async function uninstallGitHooks(rootDir) {
582
+ const repoRoot = await findNearestGitRoot(rootDir);
583
+ if (!repoRoot) {
584
+ return {
585
+ repoRoot: null,
586
+ postCommit: "not_installed",
587
+ postCheckout: "not_installed"
588
+ };
589
+ }
590
+ await removeHookBlock(hookPath(repoRoot, "post-commit"));
591
+ await removeHookBlock(hookPath(repoRoot, "post-checkout"));
592
+ return getGitHookStatus(rootDir);
129
593
  }
130
594
 
131
595
  // src/ingest.ts
132
- import fs5 from "fs/promises";
133
- import path5 from "path";
596
+ import fs8 from "fs/promises";
597
+ import path8 from "path";
134
598
  import { Readability } from "@mozilla/readability";
135
599
  import ignore from "ignore";
136
600
  import { JSDOM } from "jsdom";
@@ -138,16 +602,16 @@ import mime from "mime-types";
138
602
  import TurndownService from "turndown";
139
603
 
140
604
  // src/code-analysis.ts
141
- import fs3 from "fs/promises";
142
- import path3 from "path";
605
+ import fs5 from "fs/promises";
606
+ import path5 from "path";
143
607
  import ts from "typescript";
144
608
 
145
609
  // src/code-tree-sitter.ts
146
- import fs2 from "fs/promises";
610
+ import fs4 from "fs/promises";
147
611
  import { createRequire } from "module";
148
- import path2 from "path";
612
+ import path4 from "path";
149
613
  var require2 = createRequire(import.meta.url);
150
- var TREE_SITTER_PACKAGE_ROOT = path2.dirname(path2.dirname(require2.resolve("@vscode/tree-sitter-wasm")));
614
+ var TREE_SITTER_PACKAGE_ROOT = path4.dirname(path4.dirname(require2.resolve("@vscode/tree-sitter-wasm")));
151
615
  var RATIONALE_MARKERS = ["NOTE:", "IMPORTANT:", "HACK:", "WHY:", "RATIONALE:"];
152
616
  function stripKnownCommentPrefix(line) {
153
617
  let next = line.trim();
@@ -169,7 +633,9 @@ var grammarFileByLanguage = {
169
633
  csharp: "tree-sitter-c-sharp.wasm",
170
634
  c: "tree-sitter-cpp.wasm",
171
635
  cpp: "tree-sitter-cpp.wasm",
172
- php: "tree-sitter-php.wasm"
636
+ php: "tree-sitter-php.wasm",
637
+ ruby: "tree-sitter-ruby.wasm",
638
+ powershell: "tree-sitter-powershell.wasm"
173
639
  };
174
640
  async function getTreeSitterModule() {
175
641
  if (!treeSitterModulePromise) {
@@ -182,7 +648,7 @@ async function getTreeSitterModule() {
182
648
  async function ensureTreeSitterInit(module) {
183
649
  if (!treeSitterInitPromise) {
184
650
  treeSitterInitPromise = module.Parser.init({
185
- locateFile: () => path2.join(TREE_SITTER_PACKAGE_ROOT, "wasm", "tree-sitter.wasm")
651
+ locateFile: () => path4.join(TREE_SITTER_PACKAGE_ROOT, "wasm", "tree-sitter.wasm")
186
652
  });
187
653
  }
188
654
  return treeSitterInitPromise;
@@ -195,7 +661,7 @@ async function loadLanguage(language) {
195
661
  const loader = (async () => {
196
662
  const module = await getTreeSitterModule();
197
663
  await ensureTreeSitterInit(module);
198
- const bytes = await fs2.readFile(path2.join(TREE_SITTER_PACKAGE_ROOT, "wasm", grammarFileByLanguage[language]));
664
+ const bytes = await fs4.readFile(path4.join(TREE_SITTER_PACKAGE_ROOT, "wasm", grammarFileByLanguage[language]));
199
665
  return module.Language.load(bytes);
200
666
  })();
201
667
  languageCache.set(language, loader);
@@ -212,16 +678,16 @@ function stripCodeExtension(filePath) {
212
678
  return filePath.replace(/\.(?:[cm]?jsx?|tsx?|mts|cts|py|go|rs|java|cs|php|c|cc|cpp|cxx|h|hh|hpp|hxx)$/i, "");
213
679
  }
214
680
  function manifestModuleName(manifest, language) {
215
- const repoPath = manifest.repoRelativePath ?? path2.basename(manifest.originalPath ?? manifest.storedPath);
681
+ const repoPath = manifest.repoRelativePath ?? path4.basename(manifest.originalPath ?? manifest.storedPath);
216
682
  const normalized = toPosix(stripCodeExtension(repoPath)).replace(/^\.\/+/, "");
217
683
  if (!normalized) {
218
684
  return void 0;
219
685
  }
220
686
  if (language === "python") {
221
687
  const dotted = normalized.replace(/\/__init__$/i, "").replace(/\//g, ".").replace(/^src\./, "");
222
- return dotted || path2.posix.basename(normalized);
688
+ return dotted || path4.posix.basename(normalized);
223
689
  }
224
- return normalized.endsWith("/index") ? normalized.slice(0, -"/index".length) || path2.posix.basename(normalized) : normalized;
690
+ return normalized.endsWith("/index") ? normalized.slice(0, -"/index".length) || path4.posix.basename(normalized) : normalized;
225
691
  }
226
692
  function singleLineSignature(value) {
227
693
  return truncate(
@@ -397,11 +863,29 @@ function extractIdentifier(node) {
397
863
  if (!node) {
398
864
  return void 0;
399
865
  }
400
- if (["identifier", "field_identifier", "type_identifier", "name", "package_identifier"].includes(node.type)) {
866
+ if ([
867
+ "identifier",
868
+ "field_identifier",
869
+ "type_identifier",
870
+ "name",
871
+ "package_identifier",
872
+ "constant",
873
+ "simple_name",
874
+ "function_name"
875
+ ].includes(node.type)) {
401
876
  return node.text.trim();
402
877
  }
403
878
  const preferred = node.childForFieldName("name") ?? node.namedChildren.find(
404
- (child) => child && ["identifier", "field_identifier", "type_identifier", "name", "package_identifier"].includes(child.type)
879
+ (child) => child && [
880
+ "identifier",
881
+ "field_identifier",
882
+ "type_identifier",
883
+ "name",
884
+ "package_identifier",
885
+ "constant",
886
+ "simple_name",
887
+ "function_name"
888
+ ].includes(child.type)
405
889
  ) ?? node.namedChildren.at(-1) ?? null;
406
890
  return preferred ? extractIdentifier(preferred) : void 0;
407
891
  }
@@ -574,6 +1058,49 @@ function parseCppInclude(text) {
574
1058
  reExport: false
575
1059
  };
576
1060
  }
1061
+ function rubyStringContent(node) {
1062
+ if (!node) {
1063
+ return void 0;
1064
+ }
1065
+ const contentNode = node.descendantsOfType(["string_content", "simple_symbol", "bare_string"]).find((item) => item !== null) ?? null;
1066
+ return contentNode?.text.trim() || void 0;
1067
+ }
1068
+ function parsePowerShellImport(commandNode) {
1069
+ const commandName = commandNode.descendantsOfType(["command_name", "command_name_expr"]).find((item) => item !== null)?.text.trim();
1070
+ const genericTokens = commandNode.descendantsOfType("generic_token").filter((item) => item !== null).map((item) => item.text.trim());
1071
+ if (commandNode.namedChildren.some((child) => child?.type === "command_invokation_operator")) {
1072
+ const specifier = commandName?.trim();
1073
+ if (specifier) {
1074
+ return {
1075
+ specifier,
1076
+ importedSymbols: [],
1077
+ isExternal: false,
1078
+ reExport: false
1079
+ };
1080
+ }
1081
+ }
1082
+ if (!commandName) {
1083
+ return void 0;
1084
+ }
1085
+ const lowerName = commandName.toLowerCase();
1086
+ if (lowerName === "using" && genericTokens.length >= 2 && genericTokens[0]?.toLowerCase() === "module") {
1087
+ return {
1088
+ specifier: genericTokens[1],
1089
+ importedSymbols: [],
1090
+ isExternal: !genericTokens[1]?.startsWith("."),
1091
+ reExport: false
1092
+ };
1093
+ }
1094
+ if (lowerName === "import-module" && genericTokens[0]) {
1095
+ return {
1096
+ specifier: genericTokens[0],
1097
+ importedSymbols: [],
1098
+ isExternal: !genericTokens[0].startsWith("."),
1099
+ reExport: false
1100
+ };
1101
+ }
1102
+ return void 0;
1103
+ }
577
1104
  function pythonCodeAnalysis(manifest, rootNode, diagnostics) {
578
1105
  const imports = [];
579
1106
  const draftSymbols = [];
@@ -976,6 +1503,161 @@ function phpCodeAnalysis(manifest, rootNode, diagnostics) {
976
1503
  namespace: namespaceName
977
1504
  });
978
1505
  }
1506
+ function rubyCodeAnalysis(manifest, rootNode, diagnostics) {
1507
+ const imports = [];
1508
+ const draftSymbols = [];
1509
+ const exportLabels = [];
1510
+ let namespaceName;
1511
+ const visitStatements = (node, scopeName, namespaceParts = []) => {
1512
+ if (!node) {
1513
+ return;
1514
+ }
1515
+ for (const child of node.namedChildren) {
1516
+ if (!child) {
1517
+ continue;
1518
+ }
1519
+ if (child.type === "call") {
1520
+ const callee = extractIdentifier(child.namedChildren.at(0) ?? null);
1521
+ if (callee === "require" || callee === "require_relative") {
1522
+ const specifier = rubyStringContent(child.childForFieldName("arguments") ?? child.namedChildren.at(1) ?? null);
1523
+ if (specifier) {
1524
+ imports.push({
1525
+ specifier,
1526
+ importedSymbols: [],
1527
+ isExternal: callee === "require" && !specifier.startsWith("."),
1528
+ reExport: false
1529
+ });
1530
+ }
1531
+ }
1532
+ continue;
1533
+ }
1534
+ if (child.type === "module") {
1535
+ const moduleName = extractIdentifier(child.childForFieldName("name") ?? child.namedChildren.at(0) ?? null);
1536
+ if (!moduleName) {
1537
+ continue;
1538
+ }
1539
+ const nextNamespace = [...namespaceParts, moduleName];
1540
+ namespaceName ??= nextNamespace.join("::");
1541
+ visitStatements(findNamedChild(child, "body_statement"), void 0, nextNamespace);
1542
+ continue;
1543
+ }
1544
+ if (child.type === "class") {
1545
+ const className = extractIdentifier(child.childForFieldName("name") ?? child.namedChildren.at(0) ?? null);
1546
+ if (!className) {
1547
+ continue;
1548
+ }
1549
+ const body = findNamedChild(child, "body_statement");
1550
+ const mixins = body ? body.namedChildren.filter((item) => item !== null && item.type === "call").filter((item) => extractIdentifier(item.namedChildren.at(0) ?? null) === "include").flatMap(
1551
+ (item) => item.descendantsOfType(["constant", "identifier"]).filter((descendant) => descendant !== null).slice(1).map((descendant) => normalizeSymbolReference(descendant.text)).filter(Boolean)
1552
+ ) : [];
1553
+ draftSymbols.push({
1554
+ name: scopeName ? `${scopeName}::${className}` : className,
1555
+ kind: "class",
1556
+ signature: singleLineSignature(child.text),
1557
+ exported: true,
1558
+ callNames: [],
1559
+ extendsNames: descendantTypeNames(child.childForFieldName("superclass")),
1560
+ implementsNames: uniqueBy(mixins, (item) => item),
1561
+ bodyText: body?.text
1562
+ });
1563
+ exportLabels.push(scopeName ? `${scopeName}::${className}` : className);
1564
+ visitStatements(body, scopeName ? `${scopeName}::${className}` : className, namespaceParts);
1565
+ continue;
1566
+ }
1567
+ if (child.type === "method") {
1568
+ const methodName = extractIdentifier(child.childForFieldName("name") ?? child.namedChildren.at(0) ?? null);
1569
+ if (!methodName) {
1570
+ continue;
1571
+ }
1572
+ const symbolName = scopeName ? `${scopeName}#${methodName}` : methodName;
1573
+ draftSymbols.push({
1574
+ name: symbolName,
1575
+ kind: "function",
1576
+ signature: singleLineSignature(child.text),
1577
+ exported: true,
1578
+ callNames: [],
1579
+ extendsNames: [],
1580
+ implementsNames: [],
1581
+ bodyText: nodeText(findNamedChild(child, "body_statement") ?? child.childForFieldName("body"))
1582
+ });
1583
+ exportLabels.push(symbolName);
1584
+ }
1585
+ }
1586
+ };
1587
+ visitStatements(rootNode, void 0, []);
1588
+ return finalizeCodeAnalysis(manifest, "ruby", imports, draftSymbols, exportLabels, diagnostics, {
1589
+ namespace: namespaceName
1590
+ });
1591
+ }
1592
+ function powershellCodeAnalysis(manifest, rootNode, diagnostics) {
1593
+ const imports = [];
1594
+ const draftSymbols = [];
1595
+ const exportLabels = [];
1596
+ for (const child of rootNode.descendantsOfType(["command", "class_statement", "function_statement"]).filter((item) => item !== null)) {
1597
+ if (child.type === "command") {
1598
+ const parsed = parsePowerShellImport(child);
1599
+ if (parsed) {
1600
+ imports.push(parsed);
1601
+ }
1602
+ continue;
1603
+ }
1604
+ if (child.type === "class_statement") {
1605
+ const names = child.namedChildren.filter((item) => item !== null && item.type === "simple_name").map((item) => item.text.trim());
1606
+ const className = names[0];
1607
+ if (!className) {
1608
+ continue;
1609
+ }
1610
+ draftSymbols.push({
1611
+ name: className,
1612
+ kind: "class",
1613
+ signature: singleLineSignature(child.text),
1614
+ exported: true,
1615
+ callNames: [],
1616
+ extendsNames: names.slice(1, 2),
1617
+ implementsNames: [],
1618
+ bodyText: nodeText(child.childForFieldName("body")) || child.text
1619
+ });
1620
+ exportLabels.push(className);
1621
+ for (const methodNode of child.descendantsOfType("class_method_definition").filter((item) => item !== null)) {
1622
+ const methodName = methodNode.descendantsOfType("simple_name").filter((item) => item !== null).map((item) => item.text.trim())[0];
1623
+ if (!methodName) {
1624
+ continue;
1625
+ }
1626
+ const symbolName = `${className}.${methodName}`;
1627
+ draftSymbols.push({
1628
+ name: symbolName,
1629
+ kind: "function",
1630
+ signature: singleLineSignature(methodNode.text),
1631
+ exported: true,
1632
+ callNames: [],
1633
+ extendsNames: [],
1634
+ implementsNames: [],
1635
+ bodyText: nodeText(findNamedChild(methodNode, "script_block") ?? methodNode.childForFieldName("body")) || methodNode.text
1636
+ });
1637
+ exportLabels.push(symbolName);
1638
+ }
1639
+ continue;
1640
+ }
1641
+ if (child.type === "function_statement") {
1642
+ const functionName = extractIdentifier(findNamedChild(child, "function_name") ?? child.childForFieldName("name"));
1643
+ if (!functionName) {
1644
+ continue;
1645
+ }
1646
+ draftSymbols.push({
1647
+ name: functionName,
1648
+ kind: "function",
1649
+ signature: singleLineSignature(child.text),
1650
+ exported: true,
1651
+ callNames: [],
1652
+ extendsNames: [],
1653
+ implementsNames: [],
1654
+ bodyText: nodeText(findNamedChild(child, "script_block") ?? child.childForFieldName("body")) || child.text
1655
+ });
1656
+ exportLabels.push(functionName);
1657
+ }
1658
+ }
1659
+ return finalizeCodeAnalysis(manifest, "powershell", imports, draftSymbols, exportLabels, diagnostics);
1660
+ }
979
1661
  function cFamilyCodeAnalysis(manifest, language, rootNode, diagnostics) {
980
1662
  const imports = [];
981
1663
  const draftSymbols = [];
@@ -1094,6 +1776,10 @@ async function analyzeTreeSitterCode(manifest, content, language) {
1094
1776
  return { code: csharpCodeAnalysis(manifest, tree.rootNode, diagnostics), rationales };
1095
1777
  case "php":
1096
1778
  return { code: phpCodeAnalysis(manifest, tree.rootNode, diagnostics), rationales };
1779
+ case "ruby":
1780
+ return { code: rubyCodeAnalysis(manifest, tree.rootNode, diagnostics), rationales };
1781
+ case "powershell":
1782
+ return { code: powershellCodeAnalysis(manifest, tree.rootNode, diagnostics), rationales };
1097
1783
  case "c":
1098
1784
  case "cpp":
1099
1785
  return { code: cFamilyCodeAnalysis(manifest, language, tree.rootNode, diagnostics), rationales };
@@ -1355,16 +2041,16 @@ function stripCodeExtension2(filePath) {
1355
2041
  return filePath.replace(/\.(?:[cm]?jsx?|tsx?|mts|cts|py|go|rs|java|cs|php|c|cc|cpp|cxx|h|hh|hpp|hxx)$/i, "");
1356
2042
  }
1357
2043
  function manifestModuleName2(manifest, language) {
1358
- const repoPath = manifest.repoRelativePath ?? path3.basename(manifest.originalPath ?? manifest.storedPath);
2044
+ const repoPath = manifest.repoRelativePath ?? path5.basename(manifest.originalPath ?? manifest.storedPath);
1359
2045
  const normalized = toPosix(stripCodeExtension2(repoPath)).replace(/^\.\/+/, "");
1360
2046
  if (!normalized) {
1361
2047
  return void 0;
1362
2048
  }
1363
2049
  if (language === "python") {
1364
2050
  const dotted = normalized.replace(/\/__init__$/i, "").replace(/\//g, ".").replace(/^src\./, "");
1365
- return dotted || path3.posix.basename(normalized);
2051
+ return dotted || path5.posix.basename(normalized);
1366
2052
  }
1367
- return normalized.endsWith("/index") ? normalized.slice(0, -"/index".length) || path3.posix.basename(normalized) : normalized;
2053
+ return normalized.endsWith("/index") ? normalized.slice(0, -"/index".length) || path5.posix.basename(normalized) : normalized;
1368
2054
  }
1369
2055
  function finalizeCodeAnalysis2(manifest, language, imports, draftSymbols, exportLabels, diagnostics, metadata) {
1370
2056
  const topLevelNames = new Set(draftSymbols.map((symbol) => symbol.name));
@@ -1657,7 +2343,7 @@ function analyzeTypeScriptLikeCode(manifest, content) {
1657
2343
  };
1658
2344
  }
1659
2345
  function inferCodeLanguage(filePath, mimeType = "") {
1660
- const extension = path3.extname(filePath).toLowerCase();
2346
+ const extension = path5.extname(filePath).toLowerCase();
1661
2347
  if (extension === ".ts" || extension === ".mts" || extension === ".cts") {
1662
2348
  return "typescript";
1663
2349
  }
@@ -1688,6 +2374,12 @@ function inferCodeLanguage(filePath, mimeType = "") {
1688
2374
  if (extension === ".php") {
1689
2375
  return "php";
1690
2376
  }
2377
+ if (extension === ".rb") {
2378
+ return "ruby";
2379
+ }
2380
+ if (extension === ".ps1" || extension === ".psm1" || extension === ".psd1") {
2381
+ return "powershell";
2382
+ }
1691
2383
  if (extension === ".c") {
1692
2384
  return "c";
1693
2385
  }
@@ -1700,12 +2392,12 @@ function modulePageTitle(manifest) {
1700
2392
  return `${manifest.title} module`;
1701
2393
  }
1702
2394
  function importResolutionCandidates(basePath, specifier, extensions) {
1703
- const resolved = path3.posix.normalize(path3.posix.join(path3.posix.dirname(basePath), specifier));
1704
- if (path3.posix.extname(resolved)) {
2395
+ const resolved = path5.posix.normalize(path5.posix.join(path5.posix.dirname(basePath), specifier));
2396
+ if (path5.posix.extname(resolved)) {
1705
2397
  return [resolved];
1706
2398
  }
1707
- const direct = extensions.map((extension) => path3.posix.normalize(`${resolved}${extension}`));
1708
- const indexFiles = extensions.map((extension) => path3.posix.normalize(path3.posix.join(resolved, `index${extension}`)));
2399
+ const direct = extensions.map((extension) => path5.posix.normalize(`${resolved}${extension}`));
2400
+ const indexFiles = extensions.map((extension) => path5.posix.normalize(path5.posix.join(resolved, `index${extension}`)));
1709
2401
  return uniqueBy([resolved, ...direct, ...indexFiles], (candidate) => candidate);
1710
2402
  }
1711
2403
  function normalizeAlias(value) {
@@ -1724,32 +2416,32 @@ function recordAlias(target, value) {
1724
2416
  }
1725
2417
  function manifestBasenameWithoutExtension(manifest) {
1726
2418
  const target = manifest.repoRelativePath ?? manifest.originalPath ?? manifest.storedPath;
1727
- return path3.posix.basename(stripCodeExtension2(normalizeAlias(target)));
2419
+ return path5.posix.basename(stripCodeExtension2(normalizeAlias(target)));
1728
2420
  }
1729
2421
  async function readNearestGoModulePath(startPath, cache) {
1730
- let current = path3.resolve(startPath);
2422
+ let current = path5.resolve(startPath);
1731
2423
  try {
1732
- const stat = await fs3.stat(current);
2424
+ const stat = await fs5.stat(current);
1733
2425
  if (!stat.isDirectory()) {
1734
- current = path3.dirname(current);
2426
+ current = path5.dirname(current);
1735
2427
  }
1736
2428
  } catch {
1737
- current = path3.dirname(current);
2429
+ current = path5.dirname(current);
1738
2430
  }
1739
2431
  while (true) {
1740
2432
  if (cache.has(current)) {
1741
2433
  const cached = cache.get(current);
1742
2434
  return cached === null ? void 0 : cached;
1743
2435
  }
1744
- const goModPath = path3.join(current, "go.mod");
1745
- if (await fs3.access(goModPath).then(() => true).catch(() => false)) {
1746
- const content = await fs3.readFile(goModPath, "utf8");
2436
+ const goModPath = path5.join(current, "go.mod");
2437
+ if (await fs5.access(goModPath).then(() => true).catch(() => false)) {
2438
+ const content = await fs5.readFile(goModPath, "utf8");
1747
2439
  const match = content.match(/^\s*module\s+(\S+)/m);
1748
2440
  const modulePath = match?.[1]?.trim() ?? null;
1749
2441
  cache.set(current, modulePath);
1750
2442
  return modulePath ?? void 0;
1751
2443
  }
1752
- const parent = path3.dirname(current);
2444
+ const parent = path5.dirname(current);
1753
2445
  if (parent === current) {
1754
2446
  cache.set(current, null);
1755
2447
  return void 0;
@@ -1784,6 +2476,10 @@ function candidateExtensionsFor(language) {
1784
2476
  return [".cs"];
1785
2477
  case "php":
1786
2478
  return [".php"];
2479
+ case "ruby":
2480
+ return [".rb"];
2481
+ case "powershell":
2482
+ return [".ps1", ".psm1", ".psd1"];
1787
2483
  case "c":
1788
2484
  return [".c", ".h"];
1789
2485
  case "cpp":
@@ -1826,10 +2522,10 @@ async function buildCodeIndex(rootDir, manifests, analyses) {
1826
2522
  if (normalizedNamespace) {
1827
2523
  recordAlias(aliases, normalizedNamespace);
1828
2524
  }
1829
- const originalPath = manifest.originalPath ? path3.resolve(manifest.originalPath) : path3.resolve(rootDir, manifest.storedPath);
2525
+ const originalPath = manifest.originalPath ? path5.resolve(manifest.originalPath) : path5.resolve(rootDir, manifest.storedPath);
1830
2526
  const goModulePath = await readNearestGoModulePath(originalPath, goModuleCache);
1831
2527
  if (goModulePath && repoRelativePath) {
1832
- const dir = path3.posix.dirname(repoRelativePath);
2528
+ const dir = path5.posix.dirname(repoRelativePath);
1833
2529
  const packageAlias = dir === "." ? goModulePath : `${goModulePath}/${dir}`;
1834
2530
  recordAlias(aliases, packageAlias);
1835
2531
  }
@@ -1846,6 +2542,10 @@ async function buildCodeIndex(rootDir, manifests, analyses) {
1846
2542
  recordAlias(aliases, `${normalizedNamespace}\\${basename}`);
1847
2543
  }
1848
2544
  break;
2545
+ case "ruby":
2546
+ case "powershell":
2547
+ recordAlias(aliases, basename);
2548
+ break;
1849
2549
  default:
1850
2550
  break;
1851
2551
  }
@@ -1901,10 +2601,10 @@ function resolvePythonRelativeAliases(repoRelativePath, specifier) {
1901
2601
  const dotMatch = specifier.match(/^\.+/);
1902
2602
  const depth = dotMatch ? dotMatch[0].length : 0;
1903
2603
  const relativeModule = specifier.slice(depth).replace(/\./g, "/");
1904
- const baseDir = path3.posix.dirname(repoRelativePath);
1905
- const parentDir = path3.posix.normalize(path3.posix.join(baseDir, ...Array(Math.max(depth - 1, 0)).fill("..")));
1906
- const moduleBase = relativeModule ? path3.posix.join(parentDir, relativeModule) : parentDir;
1907
- return uniqueBy([`${moduleBase}.py`, path3.posix.join(moduleBase, "__init__.py")], (item) => item);
2604
+ const baseDir = path5.posix.dirname(repoRelativePath);
2605
+ const parentDir = path5.posix.normalize(path5.posix.join(baseDir, ...Array(Math.max(depth - 1, 0)).fill("..")));
2606
+ const moduleBase = relativeModule ? path5.posix.join(parentDir, relativeModule) : parentDir;
2607
+ return uniqueBy([`${moduleBase}.py`, path5.posix.join(moduleBase, "__init__.py")], (item) => item);
1908
2608
  }
1909
2609
  function resolveRustAliases(manifest, specifier) {
1910
2610
  const repoRelativePath = manifest.repoRelativePath ? normalizeAlias(manifest.repoRelativePath) : "";
@@ -1945,13 +2645,20 @@ function findImportCandidates(manifest, codeImport, lookup) {
1945
2645
  case "csharp":
1946
2646
  return aliasMatches(lookup, codeImport.specifier);
1947
2647
  case "php":
2648
+ case "ruby":
2649
+ case "powershell":
1948
2650
  if (repoRelativePath && isLocalIncludeSpecifier(codeImport.specifier)) {
1949
2651
  return repoPathMatches(
1950
2652
  lookup,
1951
2653
  ...importResolutionCandidates(repoRelativePath, codeImport.specifier, candidateExtensionsFor(language))
1952
2654
  );
1953
2655
  }
1954
- return aliasMatches(lookup, codeImport.specifier, codeImport.specifier.replace(/\\/g, "/"));
2656
+ return aliasMatches(
2657
+ lookup,
2658
+ codeImport.specifier,
2659
+ codeImport.specifier.replace(/\\/g, "/"),
2660
+ stripCodeExtension2(codeImport.specifier.replace(/\\/g, "/"))
2661
+ );
1955
2662
  case "rust":
1956
2663
  return aliasMatches(lookup, codeImport.specifier, ...resolveRustAliases(manifest, codeImport.specifier));
1957
2664
  case "c":
@@ -1977,6 +2684,8 @@ function importLooksLocal(manifest, codeImport, candidates) {
1977
2684
  case "rust":
1978
2685
  return /^(crate|self|super)::/.test(codeImport.specifier);
1979
2686
  case "php":
2687
+ case "ruby":
2688
+ case "powershell":
1980
2689
  case "c":
1981
2690
  case "cpp":
1982
2691
  return !codeImport.isExternal;
@@ -2036,18 +2745,18 @@ async function analyzeCodeSource(manifest, extractedText, schemaHash) {
2036
2745
  }
2037
2746
 
2038
2747
  // src/logs.ts
2039
- import fs4 from "fs/promises";
2040
- import path4 from "path";
2748
+ import fs6 from "fs/promises";
2749
+ import path6 from "path";
2041
2750
  import matter from "gray-matter";
2042
2751
  async function resolveUniqueSessionPath(rootDir, operation, title, startedAt) {
2043
2752
  const { paths } = await initWorkspace(rootDir);
2044
2753
  await ensureDir(paths.sessionsDir);
2045
2754
  const timestamp = startedAt.replace(/[:.]/g, "-");
2046
2755
  const baseName = `${timestamp}-${operation}-${slugify(title)}`;
2047
- let candidate = path4.join(paths.sessionsDir, `${baseName}.md`);
2756
+ let candidate = path6.join(paths.sessionsDir, `${baseName}.md`);
2048
2757
  let counter = 2;
2049
2758
  while (await fileExists(candidate)) {
2050
- candidate = path4.join(paths.sessionsDir, `${baseName}-${counter}.md`);
2759
+ candidate = path6.join(paths.sessionsDir, `${baseName}-${counter}.md`);
2051
2760
  counter++;
2052
2761
  }
2053
2762
  return candidate;
@@ -2055,11 +2764,11 @@ async function resolveUniqueSessionPath(rootDir, operation, title, startedAt) {
2055
2764
  async function appendLogEntry(rootDir, action, title, lines = []) {
2056
2765
  const { paths } = await initWorkspace(rootDir);
2057
2766
  await ensureDir(paths.wikiDir);
2058
- const logPath = path4.join(paths.wikiDir, "log.md");
2767
+ const logPath = path6.join(paths.wikiDir, "log.md");
2059
2768
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
2060
2769
  const entry = [`## [${timestamp}] ${action} | ${title}`, ...lines.map((line) => `- ${line}`), ""].join("\n");
2061
- const existing = await fileExists(logPath) ? await fs4.readFile(logPath, "utf8") : "# Log\n\n";
2062
- await fs4.writeFile(logPath, `${existing}${entry}
2770
+ const existing = await fileExists(logPath) ? await fs6.readFile(logPath, "utf8") : "# Log\n\n";
2771
+ await fs6.writeFile(logPath, `${existing}${entry}
2063
2772
  `, "utf8");
2064
2773
  }
2065
2774
  async function recordSession(rootDir, input) {
@@ -2069,8 +2778,8 @@ async function recordSession(rootDir, input) {
2069
2778
  const finishedAtIso = new Date(input.finishedAt ?? input.startedAt).toISOString();
2070
2779
  const durationMs = Math.max(0, new Date(finishedAtIso).getTime() - new Date(startedAtIso).getTime());
2071
2780
  const sessionPath = await resolveUniqueSessionPath(rootDir, input.operation, input.title, startedAtIso);
2072
- const sessionId = path4.basename(sessionPath, ".md");
2073
- const relativeSessionPath = path4.relative(rootDir, sessionPath).split(path4.sep).join(path4.posix.sep);
2781
+ const sessionId = path6.basename(sessionPath, ".md");
2782
+ const relativeSessionPath = path6.relative(rootDir, sessionPath).split(path6.sep).join(path6.posix.sep);
2074
2783
  const frontmatter = Object.fromEntries(
2075
2784
  Object.entries({
2076
2785
  session_id: sessionId,
@@ -2118,7 +2827,7 @@ async function recordSession(rootDir, input) {
2118
2827
  frontmatter
2119
2828
  );
2120
2829
  await writeFileIfChanged(sessionPath, content);
2121
- const logPath = path4.join(paths.wikiDir, "log.md");
2830
+ const logPath = path6.join(paths.wikiDir, "log.md");
2122
2831
  const timestamp = startedAtIso.slice(0, 19).replace("T", " ");
2123
2832
  const entry = [
2124
2833
  `## [${timestamp}] ${input.operation} | ${input.title}`,
@@ -2126,8 +2835,8 @@ async function recordSession(rootDir, input) {
2126
2835
  ...(input.lines ?? []).map((line) => `- ${line}`),
2127
2836
  ""
2128
2837
  ].join("\n");
2129
- const existing = await fileExists(logPath) ? await fs4.readFile(logPath, "utf8") : "# Log\n\n";
2130
- await fs4.writeFile(logPath, `${existing}${entry}
2838
+ const existing = await fileExists(logPath) ? await fs6.readFile(logPath, "utf8") : "# Log\n\n";
2839
+ await fs6.writeFile(logPath, `${existing}${entry}
2131
2840
  `, "utf8");
2132
2841
  return { sessionPath, sessionId };
2133
2842
  }
@@ -2136,10 +2845,136 @@ async function appendWatchRun(rootDir, run) {
2136
2845
  await appendJsonLine(paths.jobsLogPath, run);
2137
2846
  }
2138
2847
 
2139
- // src/ingest.ts
2140
- var DEFAULT_MAX_ASSET_SIZE = 10 * 1024 * 1024;
2141
- var DEFAULT_MAX_DIRECTORY_FILES = 5e3;
2142
- var BUILT_IN_REPO_IGNORES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", "coverage", ".venv", "vendor", "target"]);
2848
+ // src/watch-state.ts
2849
+ import fs7 from "fs/promises";
2850
+ import path7 from "path";
2851
+ import matter2 from "gray-matter";
2852
+ function pendingEntryKey(entry) {
2853
+ return entry.path;
2854
+ }
2855
+ function sortPending(entries) {
2856
+ return [...entries].sort(
2857
+ (left, right) => left.path.localeCompare(right.path) || left.detectedAt.localeCompare(right.detectedAt) || left.id.localeCompare(right.id)
2858
+ );
2859
+ }
2860
+ function normalizeRelativePath(rootDir, filePath) {
2861
+ if (!filePath) {
2862
+ return void 0;
2863
+ }
2864
+ return toPosix(path7.relative(rootDir, path7.resolve(filePath)));
2865
+ }
2866
+ async function readPendingSemanticRefresh(rootDir) {
2867
+ const { paths } = await initWorkspace(rootDir);
2868
+ const entries = await readJsonFile(paths.pendingSemanticRefreshPath);
2869
+ return Array.isArray(entries) ? sortPending(entries) : [];
2870
+ }
2871
+ async function writePendingSemanticRefresh(rootDir, entries) {
2872
+ const { paths } = await initWorkspace(rootDir);
2873
+ await ensureDir(paths.watchDir);
2874
+ const normalized = sortPending(entries);
2875
+ await writeJsonFile(paths.pendingSemanticRefreshPath, normalized);
2876
+ return normalized;
2877
+ }
2878
+ async function mergePendingSemanticRefresh(rootDir, entries) {
2879
+ const existing = await readPendingSemanticRefresh(rootDir);
2880
+ const merged = new Map(existing.map((entry) => [pendingEntryKey(entry), entry]));
2881
+ for (const entry of entries) {
2882
+ merged.set(pendingEntryKey(entry), entry);
2883
+ }
2884
+ return writePendingSemanticRefresh(rootDir, [...merged.values()]);
2885
+ }
2886
+ async function clearPendingSemanticRefreshEntries(rootDir, targets) {
2887
+ const existing = await readPendingSemanticRefresh(rootDir);
2888
+ const relativePath = targets.relativePath ?? normalizeRelativePath(rootDir, targets.originalPath);
2889
+ return writePendingSemanticRefresh(
2890
+ rootDir,
2891
+ existing.filter((entry) => {
2892
+ if (targets.sourceId && entry.sourceId === targets.sourceId) {
2893
+ return false;
2894
+ }
2895
+ if (relativePath && entry.path === relativePath) {
2896
+ return false;
2897
+ }
2898
+ return true;
2899
+ })
2900
+ );
2901
+ }
2902
+ async function readWatchStatusArtifact(rootDir) {
2903
+ const { paths } = await initWorkspace(rootDir);
2904
+ return readJsonFile(paths.watchStatusPath);
2905
+ }
2906
+ async function writeWatchStatusArtifact(rootDir, status) {
2907
+ const { paths } = await initWorkspace(rootDir);
2908
+ await ensureDir(paths.watchDir);
2909
+ await writeJsonFile(paths.watchStatusPath, status);
2910
+ }
2911
+ async function markPagesStaleForSources(rootDir, sourceIds) {
2912
+ const uniqueSourceIds = [...new Set(sourceIds.filter(Boolean))];
2913
+ if (!uniqueSourceIds.length) {
2914
+ return [];
2915
+ }
2916
+ const { paths } = await initWorkspace(rootDir);
2917
+ const graph = await readJsonFile(paths.graphPath);
2918
+ if (!graph) {
2919
+ return [];
2920
+ }
2921
+ const affectedSourceIds = new Set(uniqueSourceIds);
2922
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2923
+ let graphChanged = false;
2924
+ const affectedPagePaths = [];
2925
+ const nextPages = graph.pages.map((page) => {
2926
+ if (page.freshness === "stale" || !page.sourceIds.some((sourceId) => affectedSourceIds.has(sourceId))) {
2927
+ return page;
2928
+ }
2929
+ graphChanged = true;
2930
+ affectedPagePaths.push(page.path);
2931
+ return {
2932
+ ...page,
2933
+ freshness: "stale",
2934
+ updatedAt: now
2935
+ };
2936
+ });
2937
+ const nextNodes = graph.nodes.map((node) => {
2938
+ if (node.freshness === "stale" || !node.sourceIds.some((sourceId) => affectedSourceIds.has(sourceId))) {
2939
+ return node;
2940
+ }
2941
+ graphChanged = true;
2942
+ return {
2943
+ ...node,
2944
+ freshness: "stale"
2945
+ };
2946
+ });
2947
+ if (graphChanged) {
2948
+ await writeJsonFile(paths.graphPath, {
2949
+ ...graph,
2950
+ nodes: nextNodes,
2951
+ pages: nextPages
2952
+ });
2953
+ }
2954
+ for (const page of nextPages) {
2955
+ if (page.freshness !== "stale" || !page.sourceIds.some((sourceId) => affectedSourceIds.has(sourceId))) {
2956
+ continue;
2957
+ }
2958
+ const absolutePath = path7.join(paths.wikiDir, page.path);
2959
+ if (!await fileExists(absolutePath)) {
2960
+ continue;
2961
+ }
2962
+ const raw = await fs7.readFile(absolutePath, "utf8");
2963
+ const parsed = matter2(raw);
2964
+ if (parsed.data.freshness === "stale") {
2965
+ continue;
2966
+ }
2967
+ parsed.data.freshness = "stale";
2968
+ parsed.data.updated_at = now;
2969
+ await writeFileIfChanged(absolutePath, matter2.stringify(parsed.content, parsed.data));
2970
+ }
2971
+ return affectedPagePaths;
2972
+ }
2973
+
2974
+ // src/ingest.ts
2975
+ var DEFAULT_MAX_ASSET_SIZE = 10 * 1024 * 1024;
2976
+ var DEFAULT_MAX_DIRECTORY_FILES = 5e3;
2977
+ var BUILT_IN_REPO_IGNORES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", "coverage", ".venv", "vendor", "target"]);
2143
2978
  function inferKind(mimeType, filePath) {
2144
2979
  if (inferCodeLanguage(filePath, mimeType)) {
2145
2980
  return "code";
@@ -2172,7 +3007,7 @@ function normalizeIngestOptions(options) {
2172
3007
  return {
2173
3008
  includeAssets: options?.includeAssets ?? true,
2174
3009
  maxAssetSize: Math.max(0, Math.floor(options?.maxAssetSize ?? DEFAULT_MAX_ASSET_SIZE)),
2175
- repoRoot: options?.repoRoot ? path5.resolve(options.repoRoot) : void 0,
3010
+ repoRoot: options?.repoRoot ? path8.resolve(options.repoRoot) : void 0,
2176
3011
  include: (options?.include ?? []).map((pattern) => pattern.trim()).filter(Boolean),
2177
3012
  exclude: (options?.exclude ?? []).map((pattern) => pattern.trim()).filter(Boolean),
2178
3013
  maxFiles: Math.max(1, Math.floor(options?.maxFiles ?? DEFAULT_MAX_DIRECTORY_FILES)),
@@ -2181,27 +3016,27 @@ function normalizeIngestOptions(options) {
2181
3016
  }
2182
3017
  function matchesAnyGlob(relativePath, patterns) {
2183
3018
  return patterns.some(
2184
- (pattern) => path5.matchesGlob(relativePath, pattern) || path5.matchesGlob(path5.posix.basename(relativePath), pattern)
3019
+ (pattern) => path8.matchesGlob(relativePath, pattern) || path8.matchesGlob(path8.posix.basename(relativePath), pattern)
2185
3020
  );
2186
3021
  }
2187
3022
  function supportedDirectoryKind(sourceKind) {
2188
3023
  return sourceKind !== "binary";
2189
3024
  }
2190
- async function findNearestGitRoot(startPath) {
2191
- let current = path5.resolve(startPath);
3025
+ async function findNearestGitRoot2(startPath) {
3026
+ let current = path8.resolve(startPath);
2192
3027
  try {
2193
- const stat = await fs5.stat(current);
3028
+ const stat = await fs8.stat(current);
2194
3029
  if (!stat.isDirectory()) {
2195
- current = path5.dirname(current);
3030
+ current = path8.dirname(current);
2196
3031
  }
2197
3032
  } catch {
2198
- current = path5.dirname(current);
3033
+ current = path8.dirname(current);
2199
3034
  }
2200
3035
  while (true) {
2201
- if (await fileExists(path5.join(current, ".git"))) {
3036
+ if (await fileExists(path8.join(current, ".git"))) {
2202
3037
  return current;
2203
3038
  }
2204
- const parent = path5.dirname(current);
3039
+ const parent = path8.dirname(current);
2205
3040
  if (parent === current) {
2206
3041
  return null;
2207
3042
  }
@@ -2209,14 +3044,26 @@ async function findNearestGitRoot(startPath) {
2209
3044
  }
2210
3045
  }
2211
3046
  function withinRoot(rootPath, targetPath) {
2212
- const relative = path5.relative(rootPath, targetPath);
2213
- return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
3047
+ const relative = path8.relative(rootPath, targetPath);
3048
+ return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
3049
+ }
3050
+ function repoRootFromManifest(manifest) {
3051
+ if (manifest.originType !== "file" || !manifest.originalPath || !manifest.repoRelativePath) {
3052
+ return null;
3053
+ }
3054
+ const repoDir = path8.posix.dirname(manifest.repoRelativePath);
3055
+ const fileDir = path8.dirname(path8.resolve(manifest.originalPath));
3056
+ if (repoDir === "." || !repoDir) {
3057
+ return fileDir;
3058
+ }
3059
+ const segments = repoDir.split("/").filter(Boolean);
3060
+ return path8.resolve(fileDir, ...segments.map(() => ".."));
2214
3061
  }
2215
3062
  function repoRelativePathFor(absolutePath, repoRoot) {
2216
3063
  if (!repoRoot || !withinRoot(repoRoot, absolutePath)) {
2217
3064
  return void 0;
2218
3065
  }
2219
- const relative = toPosix(path5.relative(repoRoot, absolutePath));
3066
+ const relative = toPosix(path8.relative(repoRoot, absolutePath));
2220
3067
  return relative && !relative.startsWith("..") ? relative : void 0;
2221
3068
  }
2222
3069
  function normalizeOriginUrl(input) {
@@ -2226,6 +3073,150 @@ function normalizeOriginUrl(input) {
2226
3073
  return input;
2227
3074
  }
2228
3075
  }
3076
+ function isHttpUrl(input) {
3077
+ return /^https?:\/\//i.test(input);
3078
+ }
3079
+ function stripLeadingLabel(value, label) {
3080
+ return value.startsWith(label) ? value.slice(label.length).trim() : value.trim();
3081
+ }
3082
+ function arxivIdFromInput(input) {
3083
+ const trimmed = input.trim();
3084
+ if (/^\d{4}\.\d{4,5}(v\d+)?$/i.test(trimmed)) {
3085
+ return trimmed;
3086
+ }
3087
+ try {
3088
+ const url = new URL(trimmed);
3089
+ const match = url.pathname.match(/(\d{4}\.\d{4,5}(?:v\d+)?)/i);
3090
+ return match?.[1] ?? null;
3091
+ } catch {
3092
+ return null;
3093
+ }
3094
+ }
3095
+ function isTweetUrl(input) {
3096
+ try {
3097
+ const url = new URL(input);
3098
+ return url.hostname.includes("x.com") || url.hostname.includes("twitter.com");
3099
+ } catch {
3100
+ return false;
3101
+ }
3102
+ }
3103
+ function markdownFrontmatter(value) {
3104
+ const lines = ["---"];
3105
+ for (const [key, rawValue] of Object.entries(value)) {
3106
+ if (!rawValue) {
3107
+ continue;
3108
+ }
3109
+ lines.push(`${key}: "${rawValue.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
3110
+ }
3111
+ lines.push("---", "");
3112
+ return lines;
3113
+ }
3114
+ function prepareCapturedMarkdownInput(input) {
3115
+ return {
3116
+ title: input.title,
3117
+ originType: "url",
3118
+ sourceKind: "markdown",
3119
+ url: normalizeOriginUrl(input.url),
3120
+ mimeType: "text/markdown",
3121
+ storedExtension: ".md",
3122
+ payloadBytes: Buffer.from(input.markdown, "utf8"),
3123
+ extractedText: input.markdown,
3124
+ logDetails: input.logDetails
3125
+ };
3126
+ }
3127
+ async function fetchText(url) {
3128
+ const response = await fetch(url);
3129
+ if (!response.ok) {
3130
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
3131
+ }
3132
+ return response.text();
3133
+ }
3134
+ function domTextFromHtml(html, baseUrl) {
3135
+ const dom = new JSDOM(`<body>${html}</body>`, { url: baseUrl });
3136
+ return normalizeWhitespace(dom.window.document.body.textContent ?? "");
3137
+ }
3138
+ async function captureArxivMarkdown(input, options) {
3139
+ const arxivId = arxivIdFromInput(input);
3140
+ if (!arxivId) {
3141
+ throw new Error(`Could not determine an arXiv id from ${input}`);
3142
+ }
3143
+ const normalizedUrl = `https://arxiv.org/abs/${arxivId}`;
3144
+ const html = await fetchText(normalizedUrl);
3145
+ const dom = new JSDOM(html, { url: normalizedUrl });
3146
+ const document = dom.window.document;
3147
+ const metaTitle = document.querySelector('meta[name="citation_title"]')?.getAttribute("content")?.trim();
3148
+ const headingTitle = document.querySelector("h1.title")?.textContent?.trim();
3149
+ const title = stripLeadingLabel(metaTitle ?? headingTitle ?? arxivId, "Title:");
3150
+ const authors = [...document.querySelectorAll('meta[name="citation_author"]')].map((node) => node.getAttribute("content")?.trim()).filter((value) => Boolean(value));
3151
+ const authorsText = authors.join(", ") || stripLeadingLabel(document.querySelector(".authors")?.textContent?.trim() ?? "", "Authors:");
3152
+ const abstract = stripLeadingLabel(document.querySelector("blockquote.abstract")?.textContent?.trim() ?? "", "Abstract:");
3153
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
3154
+ const markdown = [
3155
+ ...markdownFrontmatter({
3156
+ capture_type: "arxiv",
3157
+ source_url: normalizedUrl,
3158
+ arxiv_id: arxivId,
3159
+ author: options.author,
3160
+ contributor: options.contributor,
3161
+ captured_at: capturedAt
3162
+ }),
3163
+ `# ${title}`,
3164
+ "",
3165
+ `- arXiv: ${arxivId}`,
3166
+ ...authorsText ? [`- Authors: ${authorsText}`] : [],
3167
+ ...options.author ? [`- Added By: ${options.author}`] : [],
3168
+ ...options.contributor ? [`- Contributor: ${options.contributor}`] : [],
3169
+ "",
3170
+ "## Abstract",
3171
+ "",
3172
+ abstract || "Abstract not available from the fetched arXiv page.",
3173
+ "",
3174
+ "## Source",
3175
+ "",
3176
+ `- URL: ${normalizedUrl}`,
3177
+ ""
3178
+ ].join("\n");
3179
+ return { title, normalizedUrl, markdown };
3180
+ }
3181
+ async function captureTweetMarkdown(input, options) {
3182
+ const normalizedUrl = normalizeOriginUrl(input);
3183
+ const canonicalUrl = normalizedUrl.replace("x.com", "twitter.com");
3184
+ const oembedUrl = `https://publish.twitter.com/oembed?url=${encodeURIComponent(canonicalUrl)}&omit_script=true`;
3185
+ const response = await fetch(oembedUrl);
3186
+ let postText = "";
3187
+ let postAuthor = "";
3188
+ if (response.ok) {
3189
+ const payload = await response.json();
3190
+ postText = payload.html ? domTextFromHtml(payload.html, canonicalUrl) : "";
3191
+ postAuthor = payload.author_name?.trim() ?? "";
3192
+ }
3193
+ const title = postAuthor ? `X Post by ${postAuthor}` : "X Post";
3194
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
3195
+ const markdown = [
3196
+ ...markdownFrontmatter({
3197
+ capture_type: "tweet",
3198
+ source_url: normalizedUrl,
3199
+ author: options.author,
3200
+ contributor: options.contributor,
3201
+ captured_at: capturedAt
3202
+ }),
3203
+ `# ${title}`,
3204
+ "",
3205
+ ...postAuthor ? [`- Post Author: ${postAuthor}`] : [],
3206
+ ...options.author ? [`- Added By: ${options.author}`] : [],
3207
+ ...options.contributor ? [`- Contributor: ${options.contributor}`] : [],
3208
+ "",
3209
+ "## Content",
3210
+ "",
3211
+ postText || `Captured the post link at ${normalizedUrl}. Rich text was unavailable from the public oEmbed response.`,
3212
+ "",
3213
+ "## Source",
3214
+ "",
3215
+ `- URL: ${normalizedUrl}`,
3216
+ ""
3217
+ ].join("\n");
3218
+ return { title, normalizedUrl, markdown };
3219
+ }
2229
3220
  function manifestMatchesOrigin(manifest, prepared) {
2230
3221
  if (prepared.originType === "url") {
2231
3222
  return Boolean(prepared.url && manifest.url && normalizeOriginUrl(manifest.url) === normalizeOriginUrl(prepared.url));
@@ -2240,7 +3231,7 @@ function buildCompositeHash(payloadBytes, attachments = []) {
2240
3231
  return sha256(`${sha256(payloadBytes)}|${attachmentSignature}`);
2241
3232
  }
2242
3233
  function sanitizeAssetRelativePath(value) {
2243
- const normalized = path5.posix.normalize(value.replace(/\\/g, "/"));
3234
+ const normalized = path8.posix.normalize(value.replace(/\\/g, "/"));
2244
3235
  const segments = normalized.split("/").filter(Boolean).map((segment) => {
2245
3236
  if (segment === ".") {
2246
3237
  return "";
@@ -2260,7 +3251,7 @@ function normalizeLocalReference(value) {
2260
3251
  return null;
2261
3252
  }
2262
3253
  const lowered = candidate.toLowerCase();
2263
- if (lowered.startsWith("http://") || lowered.startsWith("https://") || lowered.startsWith("data:") || lowered.startsWith("mailto:") || lowered.startsWith("#") || path5.isAbsolute(candidate)) {
3254
+ if (lowered.startsWith("http://") || lowered.startsWith("https://") || lowered.startsWith("data:") || lowered.startsWith("mailto:") || lowered.startsWith("#") || path8.isAbsolute(candidate)) {
2264
3255
  return null;
2265
3256
  }
2266
3257
  return candidate.replace(/\\/g, "/");
@@ -2322,12 +3313,12 @@ async function convertHtmlToMarkdown(html, url) {
2322
3313
  };
2323
3314
  }
2324
3315
  async function readManifestByHash(manifestsDir, contentHash) {
2325
- const entries = await fs5.readdir(manifestsDir, { withFileTypes: true }).catch(() => []);
3316
+ const entries = await fs8.readdir(manifestsDir, { withFileTypes: true }).catch(() => []);
2326
3317
  for (const entry of entries) {
2327
3318
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
2328
3319
  continue;
2329
3320
  }
2330
- const manifest = await readJsonFile(path5.join(manifestsDir, entry.name));
3321
+ const manifest = await readJsonFile(path8.join(manifestsDir, entry.name));
2331
3322
  if (manifest?.contentHash === contentHash) {
2332
3323
  return manifest;
2333
3324
  }
@@ -2335,12 +3326,12 @@ async function readManifestByHash(manifestsDir, contentHash) {
2335
3326
  return null;
2336
3327
  }
2337
3328
  async function readManifestByOrigin(manifestsDir, prepared) {
2338
- const entries = await fs5.readdir(manifestsDir, { withFileTypes: true }).catch(() => []);
3329
+ const entries = await fs8.readdir(manifestsDir, { withFileTypes: true }).catch(() => []);
2339
3330
  for (const entry of entries) {
2340
3331
  if (!entry.isFile() || !entry.name.endsWith(".json")) {
2341
3332
  continue;
2342
3333
  }
2343
- const manifest = await readJsonFile(path5.join(manifestsDir, entry.name));
3334
+ const manifest = await readJsonFile(path8.join(manifestsDir, entry.name));
2344
3335
  if (manifest && manifestMatchesOrigin(manifest, prepared)) {
2345
3336
  return manifest;
2346
3337
  }
@@ -2351,12 +3342,12 @@ async function loadGitignoreMatcher(repoRoot, enabled) {
2351
3342
  if (!enabled) {
2352
3343
  return null;
2353
3344
  }
2354
- const gitignorePath = path5.join(repoRoot, ".gitignore");
3345
+ const gitignorePath = path8.join(repoRoot, ".gitignore");
2355
3346
  if (!await fileExists(gitignorePath)) {
2356
3347
  return null;
2357
3348
  }
2358
3349
  const matcher = ignore();
2359
- matcher.add(await fs5.readFile(gitignorePath, "utf8"));
3350
+ matcher.add(await fs8.readFile(gitignorePath, "utf8"));
2360
3351
  return matcher;
2361
3352
  }
2362
3353
  function builtInIgnoreReason(relativePath) {
@@ -2377,23 +3368,23 @@ async function collectDirectoryFiles(rootDir, inputDir, repoRoot, options) {
2377
3368
  if (!currentDir) {
2378
3369
  continue;
2379
3370
  }
2380
- const entries = await fs5.readdir(currentDir, { withFileTypes: true });
3371
+ const entries = await fs8.readdir(currentDir, { withFileTypes: true });
2381
3372
  entries.sort((left, right) => left.name.localeCompare(right.name));
2382
3373
  for (const entry of entries) {
2383
- const absolutePath = path5.join(currentDir, entry.name);
2384
- const relativeToRepo = repoRelativePathFor(absolutePath, repoRoot) ?? toPosix(path5.relative(inputDir, absolutePath));
3374
+ const absolutePath = path8.join(currentDir, entry.name);
3375
+ const relativeToRepo = repoRelativePathFor(absolutePath, repoRoot) ?? toPosix(path8.relative(inputDir, absolutePath));
2385
3376
  const relativePath = relativeToRepo || entry.name;
2386
3377
  const builtInReason = builtInIgnoreReason(relativePath);
2387
3378
  if (builtInReason) {
2388
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: builtInReason });
3379
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: builtInReason });
2389
3380
  continue;
2390
3381
  }
2391
3382
  if (matcher?.ignores(relativePath)) {
2392
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: "gitignore" });
3383
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: "gitignore" });
2393
3384
  continue;
2394
3385
  }
2395
3386
  if (matchesAnyGlob(relativePath, options.exclude)) {
2396
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: "exclude_glob" });
3387
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: "exclude_glob" });
2397
3388
  continue;
2398
3389
  }
2399
3390
  if (entry.isDirectory()) {
@@ -2401,21 +3392,21 @@ async function collectDirectoryFiles(rootDir, inputDir, repoRoot, options) {
2401
3392
  continue;
2402
3393
  }
2403
3394
  if (!entry.isFile()) {
2404
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: "unsupported_entry" });
3395
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: "unsupported_entry" });
2405
3396
  continue;
2406
3397
  }
2407
3398
  if (options.include.length > 0 && !matchesAnyGlob(relativePath, options.include)) {
2408
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: "include_glob" });
3399
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: "include_glob" });
2409
3400
  continue;
2410
3401
  }
2411
3402
  const mimeType = guessMimeType(absolutePath);
2412
3403
  const sourceKind = inferKind(mimeType, absolutePath);
2413
3404
  if (!supportedDirectoryKind(sourceKind)) {
2414
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
3405
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
2415
3406
  continue;
2416
3407
  }
2417
3408
  if (files.length >= options.maxFiles) {
2418
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: "max_files" });
3409
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: "max_files" });
2419
3410
  continue;
2420
3411
  }
2421
3412
  files.push(absolutePath);
@@ -2437,12 +3428,12 @@ function resolveUrlMimeType(input, response) {
2437
3428
  function buildRemoteAssetRelativePath(assetUrl, mimeType) {
2438
3429
  const url = new URL(assetUrl);
2439
3430
  const normalized = sanitizeAssetRelativePath(`${url.hostname}${url.pathname || "/asset"}`);
2440
- const extension = path5.posix.extname(normalized);
2441
- const directory = path5.posix.dirname(normalized);
2442
- const basename = extension ? path5.posix.basename(normalized, extension) : path5.posix.basename(normalized);
3431
+ const extension = path8.posix.extname(normalized);
3432
+ const directory = path8.posix.dirname(normalized);
3433
+ const basename = extension ? path8.posix.basename(normalized, extension) : path8.posix.basename(normalized);
2443
3434
  const resolvedExtension = extension || `.${mime.extension(mimeType) || "bin"}`;
2444
3435
  const hashedName = `${basename || "asset"}-${sha256(assetUrl).slice(0, 8)}${resolvedExtension}`;
2445
- return directory === "." ? hashedName : path5.posix.join(directory, hashedName);
3436
+ return directory === "." ? hashedName : path8.posix.join(directory, hashedName);
2446
3437
  }
2447
3438
  async function readResponseBytesWithinLimit(response, maxBytes) {
2448
3439
  const contentLength = Number.parseInt(response.headers.get("content-length") ?? "", 10);
@@ -2577,27 +3568,27 @@ async function persistPreparedInput(rootDir, prepared, paths) {
2577
3568
  const previous = existingByOrigin ?? void 0;
2578
3569
  const sourceId = previous?.sourceId ?? `${slugify(prepared.title)}-${contentHash.slice(0, 8)}`;
2579
3570
  const now = (/* @__PURE__ */ new Date()).toISOString();
2580
- const storedPath = path5.join(paths.rawSourcesDir, `${sourceId}${prepared.storedExtension}`);
2581
- const extractedTextPath = prepared.extractedText ? path5.join(paths.extractsDir, `${sourceId}.md`) : void 0;
2582
- const attachmentsDir = path5.join(paths.rawAssetsDir, sourceId);
3571
+ const storedPath = path8.join(paths.rawSourcesDir, `${sourceId}${prepared.storedExtension}`);
3572
+ const extractedTextPath = prepared.extractedText ? path8.join(paths.extractsDir, `${sourceId}.md`) : void 0;
3573
+ const attachmentsDir = path8.join(paths.rawAssetsDir, sourceId);
2583
3574
  if (previous?.storedPath) {
2584
- await fs5.rm(path5.resolve(rootDir, previous.storedPath), { force: true });
3575
+ await fs8.rm(path8.resolve(rootDir, previous.storedPath), { force: true });
2585
3576
  }
2586
3577
  if (previous?.extractedTextPath) {
2587
- await fs5.rm(path5.resolve(rootDir, previous.extractedTextPath), { force: true });
3578
+ await fs8.rm(path8.resolve(rootDir, previous.extractedTextPath), { force: true });
2588
3579
  }
2589
- await fs5.rm(attachmentsDir, { recursive: true, force: true });
2590
- await fs5.writeFile(storedPath, prepared.payloadBytes);
3580
+ await fs8.rm(attachmentsDir, { recursive: true, force: true });
3581
+ await fs8.writeFile(storedPath, prepared.payloadBytes);
2591
3582
  if (prepared.extractedText && extractedTextPath) {
2592
- await fs5.writeFile(extractedTextPath, prepared.extractedText, "utf8");
3583
+ await fs8.writeFile(extractedTextPath, prepared.extractedText, "utf8");
2593
3584
  }
2594
3585
  const manifestAttachments = [];
2595
3586
  for (const attachment of attachments) {
2596
- const absoluteAttachmentPath = path5.join(attachmentsDir, attachment.relativePath);
2597
- await ensureDir(path5.dirname(absoluteAttachmentPath));
2598
- await fs5.writeFile(absoluteAttachmentPath, attachment.bytes);
3587
+ const absoluteAttachmentPath = path8.join(attachmentsDir, attachment.relativePath);
3588
+ await ensureDir(path8.dirname(absoluteAttachmentPath));
3589
+ await fs8.writeFile(absoluteAttachmentPath, attachment.bytes);
2599
3590
  manifestAttachments.push({
2600
- path: toPosix(path5.relative(rootDir, absoluteAttachmentPath)),
3591
+ path: toPosix(path8.relative(rootDir, absoluteAttachmentPath)),
2601
3592
  mimeType: attachment.mimeType,
2602
3593
  originalPath: attachment.originalPath
2603
3594
  });
@@ -2611,15 +3602,15 @@ async function persistPreparedInput(rootDir, prepared, paths) {
2611
3602
  originalPath: prepared.originalPath,
2612
3603
  repoRelativePath: prepared.repoRelativePath,
2613
3604
  url: prepared.url,
2614
- storedPath: toPosix(path5.relative(rootDir, storedPath)),
2615
- extractedTextPath: extractedTextPath ? toPosix(path5.relative(rootDir, extractedTextPath)) : void 0,
3605
+ storedPath: toPosix(path8.relative(rootDir, storedPath)),
3606
+ extractedTextPath: extractedTextPath ? toPosix(path8.relative(rootDir, extractedTextPath)) : void 0,
2616
3607
  mimeType: prepared.mimeType,
2617
3608
  contentHash,
2618
3609
  createdAt: previous?.createdAt ?? now,
2619
3610
  updatedAt: now,
2620
3611
  attachments: manifestAttachments.length ? manifestAttachments : void 0
2621
3612
  };
2622
- await writeJsonFile(path5.join(paths.manifestsDir, `${sourceId}.json`), manifest);
3613
+ await writeJsonFile(path8.join(paths.manifestsDir, `${sourceId}.json`), manifest);
2623
3614
  await appendLogEntry(rootDir, "ingest", prepared.title, [
2624
3615
  `source_id=${sourceId}`,
2625
3616
  `kind=${prepared.sourceKind}`,
@@ -2627,21 +3618,292 @@ async function persistPreparedInput(rootDir, prepared, paths) {
2627
3618
  `updated=${previous ? "true" : "false"}`,
2628
3619
  ...prepared.logDetails ?? []
2629
3620
  ]);
3621
+ if (manifest.originalPath || manifest.repoRelativePath || manifest.sourceId) {
3622
+ await clearPendingSemanticRefreshEntries(rootDir, {
3623
+ sourceId: manifest.sourceId,
3624
+ originalPath: manifest.originalPath,
3625
+ relativePath: manifest.repoRelativePath
3626
+ });
3627
+ }
2630
3628
  return { manifest, isNew: !previous, wasUpdated: Boolean(previous) };
2631
3629
  }
3630
+ async function removeManifestArtifacts(rootDir, manifest, paths) {
3631
+ await fs8.rm(path8.join(paths.manifestsDir, `${manifest.sourceId}.json`), { force: true });
3632
+ await fs8.rm(path8.resolve(rootDir, manifest.storedPath), { force: true });
3633
+ if (manifest.extractedTextPath) {
3634
+ await fs8.rm(path8.resolve(rootDir, manifest.extractedTextPath), { force: true });
3635
+ }
3636
+ await fs8.rm(path8.join(paths.rawAssetsDir, manifest.sourceId), { recursive: true, force: true });
3637
+ await fs8.rm(path8.join(paths.analysesDir, `${manifest.sourceId}.json`), { force: true });
3638
+ }
3639
+ function repoSyncWorkspaceIgnorePaths(rootDir, paths, repoRoot) {
3640
+ const candidates = [
3641
+ paths.rawDir,
3642
+ paths.wikiDir,
3643
+ paths.stateDir,
3644
+ paths.agentDir,
3645
+ paths.inboxDir,
3646
+ path8.join(rootDir, ".claude"),
3647
+ path8.join(rootDir, ".cursor"),
3648
+ path8.join(rootDir, ".obsidian")
3649
+ ];
3650
+ return candidates.map((candidate) => path8.resolve(candidate)).filter((candidate, index, items) => items.indexOf(candidate) === index).filter((candidate) => withinRoot(repoRoot, candidate));
3651
+ }
3652
+ function preparedMatchesManifest(manifest, prepared, contentHash) {
3653
+ return manifest.contentHash === contentHash && manifest.title === prepared.title && manifest.sourceKind === prepared.sourceKind && manifest.language === prepared.language && manifest.mimeType === prepared.mimeType && manifest.repoRelativePath === prepared.repoRelativePath;
3654
+ }
3655
+ function shouldDeferWatchSemanticRefresh(sourceKind) {
3656
+ return sourceKind === "markdown" || sourceKind === "text" || sourceKind === "html" || sourceKind === "pdf" || sourceKind === "image";
3657
+ }
3658
+ function pendingSemanticRefreshId(changeType, repoRoot, relativePath) {
3659
+ return `pending:${changeType}:${sha256(`${toPosix(repoRoot)}:${relativePath}`).slice(0, 12)}`;
3660
+ }
3661
+ async function listTrackedRepoRoots(rootDir) {
3662
+ const manifests = await listManifests(rootDir);
3663
+ return [...new Set(manifests.map((manifest) => repoRootFromManifest(manifest)).filter((item) => Boolean(item)))].sort(
3664
+ (left, right) => left.localeCompare(right)
3665
+ );
3666
+ }
3667
+ async function syncTrackedRepos(rootDir, options, repoRoots) {
3668
+ const { paths } = await initWorkspace(rootDir);
3669
+ const normalizedOptions = normalizeIngestOptions(options);
3670
+ const manifests = await listManifests(rootDir);
3671
+ const trackedRoots = (repoRoots && repoRoots.length > 0 ? repoRoots : await listTrackedRepoRoots(rootDir)).map(
3672
+ (item) => path8.resolve(item)
3673
+ );
3674
+ const uniqueRoots = [...new Set(trackedRoots)].sort((left, right) => left.localeCompare(right));
3675
+ const manifestsByRepoRoot = /* @__PURE__ */ new Map();
3676
+ for (const manifest of manifests) {
3677
+ const repoRoot = repoRootFromManifest(manifest);
3678
+ if (!repoRoot || !uniqueRoots.includes(path8.resolve(repoRoot))) {
3679
+ continue;
3680
+ }
3681
+ const key = path8.resolve(repoRoot);
3682
+ const bucket = manifestsByRepoRoot.get(key) ?? [];
3683
+ bucket.push(manifest);
3684
+ manifestsByRepoRoot.set(key, bucket);
3685
+ }
3686
+ const imported = [];
3687
+ const updated = [];
3688
+ const removed = [];
3689
+ const skipped = [];
3690
+ let scannedCount = 0;
3691
+ for (const repoRoot of uniqueRoots) {
3692
+ const repoManifests = manifestsByRepoRoot.get(repoRoot) ?? [];
3693
+ if (!await fileExists(repoRoot)) {
3694
+ for (const manifest of repoManifests) {
3695
+ await removeManifestArtifacts(rootDir, manifest, paths);
3696
+ removed.push(manifest);
3697
+ }
3698
+ continue;
3699
+ }
3700
+ const ignoreRoots = repoSyncWorkspaceIgnorePaths(rootDir, paths, repoRoot);
3701
+ const collected = await collectDirectoryFiles(rootDir, repoRoot, repoRoot, normalizedOptions);
3702
+ const files = collected.files.filter((absolutePath) => !ignoreRoots.some((ignoreRoot) => withinRoot(ignoreRoot, absolutePath)));
3703
+ skipped.push(
3704
+ ...collected.skipped,
3705
+ ...collected.files.filter((absolutePath) => ignoreRoots.some((ignoreRoot) => withinRoot(ignoreRoot, absolutePath))).map((absolutePath) => ({
3706
+ path: toPosix(path8.relative(rootDir, absolutePath)),
3707
+ reason: "workspace_generated"
3708
+ }))
3709
+ );
3710
+ scannedCount += files.length;
3711
+ const currentPaths = new Set(files.map((absolutePath) => path8.resolve(absolutePath)));
3712
+ for (const absolutePath of files) {
3713
+ const prepared = await prepareFileInput(rootDir, absolutePath, repoRoot);
3714
+ const result = await persistPreparedInput(rootDir, prepared, paths);
3715
+ if (result.isNew) {
3716
+ imported.push(result.manifest);
3717
+ } else if (result.wasUpdated) {
3718
+ updated.push(result.manifest);
3719
+ }
3720
+ }
3721
+ for (const manifest of repoManifests) {
3722
+ const originalPath = manifest.originalPath ? path8.resolve(manifest.originalPath) : null;
3723
+ if (originalPath && !currentPaths.has(originalPath)) {
3724
+ await removeManifestArtifacts(rootDir, manifest, paths);
3725
+ removed.push(manifest);
3726
+ }
3727
+ }
3728
+ }
3729
+ if (uniqueRoots.length > 0) {
3730
+ await appendLogEntry(rootDir, "sync_repo", uniqueRoots.map((repoRoot) => toPosix(path8.relative(rootDir, repoRoot)) || ".").join(","), [
3731
+ `repo_roots=${uniqueRoots.length}`,
3732
+ `scanned=${scannedCount}`,
3733
+ `imported=${imported.length}`,
3734
+ `updated=${updated.length}`,
3735
+ `removed=${removed.length}`,
3736
+ `skipped=${skipped.length}`
3737
+ ]);
3738
+ }
3739
+ return {
3740
+ repoRoots: uniqueRoots,
3741
+ scannedCount,
3742
+ imported,
3743
+ updated,
3744
+ removed,
3745
+ skipped
3746
+ };
3747
+ }
3748
+ async function syncTrackedReposForWatch(rootDir, options, repoRoots) {
3749
+ const { paths } = await initWorkspace(rootDir);
3750
+ const normalizedOptions = normalizeIngestOptions(options);
3751
+ const manifests = await listManifests(rootDir);
3752
+ const trackedRoots = (repoRoots && repoRoots.length > 0 ? repoRoots : await listTrackedRepoRoots(rootDir)).map(
3753
+ (item) => path8.resolve(item)
3754
+ );
3755
+ const uniqueRoots = [...new Set(trackedRoots)].sort((left, right) => left.localeCompare(right));
3756
+ const manifestsByRepoRoot = /* @__PURE__ */ new Map();
3757
+ for (const manifest of manifests) {
3758
+ const repoRoot = repoRootFromManifest(manifest);
3759
+ if (!repoRoot || !uniqueRoots.includes(path8.resolve(repoRoot))) {
3760
+ continue;
3761
+ }
3762
+ const key = path8.resolve(repoRoot);
3763
+ const bucket = manifestsByRepoRoot.get(key) ?? [];
3764
+ bucket.push(manifest);
3765
+ manifestsByRepoRoot.set(key, bucket);
3766
+ }
3767
+ const imported = [];
3768
+ const updated = [];
3769
+ const removed = [];
3770
+ const skipped = [];
3771
+ const pendingSemanticRefresh = [];
3772
+ const staleSourceIds = /* @__PURE__ */ new Set();
3773
+ let scannedCount = 0;
3774
+ for (const repoRoot of uniqueRoots) {
3775
+ const repoManifests = manifestsByRepoRoot.get(repoRoot) ?? [];
3776
+ const manifestsByOriginalPath = new Map(
3777
+ repoManifests.filter((manifest) => manifest.originalPath).map((manifest) => [path8.resolve(manifest.originalPath), manifest])
3778
+ );
3779
+ if (!await fileExists(repoRoot)) {
3780
+ for (const manifest of repoManifests) {
3781
+ if (shouldDeferWatchSemanticRefresh(manifest.sourceKind)) {
3782
+ pendingSemanticRefresh.push({
3783
+ id: pendingSemanticRefreshId("removed", repoRoot, manifest.repoRelativePath ?? manifest.storedPath),
3784
+ repoRoot,
3785
+ path: toPosix(path8.relative(rootDir, manifest.originalPath ?? manifest.storedPath)),
3786
+ changeType: "removed",
3787
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
3788
+ sourceId: manifest.sourceId,
3789
+ sourceKind: manifest.sourceKind
3790
+ });
3791
+ staleSourceIds.add(manifest.sourceId);
3792
+ } else {
3793
+ await removeManifestArtifacts(rootDir, manifest, paths);
3794
+ removed.push(manifest);
3795
+ }
3796
+ }
3797
+ continue;
3798
+ }
3799
+ const ignoreRoots = repoSyncWorkspaceIgnorePaths(rootDir, paths, repoRoot);
3800
+ const collected = await collectDirectoryFiles(rootDir, repoRoot, repoRoot, normalizedOptions);
3801
+ const files = collected.files.filter((absolutePath) => !ignoreRoots.some((ignoreRoot) => withinRoot(ignoreRoot, absolutePath)));
3802
+ skipped.push(
3803
+ ...collected.skipped,
3804
+ ...collected.files.filter((absolutePath) => ignoreRoots.some((ignoreRoot) => withinRoot(ignoreRoot, absolutePath))).map((absolutePath) => ({
3805
+ path: toPosix(path8.relative(rootDir, absolutePath)),
3806
+ reason: "workspace_generated"
3807
+ }))
3808
+ );
3809
+ scannedCount += files.length;
3810
+ const currentPaths = new Set(files.map((absolutePath) => path8.resolve(absolutePath)));
3811
+ for (const absolutePath of files) {
3812
+ const prepared = await prepareFileInput(rootDir, absolutePath, repoRoot);
3813
+ if (shouldDeferWatchSemanticRefresh(prepared.sourceKind)) {
3814
+ const existing = manifestsByOriginalPath.get(path8.resolve(absolutePath));
3815
+ const contentHash = buildCompositeHash(prepared.payloadBytes, prepared.attachments);
3816
+ const changed = !existing || !preparedMatchesManifest(existing, prepared, contentHash);
3817
+ if (changed) {
3818
+ pendingSemanticRefresh.push({
3819
+ id: pendingSemanticRefreshId(
3820
+ existing ? "modified" : "added",
3821
+ repoRoot,
3822
+ prepared.repoRelativePath ?? toPosix(path8.relative(repoRoot, absolutePath))
3823
+ ),
3824
+ repoRoot,
3825
+ path: toPosix(path8.relative(rootDir, absolutePath)),
3826
+ changeType: existing ? "modified" : "added",
3827
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
3828
+ sourceId: existing?.sourceId,
3829
+ sourceKind: prepared.sourceKind
3830
+ });
3831
+ if (existing?.sourceId) {
3832
+ staleSourceIds.add(existing.sourceId);
3833
+ }
3834
+ }
3835
+ continue;
3836
+ }
3837
+ const result = await persistPreparedInput(rootDir, prepared, paths);
3838
+ if (result.isNew) {
3839
+ imported.push(result.manifest);
3840
+ } else if (result.wasUpdated) {
3841
+ updated.push(result.manifest);
3842
+ }
3843
+ }
3844
+ for (const manifest of repoManifests) {
3845
+ const originalPath = manifest.originalPath ? path8.resolve(manifest.originalPath) : null;
3846
+ if (originalPath && !currentPaths.has(originalPath)) {
3847
+ if (shouldDeferWatchSemanticRefresh(manifest.sourceKind)) {
3848
+ pendingSemanticRefresh.push({
3849
+ id: pendingSemanticRefreshId("removed", repoRoot, manifest.repoRelativePath ?? toPosix(path8.relative(repoRoot, originalPath))),
3850
+ repoRoot,
3851
+ path: toPosix(path8.relative(rootDir, originalPath)),
3852
+ changeType: "removed",
3853
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
3854
+ sourceId: manifest.sourceId,
3855
+ sourceKind: manifest.sourceKind
3856
+ });
3857
+ staleSourceIds.add(manifest.sourceId);
3858
+ } else {
3859
+ await removeManifestArtifacts(rootDir, manifest, paths);
3860
+ removed.push(manifest);
3861
+ }
3862
+ }
3863
+ }
3864
+ }
3865
+ if (uniqueRoots.length > 0) {
3866
+ await appendLogEntry(
3867
+ rootDir,
3868
+ "sync_repo_watch",
3869
+ uniqueRoots.map((repoRoot) => toPosix(path8.relative(rootDir, repoRoot)) || ".").join(","),
3870
+ [
3871
+ `repo_roots=${uniqueRoots.length}`,
3872
+ `scanned=${scannedCount}`,
3873
+ `imported=${imported.length}`,
3874
+ `updated=${updated.length}`,
3875
+ `removed=${removed.length}`,
3876
+ `pending_semantic_refresh=${pendingSemanticRefresh.length}`,
3877
+ `skipped=${skipped.length}`
3878
+ ]
3879
+ );
3880
+ }
3881
+ return {
3882
+ repoRoots: uniqueRoots,
3883
+ scannedCount,
3884
+ imported,
3885
+ updated,
3886
+ removed,
3887
+ skipped,
3888
+ pendingSemanticRefresh: pendingSemanticRefresh.filter(
3889
+ (entry, index, items) => index === items.findIndex((candidate) => candidate.id === entry.id)
3890
+ ),
3891
+ staleSourceIds: [...staleSourceIds]
3892
+ };
3893
+ }
2632
3894
  async function prepareFileInput(_rootDir, absoluteInput, repoRoot) {
2633
- const payloadBytes = await fs5.readFile(absoluteInput);
3895
+ const payloadBytes = await fs8.readFile(absoluteInput);
2634
3896
  const mimeType = guessMimeType(absoluteInput);
2635
3897
  const sourceKind = inferKind(mimeType, absoluteInput);
2636
3898
  const language = inferCodeLanguage(absoluteInput, mimeType);
2637
- const storedExtension = path5.extname(absoluteInput) || `.${mime.extension(mimeType) || "bin"}`;
3899
+ const storedExtension = path8.extname(absoluteInput) || `.${mime.extension(mimeType) || "bin"}`;
2638
3900
  let title;
2639
3901
  let extractedText;
2640
3902
  if (sourceKind === "markdown" || sourceKind === "text" || sourceKind === "code") {
2641
3903
  extractedText = payloadBytes.toString("utf8");
2642
- title = titleFromText(path5.basename(absoluteInput, path5.extname(absoluteInput)), extractedText);
3904
+ title = titleFromText(path8.basename(absoluteInput, path8.extname(absoluteInput)), extractedText);
2643
3905
  } else {
2644
- title = path5.basename(absoluteInput, path5.extname(absoluteInput));
3906
+ title = path8.basename(absoluteInput, path8.extname(absoluteInput));
2645
3907
  }
2646
3908
  return {
2647
3909
  title,
@@ -2711,7 +3973,7 @@ async function prepareUrlInput(input, options) {
2711
3973
  sourceKind = "markdown";
2712
3974
  storedExtension = ".md";
2713
3975
  } else {
2714
- const extension = path5.extname(inputUrl.pathname);
3976
+ const extension = path8.extname(inputUrl.pathname);
2715
3977
  storedExtension = extension || `.${mime.extension(mimeType) || "bin"}`;
2716
3978
  if (sourceKind === "markdown" || sourceKind === "text" || sourceKind === "code") {
2717
3979
  extractedText = payloadBytes.toString("utf8");
@@ -2761,14 +4023,14 @@ async function collectInboxAttachmentRefs(inputDir, files) {
2761
4023
  if (sourceKind !== "markdown") {
2762
4024
  continue;
2763
4025
  }
2764
- const content = await fs5.readFile(absolutePath, "utf8");
4026
+ const content = await fs8.readFile(absolutePath, "utf8");
2765
4027
  const refs = extractMarkdownReferences(content);
2766
4028
  if (!refs.length) {
2767
4029
  continue;
2768
4030
  }
2769
4031
  const sourceRefs = [];
2770
4032
  for (const ref of refs) {
2771
- const resolved = path5.resolve(path5.dirname(absolutePath), ref);
4033
+ const resolved = path8.resolve(path8.dirname(absolutePath), ref);
2772
4034
  if (!resolved.startsWith(inputDir) || !await fileExists(resolved)) {
2773
4035
  continue;
2774
4036
  }
@@ -2802,12 +4064,12 @@ function rewriteMarkdownReferences(content, replacements) {
2802
4064
  });
2803
4065
  }
2804
4066
  async function prepareInboxMarkdownInput(absolutePath, attachmentRefs) {
2805
- const originalBytes = await fs5.readFile(absolutePath);
4067
+ const originalBytes = await fs8.readFile(absolutePath);
2806
4068
  const originalText = originalBytes.toString("utf8");
2807
- const title = titleFromText(path5.basename(absolutePath, path5.extname(absolutePath)), originalText);
4069
+ const title = titleFromText(path8.basename(absolutePath, path8.extname(absolutePath)), originalText);
2808
4070
  const attachments = [];
2809
4071
  for (const attachmentRef of attachmentRefs) {
2810
- const bytes = await fs5.readFile(attachmentRef.absolutePath);
4072
+ const bytes = await fs8.readFile(attachmentRef.absolutePath);
2811
4073
  attachments.push({
2812
4074
  relativePath: sanitizeAssetRelativePath(attachmentRef.relativeRef),
2813
4075
  mimeType: guessMimeType(attachmentRef.absolutePath),
@@ -2830,7 +4092,7 @@ async function prepareInboxMarkdownInput(absolutePath, attachmentRefs) {
2830
4092
  sourceKind: "markdown",
2831
4093
  originalPath: toPosix(absolutePath),
2832
4094
  mimeType: "text/markdown",
2833
- storedExtension: path5.extname(absolutePath) || ".md",
4095
+ storedExtension: path8.extname(absolutePath) || ".md",
2834
4096
  payloadBytes: Buffer.from(rewrittenText, "utf8"),
2835
4097
  extractedText: rewrittenText,
2836
4098
  attachments,
@@ -2843,17 +4105,70 @@ function isSupportedInboxKind(sourceKind) {
2843
4105
  async function ingestInput(rootDir, input, options) {
2844
4106
  const { paths } = await initWorkspace(rootDir);
2845
4107
  const normalizedOptions = normalizeIngestOptions(options);
2846
- const absoluteInput = path5.resolve(rootDir, input);
2847
- const repoRoot = /^https?:\/\//i.test(input) || normalizedOptions.repoRoot ? normalizedOptions.repoRoot : await findNearestGitRoot(absoluteInput).then((value) => value ?? path5.dirname(absoluteInput));
2848
- const prepared = /^https?:\/\//i.test(input) ? await prepareUrlInput(input, normalizedOptions) : await prepareFileInput(rootDir, absoluteInput, repoRoot);
4108
+ const absoluteInput = path8.resolve(rootDir, input);
4109
+ const repoRoot = isHttpUrl(input) || normalizedOptions.repoRoot ? normalizedOptions.repoRoot : await findNearestGitRoot2(absoluteInput).then((value) => value ?? path8.dirname(absoluteInput));
4110
+ const prepared = isHttpUrl(input) ? await prepareUrlInput(input, normalizedOptions) : await prepareFileInput(rootDir, absoluteInput, repoRoot);
2849
4111
  const result = await persistPreparedInput(rootDir, prepared, paths);
2850
4112
  return result.manifest;
2851
4113
  }
4114
+ async function addInput(rootDir, input, options = {}) {
4115
+ const { paths } = await initWorkspace(rootDir);
4116
+ if (!isHttpUrl(input) && !arxivIdFromInput(input)) {
4117
+ throw new Error("`swarmvault add` only supports URLs and bare arXiv ids in the current release.");
4118
+ }
4119
+ let prepared = null;
4120
+ let captureType = "url";
4121
+ let normalizedUrl = input;
4122
+ let fallback = false;
4123
+ try {
4124
+ if (arxivIdFromInput(input)) {
4125
+ const captured = await captureArxivMarkdown(input, options);
4126
+ prepared = prepareCapturedMarkdownInput({
4127
+ title: captured.title,
4128
+ url: captured.normalizedUrl,
4129
+ markdown: captured.markdown,
4130
+ logDetails: ["capture_type=arxiv"]
4131
+ });
4132
+ captureType = "arxiv";
4133
+ normalizedUrl = captured.normalizedUrl;
4134
+ } else if (isTweetUrl(input)) {
4135
+ const captured = await captureTweetMarkdown(input, options);
4136
+ prepared = prepareCapturedMarkdownInput({
4137
+ title: captured.title,
4138
+ url: captured.normalizedUrl,
4139
+ markdown: captured.markdown,
4140
+ logDetails: ["capture_type=tweet"]
4141
+ });
4142
+ captureType = "tweet";
4143
+ normalizedUrl = captured.normalizedUrl;
4144
+ }
4145
+ } catch {
4146
+ fallback = true;
4147
+ }
4148
+ if (!prepared) {
4149
+ normalizedUrl = arxivIdFromInput(input) ? `https://arxiv.org/abs/${arxivIdFromInput(input)}` : normalizeOriginUrl(input);
4150
+ return {
4151
+ captureType: "url",
4152
+ manifest: await ingestInput(rootDir, normalizedUrl, options),
4153
+ normalizedUrl,
4154
+ title: normalizedUrl,
4155
+ fallback: true
4156
+ };
4157
+ }
4158
+ const result = await persistPreparedInput(rootDir, prepared, paths);
4159
+ return {
4160
+ captureType,
4161
+ manifest: result.manifest,
4162
+ normalizedUrl,
4163
+ title: prepared.title,
4164
+ fallback
4165
+ };
4166
+ }
2852
4167
  async function ingestDirectory(rootDir, inputDir, options) {
2853
4168
  const { paths } = await initWorkspace(rootDir);
2854
4169
  const normalizedOptions = normalizeIngestOptions(options);
2855
- const absoluteInputDir = path5.resolve(rootDir, inputDir);
2856
- const repoRoot = normalizedOptions.repoRoot ?? await findNearestGitRoot(absoluteInputDir) ?? absoluteInputDir;
4170
+ const absoluteInputDir = path8.resolve(rootDir, inputDir);
4171
+ const repoRoot = normalizedOptions.repoRoot ?? await findNearestGitRoot2(absoluteInputDir) ?? absoluteInputDir;
2857
4172
  if (!await fileExists(absoluteInputDir)) {
2858
4173
  throw new Error(`Directory not found: ${absoluteInputDir}`);
2859
4174
  }
@@ -2868,11 +4183,11 @@ async function ingestDirectory(rootDir, inputDir, options) {
2868
4183
  } else if (result.wasUpdated) {
2869
4184
  updated.push(result.manifest);
2870
4185
  } else {
2871
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: "duplicate_content" });
4186
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: "duplicate_content" });
2872
4187
  }
2873
4188
  }
2874
- await appendLogEntry(rootDir, "ingest_directory", toPosix(path5.relative(rootDir, absoluteInputDir)) || ".", [
2875
- `repo_root=${toPosix(path5.relative(rootDir, repoRoot)) || "."}`,
4189
+ await appendLogEntry(rootDir, "ingest_directory", toPosix(path8.relative(rootDir, absoluteInputDir)) || ".", [
4190
+ `repo_root=${toPosix(path8.relative(rootDir, repoRoot)) || "."}`,
2876
4191
  `scanned=${files.length}`,
2877
4192
  `imported=${imported.length}`,
2878
4193
  `updated=${updated.length}`,
@@ -2889,7 +4204,7 @@ async function ingestDirectory(rootDir, inputDir, options) {
2889
4204
  }
2890
4205
  async function importInbox(rootDir, inputDir) {
2891
4206
  const { paths } = await initWorkspace(rootDir);
2892
- const effectiveInputDir = path5.resolve(rootDir, inputDir ?? paths.inboxDir);
4207
+ const effectiveInputDir = path8.resolve(rootDir, inputDir ?? paths.inboxDir);
2893
4208
  if (!await fileExists(effectiveInputDir)) {
2894
4209
  throw new Error(`Inbox directory not found: ${effectiveInputDir}`);
2895
4210
  }
@@ -2900,31 +4215,31 @@ async function importInbox(rootDir, inputDir) {
2900
4215
  const skipped = [];
2901
4216
  let attachmentCount = 0;
2902
4217
  for (const absolutePath of files) {
2903
- const basename = path5.basename(absolutePath);
4218
+ const basename = path8.basename(absolutePath);
2904
4219
  if (basename.startsWith(".")) {
2905
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: "hidden_file" });
4220
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: "hidden_file" });
2906
4221
  continue;
2907
4222
  }
2908
4223
  if (claimedAttachments.has(absolutePath)) {
2909
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: "referenced_attachment" });
4224
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: "referenced_attachment" });
2910
4225
  continue;
2911
4226
  }
2912
4227
  const mimeType = guessMimeType(absolutePath);
2913
4228
  const sourceKind = inferKind(mimeType, absolutePath);
2914
4229
  if (!isSupportedInboxKind(sourceKind)) {
2915
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
4230
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
2916
4231
  continue;
2917
4232
  }
2918
4233
  const prepared = sourceKind === "markdown" && refsBySource.has(absolutePath) ? await prepareInboxMarkdownInput(absolutePath, refsBySource.get(absolutePath) ?? []) : await prepareFileInput(rootDir, absolutePath);
2919
4234
  const result = await persistPreparedInput(rootDir, prepared, paths);
2920
4235
  if (!result.isNew) {
2921
- skipped.push({ path: toPosix(path5.relative(rootDir, absolutePath)), reason: "duplicate_content" });
4236
+ skipped.push({ path: toPosix(path8.relative(rootDir, absolutePath)), reason: "duplicate_content" });
2922
4237
  continue;
2923
4238
  }
2924
4239
  attachmentCount += result.manifest.attachments?.length ?? 0;
2925
4240
  imported.push(result.manifest);
2926
4241
  }
2927
- await appendLogEntry(rootDir, "inbox_import", toPosix(path5.relative(rootDir, effectiveInputDir)) || ".", [
4242
+ await appendLogEntry(rootDir, "inbox_import", toPosix(path8.relative(rootDir, effectiveInputDir)) || ".", [
2928
4243
  `scanned=${files.length}`,
2929
4244
  `imported=${imported.length}`,
2930
4245
  `attachments=${attachmentCount}`,
@@ -2943,9 +4258,9 @@ async function listManifests(rootDir) {
2943
4258
  if (!await fileExists(paths.manifestsDir)) {
2944
4259
  return [];
2945
4260
  }
2946
- const entries = await fs5.readdir(paths.manifestsDir);
4261
+ const entries = await fs8.readdir(paths.manifestsDir);
2947
4262
  const manifests = await Promise.all(
2948
- entries.filter((entry) => entry.endsWith(".json")).map((entry) => readJsonFile(path5.join(paths.manifestsDir, entry)))
4263
+ entries.filter((entry) => entry.endsWith(".json")).map((entry) => readJsonFile(path8.join(paths.manifestsDir, entry)))
2949
4264
  );
2950
4265
  return manifests.filter((manifest) => Boolean(manifest));
2951
4266
  }
@@ -2953,28 +4268,28 @@ async function readExtractedText(rootDir, manifest) {
2953
4268
  if (!manifest.extractedTextPath) {
2954
4269
  return void 0;
2955
4270
  }
2956
- const absolutePath = path5.resolve(rootDir, manifest.extractedTextPath);
4271
+ const absolutePath = path8.resolve(rootDir, manifest.extractedTextPath);
2957
4272
  if (!await fileExists(absolutePath)) {
2958
4273
  return void 0;
2959
4274
  }
2960
- return fs5.readFile(absolutePath, "utf8");
4275
+ return fs8.readFile(absolutePath, "utf8");
2961
4276
  }
2962
4277
 
2963
4278
  // src/mcp.ts
2964
- import fs12 from "fs/promises";
2965
- import path15 from "path";
4279
+ import fs15 from "fs/promises";
4280
+ import path18 from "path";
2966
4281
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2967
4282
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2968
4283
  import { z as z7 } from "zod";
2969
4284
 
2970
4285
  // src/schema.ts
2971
- import fs6 from "fs/promises";
2972
- import path6 from "path";
4286
+ import fs9 from "fs/promises";
4287
+ import path9 from "path";
2973
4288
  function normalizeSchemaContent(content) {
2974
4289
  return content.trim() ? content.trim() : defaultVaultSchema().trim();
2975
4290
  }
2976
4291
  async function readSchemaFile(schemaPath, fallback = defaultVaultSchema()) {
2977
- const content = await fileExists(schemaPath) ? await fs6.readFile(schemaPath, "utf8") : fallback;
4292
+ const content = await fileExists(schemaPath) ? await fs9.readFile(schemaPath, "utf8") : fallback;
2978
4293
  const normalized = normalizeSchemaContent(content);
2979
4294
  return {
2980
4295
  path: schemaPath,
@@ -2983,7 +4298,7 @@ async function readSchemaFile(schemaPath, fallback = defaultVaultSchema()) {
2983
4298
  };
2984
4299
  }
2985
4300
  function resolveProjectSchemaPath(rootDir, schemaPath) {
2986
- return path6.resolve(rootDir, schemaPath);
4301
+ return path9.resolve(rootDir, schemaPath);
2987
4302
  }
2988
4303
  function composeVaultSchema(root, projectSchemas = []) {
2989
4304
  if (!projectSchemas.length) {
@@ -2999,7 +4314,7 @@ function composeVaultSchema(root, projectSchemas = []) {
2999
4314
  (schema) => [
3000
4315
  `## Project Schema`,
3001
4316
  "",
3002
- `Path: ${toPosix(path6.relative(path6.dirname(root.path), schema.path) || schema.path)}`,
4317
+ `Path: ${toPosix(path9.relative(path9.dirname(root.path), schema.path) || schema.path)}`,
3003
4318
  "",
3004
4319
  schema.content
3005
4320
  ].join("\n")
@@ -3075,13 +4390,13 @@ function buildSchemaPrompt(schema, instruction) {
3075
4390
  }
3076
4391
 
3077
4392
  // src/vault.ts
3078
- import fs11 from "fs/promises";
3079
- import path14 from "path";
3080
- import matter7 from "gray-matter";
4393
+ import fs14 from "fs/promises";
4394
+ import path17 from "path";
4395
+ import matter8 from "gray-matter";
3081
4396
  import { z as z6 } from "zod";
3082
4397
 
3083
4398
  // src/analysis.ts
3084
- import path7 from "path";
4399
+ import path10 from "path";
3085
4400
  import { z } from "zod";
3086
4401
  var ANALYSIS_FORMAT_VERSION = 4;
3087
4402
  var sourceAnalysisSchema = z.object({
@@ -3260,7 +4575,7 @@ ${truncate(text, 18e3)}`
3260
4575
  };
3261
4576
  }
3262
4577
  async function analyzeSource(manifest, extractedText, provider, paths, schema) {
3263
- const cachePath = path7.join(paths.analysesDir, `${manifest.sourceId}.json`);
4578
+ const cachePath = path10.join(paths.analysesDir, `${manifest.sourceId}.json`);
3264
4579
  const cached = await readJsonFile(cachePath);
3265
4580
  if (cached && cached.analysisVersion === ANALYSIS_FORMAT_VERSION && cached.sourceHash === manifest.contentHash && cached.schemaHash === schema.hash) {
3266
4581
  return cached;
@@ -3300,30 +4615,112 @@ function analysisSignature(analysis) {
3300
4615
  return sha256(JSON.stringify(analysis));
3301
4616
  }
3302
4617
 
3303
- // src/confidence.ts
3304
- function nodeConfidence(sourceCount) {
3305
- return Math.min(0.5 + sourceCount * 0.15, 0.95);
4618
+ // src/benchmark.ts
4619
+ var CHARS_PER_TOKEN = 4;
4620
+ var DEFAULT_BENCHMARK_QUESTIONS = [
4621
+ "How does this vault connect the main concepts?",
4622
+ "Which pages bridge the biggest communities?",
4623
+ "What are the core abstractions in this vault?",
4624
+ "Where are the biggest knowledge gaps?",
4625
+ "What evidence should I read first?"
4626
+ ];
4627
+ function nodeMap(graph) {
4628
+ return new Map(graph.nodes.map((node) => [node.id, node]));
3306
4629
  }
3307
- function edgeConfidence(claims, conceptName) {
3308
- const lower = conceptName.toLowerCase();
3309
- const relevant = claims.filter((c) => c.text.toLowerCase().includes(lower));
3310
- if (!relevant.length) {
3311
- return 0.5;
3312
- }
3313
- return relevant.reduce((sum, c) => sum + c.confidence, 0) / relevant.length;
4630
+ function pageMap(graph) {
4631
+ return new Map(graph.pages.map((page) => [page.id, page]));
3314
4632
  }
3315
- function conflictConfidence(claimA, claimB) {
3316
- return Math.min(claimA.confidence, claimB.confidence);
4633
+ function estimateTokens(text) {
4634
+ return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
3317
4635
  }
3318
-
3319
- // src/deep-lint.ts
3320
- import fs7 from "fs/promises";
3321
- import path10 from "path";
3322
- import matter2 from "gray-matter";
3323
- import { z as z4 } from "zod";
3324
-
3325
- // src/findings.ts
3326
- function normalizeFindingSeverity(value) {
4636
+ function estimateCorpusWords(texts) {
4637
+ return texts.reduce((total, text) => total + normalizeWhitespace(text).split(/\s+/).filter(Boolean).length, 0);
4638
+ }
4639
+ function benchmarkQueryTokens(graph, queryResult, pageContentsById) {
4640
+ const nodesById = nodeMap(graph);
4641
+ const pagesById = pageMap(graph);
4642
+ const edgeIds = new Set(queryResult.visitedEdgeIds);
4643
+ const lines = [];
4644
+ for (const pageId of queryResult.pageIds) {
4645
+ const page = pagesById.get(pageId);
4646
+ if (!page) {
4647
+ continue;
4648
+ }
4649
+ const content = normalizeWhitespace(pageContentsById.get(pageId) ?? "").slice(0, 280);
4650
+ lines.push(`PAGE ${page.title} path=${page.path} kind=${page.kind}`);
4651
+ if (content) {
4652
+ lines.push(`PAGE_BODY ${content}`);
4653
+ }
4654
+ }
4655
+ for (const nodeId of queryResult.visitedNodeIds) {
4656
+ const node = nodesById.get(nodeId);
4657
+ if (!node) {
4658
+ continue;
4659
+ }
4660
+ lines.push(`NODE ${node.label} type=${node.type} community=${node.communityId ?? "unassigned"} page=${node.pageId ?? "none"}`);
4661
+ }
4662
+ for (const edge of graph.edges) {
4663
+ if (!edgeIds.has(edge.id)) {
4664
+ continue;
4665
+ }
4666
+ const source = nodesById.get(edge.source)?.label ?? edge.source;
4667
+ const target = nodesById.get(edge.target)?.label ?? edge.target;
4668
+ lines.push(`EDGE ${source} --${edge.relation}/${edge.evidenceClass}/${edge.confidence.toFixed(2)}--> ${target}`);
4669
+ }
4670
+ const queryTokens = estimateTokens(lines.join("\n"));
4671
+ return {
4672
+ question: queryResult.question,
4673
+ queryTokens,
4674
+ reduction: 0,
4675
+ visitedNodeIds: queryResult.visitedNodeIds,
4676
+ pageIds: queryResult.pageIds
4677
+ };
4678
+ }
4679
+ function buildBenchmarkArtifact(input) {
4680
+ const corpusTokens = Math.max(1, Math.round(input.corpusWords * (100 / 75)));
4681
+ const perQuestion = input.perQuestion.filter((entry) => entry.queryTokens > 0).map((entry) => ({
4682
+ ...entry,
4683
+ reduction: Number(Math.max(0, 1 - entry.queryTokens / Math.max(1, corpusTokens)).toFixed(3))
4684
+ }));
4685
+ const avgQueryTokens = perQuestion.length ? Math.max(1, Math.round(perQuestion.reduce((total, entry) => total + entry.queryTokens, 0) / perQuestion.length)) : 0;
4686
+ const reductionRatio = avgQueryTokens ? Number(Math.max(0, 1 - avgQueryTokens / Math.max(1, corpusTokens)).toFixed(3)) : 0;
4687
+ return {
4688
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4689
+ corpusWords: input.corpusWords,
4690
+ corpusTokens,
4691
+ nodes: input.graph.nodes.length,
4692
+ edges: input.graph.edges.length,
4693
+ avgQueryTokens,
4694
+ reductionRatio,
4695
+ sampleQuestions: input.questions,
4696
+ perQuestion
4697
+ };
4698
+ }
4699
+
4700
+ // src/confidence.ts
4701
+ function nodeConfidence(sourceCount) {
4702
+ return Math.min(0.5 + sourceCount * 0.15, 0.95);
4703
+ }
4704
+ function edgeConfidence(claims, conceptName) {
4705
+ const lower = conceptName.toLowerCase();
4706
+ const relevant = claims.filter((c) => c.text.toLowerCase().includes(lower));
4707
+ if (!relevant.length) {
4708
+ return 0.5;
4709
+ }
4710
+ return relevant.reduce((sum, c) => sum + c.confidence, 0) / relevant.length;
4711
+ }
4712
+ function conflictConfidence(claimA, claimB) {
4713
+ return Math.min(claimA.confidence, claimB.confidence);
4714
+ }
4715
+
4716
+ // src/deep-lint.ts
4717
+ import fs10 from "fs/promises";
4718
+ import path13 from "path";
4719
+ import matter3 from "gray-matter";
4720
+ import { z as z4 } from "zod";
4721
+
4722
+ // src/findings.ts
4723
+ function normalizeFindingSeverity(value) {
3327
4724
  if (typeof value !== "string") {
3328
4725
  return "info";
3329
4726
  }
@@ -3339,7 +4736,7 @@ function normalizeFindingSeverity(value) {
3339
4736
 
3340
4737
  // src/orchestration.ts
3341
4738
  import { spawn } from "child_process";
3342
- import path8 from "path";
4739
+ import path11 from "path";
3343
4740
  import { z as z2 } from "zod";
3344
4741
  var orchestrationRoleResultSchema = z2.object({
3345
4742
  summary: z2.string().optional(),
@@ -3432,7 +4829,7 @@ async function runProviderRole(rootDir, role, roleConfig, input) {
3432
4829
  }
3433
4830
  async function runCommandRole(rootDir, role, executor, input) {
3434
4831
  const [command, ...args] = executor.command;
3435
- const cwd = executor.cwd ? path8.resolve(rootDir, executor.cwd) : rootDir;
4832
+ const cwd = executor.cwd ? path11.resolve(rootDir, executor.cwd) : rootDir;
3436
4833
  const child = spawn(command, args, {
3437
4834
  cwd,
3438
4835
  env: {
@@ -3526,7 +4923,7 @@ function summarizeRoleQuestions(results) {
3526
4923
  }
3527
4924
 
3528
4925
  // src/web-search/registry.ts
3529
- import path9 from "path";
4926
+ import path12 from "path";
3530
4927
  import { pathToFileURL } from "url";
3531
4928
  import { z as z3 } from "zod";
3532
4929
 
@@ -3624,7 +5021,7 @@ async function createWebSearchAdapter(id, config, rootDir) {
3624
5021
  if (!config.module) {
3625
5022
  throw new Error(`Web search provider ${id} is type "custom" but no module path was configured.`);
3626
5023
  }
3627
- const resolvedModule = path9.isAbsolute(config.module) ? config.module : path9.resolve(rootDir, config.module);
5024
+ const resolvedModule = path12.isAbsolute(config.module) ? config.module : path12.resolve(rootDir, config.module);
3628
5025
  const loaded = await import(pathToFileURL(resolvedModule).href);
3629
5026
  const parsed = customWebSearchModuleSchema.parse(loaded);
3630
5027
  return parsed.createAdapter(id, config, rootDir);
@@ -3684,9 +5081,9 @@ async function loadContextPages(rootDir, graph) {
3684
5081
  );
3685
5082
  return Promise.all(
3686
5083
  contextPages.slice(0, 18).map(async (page) => {
3687
- const absolutePath = path10.join(paths.wikiDir, page.path);
3688
- const raw = await fs7.readFile(absolutePath, "utf8").catch(() => "");
3689
- const parsed = matter2(raw);
5084
+ const absolutePath = path13.join(paths.wikiDir, page.path);
5085
+ const raw = await fs10.readFile(absolutePath, "utf8").catch(() => "");
5086
+ const parsed = matter3(raw);
3690
5087
  return {
3691
5088
  id: page.id,
3692
5089
  title: page.title,
@@ -3733,7 +5130,7 @@ function heuristicDeepFindings(contextPages, structuralFindings, graph) {
3733
5130
  code: "missing_citation",
3734
5131
  message: finding.message,
3735
5132
  pagePath: finding.pagePath,
3736
- suggestedQuery: finding.pagePath ? `Which sources support the claims in ${path10.basename(finding.pagePath, ".md")}?` : void 0
5133
+ suggestedQuery: finding.pagePath ? `Which sources support the claims in ${path13.basename(finding.pagePath, ".md")}?` : void 0
3737
5134
  });
3738
5135
  }
3739
5136
  for (const page of contextPages.filter((item) => item.kind === "source").slice(0, 3)) {
@@ -4214,7 +5611,7 @@ function topGodNodes(graph, limit = 10) {
4214
5611
  }
4215
5612
 
4216
5613
  // src/markdown.ts
4217
- import matter3 from "gray-matter";
5614
+ import matter4 from "gray-matter";
4218
5615
  function uniqueStrings(values) {
4219
5616
  return uniqueBy(values.filter(Boolean), (value) => value);
4220
5617
  }
@@ -4381,7 +5778,7 @@ function buildSourcePage(manifest, analysis, schemaHash, metadata, relatedOutput
4381
5778
  compiledFrom: metadata.compiledFrom,
4382
5779
  managedBy: metadata.managedBy
4383
5780
  },
4384
- content: matter3.stringify(body, frontmatter)
5781
+ content: matter4.stringify(body, frontmatter)
4385
5782
  };
4386
5783
  }
4387
5784
  function buildModulePage(input) {
@@ -4528,7 +5925,7 @@ function buildModulePage(input) {
4528
5925
  compiledFrom: metadata.compiledFrom,
4529
5926
  managedBy: metadata.managedBy
4530
5927
  },
4531
- content: matter3.stringify(body, frontmatter)
5928
+ content: matter4.stringify(body, frontmatter)
4532
5929
  };
4533
5930
  }
4534
5931
  function buildAggregatePage(kind, name, descriptions, sourceAnalyses, sourceHashes, schemaHash, metadata, relativePath, relatedOutputs = [], decorations) {
@@ -4599,7 +5996,7 @@ function buildAggregatePage(kind, name, descriptions, sourceAnalyses, sourceHash
4599
5996
  compiledFrom: metadata.compiledFrom,
4600
5997
  managedBy: metadata.managedBy
4601
5998
  },
4602
- content: matter3.stringify(body, frontmatter)
5999
+ content: matter4.stringify(body, frontmatter)
4603
6000
  };
4604
6001
  }
4605
6002
  function buildIndexPage(pages, schemaHash, metadata, projectPages = []) {
@@ -4675,7 +6072,7 @@ function buildIndexPage(pages, schemaHash, metadata, projectPages = []) {
4675
6072
  }
4676
6073
  function buildSectionIndex(kind, pages, schemaHash, metadata, projectIds = []) {
4677
6074
  const title = kind.charAt(0).toUpperCase() + kind.slice(1);
4678
- return matter3.stringify(
6075
+ return matter4.stringify(
4679
6076
  [`# ${title}`, "", ...pages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`), ""].join("\n"),
4680
6077
  {
4681
6078
  page_id: `${kind}:index`,
@@ -4770,6 +6167,15 @@ function buildGraphReportPage(input) {
4770
6167
  `- Pages: ${input.graph.pages.length}`,
4771
6168
  `- Communities: ${input.graph.communities?.length ?? 0}`,
4772
6169
  "",
6170
+ ...input.benchmark ? [
6171
+ "## Benchmark",
6172
+ "",
6173
+ `- Corpus Tokens: ${input.benchmark.corpusTokens}`,
6174
+ `- Avg Query Tokens: ${input.benchmark.avgQueryTokens}`,
6175
+ `- Reduction Ratio: ${(input.benchmark.reductionRatio * 100).toFixed(1)}%`,
6176
+ `- Sample Questions: ${input.benchmark.sampleQuestions.length}`,
6177
+ ""
6178
+ ] : [],
4773
6179
  "## God Nodes",
4774
6180
  "",
4775
6181
  ...godNodes.length ? godNodes.map((node) => `- ${graphNodeLink(node, pagesById)} (${nodeSummary(node)})`) : ["- No high-connectivity nodes detected."],
@@ -4822,7 +6228,7 @@ function buildGraphReportPage(input) {
4822
6228
  compiledFrom: input.metadata.compiledFrom,
4823
6229
  managedBy: input.metadata.managedBy
4824
6230
  },
4825
- content: matter3.stringify(body, frontmatter)
6231
+ content: matter4.stringify(body, frontmatter)
4826
6232
  };
4827
6233
  }
4828
6234
  function buildCommunitySummaryPage(input) {
@@ -4904,11 +6310,11 @@ function buildCommunitySummaryPage(input) {
4904
6310
  compiledFrom: input.metadata.compiledFrom,
4905
6311
  managedBy: input.metadata.managedBy
4906
6312
  },
4907
- content: matter3.stringify(body, frontmatter)
6313
+ content: matter4.stringify(body, frontmatter)
4908
6314
  };
4909
6315
  }
4910
6316
  function buildProjectsIndex(projectPages, schemaHash, metadata) {
4911
- return matter3.stringify(
6317
+ return matter4.stringify(
4912
6318
  [
4913
6319
  "# Projects",
4914
6320
  "",
@@ -4938,7 +6344,7 @@ function buildProjectsIndex(projectPages, schemaHash, metadata) {
4938
6344
  }
4939
6345
  function buildProjectIndex(input) {
4940
6346
  const title = `Project: ${input.projectId}`;
4941
- return matter3.stringify(
6347
+ return matter4.stringify(
4942
6348
  [
4943
6349
  `# ${title}`,
4944
6350
  "",
@@ -5051,7 +6457,7 @@ function buildOutputPage(input) {
5051
6457
  outputFormat: input.outputFormat,
5052
6458
  outputAssets
5053
6459
  },
5054
- content: matter3.stringify(
6460
+ content: matter4.stringify(
5055
6461
  (input.outputFormat === "slides" ? [
5056
6462
  input.answer,
5057
6463
  "",
@@ -5177,7 +6583,7 @@ function buildExploreHubPage(input) {
5177
6583
  outputFormat: input.outputFormat,
5178
6584
  outputAssets
5179
6585
  },
5180
- content: matter3.stringify(
6586
+ content: matter4.stringify(
5181
6587
  (input.outputFormat === "slides" ? [
5182
6588
  `# ${title}`,
5183
6589
  "",
@@ -5441,14 +6847,14 @@ function buildOutputAssetManifest(input) {
5441
6847
  }
5442
6848
 
5443
6849
  // src/outputs.ts
5444
- import fs9 from "fs/promises";
5445
- import path12 from "path";
5446
- import matter5 from "gray-matter";
6850
+ import fs12 from "fs/promises";
6851
+ import path15 from "path";
6852
+ import matter6 from "gray-matter";
5447
6853
 
5448
6854
  // src/pages.ts
5449
- import fs8 from "fs/promises";
5450
- import path11 from "path";
5451
- import matter4 from "gray-matter";
6855
+ import fs11 from "fs/promises";
6856
+ import path14 from "path";
6857
+ import matter5 from "gray-matter";
5452
6858
  function normalizeStringArray(value) {
5453
6859
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
5454
6860
  }
@@ -5519,8 +6925,8 @@ async function loadExistingManagedPageState(absolutePath, defaults = {}) {
5519
6925
  updatedAt: updatedFallback
5520
6926
  };
5521
6927
  }
5522
- const content = await fs8.readFile(absolutePath, "utf8");
5523
- const parsed = matter4(content);
6928
+ const content = await fs11.readFile(absolutePath, "utf8");
6929
+ const parsed = matter5(content);
5524
6930
  return {
5525
6931
  status: normalizePageStatus(parsed.data.status, defaults.status ?? "active"),
5526
6932
  managedBy: normalizePageManager(parsed.data.managed_by, defaults.managedBy ?? "system"),
@@ -5554,11 +6960,11 @@ function inferPageKind(relativePath, explicitKind = void 0) {
5554
6960
  return "index";
5555
6961
  }
5556
6962
  function parseStoredPage(relativePath, content, defaults = {}) {
5557
- const parsed = matter4(content);
6963
+ const parsed = matter5(content);
5558
6964
  const now = (/* @__PURE__ */ new Date()).toISOString();
5559
6965
  const fallbackCreatedAt = defaults.createdAt ?? now;
5560
6966
  const fallbackUpdatedAt = defaults.updatedAt ?? fallbackCreatedAt;
5561
- const title = typeof parsed.data.title === "string" ? parsed.data.title : path11.basename(relativePath, ".md");
6967
+ const title = typeof parsed.data.title === "string" ? parsed.data.title : path14.basename(relativePath, ".md");
5562
6968
  const kind = inferPageKind(relativePath, parsed.data.kind);
5563
6969
  const sourceIds = normalizeStringArray(parsed.data.source_ids);
5564
6970
  const projectIds = normalizeProjectIds(parsed.data.project_ids);
@@ -5597,18 +7003,18 @@ function parseStoredPage(relativePath, content, defaults = {}) {
5597
7003
  };
5598
7004
  }
5599
7005
  async function loadInsightPages(wikiDir) {
5600
- const insightsDir = path11.join(wikiDir, "insights");
7006
+ const insightsDir = path14.join(wikiDir, "insights");
5601
7007
  if (!await fileExists(insightsDir)) {
5602
7008
  return [];
5603
7009
  }
5604
- const files = (await listFilesRecursive(insightsDir)).filter((filePath) => filePath.endsWith(".md")).filter((filePath) => path11.basename(filePath) !== "index.md").sort((left, right) => left.localeCompare(right));
7010
+ const files = (await listFilesRecursive(insightsDir)).filter((filePath) => filePath.endsWith(".md")).filter((filePath) => path14.basename(filePath) !== "index.md").sort((left, right) => left.localeCompare(right));
5605
7011
  const insights = [];
5606
7012
  for (const absolutePath of files) {
5607
- const relativePath = toPosix(path11.relative(wikiDir, absolutePath));
5608
- const content = await fs8.readFile(absolutePath, "utf8");
5609
- const parsed = matter4(content);
5610
- const stats = await fs8.stat(absolutePath);
5611
- const title = typeof parsed.data.title === "string" ? parsed.data.title : path11.basename(absolutePath, ".md");
7013
+ const relativePath = toPosix(path14.relative(wikiDir, absolutePath));
7014
+ const content = await fs11.readFile(absolutePath, "utf8");
7015
+ const parsed = matter5(content);
7016
+ const stats = await fs11.stat(absolutePath);
7017
+ const title = typeof parsed.data.title === "string" ? parsed.data.title : path14.basename(absolutePath, ".md");
5612
7018
  const sourceIds = normalizeStringArray(parsed.data.source_ids);
5613
7019
  const projectIds = normalizeProjectIds(parsed.data.project_ids);
5614
7020
  const nodeIds = normalizeStringArray(parsed.data.node_ids);
@@ -5670,28 +7076,28 @@ function relatedOutputsForPage(targetPage, outputPages) {
5670
7076
  return outputPages.map((page) => ({ page, rank: relationRank(page, targetPage) })).filter((item) => item.rank > 0).sort((left, right) => right.rank - left.rank || left.page.title.localeCompare(right.page.title)).map((item) => item.page);
5671
7077
  }
5672
7078
  async function resolveUniqueOutputSlug(wikiDir, baseSlug) {
5673
- const outputsDir = path12.join(wikiDir, "outputs");
7079
+ const outputsDir = path15.join(wikiDir, "outputs");
5674
7080
  const root = baseSlug || "output";
5675
7081
  let candidate = root;
5676
7082
  let counter = 2;
5677
- while (await fileExists(path12.join(outputsDir, `${candidate}.md`))) {
7083
+ while (await fileExists(path15.join(outputsDir, `${candidate}.md`))) {
5678
7084
  candidate = `${root}-${counter}`;
5679
7085
  counter++;
5680
7086
  }
5681
7087
  return candidate;
5682
7088
  }
5683
7089
  async function loadSavedOutputPages(wikiDir) {
5684
- const outputsDir = path12.join(wikiDir, "outputs");
5685
- const entries = await fs9.readdir(outputsDir, { withFileTypes: true }).catch(() => []);
7090
+ const outputsDir = path15.join(wikiDir, "outputs");
7091
+ const entries = await fs12.readdir(outputsDir, { withFileTypes: true }).catch(() => []);
5686
7092
  const outputs = [];
5687
7093
  for (const entry of entries) {
5688
7094
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name === "index.md") {
5689
7095
  continue;
5690
7096
  }
5691
- const relativePath = path12.posix.join("outputs", entry.name);
5692
- const absolutePath = path12.join(outputsDir, entry.name);
5693
- const content = await fs9.readFile(absolutePath, "utf8");
5694
- const parsed = matter5(content);
7097
+ const relativePath = path15.posix.join("outputs", entry.name);
7098
+ const absolutePath = path15.join(outputsDir, entry.name);
7099
+ const content = await fs12.readFile(absolutePath, "utf8");
7100
+ const parsed = matter6(content);
5695
7101
  const slug = entry.name.replace(/\.md$/, "");
5696
7102
  const title = typeof parsed.data.title === "string" ? parsed.data.title : slug;
5697
7103
  const pageId = typeof parsed.data.page_id === "string" ? parsed.data.page_id : `output:${slug}`;
@@ -5703,7 +7109,7 @@ async function loadSavedOutputPages(wikiDir) {
5703
7109
  const relatedSourceIds = normalizeStringArray(parsed.data.related_source_ids);
5704
7110
  const backlinks = normalizeStringArray(parsed.data.backlinks);
5705
7111
  const compiledFrom = normalizeStringArray(parsed.data.compiled_from);
5706
- const stats = await fs9.stat(absolutePath);
7112
+ const stats = await fs12.stat(absolutePath);
5707
7113
  const createdAt = typeof parsed.data.created_at === "string" ? parsed.data.created_at : stats.birthtimeMs > 0 ? stats.birthtime.toISOString() : stats.mtime.toISOString();
5708
7114
  const updatedAt = typeof parsed.data.updated_at === "string" ? parsed.data.updated_at : stats.mtime.toISOString();
5709
7115
  outputs.push({
@@ -5741,9 +7147,9 @@ async function loadSavedOutputPages(wikiDir) {
5741
7147
  }
5742
7148
 
5743
7149
  // src/search.ts
5744
- import fs10 from "fs/promises";
5745
- import path13 from "path";
5746
- import matter6 from "gray-matter";
7150
+ import fs13 from "fs/promises";
7151
+ import path16 from "path";
7152
+ import matter7 from "gray-matter";
5747
7153
  function getDatabaseSync() {
5748
7154
  const builtin = process.getBuiltinModule?.("node:sqlite");
5749
7155
  if (!builtin?.DatabaseSync) {
@@ -5762,7 +7168,7 @@ function normalizeStatus(value) {
5762
7168
  return value === "draft" || value === "candidate" || value === "active" || value === "archived" ? value : void 0;
5763
7169
  }
5764
7170
  async function rebuildSearchIndex(dbPath, pages, wikiDir) {
5765
- await ensureDir(path13.dirname(dbPath));
7171
+ await ensureDir(path16.dirname(dbPath));
5766
7172
  const DatabaseSync = getDatabaseSync();
5767
7173
  const db = new DatabaseSync(dbPath);
5768
7174
  db.exec("PRAGMA journal_mode = WAL;");
@@ -5792,9 +7198,9 @@ async function rebuildSearchIndex(dbPath, pages, wikiDir) {
5792
7198
  "INSERT INTO pages (id, path, title, body, kind, status, project_ids, project_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
5793
7199
  );
5794
7200
  for (const page of pages) {
5795
- const absolutePath = path13.join(wikiDir, page.path);
5796
- const content = await fs10.readFile(absolutePath, "utf8");
5797
- const parsed = matter6(content);
7201
+ const absolutePath = path16.join(wikiDir, page.path);
7202
+ const content = await fs13.readFile(absolutePath, "utf8");
7203
+ const parsed = matter7(content);
5798
7204
  insertPage.run(
5799
7205
  page.id,
5800
7206
  page.path,
@@ -5896,7 +7302,7 @@ function outputFormatInstruction(format) {
5896
7302
  }
5897
7303
  }
5898
7304
  function outputAssetPath(slug, fileName) {
5899
- return toPosix(path14.join("outputs", "assets", slug, fileName));
7305
+ return toPosix(path17.join("outputs", "assets", slug, fileName));
5900
7306
  }
5901
7307
  function outputAssetId(slug, role) {
5902
7308
  return `output:${slug}:asset:${role}`;
@@ -6036,7 +7442,7 @@ async function resolveImageGenerationProvider(rootDir) {
6036
7442
  if (!providerConfig) {
6037
7443
  throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
6038
7444
  }
6039
- const { createProvider: createProvider2 } = await import("./registry-JFEW5RUP.js");
7445
+ const { createProvider: createProvider2 } = await import("./registry-X5PMZTZY.js");
6040
7446
  return createProvider2(preferredProviderId, providerConfig, rootDir);
6041
7447
  }
6042
7448
  async function generateOutputArtifacts(rootDir, input) {
@@ -6234,7 +7640,7 @@ async function generateOutputArtifacts(rootDir, input) {
6234
7640
  };
6235
7641
  }
6236
7642
  function normalizeProjectRoot(root) {
6237
- const normalized = toPosix(path14.posix.normalize(root.replace(/\\/g, "/"))).replace(/^\.\/+/, "").replace(/\/+$/, "");
7643
+ const normalized = toPosix(path17.posix.normalize(root.replace(/\\/g, "/"))).replace(/^\.\/+/, "").replace(/\/+$/, "");
6238
7644
  return normalized;
6239
7645
  }
6240
7646
  function projectEntries(config) {
@@ -6260,10 +7666,10 @@ function manifestPathForProject(rootDir, manifest) {
6260
7666
  if (!rawPath) {
6261
7667
  return toPosix(manifest.storedPath);
6262
7668
  }
6263
- if (!path14.isAbsolute(rawPath)) {
7669
+ if (!path17.isAbsolute(rawPath)) {
6264
7670
  return normalizeProjectRoot(rawPath);
6265
7671
  }
6266
- const relative = toPosix(path14.relative(rootDir, rawPath));
7672
+ const relative = toPosix(path17.relative(rootDir, rawPath));
6267
7673
  return relative.startsWith("..") ? toPosix(rawPath) : normalizeProjectRoot(relative);
6268
7674
  }
6269
7675
  function prefixMatches(value, prefix) {
@@ -6291,9 +7697,9 @@ function scopedProjectIdsFromSources(sourceIds, sourceProjects) {
6291
7697
  const projectIds = uniqueStrings2(sourceIds.map((sourceId) => sourceProjects[sourceId] ?? "").filter(Boolean));
6292
7698
  return projectIds.length === 1 ? projectIds : [];
6293
7699
  }
6294
- function schemaProjectIdsFromPages(pageIds, pageMap) {
7700
+ function schemaProjectIdsFromPages(pageIds, pageMap2) {
6295
7701
  return uniqueStrings2(
6296
- pageIds.flatMap((pageId) => pageMap.get(pageId)?.projectIds ?? []).filter(Boolean).sort((left, right) => left.localeCompare(right))
7702
+ pageIds.flatMap((pageId) => pageMap2.get(pageId)?.projectIds ?? []).filter(Boolean).sort((left, right) => left.localeCompare(right))
6297
7703
  );
6298
7704
  }
6299
7705
  function categoryTagsForSchema(schema, texts) {
@@ -6317,12 +7723,12 @@ function previousProjectSchemaHash(previousState, projectId) {
6317
7723
  }
6318
7724
  return previousState?.effectiveSchemaHashes?.projects?.[projectId] ?? previousState?.projectSchemaHashes?.[projectId] ?? previousGlobalSchemaHash(previousState);
6319
7725
  }
6320
- function expectedSchemaHashForPage(page, schemas, pageMap, sourceProjects) {
7726
+ function expectedSchemaHashForPage(page, schemas, pageMap2, sourceProjects) {
6321
7727
  if (page.kind === "source" || page.kind === "module" || page.kind === "concept" || page.kind === "entity") {
6322
7728
  return effectiveHashForProject(schemas, scopedProjectIdsFromSources(page.sourceIds, sourceProjects)[0] ?? null);
6323
7729
  }
6324
7730
  if (page.kind === "output") {
6325
- const projectIds = schemaProjectIdsFromPages(page.relatedPageIds, pageMap);
7731
+ const projectIds = schemaProjectIdsFromPages(page.relatedPageIds, pageMap2);
6326
7732
  if (projectIds.length) {
6327
7733
  return composeVaultSchema(
6328
7734
  schemas.root,
@@ -6437,7 +7843,7 @@ function pageHashes(pages) {
6437
7843
  return Object.fromEntries(pages.map((page) => [page.page.id, page.contentHash]));
6438
7844
  }
6439
7845
  async function buildManagedGraphPage(absolutePath, defaults, build) {
6440
- const existingContent = await fileExists(absolutePath) ? await fs11.readFile(absolutePath, "utf8") : null;
7846
+ const existingContent = await fileExists(absolutePath) ? await fs14.readFile(absolutePath, "utf8") : null;
6441
7847
  let existing = await loadExistingManagedPageState(absolutePath, {
6442
7848
  status: defaults.status ?? "active",
6443
7849
  managedBy: defaults.managedBy
@@ -6475,7 +7881,7 @@ async function buildManagedGraphPage(absolutePath, defaults, build) {
6475
7881
  return built;
6476
7882
  }
6477
7883
  async function buildManagedContent(absolutePath, defaults, build) {
6478
- const existingContent = await fileExists(absolutePath) ? await fs11.readFile(absolutePath, "utf8") : null;
7884
+ const existingContent = await fileExists(absolutePath) ? await fs14.readFile(absolutePath, "utf8") : null;
6479
7885
  let existing = await loadExistingManagedPageState(absolutePath, {
6480
7886
  status: defaults.status ?? "active",
6481
7887
  managedBy: defaults.managedBy
@@ -6916,9 +8322,10 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
6916
8322
  };
6917
8323
  }
6918
8324
  async function buildGraphOrientationPages(graph, paths, schemaHash) {
8325
+ const benchmark = await readJsonFile(paths.benchmarkPath);
6919
8326
  const communityRecords = [];
6920
8327
  for (const community of graph.communities ?? []) {
6921
- const absolutePath = path14.join(paths.wikiDir, "graph", "communities", `${community.id.replace(/^community:/, "")}.md`);
8328
+ const absolutePath = path17.join(paths.wikiDir, "graph", "communities", `${community.id.replace(/^community:/, "")}.md`);
6922
8329
  communityRecords.push(
6923
8330
  await buildManagedGraphPage(
6924
8331
  absolutePath,
@@ -6938,7 +8345,7 @@ async function buildGraphOrientationPages(graph, paths, schemaHash) {
6938
8345
  )
6939
8346
  );
6940
8347
  }
6941
- const reportAbsolutePath = path14.join(paths.wikiDir, "graph", "report.md");
8348
+ const reportAbsolutePath = path17.join(paths.wikiDir, "graph", "report.md");
6942
8349
  const reportRecord = await buildManagedGraphPage(
6943
8350
  reportAbsolutePath,
6944
8351
  {
@@ -6950,13 +8357,14 @@ async function buildGraphOrientationPages(graph, paths, schemaHash) {
6950
8357
  graph,
6951
8358
  schemaHash,
6952
8359
  metadata,
6953
- communityPages: communityRecords.map((record) => record.page)
8360
+ communityPages: communityRecords.map((record) => record.page),
8361
+ benchmark
6954
8362
  })
6955
8363
  );
6956
8364
  return [reportRecord, ...communityRecords];
6957
8365
  }
6958
8366
  async function writePage(wikiDir, relativePath, content, changedPages) {
6959
- const absolutePath = path14.resolve(wikiDir, relativePath);
8367
+ const absolutePath = path17.resolve(wikiDir, relativePath);
6960
8368
  const changed = await writeFileIfChanged(absolutePath, content);
6961
8369
  if (changed) {
6962
8370
  changedPages.push(relativePath);
@@ -7018,34 +8426,29 @@ async function requiredCompileArtifactsExist(paths) {
7018
8426
  paths.graphPath,
7019
8427
  paths.codeIndexPath,
7020
8428
  paths.searchDbPath,
7021
- path14.join(paths.wikiDir, "index.md"),
7022
- path14.join(paths.wikiDir, "sources", "index.md"),
7023
- path14.join(paths.wikiDir, "code", "index.md"),
7024
- path14.join(paths.wikiDir, "concepts", "index.md"),
7025
- path14.join(paths.wikiDir, "entities", "index.md"),
7026
- path14.join(paths.wikiDir, "outputs", "index.md"),
7027
- path14.join(paths.wikiDir, "projects", "index.md"),
7028
- path14.join(paths.wikiDir, "candidates", "index.md")
8429
+ path17.join(paths.wikiDir, "index.md"),
8430
+ path17.join(paths.wikiDir, "sources", "index.md"),
8431
+ path17.join(paths.wikiDir, "code", "index.md"),
8432
+ path17.join(paths.wikiDir, "concepts", "index.md"),
8433
+ path17.join(paths.wikiDir, "entities", "index.md"),
8434
+ path17.join(paths.wikiDir, "outputs", "index.md"),
8435
+ path17.join(paths.wikiDir, "projects", "index.md"),
8436
+ path17.join(paths.wikiDir, "candidates", "index.md")
7029
8437
  ];
7030
8438
  const checks = await Promise.all(requiredPaths.map((filePath) => fileExists(filePath)));
7031
8439
  return checks.every(Boolean);
7032
8440
  }
7033
- async function loadCachedAnalyses(paths, manifests) {
7034
- return Promise.all(
7035
- manifests.map(async (manifest) => {
7036
- const cached = await readJsonFile(path14.join(paths.analysesDir, `${manifest.sourceId}.json`));
7037
- if (!cached) {
7038
- throw new Error(`Missing cached analysis for ${manifest.sourceId}. Run \`swarmvault compile\` first.`);
7039
- }
7040
- return cached;
7041
- })
8441
+ async function loadAvailableCachedAnalyses(paths, manifests) {
8442
+ const analyses = await Promise.all(
8443
+ manifests.map(async (manifest) => readJsonFile(path17.join(paths.analysesDir, `${manifest.sourceId}.json`)))
7042
8444
  );
8445
+ return analyses.filter((analysis) => Boolean(analysis));
7043
8446
  }
7044
8447
  function approvalManifestPath(paths, approvalId) {
7045
- return path14.join(paths.approvalsDir, approvalId, "manifest.json");
8448
+ return path17.join(paths.approvalsDir, approvalId, "manifest.json");
7046
8449
  }
7047
8450
  function approvalGraphPath(paths, approvalId) {
7048
- return path14.join(paths.approvalsDir, approvalId, "state", "graph.json");
8451
+ return path17.join(paths.approvalsDir, approvalId, "state", "graph.json");
7049
8452
  }
7050
8453
  async function readApprovalManifest(paths, approvalId) {
7051
8454
  const manifest = await readJsonFile(approvalManifestPath(paths, approvalId));
@@ -7055,7 +8458,7 @@ async function readApprovalManifest(paths, approvalId) {
7055
8458
  return manifest;
7056
8459
  }
7057
8460
  async function writeApprovalManifest(paths, manifest) {
7058
- await fs11.writeFile(approvalManifestPath(paths, manifest.approvalId), `${JSON.stringify(manifest, null, 2)}
8461
+ await fs14.writeFile(approvalManifestPath(paths, manifest.approvalId), `${JSON.stringify(manifest, null, 2)}
7059
8462
  `, "utf8");
7060
8463
  }
7061
8464
  async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousGraph, graph) {
@@ -7070,7 +8473,7 @@ async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousG
7070
8473
  continue;
7071
8474
  }
7072
8475
  const previousPage = previousPagesById.get(nextPage.id);
7073
- const currentExists = await fileExists(path14.join(paths.wikiDir, file.relativePath));
8476
+ const currentExists = await fileExists(path17.join(paths.wikiDir, file.relativePath));
7074
8477
  if (previousPage && previousPage.path !== nextPage.path) {
7075
8478
  entries.push({
7076
8479
  pageId: nextPage.id,
@@ -7103,7 +8506,7 @@ async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousG
7103
8506
  const previousPage = previousPagesByPath.get(deletedPath);
7104
8507
  entries.push({
7105
8508
  pageId: previousPage?.id ?? `page:${slugify(deletedPath)}`,
7106
- title: previousPage?.title ?? path14.basename(deletedPath, ".md"),
8509
+ title: previousPage?.title ?? path17.basename(deletedPath, ".md"),
7107
8510
  kind: previousPage?.kind ?? "index",
7108
8511
  changeType: "delete",
7109
8512
  status: "pending",
@@ -7115,16 +8518,16 @@ async function buildApprovalEntries(paths, changedFiles, deletedPaths, previousG
7115
8518
  }
7116
8519
  async function stageApprovalBundle(paths, changedFiles, deletedPaths, previousGraph, graph) {
7117
8520
  const approvalId = `compile-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
7118
- const approvalDir = path14.join(paths.approvalsDir, approvalId);
8521
+ const approvalDir = path17.join(paths.approvalsDir, approvalId);
7119
8522
  await ensureDir(approvalDir);
7120
- await ensureDir(path14.join(approvalDir, "wiki"));
7121
- await ensureDir(path14.join(approvalDir, "state"));
8523
+ await ensureDir(path17.join(approvalDir, "wiki"));
8524
+ await ensureDir(path17.join(approvalDir, "state"));
7122
8525
  for (const file of changedFiles) {
7123
- const targetPath = path14.join(approvalDir, "wiki", file.relativePath);
7124
- await ensureDir(path14.dirname(targetPath));
7125
- await fs11.writeFile(targetPath, file.content, "utf8");
8526
+ const targetPath = path17.join(approvalDir, "wiki", file.relativePath);
8527
+ await ensureDir(path17.dirname(targetPath));
8528
+ await fs14.writeFile(targetPath, file.content, "utf8");
7126
8529
  }
7127
- await fs11.writeFile(path14.join(approvalDir, "state", "graph.json"), JSON.stringify(graph, null, 2), "utf8");
8530
+ await fs14.writeFile(path17.join(approvalDir, "state", "graph.json"), JSON.stringify(graph, null, 2), "utf8");
7128
8531
  await writeApprovalManifest(paths, {
7129
8532
  approvalId,
7130
8533
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7184,7 +8587,7 @@ async function syncVaultArtifacts(rootDir, input) {
7184
8587
  confidence: 1
7185
8588
  });
7186
8589
  const sourceRecord = await buildManagedGraphPage(
7187
- path14.join(paths.wikiDir, preview.path),
8590
+ path17.join(paths.wikiDir, preview.path),
7188
8591
  {
7189
8592
  managedBy: "system",
7190
8593
  confidence: 1,
@@ -7229,7 +8632,7 @@ async function syncVaultArtifacts(rootDir, input) {
7229
8632
  );
7230
8633
  records.push(
7231
8634
  await buildManagedGraphPage(
7232
- path14.join(paths.wikiDir, modulePreview.path),
8635
+ path17.join(paths.wikiDir, modulePreview.path),
7233
8636
  {
7234
8637
  managedBy: "system",
7235
8638
  confidence: 1,
@@ -7262,8 +8665,8 @@ async function syncVaultArtifacts(rootDir, input) {
7262
8665
  const promoted = previousEntry?.status === "active" || promoteCandidates && shouldPromoteCandidate(previousEntry, sourceIds);
7263
8666
  const relativePath = promoted ? activeAggregatePath(itemKind, slug) : candidatePagePathFor(itemKind, slug);
7264
8667
  const fallbackPaths = [
7265
- path14.join(paths.wikiDir, activeAggregatePath(itemKind, slug)),
7266
- path14.join(paths.wikiDir, candidatePagePathFor(itemKind, slug))
8668
+ path17.join(paths.wikiDir, activeAggregatePath(itemKind, slug)),
8669
+ path17.join(paths.wikiDir, candidatePagePathFor(itemKind, slug))
7267
8670
  ];
7268
8671
  const confidence = nodeConfidence(aggregate.sourceAnalyses.length);
7269
8672
  const preview = emptyGraphPage({
@@ -7280,7 +8683,7 @@ async function syncVaultArtifacts(rootDir, input) {
7280
8683
  status: promoted ? "active" : "candidate"
7281
8684
  });
7282
8685
  const pageRecord = await buildManagedGraphPage(
7283
- path14.join(paths.wikiDir, relativePath),
8686
+ path17.join(paths.wikiDir, relativePath),
7284
8687
  {
7285
8688
  status: promoted ? "active" : "candidate",
7286
8689
  managedBy: "system",
@@ -7361,7 +8764,7 @@ async function syncVaultArtifacts(rootDir, input) {
7361
8764
  confidence: 1
7362
8765
  }),
7363
8766
  content: await buildManagedContent(
7364
- path14.join(paths.wikiDir, "projects", "index.md"),
8767
+ path17.join(paths.wikiDir, "projects", "index.md"),
7365
8768
  {
7366
8769
  managedBy: "system",
7367
8770
  compiledFrom: indexCompiledFrom(projectIndexRefs)
@@ -7385,7 +8788,7 @@ async function syncVaultArtifacts(rootDir, input) {
7385
8788
  records.push({
7386
8789
  page: projectIndexRef,
7387
8790
  content: await buildManagedContent(
7388
- path14.join(paths.wikiDir, projectIndexRef.path),
8791
+ path17.join(paths.wikiDir, projectIndexRef.path),
7389
8792
  {
7390
8793
  managedBy: "system",
7391
8794
  compiledFrom: indexCompiledFrom(Object.values(sections).flat())
@@ -7413,7 +8816,7 @@ async function syncVaultArtifacts(rootDir, input) {
7413
8816
  confidence: 1
7414
8817
  }),
7415
8818
  content: await buildManagedContent(
7416
- path14.join(paths.wikiDir, "index.md"),
8819
+ path17.join(paths.wikiDir, "index.md"),
7417
8820
  {
7418
8821
  managedBy: "system",
7419
8822
  compiledFrom: indexCompiledFrom(allPages)
@@ -7444,7 +8847,7 @@ async function syncVaultArtifacts(rootDir, input) {
7444
8847
  confidence: 1
7445
8848
  }),
7446
8849
  content: await buildManagedContent(
7447
- path14.join(paths.wikiDir, relativePath),
8850
+ path17.join(paths.wikiDir, relativePath),
7448
8851
  {
7449
8852
  managedBy: "system",
7450
8853
  compiledFrom: indexCompiledFrom(pages)
@@ -7455,12 +8858,12 @@ async function syncVaultArtifacts(rootDir, input) {
7455
8858
  }
7456
8859
  const nextPagePaths = new Set(records.map((record) => record.page.path));
7457
8860
  const obsoleteGraphPaths = (previousGraph?.pages ?? []).filter((page) => page.kind !== "output" && page.kind !== "insight").map((page) => page.path).filter((relativePath) => !nextPagePaths.has(relativePath));
7458
- const existingProjectIndexPaths = (await listFilesRecursive(paths.projectsDir)).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path14.relative(paths.wikiDir, absolutePath))).filter((relativePath) => !nextPagePaths.has(relativePath));
8861
+ const existingProjectIndexPaths = (await listFilesRecursive(paths.projectsDir)).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path17.relative(paths.wikiDir, absolutePath))).filter((relativePath) => !nextPagePaths.has(relativePath));
7459
8862
  const obsoletePaths = uniqueStrings2([...obsoleteGraphPaths, ...existingProjectIndexPaths]);
7460
8863
  const changedFiles = [];
7461
8864
  for (const record of records) {
7462
- const absolutePath = path14.join(paths.wikiDir, record.page.path);
7463
- const current = await fileExists(absolutePath) ? await fs11.readFile(absolutePath, "utf8") : null;
8865
+ const absolutePath = path17.join(paths.wikiDir, record.page.path);
8866
+ const current = await fileExists(absolutePath) ? await fs14.readFile(absolutePath, "utf8") : null;
7464
8867
  if (current !== record.content) {
7465
8868
  changedPages.push(record.page.path);
7466
8869
  changedFiles.push({ relativePath: record.page.path, content: record.content });
@@ -7485,7 +8888,7 @@ async function syncVaultArtifacts(rootDir, input) {
7485
8888
  await writePage(paths.wikiDir, record.page.path, record.content, writeChanges);
7486
8889
  }
7487
8890
  for (const relativePath of obsoletePaths) {
7488
- await fs11.rm(path14.join(paths.wikiDir, relativePath), { force: true });
8891
+ await fs14.rm(path17.join(paths.wikiDir, relativePath), { force: true });
7489
8892
  }
7490
8893
  await writeJsonFile(paths.graphPath, graph);
7491
8894
  await writeJsonFile(paths.codeIndexPath, input.codeIndex);
@@ -7556,17 +8959,17 @@ async function refreshIndexesAndSearch(rootDir, pages) {
7556
8959
  })
7557
8960
  );
7558
8961
  await Promise.all([
7559
- ensureDir(path14.join(paths.wikiDir, "sources")),
7560
- ensureDir(path14.join(paths.wikiDir, "code")),
7561
- ensureDir(path14.join(paths.wikiDir, "concepts")),
7562
- ensureDir(path14.join(paths.wikiDir, "entities")),
7563
- ensureDir(path14.join(paths.wikiDir, "outputs")),
7564
- ensureDir(path14.join(paths.wikiDir, "graph")),
7565
- ensureDir(path14.join(paths.wikiDir, "graph", "communities")),
7566
- ensureDir(path14.join(paths.wikiDir, "projects")),
7567
- ensureDir(path14.join(paths.wikiDir, "candidates"))
8962
+ ensureDir(path17.join(paths.wikiDir, "sources")),
8963
+ ensureDir(path17.join(paths.wikiDir, "code")),
8964
+ ensureDir(path17.join(paths.wikiDir, "concepts")),
8965
+ ensureDir(path17.join(paths.wikiDir, "entities")),
8966
+ ensureDir(path17.join(paths.wikiDir, "outputs")),
8967
+ ensureDir(path17.join(paths.wikiDir, "graph")),
8968
+ ensureDir(path17.join(paths.wikiDir, "graph", "communities")),
8969
+ ensureDir(path17.join(paths.wikiDir, "projects")),
8970
+ ensureDir(path17.join(paths.wikiDir, "candidates"))
7568
8971
  ]);
7569
- const projectsIndexPath = path14.join(paths.wikiDir, "projects", "index.md");
8972
+ const projectsIndexPath = path17.join(paths.wikiDir, "projects", "index.md");
7570
8973
  await writeFileIfChanged(
7571
8974
  projectsIndexPath,
7572
8975
  await buildManagedContent(
@@ -7587,7 +8990,7 @@ async function refreshIndexesAndSearch(rootDir, pages) {
7587
8990
  outputs: pages.filter((page) => page.kind === "output" && page.projectIds.includes(project.id)),
7588
8991
  candidates: pages.filter((page) => page.status === "candidate" && page.projectIds.includes(project.id))
7589
8992
  };
7590
- const absolutePath = path14.join(paths.wikiDir, "projects", project.id, "index.md");
8993
+ const absolutePath = path17.join(paths.wikiDir, "projects", project.id, "index.md");
7591
8994
  await writeFileIfChanged(
7592
8995
  absolutePath,
7593
8996
  await buildManagedContent(
@@ -7605,7 +9008,7 @@ async function refreshIndexesAndSearch(rootDir, pages) {
7605
9008
  )
7606
9009
  );
7607
9010
  }
7608
- const rootIndexPath = path14.join(paths.wikiDir, "index.md");
9011
+ const rootIndexPath = path17.join(paths.wikiDir, "index.md");
7609
9012
  await writeFileIfChanged(
7610
9013
  rootIndexPath,
7611
9014
  await buildManagedContent(
@@ -7626,7 +9029,7 @@ async function refreshIndexesAndSearch(rootDir, pages) {
7626
9029
  ["candidates/index.md", "candidates", pagesWithGraph.filter((page) => page.status === "candidate")],
7627
9030
  ["graph/index.md", "graph", pagesWithGraph.filter((page) => page.kind === "graph_report" || page.kind === "community_summary")]
7628
9031
  ]) {
7629
- const absolutePath = path14.join(paths.wikiDir, relativePath);
9032
+ const absolutePath = path17.join(paths.wikiDir, relativePath);
7630
9033
  await writeFileIfChanged(
7631
9034
  absolutePath,
7632
9035
  await buildManagedContent(
@@ -7640,20 +9043,20 @@ async function refreshIndexesAndSearch(rootDir, pages) {
7640
9043
  );
7641
9044
  }
7642
9045
  for (const record of graphOrientationRecords) {
7643
- await writeFileIfChanged(path14.join(paths.wikiDir, record.page.path), record.content);
9046
+ await writeFileIfChanged(path17.join(paths.wikiDir, record.page.path), record.content);
7644
9047
  }
7645
- const existingProjectIndexPaths = (await listFilesRecursive(paths.projectsDir)).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path14.relative(paths.wikiDir, absolutePath)));
9048
+ const existingProjectIndexPaths = (await listFilesRecursive(paths.projectsDir)).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path17.relative(paths.wikiDir, absolutePath)));
7646
9049
  const allowedProjectIndexPaths = /* @__PURE__ */ new Set([
7647
9050
  "projects/index.md",
7648
9051
  ...configuredProjects.map((project) => `projects/${project.id}/index.md`)
7649
9052
  ]);
7650
9053
  await Promise.all(
7651
- existingProjectIndexPaths.filter((relativePath) => !allowedProjectIndexPaths.has(relativePath)).map((relativePath) => fs11.rm(path14.join(paths.wikiDir, relativePath), { force: true }))
9054
+ existingProjectIndexPaths.filter((relativePath) => !allowedProjectIndexPaths.has(relativePath)).map((relativePath) => fs14.rm(path17.join(paths.wikiDir, relativePath), { force: true }))
7652
9055
  );
7653
- const existingGraphPages = (await listFilesRecursive(path14.join(paths.wikiDir, "graph").replace(/\/$/, "")).catch(() => [])).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path14.relative(paths.wikiDir, absolutePath)));
9056
+ const existingGraphPages = (await listFilesRecursive(path17.join(paths.wikiDir, "graph").replace(/\/$/, "")).catch(() => [])).filter((absolutePath) => absolutePath.endsWith(".md")).map((absolutePath) => toPosix(path17.relative(paths.wikiDir, absolutePath)));
7654
9057
  const allowedGraphPages = /* @__PURE__ */ new Set(["graph/index.md", ...graphOrientationRecords.map((record) => record.page.path)]);
7655
9058
  await Promise.all(
7656
- existingGraphPages.filter((relativePath) => !allowedGraphPages.has(relativePath)).map((relativePath) => fs11.rm(path14.join(paths.wikiDir, relativePath), { force: true }))
9059
+ existingGraphPages.filter((relativePath) => !allowedGraphPages.has(relativePath)).map((relativePath) => fs14.rm(path17.join(paths.wikiDir, relativePath), { force: true }))
7657
9060
  );
7658
9061
  await rebuildSearchIndex(paths.searchDbPath, pagesWithGraph, paths.wikiDir);
7659
9062
  }
@@ -7673,7 +9076,7 @@ async function prepareOutputPageSave(rootDir, input) {
7673
9076
  confidence: 0.74
7674
9077
  }
7675
9078
  });
7676
- const absolutePath = path14.join(paths.wikiDir, output.page.path);
9079
+ const absolutePath = path17.join(paths.wikiDir, output.page.path);
7677
9080
  return {
7678
9081
  page: output.page,
7679
9082
  savedPath: absolutePath,
@@ -7685,15 +9088,15 @@ async function prepareOutputPageSave(rootDir, input) {
7685
9088
  async function persistOutputPage(rootDir, input) {
7686
9089
  const { paths } = await loadVaultConfig(rootDir);
7687
9090
  const prepared = await prepareOutputPageSave(rootDir, input);
7688
- await ensureDir(path14.dirname(prepared.savedPath));
7689
- await fs11.writeFile(prepared.savedPath, prepared.content, "utf8");
9091
+ await ensureDir(path17.dirname(prepared.savedPath));
9092
+ await fs14.writeFile(prepared.savedPath, prepared.content, "utf8");
7690
9093
  for (const assetFile of prepared.assetFiles) {
7691
- const assetPath = path14.join(paths.wikiDir, assetFile.relativePath);
7692
- await ensureDir(path14.dirname(assetPath));
9094
+ const assetPath = path17.join(paths.wikiDir, assetFile.relativePath);
9095
+ await ensureDir(path17.dirname(assetPath));
7693
9096
  if (typeof assetFile.content === "string") {
7694
- await fs11.writeFile(assetPath, assetFile.content, assetFile.encoding ?? "utf8");
9097
+ await fs14.writeFile(assetPath, assetFile.content, assetFile.encoding ?? "utf8");
7695
9098
  } else {
7696
- await fs11.writeFile(assetPath, assetFile.content);
9099
+ await fs14.writeFile(assetPath, assetFile.content);
7697
9100
  }
7698
9101
  }
7699
9102
  return { page: prepared.page, savedPath: prepared.savedPath, outputAssets: prepared.outputAssets };
@@ -7714,7 +9117,7 @@ async function prepareExploreHubSave(rootDir, input) {
7714
9117
  confidence: 0.76
7715
9118
  }
7716
9119
  });
7717
- const absolutePath = path14.join(paths.wikiDir, hub.page.path);
9120
+ const absolutePath = path17.join(paths.wikiDir, hub.page.path);
7718
9121
  return {
7719
9122
  page: hub.page,
7720
9123
  savedPath: absolutePath,
@@ -7726,15 +9129,15 @@ async function prepareExploreHubSave(rootDir, input) {
7726
9129
  async function persistExploreHub(rootDir, input) {
7727
9130
  const { paths } = await loadVaultConfig(rootDir);
7728
9131
  const prepared = await prepareExploreHubSave(rootDir, input);
7729
- await ensureDir(path14.dirname(prepared.savedPath));
7730
- await fs11.writeFile(prepared.savedPath, prepared.content, "utf8");
9132
+ await ensureDir(path17.dirname(prepared.savedPath));
9133
+ await fs14.writeFile(prepared.savedPath, prepared.content, "utf8");
7731
9134
  for (const assetFile of prepared.assetFiles) {
7732
- const assetPath = path14.join(paths.wikiDir, assetFile.relativePath);
7733
- await ensureDir(path14.dirname(assetPath));
9135
+ const assetPath = path17.join(paths.wikiDir, assetFile.relativePath);
9136
+ await ensureDir(path17.dirname(assetPath));
7734
9137
  if (typeof assetFile.content === "string") {
7735
- await fs11.writeFile(assetPath, assetFile.content, assetFile.encoding ?? "utf8");
9138
+ await fs14.writeFile(assetPath, assetFile.content, assetFile.encoding ?? "utf8");
7736
9139
  } else {
7737
- await fs11.writeFile(assetPath, assetFile.content);
9140
+ await fs14.writeFile(assetPath, assetFile.content);
7738
9141
  }
7739
9142
  }
7740
9143
  return { page: prepared.page, savedPath: prepared.savedPath, outputAssets: prepared.outputAssets };
@@ -7751,17 +9154,17 @@ async function stageOutputApprovalBundle(rootDir, stagedPages) {
7751
9154
  }))
7752
9155
  ]);
7753
9156
  const approvalId = `schedule-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
7754
- const approvalDir = path14.join(paths.approvalsDir, approvalId);
9157
+ const approvalDir = path17.join(paths.approvalsDir, approvalId);
7755
9158
  await ensureDir(approvalDir);
7756
- await ensureDir(path14.join(approvalDir, "wiki"));
7757
- await ensureDir(path14.join(approvalDir, "state"));
9159
+ await ensureDir(path17.join(approvalDir, "wiki"));
9160
+ await ensureDir(path17.join(approvalDir, "state"));
7758
9161
  for (const file of changedFiles) {
7759
- const targetPath = path14.join(approvalDir, "wiki", file.relativePath);
7760
- await ensureDir(path14.dirname(targetPath));
9162
+ const targetPath = path17.join(approvalDir, "wiki", file.relativePath);
9163
+ await ensureDir(path17.dirname(targetPath));
7761
9164
  if ("binary" in file && file.binary) {
7762
- await fs11.writeFile(targetPath, Buffer.from(file.content, "base64"));
9165
+ await fs14.writeFile(targetPath, Buffer.from(file.content, "base64"));
7763
9166
  } else {
7764
- await fs11.writeFile(targetPath, file.content, "utf8");
9167
+ await fs14.writeFile(targetPath, file.content, "utf8");
7765
9168
  }
7766
9169
  }
7767
9170
  const nextPages = sortGraphPages([
@@ -7775,7 +9178,7 @@ async function stageOutputApprovalBundle(rootDir, stagedPages) {
7775
9178
  sources: previousGraph?.sources ?? [],
7776
9179
  pages: nextPages
7777
9180
  };
7778
- await fs11.writeFile(path14.join(approvalDir, "state", "graph.json"), JSON.stringify(graph, null, 2), "utf8");
9181
+ await fs14.writeFile(path17.join(approvalDir, "state", "graph.json"), JSON.stringify(graph, null, 2), "utf8");
7779
9182
  await writeApprovalManifest(paths, {
7780
9183
  approvalId,
7781
9184
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -7797,17 +9200,17 @@ async function executeQuery(rootDir, question, format) {
7797
9200
  await compileVault(rootDir, {});
7798
9201
  }
7799
9202
  const graph = await readJsonFile(paths.graphPath);
7800
- const pageMap = new Map((graph?.pages ?? []).map((page) => [page.id, page]));
9203
+ const pageMap2 = new Map((graph?.pages ?? []).map((page) => [page.id, page]));
7801
9204
  const sourceProjects = Object.fromEntries(
7802
9205
  (graph?.pages ?? []).filter((page) => page.kind === "source" && page.sourceIds.length).map((page) => [page.sourceIds[0], page.projectIds[0] ?? null])
7803
9206
  );
7804
9207
  const searchResults = searchPages(paths.searchDbPath, question, 5);
7805
9208
  const excerpts = await Promise.all(
7806
9209
  searchResults.map(async (result) => {
7807
- const absolutePath = path14.join(paths.wikiDir, result.path);
9210
+ const absolutePath = path17.join(paths.wikiDir, result.path);
7808
9211
  try {
7809
- const content = await fs11.readFile(absolutePath, "utf8");
7810
- const parsed = matter7(content);
9212
+ const content = await fs14.readFile(absolutePath, "utf8");
9213
+ const parsed = matter8(content);
7811
9214
  return `# ${result.title}
7812
9215
  ${truncate(normalizeWhitespace(parsed.content), 1200)}`;
7813
9216
  } catch {
@@ -7821,14 +9224,14 @@ ${result.snippet}`;
7821
9224
  (item) => item
7822
9225
  );
7823
9226
  const relatedNodeIds = uniqueBy(
7824
- relatedPageIds.flatMap((pageId) => pageMap.get(pageId)?.nodeIds ?? []),
9227
+ relatedPageIds.flatMap((pageId) => pageMap2.get(pageId)?.nodeIds ?? []),
7825
9228
  (item) => item
7826
9229
  );
7827
9230
  const relatedSourceIds = uniqueBy(
7828
- relatedPageIds.flatMap((pageId) => pageMap.get(pageId)?.sourceIds ?? []),
9231
+ relatedPageIds.flatMap((pageId) => pageMap2.get(pageId)?.sourceIds ?? []),
7829
9232
  (item) => item
7830
9233
  );
7831
- const schemaProjectIds = schemaProjectIdsFromPages(relatedPageIds, pageMap);
9234
+ const schemaProjectIds = schemaProjectIdsFromPages(relatedPageIds, pageMap2);
7832
9235
  const querySchema = composeVaultSchema(
7833
9236
  schemas.root,
7834
9237
  schemaProjectIds.map((projectId) => schemas.projects[projectId]).filter((schema) => Boolean(schema?.hash))
@@ -7915,7 +9318,7 @@ async function refreshVaultAfterOutputSave(rootDir) {
7915
9318
  const schemas = await loadVaultSchemas(rootDir);
7916
9319
  const manifests = await listManifests(rootDir);
7917
9320
  const sourceProjects = resolveSourceProjects(rootDir, manifests, config);
7918
- const cachedAnalyses = manifests.length ? await loadCachedAnalyses(paths, manifests) : [];
9321
+ const cachedAnalyses = manifests.length ? await loadAvailableCachedAnalyses(paths, manifests) : [];
7919
9322
  const codeIndex = await buildCodeIndex(rootDir, manifests, cachedAnalyses);
7920
9323
  const analyses = cachedAnalyses.map((analysis) => {
7921
9324
  const manifest = manifests.find((item) => item.sourceId === analysis.sourceId);
@@ -7988,7 +9391,7 @@ function sortGraphPages(pages) {
7988
9391
  async function listApprovals(rootDir) {
7989
9392
  const { paths } = await loadVaultConfig(rootDir);
7990
9393
  const manifests = await Promise.all(
7991
- (await fs11.readdir(paths.approvalsDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map(async (entry) => {
9394
+ (await fs14.readdir(paths.approvalsDir, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map(async (entry) => {
7992
9395
  try {
7993
9396
  return await readApprovalManifest(paths, entry.name);
7994
9397
  } catch {
@@ -8004,8 +9407,8 @@ async function readApproval(rootDir, approvalId) {
8004
9407
  const details = await Promise.all(
8005
9408
  manifest.entries.map(async (entry) => {
8006
9409
  const currentPath = entry.previousPath ?? entry.nextPath;
8007
- const currentContent = currentPath ? await fs11.readFile(path14.join(paths.wikiDir, currentPath), "utf8").catch(() => void 0) : void 0;
8008
- const stagedContent = entry.nextPath ? await fs11.readFile(path14.join(paths.approvalsDir, approvalId, "wiki", entry.nextPath), "utf8").catch(() => void 0) : void 0;
9410
+ const currentContent = currentPath ? await fs14.readFile(path17.join(paths.wikiDir, currentPath), "utf8").catch(() => void 0) : void 0;
9411
+ const stagedContent = entry.nextPath ? await fs14.readFile(path17.join(paths.approvalsDir, approvalId, "wiki", entry.nextPath), "utf8").catch(() => void 0) : void 0;
8009
9412
  return {
8010
9413
  ...entry,
8011
9414
  currentContent,
@@ -8033,26 +9436,26 @@ async function acceptApproval(rootDir, approvalId, targets = []) {
8033
9436
  if (!entry.nextPath) {
8034
9437
  throw new Error(`Approval entry ${entry.pageId} is missing a staged path.`);
8035
9438
  }
8036
- const stagedAbsolutePath = path14.join(paths.approvalsDir, approvalId, "wiki", entry.nextPath);
8037
- const stagedContent = await fs11.readFile(stagedAbsolutePath, "utf8");
8038
- const targetAbsolutePath = path14.join(paths.wikiDir, entry.nextPath);
8039
- await ensureDir(path14.dirname(targetAbsolutePath));
8040
- await fs11.writeFile(targetAbsolutePath, stagedContent, "utf8");
9439
+ const stagedAbsolutePath = path17.join(paths.approvalsDir, approvalId, "wiki", entry.nextPath);
9440
+ const stagedContent = await fs14.readFile(stagedAbsolutePath, "utf8");
9441
+ const targetAbsolutePath = path17.join(paths.wikiDir, entry.nextPath);
9442
+ await ensureDir(path17.dirname(targetAbsolutePath));
9443
+ await fs14.writeFile(targetAbsolutePath, stagedContent, "utf8");
8041
9444
  if (entry.changeType === "promote" && entry.previousPath) {
8042
- await fs11.rm(path14.join(paths.wikiDir, entry.previousPath), { force: true });
9445
+ await fs14.rm(path17.join(paths.wikiDir, entry.previousPath), { force: true });
8043
9446
  }
8044
9447
  const nextPage = bundleGraph?.pages.find((page) => page.id === entry.pageId && page.path === entry.nextPath) ?? parseStoredPage(entry.nextPath, stagedContent);
8045
9448
  if (nextPage.kind === "output" && nextPage.outputAssets?.length) {
8046
- const outputAssetDir = path14.join(paths.wikiDir, "outputs", "assets", path14.basename(nextPage.path, ".md"));
8047
- await fs11.rm(outputAssetDir, { recursive: true, force: true });
9449
+ const outputAssetDir = path17.join(paths.wikiDir, "outputs", "assets", path17.basename(nextPage.path, ".md"));
9450
+ await fs14.rm(outputAssetDir, { recursive: true, force: true });
8048
9451
  for (const asset of nextPage.outputAssets) {
8049
- const stagedAssetPath = path14.join(paths.approvalsDir, approvalId, "wiki", asset.path);
9452
+ const stagedAssetPath = path17.join(paths.approvalsDir, approvalId, "wiki", asset.path);
8050
9453
  if (!await fileExists(stagedAssetPath)) {
8051
9454
  continue;
8052
9455
  }
8053
- const targetAssetPath = path14.join(paths.wikiDir, asset.path);
8054
- await ensureDir(path14.dirname(targetAssetPath));
8055
- await fs11.copyFile(stagedAssetPath, targetAssetPath);
9456
+ const targetAssetPath = path17.join(paths.wikiDir, asset.path);
9457
+ await ensureDir(path17.dirname(targetAssetPath));
9458
+ await fs14.copyFile(stagedAssetPath, targetAssetPath);
8056
9459
  }
8057
9460
  }
8058
9461
  nextPages = nextPages.filter(
@@ -8063,10 +9466,10 @@ async function acceptApproval(rootDir, approvalId, targets = []) {
8063
9466
  } else {
8064
9467
  const deletedPage = nextPages.find((page) => page.id === entry.pageId || page.path === entry.previousPath) ?? bundleGraph?.pages.find((page) => page.id === entry.pageId || page.path === entry.previousPath) ?? null;
8065
9468
  if (entry.previousPath) {
8066
- await fs11.rm(path14.join(paths.wikiDir, entry.previousPath), { force: true });
9469
+ await fs14.rm(path17.join(paths.wikiDir, entry.previousPath), { force: true });
8067
9470
  }
8068
9471
  if (deletedPage?.kind === "output") {
8069
- await fs11.rm(path14.join(paths.wikiDir, "outputs", "assets", path14.basename(deletedPage.path, ".md")), {
9472
+ await fs14.rm(path17.join(paths.wikiDir, "outputs", "assets", path17.basename(deletedPage.path, ".md")), {
8070
9473
  recursive: true,
8071
9474
  force: true
8072
9475
  });
@@ -8156,10 +9559,10 @@ async function promoteCandidate(rootDir, target) {
8156
9559
  const { paths } = await loadVaultConfig(rootDir);
8157
9560
  const graph = await readJsonFile(paths.graphPath);
8158
9561
  const candidate = resolveCandidateTarget(graph?.pages ?? [], target);
8159
- const raw = await fs11.readFile(path14.join(paths.wikiDir, candidate.path), "utf8");
8160
- const parsed = matter7(raw);
9562
+ const raw = await fs14.readFile(path17.join(paths.wikiDir, candidate.path), "utf8");
9563
+ const parsed = matter8(raw);
8161
9564
  const nextUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
8162
- const nextContent = matter7.stringify(parsed.content, {
9565
+ const nextContent = matter8.stringify(parsed.content, {
8163
9566
  ...parsed.data,
8164
9567
  status: "active",
8165
9568
  updated_at: nextUpdatedAt,
@@ -8168,10 +9571,10 @@ async function promoteCandidate(rootDir, target) {
8168
9571
  )
8169
9572
  });
8170
9573
  const nextPath = candidateActivePath(candidate);
8171
- const nextAbsolutePath = path14.join(paths.wikiDir, nextPath);
8172
- await ensureDir(path14.dirname(nextAbsolutePath));
8173
- await fs11.writeFile(nextAbsolutePath, nextContent, "utf8");
8174
- await fs11.rm(path14.join(paths.wikiDir, candidate.path), { force: true });
9574
+ const nextAbsolutePath = path17.join(paths.wikiDir, nextPath);
9575
+ await ensureDir(path17.dirname(nextAbsolutePath));
9576
+ await fs14.writeFile(nextAbsolutePath, nextContent, "utf8");
9577
+ await fs14.rm(path17.join(paths.wikiDir, candidate.path), { force: true });
8175
9578
  const nextPage = parseStoredPage(nextPath, nextContent, { createdAt: candidate.createdAt, updatedAt: nextUpdatedAt });
8176
9579
  const nextPages = sortGraphPages(
8177
9580
  (graph?.pages ?? []).filter((page) => page.id !== candidate.id && page.path !== candidate.path).concat(nextPage)
@@ -8215,7 +9618,7 @@ async function archiveCandidate(rootDir, target) {
8215
9618
  const { paths } = await loadVaultConfig(rootDir);
8216
9619
  const graph = await readJsonFile(paths.graphPath);
8217
9620
  const candidate = resolveCandidateTarget(graph?.pages ?? [], target);
8218
- await fs11.rm(path14.join(paths.wikiDir, candidate.path), { force: true });
9621
+ await fs14.rm(path17.join(paths.wikiDir, candidate.path), { force: true });
8219
9622
  const nextPages = sortGraphPages((graph?.pages ?? []).filter((page) => page.id !== candidate.id && page.path !== candidate.path));
8220
9623
  const nextGraph = {
8221
9624
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8253,18 +9656,18 @@ async function archiveCandidate(rootDir, target) {
8253
9656
  }
8254
9657
  async function ensureObsidianWorkspace(rootDir) {
8255
9658
  const { config } = await loadVaultConfig(rootDir);
8256
- const obsidianDir = path14.join(rootDir, ".obsidian");
9659
+ const obsidianDir = path17.join(rootDir, ".obsidian");
8257
9660
  const projectIds = projectEntries(config).map((project) => project.id);
8258
9661
  await ensureDir(obsidianDir);
8259
9662
  await Promise.all([
8260
- writeJsonFile(path14.join(obsidianDir, "app.json"), {
9663
+ writeJsonFile(path17.join(obsidianDir, "app.json"), {
8261
9664
  alwaysUpdateLinks: true,
8262
9665
  newFileLocation: "folder",
8263
9666
  newFileFolderPath: "wiki/insights",
8264
9667
  useMarkdownLinks: false,
8265
9668
  attachmentFolderPath: "raw/assets"
8266
9669
  }),
8267
- writeJsonFile(path14.join(obsidianDir, "core-plugins.json"), [
9670
+ writeJsonFile(path17.join(obsidianDir, "core-plugins.json"), [
8268
9671
  "file-explorer",
8269
9672
  "global-search",
8270
9673
  "switcher",
@@ -8274,7 +9677,7 @@ async function ensureObsidianWorkspace(rootDir) {
8274
9677
  "tag-pane",
8275
9678
  "page-preview"
8276
9679
  ]),
8277
- writeJsonFile(path14.join(obsidianDir, "graph.json"), {
9680
+ writeJsonFile(path17.join(obsidianDir, "graph.json"), {
8278
9681
  "collapse-filter": false,
8279
9682
  search: "",
8280
9683
  showTags: true,
@@ -8286,7 +9689,7 @@ async function ensureObsidianWorkspace(rootDir) {
8286
9689
  })),
8287
9690
  localJumps: false
8288
9691
  }),
8289
- writeJsonFile(path14.join(obsidianDir, "workspace.json"), {
9692
+ writeJsonFile(path17.join(obsidianDir, "workspace.json"), {
8290
9693
  active: "root",
8291
9694
  lastOpenFiles: ["wiki/index.md", "wiki/projects/index.md", "wiki/candidates/index.md", "wiki/insights/index.md"],
8292
9695
  left: {
@@ -8301,11 +9704,11 @@ async function ensureObsidianWorkspace(rootDir) {
8301
9704
  async function initVault(rootDir, options = {}) {
8302
9705
  const { paths } = await initWorkspace(rootDir);
8303
9706
  await installConfiguredAgents(rootDir);
8304
- const insightsIndexPath = path14.join(paths.wikiDir, "insights", "index.md");
9707
+ const insightsIndexPath = path17.join(paths.wikiDir, "insights", "index.md");
8305
9708
  const now = (/* @__PURE__ */ new Date()).toISOString();
8306
9709
  await writeFileIfChanged(
8307
9710
  insightsIndexPath,
8308
- matter7.stringify(
9711
+ matter8.stringify(
8309
9712
  [
8310
9713
  "# Insights",
8311
9714
  "",
@@ -8337,8 +9740,8 @@ async function initVault(rootDir, options = {}) {
8337
9740
  )
8338
9741
  );
8339
9742
  await writeFileIfChanged(
8340
- path14.join(paths.wikiDir, "projects", "index.md"),
8341
- matter7.stringify(["# Projects", "", "- Run `swarmvault compile` to build project rollups.", ""].join("\n"), {
9743
+ path17.join(paths.wikiDir, "projects", "index.md"),
9744
+ matter8.stringify(["# Projects", "", "- Run `swarmvault compile` to build project rollups.", ""].join("\n"), {
8342
9745
  page_id: "projects:index",
8343
9746
  kind: "index",
8344
9747
  title: "Projects",
@@ -8359,8 +9762,8 @@ async function initVault(rootDir, options = {}) {
8359
9762
  })
8360
9763
  );
8361
9764
  await writeFileIfChanged(
8362
- path14.join(paths.wikiDir, "candidates", "index.md"),
8363
- matter7.stringify(["# Candidates", "", "- Run `swarmvault compile` to stage candidate pages.", ""].join("\n"), {
9765
+ path17.join(paths.wikiDir, "candidates", "index.md"),
9766
+ matter8.stringify(["# Candidates", "", "- Run `swarmvault compile` to stage candidate pages.", ""].join("\n"), {
8364
9767
  page_id: "candidates:index",
8365
9768
  kind: "index",
8366
9769
  title: "Candidates",
@@ -8476,7 +9879,7 @@ async function compileVault(rootDir, options = {}) {
8476
9879
  ),
8477
9880
  Promise.all(
8478
9881
  clean.map(async (manifest) => {
8479
- const cached = await readJsonFile(path14.join(paths.analysesDir, `${manifest.sourceId}.json`));
9882
+ const cached = await readJsonFile(path17.join(paths.analysesDir, `${manifest.sourceId}.json`));
8480
9883
  if (cached) {
8481
9884
  return cached;
8482
9885
  }
@@ -8500,22 +9903,22 @@ async function compileVault(rootDir, options = {}) {
8500
9903
  }
8501
9904
  const enriched = enrichResolvedCodeImports(manifest, analysis, codeIndex);
8502
9905
  if (analysisSignature(enriched) !== analysisSignature(analysis)) {
8503
- await writeJsonFile(path14.join(paths.analysesDir, `${analysis.sourceId}.json`), enriched);
9906
+ await writeJsonFile(path17.join(paths.analysesDir, `${analysis.sourceId}.json`), enriched);
8504
9907
  }
8505
9908
  return enriched;
8506
9909
  })
8507
9910
  );
8508
9911
  await Promise.all([
8509
- ensureDir(path14.join(paths.wikiDir, "sources")),
8510
- ensureDir(path14.join(paths.wikiDir, "code")),
8511
- ensureDir(path14.join(paths.wikiDir, "concepts")),
8512
- ensureDir(path14.join(paths.wikiDir, "entities")),
8513
- ensureDir(path14.join(paths.wikiDir, "outputs")),
8514
- ensureDir(path14.join(paths.wikiDir, "projects")),
8515
- ensureDir(path14.join(paths.wikiDir, "insights")),
8516
- ensureDir(path14.join(paths.wikiDir, "candidates")),
8517
- ensureDir(path14.join(paths.wikiDir, "candidates", "concepts")),
8518
- ensureDir(path14.join(paths.wikiDir, "candidates", "entities"))
9912
+ ensureDir(path17.join(paths.wikiDir, "sources")),
9913
+ ensureDir(path17.join(paths.wikiDir, "code")),
9914
+ ensureDir(path17.join(paths.wikiDir, "concepts")),
9915
+ ensureDir(path17.join(paths.wikiDir, "entities")),
9916
+ ensureDir(path17.join(paths.wikiDir, "outputs")),
9917
+ ensureDir(path17.join(paths.wikiDir, "projects")),
9918
+ ensureDir(path17.join(paths.wikiDir, "insights")),
9919
+ ensureDir(path17.join(paths.wikiDir, "candidates")),
9920
+ ensureDir(path17.join(paths.wikiDir, "candidates", "concepts")),
9921
+ ensureDir(path17.join(paths.wikiDir, "candidates", "entities"))
8519
9922
  ]);
8520
9923
  const sync = await syncVaultArtifacts(rootDir, {
8521
9924
  schemas,
@@ -8657,7 +10060,7 @@ async function queryVault(rootDir, options) {
8657
10060
  assetFiles: staged.assetFiles
8658
10061
  }
8659
10062
  ]);
8660
- stagedPath = path14.join(approval.approvalDir, "wiki", staged.page.path);
10063
+ stagedPath = path17.join(approval.approvalDir, "wiki", staged.page.path);
8661
10064
  savedPageId = staged.page.id;
8662
10065
  approvalId = approval.approvalId;
8663
10066
  approvalDir = approval.approvalDir;
@@ -8913,9 +10316,9 @@ ${orchestrationNotes.join("\n")}
8913
10316
  approvalId = approval.approvalId;
8914
10317
  approvalDir = approval.approvalDir;
8915
10318
  stepResults.forEach((result, index) => {
8916
- result.stagedPath = path14.join(approval.approvalDir, "wiki", stagedStepPages[index]?.page.path ?? "");
10319
+ result.stagedPath = path17.join(approval.approvalDir, "wiki", stagedStepPages[index]?.page.path ?? "");
8917
10320
  });
8918
- stagedHubPath = path14.join(approval.approvalDir, "wiki", hubPage.path);
10321
+ stagedHubPath = path17.join(approval.approvalDir, "wiki", hubPage.path);
8919
10322
  } else {
8920
10323
  await refreshVaultAfterOutputSave(rootDir);
8921
10324
  }
@@ -8982,6 +10385,50 @@ async function queryGraphVault(rootDir, question, options = {}) {
8982
10385
  const searchResults = searchPages(paths.searchDbPath, question, { limit: Math.max(5, options.budget ?? 10) });
8983
10386
  return queryGraph(graph, question, searchResults, options);
8984
10387
  }
10388
+ async function benchmarkVault(rootDir, options = {}) {
10389
+ const { paths } = await loadVaultConfig(rootDir);
10390
+ const graph = await ensureCompiledGraph(rootDir);
10391
+ const manifests = await listManifests(rootDir);
10392
+ const pageContentsById = /* @__PURE__ */ new Map();
10393
+ let corpusWords = 0;
10394
+ for (const manifest of manifests) {
10395
+ const extractedText = await readExtractedText(rootDir, manifest);
10396
+ if (extractedText) {
10397
+ corpusWords += estimateCorpusWords([extractedText]);
10398
+ }
10399
+ }
10400
+ for (const page of graph.pages) {
10401
+ const absolutePath = path17.join(paths.wikiDir, page.path);
10402
+ if (!await fileExists(absolutePath)) {
10403
+ continue;
10404
+ }
10405
+ const parsed = matter8(await fs14.readFile(absolutePath, "utf8"));
10406
+ pageContentsById.set(page.id, parsed.content);
10407
+ }
10408
+ const questions = (options.questions ?? []).map((question) => normalizeWhitespace(question)).filter(Boolean);
10409
+ const sampleQuestions = questions.length ? questions : [...DEFAULT_BENCHMARK_QUESTIONS];
10410
+ const perQuestion = sampleQuestions.map((question) => {
10411
+ const searchResults = searchPages(paths.searchDbPath, question, { limit: 12 });
10412
+ const result = queryGraph(graph, question, searchResults, { budget: 12 });
10413
+ const metrics = benchmarkQueryTokens(graph, result, pageContentsById);
10414
+ return {
10415
+ question,
10416
+ queryTokens: metrics.queryTokens,
10417
+ reduction: metrics.reduction,
10418
+ visitedNodeIds: result.visitedNodeIds,
10419
+ pageIds: result.pageIds
10420
+ };
10421
+ });
10422
+ const artifact = buildBenchmarkArtifact({
10423
+ graph,
10424
+ corpusWords,
10425
+ questions: sampleQuestions,
10426
+ perQuestion
10427
+ });
10428
+ await writeJsonFile(paths.benchmarkPath, artifact);
10429
+ await refreshIndexesAndSearch(rootDir, graph.pages);
10430
+ return artifact;
10431
+ }
8985
10432
  async function pathGraphVault(rootDir, from, to) {
8986
10433
  const graph = await ensureCompiledGraph(rootDir);
8987
10434
  return shortestGraphPath(graph, from, to);
@@ -9001,15 +10448,15 @@ async function listPages(rootDir) {
9001
10448
  }
9002
10449
  async function readPage(rootDir, relativePath) {
9003
10450
  const { paths } = await loadVaultConfig(rootDir);
9004
- const absolutePath = path14.resolve(paths.wikiDir, relativePath);
10451
+ const absolutePath = path17.resolve(paths.wikiDir, relativePath);
9005
10452
  if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
9006
10453
  return null;
9007
10454
  }
9008
- const raw = await fs11.readFile(absolutePath, "utf8");
9009
- const parsed = matter7(raw);
10455
+ const raw = await fs14.readFile(absolutePath, "utf8");
10456
+ const parsed = matter8(raw);
9010
10457
  return {
9011
10458
  path: relativePath,
9012
- title: typeof parsed.data.title === "string" ? parsed.data.title : path14.basename(relativePath, path14.extname(relativePath)),
10459
+ title: typeof parsed.data.title === "string" ? parsed.data.title : path17.basename(relativePath, path17.extname(relativePath)),
9013
10460
  frontmatter: parsed.data,
9014
10461
  content: parsed.content
9015
10462
  };
@@ -9033,19 +10480,19 @@ async function getWorkspaceInfo(rootDir) {
9033
10480
  }
9034
10481
  function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sourceProjects) {
9035
10482
  const manifestMap = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
9036
- const pageMap = new Map(graph.pages.map((page) => [page.id, page]));
10483
+ const pageMap2 = new Map(graph.pages.map((page) => [page.id, page]));
9037
10484
  return Promise.all(
9038
10485
  graph.pages.map(async (page) => {
9039
10486
  const findings = [];
9040
10487
  if (page.kind === "insight") {
9041
10488
  return findings;
9042
10489
  }
9043
- if (page.schemaHash !== expectedSchemaHashForPage(page, schemas, pageMap, sourceProjects)) {
10490
+ if (page.schemaHash !== expectedSchemaHashForPage(page, schemas, pageMap2, sourceProjects)) {
9044
10491
  findings.push({
9045
10492
  severity: "warning",
9046
10493
  code: "stale_page",
9047
10494
  message: `Page ${page.title} is stale because the vault schema changed.`,
9048
- pagePath: path14.join(paths.wikiDir, page.path),
10495
+ pagePath: path17.join(paths.wikiDir, page.path),
9049
10496
  relatedPageIds: [page.id]
9050
10497
  });
9051
10498
  }
@@ -9056,7 +10503,7 @@ function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sour
9056
10503
  severity: "warning",
9057
10504
  code: "stale_page",
9058
10505
  message: `Page ${page.title} is stale because source ${sourceId} changed.`,
9059
- pagePath: path14.join(paths.wikiDir, page.path),
10506
+ pagePath: path17.join(paths.wikiDir, page.path),
9060
10507
  relatedSourceIds: [sourceId],
9061
10508
  relatedPageIds: [page.id]
9062
10509
  });
@@ -9067,13 +10514,13 @@ function structuralLintFindings(_rootDir, paths, graph, schemas, manifests, sour
9067
10514
  severity: "info",
9068
10515
  code: "orphan_page",
9069
10516
  message: `Page ${page.title} has no backlinks.`,
9070
- pagePath: path14.join(paths.wikiDir, page.path),
10517
+ pagePath: path17.join(paths.wikiDir, page.path),
9071
10518
  relatedPageIds: [page.id]
9072
10519
  });
9073
10520
  }
9074
- const absolutePath = path14.join(paths.wikiDir, page.path);
10521
+ const absolutePath = path17.join(paths.wikiDir, page.path);
9075
10522
  if (await fileExists(absolutePath)) {
9076
- const content = await fs11.readFile(absolutePath, "utf8");
10523
+ const content = await fs14.readFile(absolutePath, "utf8");
9077
10524
  if (content.includes("## Claims")) {
9078
10525
  const uncited = content.split("\n").filter((line) => line.startsWith("- ") && !line.includes("[source:"));
9079
10526
  if (uncited.length) {
@@ -9153,7 +10600,7 @@ async function bootstrapDemo(rootDir, input) {
9153
10600
  }
9154
10601
 
9155
10602
  // src/mcp.ts
9156
- var SERVER_VERSION = "0.1.19";
10603
+ var SERVER_VERSION = "0.1.21";
9157
10604
  async function createMcpServer(rootDir) {
9158
10605
  const server = new McpServer({
9159
10606
  name: "swarmvault",
@@ -9402,7 +10849,7 @@ async function createMcpServer(rootDir) {
9402
10849
  },
9403
10850
  async () => {
9404
10851
  const { paths } = await loadVaultConfig(rootDir);
9405
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path15.relative(paths.sessionsDir, filePath))).sort();
10852
+ const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path18.relative(paths.sessionsDir, filePath))).sort();
9406
10853
  return asTextResource("swarmvault://sessions", JSON.stringify(files, null, 2));
9407
10854
  }
9408
10855
  );
@@ -9435,8 +10882,8 @@ async function createMcpServer(rootDir) {
9435
10882
  return asTextResource(`swarmvault://pages/${encodedPath}`, `Page not found: ${relativePath}`);
9436
10883
  }
9437
10884
  const { paths } = await loadVaultConfig(rootDir);
9438
- const absolutePath = path15.resolve(paths.wikiDir, relativePath);
9439
- return asTextResource(`swarmvault://pages/${encodedPath}`, await fs12.readFile(absolutePath, "utf8"));
10885
+ const absolutePath = path18.resolve(paths.wikiDir, relativePath);
10886
+ return asTextResource(`swarmvault://pages/${encodedPath}`, await fs15.readFile(absolutePath, "utf8"));
9440
10887
  }
9441
10888
  );
9442
10889
  server.registerResource(
@@ -9444,11 +10891,11 @@ async function createMcpServer(rootDir) {
9444
10891
  new ResourceTemplate("swarmvault://sessions/{path}", {
9445
10892
  list: async () => {
9446
10893
  const { paths } = await loadVaultConfig(rootDir);
9447
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path15.relative(paths.sessionsDir, filePath))).sort();
10894
+ const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path18.relative(paths.sessionsDir, filePath))).sort();
9448
10895
  return {
9449
10896
  resources: files.map((relativePath) => ({
9450
10897
  uri: `swarmvault://sessions/${encodeURIComponent(relativePath)}`,
9451
- name: path15.basename(relativePath, ".md"),
10898
+ name: path18.basename(relativePath, ".md"),
9452
10899
  title: relativePath,
9453
10900
  description: "SwarmVault session artifact",
9454
10901
  mimeType: "text/markdown"
@@ -9465,11 +10912,11 @@ async function createMcpServer(rootDir) {
9465
10912
  const { paths } = await loadVaultConfig(rootDir);
9466
10913
  const encodedPath = typeof variables.path === "string" ? variables.path : "";
9467
10914
  const relativePath = decodeURIComponent(encodedPath);
9468
- const absolutePath = path15.resolve(paths.sessionsDir, relativePath);
10915
+ const absolutePath = path18.resolve(paths.sessionsDir, relativePath);
9469
10916
  if (!absolutePath.startsWith(paths.sessionsDir) || !await fileExists(absolutePath)) {
9470
10917
  return asTextResource(`swarmvault://sessions/${encodedPath}`, `Session not found: ${relativePath}`);
9471
10918
  }
9472
- return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs12.readFile(absolutePath, "utf8"));
10919
+ return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs15.readFile(absolutePath, "utf8"));
9473
10920
  }
9474
10921
  );
9475
10922
  return server;
@@ -9517,13 +10964,13 @@ function asTextResource(uri, text) {
9517
10964
  }
9518
10965
 
9519
10966
  // src/schedule.ts
9520
- import fs13 from "fs/promises";
9521
- import path16 from "path";
10967
+ import fs16 from "fs/promises";
10968
+ import path19 from "path";
9522
10969
  function scheduleStatePath(schedulesDir, jobId) {
9523
- return path16.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
10970
+ return path19.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
9524
10971
  }
9525
10972
  function scheduleLockPath(schedulesDir, jobId) {
9526
- return path16.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
10973
+ return path19.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
9527
10974
  }
9528
10975
  function parseEveryDuration(value) {
9529
10976
  const match = value.trim().match(/^(\d+)(m|h|d)$/i);
@@ -9626,13 +11073,13 @@ async function acquireJobLease(rootDir, jobId) {
9626
11073
  const { paths } = await loadVaultConfig(rootDir);
9627
11074
  const leasePath = scheduleLockPath(paths.schedulesDir, jobId);
9628
11075
  await ensureDir(paths.schedulesDir);
9629
- const handle = await fs13.open(leasePath, "wx");
11076
+ const handle = await fs16.open(leasePath, "wx");
9630
11077
  await handle.writeFile(`${process.pid}
9631
11078
  ${(/* @__PURE__ */ new Date()).toISOString()}
9632
11079
  `);
9633
11080
  await handle.close();
9634
11081
  return async () => {
9635
- await fs13.rm(leasePath, { force: true });
11082
+ await fs16.rm(leasePath, { force: true });
9636
11083
  };
9637
11084
  }
9638
11085
  async function listSchedules(rootDir) {
@@ -9780,24 +11227,415 @@ async function serveSchedules(rootDir, pollMs = 3e4) {
9780
11227
 
9781
11228
  // src/viewer.ts
9782
11229
  import { execFile } from "child_process";
9783
- import fs14 from "fs/promises";
11230
+ import fs17 from "fs/promises";
9784
11231
  import http from "http";
9785
- import path17 from "path";
11232
+ import path21 from "path";
9786
11233
  import { promisify } from "util";
9787
- import matter8 from "gray-matter";
11234
+ import matter9 from "gray-matter";
9788
11235
  import mime2 from "mime-types";
11236
+
11237
+ // src/watch.ts
11238
+ import path20 from "path";
11239
+ import process2 from "process";
11240
+ import chokidar from "chokidar";
11241
+ var MAX_BACKOFF_MS = 3e4;
11242
+ var BACKOFF_THRESHOLD = 3;
11243
+ var CRITICAL_THRESHOLD = 10;
11244
+ var REPO_WATCH_IGNORES = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", "coverage", ".venv", "vendor", "target"]);
11245
+ function withinRoot2(rootPath, targetPath) {
11246
+ const relative = path20.relative(rootPath, targetPath);
11247
+ return relative === "" || !relative.startsWith("..") && !path20.isAbsolute(relative);
11248
+ }
11249
+ function hasIgnoredRepoSegment(baseDir, targetPath) {
11250
+ const relativePath = path20.relative(baseDir, targetPath);
11251
+ if (!relativePath || relativePath.startsWith("..")) {
11252
+ return false;
11253
+ }
11254
+ return relativePath.split(path20.sep).some((segment) => REPO_WATCH_IGNORES.has(segment));
11255
+ }
11256
+ function workspaceIgnoreRoots(rootDir, paths) {
11257
+ return [
11258
+ paths.rawDir,
11259
+ paths.wikiDir,
11260
+ paths.stateDir,
11261
+ paths.agentDir,
11262
+ paths.inboxDir,
11263
+ path20.join(rootDir, ".claude"),
11264
+ path20.join(rootDir, ".cursor"),
11265
+ path20.join(rootDir, ".obsidian")
11266
+ ].map((candidate) => path20.resolve(candidate));
11267
+ }
11268
+ async function resolveWatchTargets(rootDir, paths, options) {
11269
+ const targets = /* @__PURE__ */ new Set([path20.resolve(paths.inboxDir)]);
11270
+ if (options.repo) {
11271
+ for (const repoRoot of await listTrackedRepoRoots(rootDir)) {
11272
+ targets.add(path20.resolve(repoRoot));
11273
+ }
11274
+ }
11275
+ return [...targets].sort((left, right) => left.localeCompare(right));
11276
+ }
11277
+ async function performWatchCycle(rootDir, paths, options) {
11278
+ const imported = await importInbox(rootDir, paths.inboxDir);
11279
+ const repoSync = options.repo ? await syncTrackedReposForWatch(rootDir) : null;
11280
+ const compile = await compileVault(rootDir);
11281
+ const pendingSemanticRefresh = repoSync ? await mergePendingSemanticRefresh(rootDir, repoSync.pendingSemanticRefresh) : await readPendingSemanticRefresh(rootDir);
11282
+ const stalePagePaths = await markPagesStaleForSources(
11283
+ rootDir,
11284
+ pendingSemanticRefresh.map((entry) => entry.sourceId).filter((sourceId) => Boolean(sourceId))
11285
+ );
11286
+ const lintFindingCount = options.lint ? (await lintVault(rootDir)).length : void 0;
11287
+ return {
11288
+ watchedRepoRoots: repoSync?.repoRoots ?? [],
11289
+ importedCount: imported.imported.length,
11290
+ scannedCount: imported.scannedCount,
11291
+ attachmentCount: imported.attachmentCount,
11292
+ repoImportedCount: repoSync?.imported.length ?? 0,
11293
+ repoUpdatedCount: repoSync?.updated.length ?? 0,
11294
+ repoRemovedCount: repoSync?.removed.length ?? 0,
11295
+ repoScannedCount: repoSync?.scannedCount ?? 0,
11296
+ pendingSemanticRefreshCount: pendingSemanticRefresh.length,
11297
+ pendingSemanticRefreshPaths: pendingSemanticRefresh.map((entry) => entry.path),
11298
+ changedPages: [.../* @__PURE__ */ new Set([...compile.changedPages, ...stalePagePaths])],
11299
+ lintFindingCount
11300
+ };
11301
+ }
11302
+ async function runWatchCycle(rootDir, options = {}) {
11303
+ const { paths } = await initWorkspace(rootDir);
11304
+ const startedAt = /* @__PURE__ */ new Date();
11305
+ let success = true;
11306
+ let error;
11307
+ let result = {
11308
+ watchedRepoRoots: [],
11309
+ importedCount: 0,
11310
+ scannedCount: 0,
11311
+ attachmentCount: 0,
11312
+ repoImportedCount: 0,
11313
+ repoUpdatedCount: 0,
11314
+ repoRemovedCount: 0,
11315
+ repoScannedCount: 0,
11316
+ pendingSemanticRefreshCount: 0,
11317
+ pendingSemanticRefreshPaths: [],
11318
+ changedPages: []
11319
+ };
11320
+ try {
11321
+ result = await performWatchCycle(rootDir, paths, options);
11322
+ return result;
11323
+ } catch (caught) {
11324
+ success = false;
11325
+ error = caught instanceof Error ? caught.message : String(caught);
11326
+ throw caught;
11327
+ } finally {
11328
+ const finishedAt = /* @__PURE__ */ new Date();
11329
+ await recordSession(rootDir, {
11330
+ operation: "watch",
11331
+ title: `Watch cycle for ${paths.inboxDir}${options.repo ? " and tracked repos" : ""}`,
11332
+ startedAt: startedAt.toISOString(),
11333
+ finishedAt: finishedAt.toISOString(),
11334
+ success,
11335
+ error,
11336
+ changedPages: result.changedPages,
11337
+ lintFindingCount: result.lintFindingCount,
11338
+ lines: [
11339
+ "reasons=once",
11340
+ `imported=${result.importedCount}`,
11341
+ `scanned=${result.scannedCount}`,
11342
+ `attachments=${result.attachmentCount}`,
11343
+ `repo_scanned=${result.repoScannedCount}`,
11344
+ `repo_imported=${result.repoImportedCount}`,
11345
+ `repo_updated=${result.repoUpdatedCount}`,
11346
+ `repo_removed=${result.repoRemovedCount}`,
11347
+ `pending_semantic_refresh=${result.pendingSemanticRefreshCount}`,
11348
+ `lint=${result.lintFindingCount ?? 0}`
11349
+ ]
11350
+ });
11351
+ await appendWatchRun(rootDir, {
11352
+ startedAt: startedAt.toISOString(),
11353
+ finishedAt: finishedAt.toISOString(),
11354
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
11355
+ inputDir: paths.inboxDir,
11356
+ reasons: ["once"],
11357
+ importedCount: result.importedCount + result.repoImportedCount + result.repoUpdatedCount,
11358
+ scannedCount: result.scannedCount + result.repoScannedCount,
11359
+ attachmentCount: result.attachmentCount,
11360
+ changedPages: result.changedPages,
11361
+ repoImportedCount: result.repoImportedCount,
11362
+ repoUpdatedCount: result.repoUpdatedCount,
11363
+ repoRemovedCount: result.repoRemovedCount,
11364
+ repoScannedCount: result.repoScannedCount,
11365
+ pendingSemanticRefreshCount: result.pendingSemanticRefreshCount,
11366
+ pendingSemanticRefreshPaths: result.pendingSemanticRefreshPaths,
11367
+ lintFindingCount: result.lintFindingCount,
11368
+ success,
11369
+ error
11370
+ });
11371
+ await writeWatchStatusArtifact(rootDir, {
11372
+ generatedAt: finishedAt.toISOString(),
11373
+ watchedRepoRoots: result.watchedRepoRoots,
11374
+ lastRun: {
11375
+ startedAt: startedAt.toISOString(),
11376
+ finishedAt: finishedAt.toISOString(),
11377
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
11378
+ inputDir: paths.inboxDir,
11379
+ reasons: ["once"],
11380
+ importedCount: result.importedCount + result.repoImportedCount + result.repoUpdatedCount,
11381
+ scannedCount: result.scannedCount + result.repoScannedCount,
11382
+ attachmentCount: result.attachmentCount,
11383
+ changedPages: result.changedPages,
11384
+ repoImportedCount: result.repoImportedCount,
11385
+ repoUpdatedCount: result.repoUpdatedCount,
11386
+ repoRemovedCount: result.repoRemovedCount,
11387
+ repoScannedCount: result.repoScannedCount,
11388
+ pendingSemanticRefreshCount: result.pendingSemanticRefreshCount,
11389
+ pendingSemanticRefreshPaths: result.pendingSemanticRefreshPaths,
11390
+ lintFindingCount: result.lintFindingCount,
11391
+ success,
11392
+ error
11393
+ },
11394
+ pendingSemanticRefresh: await readPendingSemanticRefresh(rootDir)
11395
+ });
11396
+ }
11397
+ }
11398
+ async function watchVault(rootDir, options = {}) {
11399
+ const { paths } = await initWorkspace(rootDir);
11400
+ const baseDebounceMs = options.debounceMs ?? 900;
11401
+ const ignoredRoots = workspaceIgnoreRoots(rootDir, paths);
11402
+ const inboxWatchRoot = path20.resolve(paths.inboxDir);
11403
+ let watchTargets = await resolveWatchTargets(rootDir, paths, options);
11404
+ let timer;
11405
+ let running = false;
11406
+ let pending = false;
11407
+ let closed = false;
11408
+ let consecutiveFailures = 0;
11409
+ let currentDebounceMs = baseDebounceMs;
11410
+ const reasons = /* @__PURE__ */ new Set();
11411
+ const watcher = chokidar.watch(watchTargets, {
11412
+ ignoreInitial: true,
11413
+ usePolling: true,
11414
+ interval: 100,
11415
+ ignored: (targetPath) => {
11416
+ const absolutePath = path20.resolve(targetPath);
11417
+ const primaryTarget = watchTargets.filter((watchTarget) => withinRoot2(watchTarget, absolutePath)).sort((left, right) => right.length - left.length)[0] ?? null;
11418
+ if (!primaryTarget) {
11419
+ return false;
11420
+ }
11421
+ if (primaryTarget !== inboxWatchRoot && ignoredRoots.some((ignoreRoot) => withinRoot2(ignoreRoot, absolutePath))) {
11422
+ return true;
11423
+ }
11424
+ return hasIgnoredRepoSegment(primaryTarget, absolutePath);
11425
+ },
11426
+ awaitWriteFinish: {
11427
+ stabilityThreshold: Math.max(250, Math.floor(baseDebounceMs / 2)),
11428
+ pollInterval: 100
11429
+ }
11430
+ });
11431
+ const syncWatchTargets = async () => {
11432
+ const nextTargets = await resolveWatchTargets(rootDir, paths, options);
11433
+ const nextSet = new Set(nextTargets);
11434
+ const currentSet = new Set(watchTargets);
11435
+ const toRemove = watchTargets.filter((target) => !nextSet.has(target));
11436
+ const toAdd = nextTargets.filter((target) => !currentSet.has(target));
11437
+ if (toRemove.length > 0) {
11438
+ await watcher.unwatch(toRemove);
11439
+ }
11440
+ if (toAdd.length > 0) {
11441
+ await watcher.add(toAdd);
11442
+ }
11443
+ watchTargets = nextTargets;
11444
+ };
11445
+ const schedule = (reason) => {
11446
+ if (closed) {
11447
+ return;
11448
+ }
11449
+ reasons.add(reason);
11450
+ pending = true;
11451
+ if (timer) {
11452
+ clearTimeout(timer);
11453
+ }
11454
+ timer = setTimeout(() => {
11455
+ void runCycle();
11456
+ }, currentDebounceMs);
11457
+ };
11458
+ const runCycle = async () => {
11459
+ if (running || closed || !pending) {
11460
+ return;
11461
+ }
11462
+ pending = false;
11463
+ running = true;
11464
+ const startedAt = /* @__PURE__ */ new Date();
11465
+ const runReasons = [...reasons];
11466
+ reasons.clear();
11467
+ let importedCount = 0;
11468
+ let scannedCount = 0;
11469
+ let attachmentCount = 0;
11470
+ let repoImportedCount = 0;
11471
+ let repoUpdatedCount = 0;
11472
+ let repoRemovedCount = 0;
11473
+ let repoScannedCount = 0;
11474
+ let watchedRepoRoots = [];
11475
+ let pendingSemanticRefreshCount = 0;
11476
+ let pendingSemanticRefreshPaths = [];
11477
+ let changedPages = [];
11478
+ let lintFindingCount;
11479
+ let success = true;
11480
+ let error;
11481
+ try {
11482
+ const result = await performWatchCycle(rootDir, paths, options);
11483
+ importedCount = result.importedCount;
11484
+ scannedCount = result.scannedCount;
11485
+ attachmentCount = result.attachmentCount;
11486
+ repoImportedCount = result.repoImportedCount;
11487
+ repoUpdatedCount = result.repoUpdatedCount;
11488
+ repoRemovedCount = result.repoRemovedCount;
11489
+ repoScannedCount = result.repoScannedCount;
11490
+ watchedRepoRoots = result.watchedRepoRoots;
11491
+ pendingSemanticRefreshCount = result.pendingSemanticRefreshCount;
11492
+ pendingSemanticRefreshPaths = result.pendingSemanticRefreshPaths;
11493
+ changedPages = result.changedPages;
11494
+ lintFindingCount = result.lintFindingCount;
11495
+ consecutiveFailures = 0;
11496
+ currentDebounceMs = baseDebounceMs;
11497
+ await syncWatchTargets();
11498
+ } catch (caught) {
11499
+ success = false;
11500
+ error = caught instanceof Error ? caught.message : String(caught);
11501
+ consecutiveFailures++;
11502
+ pending = true;
11503
+ if (consecutiveFailures >= CRITICAL_THRESHOLD) {
11504
+ process2.stderr.write(
11505
+ `[swarmvault watch] ${consecutiveFailures} consecutive failures. Check vault state. Continuing at max backoff.
11506
+ `
11507
+ );
11508
+ }
11509
+ if (consecutiveFailures >= BACKOFF_THRESHOLD) {
11510
+ const multiplier = 2 ** (consecutiveFailures - BACKOFF_THRESHOLD);
11511
+ currentDebounceMs = Math.min(baseDebounceMs * multiplier, MAX_BACKOFF_MS);
11512
+ }
11513
+ } finally {
11514
+ const finishedAt = /* @__PURE__ */ new Date();
11515
+ await recordSession(rootDir, {
11516
+ operation: "watch",
11517
+ title: `Watch cycle for ${paths.inboxDir}${options.repo ? " and tracked repos" : ""}`,
11518
+ startedAt: startedAt.toISOString(),
11519
+ finishedAt: finishedAt.toISOString(),
11520
+ success,
11521
+ error,
11522
+ changedPages,
11523
+ lintFindingCount,
11524
+ lines: [
11525
+ `reasons=${runReasons.join(",") || "none"}`,
11526
+ `imported=${importedCount}`,
11527
+ `scanned=${scannedCount}`,
11528
+ `attachments=${attachmentCount}`,
11529
+ `repo_scanned=${repoScannedCount}`,
11530
+ `repo_imported=${repoImportedCount}`,
11531
+ `repo_updated=${repoUpdatedCount}`,
11532
+ `repo_removed=${repoRemovedCount}`,
11533
+ `lint=${lintFindingCount ?? 0}`
11534
+ ]
11535
+ });
11536
+ await appendWatchRun(rootDir, {
11537
+ startedAt: startedAt.toISOString(),
11538
+ finishedAt: finishedAt.toISOString(),
11539
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
11540
+ inputDir: paths.inboxDir,
11541
+ reasons: runReasons,
11542
+ importedCount: importedCount + repoImportedCount + repoUpdatedCount,
11543
+ scannedCount: scannedCount + repoScannedCount,
11544
+ attachmentCount,
11545
+ changedPages,
11546
+ repoImportedCount,
11547
+ repoUpdatedCount,
11548
+ repoRemovedCount,
11549
+ repoScannedCount,
11550
+ pendingSemanticRefreshCount,
11551
+ pendingSemanticRefreshPaths,
11552
+ lintFindingCount,
11553
+ success,
11554
+ error
11555
+ });
11556
+ await writeWatchStatusArtifact(rootDir, {
11557
+ generatedAt: finishedAt.toISOString(),
11558
+ watchedRepoRoots,
11559
+ lastRun: {
11560
+ startedAt: startedAt.toISOString(),
11561
+ finishedAt: finishedAt.toISOString(),
11562
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
11563
+ inputDir: paths.inboxDir,
11564
+ reasons: runReasons,
11565
+ importedCount: importedCount + repoImportedCount + repoUpdatedCount,
11566
+ scannedCount: scannedCount + repoScannedCount,
11567
+ attachmentCount,
11568
+ changedPages,
11569
+ repoImportedCount,
11570
+ repoUpdatedCount,
11571
+ repoRemovedCount,
11572
+ repoScannedCount,
11573
+ pendingSemanticRefreshCount,
11574
+ pendingSemanticRefreshPaths,
11575
+ lintFindingCount,
11576
+ success,
11577
+ error
11578
+ },
11579
+ pendingSemanticRefresh: await readPendingSemanticRefresh(rootDir)
11580
+ });
11581
+ running = false;
11582
+ if (pending && !closed) {
11583
+ schedule("queued");
11584
+ }
11585
+ }
11586
+ };
11587
+ const reasonForPath = (targetPath) => {
11588
+ const baseDir = watchTargets.filter((watchTarget) => withinRoot2(watchTarget, path20.resolve(targetPath))).sort((left, right) => right.length - left.length)[0] ?? paths.inboxDir;
11589
+ return path20.relative(baseDir, targetPath) || ".";
11590
+ };
11591
+ watcher.on("add", (filePath) => schedule(`add:${reasonForPath(filePath)}`)).on("change", (filePath) => schedule(`change:${reasonForPath(filePath)}`)).on("unlink", (filePath) => schedule(`unlink:${reasonForPath(filePath)}`)).on("addDir", (dirPath) => schedule(`addDir:${reasonForPath(dirPath)}`)).on("unlinkDir", (dirPath) => schedule(`unlinkDir:${reasonForPath(dirPath)}`)).on("error", (caught) => schedule(`error:${caught instanceof Error ? caught.message : String(caught)}`));
11592
+ await new Promise((resolve, reject) => {
11593
+ const handleReady = () => {
11594
+ watcher.off("error", handleError);
11595
+ resolve();
11596
+ };
11597
+ const handleError = (caught) => {
11598
+ watcher.off("ready", handleReady);
11599
+ reject(caught);
11600
+ };
11601
+ watcher.once("ready", handleReady);
11602
+ watcher.once("error", handleError);
11603
+ });
11604
+ return {
11605
+ close: async () => {
11606
+ closed = true;
11607
+ if (timer) {
11608
+ clearTimeout(timer);
11609
+ }
11610
+ await watcher.close();
11611
+ }
11612
+ };
11613
+ }
11614
+ async function getWatchStatus(rootDir) {
11615
+ const persisted = await readWatchStatusArtifact(rootDir);
11616
+ const watchedRepoRoots = await listTrackedRepoRoots(rootDir);
11617
+ const pendingSemanticRefresh = await readPendingSemanticRefresh(rootDir);
11618
+ return {
11619
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
11620
+ watchedRepoRoots,
11621
+ lastRun: persisted?.lastRun,
11622
+ pendingSemanticRefresh
11623
+ };
11624
+ }
11625
+
11626
+ // src/viewer.ts
9789
11627
  var execFileAsync = promisify(execFile);
9790
11628
  async function readViewerPage(rootDir, relativePath) {
9791
11629
  const { paths } = await loadVaultConfig(rootDir);
9792
- const absolutePath = path17.resolve(paths.wikiDir, relativePath);
11630
+ const absolutePath = path21.resolve(paths.wikiDir, relativePath);
9793
11631
  if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
9794
11632
  return null;
9795
11633
  }
9796
- const raw = await fs14.readFile(absolutePath, "utf8");
9797
- const parsed = matter8(raw);
11634
+ const raw = await fs17.readFile(absolutePath, "utf8");
11635
+ const parsed = matter9(raw);
9798
11636
  return {
9799
11637
  path: relativePath,
9800
- title: typeof parsed.data.title === "string" ? parsed.data.title : path17.basename(relativePath, path17.extname(relativePath)),
11638
+ title: typeof parsed.data.title === "string" ? parsed.data.title : path21.basename(relativePath, path21.extname(relativePath)),
9801
11639
  frontmatter: parsed.data,
9802
11640
  content: parsed.content,
9803
11641
  assets: normalizeOutputAssets(parsed.data.output_assets)
@@ -9805,12 +11643,12 @@ async function readViewerPage(rootDir, relativePath) {
9805
11643
  }
9806
11644
  async function readViewerAsset(rootDir, relativePath) {
9807
11645
  const { paths } = await loadVaultConfig(rootDir);
9808
- const absolutePath = path17.resolve(paths.wikiDir, relativePath);
11646
+ const absolutePath = path21.resolve(paths.wikiDir, relativePath);
9809
11647
  if (!absolutePath.startsWith(paths.wikiDir) || !await fileExists(absolutePath)) {
9810
11648
  return null;
9811
11649
  }
9812
11650
  return {
9813
- buffer: await fs14.readFile(absolutePath),
11651
+ buffer: await fs17.readFile(absolutePath),
9814
11652
  mimeType: mime2.lookup(absolutePath) || "application/octet-stream"
9815
11653
  };
9816
11654
  }
@@ -9833,12 +11671,12 @@ async function readJsonBody(request) {
9833
11671
  return JSON.parse(raw);
9834
11672
  }
9835
11673
  async function ensureViewerDist(viewerDistDir) {
9836
- const indexPath = path17.join(viewerDistDir, "index.html");
11674
+ const indexPath = path21.join(viewerDistDir, "index.html");
9837
11675
  if (await fileExists(indexPath)) {
9838
11676
  return;
9839
11677
  }
9840
- const viewerProjectDir = path17.dirname(viewerDistDir);
9841
- if (await fileExists(path17.join(viewerProjectDir, "package.json"))) {
11678
+ const viewerProjectDir = path21.dirname(viewerDistDir);
11679
+ if (await fileExists(path21.join(viewerProjectDir, "package.json"))) {
9842
11680
  await execFileAsync("pnpm", ["build"], { cwd: viewerProjectDir });
9843
11681
  }
9844
11682
  }
@@ -9855,7 +11693,7 @@ async function startGraphServer(rootDir, port) {
9855
11693
  return;
9856
11694
  }
9857
11695
  response.writeHead(200, { "content-type": "application/json" });
9858
- response.end(await fs14.readFile(paths.graphPath, "utf8"));
11696
+ response.end(await fs17.readFile(paths.graphPath, "utf8"));
9859
11697
  return;
9860
11698
  }
9861
11699
  if (url.pathname === "/api/graph/query") {
@@ -9907,6 +11745,11 @@ async function startGraphServer(rootDir, port) {
9907
11745
  response.end(JSON.stringify(results));
9908
11746
  return;
9909
11747
  }
11748
+ if (url.pathname === "/api/watch-status") {
11749
+ response.writeHead(200, { "content-type": "application/json" });
11750
+ response.end(JSON.stringify(await getWatchStatus(rootDir)));
11751
+ return;
11752
+ }
9910
11753
  if (url.pathname === "/api/page") {
9911
11754
  const relativePath2 = url.searchParams.get("path") ?? "";
9912
11755
  const page = await readViewerPage(rootDir, relativePath2);
@@ -9982,8 +11825,8 @@ async function startGraphServer(rootDir, port) {
9982
11825
  return;
9983
11826
  }
9984
11827
  const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
9985
- const target = path17.join(paths.viewerDistDir, relativePath);
9986
- const fallback = path17.join(paths.viewerDistDir, "index.html");
11828
+ const target = path21.join(paths.viewerDistDir, relativePath);
11829
+ const fallback = path21.join(paths.viewerDistDir, "index.html");
9987
11830
  const filePath = await fileExists(target) ? target : fallback;
9988
11831
  if (!await fileExists(filePath)) {
9989
11832
  response.writeHead(503, { "content-type": "text/plain" });
@@ -9991,7 +11834,7 @@ async function startGraphServer(rootDir, port) {
9991
11834
  return;
9992
11835
  }
9993
11836
  response.writeHead(200, { "content-type": mime2.lookup(filePath) || "text/plain" });
9994
- response.end(await fs14.readFile(filePath));
11837
+ response.end(await fs17.readFile(filePath));
9995
11838
  });
9996
11839
  await new Promise((resolve) => {
9997
11840
  server.listen(effectivePort, resolve);
@@ -10018,7 +11861,7 @@ async function exportGraphHtml(rootDir, outputPath) {
10018
11861
  throw new Error("Graph artifact not found. Run `swarmvault compile` first.");
10019
11862
  }
10020
11863
  await ensureViewerDist(paths.viewerDistDir);
10021
- const indexPath = path17.join(paths.viewerDistDir, "index.html");
11864
+ const indexPath = path21.join(paths.viewerDistDir, "index.html");
10022
11865
  if (!await fileExists(indexPath)) {
10023
11866
  throw new Error("Viewer build not found. Run `pnpm build` first.");
10024
11867
  }
@@ -10042,16 +11885,16 @@ async function exportGraphHtml(rootDir, outputPath) {
10042
11885
  } : null;
10043
11886
  })
10044
11887
  );
10045
- const rawHtml = await fs14.readFile(indexPath, "utf8");
11888
+ const rawHtml = await fs17.readFile(indexPath, "utf8");
10046
11889
  const scriptMatch = rawHtml.match(/<script type="module" crossorigin src="([^"]+)"><\/script>/);
10047
11890
  const styleMatch = rawHtml.match(/<link rel="stylesheet" crossorigin href="([^"]+)">/);
10048
- const scriptPath = scriptMatch?.[1] ? path17.join(paths.viewerDistDir, scriptMatch[1].replace(/^\//, "")) : null;
10049
- const stylePath = styleMatch?.[1] ? path17.join(paths.viewerDistDir, styleMatch[1].replace(/^\//, "")) : null;
11891
+ const scriptPath = scriptMatch?.[1] ? path21.join(paths.viewerDistDir, scriptMatch[1].replace(/^\//, "")) : null;
11892
+ const stylePath = styleMatch?.[1] ? path21.join(paths.viewerDistDir, styleMatch[1].replace(/^\//, "")) : null;
10050
11893
  if (!scriptPath || !await fileExists(scriptPath)) {
10051
11894
  throw new Error("Viewer script bundle not found. Run `pnpm build` first.");
10052
11895
  }
10053
- const script = await fs14.readFile(scriptPath, "utf8");
10054
- const style = stylePath && await fileExists(stylePath) ? await fs14.readFile(stylePath, "utf8") : "";
11896
+ const script = await fs17.readFile(scriptPath, "utf8");
11897
+ const style = stylePath && await fileExists(stylePath) ? await fs17.readFile(stylePath, "utf8") : "";
10055
11898
  const embeddedData = JSON.stringify({ graph, pages: pages.filter(Boolean) }, null, 2).replace(/</g, "\\u003c");
10056
11899
  const html = [
10057
11900
  "<!doctype html>",
@@ -10070,163 +11913,16 @@ async function exportGraphHtml(rootDir, outputPath) {
10070
11913
  "</html>",
10071
11914
  ""
10072
11915
  ].filter(Boolean).join("\n");
10073
- await fs14.mkdir(path17.dirname(outputPath), { recursive: true });
10074
- await fs14.writeFile(outputPath, html, "utf8");
10075
- return path17.resolve(outputPath);
10076
- }
10077
-
10078
- // src/watch.ts
10079
- import path18 from "path";
10080
- import process2 from "process";
10081
- import chokidar from "chokidar";
10082
- var MAX_BACKOFF_MS = 3e4;
10083
- var BACKOFF_THRESHOLD = 3;
10084
- var CRITICAL_THRESHOLD = 10;
10085
- async function watchVault(rootDir, options = {}) {
10086
- const { paths } = await initWorkspace(rootDir);
10087
- const baseDebounceMs = options.debounceMs ?? 900;
10088
- let timer;
10089
- let running = false;
10090
- let pending = false;
10091
- let closed = false;
10092
- let consecutiveFailures = 0;
10093
- let currentDebounceMs = baseDebounceMs;
10094
- const reasons = /* @__PURE__ */ new Set();
10095
- const watcher = chokidar.watch(paths.inboxDir, {
10096
- ignoreInitial: true,
10097
- usePolling: true,
10098
- interval: 100,
10099
- awaitWriteFinish: {
10100
- stabilityThreshold: Math.max(250, Math.floor(baseDebounceMs / 2)),
10101
- pollInterval: 100
10102
- }
10103
- });
10104
- const schedule = (reason) => {
10105
- if (closed) {
10106
- return;
10107
- }
10108
- reasons.add(reason);
10109
- pending = true;
10110
- if (timer) {
10111
- clearTimeout(timer);
10112
- }
10113
- timer = setTimeout(() => {
10114
- void runCycle();
10115
- }, currentDebounceMs);
10116
- };
10117
- const runCycle = async () => {
10118
- if (running || closed || !pending) {
10119
- return;
10120
- }
10121
- pending = false;
10122
- running = true;
10123
- const startedAt = /* @__PURE__ */ new Date();
10124
- const runReasons = [...reasons];
10125
- reasons.clear();
10126
- let importedCount = 0;
10127
- let scannedCount = 0;
10128
- let attachmentCount = 0;
10129
- let changedPages = [];
10130
- let lintFindingCount;
10131
- let success = true;
10132
- let error;
10133
- try {
10134
- const imported = await importInbox(rootDir, paths.inboxDir);
10135
- importedCount = imported.imported.length;
10136
- scannedCount = imported.scannedCount;
10137
- attachmentCount = imported.attachmentCount;
10138
- const compile = await compileVault(rootDir);
10139
- changedPages = compile.changedPages;
10140
- if (options.lint) {
10141
- const findings = await lintVault(rootDir);
10142
- lintFindingCount = findings.length;
10143
- }
10144
- consecutiveFailures = 0;
10145
- currentDebounceMs = baseDebounceMs;
10146
- } catch (caught) {
10147
- success = false;
10148
- error = caught instanceof Error ? caught.message : String(caught);
10149
- consecutiveFailures++;
10150
- pending = true;
10151
- if (consecutiveFailures >= CRITICAL_THRESHOLD) {
10152
- process2.stderr.write(
10153
- `[swarmvault watch] ${consecutiveFailures} consecutive failures. Check vault state. Continuing at max backoff.
10154
- `
10155
- );
10156
- }
10157
- if (consecutiveFailures >= BACKOFF_THRESHOLD) {
10158
- const multiplier = 2 ** (consecutiveFailures - BACKOFF_THRESHOLD);
10159
- currentDebounceMs = Math.min(baseDebounceMs * multiplier, MAX_BACKOFF_MS);
10160
- }
10161
- } finally {
10162
- const finishedAt = /* @__PURE__ */ new Date();
10163
- await recordSession(rootDir, {
10164
- operation: "watch",
10165
- title: `Watch cycle for ${paths.inboxDir}`,
10166
- startedAt: startedAt.toISOString(),
10167
- finishedAt: finishedAt.toISOString(),
10168
- success,
10169
- error,
10170
- changedPages,
10171
- lintFindingCount,
10172
- lines: [
10173
- `reasons=${runReasons.join(",") || "none"}`,
10174
- `imported=${importedCount}`,
10175
- `scanned=${scannedCount}`,
10176
- `attachments=${attachmentCount}`,
10177
- `lint=${lintFindingCount ?? 0}`
10178
- ]
10179
- });
10180
- await appendWatchRun(rootDir, {
10181
- startedAt: startedAt.toISOString(),
10182
- finishedAt: finishedAt.toISOString(),
10183
- durationMs: finishedAt.getTime() - startedAt.getTime(),
10184
- inputDir: paths.inboxDir,
10185
- reasons: runReasons,
10186
- importedCount,
10187
- scannedCount,
10188
- attachmentCount,
10189
- changedPages,
10190
- lintFindingCount,
10191
- success,
10192
- error
10193
- });
10194
- running = false;
10195
- if (pending && !closed) {
10196
- schedule("queued");
10197
- }
10198
- }
10199
- };
10200
- watcher.on("add", (filePath) => schedule(`add:${toWatchReason(paths.inboxDir, filePath)}`)).on("change", (filePath) => schedule(`change:${toWatchReason(paths.inboxDir, filePath)}`)).on("unlink", (filePath) => schedule(`unlink:${toWatchReason(paths.inboxDir, filePath)}`)).on("addDir", (dirPath) => schedule(`addDir:${toWatchReason(paths.inboxDir, dirPath)}`)).on("unlinkDir", (dirPath) => schedule(`unlinkDir:${toWatchReason(paths.inboxDir, dirPath)}`)).on("error", (caught) => schedule(`error:${caught instanceof Error ? caught.message : String(caught)}`));
10201
- await new Promise((resolve, reject) => {
10202
- const handleReady = () => {
10203
- watcher.off("error", handleError);
10204
- resolve();
10205
- };
10206
- const handleError = (caught) => {
10207
- watcher.off("ready", handleReady);
10208
- reject(caught);
10209
- };
10210
- watcher.once("ready", handleReady);
10211
- watcher.once("error", handleError);
10212
- });
10213
- return {
10214
- close: async () => {
10215
- closed = true;
10216
- if (timer) {
10217
- clearTimeout(timer);
10218
- }
10219
- await watcher.close();
10220
- }
10221
- };
10222
- }
10223
- function toWatchReason(baseDir, targetPath) {
10224
- return path18.relative(baseDir, targetPath) || ".";
11916
+ await fs17.mkdir(path21.dirname(outputPath), { recursive: true });
11917
+ await fs17.writeFile(outputPath, html, "utf8");
11918
+ return path21.resolve(outputPath);
10225
11919
  }
10226
11920
  export {
10227
11921
  acceptApproval,
11922
+ addInput,
10228
11923
  archiveCandidate,
10229
11924
  assertProviderCapability,
11925
+ benchmarkVault,
10230
11926
  bootstrapDemo,
10231
11927
  compileVault,
10232
11928
  createMcpServer,
@@ -10236,8 +11932,11 @@ export {
10236
11932
  defaultVaultSchema,
10237
11933
  explainGraphVault,
10238
11934
  exploreVault,
11935
+ exportGraphFormat,
10239
11936
  exportGraphHtml,
11937
+ getGitHookStatus,
10240
11938
  getProviderForTask,
11939
+ getWatchStatus,
10241
11940
  getWebSearchAdapterForTask,
10242
11941
  getWorkspaceInfo,
10243
11942
  importInbox,
@@ -10247,6 +11946,7 @@ export {
10247
11946
  initWorkspace,
10248
11947
  installAgent,
10249
11948
  installConfiguredAgents,
11949
+ installGitHooks,
10250
11950
  lintVault,
10251
11951
  listApprovals,
10252
11952
  listCandidates,
@@ -10254,6 +11954,7 @@ export {
10254
11954
  listManifests,
10255
11955
  listPages,
10256
11956
  listSchedules,
11957
+ listTrackedRepoRoots,
10257
11958
  loadVaultConfig,
10258
11959
  loadVaultSchema,
10259
11960
  loadVaultSchemas,
@@ -10267,9 +11968,13 @@ export {
10267
11968
  rejectApproval,
10268
11969
  resolvePaths,
10269
11970
  runSchedule,
11971
+ runWatchCycle,
10270
11972
  searchVault,
10271
11973
  serveSchedules,
10272
11974
  startGraphServer,
10273
11975
  startMcpServer,
11976
+ syncTrackedRepos,
11977
+ syncTrackedReposForWatch,
11978
+ uninstallGitHooks,
10274
11979
  watchVault
10275
11980
  };