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.
- package/LICENSE +21 -0
- package/README.md +493 -0
- package/agent/memory-hints.md +98 -0
- package/agent/memory-researcher.md +56 -0
- package/commands/memory-auto-test.md +10 -0
- package/commands/memory-cache-status.md +13 -0
- package/commands/memory-check-context.md +4 -0
- package/commands/memory-compress.md +13 -0
- package/commands/memory-dashboard.md +23 -0
- package/commands/memory-delete.md +24 -0
- package/commands/memory-detect-topics.md +28 -0
- package/commands/memory-distill.md +35 -0
- package/commands/memory-drilldown-query.md +28 -0
- package/commands/memory-drilldown.md +11 -0
- package/commands/memory-extract-patterns.md +4 -0
- package/commands/memory-generate-embeddings.md +26 -0
- package/commands/memory-get.md +26 -0
- package/commands/memory-help.md +55 -0
- package/commands/memory-injection-feedback.md +26 -0
- package/commands/memory-injection-stats.md +11 -0
- package/commands/memory-list.md +4 -0
- package/commands/memory-llm-compress.md +34 -0
- package/commands/memory-mcp.md +20 -0
- package/commands/memory-prune.md +4 -0
- package/commands/memory-rate.md +48 -0
- package/commands/memory-reflect.md +37 -0
- package/commands/memory-replace.md +26 -0
- package/commands/memory-retrieve.md +34 -0
- package/commands/memory-search.md +28 -0
- package/commands/memory-session-stats.md +4 -0
- package/commands/memory-set.md +31 -0
- package/commands/memory-stats.md +11 -0
- package/commands/memory-summarize.md +29 -0
- package/commands/memory-tool-stats.md +4 -0
- package/commands/memory-total-tokens.md +10 -0
- package/commands/memory-verify.md +4 -0
- package/commands/memory-version.md +9 -0
- package/dist/cache.js +39 -0
- package/dist/config.js +120 -0
- package/dist/embeddings.js +125 -0
- package/dist/ensure-models.js +70 -0
- package/dist/file-summary.js +143 -0
- package/dist/frontmatter.js +28 -0
- package/dist/hnsw-index.js +138 -0
- package/dist/hooks/auto-discover.js +4 -0
- package/dist/hooks/auto-distill.js +120 -0
- package/dist/hooks/auto-retrieve/content.js +47 -0
- package/dist/hooks/auto-retrieve/detection.js +50 -0
- package/dist/hooks/auto-retrieve/formatting.js +19 -0
- package/dist/hooks/auto-retrieve/index.js +163 -0
- package/dist/hooks/auto-retrieve/scoring.js +56 -0
- package/dist/hooks/auto-retrieve.js +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/predictive-rating.js +87 -0
- package/dist/journal.js +279 -0
- package/dist/logging.js +147 -0
- package/dist/management/helpers.js +227 -0
- package/dist/management/router.js +48 -0
- package/dist/management/routes.js +197 -0
- package/dist/management-server.js +4 -0
- package/dist/management-standalone.js +31 -0
- package/dist/mcp/logging.js +57 -0
- package/dist/mcp/server.js +251 -0
- package/dist/mcp/transform.js +48 -0
- package/dist/mcp-server.js +18 -0
- package/dist/memory.js +2 -0
- package/dist/ollama.js +74 -0
- package/dist/plugin/hooks.js +168 -0
- package/dist/plugin/index.js +28 -0
- package/dist/plugin/init.js +109 -0
- package/dist/plugin/state.js +75 -0
- package/dist/plugin/tools.js +45 -0
- package/dist/plugin.js +2 -0
- package/dist/procedural/store.js +1 -0
- package/dist/procedural/types.js +1 -0
- package/dist/seed-nodes.js +804 -0
- package/dist/storage/compress-ops.js +129 -0
- package/dist/storage/compression/formatters.js +243 -0
- package/dist/storage/compression/index.js +107 -0
- package/dist/storage/compression/patterns.js +138 -0
- package/dist/storage/expiration.js +66 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/injection-events.js +82 -0
- package/dist/storage/lifecycle.js +65 -0
- package/dist/storage/maintenance.js +60 -0
- package/dist/storage/migrations/definitions.js +374 -0
- package/dist/storage/migrations/index.js +21 -0
- package/dist/storage/navigation.js +98 -0
- package/dist/storage/queries/base.js +44 -0
- package/dist/storage/queries/links.js +32 -0
- package/dist/storage/queries/nodes.js +189 -0
- package/dist/storage/queries/search-helpers.js +239 -0
- package/dist/storage/scoring.js +36 -0
- package/dist/storage/search.js +233 -0
- package/dist/storage/session-tracking.js +180 -0
- package/dist/storage/sqlite.js +329 -0
- package/dist/storage/tool-usage.js +56 -0
- package/dist/storage/types.js +1 -0
- package/dist/storage/utils.js +94 -0
- package/dist/tools/auto-test.js +24 -0
- package/dist/tools/cache-status.js +36 -0
- package/dist/tools/compress.js +186 -0
- package/dist/tools/core.js +307 -0
- package/dist/tools/dashboard.js +97 -0
- package/dist/tools/help.js +59 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/inject.js +91 -0
- package/dist/tools/injection-debug.js +48 -0
- package/dist/tools/journal.js +105 -0
- package/dist/tools/llm-compress.js +41 -0
- package/dist/tools/middle-term.js +68 -0
- package/dist/tools/playbook.js +64 -0
- package/dist/tools/reflect.js +291 -0
- package/dist/tools/search.js +188 -0
- package/dist/tools/session.js +189 -0
- package/dist/tools/shared.js +74 -0
- package/dist/tools/skill.js +37 -0
- package/dist/tools/stats.js +256 -0
- package/dist/tools/version.js +13 -0
- package/dist/tools.js +18 -0
- package/dist/utils/hybridScore.js +67 -0
- package/management/public/app.js +1529 -0
- package/management/public/index.html +486 -0
- package/management/public/three.min.js +6 -0
- package/package.json +65 -0
- package/scripts/download-models.ts +16 -0
- 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,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
|
+
}
|