@swarmvaultai/engine 3.5.0 → 3.7.0

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
@@ -23,6 +23,7 @@ import {
23
23
  buildOutputPage,
24
24
  buildRedactor,
25
25
  buildSchemaPrompt,
26
+ checkTrackedRepoChanges,
26
27
  compileVault,
27
28
  composeVaultSchema,
28
29
  computeDecayScore,
@@ -91,6 +92,7 @@ import {
91
92
  readWatchStatusArtifact,
92
93
  rebuildRetrievalIndex,
93
94
  recordSession,
95
+ refreshGraphClusters,
94
96
  refreshVaultAfterOutputSave,
95
97
  rejectApproval,
96
98
  removeManifestBySourceId,
@@ -123,9 +125,10 @@ import {
123
125
  writeGuidedSourceSession,
124
126
  writeRetrievalManifest,
125
127
  writeWatchStatusArtifact
126
- } from "./chunk-HKU2T5JX.js";
128
+ } from "./chunk-S2E65WRI.js";
127
129
  import {
128
130
  LocalWhisperProviderAdapter,
131
+ SWARMVAULT_OUT_ENV,
129
132
  appendJsonLine,
130
133
  assertProviderCapability,
131
134
  createProvider,
@@ -140,6 +143,7 @@ import {
140
143
  loadVaultConfig,
141
144
  normalizeWhitespace,
142
145
  readJsonFile,
146
+ resolveArtifactRootDir,
143
147
  resolvePaths,
144
148
  sha256,
145
149
  slugify,
@@ -147,7 +151,7 @@ import {
147
151
  truncate,
148
152
  uniqueBy,
149
153
  writeJsonFile
150
- } from "./chunk-7QHDATCQ.js";
154
+ } from "./chunk-7O2HJSWQ.js";
151
155
  import {
152
156
  estimatePageTokens,
153
157
  estimateTokens,
@@ -559,6 +563,7 @@ import chokidar from "chokidar";
559
563
  var MAX_BACKOFF_MS = 3e4;
560
564
  var BACKOFF_THRESHOLD = 3;
561
565
  var CRITICAL_THRESHOLD = 10;
566
+ var DEFAULT_MAX_GRAPH_SHRINK_RATIO = 0.25;
562
567
  var REPO_WATCH_IGNORES = /* @__PURE__ */ new Set([".git", ".venv"]);
563
568
  var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
564
569
  ".ts",
@@ -598,7 +603,12 @@ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
598
603
  ".psm1",
599
604
  ".ex",
600
605
  ".exs",
606
+ ".svelte",
601
607
  ".jl",
608
+ ".v",
609
+ ".vh",
610
+ ".sv",
611
+ ".svh",
602
612
  ".r",
603
613
  ".R"
604
614
  ]);
@@ -634,6 +644,35 @@ function collectNonCodePaths(reasons) {
634
644
  }
635
645
  return result;
636
646
  }
