@swarmvaultai/engine 0.1.28 → 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,7 +21,7 @@ import {
21
21
  uniqueBy,
22
22
  writeFileIfChanged,
23
23
  writeJsonFile
24
- } from "./chunk-EXD4RWT3.js";
24
+ } from "./chunk-IHMJCCXR.js";
25
25
 
26
26
  // src/agents.ts
27
27
  import crypto from "crypto";
@@ -32,8 +32,8 @@ var managedStart = "<!-- swarmvault:managed:start -->";
32
32
  var managedEnd = "<!-- swarmvault:managed:end -->";
33
33
  var legacyManagedStart = "<!-- vault:managed:start -->";
34
34
  var legacyManagedEnd = "<!-- vault:managed:end -->";
35
- var claudeHookMatcher = "Glob|Grep";
36
- 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";
35
+ var claudeSearchMatcher = "Glob|Grep";
36
+ var claudeSessionMatchers = ["startup", "resume", "clear", "compact"];
37
37
  var geminiSessionMatcher = "startup";
38
38
  var geminiSearchMatcher = "glob|grep|search|find";
39
39
  var copilotHookVersion = 1;
@@ -89,6 +89,8 @@ function primaryTargetPathForAgent(rootDir, agent) {
89
89
  }
90
90
  function hookScriptPathForAgent(rootDir, agent) {
91
91
  switch (agent) {
92
+ case "claude":
93
+ return path.join(rootDir, ".claude", "hooks", "swarmvault-graph-first.js");
92
94
  case "opencode":
93
95
  return path.join(rootDir, ".opencode", "plugins", "swarmvault-graph-first.js");
94
96
  case "gemini":
@@ -177,25 +179,106 @@ async function readJsonWithWarnings(filePath, fallback, label) {
177
179
  }
178
180
  async function installClaudeHook(rootDir) {
179
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);
180
184
  await ensureDir(path.dirname(settingsPath));
181
185
  const { data: settings, warnings } = await readJsonWithWarnings(settingsPath, {}, ".claude/settings.json");
182
186
  if (warnings.length > 0 && await fileExists(settingsPath)) {
183
187
  return { path: settingsPath, warnings };
184
188
  }
185
189
  const hooks = settings.hooks ?? {};
190
+ const sessionStart = hooks.SessionStart ?? [];
186
191
  const preToolUse = hooks.PreToolUse ?? [];
187
- const exists = preToolUse.some((entry) => entry.matcher === claudeHookMatcher && JSON.stringify(entry).includes("swarmvault:"));
188
- if (!exists) {
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"))) {
189
203
  preToolUse.push({
190
- matcher: claudeHookMatcher,
191
- hooks: [{ type: "command", command: claudeHookCommand }]
204
+ matcher: claudeSearchMatcher,
205
+ hooks: [{ type: "command", command: preToolUseCommand }]
192
206
  });
193
207
  }
194
- settings.hooks = { ...hooks, PreToolUse: preToolUse };
208
+ settings.hooks = { ...hooks, SessionStart: sessionStart, PreToolUse: preToolUse };
195
209
  await fs.writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
196
210
  `, "utf8");
197
211
  return { path: settingsPath, warnings: [] };
198
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
+ }
199
282
  function markerStateSnippet(agentKey) {
200
283
  return `
201
284
  function markerState(cwd) {
@@ -237,7 +320,9 @@ function collectCandidatePaths(node, acc = []) {
237
320
  for (const [key, value] of Object.entries(node)) {
238
321
  if (["path", "filePath", "file_path", "paths", "target", "targets"].includes(key)) {
239
322
  collectCandidatePaths(value, acc);
323
+ continue;
240
324
  }
325
+ collectCandidatePaths(value, acc);
241
326
  }
242
327
  return acc;
243
328
  }
@@ -674,8 +759,38 @@ var NODE_COLORS = {
674
759
  function xmlEscape(value) {
675
760
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
676
761
  }
677
- function cypherEscape(value) {
678
- 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}'`;
679
794
  }
680
795
  function relationType(relation) {
681
796
  const normalized = relation.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
@@ -951,33 +1066,33 @@ function renderCypher(graph) {
951
1066
  for (const node of [...graph.nodes].sort((left, right) => left.id.localeCompare(right.id))) {
952
1067
  const page = node.pageId ? pageById2.get(node.pageId) : void 0;
953
1068
  const props = [
954
- `id: '${cypherEscape(node.id)}'`,
955
- `label: '${cypherEscape(node.label)}'`,
956
- `type: '${cypherEscape(node.type)}'`,
957
- `sourceIds: '${cypherEscape(JSON.stringify(node.sourceIds))}'`,
958
- `projectIds: '${cypherEscape(JSON.stringify(node.projectIds))}'`,
959
- node.pageId ? `pageId: '${cypherEscape(node.pageId)}'` : "",
960
- page?.path ? `pagePath: '${cypherEscape(page.path)}'` : "",
961
- node.language ? `language: '${cypherEscape(node.language)}'` : "",
962
- node.symbolKind ? `symbolKind: '${cypherEscape(node.symbolKind)}'` : "",
963
- 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)}` : "",
964
1079
  node.degree !== void 0 ? `degree: ${node.degree}` : "",
965
1080
  node.bridgeScore !== void 0 ? `bridgeScore: ${node.bridgeScore}` : "",
966
1081
  node.isGodNode !== void 0 ? `isGodNode: ${node.isGodNode}` : ""
967
1082
  ].filter(Boolean).join(", ");
968
- 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} };`);
969
1084
  }
970
1085
  lines.push("");
971
1086
  for (const hyperedge of [...graph.hyperedges ?? []].sort((left, right) => left.id.localeCompare(right.id))) {
972
1087
  const hyperedgeNodeId = exportHyperedgeNodeId(hyperedge);
973
1088
  lines.push(
974
- `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(
975
1090
  hyperedge.label
976
- )}', type: 'hyperedge', relation: '${cypherEscape(hyperedge.relation)}', evidenceClass: '${cypherEscape(
1091
+ )}, type: ${cypherStringLiteral("hyperedge")}, relation: ${cypherStringLiteral(hyperedge.relation)}, evidenceClass: ${cypherStringLiteral(
977
1092
  hyperedge.evidenceClass
978
- )}', confidence: ${hyperedge.confidence}, sourcePageIds: '${cypherEscape(JSON.stringify(hyperedge.sourcePageIds))}', why: '${cypherEscape(
1093
+ )}, confidence: ${hyperedge.confidence}, sourcePageIds: ${cypherStringLiteral(JSON.stringify(hyperedge.sourcePageIds))}, why: ${cypherStringLiteral(
979
1094
  hyperedge.why
980
- )}' };`
1095
+ )} };`
981
1096
  );
982
1097
  }
983
1098
  if ((graph.hyperedges ?? []).length) {
@@ -987,22 +1102,22 @@ function renderCypher(graph) {
987
1102
  const hyperedgeNodeId = exportHyperedgeNodeId(hyperedge);
988
1103
  for (const nodeId of hyperedge.nodeIds) {
989
1104
  lines.push(
990
- `MATCH (h:SwarmNode {id: '${cypherEscape(hyperedgeNodeId)}'}), (n:SwarmNode {id: '${cypherEscape(nodeId)}'})`,
991
- `MERGE (h)-[r:GROUP_MEMBER {id: '${cypherEscape(`member:${hyperedge.id}:${nodeId}`)}'}]->(n)`,
992
- `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(
993
1108
  hyperedge.evidenceClass
994
- )}', confidence: ${hyperedge.confidence}, provenance: '${cypherEscape(JSON.stringify(hyperedge.sourcePageIds))}' };`
1109
+ )}, confidence: ${hyperedge.confidence}, provenance: ${cypherStringLiteral(JSON.stringify(hyperedge.sourcePageIds))} };`
995
1110
  );
996
1111
  }
997
1112
  }
998
1113
  lines.push("");
999
1114
  for (const edge of [...graph.edges].sort((left, right) => left.id.localeCompare(right.id))) {
1000
1115
  lines.push(
1001
- `MATCH (a:SwarmNode {id: '${cypherEscape(edge.source)}'}), (b:SwarmNode {id: '${cypherEscape(edge.target)}'})`,
1002
- `MERGE (a)-[r:${relationType(edge.relation)} {id: '${cypherEscape(edge.id)}'}]->(b)`,
1003
- `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(
1004
1119
  edge.evidenceClass
1005
- )}', 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))}` : ""} };`
1006
1121
  );
