opencode-fractal-memory 0.6.14 → 0.6.16
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 +30 -6
- package/dist/management/helpers.js +19 -1
- package/dist/management/routes.js +10 -1
- package/management/public/app.js +84 -1
- package/management/public/index.html +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -377,12 +377,18 @@ Opens at [http://localhost:8787](http://localhost:8787). The server starts as a
|
|
|
377
377
|
- **Scroll** to zoom in/out
|
|
378
378
|
- **Left-click** a node to select and inspect it
|
|
379
379
|
- **Right-click drag** to pan
|
|
380
|
-
- Nodes are color-coded by type (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
-
|
|
385
|
-
-
|
|
380
|
+
- Nodes are color-coded by level and type (skill = gold icosahedron, playbook = orange torus, note = blue sphere)
|
|
381
|
+
- Playbook nodes render as orange torus shapes with steps visible in the detail panel
|
|
382
|
+
|
|
383
|
+
**Filters** — narrow down visible nodes:
|
|
384
|
+
- **Scope** (global/project)
|
|
385
|
+
- **Level** (L0–L5), **Type** (note, skill, playbook, etc.), **Shape**, **Custom Type**
|
|
386
|
+
- **Project** — when multiple projects exist, filter by project name
|
|
387
|
+
- **Clear All Filters** button resets everything at once
|
|
388
|
+
- **Search** — find nodes by content, label, or type:
|
|
389
|
+
- Type a query and press Enter
|
|
390
|
+
- Results show relevance scores and preview snippets
|
|
391
|
+
- Click a result to navigate to it in the graph
|
|
386
392
|
|
|
387
393
|
**Inspect** — when you click a node (graph or search results):
|
|
388
394
|
- View full content and summary
|
|
@@ -506,3 +512,21 @@ Unified SQLite database with `project_name` discriminator:
|
|
|
506
512
|
## License
|
|
507
513
|
|
|
508
514
|
MIT
|
|
515
|
+
|
|
516
|
+
## Changelog
|
|
517
|
+
|
|
518
|
+
### v0.6.15 (2026-06-06)
|
|
519
|
+
- **Project switcher** — filter memory nodes by project name in management UI
|
|
520
|
+
- **Clear all filters** — one-click reset of all active filters
|
|
521
|
+
- **Playbook nodes** — now render as orange torus with step details in management UI
|
|
522
|
+
- **TYPE_COLORS** — playbooks (orange) and skills (gold) have dedicated colors in 3D scene
|
|
523
|
+
- Backend: `project_name` in API responses, `GET /api/projects` endpoint
|
|
524
|
+
|
|
525
|
+
### v0.6.14 (2026-06-06)
|
|
526
|
+
- Session reference counter fix — management server only stops when all sessions end
|
|
527
|
+
|
|
528
|
+
### v0.6.13 (2026-06-06)
|
|
529
|
+
- Event hook refactor — management server lifecycle tied to real `session.created`/`session.deleted` events
|
|
530
|
+
|
|
531
|
+
### v0.6.12 (2026-06-06)
|
|
532
|
+
- Fixed management server lifecycle — SIGKILL instead of SIGTERM, proper event hooks
|
|
@@ -75,6 +75,7 @@ export function rowToNode(r) {
|
|
|
75
75
|
parentIds: r.parent_ids ? JSON.parse(r.parent_ids) : null,
|
|
76
76
|
contentLength: r.content_length,
|
|
77
77
|
metadata: r.metadata ? JSON.parse(r.metadata) : null,
|
|
78
|
+
projectName: r.project_name ?? null,
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
export function queryNodes(scope) {
|
|
@@ -85,7 +86,7 @@ export function queryNodes(scope) {
|
|
|
85
86
|
SELECT id, label, content, summary, level, type, importance,
|
|
86
87
|
usefulness_score, times_used, times_helpful, access_count,
|
|
87
88
|
sticky, confidence, created_at, updated_at, parent_ids,
|
|
88
|
-
LENGTH(content) as content_length, metadata
|
|
89
|
+
LENGTH(content) as content_length, metadata, project_name
|
|
89
90
|
FROM memory_nodes
|
|
90
91
|
WHERE scope = ?
|
|
91
92
|
ORDER BY level, importance DESC
|
|
@@ -169,6 +170,7 @@ export function computeStats(nodes) {
|
|
|
169
170
|
const nodesPerType = {};
|
|
170
171
|
const nodesPerCustomType = {};
|
|
171
172
|
const nodesPerShape = {};
|
|
173
|
+
const nodesPerProject = {};
|
|
172
174
|
let totalImportance = 0;
|
|
173
175
|
let totalUsefulness = 0;
|
|
174
176
|
let totalAccessCount = 0;
|
|
@@ -183,6 +185,8 @@ export function computeStats(nodes) {
|
|
|
183
185
|
}
|
|
184
186
|
const shape = resolveNodeShape(node);
|
|
185
187
|
nodesPerShape[shape] = (nodesPerShape[shape] ?? 0) + 1;
|
|
188
|
+
const project = node.projectName || "(default)";
|
|
189
|
+
nodesPerProject[project] = (nodesPerProject[project] ?? 0) + 1;
|
|
186
190
|
totalImportance += node.importance;
|
|
187
191
|
totalUsefulness += node.usefulnessScore;
|
|
188
192
|
totalAccessCount += node.accessCount;
|
|
@@ -196,12 +200,26 @@ export function computeStats(nodes) {
|
|
|
196
200
|
nodesPerType,
|
|
197
201
|
nodesPerCustomType,
|
|
198
202
|
nodesPerShape,
|
|
203
|
+
nodesPerProject,
|
|
199
204
|
avgImportance: Math.round((totalImportance / n) * 100) / 100,
|
|
200
205
|
avgUsefulness: Math.round((totalUsefulness / n) * 100) / 100,
|
|
201
206
|
totalAccessCount,
|
|
202
207
|
stickyCount,
|
|
203
208
|
};
|
|
204
209
|
}
|
|
210
|
+
export function getAvailableProjects(scope) {
|
|
211
|
+
const db = openDb(scope);
|
|
212
|
+
if (!db)
|
|
213
|
+
return [];
|
|
214
|
+
const rows = db.query(`
|
|
215
|
+
SELECT DISTINCT COALESCE(project_name, '') as project_name
|
|
216
|
+
FROM memory_nodes
|
|
217
|
+
WHERE scope = ?
|
|
218
|
+
ORDER BY project_name
|
|
219
|
+
`).all(scope);
|
|
220
|
+
db.close();
|
|
221
|
+
return rows.map(r => r.project_name || "(default)");
|
|
222
|
+
}
|
|
205
223
|
export function readProjectConfig() {
|
|
206
224
|
const configPath = path.join(os.homedir(), ".config", "opencode", "opencode-mem.json");
|
|
207
225
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { memLog } from "../logging";
|
|
2
2
|
import { generateEmbedding } from "../embeddings";
|
|
3
|
-
import { queryNodes, getAvailableScopes, extractLinks, computeStats, readProjectConfig, writeProjectConfig, rowToNode, withDb, jsonResponse, cosineSimilarity, } from "./helpers";
|
|
3
|
+
import { queryNodes, getAvailableScopes, extractLinks, computeStats, readProjectConfig, writeProjectConfig, rowToNode, withDb, jsonResponse, cosineSimilarity, getAvailableProjects, } from "./helpers";
|
|
4
|
+
import { VERSION } from "../version";
|
|
4
5
|
function handleScopes() {
|
|
5
6
|
return jsonResponse(getAvailableScopes());
|
|
6
7
|
}
|
|
@@ -181,11 +182,19 @@ async function handleNodeDelete(ctx) {
|
|
|
181
182
|
return jsonResponse({ success: true });
|
|
182
183
|
});
|
|
183
184
|
}
|
|
185
|
+
function handleProjects(ctx) {
|
|
186
|
+
return jsonResponse(getAvailableProjects(ctx.scope));
|
|
187
|
+
}
|
|
188
|
+
function handleVersion() {
|
|
189
|
+
return jsonResponse({ version: VERSION });
|
|
190
|
+
}
|
|
184
191
|
export function registerRoutes(router) {
|
|
185
192
|
router.get(/^\/api\/scopes$/, () => handleScopes());
|
|
193
|
+
router.get(/^\/api\/version$/, () => handleVersion());
|
|
186
194
|
router.get(/^\/api\/nodes$/, (_, ctx) => handleNodes(ctx));
|
|
187
195
|
router.get(/^\/api\/links$/, (_, ctx) => handleLinks(ctx));
|
|
188
196
|
router.get(/^\/api\/stats$/, (_, ctx) => handleStats(ctx));
|
|
197
|
+
router.get(/^\/api\/projects$/, (_, ctx) => handleProjects(ctx));
|
|
189
198
|
router.get(/^\/api\/config$/, () => handleConfigGet());
|
|
190
199
|
router.put(/^\/api\/config$/, (req) => handleConfigSave(req));
|
|
191
200
|
router.post(/^\/api\/config$/, (req) => handleConfigSave(req));
|
package/management/public/app.js
CHANGED
|
@@ -17,9 +17,15 @@ const TYPE_SHAPES = {
|
|
|
17
17
|
improvement: "sphere",
|
|
18
18
|
howto: "sphere",
|
|
19
19
|
skill: "icosahedron",
|
|
20
|
+
playbook: "torus",
|
|
20
21
|
unknown: "sphere",
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
const TYPE_COLORS = {
|
|
25
|
+
skill: 0xfbbf24,
|
|
26
|
+
playbook: 0xff8c00,
|
|
27
|
+
};
|
|
28
|
+
|
|
23
29
|
const CUSTOM_TYPE_COLORS = {
|
|
24
30
|
'middle-term': 0xff6b6b,
|
|
25
31
|
};
|
|
@@ -36,6 +42,7 @@ class NodeFilterEngine {
|
|
|
36
42
|
this.types = new Set();
|
|
37
43
|
this.customTypes = new Set();
|
|
38
44
|
this.shapes = new Set();
|
|
45
|
+
this.projects = new Set();
|
|
39
46
|
this.searchQuery = "";
|
|
40
47
|
this.searchMode = "text";
|
|
41
48
|
this.serverSearchIds = null;
|
|
@@ -47,17 +54,31 @@ class NodeFilterEngine {
|
|
|
47
54
|
this.types.clear();
|
|
48
55
|
this.customTypes.clear();
|
|
49
56
|
this.shapes.clear();
|
|
57
|
+
this.projects.clear();
|
|
50
58
|
|
|
51
59
|
Object.keys(stats.nodesPerLevel || {}).map(Number).sort((a, b) => a - b).forEach(l => this.levels.add(l));
|
|
52
60
|
Object.keys(stats.nodesPerType || {}).sort().forEach(t => this.types.add(t));
|
|
53
61
|
Object.keys(stats.nodesPerCustomType || {}).sort().forEach(ct => this.customTypes.add(ct));
|
|
54
62
|
Object.keys(stats.nodesPerShape || {}).sort().forEach(s => this.shapes.add(s));
|
|
63
|
+
Object.keys(stats.nodesPerProject || {}).sort().forEach(p => this.projects.add(p));
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
toggleLevel(v) { this._toggle(this.levels, v); this.changed(); }
|
|
58
67
|
toggleType(v) { this._toggle(this.types, v); this.changed(); }
|
|
59
68
|
toggleCustomType(v) { this._toggle(this.customTypes, v); this.changed(); }
|
|
60
69
|
toggleShape(v) { this._toggle(this.shapes, v); this.changed(); }
|
|
70
|
+
toggleProject(v) { this._toggle(this.projects, v); this.changed(); }
|
|
71
|
+
|
|
72
|
+
clearAll() {
|
|
73
|
+
this.levels.clear();
|
|
74
|
+
this.types.clear();
|
|
75
|
+
this.customTypes.clear();
|
|
76
|
+
this.shapes.clear();
|
|
77
|
+
this.projects.clear();
|
|
78
|
+
this.searchQuery = "";
|
|
79
|
+
this.serverSearchIds = null;
|
|
80
|
+
this.changed();
|
|
81
|
+
}
|
|
61
82
|
|
|
62
83
|
setSearchQuery(q) { this.searchQuery = (q || "").toLowerCase(); }
|
|
63
84
|
setSearchMode(m) { this.searchMode = m; }
|
|
@@ -86,6 +107,11 @@ class NodeFilterEngine {
|
|
|
86
107
|
if (!this.shapes.has(getNodeShape(node))) return false;
|
|
87
108
|
}
|
|
88
109
|
|
|
110
|
+
if (this.projects.size > 0) {
|
|
111
|
+
const p = node.projectName || "(default)";
|
|
112
|
+
if (!this.projects.has(p)) return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
89
115
|
if (this.searchQuery) {
|
|
90
116
|
if (this.searchMode === "text") {
|
|
91
117
|
const q = this.searchQuery;
|
|
@@ -305,6 +331,9 @@ class SceneController {
|
|
|
305
331
|
if (customType && CUSTOM_TYPE_COLORS[customType]) {
|
|
306
332
|
color = CUSTOM_TYPE_COLORS[customType];
|
|
307
333
|
shape = CUSTOM_TYPE_SHAPES[customType] ?? "sphere";
|
|
334
|
+
} else if (node.type && TYPE_COLORS[node.type]) {
|
|
335
|
+
color = TYPE_COLORS[node.type];
|
|
336
|
+
shape = TYPE_SHAPES[node.type] ?? "sphere";
|
|
308
337
|
} else {
|
|
309
338
|
color = LEVEL_COLORS[node.level] ?? 0x888888;
|
|
310
339
|
shape = TYPE_SHAPES[node.type] ?? "sphere";
|
|
@@ -670,9 +699,23 @@ function setupEventListeners() {
|
|
|
670
699
|
} else if (btn.dataset.shape !== undefined) {
|
|
671
700
|
filterEngine.toggleShape(btn.dataset.shape);
|
|
672
701
|
btn.classList.toggle("active");
|
|
702
|
+
} else if (btn.dataset.project !== undefined) {
|
|
703
|
+
filterEngine.toggleProject(btn.dataset.project);
|
|
704
|
+
btn.classList.toggle("active");
|
|
673
705
|
}
|
|
674
706
|
});
|
|
675
707
|
|
|
708
|
+
// Clear all filters button
|
|
709
|
+
document.getElementById("clear-filters").addEventListener("click", () => {
|
|
710
|
+
filterEngine.initFromStats(statsData);
|
|
711
|
+
filterEngine.setSearchQuery("");
|
|
712
|
+
filterEngine.setServerSearchIds(null);
|
|
713
|
+
document.getElementById("search-input").value = "";
|
|
714
|
+
document.getElementById("search-info").textContent = "";
|
|
715
|
+
buildFilters();
|
|
716
|
+
sceneCtrl.updateVisibility(filterEngine);
|
|
717
|
+
});
|
|
718
|
+
|
|
676
719
|
// Search mode toggles
|
|
677
720
|
document.querySelectorAll(".search-mode-btn").forEach(btn => {
|
|
678
721
|
btn.addEventListener("click", () => {
|
|
@@ -727,11 +770,12 @@ function setupEventListeners() {
|
|
|
727
770
|
|
|
728
771
|
async function loadData() {
|
|
729
772
|
try {
|
|
730
|
-
const [scopesRes, nodesRes, linksRes, statsRes] = await Promise.all([
|
|
773
|
+
const [scopesRes, nodesRes, linksRes, statsRes, versionRes] = await Promise.all([
|
|
731
774
|
fetch("/api/scopes"),
|
|
732
775
|
fetch(`/api/nodes?scope=${currentScope}`),
|
|
733
776
|
fetch(`/api/links?scope=${currentScope}`),
|
|
734
777
|
fetch(`/api/stats?scope=${currentScope}`),
|
|
778
|
+
fetch("/api/version"),
|
|
735
779
|
]);
|
|
736
780
|
|
|
737
781
|
if (!nodesRes.ok || !linksRes.ok || !statsRes.ok) {
|
|
@@ -742,6 +786,8 @@ async function loadData() {
|
|
|
742
786
|
nodeData = await nodesRes.json();
|
|
743
787
|
linkData = await linksRes.json();
|
|
744
788
|
statsData = await statsRes.json();
|
|
789
|
+
const versionData = await versionRes.json();
|
|
790
|
+
document.getElementById("version").textContent = `v${versionData.version}`;
|
|
745
791
|
|
|
746
792
|
document.getElementById("loading").style.display = "none";
|
|
747
793
|
|
|
@@ -920,6 +966,16 @@ function buildFilters() {
|
|
|
920
966
|
`<button class="filter-btn active" data-shape="${s}">${shapeLabels[s] || s} (${statsData.nodesPerShape[s]})</button>`
|
|
921
967
|
).join("");
|
|
922
968
|
}
|
|
969
|
+
|
|
970
|
+
// Project filters
|
|
971
|
+
const projects = Object.keys(statsData.nodesPerProject || {}).sort();
|
|
972
|
+
const projectContainer = document.getElementById("project-filters");
|
|
973
|
+
if (projectContainer && projects.length > 1) {
|
|
974
|
+
projectContainer.closest(".section").style.display = "block";
|
|
975
|
+
projectContainer.innerHTML = projects.map(p =>
|
|
976
|
+
`<button class="filter-btn active" data-project="${p}">${p} (${statsData.nodesPerProject[p]})</button>`
|
|
977
|
+
).join("");
|
|
978
|
+
}
|
|
923
979
|
}
|
|
924
980
|
|
|
925
981
|
function buildLegend() {
|
|
@@ -937,6 +993,7 @@ function buildLegend() {
|
|
|
937
993
|
html += `<div class="legend-item"><div class="legend-dot" style="background: #4a9eff; border-radius: 2px;"></div><span class="legend-label">Box = Event/Episode</span></div>`;
|
|
938
994
|
html += `<div class="legend-item"><div class="legend-dot" style="background: #4a9eff; clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);"></div><span class="legend-label">Diamond = Concept/Summary</span></div>`;
|
|
939
995
|
html += `<div class="legend-item"><div class="legend-dot" style="background: #4a9eff; clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);"></div><span class="legend-label">Pentagon = Skill</span></div>`;
|
|
996
|
+
html += `<div class="legend-item"><div class="legend-dot" style="background: #ff8c00; border-radius: 50%;"></div><span class="legend-label">Torus = Playbook</span></div>`;
|
|
940
997
|
html += `<div class="legend-item"><div class="legend-dot" style="background: #ff6b6b; border-radius: 50%;"></div><span class="legend-label">Torus = Middle-Term</span></div>`;
|
|
941
998
|
html += `</div>`;
|
|
942
999
|
legend.innerHTML = html;
|
|
@@ -971,6 +1028,9 @@ function buildNodeList() {
|
|
|
971
1028
|
if (node.type === 'skill') {
|
|
972
1029
|
customIndicator += ' <span style="color: #fbbf24; font-size: 10px;">[SKILL]</span>';
|
|
973
1030
|
}
|
|
1031
|
+
if (node.type === 'playbook') {
|
|
1032
|
+
customIndicator += ' <span style="color: #ff8c00; font-size: 10px;">[PLAYBOOK]</span>';
|
|
1033
|
+
}
|
|
974
1034
|
|
|
975
1035
|
return `
|
|
976
1036
|
<div class="node-list-item ${isSelected ? 'selected' : ''}" data-node-id="${node.id}">
|
|
@@ -1023,6 +1083,28 @@ function showDetailPanel(node) {
|
|
|
1023
1083
|
`;
|
|
1024
1084
|
}
|
|
1025
1085
|
|
|
1086
|
+
let playbookHtml = "";
|
|
1087
|
+
if (node.type === 'playbook') {
|
|
1088
|
+
const steps = node.metadata?.steps;
|
|
1089
|
+
let stepsHtml = "";
|
|
1090
|
+
if (steps && Array.isArray(steps)) {
|
|
1091
|
+
stepsHtml = steps.map((s, i) => `
|
|
1092
|
+
<div style="padding: 6px 8px; margin-bottom: 4px; background: rgba(255,140,0,0.1); border-radius: 4px; border-left: 3px solid #ff8c00;">
|
|
1093
|
+
<div style="font-size: 12px; color: #ff8c00; font-weight: 600;">Step ${i + 1}: ${escapeHtml(s.description || s.toolName || "Unnamed")}</div>
|
|
1094
|
+
<div style="font-size: 11px; color: #aaa; margin-top: 2px;">Tool: ${escapeHtml(s.toolName || "none")}${s.critical ? ' <span style="color: #f44;">[CRITICAL]</span>' : ''}</div>
|
|
1095
|
+
</div>
|
|
1096
|
+
`).join("");
|
|
1097
|
+
} else {
|
|
1098
|
+
stepsHtml = '<div style="font-size: 12px; color: #888;">No steps defined in metadata</div>';
|
|
1099
|
+
}
|
|
1100
|
+
playbookHtml = `
|
|
1101
|
+
<div class="detail-section">
|
|
1102
|
+
<h4>Playbook Steps</h4>
|
|
1103
|
+
${stepsHtml}
|
|
1104
|
+
</div>
|
|
1105
|
+
`;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1026
1108
|
content.innerHTML = `
|
|
1027
1109
|
<div class="detail-section">
|
|
1028
1110
|
<h4>ID</h4>
|
|
@@ -1043,6 +1125,7 @@ function showDetailPanel(node) {
|
|
|
1043
1125
|
</div>
|
|
1044
1126
|
${metadataHtml}
|
|
1045
1127
|
${skillHtml}
|
|
1128
|
+
${playbookHtml}
|
|
1046
1129
|
<div class="detail-section">
|
|
1047
1130
|
<h4>Timestamps</h4>
|
|
1048
1131
|
<div class="detail-value">Created: ${created}</div>
|
|
@@ -254,6 +254,15 @@
|
|
|
254
254
|
<div class="filter-group" id="shape-filters"></div>
|
|
255
255
|
</div>
|
|
256
256
|
|
|
257
|
+
<div class="section" style="display:none;">
|
|
258
|
+
<h3>Filter by Project</h3>
|
|
259
|
+
<div class="filter-group" id="project-filters"></div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div class="section">
|
|
263
|
+
<button class="search-btn" id="clear-filters" style="border-color: rgba(255,100,100,0.4); background: rgba(255,100,100,0.15);">Clear All Filters</button>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
257
266
|
<div class="section">
|
|
258
267
|
<h3>Legend</h3>
|
|
259
268
|
<div id="legend"></div>
|