@sksoftofficial/mindroot 1.0.6 → 1.1.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/INSTRUCTIONS.md +41 -0
- package/README.md +80 -13
- package/bin/mindroot.js +4 -0
- package/package.json +6 -2
- package/src/commands/install.js +210 -0
- package/src/core.js +9 -2
- package/src/dashboard/graph.js +175 -0
- package/src/dashboard/index.html +246 -80
- package/src/mcp.js +62 -1
- package/src/paths.js +15 -3
- package/src/search.js +28 -0
- package/src/server.js +25 -4
- package/src/skills.js +133 -0
- package/src/store/db.js +50 -0
- package/test/install-clients.test.js +187 -0
- package/test/install.test.js +127 -0
- package/test/mcp.test.js +4 -0
- package/test/paths.test.js +53 -0
- package/test/skills.test.js +253 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
let library;
|
|
2
|
+
|
|
3
|
+
export async function mountGraph(view, data) {
|
|
4
|
+
const notes = new Map(data.docs.map((doc) => [doc.path, {
|
|
5
|
+
id: "note:" + doc.path, kind: "note", label: doc.title || doc.path,
|
|
6
|
+
neighbors: new Set(),
|
|
7
|
+
}]));
|
|
8
|
+
const nodes = [...notes.values()];
|
|
9
|
+
const links = [];
|
|
10
|
+
for (const memory of data.memories) {
|
|
11
|
+
const [path] = (memory.target_path || "").split("::");
|
|
12
|
+
const note = notes.get(path);
|
|
13
|
+
const node = {
|
|
14
|
+
id: "memory:" + memory.id, kind: "memory", label: memory.text,
|
|
15
|
+
memoryId: memory.id,
|
|
16
|
+
neighbors: new Set(),
|
|
17
|
+
};
|
|
18
|
+
nodes.push(node);
|
|
19
|
+
// Section targets share their note's node; no new relationships are inferred.
|
|
20
|
+
if (note) {
|
|
21
|
+
links.push({ source: node.id, target: note.id });
|
|
22
|
+
node.neighbors.add(note.id);
|
|
23
|
+
note.neighbors.add(node.id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!nodes.length) {
|
|
27
|
+
view.innerHTML = '<div class="empty">No notes or memories to graph yet. Existing memory links will appear here automatically.</div>';
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!library) {
|
|
32
|
+
library = new Promise((resolve, reject) => {
|
|
33
|
+
const script = document.createElement("script");
|
|
34
|
+
script.src = "/vendor/force-graph.min.js";
|
|
35
|
+
script.onload = () => resolve(window.ForceGraph);
|
|
36
|
+
script.onerror = () => { script.remove(); library = null; reject(new Error("Unable to load the graph renderer. Reopen Graph to retry.")); };
|
|
37
|
+
document.head.appendChild(script);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const ForceGraph = await library;
|
|
41
|
+
if (!view.isConnected) return;
|
|
42
|
+
view.className = "view graph-view";
|
|
43
|
+
view.innerHTML = `
|
|
44
|
+
<div class="graph-workspace">
|
|
45
|
+
<div class="graph-stage">
|
|
46
|
+
<div class="graph-canvas" role="img" aria-label="Project graph showing notes, memories, and their existing connections."></div>
|
|
47
|
+
<div class="graph-legend"><span><i></i>Notes</span><span><i class="memory-dot"></i>Memories</span></div>
|
|
48
|
+
<div class="graph-controls"><button aria-label="Zoom in">+</button><button aria-label="Zoom out">-</button></div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<p class="graph-hint">Drag to pan or move a node. Scroll or pinch to zoom. Only existing memory-to-note links are shown.</p>`;
|
|
52
|
+
|
|
53
|
+
const host = view.querySelector(".graph-canvas");
|
|
54
|
+
const styles = getComputedStyle(view);
|
|
55
|
+
const colors = { note: styles.getPropertyValue("--accent").trim(), memory: styles.getPropertyValue("--ok").trim() };
|
|
56
|
+
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
57
|
+
const duration = reducedMotion ? 0 : 300;
|
|
58
|
+
let selected = null;
|
|
59
|
+
let hovered = null;
|
|
60
|
+
let disposed = false;
|
|
61
|
+
let initialFit = true;
|
|
62
|
+
let fitFrame;
|
|
63
|
+
const radius = (node, scale = 1) => node.kind === "note"
|
|
64
|
+
? Math.max(4 / scale, 5 + Math.min(5, Math.sqrt(node.neighbors.size)))
|
|
65
|
+
: Math.max(2 / scale, 3.5);
|
|
66
|
+
const focused = (node) => {
|
|
67
|
+
const active = hovered || selected;
|
|
68
|
+
return !active || active === node || active.neighbors.has(node.id);
|
|
69
|
+
};
|
|
70
|
+
const paint = (node, ctx, scale) => {
|
|
71
|
+
const active = hovered === node || selected === node;
|
|
72
|
+
const bright = focused(node);
|
|
73
|
+
const r = radius(node, scale);
|
|
74
|
+
ctx.save();
|
|
75
|
+
ctx.globalAlpha = bright ? 1 : 0.18;
|
|
76
|
+
if (active) {
|
|
77
|
+
ctx.beginPath();
|
|
78
|
+
ctx.arc(node.x, node.y, r + 4 / scale, 0, Math.PI * 2);
|
|
79
|
+
ctx.strokeStyle = colors[node.kind];
|
|
80
|
+
ctx.lineWidth = 1.5 / scale;
|
|
81
|
+
ctx.stroke();
|
|
82
|
+
}
|
|
83
|
+
ctx.beginPath();
|
|
84
|
+
ctx.arc(node.x, node.y, r, 0, Math.PI * 2);
|
|
85
|
+
ctx.fillStyle = colors[node.kind];
|
|
86
|
+
ctx.fill();
|
|
87
|
+
if (active || (bright && node.kind === "note")) {
|
|
88
|
+
const label = node.kind === "memory" ? "#" + node.memoryId + " " + node.label : node.label;
|
|
89
|
+
const text = label.length > 48 ? label.slice(0, 47) + "..." : label;
|
|
90
|
+
const fontSize = 12 / scale;
|
|
91
|
+
ctx.font = `${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`;
|
|
92
|
+
ctx.textAlign = "center";
|
|
93
|
+
ctx.textBaseline = "top";
|
|
94
|
+
const y = node.y + r + 5 / scale;
|
|
95
|
+
const width = ctx.measureText(text).width;
|
|
96
|
+
ctx.fillStyle = "#161616";
|
|
97
|
+
ctx.fillRect(node.x - width / 2 - 3 / scale, y - 1 / scale, width + 6 / scale, fontSize + 4 / scale);
|
|
98
|
+
ctx.fillStyle = active ? "#fff" : "#c4c4c4";
|
|
99
|
+
ctx.fillText(text, node.x, y);
|
|
100
|
+
}
|
|
101
|
+
ctx.restore();
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const graph = new ForceGraph(host)
|
|
105
|
+
.width(host.clientWidth).height(host.clientHeight)
|
|
106
|
+
.backgroundColor(styles.getPropertyValue("--bg2").trim())
|
|
107
|
+
.nodeLabel(() => "")
|
|
108
|
+
.nodeCanvasObject(paint)
|
|
109
|
+
.nodePointerAreaPaint((node, color, ctx, scale) => {
|
|
110
|
+
ctx.beginPath();
|
|
111
|
+
ctx.arc(node.x, node.y, radius(node, scale) + 5 / scale, 0, Math.PI * 2);
|
|
112
|
+
ctx.fillStyle = color;
|
|
113
|
+
ctx.fill();
|
|
114
|
+
})
|
|
115
|
+
.linkColor((link) => {
|
|
116
|
+
const active = hovered || selected;
|
|
117
|
+
if (active && (link.source === active || link.target === active)) return "#e9973f99";
|
|
118
|
+
return active ? "#ffffff0c" : "#ffffff26";
|
|
119
|
+
})
|
|
120
|
+
.linkWidth(1)
|
|
121
|
+
.minZoom(0.15).maxZoom(8)
|
|
122
|
+
.warmupTicks(100).cooldownTicks(reducedMotion ? 0 : 100)
|
|
123
|
+
.onNodeHover((node) => { hovered = node; host.style.cursor = node ? "pointer" : "grab"; redraw(); })
|
|
124
|
+
.onNodeClick((node) => { selected = node; redraw(); })
|
|
125
|
+
.onBackgroundClick(() => { selected = null; redraw(); })
|
|
126
|
+
.onEngineStop(() => {
|
|
127
|
+
if (initialFit && nodes.every((node) => Number.isFinite(node.x) && Number.isFinite(node.y))) {
|
|
128
|
+
initialFit = false;
|
|
129
|
+
fit();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
graph.d3Force("charge").strength(-90);
|
|
133
|
+
graph.d3Force("link").distance(55);
|
|
134
|
+
// Disconnected note clusters and standalone memories must not drift off-canvas.
|
|
135
|
+
graph.d3Force("contain", (alpha) => {
|
|
136
|
+
for (const node of nodes) {
|
|
137
|
+
node.vx -= node.x * 0.035 * alpha;
|
|
138
|
+
node.vy -= node.y * 0.035 * alpha;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
graph.graphData({ nodes, links });
|
|
142
|
+
|
|
143
|
+
function redraw() {
|
|
144
|
+
graph.nodeCanvasObject(paint);
|
|
145
|
+
graph.linkColor(graph.linkColor());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function fit() {
|
|
149
|
+
cancelAnimationFrame(fitFrame);
|
|
150
|
+
// Fitting before the async layout initializes poisons the camera with NaN.
|
|
151
|
+
// Defer camera changes until after the current canvas frame is painted.
|
|
152
|
+
fitFrame = requestAnimationFrame(() => {
|
|
153
|
+
if (disposed || !host.clientWidth || !host.clientHeight ||
|
|
154
|
+
!nodes.every((node) => Number.isFinite(node.x) && Number.isFinite(node.y))) return;
|
|
155
|
+
graph.zoomToFit(0, 60);
|
|
156
|
+
redraw();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const [zoomIn, zoomOut] = view.querySelectorAll(".graph-controls button");
|
|
161
|
+
zoomIn.onclick = () => graph.zoom(Math.min(8, graph.zoom() * 1.4), duration);
|
|
162
|
+
zoomOut.onclick = () => graph.zoom(Math.max(0.15, graph.zoom() / 1.4), duration);
|
|
163
|
+
const resize = new ResizeObserver(() => {
|
|
164
|
+
if (disposed || !host.clientWidth || !host.clientHeight) return;
|
|
165
|
+
graph.width(host.clientWidth).height(host.clientHeight);
|
|
166
|
+
fit();
|
|
167
|
+
});
|
|
168
|
+
resize.observe(host);
|
|
169
|
+
return () => {
|
|
170
|
+
disposed = true;
|
|
171
|
+
cancelAnimationFrame(fitFrame);
|
|
172
|
+
resize.disconnect();
|
|
173
|
+
graph._destructor();
|
|
174
|
+
};
|
|
175
|
+
}
|