1007
1122
  }
1008
1123
  lines.push("");
@@ -1173,7 +1288,7 @@ import { pathToFileURL } from "url";
1173
1288
  import { Readability } from "@mozilla/readability";
1174
1289
  import matter3 from "gray-matter";
1175
1290
  import ignore from "ignore";
1176
- import { JSDOM } from "jsdom";
1291
+ import { JSDOM as JSDOM2 } from "jsdom";
1177
1292
  import mime from "mime-types";
1178
1293
  import TurndownService from "turndown";
1179
1294
 
@@ -1298,6 +1413,55 @@ function collectCallNamesFromText(text, availableNames, selfName) {
1298
1413
  }
1299
1414
  return uniqueBy(names, (name) => name);
1300
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
+ }
1301
1465
  function finalizeCodeAnalysis(manifest, language, imports, draftSymbols, exportLabels, diagnostics, metadata) {
1302
1466
  const topLevelNames = new Set(draftSymbols.map((symbol) => symbol.name));
1303
1467
  for (const symbol of draftSymbols) {
@@ -1784,7 +1948,9 @@ function goCodeAnalysis(manifest, rootNode, diagnostics) {
1784
1948
  if (!name) {
1785
1949
  continue;
1786
1950
  }
1787
- 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");
1788
1954
  const symbolName = receiverType ? `${receiverType}.${name}` : name;
1789
1955
  const exported = exportedByCapitalization(name);
1790
1956
  draftSymbols.push({
@@ -1792,10 +1958,10 @@ function goCodeAnalysis(manifest, rootNode, diagnostics) {
1792
1958
  kind: "function",
1793
1959
  signature: singleLineSignature(child.text),
1794
1960
  exported,
1795
- callNames: [],
1961
+ callNames: goCallNamesFromBody(bodyNode, receiver),
1796
1962
  extendsNames: [],
1797
1963
  implementsNames: [],
1798
- bodyText: nodeText(child.childForFieldName("body"))
1964
+ bodyText: nodeText(bodyNode)
1799
1965
  });
1800
1966
  if (exported) {
1801
1967
  exportLabels.push(symbolName);
@@ -3325,6 +3491,8 @@ async function analyzeCodeSource(manifest, extractedText, schemaHash) {
3325
3491
  import fs6 from "fs/promises";
3326
3492
  import os from "os";
3327
3493
  import path6 from "path";
3494
+ import { strFromU8, unzipSync } from "fflate";
3495
+ import { JSDOM } from "jsdom";
3328
3496
  import { z } from "zod";
3329
3497
  var imageVisionExtractionSchema = z.object({
3330
3498
  title: z.string().min(1).nullable().optional(),
@@ -3501,6 +3669,49 @@ function normalizePdfMetadata(raw) {
3501
3669
  }
3502
3670
  return Object.keys(metadata).length ? metadata : void 0;
3503
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
+ }
3504
3715
  async function extractPdfText(input) {
3505
3716
  try {
3506
3717
  const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
@@ -3548,6 +3759,35 @@ async function extractPdfText(input) {
3548
3759
  };
3549
3760
  }
3550
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
+ }
3551
3791
 
3552
3792
  // src/logs.ts
3553
3793
  import fs7 from "fs/promises";
@@ -3858,6 +4098,9 @@ function inferKind(mimeType, filePath) {
3858
4098
  if (mimeType === "application/pdf" || filePath.toLowerCase().endsWith(".pdf")) {
3859
4099
  return "pdf";
3860
4100
  }
4101
+ if (mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || filePath.toLowerCase().endsWith(".docx")) {
4102
+ return "docx";
4103
+ }
3861
4104
  if (mimeType.startsWith("image/")) {
3862
4105
  return "image";
3863
4106
  }
@@ -4036,7 +4279,7 @@ async function fetchResolvedText(url) {
4036
4279
  };
4037
4280
  }
4038
4281
  function domTextFromHtml(html, baseUrl) {
4039
- const dom = new JSDOM(`<body>${html}</body>`, { url: baseUrl });
4282
+ const dom = new JSDOM2(`<body>${html}</body>`, { url: baseUrl });
4040
4283
  return normalizeWhitespace(dom.window.document.body.textContent ?? "");
4041
4284
  }
4042
4285
  async function captureArxivMarkdown(input, options) {
@@ -4046,7 +4289,7 @@ async function captureArxivMarkdown(input, options) {
4046
4289
  }
4047
4290
  const normalizedUrl = `https://arxiv.org/abs/${arxivId}`;
4048
4291
  const html = await fetchText(normalizedUrl);
4049
- const dom = new JSDOM(html, { url: normalizedUrl });
4292
+ const dom = new JSDOM2(html, { url: normalizedUrl });
4050
4293
  const document = dom.window.document;
4051
4294
  const metaTitle = document.querySelector('meta[name="citation_title"]')?.getAttribute("content")?.trim();
4052
4295
  const headingTitle = document.querySelector("h1.title")?.textContent?.trim();
@@ -4155,7 +4398,7 @@ async function captureArticleMarkdown(rootDir, input, options, extra = { sourceT
4155
4398
  if (!resolved.contentType.includes("html")) {
4156
4399
  throw new Error(`Unsupported article content type: ${resolved.contentType}`);
4157
4400
  }
4158
- const dom = new JSDOM(resolved.text, { url: resolved.finalUrl });
4401
+ const dom = new JSDOM2(resolved.text, { url: resolved.finalUrl });
4159
4402
  const document = dom.window.document;
4160
4403
  const canonicalHref = document.querySelector('link[rel="canonical"]')?.getAttribute("href")?.trim();
4161
4404
  const canonicalUrl = canonicalHref ? normalizeOriginUrl(new URL(canonicalHref, resolved.finalUrl).toString()) : resolved.finalUrl;
@@ -4274,6 +4517,22 @@ function extractMarkdownReferences(content) {
4274
4517
  }
4275
4518
  return references;
4276
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
+ }
4277
4536
  function normalizeRemoteReference(value, baseUrl) {
4278
4537
  const trimmed = value.trim().replace(/^<|>$/g, "");
4279
4538
  const [withoutTitle] = trimmed.split(/\s+(?=(?:[^"]*"[^"]*")*[^"]*$)/, 1);
@@ -4309,7 +4568,7 @@ function extractMarkdownImageReferences(content, baseUrl) {
4309
4568
  return references;
4310
4569
  }
4311
4570
  async function convertHtmlToMarkdown(html, url) {
4312
- const dom = new JSDOM(html, { url });
4571
+ const dom = new JSDOM2(html, { url });
4313
4572
  const article = new Readability(dom.window.document).parse();
4314
4573
  const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
4315
4574
  const body = article?.content ?? dom.window.document.body.innerHTML;
@@ -4512,7 +4771,7 @@ async function collectRemoteImageAttachments(assetUrls, options) {
4512
4771
  return { attachments, skippedCount };
4513
4772
  }
4514
4773
  function extractHtmlImageReferences(html, baseUrl) {
4515
- const dom = new JSDOM(html, { url: baseUrl });
4774
+ const dom = new JSDOM2(html, { url: baseUrl });
4516
4775
  const document = dom.window.document;
4517
4776
  const references = [];
4518
4777
  for (const image of [...document.querySelectorAll("img[src]")]) {
@@ -4528,7 +4787,7 @@ function extractHtmlImageReferences(html, baseUrl) {
4528
4787
  return references;
4529
4788
  }
4530
4789
  function rewriteHtmlImageReferences(html, baseUrl, replacements) {
4531
- const dom = new JSDOM(html, { url: baseUrl });
4790
+ const dom = new JSDOM2(html, { url: baseUrl });
4532
4791
  const document = dom.window.document;
4533
4792
  for (const image of [...document.querySelectorAll("img[src]")]) {
4534
4793
  const src = image.getAttribute("src");
@@ -4543,6 +4802,22 @@ function rewriteHtmlImageReferences(html, baseUrl, replacements) {
4543
4802
  }
4544
4803
  return dom.serialize();
4545
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
+ }
4546
4821
  function rewriteMarkdownImageReferences(content, baseUrl, replacements) {
4547
4822
  return content.replace(/(!\[[^\]]*]\()([^)]+)(\))/g, (fullMatch, prefix, target, suffix) => {
4548
4823
  const normalized = normalizeRemoteReference(target, baseUrl);
@@ -4683,7 +4958,7 @@ function preparedMatchesManifest(manifest, prepared, contentHash) {
4683
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;
4684
4959
  }
4685
4960
  function shouldDeferWatchSemanticRefresh(sourceKind) {
4686
- 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";
4687
4962
  }
4688
4963
  function pendingSemanticRefreshId(changeType, repoRoot, relativePath) {
4689
4964
  return `pending:${changeType}:${sha256(`${toPosix(repoRoot)}:${relativePath}`).slice(0, 12)}`;
@@ -4947,6 +5222,12 @@ async function prepareFileInput(rootDir, absoluteInput, repoRoot, sourceClass) {
4947
5222
  const extracted = await extractPdfText({ mimeType, bytes: payloadBytes });
4948
5223
  extractedText = extracted.extractedText;
4949
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;
4950
5231
  } else if (sourceKind === "image") {
4951
5232
  title = path10.basename(absoluteInput, path10.extname(absoluteInput));
4952
5233
  const extracted = await extractImageWithVision(rootDir, {
@@ -5064,6 +5345,11 @@ async function prepareUrlInput(rootDir, input, options) {
5064
5345
  const extracted = await extractPdfText({ mimeType, bytes: payloadBytes });
5065
5346
  extractedText = extracted.extractedText;
5066
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;
5067
5353
  } else if (sourceKind === "image") {
5068
5354
  const extracted = await extractImageWithVision(rootDir, {
5069
5355
  title,
@@ -5097,11 +5383,11 @@ async function collectInboxAttachmentRefs(inputDir, files) {
5097
5383
  for (const absolutePath of files) {
5098
5384
  const mimeType = guessMimeType(absolutePath);
5099
5385
  const sourceKind = inferKind(mimeType, absolutePath);
5100
- if (sourceKind !== "markdown") {
5386
+ if (sourceKind !== "markdown" && sourceKind !== "html") {
5101
5387
  continue;
5102
5388
  }
5103
5389
  const content = await fs9.readFile(absolutePath, "utf8");
5104
- const refs = extractMarkdownReferences(content);
5390
+ const refs = sourceKind === "html" ? extractHtmlLocalReferences(content, pathToFileURL(absolutePath).toString()) : extractMarkdownReferences(content);
5105
5391
  if (!refs.length) {
5106
5392
  continue;
5107
5393
  }
@@ -5179,8 +5465,50 @@ async function prepareInboxMarkdownInput(absolutePath, attachmentRefs) {
5179
5465
  contentHash
5180
5466
  };
5181
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
+ }
5182
5510
  function isSupportedInboxKind(sourceKind) {
5183
- return ["markdown", "text", "html", "pdf", "image"].includes(sourceKind);
5511
+ return ["markdown", "text", "html", "pdf", "docx", "image"].includes(sourceKind);
5184
5512
  }
5185
5513
  async function ingestInput(rootDir, input, options) {
5186
5514
  const { paths } = await initWorkspace(rootDir);
@@ -5340,7 +5668,7 @@ async function importInbox(rootDir, inputDir) {
5340
5668
  skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: `unsupported_kind:${sourceKind}` });
5341
5669
  continue;
5342
5670
  }
5343
- 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);
5344
5672
  const result = await persistPreparedInput(rootDir, prepared, paths);
5345
5673
  if (!result.isNew) {
5346
5674
  skipped.push({ path: toPosix(path10.relative(rootDir, absolutePath)), reason: "duplicate_content" });
@@ -6614,8 +6942,14 @@ async function loadPageContents(rootDir, graph) {
6614
6942
  await Promise.all(
6615
6943
  graph.pages.map(async (page) => {
6616
6944
  const absolutePath = path16.join(paths.wikiDir, page.path);
6617
- const content = await fs12.readFile(absolutePath, "utf8").catch(() => "");
6618
- 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
+ }
6619
6953
  })
6620
6954
  );
6621
6955
  return contents;
@@ -9713,7 +10047,7 @@ async function resolveImageGenerationProvider(rootDir) {
9713
10047
  if (!providerConfig) {
9714
10048
  throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
9715
10049
  }
9716
- const { createProvider: createProvider2 } = await import("./registry-KLO5YIHP.js");
10050
+ const { createProvider: createProvider2 } = await import("./registry-5SYH3Y3U.js");
9717
10051
  return createProvider2(preferredProviderId, providerConfig, rootDir);
9718
10052
  }
9719
10053
  async function generateOutputArtifacts(rootDir, input) {
@@ -10274,8 +10608,64 @@ function deriveGraphMetrics(nodes, edges) {
10274
10608
  function resetGraphNodeMetrics(nodes) {
10275
10609
  return nodes.map(({ communityId: _communityId, degree: _degree, bridgeScore: _bridgeScore, isGodNode: _isGodNode, ...node }) => node);
10276
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
+ }
10277
10666
  function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
10278
10667
  const manifestsById = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
10668
+ const goPackageSymbolLookups = buildGoPackageSymbolLookups(analyses, manifestsById);
10279
10669
  const sourceNodes = manifests.map((manifest) => ({
10280
10670
  id: `source:${manifest.sourceId}`,
10281
10671
  type: "source",
@@ -10422,6 +10812,10 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
10422
10812
  }
10423
10813
  }
10424
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);
10425
10819
  for (const rationale of analysis.rationales) {
10426
10820
  const targetSymbolId = rationale.symbolName ? symbolIdsByName.get(rationale.symbolName) : void 0;
10427
10821
  const targetId = targetSymbolId ?? moduleId;
@@ -10474,9 +10868,31 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
10474
10868
  }
10475
10869
  }
10476
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
+ }
10477
10893
  for (const symbol of analysis.code.symbols) {
10478
10894
  for (const targetName of symbol.calls) {
10479
- const targetId = symbolIdsByName.get(targetName);
10895
+ const targetId = resolveLocalSymbolId(targetName);
10480
10896
  if (!targetId || targetId === symbol.id) {
10481
10897
  continue;
10482
10898
  }
@@ -10492,7 +10908,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
10492
10908
  });
10493
10909
  }
10494
10910
  for (const targetName of symbol.extends) {
10495
- const targetId = symbolIdsByName.get(targetName) ?? importedSymbolIdsByName.get(targetName);
10911
+ const targetId = resolveLocalSymbolId(targetName) ?? importedSymbolIdsByName.get(targetName);
10496
10912
  if (!targetId) {
10497
10913
  continue;
10498
10914
  }
@@ -10508,7 +10924,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
10508
10924
  });
10509
10925
  }
10510
10926
  for (const targetName of symbol.implements) {
10511
- const targetId = symbolIdsByName.get(targetName) ?? importedSymbolIdsByName.get(targetName);
10927
+ const targetId = resolveLocalSymbolId(targetName) ?? importedSymbolIdsByName.get(targetName);
10512
10928
  if (!targetId) {
10513
10929
  continue;
10514
10930
  }
@@ -12988,7 +13404,7 @@ async function bootstrapDemo(rootDir, input) {
12988
13404
  }
12989
13405
 
12990
13406
  // src/mcp.ts
12991
- var SERVER_VERSION = "0.1.28";
13407
+ var SERVER_VERSION = "0.1.29";
12992
13408
  async function createMcpServer(rootDir) {
12993
13409
  const server = new McpServer({
12994
13410
  name: "swarmvault",
@@ -13922,51 +14338,33 @@ async function watchVault(rootDir, options = {}) {
13922
14338
  }
13923
14339
  } finally {
13924
14340
  const finishedAt = /* @__PURE__ */ new Date();
13925
- await recordSession(rootDir, {
13926
- operation: "watch",
13927
- title: `Watch cycle for ${paths.inboxDir}${options.repo ? " and tracked repos" : ""}`,
13928
- startedAt: startedAt.toISOString(),
13929
- finishedAt: finishedAt.toISOString(),
13930
- success,
13931
- error,
13932
- changedPages,
13933
- lintFindingCount,
13934
- lines: [
13935
- `reasons=${runReasons.join(",") || "none"}`,
13936
- `imported=${importedCount}`,
13937
- `scanned=${scannedCount}`,
13938
- `attachments=${attachmentCount}`,
13939
- `repo_scanned=${repoScannedCount}`,
13940
- `repo_imported=${repoImportedCount}`,
13941
- `repo_updated=${repoUpdatedCount}`,
13942
- `repo_removed=${repoRemovedCount}`,
13943
- `lint=${lintFindingCount ?? 0}`
13944
- ]
13945
- });
13946
- await appendWatchRun(rootDir, {
13947
- startedAt: startedAt.toISOString(),
13948
- finishedAt: finishedAt.toISOString(),
13949
- durationMs: finishedAt.getTime() - startedAt.getTime(),
13950
- inputDir: paths.inboxDir,
13951
- reasons: runReasons,
13952
- importedCount: importedCount + repoImportedCount + repoUpdatedCount,
13953
- scannedCount: scannedCount + repoScannedCount,
13954
- attachmentCount,
13955
- changedPages,
13956
- repoImportedCount,
13957
- repoUpdatedCount,
13958
- repoRemovedCount,
13959
- repoScannedCount,
13960
- pendingSemanticRefreshCount,
13961
- pendingSemanticRefreshPaths,
13962
- lintFindingCount,
13963
- success,
13964
- error
13965
- });
13966
- await writeWatchStatusArtifact(rootDir, {
13967
- generatedAt: finishedAt.toISOString(),
13968
- watchedRepoRoots,
13969
- 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, {
13970
14368
  startedAt: startedAt.toISOString(),
13971
14369
  finishedAt: finishedAt.toISOString(),
13972
14370
  durationMs: finishedAt.getTime() - startedAt.getTime(),
@@ -13985,9 +14383,39 @@ async function watchVault(rootDir, options = {}) {
13985
14383
  lintFindingCount,
13986
14384
  success,
13987
14385
  error
13988
- },
13989
- pendingSemanticRefresh: await readPendingSemanticRefresh(rootDir)
13990
- });
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
+ }
13991
14419
  running = false;
13992
14420
  if (pending && !closed) {
13993
14421
  schedule("queued");