@swarmvaultai/engine 0.7.24 → 0.7.26

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
@@ -22,7 +22,12 @@ import {
22
22
  uniqueBy,
23
23
  writeFileIfChanged,
24
24
  writeJsonFile
25
- } from "./chunk-2CH2WWS4.js";
25
+ } from "./chunk-N56FAH4N.js";
26
+ import {
27
+ estimatePageTokens,
28
+ estimateTokens,
29
+ trimToTokenBudget
30
+ } from "./chunk-NAIERP4C.js";
26
31
 
27
32
  // src/agents.ts
28
33
  import crypto from "crypto";
@@ -450,9 +455,47 @@ async function installConfiguredAgents(rootDir) {
450
455
  );
451
456
  }
452
457
 
458
+ // src/auto-commit.ts
459
+ import { execFile } from "child_process";
460
+ import { promisify } from "util";
461
+ var execFileAsync = promisify(execFile);
462
+ async function git(rootDir, ...args) {
463
+ const { stdout } = await execFileAsync("git", args, { cwd: rootDir });
464
+ return stdout.trim();
465
+ }
466
+ async function isGitRepo(rootDir) {
467
+ try {
468
+ await git(rootDir, "rev-parse", "--is-inside-work-tree");
469
+ return true;
470
+ } catch {
471
+ return false;
472
+ }
473
+ }
474
+ async function autoCommitWikiChanges(rootDir, operation, detail, options) {
475
+ const { config, paths } = await loadVaultConfig(rootDir);
476
+ if (!options?.force && !config.autoCommit) {
477
+ return null;
478
+ }
479
+ if (!await isGitRepo(rootDir)) {
480
+ return null;
481
+ }
482
+ const wikiRelative = paths.wikiDir.replace(`${rootDir}/`, "");
483
+ const stateRelative = paths.stateDir.replace(`${rootDir}/`, "");
484
+ await git(rootDir, "add", wikiRelative, stateRelative).catch(() => {
485
+ });
486
+ const status = await git(rootDir, "diff", "--cached", "--stat");
487
+ if (!status) {
488
+ return null;
489
+ }
490
+ const message = detail ? `vault ${operation}: ${detail}` : `vault ${operation}`;
491
+ await git(rootDir, "commit", "-m", message);
492
+ return message;
493
+ }
494
+
453
495
  // src/graph-export.ts
454
496
  import fs2 from "fs/promises";
455
497
  import path2 from "path";
498
+ import matter from "gray-matter";
456
499
 
457
500
  // src/graph-interchange.ts
458
501
  function exportHyperedgeNodeId(hyperedge) {
@@ -587,6 +630,316 @@ function graphCounts(graph) {
587
630
  };
588
631
  }
589
632
 
633
+ // src/graph-report-html.ts
634
+ function htmlEscape(text) {
635
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
636
+ }
637
+ function nodeTypeColor(type) {
638
+ const colors = {
639
+ source: "#f59e0b",
640
+ module: "#fb7185",
641
+ symbol: "#8b5cf6",
642
+ rationale: "#14b8a6",
643
+ concept: "#0ea5e9",
644
+ entity: "#22c55e"
645
+ };
646
+ return colors[type] ?? "#94a3b8";
647
+ }
648
+ function renderGraphReportHtml(graph, report) {
649
+ const nodesByType = /* @__PURE__ */ new Map();
650
+ for (const node of graph.nodes) {
651
+ nodesByType.set(node.type, (nodesByType.get(node.type) ?? 0) + 1);
652
+ }
653
+ const edgesByRelation = /* @__PURE__ */ new Map();
654
+ for (const edge of graph.edges) {
655
+ edgesByRelation.set(edge.relation, (edgesByRelation.get(edge.relation) ?? 0) + 1);
656
+ }
657
+ const pagesByKind = /* @__PURE__ */ new Map();
658
+ for (const page of graph.pages) {
659
+ const list = pagesByKind.get(page.kind) ?? [];
660
+ list.push(page);
661
+ pagesByKind.set(page.kind, list);
662
+ }
663
+ const godNodes = (report?.godNodes ?? []).slice(0, 15);
664
+ const bridgeNodes = (report?.bridgeNodes ?? []).slice(0, 10);
665
+ const communities = graph.communities ?? [];
666
+ const warnings = report?.warnings ?? [];
667
+ const overview = report?.overview ?? {
668
+ nodes: graph.nodes.length,
669
+ edges: graph.edges.length,
670
+ pages: graph.pages.length,
671
+ communities: communities.length
672
+ };
673
+ const sortedEdgeRelations = [...edgesByRelation.entries()].sort((a, b) => b[1] - a[1]);
674
+ const sortedNodeTypes = [...nodesByType.entries()].sort((a, b) => b[1] - a[1]);
675
+ const sortedCommunities2 = [...communities].sort((a, b) => b.nodeIds.length - a.nodeIds.length).slice(0, 20);
676
+ return `<!DOCTYPE html>
677
+ <html lang="en">
678
+ <head>
679
+ <meta charset="UTF-8">
680
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
681
+ <title>SwarmVault Graph Report</title>
682
+ <style>
683
+ :root {
684
+ --bg: #0f172a;
685
+ --surface: #1e293b;
686
+ --surface2: #334155;
687
+ --text: #e2e8f0;
688
+ --muted: #94a3b8;
689
+ --accent: #0ea5e9;
690
+ --accent2: #8b5cf6;
691
+ --border: #475569;
692
+ --success: #22c55e;
693
+ --warning: #f59e0b;
694
+ --danger: #ef4444;
695
+ }
696
+ * { box-sizing: border-box; margin: 0; padding: 0; }
697
+ body {
698
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
699
+ background: var(--bg);
700
+ color: var(--text);
701
+ line-height: 1.6;
702
+ padding: 2rem;
703
+ max-width: 1200px;
704
+ margin: 0 auto;
705
+ }
706
+ h1 {
707
+ font-size: 1.75rem;
708
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
709
+ -webkit-background-clip: text;
710
+ -webkit-text-fill-color: transparent;
711
+ margin-bottom: 0.25rem;
712
+ }
713
+ .subtitle { color: var(--muted); font-size: 0.85rem; margin-bottom: 2rem; }
714
+ h2 {
715
+ font-size: 1.15rem;
716
+ color: var(--text);
717
+ border-bottom: 1px solid var(--border);
718
+ padding-bottom: 0.5rem;
719
+ margin: 2rem 0 1rem;
720
+ }
721
+ .stats-grid {
722
+ display: grid;
723
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
724
+ gap: 1rem;
725
+ margin-bottom: 1.5rem;
726
+ }
727
+ .stat-card {
728
+ background: var(--surface);
729
+ border: 1px solid var(--border);
730
+ border-radius: 8px;
731
+ padding: 1rem;
732
+ text-align: center;
733
+ }
734
+ .stat-card .value {
735
+ font-size: 1.75rem;
736
+ font-weight: 700;
737
+ color: var(--accent);
738
+ }
739
+ .stat-card .label {
740
+ font-size: 0.8rem;
741
+ color: var(--muted);
742
+ text-transform: uppercase;
743
+ letter-spacing: 0.05em;
744
+ }
745
+ .badge {
746
+ display: inline-block;
747
+ padding: 0.15rem 0.5rem;
748
+ border-radius: 9999px;
749
+ font-size: 0.7rem;
750
+ font-weight: 600;
751
+ color: #fff;
752
+ }
753
+ table {
754
+ width: 100%;
755
+ border-collapse: collapse;
756
+ margin-bottom: 1rem;
757
+ }
758
+ th, td {
759
+ text-align: left;
760
+ padding: 0.5rem 0.75rem;
761
+ border-bottom: 1px solid var(--border);
762
+ font-size: 0.85rem;
763
+ }
764
+ th {
765
+ color: var(--muted);
766
+ font-weight: 600;
767
+ font-size: 0.75rem;
768
+ text-transform: uppercase;
769
+ letter-spacing: 0.05em;
770
+ }
771
+ tr:hover { background: var(--surface); }
772
+ .bar-container {
773
+ display: flex;
774
+ align-items: center;
775
+ gap: 0.5rem;
776
+ }
777
+ .bar {
778
+ height: 8px;
779
+ border-radius: 4px;
780
+ background: var(--accent);
781
+ min-width: 2px;
782
+ }
783
+ .warning-list {
784
+ list-style: none;
785
+ padding: 0;
786
+ }
787
+ .warning-list li {
788
+ padding: 0.5rem 0.75rem;
789
+ margin-bottom: 0.5rem;
790
+ background: var(--surface);
791
+ border-left: 3px solid var(--warning);
792
+ border-radius: 0 4px 4px 0;
793
+ font-size: 0.85rem;
794
+ }
795
+ .page-group { margin-bottom: 1.5rem; }
796
+ .page-group-title {
797
+ font-size: 0.85rem;
798
+ font-weight: 600;
799
+ color: var(--accent);
800
+ text-transform: capitalize;
801
+ margin-bottom: 0.5rem;
802
+ }
803
+ .page-list {
804
+ display: grid;
805
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
806
+ gap: 0.5rem;
807
+ }
808
+ .page-item {
809
+ background: var(--surface);
810
+ border: 1px solid var(--border);
811
+ border-radius: 6px;
812
+ padding: 0.5rem 0.75rem;
813
+ font-size: 0.8rem;
814
+ overflow: hidden;
815
+ text-overflow: ellipsis;
816
+ white-space: nowrap;
817
+ }
818
+ .page-item .path { color: var(--muted); font-size: 0.7rem; }
819
+ .empty { color: var(--muted); font-style: italic; font-size: 0.85rem; }
820
+ input[type="text"] {
821
+ width: 100%;
822
+ padding: 0.5rem 0.75rem;
823
+ background: var(--surface);
824
+ border: 1px solid var(--border);
825
+ border-radius: 6px;
826
+ color: var(--text);
827
+ font-size: 0.85rem;
828
+ margin-bottom: 1rem;
829
+ outline: none;
830
+ }
831
+ input[type="text"]:focus { border-color: var(--accent); }
832
+ .section { margin-bottom: 1rem; }
833
+ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.75rem; text-align: center; }
834
+ </style>
835
+ </head>
836
+ <body>
837
+ <h1>SwarmVault Graph Report</h1>
838
+ <p class="subtitle">Generated ${htmlEscape(report?.generatedAt ?? graph.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString())}</p>
839
+
840
+ <div class="stats-grid">
841
+ <div class="stat-card"><div class="value">${overview.nodes}</div><div class="label">Nodes</div></div>
842
+ <div class="stat-card"><div class="value">${overview.edges}</div><div class="label">Edges</div></div>
843
+ <div class="stat-card"><div class="value">${overview.pages}</div><div class="label">Pages</div></div>
844
+ <div class="stat-card"><div class="value">${overview.communities}</div><div class="label">Communities</div></div>
845
+ <div class="stat-card"><div class="value">${graph.sources.length}</div><div class="label">Sources</div></div>
846
+ <div class="stat-card"><div class="value">${(graph.hyperedges ?? []).length}</div><div class="label">Hyperedges</div></div>
847
+ </div>
848
+
849
+ <h2>Node Types</h2>
850
+ <table>
851
+ <thead><tr><th>Type</th><th>Count</th><th></th></tr></thead>
852
+ <tbody>
853
+ ${sortedNodeTypes.map(([type, count]) => {
854
+ const maxCount = sortedNodeTypes[0]?.[1] ?? 1;
855
+ const pct = Math.round(count / maxCount * 100);
856
+ return `<tr><td><span class="badge" style="background:${nodeTypeColor(type)}">${htmlEscape(type)}</span></td><td>${count}</td><td><div class="bar-container"><div class="bar" style="width:${pct}%;background:${nodeTypeColor(type)}"></div></div></td></tr>`;
857
+ }).join("\n")}
858
+ </tbody>
859
+ </table>
860
+
861
+ <h2>Edge Relations</h2>
862
+ <table>
863
+ <thead><tr><th>Relation</th><th>Count</th><th></th></tr></thead>
864
+ <tbody>
865
+ ${sortedEdgeRelations.map(([relation, count]) => {
866
+ const maxCount = sortedEdgeRelations[0]?.[1] ?? 1;
867
+ const pct = Math.round(count / maxCount * 100);
868
+ return `<tr><td>${htmlEscape(relation)}</td><td>${count}</td><td><div class="bar-container"><div class="bar" style="width:${pct}%"></div></div></td></tr>`;
869
+ }).join("\n")}
870
+ </tbody>
871
+ </table>
872
+
873
+ ${godNodes.length ? `<h2>God Nodes (Highest Connectivity)</h2>
874
+ <table>
875
+ <thead><tr><th>Label</th><th>Degree</th><th>Bridge Score</th><th></th></tr></thead>
876
+ <tbody>
877
+ ${godNodes.map((node) => {
878
+ const maxDegree = godNodes[0]?.degree ?? 1;
879
+ const pct = Math.round((node.degree ?? 0) / maxDegree * 100);
880
+ return `<tr><td>${htmlEscape(node.label)}</td><td>${node.degree ?? 0}</td><td>${(node.bridgeScore ?? 0).toFixed(2)}</td><td><div class="bar-container"><div class="bar" style="width:${pct}%;background:var(--accent2)"></div></div></td></tr>`;
881
+ }).join("\n")}
882
+ </tbody>
883
+ </table>` : ""}
884
+
885
+ ${bridgeNodes.length ? `<h2>Bridge Nodes</h2>
886
+ <table>
887
+ <thead><tr><th>Label</th><th>Degree</th><th>Bridge Score</th></tr></thead>
888
+ <tbody>
889
+ ${bridgeNodes.map((node) => `<tr><td>${htmlEscape(node.label)}</td><td>${node.degree ?? 0}</td><td>${(node.bridgeScore ?? 0).toFixed(2)}</td></tr>`).join("\n")}
890
+ </tbody>
891
+ </table>` : ""}
892
+
893
+ ${sortedCommunities2.length ? `<h2>Communities</h2>
894
+ <table>
895
+ <thead><tr><th>Label</th><th>Nodes</th><th></th></tr></thead>
896
+ <tbody>
897
+ ${sortedCommunities2.map((c) => {
898
+ const maxSize = sortedCommunities2[0]?.nodeIds.length ?? 1;
899
+ const pct = Math.round(c.nodeIds.length / maxSize * 100);
900
+ return `<tr><td>${htmlEscape(c.label)}</td><td>${c.nodeIds.length}</td><td><div class="bar-container"><div class="bar" style="width:${pct}%;background:var(--success)"></div></div></td></tr>`;
901
+ }).join("\n")}
902
+ </tbody>
903
+ </table>` : ""}
904
+
905
+ ${warnings.length ? `<h2>Warnings</h2>
906
+ <ul class="warning-list">
907
+ ${warnings.map((w) => `<li>${htmlEscape(w)}</li>`).join("\n")}
908
+ </ul>` : ""}
909
+
910
+ <h2>Pages</h2>
911
+ <input type="text" id="page-filter" placeholder="Filter pages..." />
912
+ <div id="pages-container">
913
+ ${[...pagesByKind.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(
914
+ ([kind, pages]) => `<div class="page-group" data-kind="${htmlEscape(kind)}">
915
+ <div class="page-group-title">${htmlEscape(kind)} (${pages.length})</div>
916
+ <div class="page-list">
917
+ ${pages.sort((a, b) => a.title.localeCompare(b.title)).map(
918
+ (p) => `<div class="page-item" data-title="${htmlEscape(p.title.toLowerCase())}"><strong>${htmlEscape(p.title)}</strong><div class="path">${htmlEscape(p.path)}</div></div>`
919
+ ).join("\n ")}
920
+ </div>
921
+ </div>`
922
+ ).join("\n")}
923
+ </div>
924
+
925
+ <footer>Generated by SwarmVault &middot; ${graph.nodes.length} nodes &middot; ${graph.edges.length} edges &middot; ${graph.pages.length} pages</footer>
926
+
927
+ <script>
928
+ document.getElementById("page-filter").addEventListener("input", function(e) {
929
+ var query = e.target.value.toLowerCase();
930
+ document.querySelectorAll(".page-item").forEach(function(el) {
931
+ el.style.display = el.getAttribute("data-title").includes(query) ? "" : "none";
932
+ });
933
+ document.querySelectorAll(".page-group").forEach(function(group) {
934
+ var visible = group.querySelectorAll('.page-item[style=""], .page-item:not([style])').length;
935
+ group.style.display = visible > 0 || !query ? "" : "none";
936
+ });
937
+ });
938
+ </script>
939
+ </body>
940
+ </html>`;
941
+ }
942
+
590
943
  // src/graph-export.ts
