opencode-fractal-memory 0.2.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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +493 -0
  3. package/agent/memory-hints.md +98 -0
  4. package/agent/memory-researcher.md +56 -0
  5. package/commands/memory-auto-test.md +10 -0
  6. package/commands/memory-cache-status.md +13 -0
  7. package/commands/memory-check-context.md +4 -0
  8. package/commands/memory-compress.md +13 -0
  9. package/commands/memory-dashboard.md +23 -0
  10. package/commands/memory-delete.md +24 -0
  11. package/commands/memory-detect-topics.md +28 -0
  12. package/commands/memory-distill.md +35 -0
  13. package/commands/memory-drilldown-query.md +28 -0
  14. package/commands/memory-drilldown.md +11 -0
  15. package/commands/memory-extract-patterns.md +4 -0
  16. package/commands/memory-generate-embeddings.md +26 -0
  17. package/commands/memory-get.md +26 -0
  18. package/commands/memory-help.md +55 -0
  19. package/commands/memory-injection-feedback.md +26 -0
  20. package/commands/memory-injection-stats.md +11 -0
  21. package/commands/memory-list.md +4 -0
  22. package/commands/memory-llm-compress.md +34 -0
  23. package/commands/memory-mcp.md +20 -0
  24. package/commands/memory-prune.md +4 -0
  25. package/commands/memory-rate.md +48 -0
  26. package/commands/memory-reflect.md +37 -0
  27. package/commands/memory-replace.md +26 -0
  28. package/commands/memory-retrieve.md +34 -0
  29. package/commands/memory-search.md +28 -0
  30. package/commands/memory-session-stats.md +4 -0
  31. package/commands/memory-set.md +31 -0
  32. package/commands/memory-stats.md +11 -0
  33. package/commands/memory-summarize.md +29 -0
  34. package/commands/memory-tool-stats.md +4 -0
  35. package/commands/memory-total-tokens.md +10 -0
  36. package/commands/memory-verify.md +4 -0
  37. package/commands/memory-version.md +9 -0
  38. package/dist/cache.js +39 -0
  39. package/dist/config.js +120 -0
  40. package/dist/embeddings.js +125 -0
  41. package/dist/ensure-models.js +70 -0
  42. package/dist/file-summary.js +143 -0
  43. package/dist/frontmatter.js +28 -0
  44. package/dist/hnsw-index.js +138 -0
  45. package/dist/hooks/auto-discover.js +4 -0
  46. package/dist/hooks/auto-distill.js +120 -0
  47. package/dist/hooks/auto-retrieve/content.js +47 -0
  48. package/dist/hooks/auto-retrieve/detection.js +50 -0
  49. package/dist/hooks/auto-retrieve/formatting.js +19 -0
  50. package/dist/hooks/auto-retrieve/index.js +163 -0
  51. package/dist/hooks/auto-retrieve/scoring.js +56 -0
  52. package/dist/hooks/auto-retrieve.js +1 -0
  53. package/dist/hooks/index.js +4 -0
  54. package/dist/hooks/predictive-rating.js +87 -0
  55. package/dist/journal.js +279 -0
  56. package/dist/logging.js +147 -0
  57. package/dist/management/helpers.js +227 -0
  58. package/dist/management/router.js +48 -0
  59. package/dist/management/routes.js +197 -0
  60. package/dist/management-server.js +4 -0
  61. package/dist/management-standalone.js +31 -0
  62. package/dist/mcp/logging.js +57 -0
  63. package/dist/mcp/server.js +251 -0
  64. package/dist/mcp/transform.js +48 -0
  65. package/dist/mcp-server.js +18 -0
  66. package/dist/memory.js +2 -0
  67. package/dist/ollama.js +74 -0
  68. package/dist/plugin/hooks.js +168 -0
  69. package/dist/plugin/index.js +28 -0
  70. package/dist/plugin/init.js +109 -0
  71. package/dist/plugin/state.js +75 -0
  72. package/dist/plugin/tools.js +45 -0
  73. package/dist/plugin.js +2 -0
  74. package/dist/procedural/store.js +1 -0
  75. package/dist/procedural/types.js +1 -0
  76. package/dist/seed-nodes.js +804 -0
  77. package/dist/storage/compress-ops.js +129 -0
  78. package/dist/storage/compression/formatters.js +243 -0
  79. package/dist/storage/compression/index.js +107 -0
  80. package/dist/storage/compression/patterns.js +138 -0
  81. package/dist/storage/expiration.js +66 -0
  82. package/dist/storage/index.js +1 -0
  83. package/dist/storage/injection-events.js +82 -0
  84. package/dist/storage/lifecycle.js +65 -0
  85. package/dist/storage/maintenance.js +60 -0
  86. package/dist/storage/migrations/definitions.js +374 -0
  87. package/dist/storage/migrations/index.js +21 -0
  88. package/dist/storage/navigation.js +98 -0
  89. package/dist/storage/queries/base.js +44 -0
  90. package/dist/storage/queries/links.js +32 -0
  91. package/dist/storage/queries/nodes.js +189 -0
  92. package/dist/storage/queries/search-helpers.js +239 -0
  93. package/dist/storage/scoring.js +36 -0
  94. package/dist/storage/search.js +233 -0
  95. package/dist/storage/session-tracking.js +180 -0
  96. package/dist/storage/sqlite.js +329 -0
  97. package/dist/storage/tool-usage.js +56 -0
  98. package/dist/storage/types.js +1 -0
  99. package/dist/storage/utils.js +94 -0
  100. package/dist/tools/auto-test.js +24 -0
  101. package/dist/tools/cache-status.js +36 -0
  102. package/dist/tools/compress.js +186 -0
  103. package/dist/tools/core.js +307 -0
  104. package/dist/tools/dashboard.js +97 -0
  105. package/dist/tools/help.js +59 -0
  106. package/dist/tools/index.js +12 -0
  107. package/dist/tools/inject.js +91 -0
  108. package/dist/tools/injection-debug.js +48 -0
  109. package/dist/tools/journal.js +105 -0
  110. package/dist/tools/llm-compress.js +41 -0
  111. package/dist/tools/middle-term.js +68 -0
  112. package/dist/tools/playbook.js +64 -0
  113. package/dist/tools/reflect.js +291 -0
  114. package/dist/tools/search.js +188 -0
  115. package/dist/tools/session.js +189 -0
  116. package/dist/tools/shared.js +74 -0
  117. package/dist/tools/skill.js +37 -0
  118. package/dist/tools/stats.js +256 -0
  119. package/dist/tools/version.js +13 -0
  120. package/dist/tools.js +18 -0
  121. package/dist/utils/hybridScore.js +67 -0
  122. package/management/public/app.js +1529 -0
  123. package/management/public/index.html +486 -0
  124. package/management/public/three.min.js +6 -0
  125. package/package.json +65 -0
  126. package/scripts/download-models.ts +16 -0
  127. package/scripts/postinstall.cjs +30 -0
