@swarmvaultai/engine 3.6.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
@@ -125,7 +125,7 @@ import {
125
125
  writeGuidedSourceSession,
126
126
  writeRetrievalManifest,
127
127
  writeWatchStatusArtifact
128
- } from "./chunk-5GEPTIZE.js";
128
+ } from "./chunk-S2E65WRI.js";
129
129
  import {
130
130
  LocalWhisperProviderAdapter,
131
131
  SWARMVAULT_OUT_ENV,
@@ -563,6 +563,7 @@ import chokidar from "chokidar";
563
563
  var MAX_BACKOFF_MS = 3e4;
564
564
  var BACKOFF_THRESHOLD = 3;
565
565
  var CRITICAL_THRESHOLD = 10;
566
+ var DEFAULT_MAX_GRAPH_SHRINK_RATIO = 0.25;
566
567
  var REPO_WATCH_IGNORES = /* @__PURE__ */ new Set([".git", ".venv"]);
567
568
  var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
568
569
  ".ts",
@@ -602,7 +603,12 @@ var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
602
603
  ".psm1",
603
604
  ".ex",
604
605
  ".exs",
606
+ ".svelte",
605
607
  ".jl",
608
+ ".v",
609
+ ".vh",
610
+ ".sv",
611
+ ".svh",
606
612
  ".r",
607
613
  ".R"
608
614
  ]);
@@ -638,6 +644,35 @@ function collectNonCodePaths(reasons) {
638
644
  }
639
645
  return result;