591
944
  var NODE_COLORS = {
592
945
  source: "#f59e0b",
@@ -1220,6 +1573,14 @@ async function exportGraphFormat(rootDir, format, outputPath) {
1220
1573
  const resolvedPath = await writeGraphExport(outputPath, rendered);
1221
1574
  return { format, outputPath: resolvedPath };
1222
1575
  }
1576
+ async function exportGraphReportHtml(rootDir, outputPath) {
1577
+ const { paths } = await loadVaultConfig(rootDir);
1578
+ const graph = await loadGraph(rootDir);
1579
+ const report = await readJsonFile(path2.join(paths.wikiDir, "graph", "report.json"));
1580
+ const html = renderGraphReportHtml(graph, report);
1581
+ const resolvedPath = await writeGraphExport(outputPath, html);
1582
+ return { format: "report", outputPath: resolvedPath };
1583
+ }
1223
1584
  function safeFileName(label) {
1224
1585
  return label.replace(/[\\/*?:"<>|#^[\]]/g, "").replace(/\s+/g, " ").trim().slice(0, 200) || "unnamed";
1225
1586
  }
@@ -1233,14 +1594,27 @@ function deduplicateFileName(baseName, used) {
1233
1594
  used.add(name);
1234
1595
  return name;
1235
1596
  }
1236
- async function exportObsidianVault(rootDir, outputDir) {
1237
- const graph = await loadGraph(rootDir);
1238
- const resolvedOutputDir = path2.resolve(outputDir);
1239
- await ensureDir(resolvedOutputDir);
1240
- const nodesById = graphNodeById(graph);
1241
- const communities = sortedCommunities(graph);
1597
+ function typePluralDir(nodeType) {
1598
+ const map = {
1599
+ source: "sources",
1600
+ module: "modules",
1601
+ symbol: "symbols",
1602
+ concept: "concepts",
1603
+ entity: "entities",
1604
+ rationale: "rationales"
1605
+ };
1606
+ return map[nodeType] ?? "other";
1607
+ }
1608
+ function obsidianNodeSlug(node, pageById2) {
1609
+ if (node.pageId) {
1610
+ const page = pageById2.get(node.pageId);
1611
+ if (page) return path2.basename(page.path, ".md");
1612
+ }
1613
+ return slugify(node.label);
1614
+ }
1615
+ function buildAdjacency(edges) {
1242
1616
  const adjacency = /* @__PURE__ */ new Map();
1243
- for (const edge of graph.edges) {
1617
+ for (const edge of edges) {
1244
1618
  if (!adjacency.has(edge.source)) adjacency.set(edge.source, []);
1245
1619
  if (!adjacency.has(edge.target)) adjacency.set(edge.target, []);
1246
1620
  adjacency.get(edge.source).push({
@@ -1258,44 +1632,164 @@ async function exportObsidianVault(rootDir, outputDir) {
1258
1632
  direction: "in"
1259
1633
  });
1260
1634
  }
1261
- const usedFileNames = /* @__PURE__ */ new Set();
1262
- const nodeFileName = /* @__PURE__ */ new Map();
1263
- for (const node of [...graph.nodes].sort((a, b) => a.label.localeCompare(b.label) || a.id.localeCompare(b.id))) {
1264
- const name = deduplicateFileName(safeFileName(node.label), usedFileNames);
1265
- nodeFileName.set(node.id, name);
1635
+ return adjacency;
1636
+ }
1637
+ async function listFilesRecursive2(dir, base = "") {
1638
+ const results = [];
1639
+ let entries;
1640
+ try {
1641
+ entries = await fs2.readdir(dir, { withFileTypes: true });
1642
+ } catch {
1643
+ return results;
1266
1644
  }
1267
- let fileCount = 0;
1645
+ for (const entry of entries) {
1646
+ const rel = base ? `${base}/${entry.name}` : entry.name;
1647
+ if (entry.isDirectory()) {
1648
+ results.push(...await listFilesRecursive2(path2.join(dir, entry.name), rel));
1649
+ } else {
1650
+ results.push(rel);
1651
+ }
1652
+ }
1653
+ return results;
1654
+ }
1655
+ function connectionsSection(nodeIds, adjacency, nodesById, wikilinkTarget) {
1656
+ const seen = /* @__PURE__ */ new Set();
1657
+ const lines = [];
1658
+ for (const nodeId of nodeIds) {
1659
+ for (const entry of adjacency.get(nodeId) ?? []) {
1660
+ const key = `${entry.neighborId}:${entry.relation}`;
1661
+ if (seen.has(key)) continue;
1662
+ seen.add(key);
1663
+ const neighbor = nodesById.get(entry.neighborId);
1664
+ if (!neighbor) continue;
1665
+ const target = wikilinkTarget.get(entry.neighborId);
1666
+ if (!target) continue;
1667
+ lines.push(`- [[${target}|${neighbor.label}]] \u2014 ${entry.relation} (${entry.evidenceClass}, ${entry.confidence.toFixed(2)})`);
1668
+ }
1669
+ }
1670
+ return lines;
1671
+ }
1672
+ async function exportObsidianVault(rootDir, outputDir) {
1673
+ const graph = await loadGraph(rootDir);
1674
+ const { paths } = await loadVaultConfig(rootDir);
1675
+ const resolvedOutputDir = path2.resolve(outputDir);
1676
+ await ensureDir(resolvedOutputDir);
1677
+ const nodesById = graphNodeById(graph);
1678
+ const pageById2 = graphPageById(graph);
1679
+ const communities = sortedCommunities(graph);
1680
+ const adjacency = buildAdjacency(graph.edges);
1681
+ const nodesByPageId = /* @__PURE__ */ new Map();
1268
1682
  for (const node of graph.nodes) {
1269
- const fileName = nodeFileName.get(node.id);
1270
- const lines = [
1271
- "---",
1272
- `id: ${JSON.stringify(node.id)}`,
1273
- `type: ${JSON.stringify(node.type)}`,
1274
- `community: ${JSON.stringify(node.communityId ?? null)}`,
1275
- `confidence: ${node.confidence ?? null}`,
1276
- `source_class: ${JSON.stringify(node.sourceClass ?? null)}`,
1277
- `tags: ${JSON.stringify(node.tags ?? [])}`,
1278
- "---",
1279
- "",
1280
- `# ${node.label}`,
1281
- ""
1282
- ];
1283
- const neighbors = adjacency.get(node.id) ?? [];
1284
- if (neighbors.length > 0) {
1285
- lines.push("## Connections", "");
1286
- for (const neighbor of neighbors) {
1287
- const neighborNode = nodesById.get(neighbor.neighborId);
1288
- if (!neighborNode) continue;
1289
- const neighborFile = nodeFileName.get(neighbor.neighborId) ?? safeFileName(neighborNode.label);
1290
- lines.push(`- [[${neighborFile}]] \u2014 ${neighbor.relation} (${neighbor.evidenceClass}, ${neighbor.confidence.toFixed(2)})`);
1683
+ if (node.pageId && pageById2.has(node.pageId)) {
1684
+ const list = nodesByPageId.get(node.pageId) ?? [];
1685
+ list.push(node);
1686
+ nodesByPageId.set(node.pageId, list);
1687
+ }
1688
+ }
1689
+ const orphanNodes = graph.nodes.filter((node) => !node.pageId || !pageById2.has(node.pageId));
1690
+ const usedOrphanSlugs = /* @__PURE__ */ new Set();
1691
+ const orphanFilePath = /* @__PURE__ */ new Map();
1692
+ for (const node of [...orphanNodes].sort((a, b) => a.label.localeCompare(b.label) || a.id.localeCompare(b.id))) {
1693
+ const slug = deduplicateFileName(obsidianNodeSlug(node, pageById2), usedOrphanSlugs);
1694
+ orphanFilePath.set(node.id, `graph/nodes/${typePluralDir(node.type)}/${slug}.md`);
1695
+ }
1696
+ const wikilinkTarget = /* @__PURE__ */ new Map();
1697
+ for (const node of graph.nodes) {
1698
+ if (node.pageId) {
1699
+ const page = pageById2.get(node.pageId);
1700
+ if (page) {
1701
+ wikilinkTarget.set(node.id, page.path.replace(/\.md$/, ""));
1702
+ continue;
1291
1703
  }
1292
- lines.push("");
1293
1704
  }
1294
- await fs2.writeFile(path2.join(resolvedOutputDir, `${fileName}.md`), lines.join("\n"), "utf8");
1705
+ const orphanPath = orphanFilePath.get(node.id);
1706
+ if (orphanPath) {
1707
+ wikilinkTarget.set(node.id, orphanPath.replace(/\.md$/, ""));
1708
+ }
1709
+ }
1710
+ let fileCount = 0;
1711
+ const wikiFiles = await listFilesRecursive2(paths.wikiDir);
1712
+ const pageByPath = new Map(graph.pages.map((p) => [p.path, p]));
1713
+ for (const relPath of wikiFiles) {
1714
+ if (!relPath.endsWith(".md")) continue;
1715
+ const srcFile = path2.join(paths.wikiDir, relPath);
1716
+ const destFile = path2.join(resolvedOutputDir, relPath);
1717
+ await ensureDir(path2.dirname(destFile));
1718
+ let rawContent;
1719
+ try {
1720
+ rawContent = await fs2.readFile(srcFile, "utf8");
1721
+ } catch {
1722
+ continue;
1723
+ }
1724
+ const matchingPage = pageByPath.get(relPath);
1725
+ const pageNodes = matchingPage ? nodesByPageId.get(matchingPage.id) ?? [] : [];
1726
+ const parsed = matter(rawContent);
1727
+ const data = parsed.data;
1728
+ if (pageNodes.length > 0) {
1729
+ const primaryNode = pageNodes[0];
1730
+ if (primaryNode.communityId) {
1731
+ data.graph_community = primaryNode.communityId;
1732
+ }
1733
+ const title = data.title ?? "";
1734
+ const nodeAliases = pageNodes.map((n) => n.label).filter((label) => label.toLowerCase() !== title.toLowerCase());
1735
+ const existingAliases = Array.isArray(data.aliases) ? data.aliases : [];
1736
+ const mergedAliases = [.../* @__PURE__ */ new Set([...existingAliases, ...nodeAliases])];
1737
+ if (mergedAliases.length > 0) {
1738
+ data.aliases = mergedAliases;
1739
+ }
1740
+ }
1741
+ let outputContent = matter.stringify(parsed.content, data);
1742
+ if (pageNodes.length > 0) {
1743
+ const connLines = connectionsSection(
1744
+ pageNodes.map((n) => n.id),
1745
+ adjacency,
1746
+ nodesById,
1747
+ wikilinkTarget
1748
+ );
1749
+ if (connLines.length > 0) {
1750
+ outputContent = `${outputContent.trimEnd()}
1751
+
1752
+ ## Graph Connections
1753
+
1754
+ ${connLines.join("\n")}
1755
+ `;
1756
+ }
1757
+ }
1758
+ await fs2.writeFile(destFile, outputContent, "utf8");
1759
+ fileCount++;
1760
+ }
1761
+ for (const node of orphanNodes) {
1762
+ const relPath = orphanFilePath.get(node.id);
1763
+ const destFile = path2.join(resolvedOutputDir, relPath);
1764
+ await ensureDir(path2.dirname(destFile));
1765
+ const slug = path2.basename(relPath, ".md");
1766
+ const aliases = node.label !== slug ? [node.label] : [];
1767
+ const frontmatter = {
1768
+ id: node.id,
1769
+ type: node.type,
1770
+ community: node.communityId ?? null,
1771
+ confidence: node.confidence ?? null,
1772
+ source_class: node.sourceClass ?? null,
1773
+ tags: node.tags ?? []
1774
+ };
1775
+ if (aliases.length > 0) {
1776
+ frontmatter.aliases = aliases;
1777
+ }
1778
+ const lines = [`# ${node.label}`, ""];
1779
+ const connLines = connectionsSection([node.id], adjacency, nodesById, wikilinkTarget);
1780
+ if (connLines.length > 0) {
1781
+ lines.push("## Connections", "", ...connLines, "");
1782
+ }
1783
+ const content = matter.stringify(lines.join("\n"), frontmatter);
1784
+ await fs2.writeFile(destFile, content, "utf8");
1295
1785
  fileCount++;
1296
1786
  }
1297
1787
  const usedCommunityFileNames = /* @__PURE__ */ new Set();
1298
1788
  for (const community of communities) {
1789
+ const wikiCommunityPage = graph.pages.find(
1790
+ (p) => p.kind === "community_summary" && p.nodeIds.some((nid) => community.nodeIds.includes(nid))
1791
+ );
1792
+ if (wikiCommunityPage) continue;
1299
1793
  const memberNodes = community.nodeIds.map((id) => nodesById.get(id)).filter((n2) => Boolean(n2));
1300
1794
  const memberIdSet = new Set(community.nodeIds);
1301
1795
  let internalEdges = 0;
@@ -1314,35 +1808,102 @@ async function exportObsidianVault(rootDir, outputDir) {
1314
1808
  return nbNode && nbNode.communityId !== community.id;
1315
1809
  });
1316
1810
  });
1317
- const communityFileName = deduplicateFileName(`_Community_${safeFileName(community.label)}`, usedCommunityFileNames);
1318
- const lines = [
1319
- "---",
1320
- `id: ${JSON.stringify(community.id)}`,
1321
- `node_count: ${memberNodes.length}`,
1322
- `cohesion: ${cohesion.toFixed(4)}`,
1323
- "---",
1324
- "",
1325
- `# ${community.label}`,
1326
- "",
1327
- "## Members",
1328
- ""
1329
- ];
1811
+ const communitySlug = deduplicateFileName(safeFileName(community.label), usedCommunityFileNames);
1812
+ const destFile = path2.join(resolvedOutputDir, "graph", "communities", `${communitySlug}.md`);
1813
+ await ensureDir(path2.dirname(destFile));
1814
+ const lines = [`# ${community.label}`, "", "## Members", ""];
1330
1815
  for (const member of memberNodes) {
1331
- const memberFile = nodeFileName.get(member.id) ?? safeFileName(member.label);
1332
- lines.push(`- [[${memberFile}]]`);
1816
+ const target = wikilinkTarget.get(member.id);
1817
+ if (target) {
1818
+ lines.push(`- [[${target}|${member.label}]]`);
1819
+ } else {
1820
+ lines.push(`- ${member.label}`);
1821
+ }
1333
1822
  }
1334
1823
  lines.push("");
1335
1824
  if (bridgeNodes.length > 0) {
1336
1825
  lines.push("## Bridge Nodes", "");
1337
1826
  for (const bridge of bridgeNodes) {
1338
- const bridgeFile = nodeFileName.get(bridge.id) ?? safeFileName(bridge.label);
1339
- lines.push(`- [[${bridgeFile}]]`);
1827
+ const target = wikilinkTarget.get(bridge.id);
1828
+ if (target) {
1829
+ lines.push(`- [[${target}|${bridge.label}]]`);
1830
+ } else {
1831
+ lines.push(`- ${bridge.label}`);
1832
+ }
1340
1833
  }
1341
1834
  lines.push("");
1342
1835
  }
1343
- await fs2.writeFile(path2.join(resolvedOutputDir, `${communityFileName}.md`), lines.join("\n"), "utf8");
1836
+ const frontmatter = {
1837
+ id: community.id,
1838
+ node_count: memberNodes.length,
1839
+ cohesion: Number(cohesion.toFixed(4))
1840
+ };
1841
+ const content = matter.stringify(lines.join("\n"), frontmatter);
1842
+ await fs2.writeFile(destFile, content, "utf8");
1344
1843
  fileCount++;
1345
1844
  }
1845
+ const outputsAssetsDir = path2.join(paths.wikiDir, "outputs", "assets");
1846
+ try {
1847
+ const assetFiles = await listFilesRecursive2(outputsAssetsDir);
1848
+ for (const relAsset of assetFiles) {
1849
+ const src = path2.join(outputsAssetsDir, relAsset);
1850
+ const dest = path2.join(resolvedOutputDir, "outputs", "assets", relAsset);
1851
+ await ensureDir(path2.dirname(dest));
1852
+ await fs2.copyFile(src, dest);
1853
+ fileCount++;
1854
+ }
1855
+ } catch {
1856
+ }
1857
+ try {
1858
+ const rawAssetFiles = await listFilesRecursive2(paths.rawAssetsDir);
1859
+ for (const relAsset of rawAssetFiles) {
1860
+ const src = path2.join(paths.rawAssetsDir, relAsset);
1861
+ const dest = path2.join(resolvedOutputDir, "raw", "assets", relAsset);
1862
+ await ensureDir(path2.dirname(dest));
1863
+ await fs2.copyFile(src, dest);
1864
+ fileCount++;
1865
+ }
1866
+ } catch {
1867
+ }
1868
+ const obsidianDir = path2.join(resolvedOutputDir, ".obsidian");
1869
+ await ensureDir(obsidianDir);
1870
+ const projectIds = Object.keys(
1871
+ graph.pages.reduce(
1872
+ (acc, page) => {
1873
+ for (const pid of page.projectIds) acc[pid] = true;
1874
+ return acc;
1875
+ },
1876
+ {}
1877
+ )
1878
+ );
1879
+ const colorGroups = projectIds.map((pid, index) => ({
1880
+ query: `tag:#project/${pid}`,
1881
+ color: ["#0ea5e9", "#22c55e", "#f59e0b", "#8b5cf6", "#fb7185", "#14b8a6"][index % 6]
1882
+ }));
1883
+ await fs2.writeFile(
1884
+ path2.join(obsidianDir, "app.json"),
1885
+ JSON.stringify(
1886
+ { newFileLocation: "folder", newFileFolderPath: "outputs", attachmentFolderPath: "raw/assets", useMarkdownLinks: false },
1887
+ null,
1888
+ 2
1889
+ ),
1890
+ "utf8"
1891
+ );
1892
+ await fs2.writeFile(
1893
+ path2.join(obsidianDir, "core-plugins.json"),
1894
+ JSON.stringify(["file-explorer", "global-search", "graph", "backlink", "tag-pane", "page-preview", "outline"], null, 2),
1895
+ "utf8"
1896
+ );
1897
+ await fs2.writeFile(
1898
+ path2.join(obsidianDir, "graph.json"),
1899
+ JSON.stringify(
1900
+ { colorGroups, "collapse-filter": false, search: "", showTags: true, showAttachments: false, showOrphans: true },
1901
+ null,
1902
+ 2
1903
+ ),
1904
+ "utf8"
1905
+ );
1906
+ fileCount += 3;
1346
1907
  return { format: "obsidian", outputPath: resolvedOutputDir, fileCount };
1347
1908
  }
1348
1909
  async function exportObsidianCanvas(rootDir, outputPath) {
@@ -1463,7 +2024,7 @@ function nodeMap(graph) {
1463
2024
  function pageMap(graph) {
1464
2025
  return new Map(graph.pages.map((page) => [page.id, page]));
1465
2026
  }
1466
- function estimateTokens(text) {
2027
+ function estimateTokens2(text) {
1467
2028
  return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
1468
2029
  }
1469
2030
  function estimateCorpusWords(texts) {
@@ -1500,7 +2061,7 @@ function benchmarkQueryTokens(graph, queryResult, pageContentsById) {
1500
2061
  const target = nodesById.get(edge.target)?.label ?? edge.target;
1501
2062
  lines.push(`EDGE ${source} --${edge.relation}/${edge.evidenceClass}/${edge.confidence.toFixed(2)}--> ${target}`);
1502
2063
  }
1503
- const queryTokens = estimateTokens(lines.join("\n"));
2064
+ const queryTokens = estimateTokens2(lines.join("\n"));
1504
2065
  return {
1505
2066
  question: queryResult.question,
1506
2067
  queryTokens,
@@ -2217,6 +2778,67 @@ function graphDiff(oldGraph, newGraph) {
2217
2778
  const summary = parts.length ? parts.join("; ") : "No changes";
2218
2779
  return { addedNodes, removedNodes, addedEdges, removedEdges, addedPages, removedPages, summary };
2219
2780
  }
2781
+ function blastRadius(graph, target, options) {
2782
+ const maxDepth = Math.max(1, Math.min(options?.maxDepth ?? 3, 10));
2783
+ const resolved = resolveNode(graph, target);
2784
+ const moduleNode = resolved?.type === "module" ? resolved : resolved?.moduleId ? graph.nodes.find((n) => n.id === resolved.moduleId) : void 0;
2785
+ if (!moduleNode) {
2786
+ const normalizedTarget = normalizeTarget(target);
2787
+ const candidate = graph.nodes.filter((n) => n.type === "module").find((n) => normalizeTarget(n.label).includes(normalizedTarget) || normalizeTarget(n.id).includes(normalizedTarget));
2788
+ if (!candidate) {
2789
+ return {
2790
+ target,
2791
+ totalAffected: 0,
2792
+ maxDepth,
2793
+ affectedModules: [],
2794
+ summary: `No module found matching "${target}".`
2795
+ };
2796
+ }
2797
+ return blastRadius(graph, candidate.id, options);
2798
+ }
2799
+ const reverseImports = /* @__PURE__ */ new Map();
2800
+ for (const edge of graph.edges) {
2801
+ if (edge.relation === "imports") {
2802
+ const dependents = reverseImports.get(edge.target) ?? [];
2803
+ dependents.push(edge.source);
2804
+ reverseImports.set(edge.target, dependents);
2805
+ }
2806
+ }
2807
+ const affected = [];
2808
+ const seen = /* @__PURE__ */ new Set([moduleNode.id]);
2809
+ const frontier = [{ id: moduleNode.id, depth: 0 }];
2810
+ const nodes = nodeById(graph);
2811
+ while (frontier.length > 0) {
2812
+ const current = frontier.shift();
2813
+ if (current.depth >= maxDepth) {
2814
+ continue;
2815
+ }
2816
+ for (const dependentId of reverseImports.get(current.id) ?? []) {
2817
+ if (seen.has(dependentId)) {
2818
+ continue;
2819
+ }
2820
+ seen.add(dependentId);
2821
+ const dependentNode = nodes.get(dependentId);
2822
+ const nextDepth = current.depth + 1;
2823
+ affected.push({
2824
+ moduleId: dependentId,
2825
+ label: dependentNode?.label ?? dependentId,
2826
+ depth: nextDepth
2827
+ });
2828
+ frontier.push({ id: dependentId, depth: nextDepth });
2829
+ }
2830
+ }
2831
+ affected.sort((a, b) => a.depth - b.depth || a.label.localeCompare(b.label));
2832
+ const summary = affected.length ? `Changing "${moduleNode.label}" affects ${affected.length} module${affected.length === 1 ? "" : "s"} (max depth ${maxDepth}).` : `No modules depend on "${moduleNode.label}".`;
2833
+ return {
2834
+ target,
2835
+ resolvedModuleId: moduleNode.id,
2836
+ affectedModules: affected,
2837
+ totalAffected: affected.length,
2838
+ maxDepth,
2839
+ summary
2840
+ };
2841
+ }
2220
2842
 
2221
2843
  // src/hooks.ts
2222
2844
  import fs4 from "fs/promises";
@@ -2361,7 +2983,7 @@ import fs11 from "fs/promises";
2361
2983
  import path12 from "path";
2362
2984
  import { pathToFileURL } from "url";
2363
2985
  import { Readability } from "@mozilla/readability";
2364
- import matter3 from "gray-matter";
2986
+ import matter4 from "gray-matter";
2365
2987
  import ignore from "ignore";
2366
2988
  import { isText } from "istextorbinary";
2367
2989
  import { JSDOM as JSDOM2 } from "jsdom";
@@ -5851,7 +6473,7 @@ function htmlCodeAnalysis(manifest, rootNode, diagnostics) {
5851
6473
  }
5852
6474
  continue;
5853
6475
  }
5854
- if (tagName && tagName.includes("-")) {
6476
+ if (tagName?.includes("-")) {
5855
6477
  if (!seenSymbolNames.has(tagName)) {
5856
6478
  seenSymbolNames.add(tagName);
5857
6479
  draftSymbols.push({
@@ -7618,6 +8240,7 @@ import { parse as parseCsvSync } from "csv-parse/sync";
7618
8240
  import { strFromU8, unzipSync } from "fflate";
7619
8241
  import { JSDOM } from "jsdom";
7620
8242
  import TurndownService from "turndown";
8243
+ import { fetchTranscript } from "youtube-transcript-plus";
7621
8244
  import { z } from "zod";
7622
8245
 
7623
8246
  // src/markdown-ast.ts
@@ -7816,6 +8439,107 @@ async function extractImageWithVision(rootDir, input) {
7816
8439
  await attachment.cleanup();
7817
8440
  }
7818
8441
  }
8442
+ async function extractAudioTranscription(rootDir, input) {
8443
+ let provider;
8444
+ try {
8445
+ provider = await getProviderForTask(rootDir, "audioProvider");
8446
+ } catch (error) {
8447
+ return {
8448
+ artifact: {
8449
+ ...extractionMetadata("audio", input.mimeType, "audio_transcription"),
8450
+ warnings: [`Audio transcription unavailable: ${error instanceof Error ? error.message : "provider not configured"}`]
8451
+ }
8452
+ };
8453
+ }
8454
+ if (!provider.capabilities.has("audio") || !provider.transcribeAudio) {
8455
+ return {
8456
+ artifact: {
8457
+ ...extractionMetadata("audio", input.mimeType, "audio_transcription"),
8458
+ warnings: [`Audio transcription unavailable for provider ${provider.id}. Configure a provider with audio capability.`]
8459
+ }
8460
+ };
8461
+ }
8462
+ try {
8463
+ const result = await provider.transcribeAudio({
8464
+ mimeType: input.mimeType,
8465
+ bytes: input.bytes,
8466
+ fileName: input.fileName
8467
+ });
8468
+ const metadata = {};
8469
+ if (result.duration !== void 0) {
8470
+ metadata.duration = String(result.duration);
8471
+ }
8472
+ if (result.language) {
8473
+ metadata.language = result.language;
8474
+ }
8475
+ return {
8476
+ extractedText: result.text || void 0,
8477
+ artifact: {
8478
+ ...extractionMetadata("audio", input.mimeType, "audio_transcription"),
8479
+ providerId: provider.id,
8480
+ providerModel: provider.model,
8481
+ metadata: Object.keys(metadata).length ? metadata : void 0
8482
+ }
8483
+ };
8484
+ } catch (error) {
8485
+ return {
8486
+ artifact: {
8487
+ ...extractionMetadata("audio", input.mimeType, "audio_transcription"),
8488
+ providerId: provider.id,
8489
+ providerModel: provider.model,
8490
+ warnings: [`Audio transcription failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
8491
+ }
8492
+ };
8493
+ }
8494
+ }
8495
+ async function extractYoutubeTranscript(input) {
8496
+ try {
8497
+ const result = await fetchTranscript(input.videoId, { videoDetails: true });
8498
+ const details = result.videoDetails;
8499
+ const title = details?.title ?? `YouTube ${input.videoId}`;
8500
+ const transcriptText = result.segments?.map((part) => part.text).join(" ") ?? "";
8501
+ const sections = [`# ${title}`];
8502
+ const metaLines = [];
8503
+ if (details?.author) metaLines.push(`**Author:** ${details.author}`);
8504
+ if (details?.lengthSeconds) {
8505
+ const seconds = details.lengthSeconds;
8506
+ const minutes = Math.floor(seconds / 60);
8507
+ const secs = seconds % 60;
8508
+ metaLines.push(`**Duration:** ${minutes}:${String(secs).padStart(2, "0")}`);
8509
+ }
8510
+ if (details?.viewCount) metaLines.push(`**Views:** ${Number(details.viewCount).toLocaleString()}`);
8511
+ metaLines.push(`**URL:** ${input.url}`);
8512
+ if (metaLines.length) {
8513
+ sections.push(metaLines.join("\n"));
8514
+ }
8515
+ if (transcriptText.trim()) {
8516
+ sections.push(`## Transcript
8517
+
8518
+ ${transcriptText.trim()}`);
8519
+ }
8520
+ const extractedText = sections.join("\n\n");
8521
+ const metadata = {};
8522
+ if (details?.title) metadata.title = details.title;
8523
+ if (details?.author) metadata.author = details.author;
8524
+ if (details?.lengthSeconds) metadata.duration = String(details.lengthSeconds);
8525
+ if (details?.viewCount) metadata.viewCount = String(details.viewCount);
8526
+ return {
8527
+ title,
8528
+ extractedText: extractedText || void 0,
8529
+ artifact: {
8530
+ ...extractionMetadata("youtube", "text/html", "youtube_transcript"),
8531
+ metadata: Object.keys(metadata).length ? metadata : void 0
8532
+ }
8533
+ };
8534
+ } catch (error) {
8535
+ return {
8536
+ artifact: {
8537
+ ...extractionMetadata("youtube", "text/html", "youtube_transcript"),
8538
+ warnings: [`YouTube transcript extraction failed: ${error instanceof Error ? truncate(error.message, 240) : "unknown error"}`]
8539
+ }
8540
+ };
8541
+ }
8542
+ }
7819
8543
  function normalizePdfMetadata(raw) {
7820
8544
  if (!raw || typeof raw !== "object") {
7821
8545
  return void 0;
@@ -9613,7 +10337,7 @@ async function extractSlackExportDirectory(directoryPath) {
9613
10337
  // src/logs.ts
9614
10338
  import fs8 from "fs/promises";
9615
10339
  import path8 from "path";
9616
- import matter from "gray-matter";
10340
+ import matter2 from "gray-matter";
9617
10341
  async function resolveUniqueSessionPath(rootDir, operation, title, startedAt) {
9618
10342
  const { paths } = await initWorkspace(rootDir);
9619
10343
  await ensureDir(paths.sessionsDir);
@@ -9666,7 +10390,7 @@ async function recordSession(rootDir, input) {
9666
10390
  token_usage: input.tokenUsage
9667
10391
  }).filter(([, value]) => value !== void 0)
9668
10392
  );
9669
- const content = matter.stringify(
10393
+ const content = matter2.stringify(
9670
10394
  [
9671
10395
  `# ${input.operation[0]?.toUpperCase() ?? ""}${input.operation.slice(1)} Session`,
9672
10396
  "",
@@ -9887,7 +10611,7 @@ async function managedSourceWorkingDir(rootDir, sourceId) {
9887
10611
  // src/watch-state.ts
9888
10612
  import fs10 from "fs/promises";
9889
10613
  import path11 from "path";
9890
- import matter2 from "gray-matter";
10614
+ import matter3 from "gray-matter";
9891
10615
  function pendingEntryKey(entry) {
9892
10616
  return entry.path;
9893
10617
  }
@@ -9999,13 +10723,13 @@ async function markPagesStaleForSources(rootDir, sourceIds) {
9999
10723
  continue;
10000
10724
  }
10001
10725
  const raw = await fs10.readFile(absolutePath, "utf8");
10002
- const parsed = matter2(raw);
10726
+ const parsed = matter3(raw);
10003
10727
  if (parsed.data.freshness === "stale") {
10004
10728
  continue;
10005
10729
  }
10006
10730
  parsed.data.freshness = "stale";
10007
10731
  parsed.data.updated_at = now;
10008
- await writeFileIfChanged(absolutePath, matter2.stringify(parsed.content, parsed.data));
10732
+ await writeFileIfChanged(absolutePath, matter3.stringify(parsed.content, parsed.data));
10009
10733
  }
10010
10734
  return affectedPagePaths;
10011
10735
  }
@@ -10101,6 +10825,9 @@ function inferKind(mimeType, filePath, detectionOptions = {}) {
10101
10825
  if (mimeType === "application/vnd.openxmlformats-officedocument.presentationml.presentation" || mimeType === "application/vnd.ms-powerpoint.presentation.macroenabled.12" || mimeType === "application/vnd.ms-powerpoint.template.macroenabled.12" || mimeType === "application/vnd.openxmlformats-officedocument.presentationml.template" || filePath.toLowerCase().endsWith(".pptx") || filePath.toLowerCase().endsWith(".pptm") || filePath.toLowerCase().endsWith(".potx") || filePath.toLowerCase().endsWith(".potm")) {
10102
10826
  return "pptx";
10103
10827
  }
10828
+ if (mimeType.startsWith("audio/") || /\.(mp3|wav|m4a|ogg|flac|webm|aac|wma)$/i.test(filePath)) {
10829
+ return "audio";
10830
+ }
10104
10831
  if (mimeType.startsWith("image/") || isImagePath(filePath)) {
10105
10832
  return "image";
10106
10833
  }
@@ -10125,6 +10852,10 @@ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
10125
10852
  function isImagePath(filePath) {
10126
10853
  return IMAGE_EXTENSIONS.has(path12.extname(filePath).toLowerCase());
10127
10854
  }
10855
+ var YOUTUBE_URL_PATTERN = /(?:youtube\.com\/watch\?.*v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/)([\w-]{11})/i;
10856
+ function parseYoutubeVideoId(url) {
10857
+ return url.match(YOUTUBE_URL_PATTERN)?.[1];
10858
+ }
10128
10859
  function isStructuredTextMime(mimeType) {
10129
10860
  switch (mimeType) {
10130
10861
  case "application/json":
@@ -10392,7 +11123,7 @@ function normalizeSemanticMarkdownList(value) {
10392
11123
  return items.length ? items : void 0;
10393
11124
  }
10394
11125
  function semanticMarkdownTitle(fallback, content, filePath) {
10395
- const parsed = matter3(content);
11126
+ const parsed = matter4(content);
10396
11127
  const frontmatterTitle = normalizeSemanticMarkdownScalar(parsed.data.title);
10397
11128
  if (frontmatterTitle) {
10398
11129
  return frontmatterTitle;
@@ -10400,7 +11131,7 @@ function semanticMarkdownTitle(fallback, content, filePath) {
10400
11131
  return titleFromText(fallback, parsed.content, filePath);
10401
11132
  }
10402
11133
  function semanticMarkdownContent(content) {
10403
- const parsed = matter3(content);
11134
+ const parsed = matter4(content);
10404
11135
  const body = parsed.content.replace(/\r\n?/g, "\n").trim();
10405
11136
  const semanticFrontmatter = Object.fromEntries(
10406
11137
  MARKDOWN_SEMANTIC_FRONTMATTER_KEYS.flatMap((key) => {
@@ -10604,7 +11335,7 @@ function markdownFrontmatter(value) {
10604
11335
  ([, rawValue]) => Array.isArray(rawValue) ? rawValue.length > 0 : Boolean(typeof rawValue === "string" ? rawValue.trim() : rawValue)
10605
11336
  )
10606
11337
  );
10607
- return matter3.stringify("", normalized).trimEnd().split("\n").concat([""]);
11338
+ return matter4.stringify("", normalized).trimEnd().split("\n").concat([""]);
10608
11339
  }
10609
11340
  function prepareCapturedMarkdownInput(input) {
10610
11341
  return finalizePreparedInput({
@@ -11422,7 +12153,7 @@ function preparedMatchesManifest(manifest, prepared, contentHash) {
11422
12153
  return manifest.contentHash === contentHash && manifest.extractionHash === (prepared.extractionHash ?? buildExtractionHash(prepared.extractedText, prepared.extractionArtifact)) && manifest.semanticHash === (prepared.semanticHash ?? contentHash) && 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 && manifest.sourceGroupId === prepared.sourceGroupId && manifest.sourceGroupTitle === prepared.sourceGroupTitle && manifest.sourcePartKey === prepared.sourcePartKey && manifest.partIndex === prepared.partIndex && manifest.partCount === prepared.partCount && manifest.partTitle === prepared.partTitle && JSON.stringify(manifest.details ?? {}) === JSON.stringify(prepared.details ?? {});
11423
12154
  }
11424
12155
  function shouldDeferWatchSemanticRefresh(sourceKind) {
11425
- return sourceKind === "markdown" || sourceKind === "text" || sourceKind === "html" || sourceKind === "pdf" || sourceKind === "docx" || sourceKind === "epub" || sourceKind === "csv" || sourceKind === "xlsx" || sourceKind === "pptx" || sourceKind === "transcript" || sourceKind === "chat_export" || sourceKind === "email" || sourceKind === "calendar" || sourceKind === "image";
12156
+ return sourceKind === "markdown" || sourceKind === "text" || sourceKind === "html" || sourceKind === "pdf" || sourceKind === "docx" || sourceKind === "epub" || sourceKind === "csv" || sourceKind === "xlsx" || sourceKind === "pptx" || sourceKind === "transcript" || sourceKind === "chat_export" || sourceKind === "email" || sourceKind === "calendar" || sourceKind === "image" || sourceKind === "audio";
11426
12157
  }
11427
12158
  function pendingSemanticRefreshId(changeType, repoRoot, relativePath) {
11428
12159
  return `pending:${changeType}:${sha256(`${toPosix(repoRoot)}:${relativePath}`).slice(0, 12)}`;
@@ -11913,6 +12644,15 @@ async function prepareFileInputs(rootDir, absoluteInput, repoRoot, sourceClass)
11913
12644
  title = extracted.title?.trim() || title;
11914
12645
  extractedText = extracted.extractedText;
11915
12646
  extractionArtifact = extracted.artifact;
12647
+ } else if (sourceKind === "audio") {
12648
+ title = path12.basename(absoluteInput, path12.extname(absoluteInput));
12649
+ const extracted = await extractAudioTranscription(rootDir, {
12650
+ mimeType,
12651
+ bytes: payloadBytes,
12652
+ fileName: absoluteInput
12653
+ });
12654
+ extractedText = extracted.extractedText;
12655
+ extractionArtifact = extracted.artifact;
11916
12656
  } else {
11917
12657
  title = path12.basename(absoluteInput, path12.extname(absoluteInput));
11918
12658
  }
@@ -11944,6 +12684,28 @@ async function prepareFileInput(rootDir, absoluteInput, repoRoot, sourceClass) {
11944
12684
  }
11945
12685
  async function prepareUrlInputs(rootDir, input, options) {
11946
12686
  await validateUrlSafety(input);
12687
+ const youtubeVideoId = parseYoutubeVideoId(input);
12688
+ if (youtubeVideoId) {
12689
+ const extracted = await extractYoutubeTranscript({ videoId: youtubeVideoId, url: input });
12690
+ const title2 = extracted.title ?? `YouTube ${youtubeVideoId}`;
12691
+ const extractedText2 = extracted.extractedText;
12692
+ const payloadBytes2 = Buffer.from(extractedText2 ?? "", "utf8");
12693
+ return [
12694
+ finalizePreparedInput({
12695
+ title: title2,
12696
+ originType: "url",
12697
+ sourceKind: "youtube",
12698
+ url: normalizeOriginUrl(input),
12699
+ mimeType: "text/html",
12700
+ storedExtension: ".md",
12701
+ payloadBytes: payloadBytes2,
12702
+ extractedText: extractedText2,
12703
+ extractionArtifact: extracted.artifact,
12704
+ extractionHash: buildExtractionHash(extractedText2, extracted.artifact),
12705
+ details: extracted.artifact.metadata
12706
+ })
12707
+ ];
12708
+ }
11947
12709
  const response = await fetch(input);
11948
12710
  if (!response.ok) {
11949
12711
  throw new Error(`Failed to fetch ${input}: ${response.status} ${response.statusText}`);
@@ -12163,6 +12925,14 @@ async function prepareUrlInputs(rootDir, input, options) {
12163
12925
  title = extracted.title?.trim() || title;
12164
12926
  extractedText = extracted.extractedText;
12165
12927
  extractionArtifact = extracted.artifact;
12928
+ } else if (sourceKind === "audio") {
12929
+ const extracted = await extractAudioTranscription(rootDir, {
12930
+ mimeType,
12931
+ bytes: payloadBytes,
12932
+ fileName: inputUrl.pathname
12933
+ });
12934
+ extractedText = extracted.extractedText;
12935
+ extractionArtifact = extracted.artifact;
12166
12936
  }
12167
12937
  }
12168
12938
  return [
@@ -12750,7 +13520,7 @@ import fs19 from "fs/promises";
12750
13520
  import path23 from "path";
12751
13521
  import Graph from "graphology";
12752
13522
  import louvain from "graphology-communities-louvain";
12753
- import matter9 from "gray-matter";
13523
+ import matter10 from "gray-matter";
12754
13524
  import { z as z7 } from "zod";
12755
13525
 
12756
13526
  // src/analysis.ts
@@ -13186,7 +13956,7 @@ function conflictConfidence(claimA, claimB) {
13186
13956
  // src/deep-lint.ts
13187
13957
  import fs13 from "fs/promises";
13188
13958
  import path17 from "path";
13189
- import matter4 from "gray-matter";
13959
+ import matter5 from "gray-matter";
13190
13960
  import { z as z5 } from "zod";
13191
13961
 
13192
13962
  // src/findings.ts
@@ -13560,7 +14330,7 @@ async function loadContextPages(rootDir, graph) {
13560
14330
  contextPages.slice(0, 18).map(async (page) => {
13561
14331
  const absolutePath = path17.join(paths.wikiDir, page.path);
13562
14332
  const raw = await fs13.readFile(absolutePath, "utf8").catch(() => "");
13563
- const parsed = matter4(raw);
14333
+ const parsed = matter5(raw);
13564
14334
  return {
13565
14335
  id: page.id,
13566
14336
  title: page.title,
@@ -14024,6 +14794,33 @@ async function semanticGraphMatches(rootDir, graph, question, limit = 12) {
14024
14794
  score: Math.max(0, Number((cosineSimilarity(queryVector, vectors.get(`${item.kind}:${item.id}`) ?? []) * 100).toFixed(2)))
14025
14795
  })).filter((match) => match.score >= 18).sort((left, right) => right.score - left.score || left.label.localeCompare(right.label)).slice(0, limit);
14026
14796
  }
14797
+ async function semanticPageSearch(rootDir, graph, query, limit = 10) {
14798
+ const items = await buildEmbeddableItems(rootDir, graph);
14799
+ const pageItems = items.filter((item) => item.kind === "page");
14800
+ if (!pageItems.length) {
14801
+ return [];
14802
+ }
14803
+ const { provider, vectors } = await resolveVectorsForItems(rootDir, graph.generatedAt, items);
14804
+ if (!provider) {
14805
+ return [];
14806
+ }
14807
+ const [queryVector] = await provider.embedTexts([query]);
14808
+ if (!Array.isArray(queryVector) || queryVector.length === 0) {
14809
+ return [];
14810
+ }
14811
+ const pageMap2 = new Map(graph.pages.map((page) => [page.id, page]));
14812
+ return pageItems.map((item) => {
14813
+ const page = pageMap2.get(item.id);
14814
+ return {
14815
+ pageId: item.id,
14816
+ path: page?.path ?? "",
14817
+ title: item.label,
14818
+ kind: page?.kind ?? "",
14819
+ status: page?.status ?? "",
14820
+ score: cosineSimilarity(queryVector, vectors.get(`page:${item.id}`) ?? [])
14821
+ };
14822
+ }).filter((result) => result.score >= 0.25 && result.path).sort((left, right) => right.score - left.score).slice(0, limit);
14823
+ }
14027
14824
  function distinctScope(left, right) {
14028
14825
  const leftSources = new Set(left.sourceIds);
14029
14826
  const rightSources = new Set(right.sourceIds);
@@ -14469,7 +15266,7 @@ function enrichGraph(graph, manifests, analyses, extraSimilarityEdges = []) {
14469
15266
  }
14470
15267
 
14471
15268
  // src/markdown.ts
14472
- import matter5 from "gray-matter";
15269
+ import matter6 from "gray-matter";
14473
15270
  function uniqueStrings2(values) {
14474
15271
  return uniqueBy(values.filter(Boolean), (value) => value);
14475
15272
  }
@@ -14724,7 +15521,7 @@ function buildSourcePage(manifest, analysis, schemaHash, metadata, relatedOutput
14724
15521
  compiledFrom: metadata.compiledFrom,
14725
15522
  managedBy: metadata.managedBy
14726
15523
  },
14727
- content: matter5.stringify(body, safeFrontmatter(frontmatter))
15524
+ content: matter6.stringify(body, safeFrontmatter(frontmatter))
14728
15525
  };
14729
15526
  }
14730
15527
  function buildModulePage(input) {
@@ -14874,7 +15671,7 @@ function buildModulePage(input) {
14874
15671
  compiledFrom: metadata.compiledFrom,
14875
15672
  managedBy: metadata.managedBy
14876
15673
  },
14877
- content: matter5.stringify(body, frontmatter)
15674
+ content: matter6.stringify(body, frontmatter)
14878
15675
  };
14879
15676
  }
14880
15677
  function buildAggregatePage(kind, name, descriptions, sourceAnalyses, sourceHashes, sourceSemanticHashes, schemaHash, metadata, relativePath, relatedOutputs = [], decorations, existingContent) {
@@ -14951,7 +15748,7 @@ function buildAggregatePage(kind, name, descriptions, sourceAnalyses, sourceHash
14951
15748
  compiledFrom: metadata.compiledFrom,
14952
15749
  managedBy: metadata.managedBy
14953
15750
  },
14954
- content: matter5.stringify(body, frontmatter)
15751
+ content: matter6.stringify(body, frontmatter)
14955
15752
  };
14956
15753
  }
14957
15754
  function buildIndexPage(pages, schemaHash, metadata, projectPages = []) {
@@ -15035,7 +15832,7 @@ function buildIndexPage(pages, schemaHash, metadata, projectPages = []) {
15035
15832
  }
15036
15833
  function buildSectionIndex(kind, pages, schemaHash, metadata, projectIds = []) {
15037
15834
  const title = kind.charAt(0).toUpperCase() + kind.slice(1);
15038
- return matter5.stringify(
15835
+ return matter6.stringify(
15039
15836
  [`# ${title}`, "", ...pages.map((page) => `- [[${page.path.replace(/\.md$/, "")}|${page.title}]]`), ""].join("\n"),
15040
15837
  {
15041
15838
  page_id: `${kind}:index`,
@@ -15547,7 +16344,7 @@ function buildGraphReportPage(input) {
15547
16344
  compiledFrom: input.metadata.compiledFrom,
15548
16345
  managedBy: input.metadata.managedBy
15549
16346
  },
15550
- content: matter5.stringify(body, frontmatter)
16347
+ content: matter6.stringify(body, frontmatter)
15551
16348
  };
15552
16349
  }
15553
16350
  function buildCommunitySummaryPage(input) {
@@ -15631,11 +16428,11 @@ function buildCommunitySummaryPage(input) {
15631
16428
  compiledFrom: input.metadata.compiledFrom,
15632
16429
  managedBy: input.metadata.managedBy
15633
16430
  },
15634
- content: matter5.stringify(body, frontmatter)
16431
+ content: matter6.stringify(body, frontmatter)
15635
16432
  };
15636
16433
  }
15637
16434
  function buildProjectsIndex(projectPages, schemaHash, metadata) {
15638
- return matter5.stringify(
16435
+ return matter6.stringify(
15639
16436
  [
15640
16437
  "# Projects",
15641
16438
  "",
@@ -15666,7 +16463,7 @@ function buildProjectsIndex(projectPages, schemaHash, metadata) {
15666
16463
  }
15667
16464
  function buildProjectIndex(input) {
15668
16465
  const title = `Project: ${input.projectId}`;
15669
- return matter5.stringify(
16466
+ return matter6.stringify(
15670
16467
  [
15671
16468
  `# ${title}`,
15672
16469
  "",
@@ -15783,7 +16580,7 @@ function buildOutputPage(input) {
15783
16580
  outputFormat: input.outputFormat,
15784
16581
  outputAssets
15785
16582
  },
15786
- content: matter5.stringify(
16583
+ content: matter6.stringify(
15787
16584
  (input.outputFormat === "slides" ? [
15788
16585
  input.answer,
15789
16586
  "",
@@ -15911,7 +16708,7 @@ function buildExploreHubPage(input) {
15911
16708
  outputFormat: input.outputFormat,
15912
16709
  outputAssets
15913
16710
  },
15914
- content: matter5.stringify(
16711
+ content: matter6.stringify(
15915
16712
  (input.outputFormat === "slides" ? [
15916
16713
  `# ${title}`,
15917
16714
  "",
@@ -16177,12 +16974,12 @@ function buildOutputAssetManifest(input) {
16177
16974
  // src/outputs.ts
16178
16975
  import fs16 from "fs/promises";
16179
16976
  import path20 from "path";
16180
- import matter7 from "gray-matter";
16977
+ import matter8 from "gray-matter";
16181
16978
 
16182
16979
  // src/pages.ts
16183
16980
  import fs15 from "fs/promises";
16184
16981
  import path19 from "path";
16185
- import matter6 from "gray-matter";
16982
+ import matter7 from "gray-matter";
16186
16983
  function normalizeStringArray(value) {
16187
16984
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
16188
16985
  }
@@ -16263,7 +17060,7 @@ async function loadExistingManagedPageState(absolutePath, defaults = {}) {
16263
17060
  };
16264
17061
  }
16265
17062
  const content = await fs15.readFile(absolutePath, "utf8");
16266
- const parsed = matter6(content);
17063
+ const parsed = matter7(content);
16267
17064
  return {
16268
17065
  status: normalizePageStatus(parsed.data.status, defaults.status ?? "active"),
16269
17066
  managedBy: normalizePageManager(parsed.data.managed_by, defaults.managedBy ?? "system"),
@@ -16297,7 +17094,7 @@ function inferPageKind(relativePath, explicitKind = void 0) {
16297
17094
  return "index";
16298
17095
  }
16299
17096
  function parseStoredPage(relativePath, content, defaults = {}) {
16300
- const parsed = matter6(content);
17097
+ const parsed = matter7(content);
16301
17098
  const now = (/* @__PURE__ */ new Date()).toISOString();
16302
17099
  const fallbackCreatedAt = defaults.createdAt ?? now;
16303
17100
  const fallbackUpdatedAt = defaults.updatedAt ?? fallbackCreatedAt;
@@ -16352,7 +17149,7 @@ async function loadInsightPages(wikiDir) {
16352
17149
  for (const absolutePath of files) {
16353
17150
  const relativePath = toPosix(path19.relative(wikiDir, absolutePath));
16354
17151
  const content = await fs15.readFile(absolutePath, "utf8");
16355
- const parsed = matter6(content);
17152
+ const parsed = matter7(content);
16356
17153
  const stats = await fs15.stat(absolutePath);
16357
17154
  const title = typeof parsed.data.title === "string" ? parsed.data.title : path19.basename(absolutePath, ".md");
16358
17155
  const sourceIds = normalizeStringArray(parsed.data.source_ids);
@@ -16453,7 +17250,7 @@ async function loadSavedOutputPages(wikiDir) {
16453
17250
  const relativePath = path20.posix.join(current.relativeDir, entry.name);
16454
17251
  const absolutePath = path20.join(current.absoluteDir, entry.name);
16455
17252
  const content = await fs16.readFile(absolutePath, "utf8");
16456
- const parsed = matter7(content);
17253
+ const parsed = matter8(content);
16457
17254
  const slug = relativePath.replace(/^outputs\//, "").replace(/\.md$/, "");
16458
17255
  const title = typeof parsed.data.title === "string" ? parsed.data.title : path20.basename(slug);
16459
17256
  const pageId = typeof parsed.data.page_id === "string" ? parsed.data.page_id : `output:${slug}`;
@@ -16507,7 +17304,7 @@ async function loadSavedOutputPages(wikiDir) {
16507
17304
  // src/search.ts
16508
17305
  import fs17 from "fs/promises";
16509
17306
  import path21 from "path";
16510
- import matter8 from "gray-matter";
17307
+ import matter9 from "gray-matter";
16511
17308
  function warningMessage(warning) {
16512
17309
  return warning instanceof Error ? warning.message : String(warning);
16513
17310
  }
@@ -16595,7 +17392,7 @@ async function rebuildSearchIndex(dbPath, pages, wikiDir) {
16595
17392
  for (const page of pages) {
16596
17393
  const absolutePath = path21.join(wikiDir, page.path);
16597
17394
  const content = await fs17.readFile(absolutePath, "utf8");
16598
- const parsed = matter8(content);
17395
+ const parsed = matter9(content);
16599
17396
  let body = parsed.content;
16600
17397
  const primarySourceId = Array.isArray(parsed.data.source_ids) && typeof parsed.data.source_ids[0] === "string" ? parsed.data.source_ids[0] : page.sourceIds[0];
16601
17398
  if ((page.kind === "source" || page.kind === "module") && primarySourceId) {
@@ -16633,6 +17430,38 @@ ${excerpt.trim()}`.trim();
16633
17430
  db.exec("INSERT INTO page_search (rowid, title, body) SELECT rowid, title, body FROM pages;");
16634
17431
  db.close();
16635
17432
  }
17433
+ function mergeSearchResults(ftsResults, semanticHits, limit) {
17434
+ const k = 60;
17435
+ const scores = /* @__PURE__ */ new Map();
17436
+ const resultMap = /* @__PURE__ */ new Map();
17437
+ for (let i = 0; i < ftsResults.length; i++) {
17438
+ const r = ftsResults[i];
17439
+ scores.set(r.pageId, (scores.get(r.pageId) ?? 0) + 1 / (k + i + 1));
17440
+ resultMap.set(r.pageId, r);
17441
+ }
17442
+ for (let i = 0; i < semanticHits.length; i++) {
17443
+ const hit = semanticHits[i];
17444
+ scores.set(hit.pageId, (scores.get(hit.pageId) ?? 0) + 1 / (k + i + 1));
17445
+ if (!resultMap.has(hit.pageId)) {
17446
+ resultMap.set(hit.pageId, {
17447
+ pageId: hit.pageId,
17448
+ path: hit.path,
17449
+ title: hit.title,
17450
+ snippet: "",
17451
+ rank: -hit.score,
17452
+ kind: hit.kind,
17453
+ status: hit.status,
17454
+ projectIds: [],
17455
+ sourceType: void 0,
17456
+ sourceClass: void 0
17457
+ });
17458
+ }
17459
+ }
17460
+ return [...scores.entries()].sort(([, a], [, b]) => b - a).slice(0, limit).map(([pageId, rrfScore]) => {
17461
+ const result = resultMap.get(pageId);
17462
+ return { ...result, rank: -rrfScore };
17463
+ });
17464
+ }
16636
17465
  function searchPages(dbPath, query, limitOrOptions = 5) {
16637
17466
  const options = typeof limitOrOptions === "number" ? { limit: limitOrOptions } : limitOrOptions;
16638
17467
  const ftsQuery = toFtsQuery(query);
@@ -16974,7 +17803,7 @@ async function resolveImageGenerationProvider(rootDir) {
16974
17803
  if (!providerConfig) {
16975
17804
  throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
16976
17805
  }
16977
- const { createProvider: createProvider2 } = await import("./registry-MYJX6AEE.js");
17806
+ const { createProvider: createProvider2 } = await import("./registry-SYCRRA65.js");
16978
17807
  return createProvider2(preferredProviderId, providerConfig, rootDir);
16979
17808
  }
16980
17809
  async function generateOutputArtifacts(rootDir, input) {
@@ -17493,7 +18322,7 @@ async function buildDashboardRecords(config, paths, graph, schemaHash, report) {
17493
18322
  {
17494
18323
  relativePath: "dashboards/index.md",
17495
18324
  title: "Dashboards",
17496
- content: (metadata) => matter9.stringify(
18325
+ content: (metadata) => matter10.stringify(
17497
18326
  [
17498
18327
  "# Dashboards",
17499
18328
  "",
@@ -17545,7 +18374,7 @@ async function buildDashboardRecords(config, paths, graph, schemaHash, report) {
17545
18374
  {
17546
18375
  relativePath: "dashboards/recent-sources.md",
17547
18376
  title: "Recent Sources",
17548
- content: (metadata) => matter9.stringify(
18377
+ content: (metadata) => matter10.stringify(
17549
18378
  [
17550
18379
  "# Recent Sources",
17551
18380
  "",
@@ -17588,7 +18417,7 @@ async function buildDashboardRecords(config, paths, graph, schemaHash, report) {
17588
18417
  {
17589
18418
  relativePath: "dashboards/reading-log.md",
17590
18419
  title: "Reading Log",
17591
- content: (metadata) => matter9.stringify(
18420
+ content: (metadata) => matter10.stringify(
17592
18421
  [
17593
18422
  "# Reading Log",
17594
18423
  "",
@@ -17648,7 +18477,7 @@ async function buildDashboardRecords(config, paths, graph, schemaHash, report) {
17648
18477
  {
17649
18478
  relativePath: "dashboards/timeline.md",
17650
18479
  title: "Timeline",
17651
- content: (metadata) => matter9.stringify(
18480
+ content: (metadata) => matter10.stringify(
17652
18481
  [
17653
18482
  "# Timeline",
17654
18483
  "",
@@ -17694,7 +18523,7 @@ async function buildDashboardRecords(config, paths, graph, schemaHash, report) {
17694
18523
  {
17695
18524
  relativePath: "dashboards/source-sessions.md",
17696
18525
  title: "Source Sessions",
17697
- content: (metadata) => matter9.stringify(
18526
+ content: (metadata) => matter10.stringify(
17698
18527
  [
17699
18528
  "# Source Sessions",
17700
18529
  "",
@@ -17751,7 +18580,7 @@ async function buildDashboardRecords(config, paths, graph, schemaHash, report) {
17751
18580
  {
17752
18581
  relativePath: "dashboards/source-guides.md",
17753
18582
  title: "Source Guides",
17754
- content: (metadata) => matter9.stringify(
18583
+ content: (metadata) => matter10.stringify(
17755
18584
  [
17756
18585
  "# Source Guides",
17757
18586
  "",
@@ -17804,7 +18633,7 @@ async function buildDashboardRecords(config, paths, graph, schemaHash, report) {
17804
18633
  {
17805
18634
  relativePath: "dashboards/research-map.md",
17806
18635
  title: "Research Map",
17807
- content: (metadata) => matter9.stringify(
18636
+ content: (metadata) => matter10.stringify(
17808
18637
  [
17809
18638
  "# Research Map",
17810
18639
  "",
@@ -17868,7 +18697,7 @@ async function buildDashboardRecords(config, paths, graph, schemaHash, report) {
17868
18697
  {
17869
18698
  relativePath: "dashboards/contradictions.md",
17870
18699
  title: "Contradictions",
17871
- content: (metadata) => matter9.stringify(
18700
+ content: (metadata) => matter10.stringify(
17872
18701
  [
17873
18702
  "# Contradictions",
17874
18703
  "",
@@ -17927,7 +18756,7 @@ async function buildDashboardRecords(config, paths, graph, schemaHash, report) {
17927
18756
  {
17928
18757
  relativePath: "dashboards/open-questions.md",
17929
18758
  title: "Open Questions",
17930
- content: (metadata) => matter9.stringify(
18759
+ content: (metadata) => matter10.stringify(
17931
18760
  [
17932
18761
  "# Open Questions",
17933
18762
  "",
@@ -18003,7 +18832,12 @@ async function buildDashboardRecords(config, paths, graph, schemaHash, report) {
18003
18832
  function indexCompiledFrom(pages) {
18004
18833
  return uniqueStrings3(pages.flatMap((page) => page.sourceIds));
18005
18834
  }
18006
- function deriveGraphMetrics(nodes, edges) {
18835
+ function autoResolution(nodeCount, edgeCount) {
18836
+ if (nodeCount <= 20) return 0.5;
18837
+ if (edgeCount / Math.max(1, nodeCount) < 2) return 0.8;
18838
+ return 1;
18839
+ }
18840
+ function deriveGraphMetrics(nodes, edges, options) {
18007
18841
  const adjacency = /* @__PURE__ */ new Map();
18008
18842
  const connect = (left, right) => {
18009
18843
  if (!adjacency.has(left)) {
@@ -18040,7 +18874,8 @@ function deriveGraphMetrics(nodes, edges) {
18040
18874
  }
18041
18875
  }
18042
18876
  }
18043
- const louvainMapping = louvainGraph.size > 0 ? louvain(louvainGraph, { resolution: 1 }) : {};
18877
+ const effectiveResolution = options?.resolution ?? autoResolution(louvainGraph.order, louvainGraph.size);
18878
+ const louvainMapping = louvainGraph.size > 0 ? louvain(louvainGraph, { resolution: effectiveResolution }) : {};
18044
18879
  const groupByCommunity = /* @__PURE__ */ new Map();
18045
18880
  let nextIsolated = -1;
18046
18881
  for (const node of nonSourceNodes) {
@@ -18186,7 +19021,7 @@ function detectContradictions(analyses) {
18186
19021
  }
18187
19022
  return contradictions;
18188
19023
  }
18189
- function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
19024
+ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex, options) {
18190
19025
  const manifestsById = new Map(manifests.map((manifest) => [manifest.sourceId, manifest]));
18191
19026
  const goPackageSymbolLookups = buildGoPackageSymbolLookups(analyses, manifestsById);
18192
19027
  const analysesBySourceId = new Map(analyses.map((analysis) => [analysis.sourceId, analysis]));
@@ -18546,7 +19381,7 @@ function buildGraph(manifests, analyses, pages, sourceProjects, _codeIndex) {
18546
19381
  manifests,
18547
19382
  analyses
18548
19383
  );
18549
- const metrics = deriveGraphMetrics(graphNodes, enriched.edges);
19384
+ const metrics = deriveGraphMetrics(graphNodes, enriched.edges, { resolution: options?.communityResolution });
18550
19385
  return {
18551
19386
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
18552
19387
  nodes: metrics.nodes,
@@ -19013,7 +19848,9 @@ async function syncVaultArtifacts(rootDir, input) {
19013
19848
  }
19014
19849
  const compiledPages = records.map((record) => record.page);
19015
19850
  const basePages = [...compiledPages, ...input.outputPages, ...input.insightPages];
19016
- const structuralGraph = buildGraph(input.manifests, input.analyses, basePages, input.sourceProjects, input.codeIndex);
19851
+ const structuralGraph = buildGraph(input.manifests, input.analyses, basePages, input.sourceProjects, input.codeIndex, {
19852
+ communityResolution: config.graph?.communityResolution
19853
+ });
19017
19854
  const contradictions = detectContradictions(input.analyses);
19018
19855
  for (const contradiction of contradictions) {
19019
19856
  const edgeId = `contradiction:${contradiction.sourceIdA}->${contradiction.sourceIdB}`;
@@ -19035,7 +19872,9 @@ async function syncVaultArtifacts(rootDir, input) {
19035
19872
  const edges = uniqueBy([...structuralGraph.edges, ...embeddingEdges], (edge) => edge.id).sort(
19036
19873
  (left, right) => left.id.localeCompare(right.id)
19037
19874
  );
19038
- const metrics = deriveGraphMetrics(resetGraphNodeMetrics(structuralGraph.nodes), edges);
19875
+ const metrics = deriveGraphMetrics(resetGraphNodeMetrics(structuralGraph.nodes), edges, {
19876
+ resolution: config.graph?.communityResolution
19877
+ });
19039
19878
  return {
19040
19879
  ...structuralGraph,
19041
19880
  nodes: metrics.nodes,
@@ -19606,7 +20445,7 @@ async function executeQuery(rootDir, question, format) {
19606
20445
  const absolutePath = path23.join(paths.wikiDir, result.path);
19607
20446
  try {
19608
20447
  const content = await fs19.readFile(absolutePath, "utf8");
19609
- const parsed = matter9(content);
20448
+ const parsed = matter10(content);
19610
20449
  return `# ${result.title}
19611
20450
  ${truncate(normalizeWhitespace(parsed.content), 1200)}`;
19612
20451
  } catch {
@@ -19819,8 +20658,8 @@ function computeChangeSummary(current, staged, changeType) {
19819
20658
  if (changeType === "delete") return "Removed page";
19820
20659
  if (changeType === "promote") return "Promoted from candidate";
19821
20660
  if (!current || !staged) return "Updated page";
19822
- const currentParsed = matter9(current);
19823
- const stagedParsed = matter9(staged);
20661
+ const currentParsed = matter10(current);
20662
+ const stagedParsed = matter10(staged);
19824
20663
  const changes = [];
19825
20664
  const currentTags = currentParsed.data.tags ?? [];
19826
20665
  const stagedTags = stagedParsed.data.tags ?? [];
@@ -20021,9 +20860,9 @@ async function promoteCandidate(rootDir, target) {
20021
20860
  const graph = await readJsonFile(paths.graphPath);
20022
20861
  const candidate = resolveCandidateTarget(graph?.pages ?? [], target);
20023
20862
  const raw = await fs19.readFile(path23.join(paths.wikiDir, candidate.path), "utf8");
20024
- const parsed = matter9(raw);
20863
+ const parsed = matter10(raw);
20025
20864
  const nextUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
20026
- const nextContent = matter9.stringify(parsed.content, {
20865
+ const nextContent = matter10.stringify(parsed.content, {
20027
20866
  ...parsed.data,
20028
20867
  status: "active",
20029
20868
  updated_at: nextUpdatedAt,
@@ -20174,7 +21013,7 @@ async function initVault(rootDir, options = {}) {
20174
21013
  const now = (/* @__PURE__ */ new Date()).toISOString();
20175
21014
  await writeFileIfChanged(
20176
21015
  insightsIndexPath,
20177
- matter9.stringify(
21016
+ matter10.stringify(
20178
21017
  (isResearchProfile ? [
20179
21018
  "# Insights",
20180
21019
  "",
@@ -20221,7 +21060,7 @@ async function initVault(rootDir, options = {}) {
20221
21060
  );
20222
21061
  await writeFileIfChanged(
20223
21062
  path23.join(paths.wikiDir, "projects", "index.md"),
20224
- matter9.stringify(["# Projects", "", "- Run `swarmvault compile` to build project rollups.", ""].join("\n"), {
21063
+ matter10.stringify(["# Projects", "", "- Run `swarmvault compile` to build project rollups.", ""].join("\n"), {
20225
21064
  page_id: "projects:index",
20226
21065
  kind: "index",
20227
21066
  title: "Projects",
@@ -20244,7 +21083,7 @@ async function initVault(rootDir, options = {}) {
20244
21083
  );
20245
21084
  await writeFileIfChanged(
20246
21085
  path23.join(paths.wikiDir, "candidates", "index.md"),
20247
- matter9.stringify(["# Candidates", "", "- Run `swarmvault compile` to stage candidate pages.", ""].join("\n"), {
21086
+ matter10.stringify(["# Candidates", "", "- Run `swarmvault compile` to stage candidate pages.", ""].join("\n"), {
20248
21087
  page_id: "candidates:index",
20249
21088
  kind: "index",
20250
21089
  title: "Candidates",
@@ -20271,7 +21110,7 @@ async function initVault(rootDir, options = {}) {
20271
21110
  if (isResearchProfile) {
20272
21111
  await writeFileIfChanged(
20273
21112
  path23.join(paths.wikiDir, "insights", "research-playbook.md"),
20274
- matter9.stringify(
21113
+ matter10.stringify(
20275
21114
  [
20276
21115
  `# ${requestedProfile === "personal-research" ? "Personal Research Playbook" : "Research Playbook"}`,
20277
21116
  "",
@@ -20553,6 +21392,45 @@ async function compileVault(rootDir, options = {}) {
20553
21392
  `benchmark=${benchmark.ok ? "ok" : `error:${benchmark.error}`}`
20554
21393
  ]
20555
21394
  });
21395
+ let tokenStats;
21396
+ if (options.maxTokens && options.maxTokens > 0) {
21397
+ const { estimatePageTokens: estimatePageTokens2, trimToTokenBudget: trimToTokenBudget2 } = await import("./token-estimation-TTONKT4O.js");
21398
+ const nodeDegreeLookup = /* @__PURE__ */ new Map();
21399
+ const graph = await readJsonFile(paths.graphPath);
21400
+ if (graph) {
21401
+ for (const node of graph.nodes) {
21402
+ if (node.pageId && node.degree) {
21403
+ const existing = nodeDegreeLookup.get(node.pageId) ?? 0;
21404
+ nodeDegreeLookup.set(node.pageId, Math.max(existing, node.degree));
21405
+ }
21406
+ }
21407
+ }
21408
+ const estimates = await Promise.all(
21409
+ sync.allPages.map(async (page) => {
21410
+ const fullPath = path23.join(paths.wikiDir, page.path);
21411
+ let content = "";
21412
+ try {
21413
+ content = await fs19.readFile(fullPath, "utf8");
21414
+ } catch {
21415
+ }
21416
+ return estimatePageTokens2(page.id, page.path, page.kind, content, nodeDegreeLookup.get(page.id), page.confidence);
21417
+ })
21418
+ );
21419
+ const budgetResult = trimToTokenBudget2(estimates, options.maxTokens);
21420
+ for (const dropped of budgetResult.dropped) {
21421
+ const fullPath = path23.join(paths.wikiDir, dropped.path);
21422
+ try {
21423
+ await fs19.unlink(fullPath);
21424
+ } catch {
21425
+ }
21426
+ }
21427
+ tokenStats = {
21428
+ estimatedTokens: budgetResult.totalTokens,
21429
+ maxTokens: options.maxTokens,
21430
+ pagesKept: budgetResult.kept.length,
21431
+ pagesDropped: budgetResult.dropped.length
21432
+ };
21433
+ }
20556
21434
  return {
20557
21435
  graphPath: paths.graphPath,
20558
21436
  pageCount: sync.allPages.length,
@@ -20564,7 +21442,8 @@ async function compileVault(rootDir, options = {}) {
20564
21442
  postPassApprovalId,
20565
21443
  postPassApprovalDir,
20566
21444
  promotedPageIds: sync.promotedPageIds,
20567
- candidatePageCount: sync.candidatePageCount
21445
+ candidatePageCount: sync.candidatePageCount,
21446
+ tokenStats
20568
21447
  };
20569
21448
  }
20570
21449
  async function queryVault(rootDir, options) {
@@ -20921,11 +21800,59 @@ ${orchestrationNotes.join("\n")}
20921
21800
  };
20922
21801
  }
20923
21802
  async function searchVault(rootDir, query, limit = 5) {
20924
- const { paths } = await loadVaultConfig(rootDir);
21803
+ const { paths, config } = await loadVaultConfig(rootDir);
20925
21804
  if (!await fileExists(paths.searchDbPath)) {
20926
21805
  await compileVault(rootDir, {});
20927
21806
  }
20928
- return searchPages(paths.searchDbPath, query, limit);
21807
+ const hybrid = config.search?.hybrid !== false;
21808
+ const ftsResults = searchPages(paths.searchDbPath, query, hybrid ? limit * 3 : limit);
21809
+ if (!hybrid || !await fileExists(paths.graphPath)) {
21810
+ return ftsResults.slice(0, limit);
21811
+ }
21812
+ const graph = await readJsonFile(paths.graphPath);
21813
+ if (!graph) {
21814
+ return ftsResults.slice(0, limit);
21815
+ }
21816
+ const semanticHits = await semanticPageSearch(rootDir, graph, query, limit * 3).catch(() => []);
21817
+ if (!semanticHits.length) {
21818
+ return ftsResults.slice(0, limit);
21819
+ }
21820
+ const merged = mergeSearchResults(ftsResults, semanticHits, limit);
21821
+ if (config.search?.rerank && merged.length > 1) {
21822
+ return rerankSearchResults(rootDir, query, merged, limit);
21823
+ }
21824
+ return merged;
21825
+ }
21826
+ async function rerankSearchResults(rootDir, query, results, limit) {
21827
+ const provider = await getProviderForTask(rootDir, "queryProvider");
21828
+ const candidates = results.slice(0, Math.min(results.length, 20)).map((r, i) => `[${i}] ${r.title} \u2014 ${r.snippet || r.path}`).join("\n");
21829
+ const prompt = `Given the search query: "${query}"
21830
+
21831
+ Rank these results by relevance (most relevant first).
21832
+
21833
+ ${candidates}`;
21834
+ try {
21835
+ const indices = await provider.generateStructured(
21836
+ { prompt, system: "You are a search result ranker." },
21837
+ z7.array(z7.number().int().nonnegative())
21838
+ );
21839
+ const reranked = [];
21840
+ const seen = /* @__PURE__ */ new Set();
21841
+ for (const idx of indices) {
21842
+ if (idx >= 0 && idx < results.length && !seen.has(idx)) {
21843
+ seen.add(idx);
21844
+ reranked.push(results[idx]);
21845
+ }
21846
+ }
21847
+ for (let i = 0; i < results.length && reranked.length < limit; i++) {
21848
+ if (!seen.has(i)) {
21849
+ reranked.push(results[i]);
21850
+ }
21851
+ }
21852
+ return reranked.slice(0, limit);
21853
+ } catch {
21854
+ return results.slice(0, limit);
21855
+ }
20929
21856
  }
20930
21857
  async function ensureCompiledGraph(rootDir) {
20931
21858
  const { paths } = await loadVaultConfig(rootDir);
@@ -20968,7 +21895,7 @@ async function benchmarkVault(rootDir, options = {}) {
20968
21895
  if (!await fileExists(absolutePath)) {
20969
21896
  continue;
20970
21897
  }
20971
- const parsed = matter9(await fs19.readFile(absolutePath, "utf8"));
21898
+ const parsed = matter10(await fs19.readFile(absolutePath, "utf8"));
20972
21899
  pageContentsById.set(page.id, parsed.content);
20973
21900
  }
20974
21901
  const configuredQuestions = (config.benchmark?.questions ?? []).map((question) => normalizeWhitespace(question)).filter(Boolean);
@@ -21029,6 +21956,10 @@ async function listGodNodes(rootDir, limit = 10) {
21029
21956
  const graph = await ensureCompiledGraph(rootDir);
21030
21957
  return topGodNodes(graph, limit);
21031
21958
  }
21959
+ async function blastRadiusVault(rootDir, target, options) {
21960
+ const graph = await ensureCompiledGraph(rootDir);
21961
+ return blastRadius(graph, target, options);
21962
+ }
21032
21963
  async function listPages(rootDir) {
21033
21964
  const { paths } = await loadVaultConfig(rootDir);
21034
21965
  const graph = await readJsonFile(paths.graphPath);
@@ -21048,7 +21979,7 @@ async function readPage(rootDir, relativePath) {
21048
21979
  return null;
21049
21980
  }
21050
21981
  const raw = await fs19.readFile(absolutePath, "utf8");
21051
- const parsed = matter9(raw);
21982
+ const parsed = matter10(raw);
21052
21983
  return {
21053
21984
  path: relativePath,
21054
21985
  title: typeof parsed.data.title === "string" ? parsed.data.title : path23.basename(relativePath, path23.extname(relativePath)),
@@ -21263,7 +22194,7 @@ async function bootstrapDemo(rootDir, input) {
21263
22194
  }
21264
22195
 
21265
22196
  // src/mcp.ts
21266
- var SERVER_VERSION = "0.7.24";
22197
+ var SERVER_VERSION = "0.7.26";
21267
22198
  async function createMcpServer(rootDir) {
21268
22199
  const server = new McpServer({
21269
22200
  name: "swarmvault",
@@ -21413,6 +22344,19 @@ async function createMcpServer(rootDir) {
21413
22344
  return asToolText(await listGodNodes(rootDir, limit ?? 10));
21414
22345
  })
21415
22346
  );
22347
+ server.registerTool(
22348
+ "blast_radius",
22349
+ {
22350
+ description: "Analyze the impact of changing a file or module by tracing reverse import edges.",
22351
+ inputSchema: {
22352
+ target: z8.string().min(1).describe("File path, module label, or module id"),
22353
+ maxDepth: z8.number().int().min(1).max(10).optional().describe("Maximum traversal depth (default 3)")
22354
+ }
22355
+ },
22356
+ safeHandler(async ({ target, maxDepth }) => {
22357
+ return asToolText(await blastRadiusVault(rootDir, target, { maxDepth: maxDepth ?? 3 }));
22358
+ })
22359
+ );
21416
22360
  server.registerTool(
21417
22361
  "query_vault",
21418
22362
  {
@@ -21450,11 +22394,12 @@ async function createMcpServer(rootDir) {
21450
22394
  {
21451
22395
  description: "Compile source manifests into wiki pages, graph data, and search index.",
21452
22396
  inputSchema: {
21453
- approve: z8.boolean().optional().describe("Stage a review bundle without applying active page changes")
22397
+ approve: z8.boolean().optional().describe("Stage a review bundle without applying active page changes"),
22398
+ maxTokens: z8.number().int().min(1e3).optional().describe("Maximum token budget for wiki output")
21454
22399
  }
21455
22400
  },
21456
- safeHandler(async ({ approve }) => {
21457
- const result = await compileVault(rootDir, { approve: approve ?? false });
22401
+ safeHandler(async ({ approve, maxTokens }) => {
22402
+ const result = await compileVault(rootDir, { approve: approve ?? false, maxTokens });
21458
22403
  return asToolText(result);
21459
22404
  })
21460
22405
  );
@@ -21934,7 +22879,7 @@ async function serveSchedules(rootDir, pollMs = 3e4) {
21934
22879
  import { spawn as spawn2 } from "child_process";
21935
22880
  import fs22 from "fs/promises";
21936
22881
  import path26 from "path";
21937
- import matter10 from "gray-matter";
22882
+ import matter11 from "gray-matter";
21938
22883
  import { JSDOM as JSDOM3 } from "jsdom";
21939
22884
  var DEFAULT_CRAWL_MAX_PAGES = 12;
21940
22885
  var DEFAULT_CRAWL_MAX_DEPTH = 2;
@@ -23277,7 +24222,7 @@ async function buildGuidedUpdatePages(rootDir, scope, session) {
23277
24222
  const relativePath = useCanonicalTargets && targetPage ? targetPage.path : targetPage ? insightRelativePathForTarget(targetPage, scope) : `insights/topics/${slugify(scope.title)}.md`;
23278
24223
  const absolutePath = path26.join(paths.wikiDir, relativePath);
23279
24224
  const existingContent = await fileExists(absolutePath) ? await fs22.readFile(absolutePath, "utf8") : "";
23280
- const parsed = existingContent ? matter10(existingContent) : { data: {}, content: "" };
24225
+ const parsed = existingContent ? matter11(existingContent) : { data: {}, content: "" };
23281
24226
  const existingData = parsed.data;
23282
24227
  const existingSourceIds = Array.isArray(existingData.source_ids) ? existingData.source_ids.filter((value) => typeof value === "string") : [];
23283
24228
  const existingProjectIds = Array.isArray(existingData.project_ids) ? existingData.project_ids.filter((value) => typeof value === "string") : [];
@@ -23338,7 +24283,7 @@ async function buildGuidedUpdatePages(rootDir, scope, session) {
23338
24283
  ""
23339
24284
  ].join("\n");
23340
24285
  const nextBody = replaceMarkedSection(baseBody, scope.id, updateBlock);
23341
- const content = matter10.stringify(
24286
+ const content = matter11.stringify(
23342
24287
  `${nextBody.trimEnd()}
23343
24288
  `,
23344
24289
  JSON.parse(
@@ -23719,12 +24664,12 @@ async function deleteManagedSource(rootDir, id) {
23719
24664
  }
23720
24665
 
23721
24666
  // src/viewer.ts
23722
- import { execFile } from "child_process";
24667
+ import { execFile as execFile2 } from "child_process";
23723
24668
  import fs23 from "fs/promises";
23724
24669
  import http from "http";
23725
24670
  import path28 from "path";
23726
- import { promisify } from "util";
23727
- import matter11 from "gray-matter";
24671
+ import { promisify as promisify2 } from "util";
24672
+ import matter12 from "gray-matter";
23728
24673
  import mime2 from "mime-types";
23729
24674
 
23730
24675
  // src/graph-presentation.ts
@@ -24337,7 +25282,7 @@ async function getWatchStatus(rootDir) {
24337
25282
  }
24338
25283
 
24339
25284
  // src/viewer.ts
24340
- var execFileAsync = promisify(execFile);
25285
+ var execFileAsync2 = promisify2(execFile2);
24341
25286
  async function isReadableFile(absolutePath) {
24342
25287
  try {
24343
25288
  const stats = await fs23.stat(absolutePath);
@@ -24356,7 +25301,7 @@ async function readViewerPage(rootDir, relativePath) {
24356
25301
  return null;
24357
25302
  }
24358
25303
  const raw = await fs23.readFile(absolutePath, "utf8");
24359
- const parsed = matter11(raw);
25304
+ const parsed = matter12(raw);
24360
25305
  return {
24361
25306
  path: relativePath,
24362
25307
  title: typeof parsed.data.title === "string" ? parsed.data.title : path28.basename(relativePath, path28.extname(relativePath)),
@@ -24404,7 +25349,7 @@ async function ensureViewerDist(viewerDistDir) {
24404
25349
  }
24405
25350
  const viewerProjectDir = path28.dirname(viewerDistDir);
24406
25351
  if (await fileExists(path28.join(viewerProjectDir, "package.json"))) {
24407
- await execFileAsync("pnpm", ["build"], { cwd: viewerProjectDir });
25352
+ await execFileAsync2("pnpm", ["build"], { cwd: viewerProjectDir });
24408
25353
  }
24409
25354
  }
24410
25355
  async function startGraphServer(rootDir, port, options = {}) {
@@ -24584,6 +25529,44 @@ async function startGraphServer(rootDir, port, options = {}) {
24584
25529
  response.end(JSON.stringify(result));
24585
25530
  return;
24586
25531
  }
25532
+ if (url.pathname === "/api/clip" && request.method === "POST") {
25533
+ const body = await readJsonBody(request);
25534
+ const clipUrl = typeof body.url === "string" ? body.url.trim() : "";
25535
+ if (!clipUrl) {
25536
+ response.writeHead(400, { "content-type": "application/json" });
25537
+ response.end(JSON.stringify({ error: "Missing url field." }));
25538
+ return;
25539
+ }
25540
+ const manifest = await ingestInput(rootDir, clipUrl);
25541
+ response.writeHead(200, { "content-type": "application/json", "access-control-allow-origin": "*" });
25542
+ response.end(JSON.stringify({ ok: true, sourceId: manifest.sourceId, title: manifest.title }));
25543
+ return;
25544
+ }
25545
+ if (url.pathname === "/api/clip" && request.method === "OPTIONS") {
25546
+ response.writeHead(204, {
25547
+ "access-control-allow-origin": "*",
25548
+ "access-control-allow-methods": "POST, OPTIONS",
25549
+ "access-control-allow-headers": "content-type"
25550
+ });
25551
+ response.end();
25552
+ return;
25553
+ }
25554
+ if (url.pathname === "/api/bookmarklet") {
25555
+ const script = `javascript:void(fetch('http://localhost:${effectivePort}/api/clip',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:location.href})}).then(r=>r.json()).then(d=>alert('Clipped: '+(d.title||d.sourceId))).catch(e=>alert('Clip failed: '+e.message)))`;
25556
+ response.writeHead(200, { "content-type": "text/html" });
25557
+ response.end(
25558
+ [
25559
+ "<!doctype html><html><head><title>SwarmVault Clipper</title></head><body>",
25560
+ "<h1>SwarmVault Clipper</h1>",
25561
+ `<p>Drag this link to your bookmarks bar:</p>`,
25562
+ `<p style="font-size:1.5em"><a href="${script.replace(/&/g, "&amp;").replace(/"/g, "&quot;")}">Clip to SwarmVault</a></p>`,
25563
+ `<p>When clicked on any page, it sends the URL to your running SwarmVault instance for ingestion.</p>`,
25564
+ `<p>Server: <code>http://localhost:${effectivePort}</code></p>`,
25565
+ "</body></html>"
25566
+ ].join("\n")
25567
+ );
25568
+ return;
25569
+ }
24587
25570
  const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
24588
25571
  const target = path28.join(paths.viewerDistDir, relativePath);
24589
25572
  const fallback = path28.join(paths.viewerDistDir, "index.html");
@@ -24702,7 +25685,10 @@ export {
24702
25685
  addManagedSource,
24703
25686
  archiveCandidate,
24704
25687
  assertProviderCapability,
25688
+ autoCommitWikiChanges,
24705
25689
  benchmarkVault,
25690
+ blastRadius,
25691
+ blastRadiusVault,
24706
25692
  bootstrapDemo,
24707
25693
  compileVault,
24708
25694
  createMcpServer,
@@ -24711,10 +25697,13 @@ export {
24711
25697
  defaultVaultConfig,
24712
25698
  defaultVaultSchema,
24713
25699
  deleteManagedSource,
25700
+ estimatePageTokens,
25701
+ estimateTokens,
24714
25702
  explainGraphVault,
24715
25703
  exploreVault,
24716
25704
  exportGraphFormat,
24717
25705
  exportGraphHtml,
25706
+ exportGraphReportHtml,
24718
25707
  exportObsidianCanvas,
24719
25708
  exportObsidianVault,
24720
25709
  getGitHookStatus,
@@ -24771,6 +25760,7 @@ export {
24771
25760
  startMcpServer,
24772
25761
  syncTrackedRepos,
24773
25762
  syncTrackedReposForWatch,
25763
+ trimToTokenBudget,
24774
25764
  uninstallGitHooks,
24775
25765
  watchVault
24776
25766
  };