@@ -0,0 +1,227 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import { Database } from "bun:sqlite";
5
+ import { memLog } from "../logging";
6
+ export const DB_PATHS = {};
7
+ export function initDbPaths(projectDir) {
8
+ const globalDbPath = path.join(os.homedir(), ".config", "opencode", "memory.db");
9
+ const projectDbPath = path.join(projectDir, ".opencode", "memory.db");
10
+ DB_PATHS.global = globalDbPath;
11
+ DB_PATHS.project = projectDbPath;
12
+ }
13
+ export function openDb(scope) {
14
+ const dbPath = DB_PATHS[scope] || scope;
15
+ if (!Bun.file(dbPath).exists())
16
+ return null;
17
+ const db = Database.open(dbPath);
18
+ db.run("PRAGMA journal_mode=WAL");
19
+ db.run("PRAGMA busy_timeout=5000");
20
+ return db;
21
+ }
22
+ export async function withDb(dbOrScope, fn) {
23
+ const db = typeof dbOrScope === "string" ? openDb(dbOrScope) : dbOrScope;
24
+ if (!db)
25
+ throw new Error("Database not found");
26
+ try {
27
+ return await fn(db);
28
+ }
29
+ finally {
30
+ if (typeof dbOrScope === "string")
31
+ db.close();
32
+ }
33
+ }
34
+ export const MIME_TYPES = {
35
+ ".html": "text/html",
36
+ ".js": "application/javascript",
37
+ ".css": "text/css",
38
+ ".json": "application/json",
39
+ ".png": "image/png",
40
+ ".svg": "image/svg+xml",
41
+ };
42
+ export function serveFile(filePath) {
43
+ try {
44
+ const ext = path.extname(filePath);
45
+ const mimeType = MIME_TYPES[ext] || "application/octet-stream";
46
+ const body = Bun.file(filePath);
47
+ return new Response(body, { headers: { "Content-Type": mimeType } });
48
+ }
49
+ catch {
50
+ return new Response("Not found", { status: 404 });
51
+ }
52
+ }
53
+ export function jsonResponse(data, status = 200) {
54
+ return new Response(JSON.stringify(data), {
55
+ status,
56
+ headers: { "Content-Type": "application/json" },
57
+ });
58
+ }
59
+ export function rowToNode(r) {
60
+ return {
61
+ id: r.id,
62
+ label: r.label || "",
63
+ content: r.content,
64
+ summary: r.summary,
65
+ level: r.level,
66
+ type: r.type,
67
+ importance: r.importance,
68
+ usefulnessScore: r.usefulness_score,
69
+ timesUsed: r.times_used,
70
+ timesHelpful: r.times_helpful,
71
+ accessCount: r.access_count,
72
+ sticky: !!r.sticky,
73
+ confidence: r.confidence,
74
+ createdAt: r.created_at,
75
+ updatedAt: r.updated_at,
76
+ parentIds: r.parent_ids ? JSON.parse(r.parent_ids) : null,
77
+ contentLength: r.content_length,
78
+ metadata: r.metadata ? JSON.parse(r.metadata) : null,
79
+ };
80
+ }
81
+ export function queryNodes(scope) {
82
+ const db = openDb(scope);
83
+ if (!db)
84
+ return [];
85
+ const rows = db.query(`
86
+ SELECT id, label, content, summary, level, type, importance,
87
+ usefulness_score, times_used, times_helpful, access_count,
88
+ sticky, confidence, created_at, updated_at, parent_ids,
89
+ LENGTH(content) as content_length, metadata
90
+ FROM memory_nodes
91
+ ORDER BY level, importance DESC
92
+ `).all();
93
+ db.close();
94
+ return rows.map((r) => rowToNode(r));
95
+ }
96
+ export function cosineSimilarity(a, b) {
97
+ let dot = 0, normA = 0, normB = 0;
98
+ for (let i = 0; i < a.length && i < b.length; i++) {
99
+ const ai = a[i] ?? 0;
100
+ const bi = b[i] ?? 0;
101
+ dot += ai * bi;
102
+ normA += ai * ai;
103
+ normB += bi * bi;
104
+ }
105
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-8);
106
+ }
107
+ export function extractLinks(nodes) {
108
+ const links = [];
109
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
110
+ const seen = new Set();
111
+ for (const node of nodes) {
112
+ if (node.parentIds) {
113
+ for (const parentId of node.parentIds) {
114
+ if (nodeMap.has(parentId)) {
115
+ const key = `${node.id}-${parentId}-parent`;
116
+ if (!seen.has(key)) {
117
+ seen.add(key);
118
+ links.push({ source: node.id, target: parentId, type: "parent" });
119
+ }
120
+ }
121
+ }
122
+ }
123
+ const wikiLinkRegex = /\[\[([^\]]+)\]\]/g;
124
+ let match;
125
+ while ((match = wikiLinkRegex.exec(node.content)) !== null) {
126
+ const targetLabel = match[1];
127
+ const target = nodes.find(n => n.label === targetLabel);
128
+ if (target && target.id !== node.id) {
129
+ const key = `${node.id}-${target.id}-link`;
130
+ if (!seen.has(key)) {
131
+ seen.add(key);
132
+ links.push({ source: node.id, target: target.id, type: "link" });
133
+ }
134
+ }
135
+ }
136
+ }
137
+ return links;
138
+ }
139
+ export function getAvailableScopes() {
140
+ return Object.entries(DB_PATHS)
141
+ .filter(([_, p]) => Bun.file(p).exists())
142
+ .map(([k, v]) => ({ scope: k, path: v }));
143
+ }
144
+ export const TYPE_SHAPES = {
145
+ note: "sphere",
146
+ event: "box",
147
+ episode: "box",
148
+ concept: "octahedron",
149
+ summary: "octahedron",
150
+ core: "dodecahedron",
151
+ improvement: "sphere",
152
+ howto: "sphere",
153
+ skill: "icosahedron",
154
+ unknown: "sphere",
155
+ };
156
+ export const CUSTOM_TYPE_SHAPES = {
157
+ "middle-term": "torus",
158
+ };
159
+ export function resolveNodeShape(node) {
160
+ const customType = node.metadata?.customType;
161
+ if (customType && CUSTOM_TYPE_SHAPES[customType]) {
162
+ return CUSTOM_TYPE_SHAPES[customType];
163
+ }
164
+ return TYPE_SHAPES[node.type] ?? "sphere";
165
+ }
166
+ export function computeStats(nodes) {
167
+ const nodesPerLevel = {};
168
+ const nodesPerType = {};
169
+ const nodesPerCustomType = {};
170
+ const nodesPerShape = {};
171
+ let totalImportance = 0;
172
+ let totalUsefulness = 0;
173
+ let totalAccessCount = 0;
174
+ let stickyCount = 0;
175
+ for (const node of nodes) {
176
+ nodesPerLevel[node.level] = (nodesPerLevel[node.level] ?? 0) + 1;
177
+ const typeKey = node.type || "unknown";
178
+ nodesPerType[typeKey] = (nodesPerType[typeKey] ?? 0) + 1;
179
+ const customType = node.metadata?.customType;
180
+ if (customType) {
181
+ nodesPerCustomType[customType] = (nodesPerCustomType[customType] ?? 0) + 1;
182
+ }
183
+ const shape = resolveNodeShape(node);
184
+ nodesPerShape[shape] = (nodesPerShape[shape] ?? 0) + 1;
185
+ totalImportance += node.importance;
186
+ totalUsefulness += node.usefulnessScore;
187
+ totalAccessCount += node.accessCount;
188
+ if (node.sticky)
189
+ stickyCount++;
190
+ }
191
+ const n = nodes.length || 1;
192
+ return {
193
+ totalNodes: nodes.length,
194
+ nodesPerLevel,
195
+ nodesPerType,
196
+ nodesPerCustomType,
197
+ nodesPerShape,
198
+ avgImportance: Math.round((totalImportance / n) * 100) / 100,
199
+ avgUsefulness: Math.round((totalUsefulness / n) * 100) / 100,
200
+ totalAccessCount,
201
+ stickyCount,
202
+ };
203
+ }
204
+ export function readProjectConfig() {
205
+ const configPath = path.join(os.homedir(), ".config", "opencode", "opencode-mem.json");
206
+ try {
207
+ if (fs.existsSync(configPath)) {
208
+ const raw = fs.readFileSync(configPath, "utf-8");
209
+ return JSON.parse(raw);
210
+ }
211
+ }
212
+ catch { }
213
+ return {};
214
+ }
215
+ export function writeProjectConfig(config) {
216
+ const configPath = path.join(os.homedir(), ".config", "opencode", "opencode-mem.json");
217
+ try {
218
+ const merged = { ...config };
219
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2));
220
+ memLog("info", "config", "[config] Saved to:", { configPath });
221
+ return "ok";
222
+ }
223
+ catch (e) {
224
+ memLog("error", "config", "[config] Write error:", { error: e instanceof Error ? e.message : e });
225
+ return String(e);
226
+ }
227
+ }
@@ -0,0 +1,48 @@
1
+ import { memLog } from "../logging";
2
+ import { jsonResponse } from "./helpers";
3
+ export class Router {
4
+ routes = [];
5
+ get(pattern, handler) {
6
+ this.routes.push({ method: "GET", pattern, handler });
7
+ }
8
+ post(pattern, handler) {
9
+ this.routes.push({ method: "POST", pattern, handler });
10
+ }
11
+ put(pattern, handler) {
12
+ this.routes.push({ method: "PUT", pattern, handler });
13
+ }
14
+ delete(pattern, handler) {
15
+ this.routes.push({ method: "DELETE", pattern, handler });
16
+ }
17
+ patch(pattern, handler) {
18
+ this.routes.push({ method: "PATCH", pattern, handler });
19
+ }
20
+ any(pattern, handler) {
21
+ this.routes.push({ method: null, pattern, handler });
22
+ }
23
+ async handle(req) {
24
+ const url = new URL(req.url);
25
+ const pathname = url.pathname;
26
+ for (const route of this.routes) {
27
+ if (route.method !== null && route.method !== req.method)
28
+ continue;
29
+ const match = pathname.match(route.pattern);
30
+ if (!match)
31
+ continue;
32
+ const ctx = {
33
+ params: match.groups ?? {},
34
+ scope: url.searchParams.get("scope") || "project",
35
+ url,
36
+ pathname,
37
+ };
38
+ try {
39
+ return await route.handler(req, ctx);
40
+ }
41
+ catch (err) {
42
+ memLog("error", "management", `[api] ${req.method} ${pathname}:`, { error: err instanceof Error ? err.message : err });
43
+ return jsonResponse({ error: err instanceof Error ? err.message : String(err) }, 500);
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ }
@@ -0,0 +1,197 @@
1
+ import { memLog } from "../logging";
2
+ import { generateEmbedding } from "../embeddings";
3
+ import { queryNodes, getAvailableScopes, extractLinks, computeStats, readProjectConfig, writeProjectConfig, rowToNode, withDb, jsonResponse, cosineSimilarity, } from "./helpers";
4
+ function handleScopes() {
5
+ return jsonResponse(getAvailableScopes());
6
+ }
7
+ function handleNodes(ctx) {
8
+ return jsonResponse(queryNodes(ctx.scope));
9
+ }
10
+ function handleLinks(ctx) {
11
+ const nodes = queryNodes(ctx.scope);
12
+ return jsonResponse(extractLinks(nodes));
13
+ }
14
+ function handleStats(ctx) {
15
+ const nodes = queryNodes(ctx.scope);
16
+ return jsonResponse(computeStats(nodes));
17
+ }
18
+ function handleConfigGet() {
19
+ return jsonResponse(readProjectConfig());
20
+ }
21
+ async function handleConfigSave(req) {
22
+ const body = await req.text();
23
+ memLog("debug", "api", "[api] Received body:", { body });
24
+ const newConfig = JSON.parse(body);
25
+ const error = writeProjectConfig(newConfig);
26
+ return jsonResponse({ success: error === "ok", error: error === "ok" ? null : error });
27
+ }
28
+ async function handleInject(req) {
29
+ const body = await req.json();
30
+ if (!body.nodeId)
31
+ return jsonResponse({ success: false, error: "Missing nodeId" }, 400);
32
+ const nodeId = body.nodeId;
33
+ const injectScope = body.scope || "global";
34
+ return withDb(injectScope, async (db) => {
35
+ const exists = db.query("SELECT id FROM memory_nodes WHERE id = ?").get(nodeId);
36
+ if (!exists)
37
+ return jsonResponse({ success: false, error: "Node not found" }, 404);
38
+ db.run("CREATE TABLE IF NOT EXISTS pending_injections (id INTEGER PRIMARY KEY AUTOINCREMENT, node_id TEXT NOT NULL, scope TEXT NOT NULL DEFAULT 'global', source TEXT DEFAULT 'management', created_at TEXT NOT NULL DEFAULT (datetime('now')), processed INTEGER NOT NULL DEFAULT 0)");
39
+ db.run("INSERT INTO pending_injections (node_id, scope, source) VALUES (?, ?, 'management')", [nodeId, injectScope]);
40
+ memLog("info", "management", `[api] Queued node ${nodeId} for injection`);
41
+ return jsonResponse({ success: true });
42
+ });
43
+ }
44
+ async function handleSearch(ctx) {
45
+ const q = ctx.url.searchParams.get("q") || "";
46
+ const mode = ctx.url.searchParams.get("mode") || "text";
47
+ if (!q.trim())
48
+ return jsonResponse([]);
49
+ return withDb(ctx.scope, async (db) => {
50
+ if (mode === "text") {
51
+ const rows = db.query(`
52
+ SELECT id, label, content, summary, level, type, importance,
53
+ usefulness_score, times_used, times_helpful, access_count,
54
+ sticky, confidence, created_at, updated_at, parent_ids,
55
+ LENGTH(content) as content_length, metadata
56
+ FROM memory_nodes
57
+ WHERE label LIKE ? OR content LIKE ?
58
+ ORDER BY importance DESC
59
+ LIMIT 100
60
+ `).all(`%${q}%`, `%${q}%`);
61
+ return jsonResponse(rows.map(r => ({ ...rowToNode(r), score: r.importance })));
62
+ }
63
+ if (mode === "embedding") {
64
+ const queryEmbedding = await generateEmbedding(q);
65
+ const rows = db.query(`
66
+ SELECT id, label, content, summary, level, type, importance,
67
+ usefulness_score, times_used, times_helpful, access_count,
68
+ sticky, confidence, created_at, updated_at, parent_ids,
69
+ LENGTH(content) as content_length, metadata, embedding_blob
70
+ FROM memory_nodes
71
+ WHERE embedding_blob IS NOT NULL
72
+ `).all();
73
+ const results = rows.map(r => {
74
+ const floats = new Float32Array(r.embedding_blob.buffer, r.embedding_blob.byteOffset, r.embedding_blob.byteLength / 4);
75
+ const embedding = Array.from(floats);
76
+ return { ...rowToNode(r), score: cosineSimilarity(queryEmbedding, embedding) };
77
+ });
78
+ results.sort((a, b) => b.score - a.score);
79
+ return jsonResponse(results.slice(0, 50));
80
+ }
81
+ if (mode === "bm25") {
82
+ const terms = q.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter(t => t.length >= 2);
83
+ if (terms.length === 0)
84
+ return jsonResponse([]);
85
+ const placeholders = terms.map(() => "?").join(",");
86
+ const rows = db.query(`
87
+ SELECT n.id, n.label, n.content, n.summary, n.level, n.type, n.importance,
88
+ n.usefulness_score, n.times_used, n.times_helpful, n.access_count,
89
+ n.sticky, n.confidence, n.created_at, n.updated_at, n.parent_ids,
90
+ LENGTH(n.content) as content_length, n.metadata,
91
+ COALESCE(SUM(b.frequency), 0) as bm25_score
92
+ FROM memory_nodes n
93
+ INNER JOIN bm25_index b ON n.id = b.node_id
94
+ WHERE b.term IN (${placeholders})
95
+ GROUP BY n.id
96
+ ORDER BY bm25_score DESC
97
+ LIMIT 100
98
+ `).all(...terms);
99
+ return jsonResponse(rows.map(r => ({ ...rowToNode(r), score: r.bm25_score })));
100
+ }
101
+ return jsonResponse([]);
102
+ });
103
+ }
104
+ async function handleNodeUpdate(req, ctx) {
105
+ const nodeId = ctx.params.id;
106
+ const body = await req.json();
107
+ return withDb(ctx.scope, async (db) => {
108
+ const existing = db.query("SELECT content, label FROM memory_nodes WHERE id = ?").get(nodeId);
109
+ if (!existing)
110
+ return jsonResponse({ success: false, error: "Node not found" }, 404);
111
+ const newContent = body.content ?? existing.content;
112
+ let embeddingBuffer = null;
113
+ if (body.content !== undefined && body.content !== existing.content) {
114
+ memLog("info", "management", `[api] Regenerating embedding for node ${nodeId}`);
115
+ const embedding = await generateEmbedding(newContent);
116
+ embeddingBuffer = Buffer.from(new Float32Array(embedding).buffer);
117
+ }
118
+ const fields = [];
119
+ const values = [];
120
+ if (body.label !== undefined) {
121
+ fields.push("label = ?");
122
+ values.push(String(body.label));
123
+ }
124
+ if (body.content !== undefined) {
125
+ fields.push("content = ?");
126
+ values.push(newContent);
127
+ }
128
+ if (body.summary !== undefined) {
129
+ fields.push("summary = ?");
130
+ values.push(String(body.summary));
131
+ }
132
+ if (body.level !== undefined) {
133
+ fields.push("level = ?");
134
+ values.push(Number(body.level));
135
+ }
136
+ if (body.importance !== undefined) {
137
+ fields.push("importance = ?");
138
+ values.push(Number(body.importance));
139
+ }
140
+ if (body.type !== undefined) {
141
+ fields.push("type = ?");
142
+ values.push(String(body.type));
143
+ }
144
+ if (body.sticky !== undefined) {
145
+ fields.push("sticky = ?");
146
+ values.push(body.sticky ? 1 : 0);
147
+ }
148
+ if (body.confidence !== undefined) {
149
+ fields.push("confidence = ?");
150
+ values.push(Number(body.confidence));
151
+ }
152
+ if (body.usefulnessScore !== undefined) {
153
+ fields.push("usefulness_score = ?");
154
+ values.push(Number(body.usefulnessScore));
155
+ }
156
+ if (body.metadata !== undefined) {
157
+ fields.push("metadata = ?");
158
+ values.push(JSON.stringify(body.metadata));
159
+ }
160
+ if (embeddingBuffer) {
161
+ fields.push("embedding_blob = ?");
162
+ values.push(embeddingBuffer);
163
+ }
164
+ fields.push("updated_at = ?");
165
+ values.push(Date.now());
166
+ values.push(nodeId);
167
+ const sql = `UPDATE memory_nodes SET ${fields.join(", ")} WHERE id = ?`;
168
+ db.run(sql, ...values);
169
+ memLog("info", "management", `[api] Updated node ${nodeId}`);
170
+ return jsonResponse({ success: true });
171
+ });
172
+ }
173
+ async function handleNodeDelete(ctx) {
174
+ const nodeId = ctx.params.id;
175
+ return withDb(ctx.scope, async (db) => {
176
+ const existing = db.query("SELECT id FROM memory_nodes WHERE id = ?").get(nodeId);
177
+ if (!existing)
178
+ return jsonResponse({ success: false, error: "Node not found" }, 404);
179
+ db.run("DELETE FROM memory_nodes WHERE id = ?", [nodeId]);
180
+ memLog("info", "management", `[api] Deleted node ${nodeId}`);
181
+ return jsonResponse({ success: true });
182
+ });
183
+ }
184
+ export function registerRoutes(router) {
185
+ router.get(/^\/api\/scopes$/, () => handleScopes());
186
+ router.get(/^\/api\/nodes$/, (_, ctx) => handleNodes(ctx));
187
+ router.get(/^\/api\/links$/, (_, ctx) => handleLinks(ctx));
188
+ router.get(/^\/api\/stats$/, (_, ctx) => handleStats(ctx));
189
+ router.get(/^\/api\/config$/, () => handleConfigGet());
190
+ router.put(/^\/api\/config$/, (req) => handleConfigSave(req));
191
+ router.post(/^\/api\/config$/, (req) => handleConfigSave(req));
192
+ router.post(/^\/api\/inject$/, (req) => handleInject(req));
193
+ router.get(/^\/api\/search$/, (req, ctx) => handleSearch(ctx));
194
+ router.put(/^\/api\/nodes\/(?<id>[^/]+)$/, (req, ctx) => handleNodeUpdate(req, ctx));
195
+ router.patch(/^\/api\/nodes\/(?<id>[^/]+)$/, (req, ctx) => handleNodeUpdate(req, ctx));
196
+ router.delete(/^\/api\/nodes\/(?<id>[^/]+)$/, (_, ctx) => handleNodeDelete(ctx));
197
+ }
@@ -0,0 +1,4 @@
1
+ export function startManagementServer(_store, _directory, _config) {
2
+ // No-op: management server is now a standalone CLI
3
+ // Run: bun run ~/.config/opencode/node_modules/opencode-fractal-memory/dist/management-standalone.js
4
+ }
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bun
2
+ import * as path from "node:path";
3
+ import { memLog } from "./logging";
4
+ import { Router } from "./management/router";
5
+ import { registerRoutes } from "./management/routes";
6
+ import { initDbPaths, serveFile } from "./management/helpers";
7
+ const port = parseInt(process.env.MGMT_PORT || "8787");
8
+ const projectDir = process.env.MGMT_PROJECT_DIR || process.cwd();
9
+ const publicDir = path.join(__dirname, "..", "management", "public");
10
+ initDbPaths(projectDir);
11
+ const router = new Router();
12
+ registerRoutes(router);
13
+ Bun.serve({
14
+ port,
15
+ async fetch(req) {
16
+ const result = await router.handle(req);
17
+ if (result)
18
+ return result;
19
+ const url = new URL(req.url);
20
+ const pathname = url.pathname;
21
+ if (pathname === "/") {
22
+ return serveFile(path.join(publicDir, "index.html"));
23
+ }
24
+ const filePath = path.join(publicDir, pathname);
25
+ if (filePath.startsWith(publicDir)) {
26
+ return serveFile(filePath);
27
+ }
28
+ return new Response("Not found", { status: 404 });
29
+ },
30
+ });
31
+ memLog("info", "management", `Memory viewer running at http://localhost:${port}`);
@@ -0,0 +1,57 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ const MCP_LOG_FILE = path.join(os.homedir(), ".config", "opencode", "fractal-memory-server.log");
5
+ const MAX_LOG_SIZE = 1024 * 1024;
6
+ export function mcpLog(level, msg, data) {
7
+ try {
8
+ const ts = new Date().toISOString().slice(0, 19).replace("T", " ");
9
+ const line = `[${ts}] [${level.padEnd(5)}] [mcp] ${msg}` +
10
+ (data && Object.keys(data).length > 0 ? ` ${JSON.stringify(data)}` : "");
11
+ try {
12
+ const stat = fs.statSync(MCP_LOG_FILE);
13
+ if (stat.size > MAX_LOG_SIZE) {
14
+ fs.renameSync(MCP_LOG_FILE, MCP_LOG_FILE + ".old");
15
+ }
16
+ }
17
+ catch { }
18
+ fs.appendFileSync(MCP_LOG_FILE, line + "\n");
19
+ }
20
+ catch { }
21
+ }
22
+ export function sanitizeArgs(args) {
23
+ const sanitized = {};
24
+ for (const [k, v] of Object.entries(args)) {
25
+ if (k === "content" && typeof v === "string" && v.length > 100) {
26
+ sanitized[k] = v.slice(0, 100) + `... [${v.length} chars]`;
27
+ }
28
+ else if (k === "query" && typeof v === "string" && v.length > 50) {
29
+ sanitized[k] = v.slice(0, 50) + `... [${v.length} chars]`;
30
+ }
31
+ else {
32
+ sanitized[k] = v;
33
+ }
34
+ }
35
+ return sanitized;
36
+ }
37
+ export function withMcpLogging(toolName, handler) {
38
+ return async (args) => {
39
+ const start = Date.now();
40
+ mcpLog("info", `call`, { tool: toolName, args: sanitizeArgs(args) });
41
+ try {
42
+ const result = await handler(args);
43
+ const duration = Date.now() - start;
44
+ const resultSize = JSON.stringify(result).length;
45
+ mcpLog("info", `ok`, { tool: toolName, durationMs: duration, resultChars: resultSize });
46
+ return result;
47
+ }
48
+ catch (e) {
49
+ const duration = Date.now() - start;
50
+ mcpLog("error", `fail`, { tool: toolName, durationMs: duration, error: e instanceof Error ? e.message : String(e) });
51
+ return {
52
+ content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
53
+ isError: true,
54
+ };
55
+ }
56
+ };
57
+ }