@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/README.md +8 -4
- package/dist/chunk-5GEPTIZE.js +26010 -0
- package/dist/chunk-7O2HJSWQ.js +1686 -0
- package/dist/chunk-S2E65WRI.js +26062 -0
- package/dist/chunk-V7KX3AQD.js +26010 -0
- package/dist/hooks/claude.js +53 -5
- package/dist/hooks/codex.js +13 -3
- package/dist/hooks/copilot.js +13 -3
- package/dist/hooks/gemini.js +13 -3
- package/dist/index.d.ts +127 -5
- package/dist/index.js +905 -129
- package/dist/memory-DNSQCDHC.js +32 -0
- package/dist/memory-FVIBFROA.js +32 -0
- package/dist/memory-HE6VWUPV.js +32 -0
- package/dist/registry-NMXDBYIZ.js +12 -0
- package/package.json +2 -1
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-
|
|
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-
|
|
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-
|
|
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
|
|
3723
|
-
const label = slugify(
|
|
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
|
|
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:
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
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
|
|
3925
|
-
import
|
|
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 =
|
|
4609
|
+
let current = path9.resolve(startPath);
|
|
3931
4610
|
try {
|
|
3932
|
-
const stat = await
|
|
4611
|
+
const stat = await fs7.stat(current);
|
|
3933
4612
|
if (!stat.isDirectory()) {
|
|
3934
|
-
current =
|
|
4613
|
+
current = path9.dirname(current);
|
|
3935
4614
|
}
|
|
3936
4615
|
} catch {
|
|
3937
|
-
current =
|
|
4616
|
+
current = path9.dirname(current);
|
|
3938
4617
|
}
|
|
3939
4618
|
while (true) {
|
|
3940
|
-
if (await fileExists(
|
|
4619
|
+
if (await fileExists(path9.join(current, ".git"))) {
|
|
3941
4620
|
return current;
|
|
3942
4621
|
}
|
|
3943
|
-
const parent =
|
|
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(`${
|
|
3956
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
4000
|
-
await
|
|
4678
|
+
await ensureDir(path9.dirname(filePath));
|
|
4679
|
+
await fs7.writeFile(filePath, `${next}
|
|
4001
4680
|
`, { mode: 493, encoding: "utf8" });
|
|
4002
|
-
await
|
|
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
|
|
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
|
|
4695
|
+
await fs7.rm(filePath, { force: true });
|
|
4017
4696
|
return;
|
|
4018
4697
|
}
|
|
4019
|
-
await
|
|
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(
|
|
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
|
|
4063
|
-
import
|
|
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.
|
|
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(
|
|
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 =
|
|
4833
|
-
return asTextResource(`swarmvault://pages/${encodedPath}`, await
|
|
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(
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
5617
|
+
import fs9 from "fs/promises";
|
|
4927
5618
|
import os from "os";
|
|
4928
|
-
import
|
|
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(
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
4999
|
-
const stat = await
|
|
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
|
|
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
|
|
5137
|
-
import
|
|
5827
|
+
import fs10 from "fs/promises";
|
|
5828
|
+
import path12 from "path";
|
|
5138
5829
|
function scheduleStatePath(schedulesDir, jobId) {
|
|
5139
|
-
return
|
|
5830
|
+
return path12.join(schedulesDir, `${encodeURIComponent(jobId)}.json`);
|
|
5140
5831
|
}
|
|
5141
5832
|
function scheduleLockPath(schedulesDir, jobId) {
|
|
5142
|
-
return
|
|
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
|
|
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
|
|
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
|
|
5411
|
-
import
|
|
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 =
|
|
5458
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
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 =
|
|
6152
|
+
let current = path13.resolve(startPath);
|
|
5462
6153
|
try {
|
|
5463
|
-
const stat = await
|
|
6154
|
+
const stat = await fs11.stat(current);
|
|
5464
6155
|
if (!stat.isDirectory()) {
|
|
5465
|
-
current =
|
|
6156
|
+
current = path13.dirname(current);
|
|
5466
6157
|
}
|
|
5467
6158
|
} catch {
|
|
5468
|
-
current =
|
|
6159
|
+
current = path13.dirname(current);
|
|
5469
6160
|
}
|
|
5470
6161
|
while (true) {
|
|
5471
|
-
if (await fileExists(
|
|
6162
|
+
if (await fileExists(path13.join(current, ".git"))) {
|
|
5472
6163
|
return current;
|
|
5473
6164
|
}
|
|
5474
|
-
const parent =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
5642
|
-
const
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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 =
|
|
5691
|
-
return manifests.filter((manifest) => manifest.originalPath &&
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
5765
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
6057
|
-
await ensureDir(
|
|
6058
|
-
await
|
|
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 =
|
|
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:
|
|
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 &&
|
|
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 =
|
|
6724
|
-
await ensureDir(
|
|
6725
|
-
await
|
|
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 =
|
|
6754
|
-
const existingContent = await fileExists(absolutePath) ? await
|
|
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 =
|
|
6926
|
-
session.guidePath =
|
|
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
|
-
|
|
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
|
|
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
|
|
7966
|
+
import fs12 from "fs/promises";
|
|
7201
7967
|
import http from "http";
|
|
7202
|
-
import
|
|
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
|
|
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 =
|
|
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
|
|
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 :
|
|
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 =
|
|
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
|
|
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 =
|
|
7428
|
-
await
|
|
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
|
|
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 =
|
|
8216
|
+
const indexPath = path14.join(viewerDistDir, "index.html");
|
|
7451
8217
|
if (await fileExists(indexPath)) {
|
|
7452
8218
|
return;
|
|
7453
8219
|
}
|
|
7454
|
-
const viewerProjectDir =
|
|
7455
|
-
if (await fileExists(
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
7912
|
-
const fallback =
|
|
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
|
|
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 =
|
|
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
|
|
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] ?
|
|
7989
|
-
const stylePath = styleMatch?.[1] ?
|
|
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
|
|
7994
|
-
const style = stylePath && await fileExists(stylePath) ? await
|
|
7995
|
-
const report = await readJsonFile(
|
|
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
|
|
8019
|
-
await
|
|
8020
|
-
return
|
|
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,
|