@lotargo/memory_plugin 1.1.5 → 1.1.7
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 +133 -120
- package/mcp-server/admin/server.js +228 -228
- package/mcp-server/admin/snapshot.js +303 -303
- package/mcp-server/benchmarks/fetch_real_corpus.js +351 -351
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +170 -0
- package/mcp-server/benchmarks/stress_ingestion.js +195 -193
- package/mcp-server/benchmarks/test_dual_layer.js +140 -140
- package/mcp-server/cli.js +293 -7
- package/mcp-server/config/config_manager.js +11 -7
- package/mcp-server/db/database.js +43 -43
- package/mcp-server/graph/graph_extractor.js +72 -72
- package/mcp-server/graph/knowledge_linker.js +102 -102
- package/mcp-server/index.js +454 -454
- package/mcp-server/ingest/chunker.js +337 -337
- package/mcp-server/ingest/exporter.js +80 -80
- package/mcp-server/ingest/normalizer.js +104 -104
- package/mcp-server/ingest/pipeline.js +22 -7
- package/mcp-server/ingest/sentence_segmenter.js +74 -74
- package/mcp-server/memory.js +72 -72
- package/mcp-server/ml/gpu_monitor.js +166 -0
- package/mcp-server/ml/model_manager.js +321 -17
- package/mcp-server/preinstall.js +44 -44
- package/mcp-server/retrieval/retriever.js +10 -5
- package/mcp-server/setup.js +148 -148
- package/mcp-server/storage/blob_store.js +62 -62
- package/opencode-plugin/index.js +244 -244
- package/package.json +58 -54
- package/skills/using-memory/SKILL.md +122 -122
|
@@ -1,228 +1,228 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
|
-
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
3
|
-
import { join, dirname } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { getDatabase, DB_PATH, BLOBS_DIR } from "../db/database.js";
|
|
6
|
-
import { ingestDocument, deleteDocument } from "../ingest/pipeline.js";
|
|
7
|
-
import { hybridQuery } from "../retrieval/retriever.js";
|
|
8
|
-
import { exportSnapshot, importSnapshot } from "./snapshot.js";
|
|
9
|
-
import { readBlob } from "../storage/blob_store.js";
|
|
10
|
-
|
|
11
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
-
const __dirname = dirname(__filename);
|
|
13
|
-
|
|
14
|
-
export function findAvailablePort(startPort = 8765, maxPort = 8785) {
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
let port = startPort;
|
|
17
|
-
const tryPort = () => {
|
|
18
|
-
if (port > maxPort) {
|
|
19
|
-
return reject(new Error(`No free port found between ${startPort} and ${maxPort}`));
|
|
20
|
-
}
|
|
21
|
-
const server = createServer();
|
|
22
|
-
server.listen(port, () => {
|
|
23
|
-
server.close(() => resolve(port));
|
|
24
|
-
});
|
|
25
|
-
server.on("error", () => {
|
|
26
|
-
port++;
|
|
27
|
-
tryPort();
|
|
28
|
-
});
|
|
29
|
-
};
|
|
30
|
-
tryPort();
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function parseJsonBody(req) {
|
|
35
|
-
return new Promise((resolve, reject) => {
|
|
36
|
-
let body = "";
|
|
37
|
-
req.on("data", (chunk) => {
|
|
38
|
-
body += chunk.toString();
|
|
39
|
-
});
|
|
40
|
-
req.on("end", () => {
|
|
41
|
-
try {
|
|
42
|
-
resolve(body ? JSON.parse(body) : {});
|
|
43
|
-
} catch (err) {
|
|
44
|
-
reject(err);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
req.on("error", reject);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function startAdminServer({ port = null, customDb = null, customBlobDir = BLOBS_DIR } = {}) {
|
|
52
|
-
const db = customDb || getDatabase();
|
|
53
|
-
const selectedPort = port || (await findAvailablePort());
|
|
54
|
-
const htmlPath = join(__dirname, "index.html");
|
|
55
|
-
|
|
56
|
-
const server = createServer(async (req, res) => {
|
|
57
|
-
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
58
|
-
const pathname = url.pathname;
|
|
59
|
-
|
|
60
|
-
// Helper for CORS and JSON response
|
|
61
|
-
const sendJson = (data, status = 200) => {
|
|
62
|
-
res.writeHead(status, {
|
|
63
|
-
"Content-Type": "application/json",
|
|
64
|
-
"Access-Control-Allow-Origin": "*",
|
|
65
|
-
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
66
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
67
|
-
});
|
|
68
|
-
res.end(JSON.stringify(data));
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
if (req.method === "OPTIONS") {
|
|
72
|
-
res.writeHead(204, {
|
|
73
|
-
"Access-Control-Allow-Origin": "*",
|
|
74
|
-
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
75
|
-
"Access-Control-Allow-Headers": "Content-Type",
|
|
76
|
-
});
|
|
77
|
-
return res.end();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
// 1. Static HTML SPA
|
|
82
|
-
if (pathname === "/" || pathname === "/index.html") {
|
|
83
|
-
if (!existsSync(htmlPath)) {
|
|
84
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
85
|
-
return res.end("index.html not found");
|
|
86
|
-
}
|
|
87
|
-
const html = readFileSync(htmlPath, "utf-8");
|
|
88
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
89
|
-
return res.end(html);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// 2. API: Stats
|
|
93
|
-
if (pathname === "/api/stats" && req.method === "GET") {
|
|
94
|
-
const docCount = db.prepare("SELECT COUNT(*) as cnt FROM documents").get().cnt;
|
|
95
|
-
const secCount = db.prepare("SELECT COUNT(*) as cnt FROM sections").get().cnt;
|
|
96
|
-
const chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get().cnt;
|
|
97
|
-
const edgeCount = db.prepare("SELECT COUNT(*) as cnt FROM graph_edges").get().cnt;
|
|
98
|
-
let dbSize = 0;
|
|
99
|
-
if (existsSync(DB_PATH)) {
|
|
100
|
-
try {
|
|
101
|
-
dbSize = statSync(DB_PATH).size;
|
|
102
|
-
} catch {}
|
|
103
|
-
}
|
|
104
|
-
return sendJson({
|
|
105
|
-
documents: docCount,
|
|
106
|
-
sections: secCount,
|
|
107
|
-
micro_chunks: chunkCount,
|
|
108
|
-
graph_edges: edgeCount,
|
|
109
|
-
db_size_bytes: dbSize,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 3. API: Documents List
|
|
114
|
-
if (pathname === "/api/documents" && req.method === "GET") {
|
|
115
|
-
const docs = db.prepare("SELECT * FROM documents ORDER BY updated_at DESC").all();
|
|
116
|
-
return sendJson(docs);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// 4. API: Document Detail
|
|
120
|
-
if (pathname.startsWith("/api/documents/") && req.method === "GET") {
|
|
121
|
-
const docId = pathname.replace("/api/documents/", "");
|
|
122
|
-
const doc = db.prepare("SELECT * FROM documents WHERE id = ?").get(docId);
|
|
123
|
-
if (!doc) return sendJson({ error: "Document not found" }, 404);
|
|
124
|
-
|
|
125
|
-
const sections = db.prepare("SELECT * FROM sections WHERE doc_id = ?").all(docId);
|
|
126
|
-
const microChunks = db.prepare("SELECT id, section_id, token_count FROM micro_chunks WHERE doc_id = ?").all(docId);
|
|
127
|
-
const edges = db.prepare("SELECT * FROM graph_edges WHERE source_id = ? OR target_id = ?").all(docId, docId);
|
|
128
|
-
|
|
129
|
-
let blobContent = null;
|
|
130
|
-
if (doc.blob_hash) {
|
|
131
|
-
try {
|
|
132
|
-
blobContent = await readBlob(doc.blob_hash, customBlobDir);
|
|
133
|
-
} catch {}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return sendJson({ doc, sections, microChunks, edges, blobContent });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 5. API: Delete Document
|
|
140
|
-
if (pathname.startsWith("/api/documents/") && req.method === "DELETE") {
|
|
141
|
-
const docId = pathname.replace("/api/documents/", "");
|
|
142
|
-
const result = await deleteDocument(docId, db, customBlobDir);
|
|
143
|
-
return sendJson(result);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// 6. API: Ingest Document
|
|
147
|
-
if (pathname === "/api/ingest" && req.method === "POST") {
|
|
148
|
-
const body = await parseJsonBody(req);
|
|
149
|
-
const result = await ingestDocument({
|
|
150
|
-
content: body.content,
|
|
151
|
-
type: body.type || "text",
|
|
152
|
-
path: body.path || null,
|
|
153
|
-
title: body.title || null,
|
|
154
|
-
generateEmbeddings: body.generateEmbeddings !== false,
|
|
155
|
-
customDb: db,
|
|
156
|
-
customBlobDir,
|
|
157
|
-
});
|
|
158
|
-
return sendJson(result, 201);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 7. API: Query Knowledge Base
|
|
162
|
-
if (pathname === "/api/query" && req.method === "POST") {
|
|
163
|
-
const body = await parseJsonBody(req);
|
|
164
|
-
const results = await hybridQuery({
|
|
165
|
-
query: body.query,
|
|
166
|
-
limit: body.limit || 5,
|
|
167
|
-
generateEmbeddings: body.generateEmbeddings !== false,
|
|
168
|
-
customDb: db,
|
|
169
|
-
});
|
|
170
|
-
return sendJson({ results });
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 8. API: Graph Visualizer Data
|
|
174
|
-
if (pathname === "/api/graph" && req.method === "GET") {
|
|
175
|
-
const docs = db.prepare("SELECT id, title, path FROM documents").all();
|
|
176
|
-
const edges = db.prepare("SELECT * FROM graph_edges").all();
|
|
177
|
-
|
|
178
|
-
const nodes = docs.map((d) => ({
|
|
179
|
-
id: d.id,
|
|
180
|
-
label: d.title || d.path || d.id,
|
|
181
|
-
type: "DOCUMENT",
|
|
182
|
-
}));
|
|
183
|
-
|
|
184
|
-
// Add code symbol nodes
|
|
185
|
-
const symbolEdges = edges.filter((e) => e.relation_type === "DEFINES_SYMBOL");
|
|
186
|
-
for (const se of symbolEdges) {
|
|
187
|
-
if (!nodes.some((n) => n.id === se.target_id)) {
|
|
188
|
-
nodes.push({
|
|
189
|
-
id: se.target_id,
|
|
190
|
-
label: se.target_id,
|
|
191
|
-
type: "CODE_SYMBOL",
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return sendJson({ nodes, edges });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// 9. API: Export Snapshot
|
|
200
|
-
if (pathname === "/api/snapshot/export" && (req.method === "GET" || req.method === "POST")) {
|
|
201
|
-
const snapshot = await exportSnapshot({ customDb: db, customBlobDir });
|
|
202
|
-
return sendJson(snapshot);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 10. API: Import Snapshot
|
|
206
|
-
if (pathname === "/api/snapshot/import" && req.method === "POST") {
|
|
207
|
-
const body = await parseJsonBody(req);
|
|
208
|
-
const result = await importSnapshot({ customDb: db, customBlobDir, snapshotPathOrData: body });
|
|
209
|
-
return sendJson(result);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// 404 Fallback
|
|
213
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
214
|
-
res.end("Not Found");
|
|
215
|
-
} catch (err) {
|
|
216
|
-
console.error("Admin server error:", err);
|
|
217
|
-
sendJson({ error: err.message }, 500);
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
return new Promise((resolve) => {
|
|
222
|
-
server.listen(selectedPort, () => {
|
|
223
|
-
const url = `http://localhost:${selectedPort}`;
|
|
224
|
-
console.log(`🚀 memory-agent Web Admin Dashboard running at ${url}`);
|
|
225
|
-
resolve({ server, port: selectedPort, url });
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
}
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { getDatabase, DB_PATH, BLOBS_DIR } from "../db/database.js";
|
|
6
|
+
import { ingestDocument, deleteDocument } from "../ingest/pipeline.js";
|
|
7
|
+
import { hybridQuery } from "../retrieval/retriever.js";
|
|
8
|
+
import { exportSnapshot, importSnapshot } from "./snapshot.js";
|
|
9
|
+
import { readBlob } from "../storage/blob_store.js";
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
|
|
14
|
+
export function findAvailablePort(startPort = 8765, maxPort = 8785) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
let port = startPort;
|
|
17
|
+
const tryPort = () => {
|
|
18
|
+
if (port > maxPort) {
|
|
19
|
+
return reject(new Error(`No free port found between ${startPort} and ${maxPort}`));
|
|
20
|
+
}
|
|
21
|
+
const server = createServer();
|
|
22
|
+
server.listen(port, () => {
|
|
23
|
+
server.close(() => resolve(port));
|
|
24
|
+
});
|
|
25
|
+
server.on("error", () => {
|
|
26
|
+
port++;
|
|
27
|
+
tryPort();
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
tryPort();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseJsonBody(req) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let body = "";
|
|
37
|
+
req.on("data", (chunk) => {
|
|
38
|
+
body += chunk.toString();
|
|
39
|
+
});
|
|
40
|
+
req.on("end", () => {
|
|
41
|
+
try {
|
|
42
|
+
resolve(body ? JSON.parse(body) : {});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
reject(err);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
req.on("error", reject);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function startAdminServer({ port = null, customDb = null, customBlobDir = BLOBS_DIR } = {}) {
|
|
52
|
+
const db = customDb || getDatabase();
|
|
53
|
+
const selectedPort = port || (await findAvailablePort());
|
|
54
|
+
const htmlPath = join(__dirname, "index.html");
|
|
55
|
+
|
|
56
|
+
const server = createServer(async (req, res) => {
|
|
57
|
+
const url = new URL(req.url, `http://${req.headers.host || "localhost"}`);
|
|
58
|
+
const pathname = url.pathname;
|
|
59
|
+
|
|
60
|
+
// Helper for CORS and JSON response
|
|
61
|
+
const sendJson = (data, status = 200) => {
|
|
62
|
+
res.writeHead(status, {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
"Access-Control-Allow-Origin": "*",
|
|
65
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
66
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
67
|
+
});
|
|
68
|
+
res.end(JSON.stringify(data));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (req.method === "OPTIONS") {
|
|
72
|
+
res.writeHead(204, {
|
|
73
|
+
"Access-Control-Allow-Origin": "*",
|
|
74
|
+
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
75
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
76
|
+
});
|
|
77
|
+
return res.end();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// 1. Static HTML SPA
|
|
82
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
83
|
+
if (!existsSync(htmlPath)) {
|
|
84
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
85
|
+
return res.end("index.html not found");
|
|
86
|
+
}
|
|
87
|
+
const html = readFileSync(htmlPath, "utf-8");
|
|
88
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
89
|
+
return res.end(html);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. API: Stats
|
|
93
|
+
if (pathname === "/api/stats" && req.method === "GET") {
|
|
94
|
+
const docCount = db.prepare("SELECT COUNT(*) as cnt FROM documents").get().cnt;
|
|
95
|
+
const secCount = db.prepare("SELECT COUNT(*) as cnt FROM sections").get().cnt;
|
|
96
|
+
const chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get().cnt;
|
|
97
|
+
const edgeCount = db.prepare("SELECT COUNT(*) as cnt FROM graph_edges").get().cnt;
|
|
98
|
+
let dbSize = 0;
|
|
99
|
+
if (existsSync(DB_PATH)) {
|
|
100
|
+
try {
|
|
101
|
+
dbSize = statSync(DB_PATH).size;
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
return sendJson({
|
|
105
|
+
documents: docCount,
|
|
106
|
+
sections: secCount,
|
|
107
|
+
micro_chunks: chunkCount,
|
|
108
|
+
graph_edges: edgeCount,
|
|
109
|
+
db_size_bytes: dbSize,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 3. API: Documents List
|
|
114
|
+
if (pathname === "/api/documents" && req.method === "GET") {
|
|
115
|
+
const docs = db.prepare("SELECT * FROM documents ORDER BY updated_at DESC").all();
|
|
116
|
+
return sendJson(docs);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 4. API: Document Detail
|
|
120
|
+
if (pathname.startsWith("/api/documents/") && req.method === "GET") {
|
|
121
|
+
const docId = pathname.replace("/api/documents/", "");
|
|
122
|
+
const doc = db.prepare("SELECT * FROM documents WHERE id = ?").get(docId);
|
|
123
|
+
if (!doc) return sendJson({ error: "Document not found" }, 404);
|
|
124
|
+
|
|
125
|
+
const sections = db.prepare("SELECT * FROM sections WHERE doc_id = ?").all(docId);
|
|
126
|
+
const microChunks = db.prepare("SELECT id, section_id, token_count FROM micro_chunks WHERE doc_id = ?").all(docId);
|
|
127
|
+
const edges = db.prepare("SELECT * FROM graph_edges WHERE source_id = ? OR target_id = ?").all(docId, docId);
|
|
128
|
+
|
|
129
|
+
let blobContent = null;
|
|
130
|
+
if (doc.blob_hash) {
|
|
131
|
+
try {
|
|
132
|
+
blobContent = await readBlob(doc.blob_hash, customBlobDir);
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return sendJson({ doc, sections, microChunks, edges, blobContent });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 5. API: Delete Document
|
|
140
|
+
if (pathname.startsWith("/api/documents/") && req.method === "DELETE") {
|
|
141
|
+
const docId = pathname.replace("/api/documents/", "");
|
|
142
|
+
const result = await deleteDocument(docId, db, customBlobDir);
|
|
143
|
+
return sendJson(result);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 6. API: Ingest Document
|
|
147
|
+
if (pathname === "/api/ingest" && req.method === "POST") {
|
|
148
|
+
const body = await parseJsonBody(req);
|
|
149
|
+
const result = await ingestDocument({
|
|
150
|
+
content: body.content,
|
|
151
|
+
type: body.type || "text",
|
|
152
|
+
path: body.path || null,
|
|
153
|
+
title: body.title || null,
|
|
154
|
+
generateEmbeddings: body.generateEmbeddings !== false,
|
|
155
|
+
customDb: db,
|
|
156
|
+
customBlobDir,
|
|
157
|
+
});
|
|
158
|
+
return sendJson(result, 201);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 7. API: Query Knowledge Base
|
|
162
|
+
if (pathname === "/api/query" && req.method === "POST") {
|
|
163
|
+
const body = await parseJsonBody(req);
|
|
164
|
+
const results = await hybridQuery({
|
|
165
|
+
query: body.query,
|
|
166
|
+
limit: body.limit || 5,
|
|
167
|
+
generateEmbeddings: body.generateEmbeddings !== false,
|
|
168
|
+
customDb: db,
|
|
169
|
+
});
|
|
170
|
+
return sendJson({ results });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 8. API: Graph Visualizer Data
|
|
174
|
+
if (pathname === "/api/graph" && req.method === "GET") {
|
|
175
|
+
const docs = db.prepare("SELECT id, title, path FROM documents").all();
|
|
176
|
+
const edges = db.prepare("SELECT * FROM graph_edges").all();
|
|
177
|
+
|
|
178
|
+
const nodes = docs.map((d) => ({
|
|
179
|
+
id: d.id,
|
|
180
|
+
label: d.title || d.path || d.id,
|
|
181
|
+
type: "DOCUMENT",
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
// Add code symbol nodes
|
|
185
|
+
const symbolEdges = edges.filter((e) => e.relation_type === "DEFINES_SYMBOL");
|
|
186
|
+
for (const se of symbolEdges) {
|
|
187
|
+
if (!nodes.some((n) => n.id === se.target_id)) {
|
|
188
|
+
nodes.push({
|
|
189
|
+
id: se.target_id,
|
|
190
|
+
label: se.target_id,
|
|
191
|
+
type: "CODE_SYMBOL",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return sendJson({ nodes, edges });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 9. API: Export Snapshot
|
|
200
|
+
if (pathname === "/api/snapshot/export" && (req.method === "GET" || req.method === "POST")) {
|
|
201
|
+
const snapshot = await exportSnapshot({ customDb: db, customBlobDir });
|
|
202
|
+
return sendJson(snapshot);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 10. API: Import Snapshot
|
|
206
|
+
if (pathname === "/api/snapshot/import" && req.method === "POST") {
|
|
207
|
+
const body = await parseJsonBody(req);
|
|
208
|
+
const result = await importSnapshot({ customDb: db, customBlobDir, snapshotPathOrData: body });
|
|
209
|
+
return sendJson(result);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 404 Fallback
|
|
213
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
214
|
+
res.end("Not Found");
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error("Admin server error:", err);
|
|
217
|
+
sendJson({ error: err.message }, 500);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return new Promise((resolve) => {
|
|
222
|
+
server.listen(selectedPort, () => {
|
|
223
|
+
const url = `http://localhost:${selectedPort}`;
|
|
224
|
+
console.log(`🚀 memory-agent Web Admin Dashboard running at ${url}`);
|
|
225
|
+
resolve({ server, port: selectedPort, url });
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|