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 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 (note, skill, playbook, rule)
381
-
382
- **Search** — find nodes by content, label, or type:
383
- - Type a query and press Enter
384
- - Results show relevance scores and preview snippets
385
- - Click a result to navigate to it in the graph
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));
@@ -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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-fractal-memory",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
4
4
  "description": "Fractal memory system for OpenCode with semantic search and automatic compression.",
5
5
  "main": "dist/plugin.js",
6
6
  "exports": {