640
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
+ }
641
676
  function hasIgnoredRepoSegment(baseDir, targetPath) {
642
677
  const relativePath = path2.relative(baseDir, targetPath);
643
678
  if (!relativePath || relativePath.startsWith("..")) {
@@ -774,6 +809,7 @@ async function performWatchCycle(rootDir, paths, options, codeOnly = false) {
774
809
  }
775
810
  async function runWatchCycle(rootDir, options = {}) {
776
811
  const { paths } = await initWorkspace(rootDir);
812
+ const previousGraph = await readJsonFile(paths.graphPath);
777
813
  const startedAt = /* @__PURE__ */ new Date();
778
814
  let success = true;
779
815
  let error;
@@ -792,6 +828,12 @@ async function runWatchCycle(rootDir, options = {}) {
792
828
  };
793
829
  try {
794
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
+ }
795
837
  return result;
796
838
  } catch (caught) {
797
839
  success = false;
@@ -3710,9 +3752,350 @@ Community: ${communityLabel}`,
3710
3752
  return { format: "canvas", outputPath: resolvedPath };
3711
3753
  }
3712
3754
 
3713
- // src/graph-push.ts
3755
+ // src/graph-merge.ts
3714
3756
  import fs5 from "fs/promises";
3715
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";
3716
4099
  import neo4j from "neo4j-driver";
3717
4100
  var DEFAULT_NEO4J_BATCH_SIZE = 500;
3718
4101
  var DEFAULT_NEO4J_DATABASE = "neo4j";
@@ -3723,8 +4106,8 @@ function requireConfigValue(value, name) {
3723
4106
  throw new Error(`Neo4j push requires ${name}. Configure \`graphSinks.neo4j.${name}\` or pass the matching CLI flag.`);
3724
4107
  }
3725
4108
  async function deriveVaultId(rootDir) {
3726
- const realRoot = await fs5.realpath(rootDir).catch(() => path5.resolve(rootDir));
3727
- const label = slugify(path5.basename(realRoot));
4109
+ const realRoot = await fs6.realpath(rootDir).catch(() => path6.resolve(rootDir));
4110
+ const label = slugify(path6.basename(realRoot));
3728
4111
  return `${label}-${sha256(realRoot).slice(0, 12)}`;
3729
4112
  }
3730
4113
  async function resolveNeo4jPushConfig(rootDir, options) {
@@ -3754,7 +4137,7 @@ function normalizeBatchSize(value) {
3754
4137
  }
3755
4138
  async function loadGraph2(rootDir) {
3756
4139
  const { paths } = await loadVaultConfig(rootDir);
3757
- const raw = JSON.parse(await fs5.readFile(paths.graphPath, "utf8"));
4140
+ const raw = JSON.parse(await fs6.readFile(paths.graphPath, "utf8"));
3758
4141
  return raw;
3759
4142
  }
3760
4143
  function buildResult(input) {
@@ -3839,7 +4222,7 @@ async function writeSyncNode(session, input) {
3839
4222
  ].join("\n"),
3840
4223
  {
3841
4224
  vaultId: input.vaultId,
3842
- rootDir: path5.resolve(input.rootDir),
4225
+ rootDir: path6.resolve(input.rootDir),
3843
4226
  graphGeneratedAt: input.graph.generatedAt,
3844
4227
  graphHash: graphHash(input.graph),
3845
4228
  pushedAt: input.pushedAt,
@@ -3925,7 +4308,7 @@ async function pushGraphNeo4j(rootDir, options = {}) {
3925
4308
  }
3926
4309
 
3927
4310
  // src/graph-status.ts
3928
- import path6 from "path";
4311
+ import path7 from "path";
3929
4312
  function recommendedCommand(input) {
3930
4313
  if (!input.graphExists || !input.reportExists) {
3931
4314
  return "swarmvault compile";
@@ -3941,8 +4324,8 @@ function recommendedCommand(input) {
3941
4324
  async function getGraphStatus(rootDir, options = {}) {
3942
4325
  const { paths } = await loadVaultConfig(rootDir);
3943
4326
  const graphPath = paths.graphPath;
3944
- const reportPath = path6.join(paths.wikiDir, "graph", "report.md");
3945
- const resolvedOverrideRoots = options.repoRoots?.map((repoRoot) => path6.resolve(rootDir, repoRoot));
4327
+ const reportPath = path7.join(paths.wikiDir, "graph", "report.md");
4328
+ const resolvedOverrideRoots = options.repoRoots?.map((repoRoot) => path7.resolve(rootDir, repoRoot));
3946
4329
  const [graphExists, reportExists, trackedRepoRoots, changes, pendingSemanticRefresh] = await Promise.all([
3947
4330
  fileExists(graphPath),
3948
4331
  fileExists(reportPath),
@@ -3975,27 +4358,268 @@ async function getGraphStatus(rootDir, options = {}) {
3975
4358
  };
3976
4359
  }
3977
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
+
3978
4602
  // src/hooks.ts
3979
- import fs6 from "fs/promises";
3980
- import path7 from "path";
4603
+ import fs7 from "fs/promises";
4604
+ import path9 from "path";
3981
4605
  import process3 from "process";
3982
4606
  var hookStart = "# >>> swarmvault hook >>>";
3983
4607
  var hookEnd = "# <<< swarmvault hook <<<";
3984
4608
  async function findNearestGitRoot(startPath) {
3985
- let current = path7.resolve(startPath);
4609
+ let current = path9.resolve(startPath);
3986
4610
  try {
3987
- const stat = await fs6.stat(current);
4611
+ const stat = await fs7.stat(current);
3988
4612
  if (!stat.isDirectory()) {
3989
- current = path7.dirname(current);
4613
+ current = path9.dirname(current);
3990
4614
  }
3991
4615
  } catch {
3992
- current = path7.dirname(current);
4616
+ current = path9.dirname(current);
3993
4617
  }
3994
4618
  while (true) {
3995
- if (await fileExists(path7.join(current, ".git"))) {
4619
+ if (await fileExists(path9.join(current, ".git"))) {
3996
4620
  return current;
3997
4621
  }
3998
- const parent = path7.dirname(current);
4622
+ const parent = path9.dirname(current);
3999
4623
  if (parent === current) {
4000
4624
  return null;
4001
4625
  }
@@ -4007,8 +4631,8 @@ function shellQuote(value) {
4007
4631
  }
4008
4632
  function resolveSwarmvaultExecutableCandidate() {
4009
4633
  const argvPath = process3.argv[1];
4010
- if (typeof argvPath === "string" && argvPath.trim() && (argvPath.includes(`${path7.sep}@swarmvaultai${path7.sep}cli${path7.sep}`) || argvPath.includes(`${path7.sep}packages${path7.sep}cli${path7.sep}`))) {
4011
- return path7.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);
4012
4636
  }
4013
4637
  return "swarmvault";
4014
4638
  }
@@ -4027,17 +4651,17 @@ function managedHookBlock(vaultRoot) {
4027
4651
  ].join("\n");
4028
4652
  }
4029
4653
  function hookPath(repoRoot, hookName) {
4030
- return path7.join(repoRoot, ".git", "hooks", hookName);
4654
+ return path9.join(repoRoot, ".git", "hooks", hookName);
4031
4655
  }
4032
4656
  async function readHookStatus(filePath) {
4033
4657
  if (!await fileExists(filePath)) {
4034
4658
  return "not_installed";
4035
4659
  }
4036
- const content = await fs6.readFile(filePath, "utf8");
4660
+ const content = await fs7.readFile(filePath, "utf8");
4037
4661
  return content.includes(hookStart) && content.includes(hookEnd) ? "installed" : "other_content";
4038
4662
  }
4039
4663
  async function upsertHookFile(filePath, block) {
4040
- const existing = await fileExists(filePath) ? await fs6.readFile(filePath, "utf8") : "";
4664
+ const existing = await fileExists(filePath) ? await fs7.readFile(filePath, "utf8") : "";
4041
4665
  let next;
4042
4666
  const startIndex = existing.indexOf(hookStart);
4043
4667
  const endIndex = existing.indexOf(hookEnd);
@@ -4051,16 +4675,16 @@ ${block}`.trimEnd();
4051
4675
  next = `#!/bin/sh
4052
4676
  ${block}`.trimEnd();
4053
4677
  }
4054
- await ensureDir(path7.dirname(filePath));
4055
- await fs6.writeFile(filePath, `${next}
4678
+ await ensureDir(path9.dirname(filePath));
4679
+ await fs7.writeFile(filePath, `${next}
4056
4680
  `, { mode: 493, encoding: "utf8" });
4057
- await fs6.chmod(filePath, 493);
4681
+ await fs7.chmod(filePath, 493);
4058
4682
  }
4059
4683
  async function removeHookBlock(filePath) {
4060
4684
  if (!await fileExists(filePath)) {
4061
4685
  return;
4062
4686
  }
4063
- const existing = await fs6.readFile(filePath, "utf8");
4687
+ const existing = await fs7.readFile(filePath, "utf8");
4064
4688
  const startIndex = existing.indexOf(hookStart);
4065
4689
  const endIndex = existing.indexOf(hookEnd);
4066
4690
  if (startIndex === -1 || endIndex === -1) {
@@ -4068,10 +4692,10 @@ async function removeHookBlock(filePath) {
4068
4692
  }
4069
4693
  const next = `${existing.slice(0, startIndex)}${existing.slice(endIndex + hookEnd.length)}`.trim();
4070
4694
  if (!next || next === "#!/bin/sh") {
4071
- await fs6.rm(filePath, { force: true });
4695
+ await fs7.rm(filePath, { force: true });
4072
4696
  return;
4073
4697
  }
4074
- await fs6.writeFile(filePath, `${next}
4698
+ await fs7.writeFile(filePath, `${next}
4075
4699
  `, "utf8");
4076
4700
  }
4077
4701
  async function getGitHookStatus(rootDir) {
@@ -4094,7 +4718,7 @@ async function installGitHooks(rootDir) {
4094
4718
  if (!repoRoot) {
4095
4719
  throw new Error("No git repository found above the current vault.");
4096
4720
  }
4097
- const block = managedHookBlock(path7.resolve(rootDir));
4721
+ const block = managedHookBlock(path9.resolve(rootDir));
4098
4722
  await upsertHookFile(hookPath(repoRoot, "post-commit"), block);
4099
4723
  await upsertHookFile(hookPath(repoRoot, "post-checkout"), block);
4100
4724
  return getGitHookStatus(rootDir);
@@ -4114,12 +4738,12 @@ async function uninstallGitHooks(rootDir) {
4114
4738
  }
4115
4739
 
4116
4740
  // src/mcp.ts
4117
- import fs7 from "fs/promises";
4118
- import path8 from "path";
4741
+ import fs8 from "fs/promises";
4742
+ import path10 from "path";
4119
4743
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
4120
4744
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4121
4745
  import { z } from "zod";
4122
- var SERVER_VERSION = "3.6.0";
4746
+ var SERVER_VERSION = "3.7.0";
4123
4747
  async function createMcpServer(rootDir) {
4124
4748
  const server = new McpServer({
4125
4749
  name: "swarmvault",
@@ -4827,7 +5451,7 @@ async function createMcpServer(rootDir) {
4827
5451
  },
4828
5452
  async () => {
4829
5453
  const { paths } = await loadVaultConfig(rootDir);
4830
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path8.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();
4831
5455
  return asTextResource("swarmvault://sessions", JSON.stringify(files, null, 2));
4832
5456
  }
4833
5457
  );
@@ -4896,8 +5520,8 @@ async function createMcpServer(rootDir) {
4896
5520
  return asTextResource(`swarmvault://pages/${encodedPath}`, `Page not found: ${relativePath}`);
4897
5521
  }
4898
5522
  const { paths } = await loadVaultConfig(rootDir);
4899
- const absolutePath = path8.resolve(paths.wikiDir, relativePath);
4900
- 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"));
4901
5525
  }
4902
5526
  );
4903
5527
  server.registerResource(
@@ -4905,11 +5529,11 @@ async function createMcpServer(rootDir) {
4905
5529
  new ResourceTemplate("swarmvault://sessions/{path}", {
4906
5530
  list: async () => {
4907
5531
  const { paths } = await loadVaultConfig(rootDir);
4908
- const files = (await listFilesRecursive(paths.sessionsDir)).filter((filePath) => filePath.endsWith(".md")).map((filePath) => toPosix(path8.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();
4909
5533
  return {
4910
5534
  resources: files.map((relativePath) => ({
4911
5535
  uri: `swarmvault://sessions/${encodeURIComponent(relativePath)}`,
4912
- name: path8.basename(relativePath, ".md"),
5536
+ name: path10.basename(relativePath, ".md"),
4913
5537
  title: relativePath,
4914
5538
  description: "SwarmVault session artifact",
4915
5539
  mimeType: "text/markdown"
@@ -4926,11 +5550,11 @@ async function createMcpServer(rootDir) {
4926
5550
  const { paths } = await loadVaultConfig(rootDir);
4927
5551
  const encodedPath = typeof variables.path === "string" ? variables.path : "";
4928
5552
  const relativePath = decodeURIComponent(encodedPath);
4929
- const absolutePath = path8.resolve(paths.sessionsDir, relativePath);
5553
+ const absolutePath = path10.resolve(paths.sessionsDir, relativePath);
4930
5554
  if (!isPathWithin(paths.sessionsDir, absolutePath) || !await fileExists(absolutePath)) {
4931
5555
  return asTextResource(`swarmvault://sessions/${encodedPath}`, `Session not found: ${relativePath}`);
4932
5556
  }
4933
- return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs7.readFile(absolutePath, "utf8"));
5557
+ return asTextResource(`swarmvault://sessions/${encodedPath}`, await fs8.readFile(absolutePath, "utf8"));
4934
5558
  }
4935
5559
  );
4936
5560
  return server;
@@ -4990,9 +5614,9 @@ function asTextResource(uri, text) {
4990
5614
 
4991
5615
  // src/providers/local-whisper-setup.ts
4992
5616
  import { createWriteStream, constants as fsConstants } from "fs";
4993
- import fs8 from "fs/promises";
5617
+ import fs9 from "fs/promises";
4994
5618
  import os from "os";
4995
- import path9 from "path";
5619
+ import path11 from "path";
4996
5620
  import { Readable } from "stream";
4997
5621
  import { pipeline } from "stream/promises";
4998
5622
  var BINARY_CANDIDATES = ["whisper-cli", "whisper-cpp", "whisper"];
@@ -5020,10 +5644,10 @@ async function discoverLocalWhisperBinary(options = {}) {
5020
5644
  }
5021
5645
  const pathValue = env.PATH ?? "";
5022
5646
  const candidates = [];
5023
- for (const dir of pathValue.split(path9.delimiter)) {
5647
+ for (const dir of pathValue.split(path11.delimiter)) {
5024
5648
  if (!dir) continue;
5025
5649
  for (const name of BINARY_CANDIDATES) {
5026
- const full = path9.join(dir, name);
5650
+ const full = path11.join(dir, name);
5027
5651
  candidates.push(full);
5028
5652
  if (await isExecutable(full)) {
5029
5653
  return { binaryPath: full, candidates, source: "path" };
@@ -5034,14 +5658,14 @@ async function discoverLocalWhisperBinary(options = {}) {
5034
5658
  }
5035
5659
  function expectedModelPath(modelName, homeDir) {
5036
5660
  const home = homeDir ?? os.homedir();
5037
- return path9.join(home, ".swarmvault", "models", `ggml-${modelName}.bin`);
5661
+ return path11.join(home, ".swarmvault", "models", `ggml-${modelName}.bin`);
5038
5662
  }
5039
5663
  function modelDownloadUrl(modelName) {
5040
5664
  return `${HUGGINGFACE_BASE}/ggml-${modelName}.bin`;
5041
5665
  }
5042
5666
  async function downloadWhisperModel(options) {
5043
5667
  const destPath = expectedModelPath(options.modelName, options.homeDir);
5044
- await ensureDir(path9.dirname(destPath));
5668
+ await ensureDir(path11.dirname(destPath));
5045
5669
  const doFetch = options.fetchImpl ?? fetch;
5046
5670
  const url = modelDownloadUrl(options.modelName);
5047
5671
  const response = await doFetch(url);
@@ -5062,8 +5686,8 @@ async function downloadWhisperModel(options) {
5062
5686
  });
5063
5687
  const tmpPath = `${destPath}.part`;
5064
5688
  await pipeline(source, createWriteStream(tmpPath));
5065
- await fs8.rename(tmpPath, destPath);
5066
- const stat = await fs8.stat(destPath);
5689
+ await fs9.rename(tmpPath, destPath);
5690
+ const stat = await fs9.stat(destPath);
5067
5691
  return { path: destPath, bytes: stat.size };
5068
5692
  }
5069
5693
  async function registerLocalWhisperProvider(options) {
@@ -5130,7 +5754,7 @@ async function summarizeLocalWhisperSetup(options) {
5130
5754
  }
5131
5755
  async function isExecutable(p) {
5132
5756
  try {
5133
- await fs8.access(p, fsConstants.X_OK);
5757
+ await fs9.access(p, fsConstants.X_OK);
5134
5758
  return true;
5135
5759
  } catch {
5136
5760
  return false;
@@ -5200,13 +5824,13 @@ async function withCapabilityFallback(provider, capability, run, fallback) {
5200
5824
  }
5201
5825
 
5202
5826
  // src/schedule.ts
5203
- import fs9 from "fs/promises";
5204
- import path10 from "path";
5827
+ import fs10 from "fs/promises";
5828
+ import path12 from "path";
5205
5829
  function scheduleStatePath(schedulesDir, jobId) {
5206
- return path10.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
5830
+ return path12.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
5207
5831
  }
5208
5832
  function scheduleLockPath(schedulesDir, jobId) {
5209
- return path10.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
5833
+ return path12.join(schedulesDir, `${encodeURIComponent(jobId)}.lock`);
5210
5834
  }
5211
5835
  function parseEveryDuration(value) {
5212
5836
  const match = value.trim().match(/^(\d+)(m|h|d)$/i);
@@ -5309,13 +5933,13 @@ async function acquireJobLease(rootDir, jobId) {
5309
5933
  const { paths } = await loadVaultConfig(rootDir);
5310
5934
  const leasePath = scheduleLockPath(paths.schedulesDir, jobId);
5311
5935
  await ensureDir(paths.schedulesDir);
5312
- const handle = await fs9.open(leasePath, "wx");
5936
+ const handle = await fs10.open(leasePath, "wx");
5313
5937
  await handle.writeFile(`${process.pid}
5314
5938
  ${(/* @__PURE__ */ new Date()).toISOString()}
5315
5939
  `);
5316
5940
  await handle.close();
5317
5941
  return async () => {
5318
- await fs9.rm(leasePath, { force: true });
5942
+ await fs10.rm(leasePath, { force: true });
5319
5943
  };
5320
5944
  }
5321
5945
  async function listSchedules(rootDir) {
@@ -5474,8 +6098,8 @@ async function serveSchedules(rootDir, pollMs = 3e4) {
5474
6098
 
5475
6099
  // src/sources.ts
5476
6100
  import { spawn } from "child_process";
5477
- import fs10 from "fs/promises";
5478
- import path11 from "path";
6101
+ import fs11 from "fs/promises";
6102
+ import path13 from "path";
5479
6103
  import matter3 from "gray-matter";
5480
6104
  import { JSDOM } from "jsdom";
5481
6105
  var DEFAULT_CRAWL_MAX_PAGES = 12;
@@ -5521,24 +6145,24 @@ function emptyManagedSourceSyncCounts() {
5521
6145
  };
5522
6146
  }
5523
6147
  function withinRoot(rootPath, targetPath) {
5524
- const relative = path11.relative(rootPath, targetPath);
5525
- return relative === "" || !relative.startsWith("..") && !path11.isAbsolute(relative);
6148
+ const relative = path13.relative(rootPath, targetPath);
6149
+ return relative === "" || !relative.startsWith("..") && !path13.isAbsolute(relative);
5526
6150
  }
5527
6151
  async function findNearestGitRoot2(startPath) {
5528
- let current = path11.resolve(startPath);
6152
+ let current = path13.resolve(startPath);
5529
6153
  try {
5530
- const stat = await fs10.stat(current);
6154
+ const stat = await fs11.stat(current);
5531
6155
  if (!stat.isDirectory()) {
5532
- current = path11.dirname(current);
6156
+ current = path13.dirname(current);
5533
6157
  }
5534
6158
  } catch {
5535
- current = path11.dirname(current);
6159
+ current = path13.dirname(current);
5536
6160
  }
5537
6161
  while (true) {
5538
- if (await fileExists(path11.join(current, ".git"))) {
6162
+ if (await fileExists(path13.join(current, ".git"))) {
5539
6163
  return current;
5540
6164
  }
5541
- const parent = path11.dirname(current);
6165
+ const parent = path13.dirname(current);
5542
6166
  if (parent === current) {
5543
6167
  return null;
5544
6168
  }
@@ -5612,7 +6236,7 @@ function isAllowedDocsCandidate(candidate, startUrl) {
5612
6236
  if (candidate.origin !== startUrl.origin) {
5613
6237
  return false;
5614
6238
  }
5615
- const extension = path11.extname(candidate.pathname).toLowerCase();
6239
+ const extension = path13.extname(candidate.pathname).toLowerCase();
5616
6240
  if (extension && extension !== ".html" && extension !== ".htm" && extension !== ".md") {
5617
6241
  return false;
5618
6242
  }
@@ -5701,14 +6325,40 @@ function matchesManagedSourceSpec(existing, input) {
5701
6325
  return false;
5702
6326
  }
5703
6327
  if (input.kind === "directory" || input.kind === "file") {
5704
- return path11.resolve(existing.path ?? "") === path11.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 ?? "");
5705
6332
  }
5706
6333
  return (existing.url ?? "") === input.url;
5707
6334
  }
5708
- async function resolveManagedSourceInput(rootDir, input) {
5709
- const absoluteInput = path11.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);
5710
6357
  if (!(input.startsWith("http://") || input.startsWith("https://"))) {
5711
- 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);
5712
6362
  if (!stat) {
5713
6363
  throw new Error(`Source not found: ${input}`);
5714
6364
  }
@@ -5716,7 +6366,7 @@ async function resolveManagedSourceInput(rootDir, input) {
5716
6366
  return {
5717
6367
  kind: "file",
5718
6368
  path: absoluteInput,
5719
- title: path11.basename(absoluteInput, path11.extname(absoluteInput)) || absoluteInput
6369
+ title: path13.basename(absoluteInput, path13.extname(absoluteInput)) || absoluteInput
5720
6370
  };
5721
6371
  }
5722
6372
  if (!stat.isDirectory()) {
@@ -5728,16 +6378,22 @@ async function resolveManagedSourceInput(rootDir, input) {
5728
6378
  kind: "directory",
5729
6379
  path: absoluteInput,
5730
6380
  repoRoot,
5731
- title: path11.basename(absoluteInput) || absoluteInput
6381
+ title: path13.basename(absoluteInput) || absoluteInput
5732
6382
  };
5733
6383
  }
5734
6384
  const github = normalizeGitHubRepoRootUrl(input);
5735
6385
  if (github) {
5736
6386
  return {
5737
6387
  kind: "github_repo",
5738
- ...github
6388
+ ...github,
6389
+ branch: normalizeGitSelector(options.branch, "branch"),
6390
+ ref: normalizeGitSelector(options.ref, "ref"),
6391
+ checkoutDir: normalizeCheckoutDir(rootDir, options.checkoutDir)
5739
6392
  };
5740
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
+ }
5741
6397
  const parsed = new URL(input);
5742
6398
  if (parsed.hostname.toLowerCase().includes("github.com")) {
5743
6399
  throw new Error(
@@ -5751,16 +6407,16 @@ async function resolveManagedSourceInput(rootDir, input) {
5751
6407
  };
5752
6408
  }
5753
6409
  function directorySourceIdsFor(manifests, inputPath) {
5754
- return manifests.filter((manifest) => manifest.originalPath && withinRoot(path11.resolve(inputPath), path11.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));
5755
6411
  }
5756
6412
  function fileSourceIdsFor(manifests, inputPath) {
5757
- const absoluteInput = path11.resolve(inputPath);
5758
- return manifests.filter((manifest) => manifest.originalPath && path11.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));
5759
6415
  }
5760
6416
  async function syncDirectorySource(rootDir, inputPath, repoRoot) {
5761
6417
  const manifestsBefore = await listManifests(rootDir);
5762
6418
  const previousInScope = manifestsBefore.filter(
5763
- (manifest) => manifest.originalPath && withinRoot(path11.resolve(inputPath), path11.resolve(manifest.originalPath))
6419
+ (manifest) => manifest.originalPath && withinRoot(path13.resolve(inputPath), path13.resolve(manifest.originalPath))
5764
6420
  );
5765
6421
  const result = await ingestDirectory(rootDir, inputPath, { repoRoot });
5766
6422
  const removed = [];
@@ -5768,7 +6424,7 @@ async function syncDirectorySource(rootDir, inputPath, repoRoot) {
5768
6424
  if (!manifest.originalPath) {
5769
6425
  continue;
5770
6426
  }
5771
- if (await fileExists(path11.resolve(manifest.originalPath))) {
6427
+ if (await fileExists(path13.resolve(manifest.originalPath))) {
5772
6428
  continue;
5773
6429
  }
5774
6430
  const removedManifest = await removeManifestBySourceId(rootDir, manifest.sourceId);
@@ -5778,7 +6434,7 @@ async function syncDirectorySource(rootDir, inputPath, repoRoot) {
5778
6434
  }
5779
6435
  const manifestsAfter = await listManifests(rootDir);
5780
6436
  return {
5781
- title: path11.basename(inputPath) || inputPath,
6437
+ title: path13.basename(inputPath) || inputPath,
5782
6438
  sourceIds: directorySourceIdsFor(manifestsAfter, inputPath),
5783
6439
  counts: {
5784
6440
  scannedCount: result.scannedCount,
@@ -5794,7 +6450,7 @@ async function syncFileSource(rootDir, inputPath) {
5794
6450
  const result = await ingestInputDetailed(rootDir, inputPath);
5795
6451
  const manifestsAfter = await listManifests(rootDir);
5796
6452
  return {
5797
- title: path11.basename(inputPath, path11.extname(inputPath)) || inputPath,
6453
+ title: path13.basename(inputPath, path13.extname(inputPath)) || inputPath,
5798
6454
  sourceIds: fileSourceIdsFor(manifestsAfter, inputPath),
5799
6455
  counts: {
5800
6456
  scannedCount: result.scannedCount,
@@ -5828,8 +6484,11 @@ async function runGitCommand(cwd, args) {
5828
6484
  }
5829
6485
  async function syncGitHubRepoSource(rootDir, entry) {
5830
6486
  const workingDir = await managedSourceWorkingDir(rootDir, entry.id);
5831
- const checkoutDir = path11.join(workingDir, "checkout");
5832
- 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
+ }
5833
6492
  await ensureDir(workingDir);
5834
6493
  if (!entry.url) {
5835
6494
  throw new Error(`Managed source ${entry.id} is missing its repository URL.`);
@@ -5838,7 +6497,35 @@ async function syncGitHubRepoSource(rootDir, entry) {
5838
6497
  if (!github) {
5839
6498
  throw new Error(`Managed source ${entry.id} has an invalid GitHub repo URL.`);
5840
6499
  }
5841
- 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
+ }
5842
6529
  return await syncDirectorySource(rootDir, checkoutDir, checkoutDir);
5843
6530
  }
5844
6531
  async function syncCrawlSource(rootDir, entry, options) {
@@ -5959,7 +6646,7 @@ function scopedNodeIds(graph, sourceIds) {
5959
6646
  async function loadSourceAnalyses(rootDir, sourceIds) {
5960
6647
  const { paths } = await loadVaultConfig(rootDir);
5961
6648
  const analyses = await Promise.all(
5962
- sourceIds.map(async (sourceId) => await readJsonFile(path11.join(paths.analysesDir, `${sourceId}.json`)))
6649
+ sourceIds.map(async (sourceId) => await readJsonFile(path13.join(paths.analysesDir, `${sourceId}.json`)))
5963
6650
  );
5964
6651
  return analyses.filter((analysis) => Boolean(analysis?.sourceId));
5965
6652
  }
@@ -6120,9 +6807,9 @@ async function writeSourceBriefForScope(rootDir, source) {
6120
6807
  confidence: 0.82
6121
6808
  }
6122
6809
  });
6123
- const absolutePath = path11.join(paths.wikiDir, output.page.path);
6124
- await ensureDir(path11.dirname(absolutePath));
6125
- 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");
6126
6813
  return absolutePath;
6127
6814
  }
6128
6815
  async function writeSourceBrief(rootDir, source) {
@@ -6410,7 +7097,7 @@ function selectGuidedTargetPages(scope, sourcePages, questions) {
6410
7097
  return (matchedTargets.length ? matchedTargets : canonicalPages).slice(0, 6);
6411
7098
  }
6412
7099
  function insightRelativePathForTarget(page, scope) {
6413
- const basename = path11.basename(page.path);
7100
+ const basename = path13.basename(page.path);
6414
7101
  if (page.kind === "concept") {
6415
7102
  return `insights/concepts/${basename}`;
6416
7103
  }
@@ -6637,7 +7324,7 @@ async function stageSourceReviewForScope(rootDir, scope) {
6637
7324
  return {
6638
7325
  sourceId: scope.id,
6639
7326
  pageId: output.page.id,
6640
- reviewPath: path11.join(approval.approvalDir, "wiki", output.page.path),
7327
+ reviewPath: path13.join(approval.approvalDir, "wiki", output.page.path),
6641
7328
  staged: true,
6642
7329
  approvalId: approval.approvalId,
6643
7330
  approvalDir: approval.approvalDir
@@ -6704,7 +7391,7 @@ async function buildSourceSessionSavedPage(rootDir, scope, session) {
6704
7391
  const evidenceState = contradictions.length > 0 ? "conflicting" : session.targetedPagePaths.some(
6705
7392
  (targetPath) => sourcePages.some((page) => page.path === targetPath && page.sourceIds.some((sourceId) => !scope.sourceIds.includes(sourceId)))
6706
7393
  ) ? "reinforcing" : session.targetedPagePaths.length ? "new" : "needs_judgment";
6707
- const relativeBriefPath = session.briefPath && path11.isAbsolute(session.briefPath) ? path11.relative(paths.wikiDir, session.briefPath) : session.briefPath;
7394
+ const relativeBriefPath = session.briefPath && path13.isAbsolute(session.briefPath) ? path13.relative(paths.wikiDir, session.briefPath) : session.briefPath;
6708
7395
  const sessionMarkdown = [
6709
7396
  `# Guided Session: ${scope.title}`,
6710
7397
  "",
@@ -6787,9 +7474,9 @@ async function buildSourceSessionSavedPage(rootDir, scope, session) {
6787
7474
  async function persistSourceSessionPage(rootDir, scope, session) {
6788
7475
  const { paths } = await loadVaultConfig(rootDir);
6789
7476
  const output = await buildSourceSessionSavedPage(rootDir, scope, session);
6790
- const absolutePath = path11.join(paths.wikiDir, output.page.path);
6791
- await ensureDir(path11.dirname(absolutePath));
6792
- 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");
6793
7480
  return { pageId: output.page.id, sessionPath: absolutePath };
6794
7481
  }
6795
7482
  async function buildGuidedUpdatePages(rootDir, scope, session) {
@@ -6817,8 +7504,8 @@ async function buildGuidedUpdatePages(rootDir, scope, session) {
6817
7504
  targetPages.map(async (targetPage) => {
6818
7505
  const evidenceState = classifyGuidedEvidenceState(scope, targetPage, contradictions);
6819
7506
  const relativePath = useCanonicalTargets && targetPage ? targetPage.path : targetPage ? insightRelativePathForTarget(targetPage, scope) : `insights/topics/${slugify(scope.title)}.md`;
6820
- const absolutePath = path11.join(paths.wikiDir, relativePath);
6821
- 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") : "";
6822
7509
  const parsed = existingContent ? matter3(existingContent) : { data: {}, content: "" };
6823
7510
  const existingData = parsed.data;
6824
7511
  const existingSourceIds = Array.isArray(existingData.source_ids) ? existingData.source_ids.filter((value) => typeof value === "string") : [];
@@ -6989,8 +7676,8 @@ async function stageSourceGuideForScope(rootDir, scope, options = {}) {
6989
7676
  }
6990
7677
  );
6991
7678
  session.status = "staged";
6992
- session.reviewPath = path11.join(approval.approvalDir, "wiki", reviewOutput.page.path);
6993
- session.guidePath = path11.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);
6994
7681
  session.approvalId = approval.approvalId;
6995
7682
  session.approvalDir = approval.approvalDir;
6996
7683
  const persisted = await persistSourceSessionPage(rootDir, scope, session);
@@ -7124,16 +7811,28 @@ async function addManagedSource(rootDir, input, options = {}) {
7124
7811
  const briefRequested = guideRequested ? true : options.brief ?? true;
7125
7812
  const reviewRequested = guideRequested ? false : options.review ?? false;
7126
7813
  const sources = await loadManagedSources(rootDir);
7127
- const resolved = await resolveManagedSourceInput(rootDir, input);
7814
+ const resolved = await resolveManagedSourceInput(rootDir, input, options);
7128
7815
  const existing = sources.find((candidate) => matchesManagedSourceSpec(candidate, resolved));
7129
7816
  const now = (/* @__PURE__ */ new Date()).toISOString();
7130
- const source = existing ?? {
7131
- id: resolved.kind === "directory" || resolved.kind === "file" ? stableManagedSourceId(resolved.kind, path11.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
+ ),
7132
7828
  kind: resolved.kind,
7133
7829
  title: resolved.title,
7134
7830
  path: resolved.kind === "directory" || resolved.kind === "file" ? resolved.path : void 0,
7135
7831
  repoRoot: resolved.kind === "directory" ? resolved.repoRoot : void 0,
7136
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,
7137
7836
  createdAt: now,
7138
7837
  updatedAt: now,
7139
7838
  status: "ready",
@@ -7256,7 +7955,7 @@ async function deleteManagedSource(rootDir, id) {
7256
7955
  sources.filter((source) => source.id !== id)
7257
7956
  );
7258
7957
  const workingDir = await managedSourceWorkingDir(rootDir, id);
7259
- await fs10.rm(workingDir, { recursive: true, force: true });
7958
+ await fs11.rm(workingDir, { recursive: true, force: true });
7260
7959
  return { removed: target };
7261
7960
  }
7262
7961
 
@@ -7264,9 +7963,9 @@ async function deleteManagedSource(rootDir, id) {
7264
7963
  import { execFile as execFile2 } from "child_process";
7265
7964
  import { randomUUID } from "crypto";
7266
7965
  import { EventEmitter } from "events";
7267
- import fs11 from "fs/promises";
7966
+ import fs12 from "fs/promises";
7268
7967
  import http from "http";
7269
- import path12 from "path";
7968
+ import path14 from "path";
7270
7969
  import { promisify as promisify2 } from "util";
7271
7970
  import matter4 from "gray-matter";
7272
7971
  import mime from "mime-types";
@@ -7420,7 +8119,7 @@ function toViewerLintFindings(findings) {
7420
8119
  var execFileAsync2 = promisify2(execFile2);
7421
8120
  async function isReadableFile(absolutePath) {
7422
8121
  try {
7423
- const stats = await fs11.stat(absolutePath);
8122
+ const stats = await fs12.stat(absolutePath);
7424
8123
  return stats.isFile();
7425
8124
  } catch {
7426
8125
  return false;
@@ -7431,15 +8130,15 @@ async function readViewerPage(rootDir, relativePath) {
7431
8130
  return null;
7432
8131
  }
7433
8132
  const { paths } = await loadVaultConfig(rootDir);
7434
- const absolutePath = path12.resolve(paths.wikiDir, relativePath);
8133
+ const absolutePath = path14.resolve(paths.wikiDir, relativePath);
7435
8134
  if (!isPathWithin(paths.wikiDir, absolutePath) || !await isReadableFile(absolutePath)) {
7436
8135
  return null;
7437
8136
  }
7438
- const raw = await fs11.readFile(absolutePath, "utf8");
8137
+ const raw = await fs12.readFile(absolutePath, "utf8");
7439
8138
  const parsed = matter4(raw);
7440
8139
  return {
7441
8140
  path: relativePath,
7442
- title: typeof parsed.data.title === "string" ? parsed.data.title : path12.basename(relativePath, path12.extname(relativePath)),
8141
+ title: typeof parsed.data.title === "string" ? parsed.data.title : path14.basename(relativePath, path14.extname(relativePath)),
7443
8142
  frontmatter: parsed.data,
7444
8143
  content: parsed.content,
7445
8144
  assets: normalizeOutputAssets(parsed.data.output_assets)
@@ -7450,12 +8149,12 @@ async function readViewerAsset(rootDir, relativePath) {
7450
8149
  return null;
7451
8150
  }
7452
8151
  const { paths } = await loadVaultConfig(rootDir);
7453
- const absolutePath = path12.resolve(paths.wikiDir, relativePath);
8152
+ const absolutePath = path14.resolve(paths.wikiDir, relativePath);
7454
8153
  if (!isPathWithin(paths.wikiDir, absolutePath) || !await isReadableFile(absolutePath)) {
7455
8154
  return null;
7456
8155
  }
7457
8156
  return {
7458
- buffer: await fs11.readFile(absolutePath),
8157
+ buffer: await fs12.readFile(absolutePath),
7459
8158
  mimeType: mime.lookup(absolutePath) || "application/octet-stream"
7460
8159
  };
7461
8160
  }
@@ -7491,8 +8190,8 @@ async function writeInboxClip(rootDir, body) {
7491
8190
  const tags = Array.isArray(body.tags) ? body.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : [];
7492
8191
  const now = (/* @__PURE__ */ new Date()).toISOString();
7493
8192
  const fileName = `${now.replace(/[:.]/g, "-")}-${slugForClip(title)}.md`;
7494
- const inboxPath = path12.join(paths.inboxDir, fileName);
7495
- await fs11.mkdir(paths.inboxDir, { recursive: true });
8193
+ const inboxPath = path14.join(paths.inboxDir, fileName);
8194
+ await fs12.mkdir(paths.inboxDir, { recursive: true });
7496
8195
  const lines = [
7497
8196
  "---",
7498
8197
  `title: ${JSON.stringify(title)}`,
@@ -7509,17 +8208,17 @@ async function writeInboxClip(rootDir, body) {
7509
8208
  selectionHtml && !markdown ? ["", "## Original HTML", "", "```html", selectionHtml, "```"].join("\n") : void 0,
7510
8209
  ""
7511
8210
  ].filter((line) => line !== void 0);
7512
- await fs11.writeFile(inboxPath, lines.join("\n"), "utf8");
8211
+ await fs12.writeFile(inboxPath, lines.join("\n"), "utf8");
7513
8212
  const result = await importInbox(rootDir, paths.inboxDir);
7514
8213
  return { mode: "inbox", inboxPath, result };
7515
8214
  }
7516
8215
  async function ensureViewerDist(viewerDistDir) {
7517
- const indexPath = path12.join(viewerDistDir, "index.html");
8216
+ const indexPath = path14.join(viewerDistDir, "index.html");
7518
8217
  if (await fileExists(indexPath)) {
7519
8218
  return;
7520
8219
  }
7521
- const viewerProjectDir = path12.dirname(viewerDistDir);
7522
- if (await fileExists(path12.join(viewerProjectDir, "package.json"))) {
8220
+ const viewerProjectDir = path14.dirname(viewerDistDir);
8221
+ if (await fileExists(path14.join(viewerProjectDir, "package.json"))) {
7523
8222
  await execFileAsync2("pnpm", ["build"], { cwd: viewerProjectDir });
7524
8223
  }
7525
8224
  }
@@ -7542,7 +8241,7 @@ async function startGraphServer(rootDir, port, options = {}) {
7542
8241
  response.end(JSON.stringify({ error: "Graph artifact not found. Run `swarmvault compile` first." }));
7543
8242
  return;
7544
8243
  }
7545
- const reportPath = path12.join(paths.wikiDir, "graph", "report.json");
8244
+ const reportPath = path14.join(paths.wikiDir, "graph", "report.json");
7546
8245
  const report = await readJsonFile(reportPath) ?? null;
7547
8246
  response.writeHead(200, { "content-type": "application/json" });
7548
8247
  response.end(JSON.stringify(buildViewerGraphArtifact(graph, { report, full: options.full ?? false })));
@@ -7606,13 +8305,13 @@ async function startGraphServer(rootDir, port, options = {}) {
7606
8305
  return;
7607
8306
  }
7608
8307
  if (url.pathname === "/api/graph-report") {
7609
- const reportPath = path12.join(paths.wikiDir, "graph", "report.json");
8308
+ const reportPath = path14.join(paths.wikiDir, "graph", "report.json");
7610
8309
  if (!await fileExists(reportPath)) {
7611
8310
  response.writeHead(404, { "content-type": "application/json" });
7612
8311
  response.end(JSON.stringify({ error: "Graph report artifact not found. Run `swarmvault compile` first." }));
7613
8312
  return;
7614
8313
  }
7615
- const body = await fs11.readFile(reportPath, "utf8");
8314
+ const body = await fs12.readFile(reportPath, "utf8");
7616
8315
  response.writeHead(200, { "content-type": "application/json" });
7617
8316
  response.end(body);
7618
8317
  return;
@@ -7850,7 +8549,7 @@ async function startGraphServer(rootDir, port, options = {}) {
7850
8549
  return;
7851
8550
  }
7852
8551
  if (url.pathname === "/api/workspace") {
7853
- const reportPath = path12.join(paths.wikiDir, "graph", "report.json");
8552
+ const reportPath = path14.join(paths.wikiDir, "graph", "report.json");
7854
8553
  const [graphRaw, reportRaw, approvalsRaw, candidatesRaw, memoryTasksRaw, watchStatusRaw, lintRaw, doctorRaw] = await Promise.all([
7855
8554
  readJsonFile(paths.graphPath).catch(() => null),
7856
8555
  readJsonFile(reportPath).catch(() => null),
@@ -7975,15 +8674,15 @@ async function startGraphServer(rootDir, port, options = {}) {
7975
8674
  return;
7976
8675
  }
7977
8676
  const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
7978
- const target = path12.join(paths.viewerDistDir, relativePath);
7979
- const fallback = path12.join(paths.viewerDistDir, "index.html");
8677
+ const target = path14.join(paths.viewerDistDir, relativePath);
8678
+ const fallback = path14.join(paths.viewerDistDir, "index.html");
7980
8679
  const filePath = await fileExists(target) ? target : fallback;
7981
8680
  if (!await fileExists(filePath)) {
7982
8681
  response.writeHead(503, { "content-type": "text/plain" });
7983
8682
  response.end("Viewer build not found. Run `pnpm build` first.");
7984
8683
  return;
7985
8684
  }
7986
- const staticBody = await fs11.readFile(filePath);
8685
+ const staticBody = await fs12.readFile(filePath);
7987
8686
  response.writeHead(200, { "content-type": mime.lookup(filePath) || "text/plain" });
7988
8687
  response.end(staticBody);
7989
8688
  } catch (error) {
@@ -8023,7 +8722,7 @@ async function exportGraphHtml(rootDir, outputPath, options = {}) {
8023
8722
  throw new Error("Graph artifact not found. Run `swarmvault compile` first.");
8024
8723
  }
8025
8724
  await ensureViewerDist(paths.viewerDistDir);
8026
- const indexPath = path12.join(paths.viewerDistDir, "index.html");
8725
+ const indexPath = path14.join(paths.viewerDistDir, "index.html");
8027
8726
  if (!await fileExists(indexPath)) {
8028
8727
  throw new Error("Viewer build not found. Run `pnpm build` first.");
8029
8728
  }
@@ -8049,17 +8748,17 @@ async function exportGraphHtml(rootDir, outputPath, options = {}) {
8049
8748
  } : null;
8050
8749
  })
8051
8750
  );
8052
- const rawHtml = await fs11.readFile(indexPath, "utf8");
8751
+ const rawHtml = await fs12.readFile(indexPath, "utf8");
8053
8752
  const scriptMatch = rawHtml.match(/<script type="module" crossorigin src="([^"]+)"><\/script>/);
8054
8753
  const styleMatch = rawHtml.match(/<link rel="stylesheet" crossorigin href="([^"]+)">/);
8055
- const scriptPath = scriptMatch?.[1] ? path12.join(paths.viewerDistDir, scriptMatch[1].replace(/^\//, "")) : null;
8056
- const stylePath = styleMatch?.[1] ? path12.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;
8057
8756
  if (!scriptPath || !await fileExists(scriptPath)) {
8058
8757
  throw new Error("Viewer script bundle not found. Run `pnpm build` first.");
8059
8758
  }
8060
- const script = await fs11.readFile(scriptPath, "utf8");
8061
- const style = stylePath && await fileExists(stylePath) ? await fs11.readFile(stylePath, "utf8") : "";
8062
- const report = await readJsonFile(path12.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"));
8063
8762
  const embeddedData = JSON.stringify(
8064
8763
  { graph: buildViewerGraphArtifact(graph, { report, full: options.full ?? false }), pages: pages.filter(Boolean), report },
8065
8764
  null,
@@ -8082,9 +8781,9 @@ async function exportGraphHtml(rootDir, outputPath, options = {}) {
8082
8781
  "</html>",
8083
8782
  ""
8084
8783
  ].filter(Boolean).join("\n");
8085
- await fs11.mkdir(path12.dirname(outputPath), { recursive: true });
8086
- await fs11.writeFile(outputPath, html, "utf8");
8087
- return path12.resolve(outputPath);
8784
+ await fs12.mkdir(path14.dirname(outputPath), { recursive: true });
8785
+ await fs12.writeFile(outputPath, html, "utf8");
8786
+ return path14.resolve(outputPath);
8088
8787
  }
8089
8788
  export {
8090
8789
  ALL_MIGRATIONS,
@@ -8114,6 +8813,7 @@ export {
8114
8813
  buildConfiguredRedactor,
8115
8814
  buildContextPack,
8116
8815
  buildGraphShareArtifact,
8816
+ buildGraphTree,
8117
8817
  buildMemoryGraphElements,
8118
8818
  buildRedactor,
8119
8819
  checkTrackedRepoChanges,
@@ -8137,12 +8837,14 @@ export {
8137
8837
  estimatePageTokens,
8138
8838
  estimateTokens,
8139
8839
  evaluateCandidateForPromotion,
8840
+ evaluateGraphShrinkGuard,
8140
8841
  expectedModelPath,
8141
8842
  explainGraphVault,
8142
8843
  exploreVault,
8143
8844
  exportGraphFormat,
8144
8845
  exportGraphHtml,
8145
8846
  exportGraphReportHtml,
8847
+ exportGraphTree,
8146
8848
  exportObsidianCanvas,
8147
8849
  exportObsidianVault,
8148
8850
  finishMemoryTask,
@@ -8187,6 +8889,7 @@ export {
8187
8889
  lookupPresetCapabilities,
8188
8890
  markSuperseded,
8189
8891
  memoryTaskHashes,
8892
+ mergeGraphFiles,
8190
8893
  modelDownloadUrl,
8191
8894
  pathGraphVault,
8192
8895
  persistDecayFrontmatter,
@@ -8214,6 +8917,7 @@ export {
8214
8917
  renderGraphShareMarkdown,
8215
8918
  renderGraphSharePreviewHtml,
8216
8919
  renderGraphShareSvg,
8920
+ renderGraphTreeHtml,
8217
8921
  renderMemoryTaskMarkdown,
8218
8922
  resetDecay,
8219
8923
  resolveArtifactRootDir,