647
+ function shrinkDimension(before, after) {
648
+ const dropped = Math.max(0, before - after);
649
+ return {
650
+ before,
651
+ after,
652
+ dropped,
653
+ dropRatio: before > 0 ? dropped / before : 0
654
+ };
655
+ }
656
+ function evaluateGraphShrinkGuard(previousGraph, nextGraph, options = {}) {
657
+ const threshold = options.threshold ?? DEFAULT_MAX_GRAPH_SHRINK_RATIO;
658
+ const nodes = shrinkDimension(previousGraph?.nodes.length ?? 0, nextGraph?.nodes.length ?? 0);
659
+ const edges = shrinkDimension(previousGraph?.edges.length ?? 0, nextGraph?.edges.length ?? 0);
660
+ const blocked = nodes.dropRatio > threshold || edges.dropRatio > threshold;
661
+ return {
662
+ blocked,
663
+ threshold,
664
+ nodes,
665
+ edges,
666
+ message: blocked ? `Graph update aborted: node count changed ${nodes.before} -> ${nodes.after} (${Math.round(
667
+ nodes.dropRatio * 100
668
+ )}% drop) and edge count changed ${edges.before} -> ${edges.after} (${Math.round(
669
+ edges.dropRatio * 100
670
+ )}% drop). Re-run with --force or SWARMVAULT_FORCE_UPDATE=1 if this shrink is expected.` : void 0
671
+ };
672
+ }
673
+ function forceGraphUpdateEnabled(options) {
674
+ return options.force === true || process2.env.SWARMVAULT_FORCE_UPDATE === "1" || process2.env.SWARMVAULT_FORCE_UPDATE === "true";
675
+ }
637
676
  function hasIgnoredRepoSegment(baseDir, targetPath) {
638
677
  const relativePath = path2.relative(baseDir, targetPath);
639
678
  if (!relativePath || relativePath.startsWith("..")) {
@@ -770,6 +809,7 @@ async function performWatchCycle(rootDir, paths, options, codeOnly = false) {
770
809
  }
771
810
  async function runWatchCycle(rootDir, options = {}) {
772
811
  const { paths } = await initWorkspace(rootDir);
812
+ const previousGraph = await readJsonFile(paths.graphPath);
773
813
  const startedAt = /* @__PURE__ */ new Date();
774
814
  let success = true;
775
815
  let error;
@@ -788,6 +828,12 @@ async function runWatchCycle(rootDir, options = {}) {
788
828
  };
789
829
  try {
790
830
  result = await performWatchCycle(rootDir, paths, options, options.codeOnly ?? false);
831
+ const nextGraph = await readJsonFile(paths.graphPath);
832
+ const guard = evaluateGraphShrinkGuard(previousGraph, nextGraph, { threshold: options.maxGraphShrinkRatio });
833
+ if (previousGraph && nextGraph && guard.blocked && !forceGraphUpdateEnabled(options)) {
834
+ await writeJsonFile(paths.graphPath, previousGraph);
835
+ throw new Error(guard.message ?? "Graph update aborted because the graph shrank unexpectedly.");
836
+ }
791
837
  return result;
792
838
  } catch (caught) {
793
839
  success = false;
@@ -3706,9 +3752,350 @@ Community: ${communityLabel}`,
3706
3752
  return { format: "canvas", outputPath: resolvedPath };
3707
3753
  }
3708
3754
 
3709
- // src/graph-push.ts
3755
+ // src/graph-merge.ts
3710
3756
  import fs5 from "fs/promises";
3711
3757
  import path5 from "path";
3758
+ function isRecord(value) {
3759
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3760
+ }
3761
+ function isSwarmVaultGraph(value) {
3762
+ return isRecord(value) && Array.isArray(value.nodes) && Array.isArray(value.edges) && Array.isArray(value.sources) && Array.isArray(value.pages);
3763
+ }
3764
+ function stringField(record, ...fields) {
3765
+ for (const field of fields) {
3766
+ const value = record[field];
3767
+ if (typeof value === "string" && value.trim()) {
3768
+ return value.trim();
3769
+ }
3770
+ if (typeof value === "number" && Number.isFinite(value)) {
3771
+ return String(value);
3772
+ }
3773
+ }
3774
+ return void 0;
3775
+ }
3776
+ function arrayStringField(record, field) {
3777
+ const value = record[field];
3778
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
3779
+ }
3780
+ function numberField(record, field, fallback) {
3781
+ const value = record[field];
3782
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
3783
+ }
3784
+ function safePrefix(inputPath, index) {
3785
+ return slugify(path5.basename(inputPath, path5.extname(inputPath)) || `graph-${index + 1}`);
3786
+ }
3787
+ function prefixed(prefix, id) {
3788
+ return `${prefix}:${id}`;
3789
+ }
3790
+ function ensureUniquePrefix(base, used) {
3791
+ let candidate = base || "graph";
3792
+ let suffix = 2;
3793
+ while (used.has(candidate)) {
3794
+ candidate = `${base}-${suffix}`;
3795
+ suffix += 1;
3796
+ }
3797
+ used.add(candidate);
3798
+ return candidate;
3799
+ }
3800
+ function mapEvidenceClass(value) {
3801
+ const normalized = typeof value === "string" ? value.toLowerCase() : "";
3802
+ if (normalized === "extracted") return "extracted";
3803
+ if (normalized === "ambiguous") return "ambiguous";
3804
+ return "inferred";
3805
+ }
3806
+ function mapNodeType(value) {
3807
+ const normalized = typeof value === "string" ? value.toLowerCase() : "";
3808
+ if (["source", "file", "document", "paper", "image", "video"].includes(normalized)) return "source";
3809
+ if (["module", "code"].includes(normalized)) return "module";
3810
+ if (["function", "class", "symbol", "method", "component"].includes(normalized)) return "symbol";
3811
+ if (["entity", "person", "org", "organization"].includes(normalized)) return "entity";
3812
+ if (["rationale", "comment", "docstring", "why"].includes(normalized)) return "rationale";
3813
+ if (["decision", "adr"].includes(normalized)) return "decision";
3814
+ return "concept";
3815
+ }
3816
+ function remapSwarmVaultGraph(inputPath, graph, prefix) {
3817
+ const sourceMap = new Map(graph.sources.map((source) => [source.sourceId, prefixed(prefix, source.sourceId)]));
3818
+ const pageMap = new Map(graph.pages.map((page) => [page.id, prefixed(prefix, page.id)]));
3819
+ const nodeMap = new Map(graph.nodes.map((node) => [node.id, prefixed(prefix, node.id)]));
3820
+ const communityMap = new Map((graph.communities ?? []).map((community) => [community.id, prefixed(prefix, community.id)]));
3821
+ const sources = graph.sources.map((source) => ({
3822
+ ...source,
3823
+ sourceId: sourceMap.get(source.sourceId) ?? prefixed(prefix, source.sourceId),
3824
+ title: `[${prefix}] ${source.title}`,
3825
+ sourceGroupId: source.sourceGroupId ? prefixed(prefix, source.sourceGroupId) : void 0,
3826
+ details: {
3827
+ ...source.details ?? {},
3828
+ mergedInput: inputPath,
3829
+ mergedPrefix: prefix
3830
+ }
3831
+ }));
3832
+ const pages = graph.pages.map((page) => ({
3833
+ ...page,
3834
+ id: pageMap.get(page.id) ?? prefixed(prefix, page.id),
3835
+ path: toPosix(path5.posix.join("merged", prefix, page.path)),
3836
+ sourceIds: page.sourceIds.map((sourceId) => sourceMap.get(sourceId) ?? prefixed(prefix, sourceId)),
3837
+ nodeIds: page.nodeIds.map((nodeId) => nodeMap.get(nodeId) ?? prefixed(prefix, nodeId)),
3838
+ relatedPageIds: page.relatedPageIds.map((pageId) => pageMap.get(pageId) ?? prefixed(prefix, pageId)),
3839
+ relatedNodeIds: page.relatedNodeIds.map((nodeId) => nodeMap.get(nodeId) ?? prefixed(prefix, nodeId)),
3840
+ relatedSourceIds: page.relatedSourceIds.map((sourceId) => sourceMap.get(sourceId) ?? prefixed(prefix, sourceId)),
3841
+ backlinks: page.backlinks.map((pageId) => pageMap.get(pageId) ?? prefixed(prefix, pageId)),
3842
+ supersededBy: page.supersededBy ? pageMap.get(page.supersededBy) ?? prefixed(prefix, page.supersededBy) : void 0
3843
+ }));
3844
+ const nodes = graph.nodes.map((node) => ({
3845
+ ...node,
3846
+ id: nodeMap.get(node.id) ?? prefixed(prefix, node.id),
3847
+ pageId: node.pageId ? pageMap.get(node.pageId) ?? prefixed(prefix, node.pageId) : void 0,
3848
+ sourceIds: node.sourceIds.map((sourceId) => sourceMap.get(sourceId) ?? prefixed(prefix, sourceId)),
3849
+ moduleId: node.moduleId ? nodeMap.get(node.moduleId) ?? prefixed(prefix, node.moduleId) : void 0,
3850
+ communityId: node.communityId ? communityMap.get(node.communityId) ?? prefixed(prefix, node.communityId) : void 0
3851
+ }));
3852
+ const edges = graph.edges.map((edge) => ({
3853
+ ...edge,
3854
+ id: prefixed(prefix, edge.id),
3855
+ source: nodeMap.get(edge.source) ?? prefixed(prefix, edge.source),
3856
+ target: nodeMap.get(edge.target) ?? prefixed(prefix, edge.target),
3857
+ provenance: edge.provenance.map((id) => nodeMap.get(id) ?? pageMap.get(id) ?? sourceMap.get(id) ?? prefixed(prefix, id))
3858
+ }));
3859
+ const hyperedges = graph.hyperedges.map((hyperedge) => ({
3860
+ ...hyperedge,
3861
+ id: prefixed(prefix, hyperedge.id),
3862
+ nodeIds: hyperedge.nodeIds.map((nodeId) => nodeMap.get(nodeId) ?? prefixed(prefix, nodeId)),
3863
+ sourcePageIds: hyperedge.sourcePageIds.map((pageId) => pageMap.get(pageId) ?? prefixed(prefix, pageId))
3864
+ }));
3865
+ return {
3866
+ generatedAt: graph.generatedAt,
3867
+ nodes,
3868
+ edges,
3869
+ hyperedges,
3870
+ communities: (graph.communities ?? []).map((community) => ({
3871
+ ...community,
3872
+ id: communityMap.get(community.id) ?? prefixed(prefix, community.id),
3873
+ label: `[${prefix}] ${community.label}`,
3874
+ nodeIds: community.nodeIds.map((nodeId) => nodeMap.get(nodeId) ?? prefixed(prefix, nodeId))
3875
+ })),
3876
+ sources,
3877
+ pages
3878
+ };
3879
+ }
3880
+ function nodeLinkArrays(raw) {
3881
+ const nodes = raw.nodes;
3882
+ const edges = Array.isArray(raw.links) ? raw.links : raw.edges;
3883
+ if (!Array.isArray(nodes) || !Array.isArray(edges)) {
3884
+ return null;
3885
+ }
3886
+ return {
3887
+ nodes,
3888
+ edges: edges.filter(isRecord)
3889
+ };
3890
+ }
3891
+ function nodeLinkNodeId(node, index) {
3892
+ if (typeof node === "string" || typeof node === "number") {
3893
+ return String(node);
3894
+ }
3895
+ return stringField(node, "id", "key", "name", "label") ?? `node-${index + 1}`;
3896
+ }
3897
+ function endpointId(value) {
3898
+ if (typeof value === "string" || typeof value === "number") {
3899
+ return String(value);
3900
+ }
3901
+ if (isRecord(value)) {
3902
+ return stringField(value, "id", "key", "name", "label");
3903
+ }
3904
+ return void 0;
3905
+ }
3906
+ function remapNodeLinkGraph(inputPath, raw, prefix, now) {
3907
+ const arrays = nodeLinkArrays(raw);
3908
+ if (!arrays) {
3909
+ throw new Error(`${inputPath} is not a SwarmVault graph or node-link graph.`);
3910
+ }
3911
+ const syntheticSourceId = prefixed(prefix, "source");
3912
+ const source = {
3913
+ sourceId: syntheticSourceId,
3914
+ title: `${prefix} merged graph`,
3915
+ originType: "file",
3916
+ sourceKind: "data",
3917
+ sourceClass: "generated",
3918
+ originalPath: inputPath,
3919
+ storedPath: inputPath,
3920
+ mimeType: "application/json",
3921
+ contentHash: `sha256:${sha256(JSON.stringify(raw)).slice(0, 24)}`,
3922
+ semanticHash: `sha256:${sha256(`${inputPath}:${arrays.nodes.length}:${arrays.edges.length}`).slice(0, 24)}`,
3923
+ details: {
3924
+ mergedInput: inputPath,
3925
+ mergedFormat: "node-link"
3926
+ },
3927
+ createdAt: now,
3928
+ updatedAt: now
3929
+ };
3930
+ const idMap = /* @__PURE__ */ new Map();
3931
+ const nodes = arrays.nodes.map((node, index) => {
3932
+ const originalId = nodeLinkNodeId(node, index);
3933
+ const mappedId = prefixed(prefix, originalId);
3934
+ idMap.set(originalId, mappedId);
3935
+ const record = isRecord(node) ? node : {};
3936
+ const label = stringField(record, "label", "name", "title", "path", "id") ?? originalId;
3937
+ const type = mapNodeType(record.type ?? record.file_type ?? record.kind ?? record.category);
3938
+ return {
3939
+ id: mappedId,
3940
+ type,
3941
+ label,
3942
+ sourceIds: [syntheticSourceId],
3943
+ projectIds: [],
3944
+ sourceClass: "generated",
3945
+ confidence: numberField(record, "confidence", numberField(record, "confidence_score", 1)),
3946
+ tags: arrayStringField(record, "tags")
3947
+ };
3948
+ });
3949
+ const nodeIds = new Set(nodes.map((node) => node.id));
3950
+ const edges = arrays.edges.flatMap((edge, index) => {
3951
+ const source2 = endpointId(edge.source ?? edge.from);
3952
+ const target = endpointId(edge.target ?? edge.to);
3953
+ if (!source2 || !target) {
3954
+ return [];
3955
+ }
3956
+ const mappedSource = idMap.get(source2) ?? prefixed(prefix, source2);
3957
+ const mappedTarget = idMap.get(target) ?? prefixed(prefix, target);
3958
+ if (!nodeIds.has(mappedSource) || !nodeIds.has(mappedTarget)) {
3959
+ return [];
3960
+ }
3961
+ const evidenceClass = mapEvidenceClass(edge.evidenceClass ?? edge.evidence_class ?? edge.status ?? edge.confidence);
3962
+ return [
3963
+ {
3964
+ id: prefixed(prefix, stringField(edge, "id", "key") ?? `edge-${index + 1}`),
3965
+ source: mappedSource,
3966
+ target: mappedTarget,
3967
+ relation: stringField(edge, "relation", "type", "label") ?? "related_to",
3968
+ status: evidenceClass === "extracted" ? "extracted" : "inferred",
3969
+ evidenceClass,
3970
+ confidence: numberField(edge, "confidence", numberField(edge, "confidence_score", evidenceClass === "ambiguous" ? 0.5 : 0.75)),
3971
+ provenance: [syntheticSourceId]
3972
+ }
3973
+ ];
3974
+ });
3975
+ const page = {
3976
+ id: prefixed(prefix, "page"),
3977
+ path: toPosix(path5.posix.join("merged", prefix, "index.md")),
3978
+ title: `${prefix} merged graph`,
3979
+ kind: "source",
3980
+ sourceClass: "generated",
3981
+ sourceIds: [syntheticSourceId],
3982
+ projectIds: [],
3983
+ nodeIds: nodes.map((node) => node.id),
3984
+ freshness: "fresh",
3985
+ status: "active",
3986
+ confidence: 1,
3987
+ backlinks: [],
3988
+ schemaHash: "merged-node-link",
3989
+ sourceHashes: { [syntheticSourceId]: source.contentHash },
3990
+ sourceSemanticHashes: { [syntheticSourceId]: source.semanticHash },
3991
+ relatedPageIds: [],
3992
+ relatedNodeIds: nodes.map((node) => node.id),
3993
+ relatedSourceIds: [syntheticSourceId],
3994
+ createdAt: now,
3995
+ updatedAt: now,
3996
+ compiledFrom: [inputPath],
3997
+ managedBy: "system"
3998
+ };
3999
+ return {
4000
+ generatedAt: now,
4001
+ nodes,
4002
+ edges,
4003
+ hyperedges: [],
4004
+ communities: [],
4005
+ sources: [source],
4006
+ pages: [page]
4007
+ };
4008
+ }
4009
+ function mergeGraphs(graphs, now) {
4010
+ return {
4011
+ generatedAt: now,
4012
+ nodes: uniqueBy(
4013
+ graphs.flatMap((graph) => graph.nodes),
4014
+ (node) => node.id
4015
+ ),
4016
+ edges: uniqueBy(
4017
+ graphs.flatMap((graph) => graph.edges),
4018
+ (edge) => edge.id
4019
+ ),
4020
+ hyperedges: uniqueBy(
4021
+ graphs.flatMap((graph) => graph.hyperedges),
4022
+ (hyperedge) => hyperedge.id
4023
+ ),
4024
+ communities: uniqueBy(
4025
+ graphs.flatMap((graph) => graph.communities ?? []),
4026
+ (community) => community.id
4027
+ ),
4028
+ sources: uniqueBy(
4029
+ graphs.flatMap((graph) => graph.sources),
4030
+ (source) => source.sourceId
4031
+ ),
4032
+ pages: uniqueBy(
4033
+ graphs.flatMap((graph) => graph.pages),
4034
+ (page) => page.id
4035
+ )
4036
+ };
4037
+ }
4038
+ async function mergeGraphFiles(inputPaths, outputPath, options = {}) {
4039
+ if (inputPaths.length === 0) {
4040
+ throw new Error("At least one graph JSON path is required.");
4041
+ }
4042
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4043
+ const usedPrefixes = /* @__PURE__ */ new Set();
4044
+ const graphs = [];
4045
+ const inputGraphs = [];
4046
+ const warnings = [];
4047
+ for (const [index, inputPath] of inputPaths.entries()) {
4048
+ const resolvedInputPath = path5.resolve(inputPath);
4049
+ const raw = JSON.parse(await fs5.readFile(resolvedInputPath, "utf8"));
4050
+ const prefix = ensureUniquePrefix(
4051
+ inputPaths.length === 1 && options.label ? slugify(options.label) : safePrefix(resolvedInputPath, index),
4052
+ usedPrefixes
4053
+ );
4054
+ if (isSwarmVaultGraph(raw)) {
4055
+ const graph2 = remapSwarmVaultGraph(resolvedInputPath, raw, prefix);
4056
+ graphs.push(graph2);
4057
+ inputGraphs.push({
4058
+ path: resolvedInputPath,
4059
+ label: prefix,
4060
+ format: "swarmvault",
4061
+ nodeCount: raw.nodes.length,
4062
+ edgeCount: raw.edges.length
4063
+ });
4064
+ continue;
4065
+ }
4066
+ if (isRecord(raw) && nodeLinkArrays(raw)) {
4067
+ const graph2 = remapNodeLinkGraph(resolvedInputPath, raw, prefix, now);
4068
+ graphs.push(graph2);
4069
+ inputGraphs.push({
4070
+ path: resolvedInputPath,
4071
+ label: prefix,
4072
+ format: "node-link",
4073
+ nodeCount: graph2.nodes.length,
4074
+ edgeCount: graph2.edges.length
4075
+ });
4076
+ continue;
4077
+ }
4078
+ warnings.push(`${resolvedInputPath} was skipped because it is not a supported graph JSON shape.`);
4079
+ }
4080
+ if (graphs.length === 0) {
4081
+ throw new Error("No supported graph inputs were found.");
4082
+ }
4083
+ const graph = mergeGraphs(graphs, now);
4084
+ const resolvedOutputPath = path5.resolve(outputPath);
4085
+ await ensureDir(path5.dirname(resolvedOutputPath));
4086
+ await fs5.writeFile(resolvedOutputPath, `${JSON.stringify(graph, null, 2)}
4087
+ `, "utf8");
4088
+ return {
4089
+ outputPath: resolvedOutputPath,
4090
+ graph,
4091
+ inputGraphs,
4092
+ warnings
4093
+ };
4094
+ }
4095
+
4096
+ // src/graph-push.ts
4097
+ import fs6 from "fs/promises";
4098
+ import path6 from "path";
3712
4099
  import neo4j from "neo4j-driver";
3713
4100
  var DEFAULT_NEO4J_BATCH_SIZE = 500;
3714
4101
  var DEFAULT_NEO4J_DATABASE = "neo4j";
@@ -3719,8 +4106,8 @@ function requireConfigValue(value, name) {
3719
4106
  throw new Error(`Neo4j push requires ${name}. Configure \`graphSinks.neo4j.${name}\` or pass the matching CLI flag.`);
3720
4107
  }
3721
4108
  async function deriveVaultId(rootDir) {
3722
- const realRoot = await fs5.realpath(rootDir).catch(() => path5.resolve(rootDir));
3723
- const label = slugify(path5.basename(realRoot));
4109
+ const realRoot = await fs6.realpath(rootDir).catch(() => path6.resolve(rootDir));
4110
+ const label = slugify(path6.basename(realRoot));
3724
4111
  return `${label}-${sha256(realRoot).slice(0, 12)}`;
3725
4112
  }
3726
4113
  async function resolveNeo4jPushConfig(rootDir, options) {
@@ -3750,7 +4137,7 @@ function normalizeBatchSize(value) {
3750
4137
  }
3751
4138
  async function loadGraph2(rootDir) {
3752
4139
  const { paths } = await loadVaultConfig(rootDir);
3753
- const raw = JSON.parse(await fs5.readFile(paths.graphPath, "utf8"));
4140
+ const raw = JSON.parse(await fs6.readFile(paths.graphPath, "utf8"));
3754
4141
  return raw;
3755
4142
  }
3756
4143
  function buildResult(input) {
@@ -3835,7 +4222,7 @@ async function writeSyncNode(session, input) {
3835
4222
  ].join("\n"),
3836
4223
  {
3837
4224
  vaultId: input.vaultId,
3838
- rootDir: path5.resolve(input.rootDir),
4225
+ rootDir: path6.resolve(input.rootDir),
3839
4226
  graphGeneratedAt: input.graph.generatedAt,
3840
4227
  graphHash: graphHash(input.graph),
3841
4228
  pushedAt: input.pushedAt,
@@ -3920,27 +4307,319 @@ async function pushGraphNeo4j(rootDir, options = {}) {
3920
4307
  }
3921
4308
  }
3922
4309
 
4310
+ // src/graph-status.ts
4311
+ import path7 from "path";
4312
+ function recommendedCommand(input) {
4313
+ if (!input.graphExists || !input.reportExists) {
4314
+ return "swarmvault compile";
4315
+ }
4316
+ if (input.semanticChangeCount > 0 || input.pendingSemanticRefreshCount > 0) {
4317
+ return "swarmvault compile";
4318
+ }
4319
+ if (input.codeChangeCount > 0) {
4320
+ return "swarmvault graph update";
4321
+ }
4322
+ return null;
4323
+ }
4324
+ async function getGraphStatus(rootDir, options = {}) {
4325
+ const { paths } = await loadVaultConfig(rootDir);
4326
+ const graphPath = paths.graphPath;
4327
+ const reportPath = path7.join(paths.wikiDir, "graph", "report.md");
4328
+ const resolvedOverrideRoots = options.repoRoots?.map((repoRoot) => path7.resolve(rootDir, repoRoot));
4329
+ const [graphExists, reportExists, trackedRepoRoots, changes, pendingSemanticRefresh] = await Promise.all([
4330
+ fileExists(graphPath),
4331
+ fileExists(reportPath),
4332
+ resolvedOverrideRoots ? Promise.resolve([...new Set(resolvedOverrideRoots)].sort((left, right) => left.localeCompare(right))) : listTrackedRepoRoots(rootDir),
4333
+ checkTrackedRepoChanges(rootDir, resolvedOverrideRoots),
4334
+ readJsonFile(paths.pendingSemanticRefreshPath).then((entries) => Array.isArray(entries) ? entries : [])
4335
+ ]);
4336
+ const codeChangeCount = changes.filter((change) => change.refreshType === "code").length;
4337
+ const semanticChangeCount = changes.filter((change) => change.refreshType === "semantic").length;
4338
+ const command = recommendedCommand({
4339
+ graphExists,
4340
+ reportExists,
4341
+ codeChangeCount,
4342
+ semanticChangeCount,
4343
+ pendingSemanticRefreshCount: pendingSemanticRefresh.length
4344
+ });
4345
+ return {
4346
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4347
+ graphExists,
4348
+ graphPath,
4349
+ reportExists,
4350
+ reportPath,
4351
+ trackedRepoRoots,
4352
+ codeChangeCount,
4353
+ semanticChangeCount,
4354
+ pendingSemanticRefresh,
4355
+ stale: Boolean(command),
4356
+ recommendedCommand: command,
4357
+ changes
4358
+ };
4359
+ }
4360
+
4361
+ // src/graph-tree.ts
4362
+ import path8 from "path";
4363
+ var DEFAULT_MAX_CHILDREN = 250;
4364
+ function compareTreeNodes(left, right) {
4365
+ const kindOrder = /* @__PURE__ */ new Map([
4366
+ ["directory", 0],
4367
+ ["source", 1],
4368
+ ["module", 2],
4369
+ ["symbol", 3],
4370
+ ["rationale", 4],
4371
+ ["node", 5],
4372
+ ["more", 6],
4373
+ ["root", 7]
4374
+ ]);
4375
+ const leftKind = kindOrder.get(left.kind) ?? 99;
4376
+ const rightKind = kindOrder.get(right.kind) ?? 99;
4377
+ return leftKind - rightKind || left.label.localeCompare(right.label) || left.id.localeCompare(right.id);
4378
+ }
4379
+ function escapeHtml(value) {
4380
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
4381
+ }
4382
+ function normalizeSourcePath(rootDir, source) {
4383
+ const candidate = source.repoRelativePath ?? source.originalPath ?? source.storedPath ?? source.title;
4384
+ if (source.repoRelativePath) {
4385
+ return toPosix(source.repoRelativePath);
4386
+ }
4387
+ if (rootDir && path8.isAbsolute(candidate) && isPathWithin(rootDir, candidate)) {
4388
+ return toPosix(path8.relative(rootDir, candidate));
4389
+ }
4390
+ if (path8.isAbsolute(candidate)) {
4391
+ return toPosix(path8.basename(candidate));
4392
+ }
4393
+ return toPosix(candidate).replace(/^\/+/, "") || source.title || source.sourceId;
4394
+ }
4395
+ function makeDirectoryNode(parentId, segment) {
4396
+ return {
4397
+ id: `${parentId}/${segment}`,
4398
+ label: segment,
4399
+ kind: "directory",
4400
+ count: 0,
4401
+ children: []
4402
+ };
4403
+ }
4404
+ function ensureDirectory(parent, segment) {
4405
+ const existing = parent.children.find((child) => child.kind === "directory" && child.label === segment);
4406
+ if (existing) {
4407
+ existing.count += 1;
4408
+ return existing;
4409
+ }
4410
+ const created = makeDirectoryNode(parent.id, segment);
4411
+ created.count = 1;
4412
+ parent.children.push(created);
4413
+ return created;
4414
+ }
4415
+ function nodeChildrenForSource(sourceId, nodes) {
4416
+ const sourceNodes = nodes.filter((node) => node.sourceIds.includes(sourceId));
4417
+ const modules = sourceNodes.filter((node) => node.type === "module").sort((left, right) => left.label.localeCompare(right.label));
4418
+ const moduleIds = new Set(modules.map((node) => node.id));
4419
+ const symbols = sourceNodes.filter((node) => node.type === "symbol");
4420
+ const rationales = sourceNodes.filter((node) => node.type === "rationale");
4421
+ const children = [];
4422
+ for (const moduleNode of modules) {
4423
+ const moduleChildren = symbols.filter((symbol) => symbol.moduleId === moduleNode.id).sort((left, right) => left.label.localeCompare(right.label)).map((symbol) => graphNodeToTreeNode(symbol, "symbol"));
4424
+ children.push({
4425
+ id: `tree:${moduleNode.id}`,
4426
+ label: moduleNode.label,
4427
+ kind: "module",
4428
+ count: moduleChildren.length,
4429
+ children: moduleChildren,
4430
+ nodeId: moduleNode.id,
4431
+ sourceId,
4432
+ language: moduleNode.language
4433
+ });
4434
+ }
4435
+ for (const symbol of symbols.filter((node) => !node.moduleId || !moduleIds.has(node.moduleId))) {
4436
+ children.push(graphNodeToTreeNode(symbol, "symbol"));
4437
+ }
4438
+ for (const rationale of rationales) {
4439
+ children.push(graphNodeToTreeNode(rationale, "rationale"));
4440
+ }
4441
+ return children.sort(compareTreeNodes);
4442
+ }
4443
+ function graphNodeToTreeNode(node, kind) {
4444
+ return {
4445
+ id: `tree:${node.id}`,
4446
+ label: node.label,
4447
+ kind,
4448
+ count: 0,
4449
+ children: [],
4450
+ nodeId: node.id,
4451
+ language: node.language,
4452
+ symbolKind: node.symbolKind
4453
+ };
4454
+ }
4455
+ function sortAndCapTree(node, maxChildren) {
4456
+ const sortedChildren = node.children.map((child) => sortAndCapTree(child, maxChildren)).sort(compareTreeNodes);
4457
+ if (sortedChildren.length <= maxChildren) {
4458
+ return { ...node, children: sortedChildren };
4459
+ }
4460
+ const visible = sortedChildren.slice(0, maxChildren);
4461
+ const hidden = sortedChildren.length - visible.length;
4462
+ return {
4463
+ ...node,
4464
+ hiddenChildren: hidden,
4465
+ children: [
4466
+ ...visible,
4467
+ {
4468
+ id: `${node.id}:more`,
4469
+ label: `+${hidden} more`,
4470
+ kind: "more",
4471
+ count: hidden,
4472
+ children: []
4473
+ }
4474
+ ]
4475
+ };
4476
+ }
4477
+ function buildGraphTree(graph, options = {}) {
4478
+ const root = {
4479
+ id: "tree:root",
4480
+ label: options.label ?? "SwarmVault Graph Tree",
4481
+ kind: "root",
4482
+ count: graph.sources.length,
4483
+ children: []
4484
+ };
4485
+ const nodes = [...graph.nodes];
4486
+ for (const source of [...graph.sources].sort(
4487
+ (left, right) => normalizeSourcePath(options.rootDir, left).localeCompare(normalizeSourcePath(options.rootDir, right))
4488
+ )) {
4489
+ const normalizedPath = normalizeSourcePath(options.rootDir, source);
4490
+ const segments = normalizedPath.split("/").filter(Boolean);
4491
+ const fileLabel = segments.pop() ?? source.title ?? source.sourceId;
4492
+ let parent = root;
4493
+ for (const segment of segments) {
4494
+ parent = ensureDirectory(parent, segment);
4495
+ }
4496
+ const children = nodeChildrenForSource(source.sourceId, nodes);
4497
+ parent.children.push({
4498
+ id: `tree:source:${source.sourceId}`,
4499
+ label: fileLabel,
4500
+ kind: "source",
4501
+ count: children.length,
4502
+ children,
4503
+ path: normalizedPath,
4504
+ sourceId: source.sourceId,
4505
+ language: source.language
4506
+ });
4507
+ }
4508
+ return sortAndCapTree(root, Math.max(1, options.maxChildren ?? DEFAULT_MAX_CHILDREN));
4509
+ }
4510
+ function renderNode(node) {
4511
+ const meta = [
4512
+ node.kind,
4513
+ node.language,
4514
+ node.symbolKind,
4515
+ node.path,
4516
+ node.nodeId,
4517
+ node.sourceId,
4518
+ node.count ? `${node.count} item${node.count === 1 ? "" : "s"}` : void 0
4519
+ ].filter(Boolean);
4520
+ const content = [
4521
+ `<span class="label">${escapeHtml(node.label)}</span>`,
4522
+ meta.length ? `<span class="meta">${escapeHtml(meta.join(" \xB7 "))}</span>` : ""
4523
+ ].join("");
4524
+ if (node.children.length === 0) {
4525
+ return `<li class="tree-node kind-${escapeHtml(node.kind)}">${content}</li>`;
4526
+ }
4527
+ return `<li class="tree-node kind-${escapeHtml(node.kind)}"><details open><summary>${content}</summary><ul>${node.children.map(renderNode).join("")}</ul></details></li>`;
4528
+ }
4529
+ function renderGraphTreeHtml(tree, graph) {
4530
+ return `<!doctype html>
4531
+ <html lang="en">
4532
+ <head>
4533
+ <meta charset="utf-8">
4534
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4535
+ <title>${escapeHtml(tree.label)}</title>
4536
+ <style>
4537
+ :root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
4538
+ body { margin: 0; background: #f7f7f5; color: #171717; }
4539
+ main { max-width: 1120px; margin: 0 auto; padding: 32px 20px 56px; }
4540
+ h1 { font-size: 28px; line-height: 1.2; margin: 0 0 8px; }
4541
+ .subtitle { color: #5d5d5d; margin: 0 0 20px; }
4542
+ .toolbar { display: flex; gap: 12px; align-items: center; margin: 0 0 18px; }
4543
+ input { flex: 1; min-width: 0; border: 1px solid #c9c9c9; border-radius: 6px; padding: 10px 12px; font: inherit; background: #fff; color: inherit; }
4544
+ .tree { background: #fff; border: 1px solid #deded9; border-radius: 8px; padding: 16px 18px; }
4545
+ ul { list-style: none; margin: 0; padding-left: 20px; }
4546
+ .tree > ul { padding-left: 0; }
4547
+ li { margin: 4px 0; }
4548
+ summary { cursor: pointer; }
4549
+ .label { font-weight: 600; }
4550
+ .meta { color: #666; font-size: 12px; margin-left: 8px; }
4551
+ .kind-directory > details > summary .label { color: #245b78; }
4552
+ .kind-source > details > summary .label, .kind-source > .label { color: #22543d; }
4553
+ .kind-module > details > summary .label { color: #6b3f12; }
4554
+ .kind-rationale > .label { color: #6d2f46; }
4555
+ .hidden { display: none !important; }
4556
+ @media (prefers-color-scheme: dark) {
4557
+ body { background: #161616; color: #efefef; }
4558
+ .subtitle, .meta { color: #ababab; }
4559
+ input, .tree { background: #202020; border-color: #3a3a3a; }
4560
+ }
4561
+ </style>
4562
+ </head>
4563
+ <body>
4564
+ <main>
4565
+ <h1>${escapeHtml(tree.label)}</h1>
4566
+ <p class="subtitle">${graph.sources.length} sources \xB7 ${graph.nodes.length} nodes \xB7 ${graph.edges.length} edges \xB7 generated ${escapeHtml(graph.generatedAt)}</p>
4567
+ <div class="toolbar"><input id="filter" type="search" placeholder="Filter files, modules, symbols, or ids" aria-label="Filter graph tree"></div>
4568
+ <section class="tree"><ul>${renderNode(tree)}</ul></section>
4569
+ </main>
4570
+ <script>
4571
+ const input = document.getElementById('filter');
4572
+ input.addEventListener('input', () => {
4573
+ const query = input.value.trim().toLowerCase();
4574
+ for (const node of document.querySelectorAll('.tree-node')) {
4575
+ const text = node.textContent.toLowerCase();
4576
+ node.classList.toggle('hidden', query.length > 0 && !text.includes(query));
4577
+ }
4578
+ });
4579
+ </script>
4580
+ </body>
4581
+ </html>
4582
+ `;
4583
+ }
4584
+ async function exportGraphTree(rootDir, outputPath, options = {}) {
4585
+ const { paths } = await loadVaultConfig(rootDir);
4586
+ const graph = await readJsonFile(paths.graphPath);
4587
+ if (!graph) {
4588
+ throw new Error(`Graph artifact not found at ${paths.graphPath}. Run swarmvault compile first.`);
4589
+ }
4590
+ const tree = buildGraphTree(graph, { ...options, rootDir });
4591
+ const resolvedOutputPath = path8.resolve(rootDir, outputPath ?? path8.join(paths.wikiDir, "graph", "tree.html"));
4592
+ await ensureDir(path8.dirname(resolvedOutputPath));
4593
+ await import("fs/promises").then((fs13) => fs13.writeFile(resolvedOutputPath, renderGraphTreeHtml(tree, graph), "utf8"));
4594
+ return {
4595
+ outputPath: resolvedOutputPath,
4596
+ sourceCount: graph.sources.length,
4597
+ nodeCount: graph.nodes.length,
4598
+ tree
4599
+ };
4600
+ }
4601
+
3923
4602
  // src/hooks.ts
3924
- import fs6 from "fs/promises";
3925
- import path6 from "path";
4603
+ import fs7 from "fs/promises";
4604
+ import path9 from "path";
3926
4605
  import process3 from "process";
3927
4606
  var hookStart = "# >>> swarmvault hook >>>";
3928
4607
  var hookEnd = "# <<< swarmvault hook <<<";
3929
4608
  async function findNearestGitRoot(startPath) {
3930
- let current = path6.resolve(startPath);
4609
+ let current = path9.resolve(startPath);
3931
4610
  try {
3932
- const stat = await fs6.stat(current);
4611
+ const stat = await fs7.stat(current);
3933
4612
  if (!stat.isDirectory()) {
3934
- current = path6.dirname(current);
4613
+ current = path9.dirname(current);
3935
4614
  }
3936
4615
  } catch {
3937
- current = path6.dirname(current);
4616
+ current = path9.dirname(current);
3938
4617
  }
3939
4618
  while (true) {
3940
- if (await fileExists(path6.join(current, ".git"))) {
4619
+ if (await fileExists(path9.join(current, ".git"))) {
3941
4620
  return current;
3942
4621
  }
3943
- const parent = path6.dirname(current);
4622
+ const parent = path9.dirname(current);
3944
4623
  if (parent === current) {
3945
4624
  return null;
3946
4625
  }
@@ -3952,8 +4631,8 @@ function shellQuote(value) {
3952
4631
  }
3953
4632
  function resolveSwarmvaultExecutableCandidate() {
3954
4633
  const argvPath = process3.argv[1];
3955
- if (typeof argvPath === "string" && argvPath.trim() && (argvPath.includes(`${path6.sep}@swarmvaultai${path6.sep}cli${path6.sep}`) || argvPath.includes(`${path6.sep}packages${path6.sep}cli${path6.sep}`))) {
3956
- return path6.resolve(argvPath);
4634
+ if (typeof argvPath === "string" && argvPath.trim() && (argvPath.includes(`${path9.sep}@swarmvaultai${path9.sep}cli${path9.sep}`) || argvPath.includes(`${path9.sep}packages${path9.sep}cli${path9.sep}`))) {
4635
+ return path9.resolve(argvPath);
3957
4636
  }
3958
4637
  return "swarmvault";
3959
4638
  }
@@ -3972,17 +4651,17 @@ function managedHookBlock(vaultRoot) {
3972
4651
  ].join("\n");
3973
4652
  }
3974
4653
  function hookPath(repoRoot, hookName) {
3975
- return path6.join(repoRoot, ".git", "hooks", hookName);
4654
+ return path9.join(repoRoot, ".git", "hooks", hookName);
3976
4655
  }
3977
4656
  async function readHookStatus(filePath) {
3978
4657
  if (!await fileExists(filePath)) {
3979
4658
  return "not_installed";
3980
4659
  }
3981
- const content = await fs6.readFile(filePath, "utf8");
4660
+ const content = await fs7.readFile(filePath, "utf8");
3982
4661
  return content.includes(hookStart) && content.includes(hookEnd) ? "installed" : "other_content";
3983
4662
  }
3984
4663
  async function upsertHookFile(filePath, block) {
3985
- const existing = await fileExists(filePath) ? await fs6.readFile(filePath, "utf8") : "";
4664
+ const existing = await fileExists(filePath) ? await fs7.readFile(filePath, "utf8") : "";
3986
4665
  let next;
3987
4666
  const startIndex = existing.indexOf(hookStart);
3988
4667
  const endIndex = existing.indexOf(hookEnd);
@@ -3996,16 +4675,16 @@ ${block}`.trimEnd();
3996
4675
  next = `#!/bin/sh
3997
4676
  ${block}`.trimEnd();
3998
4677
  }
3999
- await ensureDir(path6.dirname(filePath));
4000
- await fs6.writeFile(filePath, `${next}
4678
+ await ensureDir(path9.dirname(filePath));
4679
+ await fs7.writeFile(filePath, `${next}
4001
4680
  `, { mode: 493, encoding: "utf8" });
4002
- await fs6.chmod(filePath, 493);
4681
+ await fs7.chmod(filePath, 493);
4003
4682
  }
4004
4683
  async function removeHookBlock(filePath) {
4005
4684
  if (!await fileExists(filePath)) {
4006
4685
  return;
4007
4686
  }
4008
- const existing = await fs6.readFile(filePath, "utf8");
4687
+ const existing = await fs7.readFile(filePath, "utf8");
4009
4688
  const startIndex = existing.indexOf(hookStart);
4010
4689
  const endIndex = existing.indexOf(hookEnd);
4011
4690
  if (startIndex === -1 || endIndex === -1) {
@@ -4013,10 +4692,10 @@ async function removeHookBlock(filePath) {
4013
4692
  }
4014
4693
  const next = `${existing.slice(0, startIndex)}${existing.slice(endIndex + hookEnd.length)}`.trim();
4015
4694
  if (!next || next === "#!/bin/sh") {
4016
- await fs6.rm(filePath, { force: true });
4695
+ await fs7.rm(filePath, { force: true });
4017
4696
  return;
4018
4697
  }
4019
- await fs6.writeFile(filePath, `${next}
4698
+ await fs7.writeFile(filePath, `${next}
4020
4699
  `, "utf8");
4021
4700
  }
4022
4701
  async function getGitHookStatus(rootDir) {
@@ -4039,7 +4718,7 @@ async function installGitHooks(rootDir) {
4039
4718
  if (!repoRoot) {
4040
4719
  throw new Error("No git repository found above the current vault.");
4041
4720
  }
4042
- const block = managedHookBlock(path6.resolve(rootDir));
4721
+ const block = managedHookBlock(path9.resolve(rootDir));
4043
4722
  await upsertHookFile(hookPath(repoRoot, "post-commit"), block);
4044
4723
  await upsertHookFile(hookPath(repoRoot, "post-checkout"), block);
4045
4724
  return getGitHookStatus(rootDir);
@@ -4059,12 +4738,12 @@ async function uninstallGitHooks(rootDir) {
4059
4738
  }
4060
4739
 
4061
4740
  // src/mcp.ts
4062
- import fs7 from "fs/promises";
4063
- import path7 from "path";
4741
+ import fs8 from "fs/promises";
4742
+ import path10 from "path";
4064
4743
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
4065
4744
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4066
4745
  import { z } from "zod";
4067
- var SERVER_VERSION = "3.5.0";
4746
+ var SERVER_VERSION = "3.7.0";
4068
4747
  async function createMcpServer(rootDir) {
4069
4748
  const server = new McpServer({
4070
4749
  name: "swarmvault",
@@ -4202,6 +4881,18 @@ async function createMcpServer(rootDir) {
4202
4881
  return asToolText(await graphStatsVault(rootDir));
4203
4882
  })
4204
4883
  );
4884
+ server.registerTool(
4885
+ "cluster_graph",
4886
+ {
4887
+ description: "Recompute graph communities, node degrees, god-node flags, and graph report artifacts from the existing compiled graph.",
4888
+ inputSchema: {
4889
+ resolution: z.number().positive().optional().describe("Optional Louvain community resolution override")
4890
+ }
4891
+ },
4892
+ safeHandler(async ({ resolution }) => {
4893
+ return asToolText(await refreshGraphClusters(rootDir, { resolution }));
4894
+ })
4895
+ );
4205
4896
  server.registerTool(
4206
4897
  "get_node",
4207
4898
  {
@@ -4760,7 +5451,7 @@ async function createMcpServer(rootDir) {
4760
5451
  },
4761
5452
  async () => {
4762
5453
  const { paths } = await loadVaultConfig(rootDir);
4763
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path7.relative(paths.sessionsDir, filePath))).sort();
5454
+ const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path10.relative(paths.sessionsDir, filePath))).sort();
4764
5455
  return asTextResource("swarmvault://sessions", JSON.stringify(files, null, 2));
4765
5456
  }
4766
5457
  );
@@ -4829,8 +5520,8 @@ async function createMcpServer(rootDir) {
4829
5520
  return asTextResource(`swarmvault://pages/${encodedPath}`, `Page not found: ${relativePath}`);
4830
5521
  }
4831
5522
  const { paths } = await loadVaultConfig(rootDir);
4832
- const absolutePath = path7.resolve(paths.wikiDir, relativePath);
4833
- return asTextResource(`swarmvault://pages/${encodedPath}`, await fs7.readFile(absolutePath, "utf8"));
5523
+ const absolutePath = path10.resolve(paths.wikiDir, relativePath);
5524
+ return asTextResource(`swarmvault://pages/${encodedPath}`, await fs8.readFile(absolutePath, "utf8"));
4834
5525
  }
4835
5526
  );
4836
5527
  server.registerResource(
@@ -4838,11 +5529,11 @@ async function createMcpServer(rootDir) {
4838
5529
  new ResourceTemplate("swarmvault://sessions/{path}", {
4839
5530
  list: async () => {
4840
5531
  const { paths } = await loadVaultConfig(rootDir);
4841
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path7.relative(paths.sessionsDir, filePath))).sort();
5532
+ const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path10.relative(paths.sessionsDir, filePath))).sort();
4842
5533
  return {
4843
5534
  resources: files.map((relativePath) => ({
4844
5535
  uri: `swarmvault://sessions/${encodeURIComponent(relativePath)}`,
4845
- name: path7.basename(relativePath, ".md"),
5536
+ name: path10.basename(relativePath, ".md"),
4846
5537
  title: relativePath,
4847
5538
  description: "SwarmVault session artifact",
4848
5539
  mimeType: "text/markdown"
@@ -4859,11 +5550,11 @@ async function createMcpServer(rootDir) {
4859
5550
  const { paths } = await loadVaultConfig(rootDir);
4860
5551
  const encodedPath = typeof variables.path === "string" ? variables.path : "";
4861
5552
  const relativePath = decodeURIComponent(encodedPath);
4862
- const absolutePath = path7.resolve(paths.sessionsDir, relativePath);
5553
+ const absolutePath = path10.resolve(paths.sessionsDir, relativePath);
4863
5554
  if (!isPathWithin(paths.sessionsDir, absolutePath) || !await fileExists(absolutePath)) {
4864
5555
  return asTextResource(`swarmvault://sessions/${encodedPath}`, `Session not found: ${relativePath}`);
4865
5556
  }
4866
- return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs7.readFile(absolutePath, "utf8"));
5557
+ return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs8.readFile(absolutePath, "utf8"));
4867
5558
  }
4868
5559
  );
4869
5560
  return server;
@@ -4923,9 +5614,9 @@ function asTextResource(uri, text) {
4923
5614
 
4924
5615
  // src/providers/local-whisper-setup.ts
4925
5616
  import { createWriteStream, constants as fsConstants } from "fs";
4926
- import fs8 from "fs/promises";
5617
+ import fs9 from "fs/promises";
4927
5618
  import os from "os";
4928
- import path8 from "path";
5619
+ import path11 from "path";
4929
5620
  import { Readable } from "stream";
4930
5621
  import { pipeline } from "stream/promises";
4931
5622
  var BINARY_CANDIDATES = ["whisper-cli", "whisper-cpp", "whisper"];
@@ -4953,10 +5644,10 @@ async function discoverLocalWhisperBinary(options = {}) {
4953
5644
  }
4954
5645
  const pathValue = env.PATH ?? "";
4955
5646
  const candidates = [];
4956
- for (const dir of pathValue.split(path8.delimiter)) {
5647
+ for (const dir of pathValue.split(path11.delimiter)) {
4957
5648
  if (!dir) continue;
4958
5649
  for (const name of BINARY_CANDIDATES) {
4959
- const full = path8.join(dir, name);
5650
+ const full = path11.join(dir, name);
4960
5651
  candidates.push(full);
4961
5652
  if (await isExecutable(full)) {
4962
5653
  return { binaryPath: full, candidates, source: "path" };
@@ -4967,14 +5658,14 @@ async function discoverLocalWhisperBinary(options = {}) {
4967
5658
  }
4968
5659
  function expectedModelPath(modelName, homeDir) {
4969
5660
  const home = homeDir ?? os.homedir();
4970
- return path8.join(home, ".swarmvault", "models", `ggml-${modelName}.bin`);
5661
+ return path11.join(home, ".swarmvault", "models", `ggml-${modelName}.bin`);
4971
5662
  }
4972
5663
  function modelDownloadUrl(modelName) {
4973
5664
  return `${HUGGINGFACE_BASE}/ggml-${modelName}.bin`;
4974
5665
  }
4975
5666
  async function downloadWhisperModel(options) {
4976
5667
  const destPath = expectedModelPath(options.modelName, options.homeDir);
4977
- await ensureDir(path8.dirname(destPath));
5668
+ await ensureDir(path11.dirname(destPath));
4978
5669
  const doFetch = options.fetchImpl ?? fetch;
4979
5670
  const url = modelDownloadUrl(options.modelName);
4980
5671
  const response = await doFetch(url);
@@ -4995,8 +5686,8 @@ async function downloadWhisperModel(options) {
4995
5686
  });
4996
5687
  const tmpPath = `${destPath}.part`;
4997
5688
  await pipeline(source, createWriteStream(tmpPath));
4998
- await fs8.rename(tmpPath, destPath);
4999
- const stat = await fs8.stat(destPath);
5689
+ await fs9.rename(tmpPath, destPath);
5690
+ const stat = await fs9.stat(destPath);
5000
5691
  return { path: destPath, bytes: stat.size };
5001
5692
  }
5002
5693
  async function registerLocalWhisperProvider(options) {
@@ -5063,7 +5754,7 @@ async function summarizeLocalWhisperSetup(options) {
5063
5754
  }
5064
5755
  async function isExecutable(p) {
5065
5756
  try {
5066
- await fs8.access(p, fsConstants.X_OK);
5757
+ await fs9.access(p, fsConstants.X_OK);
5067
5758
  return true;
5068
5759
  } catch {
5069
5760
  return false;
@@ -5133,13 +5824,13 @@ async function withCapabilityFallback(provider, capability, run, fallback) {
5133
5824
  }
5134
5825
 
5135
5826
  // src/schedule.ts
5136
- import fs9 from "fs/promises";
5137
- import path9 from "path";
5827
+ import fs10 from "fs/promises";
5828
+ import path12 from "path";
5138
5829
  function scheduleStatePath(schedulesDir, jobId) {
5139
- return path9.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
5830
+ return path12.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
5140
5831
  }
5141
5832
  function scheduleLockPath(schedulesDir, jobId) {
5142
- return path9.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
5833
+ return path12.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
5143
5834
  }
5144
5835
  function parseEveryDuration(value) {
5145
5836
  const match = value.trim().match(/^(\d+)(m|h|d)$/i);
@@ -5242,13 +5933,13 @@ async function acquireJobLease(rootDir, jobId) {
5242
5933
  const { paths } = await loadVaultConfig(rootDir);
5243
5934
  const leasePath = scheduleLockPath(paths.schedulesDir, jobId);
5244
5935
  await ensureDir(paths.schedulesDir);
5245
- const handle = await fs9.open(leasePath, "wx");
5936
+ const handle = await fs10.open(leasePath, "wx");
5246
5937
  await handle.writeFile(`${process.pid}
5247
5938
  ${(/* @__PURE__ */ new Date()).toISOString()}
5248
5939
  `);
5249
5940
  await handle.close();
5250
5941
  return async () => {
5251
- await fs9.rm(leasePath, { force: true });
5942
+ await fs10.rm(leasePath, { force: true });
5252
5943
  };
5253
5944
  }
5254
5945
  async function listSchedules(rootDir) {
@@ -5407,8 +6098,8 @@ async function serveSchedules(rootDir, pollMs = 3e4) {
5407
6098
 
5408
6099
  // src/sources.ts
5409
6100
  import { spawn } from "child_process";
5410
- import fs10 from "fs/promises";
5411
- import path10 from "path";
6101
+ import fs11 from "fs/promises";
6102
+ import path13 from "path";
5412
6103
  import matter3 from "gray-matter";
5413
6104
  import { JSDOM } from "jsdom";
5414
6105
  var DEFAULT_CRAWL_MAX_PAGES = 12;
@@ -5454,24 +6145,24 @@ function emptyManagedSourceSyncCounts() {
5454
6145
  };
5455
6146
  }
5456
6147
  function withinRoot(rootPath, targetPath) {
5457
- const relative = path10.relative(rootPath, targetPath);
5458
- return relative === "" || !relative.startsWith("..") && !path10.isAbsolute(relative);
6148
+ const relative = path13.relative(rootPath, targetPath);
6149
+ return relative === "" || !relative.startsWith("..") && !path13.isAbsolute(relative);
5459
6150
  }
5460
6151
  async function findNearestGitRoot2(startPath) {
5461
- let current = path10.resolve(startPath);
6152
+ let current = path13.resolve(startPath);
5462
6153
  try {
5463
- const stat = await fs10.stat(current);
6154
+ const stat = await fs11.stat(current);
5464
6155
  if (!stat.isDirectory()) {
5465
- current = path10.dirname(current);
6156
+ current = path13.dirname(current);
5466
6157
  }
5467
6158
  } catch {
5468
- current = path10.dirname(current);
6159
+ current = path13.dirname(current);
5469
6160
  }
5470
6161
  while (true) {
5471
- if (await fileExists(path10.join(current, ".git"))) {
6162
+ if (await fileExists(path13.join(current, ".git"))) {
5472
6163
  return current;
5473
6164
  }
5474
- const parent = path10.dirname(current);
6165
+ const parent = path13.dirname(current);
5475
6166
  if (parent === current) {
5476
6167
  return null;
5477
6168
  }
@@ -5545,7 +6236,7 @@ function isAllowedDocsCandidate(candidate, startUrl) {
5545
6236
  if (candidate.origin !== startUrl.origin) {
5546
6237
  return false;
5547
6238
  }
5548
- const extension = path10.extname(candidate.pathname).toLowerCase();
6239
+ const extension = path13.extname(candidate.pathname).toLowerCase();
5549
6240
  if (extension && extension !== ".html" && extension !== ".htm" && extension !== ".md") {
5550
6241
  return false;
5551
6242
  }
@@ -5634,14 +6325,40 @@ function matchesManagedSourceSpec(existing, input) {
5634
6325
  return false;
5635
6326
  }
5636
6327
  if (input.kind === "directory" || input.kind === "file") {
5637
- return path10.resolve(existing.path ?? "") === path10.resolve(input.path);
6328
+ return path13.resolve(existing.path ?? "") === path13.resolve(input.path);
6329
+ }
6330
+ if (input.kind === "github_repo") {
6331
+ return (existing.url ?? "") === input.url && (existing.branch ?? "") === (input.branch ?? "") && (existing.ref ?? "") === (input.ref ?? "");
5638
6332
  }
5639
6333
  return (existing.url ?? "") === input.url;
5640
6334
  }
5641
- async function resolveManagedSourceInput(rootDir, input) {
5642
- const absoluteInput = path10.resolve(rootDir, input);
6335
+ function normalizeGitSelector(value, label) {
6336
+ const trimmed = value?.trim();
6337
+ if (!trimmed) {
6338
+ return void 0;
6339
+ }
6340
+ if (trimmed.startsWith("-")) {
6341
+ throw new Error(`Git ${label} must not start with "-".`);
6342
+ }
6343
+ if (/[\s\0]/.test(trimmed)) {
6344
+ throw new Error(`Git ${label} must not contain whitespace or NUL bytes.`);
6345
+ }
6346
+ return trimmed;
6347
+ }
6348
+ function normalizeCheckoutDir(rootDir, value) {
6349
+ const trimmed = value?.trim();
6350
+ if (!trimmed) {
6351
+ return void 0;
6352
+ }
6353
+ return path13.isAbsolute(trimmed) ? path13.resolve(trimmed) : path13.resolve(rootDir, trimmed);
6354
+ }
6355
+ async function resolveManagedSourceInput(rootDir, input, options = {}) {
6356
+ const absoluteInput = path13.resolve(rootDir, input);
5643
6357
  if (!(input.startsWith("http://") || input.startsWith("https://"))) {
5644
- const stat = await fs10.stat(absoluteInput).catch(() => null);
6358
+ if (options.branch || options.ref || options.checkoutDir) {
6359
+ throw new Error("Git branch/ref/checkout options are only supported for public GitHub repo root URLs.");
6360
+ }
6361
+ const stat = await fs11.stat(absoluteInput).catch(() => null);
5645
6362
  if (!stat) {
5646
6363
  throw new Error(`Source not found: ${input}`);
5647
6364
  }
@@ -5649,7 +6366,7 @@ async function resolveManagedSourceInput(rootDir, input) {
5649
6366
  return {
5650
6367
  kind: "file",
5651
6368
  path: absoluteInput,
5652
- title: path10.basename(absoluteInput, path10.extname(absoluteInput)) || absoluteInput
6369
+ title: path13.basename(absoluteInput, path13.extname(absoluteInput)) || absoluteInput
5653
6370
  };
5654
6371
  }
5655
6372
  if (!stat.isDirectory()) {
@@ -5661,16 +6378,22 @@ async function resolveManagedSourceInput(rootDir, input) {
5661
6378
  kind: "directory",
5662
6379
  path: absoluteInput,
5663
6380
  repoRoot,
5664
- title: path10.basename(absoluteInput) || absoluteInput
6381
+ title: path13.basename(absoluteInput) || absoluteInput
5665
6382
  };
5666
6383
  }
5667
6384
  const github = normalizeGitHubRepoRootUrl(input);
5668
6385
  if (github) {
5669
6386
  return {
5670
6387
  kind: "github_repo",
5671
- ...github
6388
+ ...github,
6389
+ branch: normalizeGitSelector(options.branch, "branch"),
6390
+ ref: normalizeGitSelector(options.ref, "ref"),
6391
+ checkoutDir: normalizeCheckoutDir(rootDir, options.checkoutDir)
5672
6392
  };
5673
6393
  }
6394
+ if (options.branch || options.ref || options.checkoutDir) {
6395
+ throw new Error("Git branch/ref/checkout options are only supported for public GitHub repo root URLs.");
6396
+ }
5674
6397
  const parsed = new URL(input);
5675
6398
  if (parsed.hostname.toLowerCase().includes("github.com")) {
5676
6399
  throw new Error(
@@ -5684,16 +6407,16 @@ async function resolveManagedSourceInput(rootDir, input) {
5684
6407
  };
5685
6408
  }
5686
6409
  function directorySourceIdsFor(manifests, inputPath) {
5687
- return manifests.filter((manifest) => manifest.originalPath && withinRoot(path10.resolve(inputPath), path10.resolve(manifest.originalPath))).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
6410
+ return manifests.filter((manifest) => manifest.originalPath && withinRoot(path13.resolve(inputPath), path13.resolve(manifest.originalPath))).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
5688
6411
  }
5689
6412
  function fileSourceIdsFor(manifests, inputPath) {
5690
- const absoluteInput = path10.resolve(inputPath);
5691
- return manifests.filter((manifest) => manifest.originalPath && path10.resolve(manifest.originalPath) === absoluteInput).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
6413
+ const absoluteInput = path13.resolve(inputPath);
6414
+ return manifests.filter((manifest) => manifest.originalPath && path13.resolve(manifest.originalPath) === absoluteInput).map((manifest) => manifest.sourceId).sort((left, right) => left.localeCompare(right));
5692
6415
  }
5693
6416
  async function syncDirectorySource(rootDir, inputPath, repoRoot) {
5694
6417
  const manifestsBefore = await listManifests(rootDir);
5695
6418
  const previousInScope = manifestsBefore.filter(
5696
- (manifest) => manifest.originalPath && withinRoot(path10.resolve(inputPath), path10.resolve(manifest.originalPath))
6419
+ (manifest) => manifest.originalPath && withinRoot(path13.resolve(inputPath), path13.resolve(manifest.originalPath))
5697
6420
  );
5698
6421
  const result = await ingestDirectory(rootDir, inputPath, { repoRoot });
5699
6422
  const removed = [];
@@ -5701,7 +6424,7 @@ async function syncDirectorySource(rootDir, inputPath, repoRoot) {
5701
6424
  if (!manifest.originalPath) {
5702
6425
  continue;
5703
6426
  }
5704
- if (await fileExists(path10.resolve(manifest.originalPath))) {
6427
+ if (await fileExists(path13.resolve(manifest.originalPath))) {
5705
6428
  continue;
5706
6429
  }
5707
6430
  const removedManifest = await removeManifestBySourceId(rootDir, manifest.sourceId);
@@ -5711,7 +6434,7 @@ async function syncDirectorySource(rootDir, inputPath, repoRoot) {
5711
6434
  }
5712
6435
  const manifestsAfter = await listManifests(rootDir);
5713
6436
  return {
5714
- title: path10.basename(inputPath) || inputPath,
6437
+ title: path13.basename(inputPath) || inputPath,
5715
6438
  sourceIds: directorySourceIdsFor(manifestsAfter, inputPath),
5716
6439
  counts: {
5717
6440
  scannedCount: result.scannedCount,
@@ -5727,7 +6450,7 @@ async function syncFileSource(rootDir, inputPath) {
5727
6450
  const result = await ingestInputDetailed(rootDir, inputPath);
5728
6451
  const manifestsAfter = await listManifests(rootDir);
5729
6452
  return {
5730
- title: path10.basename(inputPath, path10.extname(inputPath)) || inputPath,
6453
+ title: path13.basename(inputPath, path13.extname(inputPath)) || inputPath,
5731
6454
  sourceIds: fileSourceIdsFor(manifestsAfter, inputPath),
5732
6455
  counts: {
5733
6456
  scannedCount: result.scannedCount,
@@ -5761,8 +6484,11 @@ async function runGitCommand(cwd, args) {
5761
6484
  }
5762
6485
  async function syncGitHubRepoSource(rootDir, entry) {
5763
6486
  const workingDir = await managedSourceWorkingDir(rootDir, entry.id);
5764
- const checkoutDir = path10.join(workingDir, "checkout");
5765
- await fs10.rm(checkoutDir, { recursive: true, force: true });
6487
+ const externalCheckoutDir = entry.checkoutDir ? path13.resolve(entry.checkoutDir) : void 0;
6488
+ const checkoutDir = externalCheckoutDir ?? path13.join(workingDir, "checkout");
6489
+ if (!externalCheckoutDir) {
6490
+ await fs11.rm(checkoutDir, { recursive: true, force: true });
6491
+ }
5766
6492
  await ensureDir(workingDir);
5767
6493
  if (!entry.url) {
5768
6494
  throw new Error(`Managed source ${entry.id} is missing its repository URL.`);
@@ -5771,7 +6497,35 @@ async function syncGitHubRepoSource(rootDir, entry) {
5771
6497
  if (!github) {
5772
6498
  throw new Error(`Managed source ${entry.id} has an invalid GitHub repo URL.`);
5773
6499
  }
5774
- await runGitCommand(workingDir, ["clone", "--depth", "1", github.cloneUrl, "checkout"]);
6500
+ const branch = normalizeGitSelector(entry.branch, "branch");
6501
+ const ref = normalizeGitSelector(entry.ref, "ref");
6502
+ const cloneArgs = ["clone", "--depth", "1"];
6503
+ if (branch) {
6504
+ cloneArgs.push("--branch", branch);
6505
+ }
6506
+ cloneArgs.push(github.cloneUrl, checkoutDir);
6507
+ if (await fileExists(path13.join(checkoutDir, ".git"))) {
6508
+ await runGitCommand(checkoutDir, ["remote", "set-url", "origin", github.cloneUrl]);
6509
+ if (branch) {
6510
+ await runGitCommand(checkoutDir, ["fetch", "--depth", "1", "origin", branch]);
6511
+ } else {
6512
+ await runGitCommand(checkoutDir, ["fetch", "--depth", "1", "origin"]);
6513
+ }
6514
+ if (!ref) {
6515
+ await runGitCommand(checkoutDir, ["checkout", "--detach", "FETCH_HEAD"]);
6516
+ }
6517
+ } else {
6518
+ const existingEntries = await fs11.readdir(checkoutDir).catch(() => []);
6519
+ if (externalCheckoutDir && existingEntries.length > 0) {
6520
+ throw new Error(`Checkout directory exists but is not a Git repository: ${checkoutDir}`);
6521
+ }
6522
+ await ensureDir(path13.dirname(checkoutDir));
6523
+ await runGitCommand(workingDir, cloneArgs);
6524
+ }
6525
+ if (ref) {
6526
+ await runGitCommand(checkoutDir, ["fetch", "--depth", "1", "origin", ref]);
6527
+ await runGitCommand(checkoutDir, ["checkout", "--detach", "FETCH_HEAD"]);
6528
+ }
5775
6529
  return await syncDirectorySource(rootDir, checkoutDir, checkoutDir);
5776
6530
  }
5777
6531
  async function syncCrawlSource(rootDir, entry, options) {
@@ -5892,7 +6646,7 @@ function scopedNodeIds(graph, sourceIds) {
5892
6646
  async function loadSourceAnalyses(rootDir, sourceIds) {
5893
6647
  const { paths } = await loadVaultConfig(rootDir);
5894
6648
  const analyses = await Promise.all(
5895
- sourceIds.map(async (sourceId) => await readJsonFile(path10.join(paths.analysesDir, `${sourceId}.json`)))
6649
+ sourceIds.map(async (sourceId) => await readJsonFile(path13.join(paths.analysesDir, `${sourceId}.json`)))
5896
6650
  );
5897
6651
  return analyses.filter((analysis) => Boolean(analysis?.sourceId));
5898
6652
  }
@@ -6053,9 +6807,9 @@ async function writeSourceBriefForScope(rootDir, source) {
6053
6807
  confidence: 0.82
6054
6808
  }
6055
6809
  });
6056
- const absolutePath = path10.join(paths.wikiDir, output.page.path);
6057
- await ensureDir(path10.dirname(absolutePath));
6058
- await fs10.writeFile(absolutePath, output.content, "utf8");
6810
+ const absolutePath = path13.join(paths.wikiDir, output.page.path);
6811
+ await ensureDir(path13.dirname(absolutePath));
6812
+ await fs11.writeFile(absolutePath, output.content, "utf8");
6059
6813
  return absolutePath;
6060
6814
  }
6061
6815
  async function writeSourceBrief(rootDir, source) {
@@ -6343,7 +7097,7 @@ function selectGuidedTargetPages(scope, sourcePages, questions) {
6343
7097
  return (matchedTargets.length ? matchedTargets : canonicalPages).slice(0, 6);
6344
7098
  }
6345
7099
  function insightRelativePathForTarget(page, scope) {
6346
- const basename = path10.basename(page.path);
7100
+ const basename = path13.basename(page.path);
6347
7101
  if (page.kind === "concept") {
6348
7102
  return `insights/concepts/${basename}`;
6349
7103
  }
@@ -6570,7 +7324,7 @@ async function stageSourceReviewForScope(rootDir, scope) {
6570
7324
  return {
6571
7325
  sourceId: scope.id,
6572
7326
  pageId: output.page.id,
6573
- reviewPath: path10.join(approval.approvalDir, "wiki", output.page.path),
7327
+ reviewPath: path13.join(approval.approvalDir, "wiki", output.page.path),
6574
7328
  staged: true,
6575
7329
  approvalId: approval.approvalId,
6576
7330
  approvalDir: approval.approvalDir
@@ -6637,7 +7391,7 @@ async function buildSourceSessionSavedPage(rootDir, scope, session) {
6637
7391
  const evidenceState = contradictions.length > 0 ? "conflicting" : session.targetedPagePaths.some(
6638
7392
  (targetPath) => sourcePages.some((page) => page.path === targetPath && page.sourceIds.some((sourceId) => !scope.sourceIds.includes(sourceId)))
6639
7393
  ) ? "reinforcing" : session.targetedPagePaths.length ? "new" : "needs_judgment";
6640
- const relativeBriefPath = session.briefPath && path10.isAbsolute(session.briefPath) ? path10.relative(paths.wikiDir, session.briefPath) : session.briefPath;
7394
+ const relativeBriefPath = session.briefPath && path13.isAbsolute(session.briefPath) ? path13.relative(paths.wikiDir, session.briefPath) : session.briefPath;
6641
7395
  const sessionMarkdown = [
6642
7396
  `# Guided Session: ${scope.title}`,
6643
7397
  "",
@@ -6720,9 +7474,9 @@ async function buildSourceSessionSavedPage(rootDir, scope, session) {
6720
7474
  async function persistSourceSessionPage(rootDir, scope, session) {
6721
7475
  const { paths } = await loadVaultConfig(rootDir);
6722
7476
  const output = await buildSourceSessionSavedPage(rootDir, scope, session);
6723
- const absolutePath = path10.join(paths.wikiDir, output.page.path);
6724
- await ensureDir(path10.dirname(absolutePath));
6725
- await fs10.writeFile(absolutePath, output.content, "utf8");
7477
+ const absolutePath = path13.join(paths.wikiDir, output.page.path);
7478
+ await ensureDir(path13.dirname(absolutePath));
7479
+ await fs11.writeFile(absolutePath, output.content, "utf8");
6726
7480
  return { pageId: output.page.id, sessionPath: absolutePath };
6727
7481
  }
6728
7482
  async function buildGuidedUpdatePages(rootDir, scope, session) {
@@ -6750,8 +7504,8 @@ async function buildGuidedUpdatePages(rootDir, scope, session) {
6750
7504
  targetPages.map(async (targetPage) => {
6751
7505
  const evidenceState = classifyGuidedEvidenceState(scope, targetPage, contradictions);
6752
7506
  const relativePath = useCanonicalTargets && targetPage ? targetPage.path : targetPage ? insightRelativePathForTarget(targetPage, scope) : `insights/topics/${slugify(scope.title)}.md`;
6753
- const absolutePath = path10.join(paths.wikiDir, relativePath);
6754
- const existingContent = await fileExists(absolutePath) ? await fs10.readFile(absolutePath, "utf8") : "";
7507
+ const absolutePath = path13.join(paths.wikiDir, relativePath);
7508
+ const existingContent = await fileExists(absolutePath) ? await fs11.readFile(absolutePath, "utf8") : "";
6755
7509
  const parsed = existingContent ? matter3(existingContent) : { data: {}, content: "" };
6756
7510
  const existingData = parsed.data;
6757
7511
  const existingSourceIds = Array.isArray(existingData.source_ids) ? existingData.source_ids.filter((value) => typeof value === "string") : [];
@@ -6922,8 +7676,8 @@ async function stageSourceGuideForScope(rootDir, scope, options = {}) {
6922
7676
  }
6923
7677
  );
6924
7678
  session.status = "staged";
6925
- session.reviewPath = path10.join(approval.approvalDir, "wiki", reviewOutput.page.path);
6926
- session.guidePath = path10.join(approval.approvalDir, "wiki", guideOutput.page.path);
7679
+ session.reviewPath = path13.join(approval.approvalDir, "wiki", reviewOutput.page.path);
7680
+ session.guidePath = path13.join(approval.approvalDir, "wiki", guideOutput.page.path);
6927
7681
  session.approvalId = approval.approvalId;
6928
7682
  session.approvalDir = approval.approvalDir;
6929
7683
  const persisted = await persistSourceSessionPage(rootDir, scope, session);
@@ -7057,16 +7811,28 @@ async function addManagedSource(rootDir, input, options = {}) {
7057
7811
  const briefRequested = guideRequested ? true : options.brief ?? true;
7058
7812
  const reviewRequested = guideRequested ? false : options.review ?? false;
7059
7813
  const sources = await loadManagedSources(rootDir);
7060
- const resolved = await resolveManagedSourceInput(rootDir, input);
7814
+ const resolved = await resolveManagedSourceInput(rootDir, input, options);
7061
7815
  const existing = sources.find((candidate) => matchesManagedSourceSpec(candidate, resolved));
7062
7816
  const now = (/* @__PURE__ */ new Date()).toISOString();
7063
- const source = existing ?? {
7064
- id: resolved.kind === "directory" || resolved.kind === "file" ? stableManagedSourceId(resolved.kind, path10.resolve(resolved.path), resolved.title) : stableManagedSourceId(resolved.kind, resolved.url, resolved.title),
7817
+ const source = existing ? {
7818
+ ...existing,
7819
+ branch: resolved.kind === "github_repo" ? resolved.branch : existing.branch,
7820
+ ref: resolved.kind === "github_repo" ? resolved.ref : existing.ref,
7821
+ checkoutDir: resolved.kind === "github_repo" ? resolved.checkoutDir ?? existing.checkoutDir : existing.checkoutDir
7822
+ } : {
7823
+ id: resolved.kind === "directory" || resolved.kind === "file" ? stableManagedSourceId(resolved.kind, path13.resolve(resolved.path), resolved.title) : stableManagedSourceId(
7824
+ resolved.kind,
7825
+ resolved.kind === "github_repo" ? `${resolved.url}#branch=${resolved.branch ?? ""}#ref=${resolved.ref ?? ""}` : resolved.url,
7826
+ resolved.title
7827
+ ),
7065
7828
  kind: resolved.kind,
7066
7829
  title: resolved.title,
7067
7830
  path: resolved.kind === "directory" || resolved.kind === "file" ? resolved.path : void 0,
7068
7831
  repoRoot: resolved.kind === "directory" ? resolved.repoRoot : void 0,
7069
7832
  url: resolved.kind === "directory" || resolved.kind === "file" ? void 0 : resolved.url,
7833
+ branch: resolved.kind === "github_repo" ? resolved.branch : void 0,
7834
+ ref: resolved.kind === "github_repo" ? resolved.ref : void 0,
7835
+ checkoutDir: resolved.kind === "github_repo" ? resolved.checkoutDir : void 0,
7070
7836
  createdAt: now,
7071
7837
  updatedAt: now,
7072
7838
  status: "ready",
@@ -7189,7 +7955,7 @@ async function deleteManagedSource(rootDir, id) {
7189
7955
  sources.filter((source) => source.id !== id)
7190
7956
  );
7191
7957
  const workingDir = await managedSourceWorkingDir(rootDir, id);
7192
- await fs10.rm(workingDir, { recursive: true, force: true });
7958
+ await fs11.rm(workingDir, { recursive: true, force: true });
7193
7959
  return { removed: target };
7194
7960
  }
7195
7961
 
@@ -7197,9 +7963,9 @@ async function deleteManagedSource(rootDir, id) {
7197
7963
  import { execFile as execFile2 } from "child_process";
7198
7964
  import { randomUUID } from "crypto";
7199
7965
  import { EventEmitter } from "events";
7200
- import fs11 from "fs/promises";
7966
+ import fs12 from "fs/promises";
7201
7967
  import http from "http";
7202
- import path11 from "path";
7968
+ import path14 from "path";
7203
7969
  import { promisify as promisify2 } from "util";
7204
7970
  import matter4 from "gray-matter";
7205
7971
  import mime from "mime-types";
@@ -7353,7 +8119,7 @@ function toViewerLintFindings(findings) {
7353
8119
  var execFileAsync2 = promisify2(execFile2);
7354
8120
  async function isReadableFile(absolutePath) {
7355
8121
  try {
7356
- const stats = await fs11.stat(absolutePath);
8122
+ const stats = await fs12.stat(absolutePath);
7357
8123
  return stats.isFile();
7358
8124
  } catch {
7359
8125
  return false;
@@ -7364,15 +8130,15 @@ async function readViewerPage(rootDir, relativePath) {
7364
8130
  return null;
7365
8131
  }
7366
8132
  const { paths } = await loadVaultConfig(rootDir);
7367
- const absolutePath = path11.resolve(paths.wikiDir, relativePath);
8133
+ const absolutePath = path14.resolve(paths.wikiDir, relativePath);
7368
8134
  if (!isPathWithin(paths.wikiDir, absolutePath) || !await isReadableFile(absolutePath)) {
7369
8135
  return null;
7370
8136
  }
7371
- const raw = await fs11.readFile(absolutePath, "utf8");
8137
+ const raw = await fs12.readFile(absolutePath, "utf8");
7372
8138
  const parsed = matter4(raw);
7373
8139
  return {
7374
8140
  path: relativePath,
7375
- title: typeof parsed.data.title === "string" ? parsed.data.title : path11.basename(relativePath, path11.extname(relativePath)),
8141
+ title: typeof parsed.data.title === "string" ? parsed.data.title : path14.basename(relativePath, path14.extname(relativePath)),
7376
8142
  frontmatter: parsed.data,
7377
8143
  content: parsed.content,
7378
8144
  assets: normalizeOutputAssets(parsed.data.output_assets)
@@ -7383,12 +8149,12 @@ async function readViewerAsset(rootDir, relativePath) {
7383
8149
  return null;
7384
8150
  }
7385
8151
  const { paths } = await loadVaultConfig(rootDir);
7386
- const absolutePath = path11.resolve(paths.wikiDir, relativePath);
8152
+ const absolutePath = path14.resolve(paths.wikiDir, relativePath);
7387
8153
  if (!isPathWithin(paths.wikiDir, absolutePath) || !await isReadableFile(absolutePath)) {
7388
8154
  return null;
7389
8155
  }
7390
8156
  return {
7391
- buffer: await fs11.readFile(absolutePath),
8157
+ buffer: await fs12.readFile(absolutePath),
7392
8158
  mimeType: mime.lookup(absolutePath) || "application/octet-stream"
7393
8159
  };
7394
8160
  }
@@ -7424,8 +8190,8 @@ async function writeInboxClip(rootDir, body) {
7424
8190
  const tags = Array.isArray(body.tags) ? body.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : [];
7425
8191
  const now = (/* @__PURE__ */ new Date()).toISOString();
7426
8192
  const fileName = `${now.replace(/[:.]/g, "-")}-${slugForClip(title)}.md`;
7427
- const inboxPath = path11.join(paths.inboxDir, fileName);
7428
- await fs11.mkdir(paths.inboxDir, { recursive: true });
8193
+ const inboxPath = path14.join(paths.inboxDir, fileName);
8194
+ await fs12.mkdir(paths.inboxDir, { recursive: true });
7429
8195
  const lines = [
7430
8196
  "---",
7431
8197
  `title: ${JSON.stringify(title)}`,
@@ -7442,17 +8208,17 @@ async function writeInboxClip(rootDir, body) {
7442
8208
  selectionHtml && !markdown ? ["", "## Original HTML", "", "```html", selectionHtml, "```"].join("\n") : void 0,
7443
8209
  ""
7444
8210
  ].filter((line) => line !== void 0);
7445
- await fs11.writeFile(inboxPath, lines.join("\n"), "utf8");
8211
+ await fs12.writeFile(inboxPath, lines.join("\n"), "utf8");
7446
8212
  const result = await importInbox(rootDir, paths.inboxDir);
7447
8213
  return { mode: "inbox", inboxPath, result };
7448
8214
  }
7449
8215
  async function ensureViewerDist(viewerDistDir) {
7450
- const indexPath = path11.join(viewerDistDir, "index.html");
8216
+ const indexPath = path14.join(viewerDistDir, "index.html");
7451
8217
  if (await fileExists(indexPath)) {
7452
8218
  return;
7453
8219
  }
7454
- const viewerProjectDir = path11.dirname(viewerDistDir);
7455
- if (await fileExists(path11.join(viewerProjectDir, "package.json"))) {
8220
+ const viewerProjectDir = path14.dirname(viewerDistDir);
8221
+ if (await fileExists(path14.join(viewerProjectDir, "package.json"))) {
7456
8222
  await execFileAsync2("pnpm", ["build"], { cwd: viewerProjectDir });
7457
8223
  }
7458
8224
  }
@@ -7475,7 +8241,7 @@ async function startGraphServer(rootDir, port, options = {}) {
7475
8241
  response.end(JSON.stringify({ error: "Graph artifact not found. Run `swarmvault compile` first." }));
7476
8242
  return;
7477
8243
  }
7478
- const reportPath = path11.join(paths.wikiDir, "graph", "report.json");
8244
+ const reportPath = path14.join(paths.wikiDir, "graph", "report.json");
7479
8245
  const report = await readJsonFile(reportPath) ?? null;
7480
8246
  response.writeHead(200, { "content-type": "application/json" });
7481
8247
  response.end(JSON.stringify(buildViewerGraphArtifact(graph, { report, full: options.full ?? false })));
@@ -7539,13 +8305,13 @@ async function startGraphServer(rootDir, port, options = {}) {
7539
8305
  return;
7540
8306
  }
7541
8307
  if (url.pathname === "/api/graph-report") {
7542
- const reportPath = path11.join(paths.wikiDir, "graph", "report.json");
8308
+ const reportPath = path14.join(paths.wikiDir, "graph", "report.json");
7543
8309
  if (!await fileExists(reportPath)) {
7544
8310
  response.writeHead(404, { "content-type": "application/json" });
7545
8311
  response.end(JSON.stringify({ error: "Graph report artifact not found. Run `swarmvault compile` first." }));
7546
8312
  return;
7547
8313
  }
7548
- const body = await fs11.readFile(reportPath, "utf8");
8314
+ const body = await fs12.readFile(reportPath, "utf8");
7549
8315
  response.writeHead(200, { "content-type": "application/json" });
7550
8316
  response.end(body);
7551
8317
  return;
@@ -7783,7 +8549,7 @@ async function startGraphServer(rootDir, port, options = {}) {
7783
8549
  return;
7784
8550
  }
7785
8551
  if (url.pathname === "/api/workspace") {
7786
- const reportPath = path11.join(paths.wikiDir, "graph", "report.json");
8552
+ const reportPath = path14.join(paths.wikiDir, "graph", "report.json");
7787
8553
  const [graphRaw, reportRaw, approvalsRaw, candidatesRaw, memoryTasksRaw, watchStatusRaw, lintRaw, doctorRaw] = await Promise.all([
7788
8554
  readJsonFile(paths.graphPath).catch(() => null),
7789
8555
  readJsonFile(reportPath).catch(() => null),
@@ -7908,15 +8674,15 @@ async function startGraphServer(rootDir, port, options = {}) {
7908
8674
  return;
7909
8675
  }
7910
8676
  const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
7911
- const target = path11.join(paths.viewerDistDir, relativePath);
7912
- const fallback = path11.join(paths.viewerDistDir, "index.html");
8677
+ const target = path14.join(paths.viewerDistDir, relativePath);
8678
+ const fallback = path14.join(paths.viewerDistDir, "index.html");
7913
8679
  const filePath = await fileExists(target) ? target : fallback;
7914
8680
  if (!await fileExists(filePath)) {
7915
8681
  response.writeHead(503, { "content-type": "text/plain" });
7916
8682
  response.end("Viewer build not found. Run `pnpm build` first.");
7917
8683
  return;
7918
8684
  }
7919
- const staticBody = await fs11.readFile(filePath);
8685
+ const staticBody = await fs12.readFile(filePath);
7920
8686
  response.writeHead(200, { "content-type": mime.lookup(filePath) || "text/plain" });
7921
8687
  response.end(staticBody);
7922
8688
  } catch (error) {
@@ -7956,7 +8722,7 @@ async function exportGraphHtml(rootDir, outputPath, options = {}) {
7956
8722
  throw new Error("Graph artifact not found. Run `swarmvault compile` first.");
7957
8723
  }
7958
8724
  await ensureViewerDist(paths.viewerDistDir);
7959
- const indexPath = path11.join(paths.viewerDistDir, "index.html");
8725
+ const indexPath = path14.join(paths.viewerDistDir, "index.html");
7960
8726
  if (!await fileExists(indexPath)) {
7961
8727
  throw new Error("Viewer build not found. Run `pnpm build` first.");
7962
8728
  }
@@ -7982,17 +8748,17 @@ async function exportGraphHtml(rootDir, outputPath, options = {}) {
7982
8748
  } : null;
7983
8749
  })
7984
8750
  );
7985
- const rawHtml = await fs11.readFile(indexPath, "utf8");
8751
+ const rawHtml = await fs12.readFile(indexPath, "utf8");
7986
8752
  const scriptMatch = rawHtml.match(/<script type="module" crossorigin src="([^"]+)"><\/script>/);
7987
8753
  const styleMatch = rawHtml.match(/<link rel="stylesheet" crossorigin href="([^"]+)">/);
7988
- const scriptPath = scriptMatch?.[1] ? path11.join(paths.viewerDistDir, scriptMatch[1].replace(/^\//, "")) : null;
7989
- const stylePath = styleMatch?.[1] ? path11.join(paths.viewerDistDir, styleMatch[1].replace(/^\//, "")) : null;
8754
+ const scriptPath = scriptMatch?.[1] ? path14.join(paths.viewerDistDir, scriptMatch[1].replace(/^\//, "")) : null;
8755
+ const stylePath = styleMatch?.[1] ? path14.join(paths.viewerDistDir, styleMatch[1].replace(/^\//, "")) : null;
7990
8756
  if (!scriptPath || !await fileExists(scriptPath)) {
7991
8757
  throw new Error("Viewer script bundle not found. Run `pnpm build` first.");
7992
8758
  }
7993
- const script = await fs11.readFile(scriptPath, "utf8");
7994
- const style = stylePath && await fileExists(stylePath) ? await fs11.readFile(stylePath, "utf8") : "";
7995
- const report = await readJsonFile(path11.join(paths.wikiDir, "graph", "report.json"));
8759
+ const script = await fs12.readFile(scriptPath, "utf8");
8760
+ const style = stylePath && await fileExists(stylePath) ? await fs12.readFile(stylePath, "utf8") : "";
8761
+ const report = await readJsonFile(path14.join(paths.wikiDir, "graph", "report.json"));
7996
8762
  const embeddedData = JSON.stringify(
7997
8763
  { graph: buildViewerGraphArtifact(graph, { report, full: options.full ?? false }), pages: pages.filter(Boolean), report },
7998
8764
  null,
@@ -8015,9 +8781,9 @@ async function exportGraphHtml(rootDir, outputPath, options = {}) {
8015
8781
  "</html>",
8016
8782
  ""
8017
8783
  ].filter(Boolean).join("\n");
8018
- await fs11.mkdir(path11.dirname(outputPath), { recursive: true });
8019
- await fs11.writeFile(outputPath, html, "utf8");
8020
- return path11.resolve(outputPath);
8784
+ await fs12.mkdir(path14.dirname(outputPath), { recursive: true });
8785
+ await fs12.writeFile(outputPath, html, "utf8");
8786
+ return path14.resolve(outputPath);
8021
8787
  }
8022
8788
  export {
8023
8789
  ALL_MIGRATIONS,
@@ -8031,6 +8797,7 @@ export {
8031
8797
  LOCAL_WHISPER_MODEL_SIZES,
8032
8798
  LocalWhisperProviderAdapter,
8033
8799
  OPENAI_COMPATIBLE_CAPABILITY_MATRIX,
8800
+ SWARMVAULT_OUT_ENV,
8034
8801
  acceptApproval,
8035
8802
  addInput,
8036
8803
  addManagedSource,
@@ -8046,8 +8813,10 @@ export {
8046
8813
  buildConfiguredRedactor,
8047
8814
  buildContextPack,
8048
8815
  buildGraphShareArtifact,
8816
+ buildGraphTree,
8049
8817
  buildMemoryGraphElements,
8050
8818
  buildRedactor,
8819
+ checkTrackedRepoChanges,
8051
8820
  compileVault,
8052
8821
  computeDecayScore,
8053
8822
  consolidateVault,
@@ -8068,17 +8837,20 @@ export {
8068
8837
  estimatePageTokens,
8069
8838
  estimateTokens,
8070
8839
  evaluateCandidateForPromotion,
8840
+ evaluateGraphShrinkGuard,
8071
8841
  expectedModelPath,
8072
8842
  explainGraphVault,
8073
8843
  exploreVault,
8074
8844
  exportGraphFormat,
8075
8845
  exportGraphHtml,
8076
8846
  exportGraphReportHtml,
8847
+ exportGraphTree,
8077
8848
  exportObsidianCanvas,
8078
8849
  exportObsidianVault,
8079
8850
  finishMemoryTask,
8080
8851
  getGitHookStatus,
8081
8852
  getGraphCommunityVault,
8853
+ getGraphStatus,
8082
8854
  getProviderForTask,
8083
8855
  getRetrievalStatus,
8084
8856
  getWatchStatus,
@@ -8117,6 +8889,7 @@ export {
8117
8889
  lookupPresetCapabilities,
8118
8890
  markSuperseded,
8119
8891
  memoryTaskHashes,
8892
+ mergeGraphFiles,
8120
8893
  modelDownloadUrl,
8121
8894
  pathGraphVault,
8122
8895
  persistDecayFrontmatter,
@@ -8133,6 +8906,7 @@ export {
8133
8906
  readMemoryTask,
8134
8907
  readPage,
8135
8908
  rebuildRetrievalIndex,
8909
+ refreshGraphClusters,
8136
8910
  registerLocalWhisperProvider,
8137
8911
  rejectApproval,
8138
8912
  reloadManagedSources,
@@ -8143,8 +8917,10 @@ export {
8143
8917
  renderGraphShareMarkdown,
8144
8918
  renderGraphSharePreviewHtml,
8145
8919
  renderGraphShareSvg,
8920
+ renderGraphTreeHtml,
8146
8921
  renderMemoryTaskMarkdown,
8147
8922
  resetDecay,
8923
+ resolveArtifactRootDir,
8148
8924
  resolveConsolidationConfig,
8149
8925
  resolveDecayConfig,
8150
8926
  resolveLargeRepoDefaults,