@swarmvaultai/engine 0.1.27 → 0.1.29

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,19 +21,35 @@ import {
21
21
  uniqueBy,
22
22
  writeFileIfChanged,
23
23
  writeJsonFile
24
- } from "./chunk-LEUV6TWJ.js";
24
+ } from "./chunk-IHMJCCXR.js";
25
25
 
26
26
  // src/agents.ts
27
+ import crypto from "crypto";
27
28
  import fs from "fs/promises";
28
29
  import path from "path";
30
+ import YAML from "yaml";
29
31
  var managedStart = "<!-- swarmvault:managed:start -->";
30
32
  var managedEnd = "<!-- swarmvault:managed:end -->";
31
33
  var legacyManagedStart = "<!-- vault:managed:start -->";
32
34
  var legacyManagedEnd = "<!-- vault:managed:end -->";
35
+ var claudeSearchMatcher = "Glob|Grep";
36
+ var claudeSessionMatchers = ["startup", "resume", "clear", "compact"];
37
+ var geminiSessionMatcher = "startup";
38
+ var geminiSearchMatcher = "glob|grep|search|find";
39
+ var copilotHookVersion = 1;
40
+ var agentFileKinds = {
41
+ agents: "AGENTS.md",
42
+ claude: "CLAUDE.md",
43
+ gemini: "GEMINI.md",
44
+ cursor: ".cursor/rules/swarmvault.mdc",
45
+ aider: "CONVENTIONS.md",
46
+ copilot: ".github/copilot-instructions.md"
47
+ };
33
48
  function buildManagedBlock(target) {
34
- const body = [
49
+ const heading = target === "aider" ? "# SwarmVault Conventions" : target === "copilot" ? "# SwarmVault Repository Instructions" : "# SwarmVault Rules";
50
+ return [
35
51
  managedStart,
36
- "# SwarmVault Rules",
52
+ heading,
37
53
  "",
38
54
  "- Read `swarmvault.schema.md` before compile or query style work. It is the canonical schema path.",
39
55
  "- Treat `raw/` as immutable source input.",
@@ -46,55 +62,77 @@ function buildManagedBlock(target) {
46
62
  managedEnd,
47
63
  ""
48
64
  ].join("\n");
49
- if (target === "cursor") {
50
- return body;
51
- }
52
- return body;
53
65
  }
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;
66
+ function supportsAgentHook(agent) {
67
+ return agent === "claude" || agent === "opencode" || agent === "gemini" || agent === "copilot";
80
68
  }
81
- function targetPathForAgent(rootDir, agent) {
69
+ function primaryTargetPathForAgent(rootDir, agent) {
82
70
  switch (agent) {
83
71
  case "codex":
84
72
  case "goose":
85
73
  case "pi":
86
74
  case "opencode":
87
- return path.join(rootDir, "AGENTS.md");
75
+ return path.join(rootDir, agentFileKinds.agents);
88
76
  case "claude":
89
- return path.join(rootDir, "CLAUDE.md");
77
+ return path.join(rootDir, agentFileKinds.claude);
90
78
  case "gemini":
91
- return path.join(rootDir, "GEMINI.md");
79
+ return path.join(rootDir, agentFileKinds.gemini);
92
80
  case "cursor":
93
- return path.join(rootDir, ".cursor", "rules", "swarmvault.mdc");
81
+ return path.join(rootDir, agentFileKinds.cursor);
82
+ case "aider":
83
+ return path.join(rootDir, agentFileKinds.aider);
84
+ case "copilot":
85
+ return path.join(rootDir, agentFileKinds.copilot);
94
86
  default:
95
87
  throw new Error(`Unsupported agent ${String(agent)}`);
96
88
  }
97
89
  }
90
+ function hookScriptPathForAgent(rootDir, agent) {
91
+ switch (agent) {
92
+ case "claude":
93
+ return path.join(rootDir, ".claude", "hooks", "swarmvault-graph-first.js");
94
+ case "opencode":
95
+ return path.join(rootDir, ".opencode", "plugins", "swarmvault-graph-first.js");
96
+ case "gemini":
97
+ return path.join(rootDir, ".gemini", "hooks", "swarmvault-graph-first.js");
98
+ case "copilot":
99
+ return path.join(rootDir, ".github", "hooks", "swarmvault-graph-first.js");
100
+ default:
101
+ return null;
102
+ }
103
+ }
104
+ function hookConfigPathForAgent(rootDir, agent) {
105
+ switch (agent) {
106
+ case "claude":
107
+ return path.join(rootDir, ".claude", "settings.json");
108
+ case "gemini":
109
+ return path.join(rootDir, ".gemini", "settings.json");
110
+ case "copilot":
111
+ return path.join(rootDir, ".github", "hooks", "swarmvault-graph-first.json");
112
+ default:
113
+ return null;
114
+ }
115
+ }
116
+ function targetsForAgent(rootDir, agent, options = {}) {
117
+ const targets = [primaryTargetPathForAgent(rootDir, agent)];
118
+ if (agent === "copilot") {
119
+ targets.push(path.join(rootDir, agentFileKinds.agents));
120
+ }
121
+ if (agent === "aider") {
122
+ targets.push(path.join(rootDir, ".aider.conf.yml"));
123
+ }
124
+ if (options.hook && supportsAgentHook(agent)) {
125
+ const configPath = hookConfigPathForAgent(rootDir, agent);
126
+ const scriptPath = hookScriptPathForAgent(rootDir, agent);
127
+ if (configPath) {
128
+ targets.push(configPath);
129
+ }
130
+ if (scriptPath) {
131
+ targets.push(scriptPath);
132
+ }
133
+ }
134
+ return [...new Set(targets)];
135
+ }
98
136
  async function upsertManagedBlock(filePath, block) {
99
137
  const existing = await fileExists(filePath) ? await fs.readFile(filePath, "utf8") : "";
100
138
  if (!existing) {
@@ -115,51 +153,593 @@ async function upsertManagedBlock(filePath, block) {
115
153
  ${block}
116
154
  `, "utf8");
117
155
  }
156
+ async function writeOwnedFile(filePath, content, executable = false) {
157
+ await ensureDir(path.dirname(filePath));
158
+ await fs.writeFile(filePath, content, {
159
+ encoding: "utf8",
160
+ mode: executable ? 493 : 420
161
+ });
162
+ if (executable) {
163
+ await fs.chmod(filePath, 493);
164
+ }
165
+ }
166
+ async function readJsonWithWarnings(filePath, fallback, label) {
167
+ if (!await fileExists(filePath)) {
168
+ return { data: fallback, warnings: [] };
169
+ }
170
+ try {
171
+ const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
172
+ return { data: parsed, warnings: [] };
173
+ } catch {
174
+ return {
175
+ data: fallback,
176
+ warnings: [`Could not parse ${label}. Left the existing file unchanged.`]
177
+ };
178
+ }
179
+ }
180
+ async function installClaudeHook(rootDir) {
181
+ const settingsPath = path.join(rootDir, ".claude", "settings.json");
182
+ const scriptPath = path.join(rootDir, ".claude", "hooks", "swarmvault-graph-first.js");
183
+ await writeOwnedFile(scriptPath, buildClaudeHookScript(), true);
184
+ await ensureDir(path.dirname(settingsPath));
185
+ const { data: settings, warnings } = await readJsonWithWarnings(settingsPath, {}, ".claude/settings.json");
186
+ if (warnings.length > 0 && await fileExists(settingsPath)) {
187
+ return { path: settingsPath, warnings };
188
+ }
189
+ const hooks = settings.hooks ?? {};
190
+ const sessionStart = hooks.SessionStart ?? [];
191
+ const preToolUse = hooks.PreToolUse ?? [];
192
+ const sessionCommand = 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/swarmvault-graph-first.js" session-start';
193
+ const preToolUseCommand = 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/swarmvault-graph-first.js" pre-tool-use';
194
+ for (const matcher of claudeSessionMatchers) {
195
+ if (!sessionStart.some((entry) => entry.matcher === matcher && JSON.stringify(entry).includes("swarmvault-graph-first.js"))) {
196
+ sessionStart.push({
197
+ matcher,
198
+ hooks: [{ type: "command", command: sessionCommand }]
199
+ });
200
+ }
201
+ }
202
+ if (!preToolUse.some((entry) => entry.matcher === claudeSearchMatcher && JSON.stringify(entry).includes("swarmvault-graph-first.js"))) {
203
+ preToolUse.push({
204
+ matcher: claudeSearchMatcher,
205
+ hooks: [{ type: "command", command: preToolUseCommand }]
206
+ });
207
+ }
208
+ settings.hooks = { ...hooks, SessionStart: sessionStart, PreToolUse: preToolUse };
209
+ await fs.writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
210
+ `, "utf8");
211
+ return { path: settingsPath, warnings: [] };
212
+ }
213
+ function buildClaudeHookScript() {
214
+ return `#!/usr/bin/env node
215
+ import crypto from "node:crypto";
216
+ import fs from "node:fs/promises";
217
+ import os from "node:os";
218
+ import path from "node:path";
219
+
220
+ ${markerStateSnippet("claude").trim()}
221
+
222
+ async function readInput() {
223
+ let body = "";
224
+ for await (const chunk of process.stdin) {
225
+ body += chunk;
226
+ }
227
+ if (!body.trim()) {
228
+ return {};
229
+ }
230
+ try {
231
+ return JSON.parse(body);
232
+ } catch {
233
+ return {};
234
+ }
235
+ }
236
+
237
+ function emit(value) {
238
+ process.stdout.write(\`\${JSON.stringify(value)}\\n\`);
239
+ }
240
+
241
+ const mode = process.argv[2] ?? "";
242
+ const input = await readInput();
243
+ const cwd = resolveInputCwd(input);
244
+ const reportNote = "SwarmVault graph report exists at wiki/graph/report.md. Read it before broad grep/glob searching.";
245
+
246
+ if (!(await hasReport(cwd))) {
247
+ emit({});
248
+ process.exit(0);
249
+ }
250
+
251
+ if (mode === "session-start") {
252
+ await resetSession(cwd);
253
+ emit({
254
+ hookSpecificOutput: {
255
+ hookEventName: "SessionStart",
256
+ additionalContext: reportNote
257
+ }
258
+ });
259
+ process.exit(0);
260
+ }
261
+
262
+ const toolName = resolveToolName(input);
263
+ if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
264
+ await markReportRead(cwd);
265
+ emit({});
266
+ process.exit(0);
267
+ }
268
+
269
+ if (isBroadSearchTool(toolName) && !(await hasSeenReport(cwd))) {
270
+ emit({
271
+ hookSpecificOutput: {
272
+ hookEventName: "PreToolUse",
273
+ additionalContext: reportNote
274
+ }
275
+ });
276
+ process.exit(0);
277
+ }
278
+
279
+ emit({});
280
+ `;
281
+ }
282
+ function markerStateSnippet(agentKey) {
283
+ return `
284
+ function markerState(cwd) {
285
+ const hash = crypto.createHash("sha256").update(cwd).digest("hex");
286
+ const dir = path.join(os.tmpdir(), "swarmvault-agent-hooks", "${agentKey}", hash);
287
+ return {
288
+ dir,
289
+ markerPath: path.join(dir, "report-read")
290
+ };
291
+ }
292
+
293
+ function isReportPath(value, cwd) {
294
+ if (typeof value !== "string" || value.length === 0) {
295
+ return false;
296
+ }
297
+ const reportSuffix = path.join("wiki", "graph", "report.md");
298
+ const normalized = value.replaceAll("\\\\", "/");
299
+ const reportNormalized = reportSuffix.replaceAll("\\\\", "/");
300
+ if (normalized.endsWith(reportNormalized)) {
301
+ return true;
302
+ }
303
+ return path.resolve(cwd, value) === path.resolve(cwd, reportSuffix);
304
+ }
305
+
306
+ function collectCandidatePaths(node, acc = []) {
307
+ if (typeof node === "string") {
308
+ acc.push(node);
309
+ return acc;
310
+ }
311
+ if (!node || typeof node !== "object") {
312
+ return acc;
313
+ }
314
+ if (Array.isArray(node)) {
315
+ for (const item of node) {
316
+ collectCandidatePaths(item, acc);
317
+ }
318
+ return acc;
319
+ }
320
+ for (const [key, value] of Object.entries(node)) {
321
+ if (["path", "filePath", "file_path", "paths", "target", "targets"].includes(key)) {
322
+ collectCandidatePaths(value, acc);
323
+ continue;
324
+ }
325
+ collectCandidatePaths(value, acc);
326
+ }
327
+ return acc;
328
+ }
329
+
330
+ function resolveInputCwd(input) {
331
+ return path.resolve(
332
+ input?.cwd ??
333
+ input?.directory ??
334
+ input?.workspace?.cwd ??
335
+ input?.toolInput?.cwd ??
336
+ process.cwd()
337
+ );
338
+ }
339
+
340
+ function resolveToolName(input) {
341
+ return String(input?.toolName ?? input?.tool_name ?? input?.tool?.name ?? input?.name ?? "");
342
+ }
343
+
344
+ async function hasReport(cwd) {
345
+ try {
346
+ await fs.access(path.join(cwd, "wiki", "graph", "report.md"));
347
+ return true;
348
+ } catch {
349
+ return false;
350
+ }
351
+ }
352
+
353
+ async function markReportRead(cwd) {
354
+ const state = markerState(cwd);
355
+ await fs.mkdir(state.dir, { recursive: true });
356
+ await fs.writeFile(state.markerPath, "seen\\n", "utf8");
357
+ }
358
+
359
+ async function hasSeenReport(cwd) {
360
+ const state = markerState(cwd);
361
+ try {
362
+ await fs.access(state.markerPath);
363
+ return true;
364
+ } catch {
365
+ return false;
366
+ }
367
+ }
368
+
369
+ async function resetSession(cwd) {
370
+ const state = markerState(cwd);
371
+ await fs.rm(state.dir, { recursive: true, force: true });
372
+ }
373
+
374
+ function isBroadSearchTool(toolName) {
375
+ return /grep|glob|search|find/i.test(toolName);
376
+ }
377
+ `;
378
+ }
379
+ function buildGeminiHookScript() {
380
+ return `#!/usr/bin/env node
381
+ import crypto from "node:crypto";
382
+ import fs from "node:fs/promises";
383
+ import os from "node:os";
384
+ import path from "node:path";
385
+
386
+ ${markerStateSnippet("gemini").trim()}
387
+
388
+ async function readInput() {
389
+ let body = "";
390
+ for await (const chunk of process.stdin) {
391
+ body += chunk;
392
+ }
393
+ if (!body.trim()) {
394
+ return {};
395
+ }
396
+ try {
397
+ return JSON.parse(body);
398
+ } catch {
399
+ return {};
400
+ }
401
+ }
402
+
403
+ function emit(value) {
404
+ process.stdout.write(\`\${JSON.stringify(value)}\\n\`);
405
+ }
406
+
407
+ const mode = process.argv[2] ?? "";
408
+ const input = await readInput();
409
+ const cwd = resolveInputCwd(input);
410
+ const reportNote = "SwarmVault graph report exists at wiki/graph/report.md. Read it before broad grep/glob searching.";
411
+
412
+ if (!(await hasReport(cwd))) {
413
+ emit({});
414
+ process.exit(0);
415
+ }
416
+
417
+ if (mode === "session-start") {
418
+ await resetSession(cwd);
419
+ emit({
420
+ systemMessage: reportNote,
421
+ hookSpecificOutput: {
422
+ hookEventName: "SessionStart",
423
+ additionalContext: "SwarmVault graph report: wiki/graph/report.md"
424
+ }
425
+ });
426
+ process.exit(0);
427
+ }
428
+
429
+ const toolName = resolveToolName(input);
430
+ if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
431
+ await markReportRead(cwd);
432
+ emit({});
433
+ process.exit(0);
434
+ }
435
+
436
+ if (isBroadSearchTool(toolName) && !(await hasSeenReport(cwd))) {
437
+ emit({ systemMessage: reportNote });
438
+ process.exit(0);
439
+ }
440
+
441
+ emit({});
442
+ `;
443
+ }
444
+ function buildCopilotHookScript() {
445
+ return `#!/usr/bin/env node
446
+ import crypto from "node:crypto";
447
+ import fs from "node:fs/promises";
448
+ import os from "node:os";
449
+ import path from "node:path";
450
+
451
+ ${markerStateSnippet("copilot").trim()}
452
+
453
+ async function readInput() {
454
+ let body = "";
455
+ for await (const chunk of process.stdin) {
456
+ body += chunk;
457
+ }
458
+ if (!body.trim()) {
459
+ return {};
460
+ }
461
+ try {
462
+ return JSON.parse(body);
463
+ } catch {
464
+ return {};
465
+ }
466
+ }
467
+
468
+ function emit(value) {
469
+ if (value !== undefined) {
470
+ process.stdout.write(\`\${JSON.stringify(value)}\\n\`);
471
+ }
472
+ }
473
+
474
+ const mode = process.argv[2] ?? "";
475
+ const input = await readInput();
476
+ const cwd = resolveInputCwd(input);
477
+ const reportNote = "SwarmVault graph report exists at wiki/graph/report.md. Read it before broad grep/glob searching.";
478
+
479
+ if (!(await hasReport(cwd))) {
480
+ emit({});
481
+ process.exit(0);
482
+ }
483
+
484
+ if (mode === "session-start") {
485
+ await resetSession(cwd);
486
+ emit({});
487
+ process.exit(0);
488
+ }
489
+
490
+ const toolName = resolveToolName(input);
491
+ if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
492
+ await markReportRead(cwd);
493
+ emit({});
494
+ process.exit(0);
495
+ }
496
+
497
+ if (isBroadSearchTool(toolName) && !(await hasSeenReport(cwd))) {
498
+ emit({
499
+ permissionDecision: "deny",
500
+ permissionDecisionReason: reportNote
501
+ });
502
+ process.exit(0);
503
+ }
504
+
505
+ emit({});
506
+ `;
507
+ }
508
+ function buildOpenCodePlugin() {
509
+ return `import path from "node:path";
510
+
511
+ const reportRelativePath = path.join("wiki", "graph", "report.md");
512
+
513
+ export const name = "swarmvault-graph-first";
514
+
515
+ export default async function swarmvaultGraphFirst({ client }) {
516
+ let reportSeen = false;
517
+
518
+ async function hasReport(cwd) {
519
+ try {
520
+ await Bun.file(path.join(cwd, reportRelativePath)).arrayBuffer();
521
+ return true;
522
+ } catch {
523
+ return false;
524
+ }
525
+ }
526
+
527
+ async function note(message) {
528
+ if (client?.app?.log) {
529
+ await client.app.log({
530
+ level: "info",
531
+ message
532
+ });
533
+ }
534
+ }
535
+
536
+ return {
537
+ async "session.created"(input) {
538
+ reportSeen = false;
539
+ const cwd = input?.session?.cwd ?? process.cwd();
540
+ if (await hasReport(cwd)) {
541
+ await note("SwarmVault graph report exists. Read wiki/graph/report.md before broad workspace searching.");
542
+ }
543
+ },
544
+ async "tool.execute.before"(input) {
545
+ const cwd = input?.session?.cwd ?? process.cwd();
546
+ if (!(await hasReport(cwd))) {
547
+ return;
548
+ }
549
+
550
+ const argsText = JSON.stringify(input?.args ?? {});
551
+ if (argsText.includes("wiki/graph/report.md")) {
552
+ reportSeen = true;
553
+ return;
554
+ }
555
+
556
+ if (!reportSeen && ["glob", "grep"].includes(String(input?.tool ?? ""))) {
557
+ await note("SwarmVault graph report exists. Read wiki/graph/report.md before broad workspace searching.");
558
+ }
559
+ }
560
+ };
561
+ }
562
+ `;
563
+ }
564
+ async function installGeminiHook(rootDir) {
565
+ const settingsPath = path.join(rootDir, ".gemini", "settings.json");
566
+ const scriptPath = path.join(rootDir, ".gemini", "hooks", "swarmvault-graph-first.js");
567
+ await writeOwnedFile(scriptPath, buildGeminiHookScript(), true);
568
+ const { data: settings, warnings } = await readJsonWithWarnings(settingsPath, {}, ".gemini/settings.json");
569
+ if (warnings.length > 0 && await fileExists(settingsPath)) {
570
+ return { paths: [settingsPath, scriptPath], warnings };
571
+ }
572
+ const hooks = settings.hooks ?? {};
573
+ const sessionStart = hooks.SessionStart ?? [];
574
+ const beforeTool = hooks.BeforeTool ?? [];
575
+ const sessionCommand = "node .gemini/hooks/swarmvault-graph-first.js session-start";
576
+ const beforeToolCommand = "node .gemini/hooks/swarmvault-graph-first.js before-tool";
577
+ if (!sessionStart.some((entry) => entry.matcher === geminiSessionMatcher && JSON.stringify(entry).includes("swarmvault-graph-first.js"))) {
578
+ sessionStart.push({
579
+ matcher: geminiSessionMatcher,
580
+ hooks: [{ name: "swarmvault-graph-first", type: "command", command: sessionCommand }]
581
+ });
582
+ }
583
+ if (!beforeTool.some((entry) => entry.matcher === geminiSearchMatcher && JSON.stringify(entry).includes("swarmvault-graph-first.js"))) {
584
+ beforeTool.push({
585
+ matcher: geminiSearchMatcher,
586
+ hooks: [{ name: "swarmvault-graph-first", type: "command", command: beforeToolCommand }]
587
+ });
588
+ }
589
+ settings.hooks = {
590
+ ...hooks,
591
+ SessionStart: sessionStart,
592
+ BeforeTool: beforeTool
593
+ };
594
+ await writeOwnedFile(settingsPath, `${JSON.stringify(settings, null, 2)}
595
+ `);
596
+ return { paths: [settingsPath, scriptPath], warnings: [] };
597
+ }
598
+ async function mergeAiderConfig(rootDir) {
599
+ const configPath = path.join(rootDir, ".aider.conf.yml");
600
+ const readTarget = "CONVENTIONS.md";
601
+ if (!await fileExists(configPath)) {
602
+ const document = new YAML.Document();
603
+ document.set("read", [readTarget]);
604
+ await writeOwnedFile(configPath, `${document.toString()}`);
605
+ return { path: configPath, warnings: [] };
606
+ }
607
+ try {
608
+ const source = await fs.readFile(configPath, "utf8");
609
+ const document = YAML.parseDocument(source);
610
+ if (document.errors.length > 0) {
611
+ return {
612
+ path: configPath,
613
+ warnings: ["Could not parse .aider.conf.yml. Left the existing file unchanged; add `read: CONVENTIONS.md` manually."]
614
+ };
615
+ }
616
+ const currentRead = document.get("read", true);
617
+ const values = typeof currentRead === "string" ? [currentRead] : Array.isArray(currentRead) ? currentRead.filter((item) => typeof item === "string") : [];
618
+ if (!values.includes(readTarget)) {
619
+ document.set("read", [...values, readTarget]);
620
+ await writeOwnedFile(configPath, `${document.toString()}`);
621
+ }
622
+ return { path: configPath, warnings: [] };
623
+ } catch {
624
+ return {
625
+ path: configPath,
626
+ warnings: ["Could not parse .aider.conf.yml. Left the existing file unchanged; add `read: CONVENTIONS.md` manually."]
627
+ };
628
+ }
629
+ }
630
+ async function installCopilotHook(rootDir) {
631
+ const hooksDir = path.join(rootDir, ".github", "hooks");
632
+ const scriptPath = path.join(hooksDir, "swarmvault-graph-first.js");
633
+ const configPath = path.join(hooksDir, "swarmvault-graph-first.json");
634
+ await writeOwnedFile(scriptPath, buildCopilotHookScript(), true);
635
+ const config = {
636
+ version: copilotHookVersion,
637
+ hooks: {
638
+ sessionStart: [
639
+ {
640
+ type: "command",
641
+ bash: "node .github/hooks/swarmvault-graph-first.js session-start",
642
+ powershell: "node .github/hooks/swarmvault-graph-first.js session-start",
643
+ cwd: ".",
644
+ timeoutSec: 10
645
+ }
646
+ ],
647
+ preToolUse: [
648
+ {
649
+ matcher: "glob|grep",
650
+ type: "command",
651
+ bash: "node .github/hooks/swarmvault-graph-first.js pre-tool-use",
652
+ powershell: "node .github/hooks/swarmvault-graph-first.js pre-tool-use",
653
+ cwd: ".",
654
+ timeoutSec: 10
655
+ }
656
+ ]
657
+ }
658
+ };
659
+ await writeOwnedFile(configPath, `${JSON.stringify(config, null, 2)}
660
+ `);
661
+ return { paths: [configPath, scriptPath], warnings: [] };
662
+ }
663
+ async function installOpenCodeHook(rootDir) {
664
+ const pluginPath = path.join(rootDir, ".opencode", "plugins", "swarmvault-graph-first.js");
665
+ await writeOwnedFile(pluginPath, buildOpenCodePlugin());
666
+ return { paths: [pluginPath], warnings: [] };
667
+ }
668
+ function stableKeyForAgent(rootDir, agent) {
669
+ if (agent === "codex" || agent === "goose" || agent === "pi") {
670
+ return `shared:${path.join(rootDir, agentFileKinds.agents)}`;
671
+ }
672
+ return `${agent}:${crypto.createHash("sha1").update(targetsForAgent(rootDir, agent, { hook: supportsAgentHook(agent) }).join("\n")).digest("hex")}`;
673
+ }
118
674
  async function installAgent(rootDir, agent, options = {}) {
119
675
  await initWorkspace(rootDir);
120
- const target = targetPathForAgent(rootDir, agent);
676
+ const target = primaryTargetPathForAgent(rootDir, agent);
677
+ const warnings = [];
121
678
  switch (agent) {
122
679
  case "codex":
123
680
  case "goose":
124
681
  case "pi":
125
682
  case "opencode":
126
- await upsertManagedBlock(target, buildManagedBlock("agents"));
127
- return target;
128
- case "claude": {
683
+ await upsertManagedBlock(path.join(rootDir, agentFileKinds.agents), buildManagedBlock("agents"));
684
+ break;
685
+ case "claude":
129
686
  await upsertManagedBlock(target, buildManagedBlock("claude"));
130
- if (options.claudeHook) {
131
- await installClaudeHook(rootDir);
132
- }
133
- return target;
134
- }
135
- case "gemini": {
687
+ break;
688
+ case "gemini":
136
689
  await upsertManagedBlock(target, buildManagedBlock("gemini"));
137
- return target;
138
- }
139
- case "cursor": {
140
- const rulesDir = path.dirname(target);
141
- await ensureDir(rulesDir);
142
- await fs.writeFile(target, `${buildManagedBlock("cursor")}
143
- `, "utf8");
144
- return target;
145
- }
690
+ break;
691
+ case "cursor":
692
+ await writeOwnedFile(target, `${buildManagedBlock("cursor")}
693
+ `);
694
+ break;
695
+ case "aider":
696
+ await upsertManagedBlock(target, buildManagedBlock("aider"));
697
+ break;
698
+ case "copilot":
699
+ await upsertManagedBlock(path.join(rootDir, agentFileKinds.agents), buildManagedBlock("agents"));
700
+ await upsertManagedBlock(target, buildManagedBlock("copilot"));
701
+ break;
146
702
  default:
147
703
  throw new Error(`Unsupported agent ${String(agent)}`);
148
704
  }
705
+ if (agent === "aider") {
706
+ const aiderResult = await mergeAiderConfig(rootDir);
707
+ warnings.push(...aiderResult.warnings);
708
+ }
709
+ if (options.hook && supportsAgentHook(agent)) {
710
+ if (agent === "claude") {
711
+ const result = await installClaudeHook(rootDir);
712
+ warnings.push(...result.warnings);
713
+ }
714
+ if (agent === "opencode") {
715
+ const result = await installOpenCodeHook(rootDir);
716
+ warnings.push(...result.warnings);
717
+ }
718
+ if (agent === "gemini") {
719
+ const result = await installGeminiHook(rootDir);
720
+ warnings.push(...result.warnings);
721
+ }
722
+ if (agent === "copilot") {
723
+ const result = await installCopilotHook(rootDir);
724
+ warnings.push(...result.warnings);
725
+ }
726
+ }
727
+ const targets = targetsForAgent(rootDir, agent, options);
728
+ return warnings.length > 0 ? { agent, target, targets, warnings } : { agent, target, targets };
149
729
  }
150
730
  async function installConfiguredAgents(rootDir) {
151
731
  const { config } = await initWorkspace(rootDir);
152
- const dedupedTargets = /* @__PURE__ */ new Map();
732
+ const dedupedAgents = /* @__PURE__ */ new Map();
153
733
  for (const agent of config.agents) {
154
- const target = targetPathForAgent(rootDir, agent);
155
- if (!dedupedTargets.has(target)) {
156
- dedupedTargets.set(target, agent);
734
+ const key = stableKeyForAgent(rootDir, agent);
735
+ if (!dedupedAgents.has(key)) {
736
+ dedupedAgents.set(key, agent);
157
737
  }
158
738
  }
159
739
  return Promise.all(
160
- [...dedupedTargets.values()].map(
740
+ [...dedupedAgents.values()].map(
161
741
  (agent) => installAgent(rootDir, agent, {
162
- claudeHook: agent === "claude"
742
+ hook: supportsAgentHook(agent)
163
743
  })
164
744
  )
165
745
  );
@@ -179,8 +759,38 @@ var NODE_COLORS = {
179
759
  function xmlEscape(value) {
180
760
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
181
761
  }
182
- function cypherEscape(value) {
183
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
762
+ function cypherStringLiteral(value) {
763
+ let escaped = "";
764
+ for (const char of value) {
765
+ switch (char) {
766
+ case "\\":
767
+ escaped += "\\\\";
768
+ break;
769
+ case "'":
770
+ escaped += "\\'";
771
+ break;
772
+ case "\n":
773
+ escaped += "\\n";
774
+ break;
775
+ case "\r":
776
+ escaped += "\\r";
777
+ break;
778
+ case " ":
779
+ escaped += "\\t";
780
+ break;
781
+ case "\b":
782
+ escaped += "\\b";
783
+ break;
784
+ case "\f":
785
+ escaped += "\\f";
786
+ break;
787
+ default: {
788
+ const code = char.codePointAt(0) ?? 0;
789
+ escaped += code < 32 || code === 8232 || code === 8233 ? `\\u${code.toString(16).padStart(4, "0")}` : char;
790
+ }
791
+ }
792
+ }
793
+ return `'${escaped}'`;
184
794
  }
185
795
  function relationType(relation) {
186
796
  const normalized = relation.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
@@ -456,33 +1066,33 @@ function renderCypher(graph) {
456
1066
  for (const node of [...graph.nodes].sort((left, right) => left.id.localeCompare(right.id))) {
457
1067
  const page = node.pageId ? pageById2.get(node.pageId) : void 0;
458
1068
  const props = [
459
- `id: '${cypherEscape(node.id)}'`,
460
- `label: '${cypherEscape(node.label)}'`,
461
- `type: '${cypherEscape(node.type)}'`,
462
- `sourceIds: '${cypherEscape(JSON.stringify(node.sourceIds))}'`,
463
- `projectIds: '${cypherEscape(JSON.stringify(node.projectIds))}'`,
464
- node.pageId ? `pageId: '${cypherEscape(node.pageId)}'` : "",
465
- page?.path ? `pagePath: '${cypherEscape(page.path)}'` : "",
466
- node.language ? `language: '${cypherEscape(node.language)}'` : "",
467
- node.symbolKind ? `symbolKind: '${cypherEscape(node.symbolKind)}'` : "",
468
- node.communityId ? `communityId: '${cypherEscape(node.communityId)}'` : "",
1069
+ `id: ${cypherStringLiteral(node.id)}`,
1070
+ `label: ${cypherStringLiteral(node.label)}`,
1071
+ `type: ${cypherStringLiteral(node.type)}`,
1072
+ `sourceIds: ${cypherStringLiteral(JSON.stringify(node.sourceIds))}`,
1073
+ `projectIds: ${cypherStringLiteral(JSON.stringify(node.projectIds))}`,
1074
+ node.pageId ? `pageId: ${cypherStringLiteral(node.pageId)}` : "",
1075
+ page?.path ? `pagePath: ${cypherStringLiteral(page.path)}` : "",
1076
+ node.language ? `language: ${cypherStringLiteral(node.language)}` : "",
1077
+ node.symbolKind ? `symbolKind: ${cypherStringLiteral(node.symbolKind)}` : "",
1078
+ node.communityId ? `communityId: ${cypherStringLiteral(node.communityId)}` : "",
469
1079
  node.degree !== void 0 ? `degree: ${node.degree}` : "",
470
1080
  node.bridgeScore !== void 0 ? `bridgeScore: ${node.bridgeScore}` : "",
471
1081
  node.isGodNode !== void 0 ? `isGodNode: ${node.isGodNode}` : ""
472
1082
  ].filter(Boolean).join(", ");
473
- lines.push(`MERGE (n:SwarmNode {id: '${cypherEscape(node.id)}'}) SET n += { ${props} };`);
1083
+ lines.push(`MERGE (n:SwarmNode {id: ${cypherStringLiteral(node.id)}}) SET n += { ${props} };`);
474
1084
  }
475
1085
  lines.push("");
476
1086
  for (const hyperedge of [...graph.hyperedges ?? []].sort((left, right) => left.id.localeCompare(right.id))) {
477
1087
  const hyperedgeNodeId = exportHyperedgeNodeId(hyperedge);
478
1088
  lines.push(
479
- `MERGE (h:SwarmNode {id: '${cypherEscape(hyperedgeNodeId)}'}) SET h += { id: '${cypherEscape(hyperedgeNodeId)}', label: '${cypherEscape(
1089
+ `MERGE (h:SwarmNode {id: ${cypherStringLiteral(hyperedgeNodeId)}}) SET h += { id: ${cypherStringLiteral(hyperedgeNodeId)}, label: ${cypherStringLiteral(
480
1090
  hyperedge.label
481
- )}', type: 'hyperedge', relation: '${cypherEscape(hyperedge.relation)}', evidenceClass: '${cypherEscape(
1091
+ )}, type: ${cypherStringLiteral("hyperedge")}, relation: ${cypherStringLiteral(hyperedge.relation)}, evidenceClass: ${cypherStringLiteral(
482
1092
  hyperedge.evidenceClass
483
- )}', confidence: ${hyperedge.confidence}, sourcePageIds: '${cypherEscape(JSON.stringify(hyperedge.sourcePageIds))}', why: '${cypherEscape(
1093
+ )}, confidence: ${hyperedge.confidence}, sourcePageIds: ${cypherStringLiteral(JSON.stringify(hyperedge.sourcePageIds))}, why: ${cypherStringLiteral(
484
1094
  hyperedge.why
485
- )}' };`
1095
+ )} };`
486
1096
  );
487
1097
  }
488
1098
  if ((graph.hyperedges ?? []).length) {
@@ -492,22 +1102,22 @@ function renderCypher(graph) {
492
1102
  const hyperedgeNodeId = exportHyperedgeNodeId(hyperedge);
493
1103
  for (const nodeId of hyperedge.nodeIds) {
494
1104
  lines.push(
495
- `MATCH (h:SwarmNode {id: '${cypherEscape(hyperedgeNodeId)}'}), (n:SwarmNode {id: '${cypherEscape(nodeId)}'})`,
496
- `MERGE (h)-[r:GROUP_MEMBER {id: '${cypherEscape(`member:${hyperedge.id}:${nodeId}`)}'}]->(n)`,
497
- `SET r += { relation: 'group_member', status: 'inferred', evidenceClass: '${cypherEscape(
1105
+ `MATCH (h:SwarmNode {id: ${cypherStringLiteral(hyperedgeNodeId)}}), (n:SwarmNode {id: ${cypherStringLiteral(nodeId)}})`,
1106
+ `MERGE (h)-[r:GROUP_MEMBER {id: ${cypherStringLiteral(`member:${hyperedge.id}:${nodeId}`)}}]->(n)`,
1107
+ `SET r += { relation: ${cypherStringLiteral("group_member")}, status: ${cypherStringLiteral("inferred")}, evidenceClass: ${cypherStringLiteral(
498
1108
  hyperedge.evidenceClass
499
- )}', confidence: ${hyperedge.confidence}, provenance: '${cypherEscape(JSON.stringify(hyperedge.sourcePageIds))}' };`
1109
+ )}, confidence: ${hyperedge.confidence}, provenance: ${cypherStringLiteral(JSON.stringify(hyperedge.sourcePageIds))} };`
500
1110
  );
501
1111
  }
502
1112
  }
503
1113
  lines.push("");
504
1114
  for (const edge of [...graph.edges].sort((left, right) => left.id.localeCompare(right.id))) {
505
1115
  lines.push(
506
- `MATCH (a:SwarmNode {id: '${cypherEscape(edge.source)}'}), (b:SwarmNode {id: '${cypherEscape(edge.target)}'})`,
507
- `MERGE (a)-[r:${relationType(edge.relation)} {id: '${cypherEscape(edge.id)}'}]->(b)`,
508
- `SET r += { relation: '${cypherEscape(edge.relation)}', status: '${cypherEscape(edge.status)}', evidenceClass: '${cypherEscape(
1116
+ `MATCH (a:SwarmNode {id: ${cypherStringLiteral(edge.source)}}), (b:SwarmNode {id: ${cypherStringLiteral(edge.target)}})`,
1117
+ `MERGE (a)-[r:${relationType(edge.relation)} {id: ${cypherStringLiteral(edge.id)}}]->(b)`,
1118
+ `SET r += { relation: ${cypherStringLiteral(edge.relation)}, status: ${cypherStringLiteral(edge.status)}, evidenceClass: ${cypherStringLiteral(
509
1119
  edge.evidenceClass
510
- )}', confidence: ${edge.confidence}, provenance: '${cypherEscape(JSON.stringify(edge.provenance))}'${edge.similarityReasons?.length ? `, similarityReasons: '${cypherEscape(JSON.stringify(edge.similarityReasons))}'` : ""} };`
1120
+ )}, confidence: ${edge.confidence}, provenance: ${cypherStringLiteral(JSON.stringify(edge.provenance))}${edge.similarityReasons?.length ? `, similarityReasons: ${cypherStringLiteral(JSON.stringify(edge.similarityReasons))}` : ""} };`
511
1121
  );
512
1122
  }
513
1123
  lines.push("");
@@ -536,6 +1146,7 @@ async function exportGraphFormat(rootDir, format, outputPath) {
536
1146
  // src/hooks.ts
537
1147
  import fs3 from "fs/promises";
538
1148
  import path3 from "path";
1149
+ import process2 from "process";
539
1150
  var hookStart = "# >>> swarmvault hook >>>";
540
1151
  var hookEnd = "# <<< swarmvault hook <<<";
541
1152
  async function findNearestGitRoot(startPath) {
@@ -562,12 +1173,22 @@ async function findNearestGitRoot(startPath) {
562
1173
  function shellQuote(value) {
563
1174
  return `'${value.replace(/'/g, `'"'"'`)}'`;
564
1175
  }
1176
+ function resolveSwarmvaultExecutableCandidate() {
1177
+ const argvPath = process2.argv[1];
1178
+ if (typeof argvPath === "string" && argvPath.trim() && (argvPath.includes(`${path3.sep}@swarmvaultai${path3.sep}cli${path3.sep}`) || argvPath.includes(`${path3.sep}packages${path3.sep}cli${path3.sep}`))) {
1179
+ return path3.resolve(argvPath);
1180
+ }
1181
+ return "swarmvault";
1182
+ }
565
1183
  function managedHookBlock(vaultRoot) {
1184
+ const resolvedExecutable = resolveSwarmvaultExecutableCandidate();
566
1185
  return [
567
1186
  hookStart,
568
1187
  `cd ${shellQuote(vaultRoot)} || exit 0`,
569
- "if command -v swarmvault >/dev/null 2>&1; then",
570
- " swarmvault watch --repo --once >/dev/null 2>&1 || printf '[swarmvault hook] refresh failed\\n' >&2",
1188
+ `swarmvault_bin=${shellQuote(resolvedExecutable)}`,
1189
+ '[ ! -x "$swarmvault_bin" ] && swarmvault_bin=$(command -v swarmvault 2>/dev/null || true)',
1190
+ 'if [ -n "$swarmvault_bin" ] && [ -x "$swarmvault_bin" ]; then',
1191
+ ` "$swarmvault_bin" watch --repo --once >/dev/null 2>&1 || printf '[swarmvault hook] refresh failed\\n' >&2`,
571
1192
  "fi",
572
1193
  hookEnd,
573
1194
  ""
@@ -667,7 +1288,7 @@ import { pathToFileURL } from "url";
667
1288
  import { Readability } from "@mozilla/readability";
668
1289
  import matter3 from "gray-matter";
669
1290
  import ignore from "ignore";
670
- import { JSDOM } from "jsdom";
1291
+ import { JSDOM as JSDOM2 } from "jsdom";
671
1292
  import mime from "mime-types";
672
1293
  import TurndownService from "turndown";
673
1294
 
@@ -792,6 +1413,55 @@ function collectCallNamesFromText(text, availableNames, selfName) {
792
1413
  }
793
1414
  return uniqueBy(names, (name) => name);
794
1415
  }
1416
+ function goReceiverBinding(node) {
1417
+ if (!node) {
1418
+ return {};
1419
+ }
1420
+ const names = uniqueBy(
1421
+ node.descendantsOfType(["identifier", "type_identifier"]).filter((item) => item !== null).map((item) => normalizeSymbolReference(item.text)).filter(Boolean),
1422
+ (item) => item
1423
+ );
1424
+ if (names.length === 0) {
1425
+ return {};
1426
+ }
1427
+ if (names.length === 1) {
1428
+ return { typeName: names[0] };
1429
+ }
1430
+ return {
1431
+ variableName: names[0],
1432
+ typeName: names.at(-1)
1433
+ };
1434
+ }
1435
+ function goCalledSymbolName(node, receiver) {
1436
+ if (!node) {
1437
+ return void 0;
1438
+ }
1439
+ if (node.type === "selector_expression") {
1440
+ const targetName = normalizeSymbolReference(
1441
+ extractIdentifier(node.childForFieldName("operand") ?? node.childForFieldName("object") ?? node.namedChildren.at(0) ?? null) ?? ""
1442
+ );
1443
+ const fieldName = normalizeSymbolReference(
1444
+ extractIdentifier(node.childForFieldName("field") ?? findNamedChild(node, "field_identifier") ?? node.namedChildren.at(-1) ?? null) ?? ""
1445
+ );
1446
+ if (!fieldName) {
1447
+ return void 0;
1448
+ }
1449
+ if (receiver.variableName && receiver.typeName && targetName === receiver.variableName) {
1450
+ return `${receiver.typeName}.${fieldName}`;
1451
+ }
1452
+ return fieldName;
1453
+ }
1454
+ return normalizeSymbolReference(extractIdentifier(node) ?? "");
1455
+ }
1456
+ function goCallNamesFromBody(bodyNode, receiver) {
1457
+ if (!bodyNode) {
1458
+ return [];
1459
+ }
1460
+ return uniqueBy(
1461
+ bodyNode.descendantsOfType("call_expression").filter((item) => item !== null).map((callNode) => goCalledSymbolName(callNode.childForFieldName("function") ?? callNode.namedChildren.at(0) ?? null, receiver)).filter((name) => Boolean(name)),
1462
+ (name) => name
1463
+ );
1464
+ }
795
1465
  function finalizeCodeAnalysis(manifest, language, imports, draftSymbols, exportLabels, diagnostics, metadata) {
796
1466
  const topLevelNames = new Set(draftSymbols.map((symbol) => symbol.name));
797
1467
  for (const symbol of draftSymbols) {
@@ -1278,7 +1948,9 @@ function goCodeAnalysis(manifest, rootNode, diagnostics) {
1278
1948
  if (!name) {
1279
1949
  continue;
1280
1950
  }
1281
- const receiverType = child.type === "method_declaration" ? normalizeSymbolReference(nodeText(child.childForFieldName("receiver")).replace(/[()]/g, " ").split(/\s+/).at(-1) ?? "") : "";
1951
+ const receiver = child.type === "method_declaration" ? goReceiverBinding(child.childForFieldName("receiver")) : {};
1952
+ const receiverType = receiver.typeName ?? "";
1953
+ const bodyNode = child.childForFieldName("body");
1282
1954
  const symbolName = receiverType ? `${receiverType}.${name}` : name;
1283
1955
  const exported = exportedByCapitalization(name);
1284
1956
  draftSymbols.push({
@@ -1286,10 +1958,10 @@ function goCodeAnalysis(manifest, rootNode, diagnostics) {
1286
1958
  kind: "function",
1287
1959
  signature: singleLineSignature(child.text),
1288
1960
  exported,
1289
- callNames: [],
1961
+ callNames: goCallNamesFromBody(bodyNode, receiver),
1290
1962
  extendsNames: [],
1291
1963
  implementsNames: [],
1292
- bodyText: nodeText(child.childForFieldName("body"))
1964
+ bodyText: nodeText(bodyNode)
1293
1965
  });
1294
1966
  if (exported) {
1295
1967
  exportLabels.push(symbolName);
@@ -2819,6 +3491,8 @@ async function analyzeCodeSource(manifest, extractedText, schemaHash) {
2819
3491
  import fs6 from "fs/promises";
2820
3492
  import os from "os";
2821
3493
  import path6 from "path";
3494
+ import { strFromU8, unzipSync } from "fflate";
3495
+ import { JSDOM } from "jsdom";
2822
3496
  import { z } from "zod";
2823
3497
  var imageVisionExtractionSchema = z.object({
2824
3498
  title: z.string().min(1).nullable().optional(),
@@ -2995,6 +3669,49 @@ function normalizePdfMetadata(raw) {
2995
3669
  }
2996
3670
  return Object.keys(metadata).length ? metadata : void 0;
2997
3671
  }
3672
+ function normalizeDocumentText(raw) {
3673
+ return raw.replace(/\r\n/g, "\n").split(/\n{2,}/).map((section) => normalizeWhitespace(section)).filter(Boolean).join("\n\n").trim();
3674
+ }
3675
+ function parseDocxCoreMetadata(bytes) {
3676
+ try {
3677
+ const archive = unzipSync(new Uint8Array(bytes));
3678
+ const coreXml = archive["docProps/core.xml"];
3679
+ if (!coreXml) {
3680
+ return void 0;
3681
+ }
3682
+ const dom = new JSDOM(strFromU8(coreXml), { contentType: "text/xml" });
3683
+ const document = dom.window.document;
3684
+ const valuesByLocalName = /* @__PURE__ */ new Map();
3685
+ for (const node of Array.from(document.getElementsByTagName("*"))) {
3686
+ const localName = node.localName?.trim().toLowerCase();
3687
+ const text = normalizeWhitespace(node.textContent ?? "");
3688
+ if (!localName || !text || valuesByLocalName.has(localName)) {
3689
+ continue;
3690
+ }
3691
+ valuesByLocalName.set(localName, text);
3692
+ }
3693
+ const metadata = {};
3694
+ const mappings = [
3695
+ ["title", "title"],
3696
+ ["author", "creator"],
3697
+ ["subject", "subject"],
3698
+ ["description", "description"],
3699
+ ["keywords", "keywords"],
3700
+ ["last_modified_by", "lastmodifiedby"],
3701
+ ["created", "created"],
3702
+ ["modified", "modified"]
3703
+ ];
3704
+ for (const [targetKey, sourceKey] of mappings) {
3705
+ const value = valuesByLocalName.get(sourceKey);
3706
+ if (value) {
3707
+ metadata[targetKey] = value;
3708
+ }
3709
+ }
3710
+ return Object.keys(metadata).length ? metadata : void 0;
3711
+ } catch {
3712
+ return void 0;
3713
+ }
3714
+ }
2998
3715
  async function extractPdfText(input) {
2999
3716
  try {
3000
3717
  const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
@@ -3042,6 +3759,35 @@ async function extractPdfText(input) {
3042
3759
  };
3043
3760
  }
3044
3761
  }
3762
+ async function extractDocxText(input) {
3763
+ try {
3764
+ const mammoth = await import("mammoth");
3765
+ const result = await mammoth.extractRawText({
3766
+ buffer: input.bytes
3767
+ });
3768
+ const extractedText = normalizeDocumentText(result.value);
3769
+ const warnings = result.messages.map((message) => normalizeWhitespace(message.message)).filter(Boolean).map((message) => truncate(message, 240));
3770
+ const artifact = {
3771
+ ...extractionMetadata("docx", input.mimeType, "docx_text"),
3772
+ metadata: parseDocxCoreMetadata(input.bytes),
3773
+ warnings: warnings.length ? warnings : void 0
3774
+ };
3775
+ if (!extractedText) {
3776
+ artifact.warnings = [...artifact.warnings ?? [], "DOCX text extraction completed but produced no extractable text."];
3777
+ }
3778
+ return {
3779
+ extractedText: extractedText || void 0,
3780
+ artifact
3781
+ };
3782
+ } catch (error) {
3783
+ return {
3784
+ artifact: {
3785
+ ...extractionMetadata("docx", input.mimeType, "docx_text"),
3786
+ warnings: [`DOCX text extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
3787
+ }
3788
+ };
3789
+ }
3790
+ }
3045
3791
 
3046
3792
  // src/logs.ts
3047
3793
  import fs7 from "fs/promises";
@@ -3352,6 +4098,9 @@ function inferKind(mimeType, filePath) {
3352
4098
  if (mimeType === "application/pdf" || filePath.toLowerCase().endsWith(".pdf")) {
3353
4099
  return "pdf";
3354
4100
  }
4101
+ if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || filePath.toLowerCase().endsWith(".docx")) {
4102
+ return "docx";
4103
+ }
3355
4104
  if (mimeType.startsWith("image/")) {
3356
4105
  return "image";
3357
4106
  }
@@ -3530,7 +4279,7 @@ async function fetchResolvedText(url) {
3530
4279
  };
3531
4280
  }
3532
4281
  function domTextFromHtml(html, baseUrl) {
3533
- const dom = new JSDOM(`<body>${html}</body>`, { url: baseUrl });
4282
+ const dom = new JSDOM2(`<body>${html}</body>`, { url: baseUrl });
3534
4283
  return normalizeWhitespace(dom.window.document.body.textContent ?? "");
3535
4284
  }
3536
4285
  async function captureArxivMarkdown(input, options) {
@@ -3540,7 +4289,7 @@ async function captureArxivMarkdown(input, options) {
3540
4289
  }
3541
4290
  const normalizedUrl = `https://arxiv.org/abs/${arxivId}`;
3542
4291
  const html = await fetchText(normalizedUrl);
3543
- const dom = new JSDOM(html, { url: normalizedUrl });
4292
+ const dom = new JSDOM2(html, { url: normalizedUrl });
3544
4293
  const document = dom.window.document;
3545
4294
  const metaTitle = document.querySelector('meta[name="citation_title"]')?.getAttribute("content")?.trim();
3546
4295
  const headingTitle = document.querySelector("h1.title")?.textContent?.trim();
@@ -3649,7 +4398,7 @@ async function captureArticleMarkdown(rootDir, input, options, extra = { sourceT
3649
4398
  if (!resolved.contentType.includes("html")) {
3650
4399
  throw new Error(`Unsupported article content type: ${resolved.contentType}`);
3651
4400
  }
3652
- const dom = new JSDOM(resolved.text, { url: resolved.finalUrl });
4401
+ const dom = new JSDOM2(resolved.text, { url: resolved.finalUrl });
3653
4402
  const document = dom.window.document;
3654
4403
  const canonicalHref = document.querySelector('link[rel="canonical"]')?.getAttribute("href")?.trim();
3655
4404
  const canonicalUrl = canonicalHref ? normalizeOriginUrl(new URL(canonicalHref, resolved.finalUrl).toString()) : resolved.finalUrl;
@@ -3768,6 +4517,22 @@ function extractMarkdownReferences(content) {
3768
4517
  }
3769
4518
  return references;
3770
4519
  }
4520
+ function extractHtmlLocalReferences(html, baseUrl) {
4521
+ const dom = new JSDOM2(html, { url: baseUrl });
4522
+ const document = dom.window.document;
4523
+ const references = [];
4524
+ for (const image of [...document.querySelectorAll("img[src]")]) {
4525
+ const src = image.getAttribute("src");
4526
+ if (!src) {
4527
+ continue;
4528
+ }
4529
+ const normalized = normalizeLocalReference(src);
4530
+ if (normalized) {
4531
+ references.push(normalized);
4532
+ }
4533
+ }
4534
+ return references;
4535
+ }
3771
4536
  function normalizeRemoteReference(value, baseUrl) {
3772
4537
  const trimmed = value.trim().replace(/^<|>$/g, "");
3773
4538
  const [withoutTitle] = trimmed.split(/\s+(?=(?:[^"]*"[^"]*")*[^"]*$)/, 1);
@@ -3803,7 +4568,7 @@ function extractMarkdownImageReferences(content, baseUrl) {
3803
4568
  return references;
3804
4569
  }
3805
4570
  async function convertHtmlToMarkdown(html, url) {
3806
- const dom = new JSDOM(html, { url });
4571
+ const dom = new JSDOM2(html, { url });
3807
4572
  const article = new Readability(dom.window.document).parse();
3808
4573
  const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
3809
4574
  const body = article?.content ?? dom.window.document.body.innerHTML;
@@ -4006,7 +4771,7 @@ async function collectRemoteImageAttachments(assetUrls, options) {
4006
4771
  return { attachments, skippedCount };
4007
4772
  }
4008
4773
  function extractHtmlImageReferences(html, baseUrl) {
4009
- const dom = new JSDOM(html, { url: baseUrl });
4774
+ const dom = new JSDOM2(html, { url: baseUrl });
4010
4775
  const document = dom.window.document;
4011
4776
  const references = [];
4012
4777
  for (const image of [...document.querySelectorAll("img[src]")]) {
@@ -4022,7 +4787,7 @@ function extractHtmlImageReferences(html, baseUrl) {
4022
4787
  return references;
4023
4788
  }
4024
4789
  function rewriteHtmlImageReferences(html, baseUrl, replacements) {
4025
- const dom = new JSDOM(html, { url: baseUrl });
4790
+ const dom = new JSDOM2(html, { url: baseUrl });
4026
4791
  const document = dom.window.document;
4027
4792
  for (const image of [...document.querySelectorAll("img[src]")]) {
4028
4793
  const src = image.getAttribute("src");
@@ -4037,6 +4802,22 @@ function rewriteHtmlImageReferences(html, baseUrl, replacements) {
4037
4802
  }
4038
4803
  return dom.serialize();
4039
4804
  }
4805
+ function rewriteHtmlLocalReferences(html, baseUrl, replacements) {
4806
+ const dom = new JSDOM2(html, { url: baseUrl });
4807
+ const document = dom.window.document;
4808
+ for (const image of [...document.querySelectorAll("img[src]")]) {
4809
+ const src = image.getAttribute("src");
4810
+ if (!src) {
4811
+ continue;
4812
+ }
4813
+ const normalized = normalizeLocalReference(src);
4814
+ const replacement = normalized ? replacements.get(normalized) : void 0;
4815
+ if (replacement) {
4816
+ image.setAttribute("src", replacement);
4817
+ }
4818
+ }
4819
+ return dom.serialize();
4820
+ }
4040
4821
  function rewriteMarkdownImageReferences(content, baseUrl, replacements) {
4041
4822
  return content.replace(/(!\[[^\]]*]\()([^)]+)(\))/g, (fullMatch, prefix, target, suffix) => {
4042
4823
  const normalized = normalizeRemoteReference(target, baseUrl);
@@ -4177,7 +4958,7 @@ function preparedMatchesManifest(manifest, prepared, contentHash) {
4177
4958
  return manifest.contentHash === contentHash && manifest.extractionHash === (prepared.extractionHash ?? buildExtractionHash(prepared.extractedText, prepared.extractionArtifact)) && manifest.title === prepared.title && manifest.sourceKind === prepared.sourceKind && manifest.sourceType === prepared.sourceType && manifest.sourceClass === prepared.sourceClass && manifest.language === prepared.language && manifest.mimeType === prepared.mimeType && manifest.repoRelativePath === prepared.repoRelativePath;
4178
4959
  }
4179
4960
  function shouldDeferWatchSemanticRefresh(sourceKind) {
4180
- return sourceKind === "markdown" || sourceKind === "text" || sourceKind === "html" || sourceKind === "pdf" || sourceKind === "image";
4961
+ return sourceKind === "markdown" || sourceKind === "text" || sourceKind === "html" || sourceKind === "pdf" || sourceKind === "docx" || sourceKind === "image";
4181
4962
  }
4182
4963
  function pendingSemanticRefreshId(changeType, repoRoot, relativePath) {
4183
4964
  return `pending:${changeType}:${sha256(`${toPosix(repoRoot)}:${relativePath}`).slice(0, 12)}`;
@@ -4441,6 +5222,12 @@ async function prepareFileInput(rootDir, absoluteInput, repoRoot, sourceClass) {
4441
5222
  const extracted = await extractPdfText({ mimeType, bytes: payloadBytes });
4442
5223
  extractedText = extracted.extractedText;
4443
5224
  extractionArtifact = extracted.artifact;
5225
+ } else if (sourceKind === "docx") {
5226
+ title = path10.basename(absoluteInput, path10.extname(absoluteInput));
5227
+ const extracted = await extractDocxText({ mimeType, bytes: payloadBytes });
5228
+ title = extracted.artifact.metadata?.title?.trim() || title;
5229
+ extractedText = extracted.extractedText;
5230
+ extractionArtifact = extracted.artifact;
4444
5231
  } else if (sourceKind === "image") {
4445
5232
  title = path10.basename(absoluteInput, path10.extname(absoluteInput));
4446
5233
  const extracted = await extractImageWithVision(rootDir, {
@@ -4558,6 +5345,11 @@ async function prepareUrlInput(rootDir, input, options) {
4558
5345
  const extracted = await extractPdfText({ mimeType, bytes: payloadBytes });
4559
5346
  extractedText = extracted.extractedText;
4560
5347
  extractionArtifact = extracted.artifact;
5348
+ } else if (sourceKind === "docx") {
5349
+ const extracted = await extractDocxText({ mimeType, bytes: payloadBytes });
5350
+ title = extracted.artifact.metadata?.title?.trim() || title;
5351
+ extractedText = extracted.extractedText;
5352
+ extractionArtifact = extracted.artifact;
4561
5353
  } else if (sourceKind === "image") {
4562
5354
  const extracted = await extractImageWithVision(rootDir, {
4563
5355
  title,
@@ -4591,11 +5383,11 @@ async function collectInboxAttachmentRefs(inputDir, files) {
4591
5383
  for (const absolutePath of files) {
4592
5384
  const mimeType = guessMimeType(absolutePath);
4593
5385
  const sourceKind = inferKind(mimeType, absolutePath);
4594
- if (sourceKind !== "markdown") {
5386
+ if (sourceKind !== "markdown" && sourceKind !== "html") {
4595
5387
  continue;
4596
5388
  }
4597
5389
  const content = await fs9.readFile(absolutePath, "utf8");
4598
- const refs = extractMarkdownReferences(content);
5390
+ const refs = sourceKind === "html" ? extractHtmlLocalReferences(content, pathToFileURL(absolutePath).toString()) : extractMarkdownReferences(content);
4599
5391
  if (!refs.length) {
4600
5392
  continue;
4601
5393
  }
@@ -4673,8 +5465,50 @@ async function prepareInboxMarkdownInput(absolutePath, attachmentRefs) {
4673
5465
  contentHash
4674
5466
  };
4675
5467
  }
5468
+ async function prepareInboxHtmlInput(absolutePath, attachmentRefs) {
5469
+ const originalBytes = await fs9.readFile(absolutePath);
5470
+ const originalHtml = originalBytes.toString("utf8");
5471
+ const initialConversion = await convertHtmlToMarkdown(originalHtml, pathToFileURL(absolutePath).toString());
5472
+ const attachments = [];
5473
+ for (const attachmentRef of attachmentRefs) {
5474
+ const bytes = await fs9.readFile(attachmentRef.absolutePath);
5475
+ attachments.push({
5476
+ relativePath: sanitizeAssetRelativePath(attachmentRef.relativeRef),
5477
+ mimeType: guessMimeType(attachmentRef.absolutePath),
5478
+ originalPath: toPosix(attachmentRef.absolutePath),
5479
+ bytes
5480
+ });
5481
+ }
5482
+ const contentHash = buildCompositeHash(originalBytes, attachments);
5483
+ const fallbackTitle = path10.basename(absolutePath, path10.extname(absolutePath));
5484
+ const title = initialConversion.title || fallbackTitle;
5485
+ const sourceId = `${slugify(title)}-${contentHash.slice(0, 8)}`;
5486
+ const replacements = new Map(
5487
+ attachmentRefs.map((attachmentRef) => [
5488
+ attachmentRef.relativeRef.replace(/\\/g, "/"),
5489
+ `../assets/${sourceId}/${sanitizeAssetRelativePath(attachmentRef.relativeRef)}`
5490
+ ])
5491
+ );
5492
+ const rewrittenHtml = rewriteHtmlLocalReferences(originalHtml, pathToFileURL(absolutePath).toString(), replacements);
5493
+ const converted = rewrittenHtml === originalHtml ? initialConversion : await convertHtmlToMarkdown(rewrittenHtml, pathToFileURL(absolutePath).toString());
5494
+ const extractionArtifact = createHtmlReadabilityExtractionArtifact("html", "text/html");
5495
+ return {
5496
+ title: converted.title || title,
5497
+ originType: "file",
5498
+ sourceKind: "html",
5499
+ originalPath: toPosix(absolutePath),
5500
+ mimeType: "text/html",
5501
+ storedExtension: path10.extname(absolutePath) || ".html",
5502
+ payloadBytes: Buffer.from(rewrittenHtml, "utf8"),
5503
+ extractedText: converted.markdown,
5504
+ extractionArtifact,
5505
+ extractionHash: buildExtractionHash(converted.markdown, extractionArtifact),
5506
+ attachments,
5507
+ contentHash
5508
+ };
5509
+ }
4676
5510
  function isSupportedInboxKind(sourceKind) {
4677
- return ["markdown", "text", "html", "pdf", "image"].includes(sourceKind);
5511
+ return ["markdown", "text", "html", "pdf", "docx", "image"].includes(sourceKind);
4678
5512
  }
4679
5513
  async function ingestInput(rootDir, input, options) {
4680
5514
  const { paths } = await initWorkspace(rootDir);
@@ -4834,7 +5668,7 @@ async function importInbox(rootDir, inputDir) {
4834
5668
  skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
4835
5669
  continue;
4836
5670
  }
4837
- const prepared = sourceKind === "markdown" && refsBySource.has(absolutePath) ? await prepareInboxMarkdownInput(absolutePath, refsBySource.get(absolutePath) ?? []) : await prepareFileInput(rootDir, absolutePath);
5671
+ const prepared = sourceKind === "markdown" && refsBySource.has(absolutePath) ? await prepareInboxMarkdownInput(absolutePath, refsBySource.get(absolutePath) ?? []) : sourceKind === "html" && refsBySource.has(absolutePath) ? await prepareInboxHtmlInput(absolutePath, refsBySource.get(absolutePath) ?? []) : await prepareFileInput(rootDir, absolutePath);
4838
5672
  const result = await persistPreparedInput(rootDir, prepared, paths);
4839
5673
  if (!result.isNew) {
4840
5674
  skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: "duplicate_content" });
@@ -6108,8 +6942,14 @@ async function loadPageContents(rootDir, graph) {
6108
6942
  await Promise.all(
6109
6943
  graph.pages.map(async (page) => {
6110
6944
  const absolutePath = path16.join(paths.wikiDir, page.path);
6111
- const content = await fs12.readFile(absolutePath, "utf8").catch(() => "");
6112
- contents.set(page.id, content);
6945
+ const content = await fs12.readFile(absolutePath, "utf8").catch(() => {
6946
+ process.stderr.write(`[swarmvault] Warning: could not read page ${page.path} for embedding
6947
+ `);
6948
+ return "";
6949
+ });
6950
+ if (content) {
6951
+ contents.set(page.id, content);
6952
+ }
6113
6953
  })
6114
6954
  );
6115
6955
  return contents;
@@ -9207,7 +10047,7 @@ async function resolveImageGenerationProvider(rootDir) {
9207
10047
  if (!providerConfig) {
9208
10048
  throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
9209
10049
  }
9210
- const { createProvider: createProvider2 } = await import("./registry-YGVTLIZH.js");
10050
+ const { createProvider: createProvider2 } = await import("./registry-5SYH3Y3U.js");
9211
10051
  return createProvider2(preferredProviderId, providerConfig, rootDir);
9212
10052
  }
9213
10053
  async function generateOutputArtifacts(rootDir, input) {
@@ -9768,8 +10608,64 @@ function deriveGraphMetrics(nodes, edges) {
9768
10608
  function resetGraphNodeMetrics(nodes) {
9769
10609
  return nodes.map(({ communityId: _communityId, degree: _degree, bridgeScore: _bridgeScore, isGodNode: _isGodNode, ...node }) => node);
9770
10610
  }
10611
+ function manifestRepoPath(manifest) {
10612
+ return toPosix(manifest.repoRelativePath ?? path20.basename(manifest.originalPath ?? manifest.storedPath));
10613
+ }
10614
+ function goPackageScopeKey(manifest, analysis) {
10615
+ if (analysis.code?.language !== "go") {
10616
+ return null;
10617
+ }
10618
+ const packageName = analysis.code.namespace?.trim();
10619
+ if (!packageName) {
10620
+ return null;
10621
+ }
10622
+ return `${packageName}:${path20.posix.dirname(manifestRepoPath(manifest))}`;
10623
+ }
10624
+ function buildGoPackageSymbolLookups(analyses, manifestsById) {
10625
+ const lookups = /* @__PURE__ */ new Map();
10626
+ for (const analysis of analyses) {
10627
+ if (analysis.code?.language !== "go") {
10628
+ continue;
10629
+ }
10630
+ const manifest = manifestsById.get(analysis.sourceId);
10631
+ if (!manifest) {
10632
+ continue;
10633
+ }
10634
+ const scopeKey = goPackageScopeKey(manifest, analysis);
10635
+ if (!scopeKey) {
10636
+ continue;
10637
+ }
10638
+ const current = lookups.get(scopeKey) ?? {
10639
+ byName: /* @__PURE__ */ new Map(),
10640
+ methodIdsByShortName: /* @__PURE__ */ new Map()
10641
+ };
10642
+ for (const symbol of analysis.code.symbols) {
10643
+ current.byName.set(symbol.name, symbol.id);
10644
+ const separator = symbol.name.lastIndexOf(".");
10645
+ if (separator > 0) {
10646
+ const shortName = symbol.name.slice(separator + 1);
10647
+ const matches = current.methodIdsByShortName.get(shortName) ?? /* @__PURE__ */ new Set();
10648
+ matches.add(symbol.id);
10649
+ current.methodIdsByShortName.set(shortName, matches);
10650
+ }
10651
+ }
10652
+ lookups.set(scopeKey, current);
10653
+ }
10654
+ return new Map(
10655
+ [...lookups.entries()].map(([scopeKey, value]) => [
10656
+ scopeKey,
10657
+ {
10658
+ byName: value.byName,
10659
+ uniqueMethodIdsByShortName: new Map(
10660
+ [...value.methodIdsByShortName.entries()].filter(([, ids]) => ids.size === 1).map(([shortName, ids]) => [shortName, [...ids][0]])
10661
+ )
10662
+ }
10663
+ ])
10664
+ );
10665
+ }
9771
10666
  function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
9772
10667
  const manifestsById = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
10668
+ const goPackageSymbolLookups = buildGoPackageSymbolLookups(analyses, manifestsById);
9773
10669
  const sourceNodes = manifests.map((manifest) => ({
9774
10670
  id: `source:${manifest.sourceId}`,
9775
10671
  type: "source",
@@ -9916,6 +10812,10 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
9916
10812
  }
9917
10813
  }
9918
10814
  const symbolIdsByName = new Map(analysis.code.symbols.map((symbol) => [symbol.name, symbol.id]));
10815
+ const goPackageLookup = analysis.code.language === "go" ? goPackageSymbolLookups.get(goPackageScopeKey(manifest, analysis) ?? "") : void 0;
10816
+ const localSymbolIdsByName = goPackageLookup?.byName ?? symbolIdsByName;
10817
+ const localGoMethodIdsByShortName = goPackageLookup?.uniqueMethodIdsByShortName ?? /* @__PURE__ */ new Map();
10818
+ const resolveLocalSymbolId = (targetName) => localSymbolIdsByName.get(targetName) ?? (analysis.code?.language === "go" ? localGoMethodIdsByShortName.get(targetName) : void 0);
9919
10819
  for (const rationale of analysis.rationales) {
9920
10820
  const targetSymbolId = rationale.symbolName ? symbolIdsByName.get(rationale.symbolName) : void 0;
9921
10821
  const targetId = targetSymbolId ?? moduleId;
@@ -9968,9 +10868,31 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
9968
10868
  }
9969
10869
  }
9970
10870
  }
10871
+ if (analysis.code.language === "go") {
10872
+ for (const symbol of analysis.code.symbols) {
10873
+ const separator = symbol.name.lastIndexOf(".");
10874
+ if (separator <= 0) {
10875
+ continue;
10876
+ }
10877
+ const receiverTypeId = localSymbolIdsByName.get(symbol.name.slice(0, separator));
10878
+ if (!receiverTypeId || receiverTypeId === symbol.id) {
10879
+ continue;
10880
+ }
10881
+ pushEdge({
10882
+ id: `${receiverTypeId}->${symbol.id}:defines:receiver`,
10883
+ source: receiverTypeId,
10884
+ target: symbol.id,
10885
+ relation: "defines",
10886
+ status: "extracted",
10887
+ evidenceClass: "extracted",
10888
+ confidence: 1,
10889
+ provenance: [analysis.sourceId]
10890
+ });
10891
+ }
10892
+ }
9971
10893
  for (const symbol of analysis.code.symbols) {
9972
10894
  for (const targetName of symbol.calls) {
9973
- const targetId = symbolIdsByName.get(targetName);
10895
+ const targetId = resolveLocalSymbolId(targetName);
9974
10896
  if (!targetId || targetId === symbol.id) {
9975
10897
  continue;
9976
10898
  }
@@ -9986,7 +10908,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
9986
10908
  });
9987
10909
  }
9988
10910
  for (const targetName of symbol.extends) {
9989
- const targetId = symbolIdsByName.get(targetName) ?? importedSymbolIdsByName.get(targetName);
10911
+ const targetId = resolveLocalSymbolId(targetName) ?? importedSymbolIdsByName.get(targetName);
9990
10912
  if (!targetId) {
9991
10913
  continue;
9992
10914
  }
@@ -10002,7 +10924,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
10002
10924
  });
10003
10925
  }
10004
10926
  for (const targetName of symbol.implements) {
10005
- const targetId = symbolIdsByName.get(targetName) ?? importedSymbolIdsByName.get(targetName);
10927
+ const targetId = resolveLocalSymbolId(targetName) ?? importedSymbolIdsByName.get(targetName);
10006
10928
  if (!targetId) {
10007
10929
  continue;
10008
10930
  }
@@ -12482,7 +13404,7 @@ async function bootstrapDemo(rootDir, input) {
12482
13404
  }
12483
13405
 
12484
13406
  // src/mcp.ts
12485
- var SERVER_VERSION = "0.1.27";
13407
+ var SERVER_VERSION = "0.1.29";
12486
13408
  async function createMcpServer(rootDir) {
12487
13409
  const server = new McpServer({
12488
13410
  name: "swarmvault",
@@ -13140,7 +14062,7 @@ import mime2 from "mime-types";
13140
14062
 
13141
14063
  // src/watch.ts
13142
14064
  import path23 from "path";
13143
- import process2 from "process";
14065
+ import process3 from "process";
13144
14066
  import chokidar from "chokidar";
13145
14067
  var MAX_BACKOFF_MS = 3e4;
13146
14068
  var BACKOFF_THRESHOLD = 3;
@@ -13405,7 +14327,7 @@ async function watchVault(rootDir, options = {}) {
13405
14327
  consecutiveFailures++;
13406
14328
  pending = true;
13407
14329
  if (consecutiveFailures >= CRITICAL_THRESHOLD) {
13408
- process2.stderr.write(
14330
+ process3.stderr.write(
13409
14331
  `[swarmvault watch] ${consecutiveFailures} consecutive failures. Check vault state. Continuing at max backoff.
13410
14332
  `
13411
14333
  );
@@ -13416,51 +14338,33 @@ async function watchVault(rootDir, options = {}) {
13416
14338
  }
13417
14339
  } finally {
13418
14340
  const finishedAt = /* @__PURE__ */ new Date();
13419
- await recordSession(rootDir, {
13420
- operation: "watch",
13421
- title: `Watch cycle for ${paths.inboxDir}${options.repo ? " and tracked repos" : ""}`,
13422
- startedAt: startedAt.toISOString(),
13423
- finishedAt: finishedAt.toISOString(),
13424
- success,
13425
- error,
13426
- changedPages,
13427
- lintFindingCount,
13428
- lines: [
13429
- `reasons=${runReasons.join(",") || "none"}`,
13430
- `imported=${importedCount}`,
13431
- `scanned=${scannedCount}`,
13432
- `attachments=${attachmentCount}`,
13433
- `repo_scanned=${repoScannedCount}`,
13434
- `repo_imported=${repoImportedCount}`,
13435
- `repo_updated=${repoUpdatedCount}`,
13436
- `repo_removed=${repoRemovedCount}`,
13437
- `lint=${lintFindingCount ?? 0}`
13438
- ]
13439
- });
13440
- await appendWatchRun(rootDir, {
13441
- startedAt: startedAt.toISOString(),
13442
- finishedAt: finishedAt.toISOString(),
13443
- durationMs: finishedAt.getTime() - startedAt.getTime(),
13444
- inputDir: paths.inboxDir,
13445
- reasons: runReasons,
13446
- importedCount: importedCount + repoImportedCount + repoUpdatedCount,
13447
- scannedCount: scannedCount + repoScannedCount,
13448
- attachmentCount,
13449
- changedPages,
13450
- repoImportedCount,
13451
- repoUpdatedCount,
13452
- repoRemovedCount,
13453
- repoScannedCount,
13454
- pendingSemanticRefreshCount,
13455
- pendingSemanticRefreshPaths,
13456
- lintFindingCount,
13457
- success,
13458
- error
13459
- });
13460
- await writeWatchStatusArtifact(rootDir, {
13461
- generatedAt: finishedAt.toISOString(),
13462
- watchedRepoRoots,
13463
- lastRun: {
14341
+ try {
14342
+ await recordSession(rootDir, {
14343
+ operation: "watch",
14344
+ title: `Watch cycle for ${paths.inboxDir}${options.repo ? " and tracked repos" : ""}`,
14345
+ startedAt: startedAt.toISOString(),
14346
+ finishedAt: finishedAt.toISOString(),
14347
+ success,
14348
+ error,
14349
+ changedPages,
14350
+ lintFindingCount,
14351
+ lines: [
14352
+ `reasons=${runReasons.join(",") || "none"}`,
14353
+ `imported=${importedCount}`,
14354
+ `scanned=${scannedCount}`,
14355
+ `attachments=${attachmentCount}`,
14356
+ `repo_scanned=${repoScannedCount}`,
14357
+ `repo_imported=${repoImportedCount}`,
14358
+ `repo_updated=${repoUpdatedCount}`,
14359
+ `repo_removed=${repoRemovedCount}`,
14360
+ `lint=${lintFindingCount ?? 0}`
14361
+ ]
14362
+ });
14363
+ } catch {
14364
+ process3.stderr.write("[swarmvault watch] Failed to record session log.\n");
14365
+ }
14366
+ try {
14367
+ await appendWatchRun(rootDir, {
13464
14368
  startedAt: startedAt.toISOString(),
13465
14369
  finishedAt: finishedAt.toISOString(),
13466
14370
  durationMs: finishedAt.getTime() - startedAt.getTime(),
@@ -13479,9 +14383,39 @@ async function watchVault(rootDir, options = {}) {
13479
14383
  lintFindingCount,
13480
14384
  success,
13481
14385
  error
13482
- },
13483
- pendingSemanticRefresh: await readPendingSemanticRefresh(rootDir)
13484
- });
14386
+ });
14387
+ } catch {
14388
+ process3.stderr.write("[swarmvault watch] Failed to append watch run.\n");
14389
+ }
14390
+ try {
14391
+ await writeWatchStatusArtifact(rootDir, {
14392
+ generatedAt: finishedAt.toISOString(),
14393
+ watchedRepoRoots,
14394
+ lastRun: {
14395
+ startedAt: startedAt.toISOString(),
14396
+ finishedAt: finishedAt.toISOString(),
14397
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
14398
+ inputDir: paths.inboxDir,
14399
+ reasons: runReasons,
14400
+ importedCount: importedCount + repoImportedCount + repoUpdatedCount,
14401
+ scannedCount: scannedCount + repoScannedCount,
14402
+ attachmentCount,
14403
+ changedPages,
14404
+ repoImportedCount,
14405
+ repoUpdatedCount,
14406
+ repoRemovedCount,
14407
+ repoScannedCount,
14408
+ pendingSemanticRefreshCount,
14409
+ pendingSemanticRefreshPaths,
14410
+ lintFindingCount,
14411
+ success,
14412
+ error
14413
+ },
14414
+ pendingSemanticRefresh: await readPendingSemanticRefresh(rootDir)
14415
+ });
14416
+ } catch {
14417
+ process3.stderr.write("[swarmvault watch] Failed to write watch status artifact.\n");
14418
+ }
13485
14419
  running = false;
13486
14420
  if (pending && !closed) {
13487
14421
  schedule("queued");