devsmind-mcp 1.2.1 → 2.0.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 -21
- package/README.md +55 -47
- package/dist/cli/runner.js +232 -151
- package/dist/cli/runner.js.map +1 -1
- package/dist/db/database.d.ts +15 -1
- package/dist/db/database.js +420 -74
- package/dist/db/database.js.map +1 -1
- package/dist/db/schema.js +33 -33
- package/dist/mcp/server.js +21 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/visualizer_2d.html +635 -635
- package/dist/mcp/visualizer_3d.html +628 -628
- package/dist/utils/json.d.ts +16 -0
- package/dist/utils/json.js +151 -0
- package/dist/utils/json.js.map +1 -0
- package/package.json +7 -3
package/dist/db/database.js
CHANGED
|
@@ -44,6 +44,7 @@ const fs = __importStar(require("fs"));
|
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
45
|
const zlib = __importStar(require("zlib"));
|
|
46
46
|
const schema_1 = require("./schema");
|
|
47
|
+
const config_1 = require("../utils/config");
|
|
47
48
|
function compressText(text) {
|
|
48
49
|
return zlib.deflateSync(Buffer.from(text, 'utf-8'));
|
|
49
50
|
}
|
|
@@ -76,13 +77,25 @@ function formatReasoning(r) {
|
|
|
76
77
|
}
|
|
77
78
|
class DevMindDatabase {
|
|
78
79
|
db;
|
|
80
|
+
dbPath;
|
|
81
|
+
context = null;
|
|
79
82
|
constructor(dbPath) {
|
|
83
|
+
this.dbPath = dbPath;
|
|
80
84
|
// Open SQLite database
|
|
81
85
|
this.db = new better_sqlite3_1.default(dbPath);
|
|
82
86
|
// Enable foreign keys
|
|
83
87
|
this.db.pragma('foreign_keys = ON');
|
|
84
88
|
// Initialize schema
|
|
85
89
|
this.initSchema();
|
|
90
|
+
// Load project context from .devmind directory path
|
|
91
|
+
try {
|
|
92
|
+
this.context = (0, config_1.loadProjectContext)(path.dirname(dbPath));
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// Ignore context errors (e.g. running from scratch scripts)
|
|
96
|
+
}
|
|
97
|
+
// Auto-sync history and graph from disk JSONs
|
|
98
|
+
this.syncFromDisk();
|
|
86
99
|
}
|
|
87
100
|
initSchema() {
|
|
88
101
|
this.db.exec(schema_1.INIT_SCHEMA_SQL);
|
|
@@ -106,34 +119,71 @@ class DevMindDatabase {
|
|
|
106
119
|
}
|
|
107
120
|
// --- Node Operations ---
|
|
108
121
|
upsertNode(node) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
const existing = this.getNode(node.id);
|
|
123
|
+
if (existing) {
|
|
124
|
+
let finalPath = existing.file_path;
|
|
125
|
+
const paths = existing.file_path.split(',').map(p => p.trim()).filter(Boolean);
|
|
126
|
+
const incoming = node.file_path.trim();
|
|
127
|
+
if (!paths.includes(incoming)) {
|
|
128
|
+
paths.push(incoming);
|
|
129
|
+
finalPath = paths.join(', ');
|
|
130
|
+
}
|
|
131
|
+
const stmt = this.db.prepare(`
|
|
132
|
+
UPDATE nodes
|
|
133
|
+
SET type = ?,
|
|
134
|
+
name = ?,
|
|
135
|
+
file_path = ?,
|
|
136
|
+
signature = COALESCE(?, signature),
|
|
137
|
+
deprecated = 0
|
|
138
|
+
WHERE id = ?
|
|
139
|
+
`);
|
|
140
|
+
stmt.run(node.type, node.name, finalPath, node.signature || null, node.id);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const stmt = this.db.prepare(`
|
|
144
|
+
INSERT INTO nodes (id, type, name, file_path, signature)
|
|
145
|
+
VALUES (?, ?, ?, ?, ?)
|
|
146
|
+
`);
|
|
147
|
+
stmt.run(node.id, node.type, node.name, node.file_path, node.signature || null);
|
|
148
|
+
}
|
|
149
|
+
this.writeGraphToDisk(node.file_path);
|
|
120
150
|
}
|
|
121
151
|
getNode(id) {
|
|
122
152
|
const stmt = this.db.prepare('SELECT * FROM nodes WHERE id = ?');
|
|
123
|
-
|
|
153
|
+
const direct = stmt.get(id);
|
|
154
|
+
if (direct)
|
|
155
|
+
return direct;
|
|
156
|
+
if (!id.includes('#')) {
|
|
157
|
+
const suffixStmt = this.db.prepare('SELECT * FROM nodes WHERE id LIKE ? AND deprecated = 0');
|
|
158
|
+
const matches = suffixStmt.all(`%#${id}`);
|
|
159
|
+
if (matches.length === 1) {
|
|
160
|
+
return matches[0];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
124
164
|
}
|
|
125
165
|
deleteNode(id) {
|
|
166
|
+
const node = this.getNode(id);
|
|
167
|
+
const resolvedId = node ? node.id : id;
|
|
126
168
|
const stmt = this.db.prepare('DELETE FROM nodes WHERE id = ?');
|
|
127
|
-
stmt.run(
|
|
169
|
+
stmt.run(resolvedId);
|
|
170
|
+
if (node && node.file_path) {
|
|
171
|
+
this.writeGraphToDisk(node.file_path);
|
|
172
|
+
}
|
|
128
173
|
}
|
|
129
174
|
deprecateNode(id) {
|
|
175
|
+
const node = this.getNode(id);
|
|
176
|
+
const resolvedId = node ? node.id : id;
|
|
130
177
|
const updateStmt = this.db.prepare('UPDATE nodes SET deprecated = 1 WHERE id = ?');
|
|
131
178
|
const deleteConnStmt = this.db.prepare('DELETE FROM node_connections WHERE source_node_id = ? OR target_node_id = ?');
|
|
132
179
|
const tx = this.db.transaction(() => {
|
|
133
|
-
updateStmt.run(
|
|
134
|
-
deleteConnStmt.run(
|
|
180
|
+
updateStmt.run(resolvedId);
|
|
181
|
+
deleteConnStmt.run(resolvedId, resolvedId);
|
|
135
182
|
});
|
|
136
183
|
tx();
|
|
184
|
+
if (node && node.file_path) {
|
|
185
|
+
this.writeGraphToDisk(node.file_path);
|
|
186
|
+
}
|
|
137
187
|
}
|
|
138
188
|
renameNode(oldId, newId, newName) {
|
|
139
189
|
const node = this.getNode(oldId);
|
|
@@ -165,6 +215,9 @@ class DevMindDatabase {
|
|
|
165
215
|
deleteOldStmt.run(oldId);
|
|
166
216
|
});
|
|
167
217
|
runTx();
|
|
218
|
+
if (node.file_path) {
|
|
219
|
+
this.writeGraphToDisk(node.file_path);
|
|
220
|
+
}
|
|
168
221
|
}
|
|
169
222
|
finally {
|
|
170
223
|
this.db.pragma('foreign_keys = ON');
|
|
@@ -172,29 +225,42 @@ class DevMindDatabase {
|
|
|
172
225
|
}
|
|
173
226
|
// --- Connection Operations ---
|
|
174
227
|
addConnection(sourceNodeId, targetNodeId) {
|
|
228
|
+
const srcNode = this.getNode(sourceNodeId);
|
|
229
|
+
const tgtNode = this.getNode(targetNodeId);
|
|
230
|
+
const resolvedSrc = srcNode ? srcNode.id : sourceNodeId;
|
|
231
|
+
const resolvedTgt = tgtNode ? tgtNode.id : targetNodeId;
|
|
232
|
+
this.db.pragma('foreign_keys = OFF');
|
|
175
233
|
try {
|
|
176
234
|
const stmt = this.db.prepare(`
|
|
177
235
|
INSERT OR IGNORE INTO node_connections (source_node_id, target_node_id)
|
|
178
236
|
VALUES (?, ?)
|
|
179
237
|
`);
|
|
180
|
-
stmt.run(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (err instanceof Error && err.message.includes('FOREIGN KEY')) {
|
|
184
|
-
// Ignore foreign key violations (e.g. target node defined in a file not indexed yet, or external library)
|
|
185
|
-
return;
|
|
238
|
+
stmt.run(resolvedSrc, resolvedTgt);
|
|
239
|
+
if (srcNode && srcNode.file_path) {
|
|
240
|
+
this.writeGraphToDisk(srcNode.file_path);
|
|
186
241
|
}
|
|
187
|
-
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
this.db.pragma('foreign_keys = ON');
|
|
188
245
|
}
|
|
189
246
|
}
|
|
190
247
|
removeConnection(sourceNodeId, targetNodeId) {
|
|
248
|
+
const srcNode = this.getNode(sourceNodeId);
|
|
249
|
+
const tgtNode = this.getNode(targetNodeId);
|
|
250
|
+
const resolvedSrc = srcNode ? srcNode.id : sourceNodeId;
|
|
251
|
+
const resolvedTgt = tgtNode ? tgtNode.id : targetNodeId;
|
|
191
252
|
const stmt = this.db.prepare(`
|
|
192
253
|
DELETE FROM node_connections
|
|
193
254
|
WHERE source_node_id = ? AND target_node_id = ?
|
|
194
255
|
`);
|
|
195
|
-
stmt.run(
|
|
256
|
+
stmt.run(resolvedSrc, resolvedTgt);
|
|
257
|
+
if (srcNode && srcNode.file_path) {
|
|
258
|
+
this.writeGraphToDisk(srcNode.file_path);
|
|
259
|
+
}
|
|
196
260
|
}
|
|
197
261
|
getConnections(nodeId) {
|
|
262
|
+
const node = this.getNode(nodeId);
|
|
263
|
+
const resolvedId = node ? node.id : nodeId;
|
|
198
264
|
const usesStmt = this.db.prepare(`
|
|
199
265
|
SELECT n.* FROM nodes n
|
|
200
266
|
JOIN node_connections c ON n.id = c.target_node_id
|
|
@@ -206,75 +272,64 @@ class DevMindDatabase {
|
|
|
206
272
|
WHERE c.target_node_id = ?
|
|
207
273
|
`);
|
|
208
274
|
return {
|
|
209
|
-
uses: usesStmt.all(
|
|
210
|
-
usedBy: usedByStmt.all(
|
|
275
|
+
uses: usesStmt.all(resolvedId),
|
|
276
|
+
usedBy: usedByStmt.all(resolvedId)
|
|
211
277
|
};
|
|
212
278
|
}
|
|
213
279
|
// --- History Operations ---
|
|
214
280
|
getLatestHistory(nodeId) {
|
|
281
|
+
const node = this.getNode(nodeId);
|
|
282
|
+
const resolvedId = node ? node.id : nodeId;
|
|
215
283
|
const stmt = this.db.prepare(`
|
|
216
|
-
SELECT
|
|
284
|
+
SELECT id, node_id, session_id, created_at, updated_at FROM history
|
|
217
285
|
WHERE node_id = ?
|
|
218
286
|
ORDER BY updated_at DESC
|
|
219
287
|
LIMIT 1
|
|
220
288
|
`);
|
|
221
|
-
const row = stmt.get(
|
|
289
|
+
const row = stmt.get(resolvedId);
|
|
222
290
|
if (!row)
|
|
223
291
|
return null;
|
|
224
|
-
return
|
|
225
|
-
...row,
|
|
226
|
-
code_snapshot: decompressText(row.code_snapshot),
|
|
227
|
-
reasoning: decompressText(row.reasoning)
|
|
228
|
-
};
|
|
292
|
+
return this.populateHistoryFromDisk(row);
|
|
229
293
|
}
|
|
230
294
|
listHistory(nodeId) {
|
|
295
|
+
const node = this.getNode(nodeId);
|
|
296
|
+
const resolvedId = node ? node.id : nodeId;
|
|
231
297
|
const stmt = this.db.prepare(`
|
|
232
298
|
SELECT id, node_id, session_id, created_at, updated_at
|
|
233
299
|
FROM history
|
|
234
300
|
WHERE node_id = ?
|
|
235
301
|
ORDER BY updated_at DESC
|
|
236
302
|
`);
|
|
237
|
-
return stmt.all(
|
|
303
|
+
return stmt.all(resolvedId);
|
|
238
304
|
}
|
|
239
305
|
getHistoryEntry(id) {
|
|
240
|
-
const stmt = this.db.prepare('SELECT
|
|
306
|
+
const stmt = this.db.prepare('SELECT id, node_id, session_id, created_at, updated_at FROM history WHERE id = ?');
|
|
241
307
|
const row = stmt.get(id);
|
|
242
308
|
if (!row)
|
|
243
309
|
return null;
|
|
244
|
-
return
|
|
245
|
-
...row,
|
|
246
|
-
code_snapshot: decompressText(row.code_snapshot),
|
|
247
|
-
reasoning: decompressText(row.reasoning)
|
|
248
|
-
};
|
|
310
|
+
return this.populateHistoryFromDisk(row);
|
|
249
311
|
}
|
|
250
312
|
getFullHistory(nodeId) {
|
|
313
|
+
const node = this.getNode(nodeId);
|
|
314
|
+
const resolvedId = node ? node.id : nodeId;
|
|
251
315
|
const stmt = this.db.prepare(`
|
|
252
|
-
SELECT
|
|
316
|
+
SELECT id, node_id, session_id, created_at, updated_at
|
|
253
317
|
FROM history
|
|
254
318
|
WHERE node_id = ?
|
|
255
319
|
ORDER BY updated_at DESC
|
|
256
320
|
`);
|
|
257
|
-
const rows = stmt.all(
|
|
258
|
-
return rows.map(row => (
|
|
259
|
-
...row,
|
|
260
|
-
code_snapshot: decompressText(row.code_snapshot),
|
|
261
|
-
reasoning: decompressText(row.reasoning)
|
|
262
|
-
}));
|
|
321
|
+
const rows = stmt.all(resolvedId);
|
|
322
|
+
return rows.map(row => this.populateHistoryFromDisk(row));
|
|
263
323
|
}
|
|
264
324
|
getLatestCode(nodeId) {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
ORDER BY updated_at DESC
|
|
270
|
-
LIMIT 1
|
|
271
|
-
`);
|
|
272
|
-
const row = stmt.get(nodeId);
|
|
273
|
-
if (!row)
|
|
325
|
+
const node = this.getNode(nodeId);
|
|
326
|
+
const resolvedId = node ? node.id : nodeId;
|
|
327
|
+
const history = this.getLatestHistory(resolvedId);
|
|
328
|
+
if (!history)
|
|
274
329
|
return null;
|
|
275
330
|
return {
|
|
276
|
-
updated_at:
|
|
277
|
-
code_snapshot:
|
|
331
|
+
updated_at: history.updated_at,
|
|
332
|
+
code_snapshot: history.code_snapshot
|
|
278
333
|
};
|
|
279
334
|
}
|
|
280
335
|
getGraph(nodeId, maxDepth = 6) {
|
|
@@ -348,11 +403,13 @@ class DevMindDatabase {
|
|
|
348
403
|
}
|
|
349
404
|
updateHistory(params) {
|
|
350
405
|
const { node_id, code_snapshot, reasoning } = params;
|
|
406
|
+
const node = this.getNode(node_id);
|
|
407
|
+
const resolvedId = node ? node.id : node_id;
|
|
351
408
|
const formattedReasoning = formatReasoning(reasoning);
|
|
352
409
|
const nowStr = new Date().toISOString();
|
|
353
410
|
const compressedCode = compressText(code_snapshot);
|
|
354
411
|
// 1-hour session boundary rule check
|
|
355
|
-
const latest = this.getLatestHistory(
|
|
412
|
+
const latest = this.getLatestHistory(resolvedId);
|
|
356
413
|
if (latest) {
|
|
357
414
|
const lastUpdate = new Date(latest.updated_at).getTime();
|
|
358
415
|
const nowTime = new Date(nowStr).getTime();
|
|
@@ -361,10 +418,12 @@ class DevMindDatabase {
|
|
|
361
418
|
if (diffMs < 3600000) {
|
|
362
419
|
const updateStmt = this.db.prepare(`
|
|
363
420
|
UPDATE history
|
|
364
|
-
SET code_snapshot =
|
|
421
|
+
SET code_snapshot = '', reasoning = '', updated_at = ?
|
|
365
422
|
WHERE id = ?
|
|
366
423
|
`);
|
|
367
|
-
updateStmt.run(
|
|
424
|
+
updateStmt.run(nowStr, latest.id);
|
|
425
|
+
// Write/Update on disk
|
|
426
|
+
this.writeHistoryToDisk(latest.id, resolvedId, latest.session_id, latest.created_at, nowStr, code_snapshot, formattedReasoning);
|
|
368
427
|
return {
|
|
369
428
|
...latest,
|
|
370
429
|
code_snapshot,
|
|
@@ -378,12 +437,14 @@ class DevMindDatabase {
|
|
|
378
437
|
const sessionId = params.session_id || crypto.randomUUID();
|
|
379
438
|
const insertStmt = this.db.prepare(`
|
|
380
439
|
INSERT INTO history (id, node_id, session_id, created_at, updated_at, code_snapshot, reasoning)
|
|
381
|
-
VALUES (?, ?, ?, ?, ?,
|
|
440
|
+
VALUES (?, ?, ?, ?, ?, '', '')
|
|
382
441
|
`);
|
|
383
|
-
insertStmt.run(newId,
|
|
442
|
+
insertStmt.run(newId, resolvedId, sessionId, nowStr, nowStr);
|
|
443
|
+
// Write to disk
|
|
444
|
+
this.writeHistoryToDisk(newId, resolvedId, sessionId, nowStr, nowStr, code_snapshot, formattedReasoning);
|
|
384
445
|
return {
|
|
385
446
|
id: newId,
|
|
386
|
-
node_id,
|
|
447
|
+
node_id: resolvedId,
|
|
387
448
|
session_id: sessionId,
|
|
388
449
|
created_at: nowStr,
|
|
389
450
|
updated_at: nowStr,
|
|
@@ -402,7 +463,7 @@ class DevMindDatabase {
|
|
|
402
463
|
const wildcard = `%${query}%`;
|
|
403
464
|
return stmt.all(wildcard, wildcard, wildcard);
|
|
404
465
|
}
|
|
405
|
-
getRecentChanges(hours = 24) {
|
|
466
|
+
getRecentChanges(hours = 24, analyzeImpact = true) {
|
|
406
467
|
const stmt = this.db.prepare(`
|
|
407
468
|
SELECT h.node_id, n.name as node_name, n.file_path, h.updated_at, h.reasoning
|
|
408
469
|
FROM history h
|
|
@@ -410,8 +471,27 @@ class DevMindDatabase {
|
|
|
410
471
|
WHERE h.updated_at >= datetime('now', ?)
|
|
411
472
|
ORDER BY h.updated_at DESC
|
|
412
473
|
`);
|
|
413
|
-
|
|
414
|
-
|
|
474
|
+
const recentChanges = stmt.all(`-${hours} hours`);
|
|
475
|
+
if (!analyzeImpact) {
|
|
476
|
+
return recentChanges;
|
|
477
|
+
}
|
|
478
|
+
const modifiedSet = new Set(recentChanges.map(c => c.node_id));
|
|
479
|
+
const callersStmt = this.db.prepare(`
|
|
480
|
+
SELECT n.id as node_id, n.name as node_name, n.file_path
|
|
481
|
+
FROM nodes n
|
|
482
|
+
JOIN node_connections c ON n.id = c.source_node_id
|
|
483
|
+
WHERE c.target_node_id = ?
|
|
484
|
+
`);
|
|
485
|
+
for (const change of recentChanges) {
|
|
486
|
+
const callers = callersStmt.all(change.node_id);
|
|
487
|
+
change.downstream_impact = callers.map(caller => ({
|
|
488
|
+
node_id: caller.node_id,
|
|
489
|
+
node_name: caller.node_name,
|
|
490
|
+
file_path: caller.file_path,
|
|
491
|
+
status: modifiedSet.has(caller.node_id) ? 'already_updated' : 'stale_warning'
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
return recentChanges;
|
|
415
495
|
}
|
|
416
496
|
getDeveloperActivity(developer, limit = 50) {
|
|
417
497
|
const stmt = this.db.prepare(`
|
|
@@ -492,12 +572,13 @@ class DevMindDatabase {
|
|
|
492
572
|
pruneSpuriousNodes(workspaceRoot) {
|
|
493
573
|
const spuriousNames = new Set([
|
|
494
574
|
'promise', 'map', 'set', 'json', 'console', 'error', 'object', 'function', 'array', 'string', 'number', 'boolean', 'regexp', 'date', 'math',
|
|
495
|
-
'any', 'void', 'unknown', 'never', 'null', 'undefined', 'dict', 'list'
|
|
575
|
+
'any', 'void', 'unknown', 'never', 'null', 'undefined', 'dict', 'list',
|
|
576
|
+
'data', 'useeffect', 'val', 'temp', 'result', 'item', 'key', 'value', 'err', 'req', 'res', 'args', 'params', 'response', 'request'
|
|
496
577
|
]);
|
|
497
|
-
// Get nodes with
|
|
578
|
+
// Get all active nodes (including those with history) to check for missing files or spurious names
|
|
498
579
|
const stmt = this.db.prepare(`
|
|
499
580
|
SELECT id, name, file_path FROM nodes
|
|
500
|
-
WHERE deprecated = 0
|
|
581
|
+
WHERE deprecated = 0
|
|
501
582
|
`);
|
|
502
583
|
const candidates = stmt.all();
|
|
503
584
|
const idsToDelete = [];
|
|
@@ -509,11 +590,17 @@ class DevMindDatabase {
|
|
|
509
590
|
// 2. Check if file path does not exist on disk
|
|
510
591
|
let fileMissing = false;
|
|
511
592
|
if (node.file_path) {
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
593
|
+
const paths = node.file_path.split(',').map(p => p.trim()).filter(Boolean);
|
|
594
|
+
if (paths.length > 0) {
|
|
595
|
+
const allMissing = paths.every(p => {
|
|
596
|
+
const resolvedPath = path.isAbsolute(p)
|
|
597
|
+
? p
|
|
598
|
+
: path.resolve(workspaceRoot, p);
|
|
599
|
+
return !fs.existsSync(resolvedPath);
|
|
600
|
+
});
|
|
601
|
+
if (allMissing) {
|
|
602
|
+
fileMissing = true;
|
|
603
|
+
}
|
|
517
604
|
}
|
|
518
605
|
}
|
|
519
606
|
if (isSpurious || fileMissing) {
|
|
@@ -524,10 +611,12 @@ class DevMindDatabase {
|
|
|
524
611
|
if (idsToDelete.length > 0) {
|
|
525
612
|
const updateStmt = this.db.prepare('UPDATE nodes SET deprecated = 1 WHERE id = ?');
|
|
526
613
|
const deleteConnStmt = this.db.prepare('DELETE FROM node_connections WHERE source_node_id = ? OR target_node_id = ?');
|
|
614
|
+
const deleteHistoryStmt = this.db.prepare('DELETE FROM history WHERE node_id = ?');
|
|
527
615
|
const deprecateTx = this.db.transaction((ids) => {
|
|
528
616
|
for (const id of ids) {
|
|
529
617
|
updateStmt.run(id);
|
|
530
618
|
deleteConnStmt.run(id, id);
|
|
619
|
+
deleteHistoryStmt.run(id);
|
|
531
620
|
}
|
|
532
621
|
});
|
|
533
622
|
deprecateTx(idsToDelete);
|
|
@@ -537,6 +626,263 @@ class DevMindDatabase {
|
|
|
537
626
|
prunedNodes: namesDeleted
|
|
538
627
|
};
|
|
539
628
|
}
|
|
629
|
+
populateHistoryFromDisk(row) {
|
|
630
|
+
try {
|
|
631
|
+
const historyDir = path.join(path.dirname(this.dbPath), 'history');
|
|
632
|
+
const filePath = path.join(historyDir, `${row.id}.json`);
|
|
633
|
+
if (fs.existsSync(filePath)) {
|
|
634
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
635
|
+
return {
|
|
636
|
+
...row,
|
|
637
|
+
code_snapshot: data.code_snapshot || '',
|
|
638
|
+
reasoning: typeof data.reasoning === 'string' ? data.reasoning : formatReasoning(data.reasoning || '')
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
// ignore errors
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
...row,
|
|
647
|
+
code_snapshot: '',
|
|
648
|
+
reasoning: ''
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
writeHistoryToDisk(id, nodeId, sessionId, createdAt, updatedAt, codeSnapshot, reasoning) {
|
|
652
|
+
try {
|
|
653
|
+
const historyDir = path.join(path.dirname(this.dbPath), 'history');
|
|
654
|
+
if (!fs.existsSync(historyDir)) {
|
|
655
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
656
|
+
}
|
|
657
|
+
const node = this.getNode(nodeId);
|
|
658
|
+
const nodeMetadata = node ? {
|
|
659
|
+
name: node.name,
|
|
660
|
+
type: node.type,
|
|
661
|
+
file_path: node.file_path,
|
|
662
|
+
signature: node.signature
|
|
663
|
+
} : null;
|
|
664
|
+
const data = {
|
|
665
|
+
id,
|
|
666
|
+
node_id: nodeId,
|
|
667
|
+
node_metadata: nodeMetadata,
|
|
668
|
+
session_id: sessionId,
|
|
669
|
+
created_at: createdAt,
|
|
670
|
+
updated_at: updatedAt,
|
|
671
|
+
code_snapshot: codeSnapshot,
|
|
672
|
+
reasoning
|
|
673
|
+
};
|
|
674
|
+
const filePath = path.join(historyDir, `${id}.json`);
|
|
675
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
console.warn('⚠️ SQLite warning: Failed to write history JSON to disk:', err);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
toRepoRelativePath(absolutePath) {
|
|
682
|
+
if (!absolutePath || !this.context)
|
|
683
|
+
return absolutePath;
|
|
684
|
+
const abs = path.resolve(absolutePath).replace(/\\/g, '/');
|
|
685
|
+
for (const repo of this.context.config.repos) {
|
|
686
|
+
const repoPath = (0, config_1.resolveRepoPath)(this.context, repo.name);
|
|
687
|
+
if (repoPath) {
|
|
688
|
+
const normalizedRepoPath = path.resolve(repoPath).replace(/\\/g, '/');
|
|
689
|
+
if (abs.startsWith(normalizedRepoPath)) {
|
|
690
|
+
const relative = path.relative(normalizedRepoPath, abs).replace(/\\/g, '/');
|
|
691
|
+
return `{${repo.name}}/${relative}`;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// Fallback: resolve relative to workspace root
|
|
696
|
+
const workspaceRoot = path.dirname(this.dbPath);
|
|
697
|
+
return path.relative(workspaceRoot, absolutePath).replace(/\\/g, '/');
|
|
698
|
+
}
|
|
699
|
+
toAbsolutePath(repoRelativePath) {
|
|
700
|
+
if (!repoRelativePath)
|
|
701
|
+
return repoRelativePath;
|
|
702
|
+
const workspaceRoot = path.dirname(this.dbPath);
|
|
703
|
+
const match = repoRelativePath.match(/^\{([^}]+)\}\/(.*)$/);
|
|
704
|
+
if (match && this.context) {
|
|
705
|
+
const repoName = match[1];
|
|
706
|
+
const relativePath = match[2];
|
|
707
|
+
const repoPath = (0, config_1.resolveRepoPath)(this.context, repoName);
|
|
708
|
+
if (repoPath) {
|
|
709
|
+
return path.resolve(repoPath, relativePath);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Fallback: resolve relative to workspace root
|
|
713
|
+
return path.resolve(workspaceRoot, repoRelativePath);
|
|
714
|
+
}
|
|
715
|
+
syncFromDisk() {
|
|
716
|
+
this.db.pragma('foreign_keys = OFF');
|
|
717
|
+
try {
|
|
718
|
+
const workspaceRoot = path.dirname(this.dbPath);
|
|
719
|
+
// 1. Sync History JSONs
|
|
720
|
+
const historyDir = path.join(workspaceRoot, 'history');
|
|
721
|
+
if (fs.existsSync(historyDir)) {
|
|
722
|
+
const files = fs.readdirSync(historyDir).filter(f => f.endsWith('.json'));
|
|
723
|
+
if (files.length > 0) {
|
|
724
|
+
const checkHistoryStmt = this.db.prepare('SELECT id FROM history WHERE id = ?');
|
|
725
|
+
const checkNodeStmt = this.db.prepare('SELECT id FROM nodes WHERE id = ?');
|
|
726
|
+
const insertNodeStmt = this.db.prepare(`
|
|
727
|
+
INSERT INTO nodes (id, type, name, file_path, signature, deprecated)
|
|
728
|
+
VALUES (?, ?, ?, ?, ?, 0)
|
|
729
|
+
`);
|
|
730
|
+
const insertHistoryStmt = this.db.prepare(`
|
|
731
|
+
INSERT INTO history (id, node_id, session_id, created_at, updated_at, code_snapshot, reasoning)
|
|
732
|
+
VALUES (?, ?, ?, ?, ?, '', '')
|
|
733
|
+
`);
|
|
734
|
+
const syncHistoryTx = this.db.transaction(() => {
|
|
735
|
+
for (const file of files) {
|
|
736
|
+
try {
|
|
737
|
+
const filePath = path.join(historyDir, file);
|
|
738
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
739
|
+
if (!data.id || !data.node_id)
|
|
740
|
+
continue;
|
|
741
|
+
if (checkHistoryStmt.get(data.id))
|
|
742
|
+
continue;
|
|
743
|
+
if (!checkNodeStmt.get(data.node_id) && data.node_metadata) {
|
|
744
|
+
insertNodeStmt.run(data.node_id, data.node_metadata.type, data.node_metadata.name, data.node_metadata.file_path, data.node_metadata.signature);
|
|
745
|
+
}
|
|
746
|
+
insertHistoryStmt.run(data.id, data.node_id, data.session_id, data.created_at, data.updated_at);
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
// ignore
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
syncHistoryTx();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// 2. Sync Graph JSONs
|
|
757
|
+
const graphDir = path.join(workspaceRoot, 'graph');
|
|
758
|
+
if (fs.existsSync(graphDir)) {
|
|
759
|
+
// Recursively find all JSON files in graphDir
|
|
760
|
+
const walkSync = (dir, fileList = []) => {
|
|
761
|
+
const files = fs.readdirSync(dir);
|
|
762
|
+
for (const file of files) {
|
|
763
|
+
const filePath = path.join(dir, file);
|
|
764
|
+
if (fs.statSync(filePath).isDirectory()) {
|
|
765
|
+
walkSync(filePath, fileList);
|
|
766
|
+
}
|
|
767
|
+
else if (file.endsWith('.json')) {
|
|
768
|
+
fileList.push(filePath);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return fileList;
|
|
772
|
+
};
|
|
773
|
+
const jsonFiles = walkSync(graphDir);
|
|
774
|
+
if (jsonFiles.length > 0) {
|
|
775
|
+
const deleteNodesForFileStmt = this.db.prepare('DELETE FROM nodes WHERE file_path = ? OR file_path LIKE ?');
|
|
776
|
+
const deleteConnsForNodesStmt = this.db.prepare('DELETE FROM node_connections WHERE source_node_id = ?');
|
|
777
|
+
const insertNodeStmt = this.db.prepare(`
|
|
778
|
+
INSERT OR REPLACE INTO nodes (id, type, name, file_path, signature, deprecated)
|
|
779
|
+
VALUES (?, ?, ?, ?, ?, 0)
|
|
780
|
+
`);
|
|
781
|
+
const insertConnStmt = this.db.prepare(`
|
|
782
|
+
INSERT OR IGNORE INTO node_connections (source_node_id, target_node_id)
|
|
783
|
+
VALUES (?, ?)
|
|
784
|
+
`);
|
|
785
|
+
// Transaction for fast batch syncing
|
|
786
|
+
const syncGraphTx = this.db.transaction(() => {
|
|
787
|
+
for (const file of jsonFiles) {
|
|
788
|
+
try {
|
|
789
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
790
|
+
if (!data.file_path)
|
|
791
|
+
continue;
|
|
792
|
+
const fileRelPath = data.file_path; // E.g. "{harrir-web}/app/page.tsx" or relative path
|
|
793
|
+
const fileAbsPath = this.toAbsolutePath(fileRelPath);
|
|
794
|
+
// Strip leading {repo} or ../ and normalize separators for matching
|
|
795
|
+
const cleanRelPath = fileRelPath.replace(/^\{[^}]+\}\//, '').replace(/^(\.\.\/)+/, '').replace(/\\/g, '/');
|
|
796
|
+
const fileQueryPath = `%${cleanRelPath.replace(/\//g, path.sep)}`;
|
|
797
|
+
// Clean existing nodes in SQLite for this file
|
|
798
|
+
deleteNodesForFileStmt.run(fileAbsPath, fileQueryPath);
|
|
799
|
+
// Insert nodes
|
|
800
|
+
const nodes = data.nodes || [];
|
|
801
|
+
for (const n of nodes) {
|
|
802
|
+
deleteConnsForNodesStmt.run(n.id);
|
|
803
|
+
insertNodeStmt.run(n.id, n.type, n.name, fileAbsPath, n.signature || null);
|
|
804
|
+
}
|
|
805
|
+
// Insert connections
|
|
806
|
+
const connections = data.connections || [];
|
|
807
|
+
for (const c of connections) {
|
|
808
|
+
insertConnStmt.run(c.source_node_id, c.target_node_id);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
// ignore
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
syncGraphTx();
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
catch (err) {
|
|
821
|
+
console.warn('⚠️ SQLite warning: Failed to sync from disk:', err);
|
|
822
|
+
}
|
|
823
|
+
finally {
|
|
824
|
+
this.db.pragma('foreign_keys = ON');
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
writeGraphToDisk(filePath) {
|
|
828
|
+
try {
|
|
829
|
+
if (!filePath)
|
|
830
|
+
return;
|
|
831
|
+
const workspaceRoot = path.dirname(this.dbPath);
|
|
832
|
+
// Clean/resolve the file path
|
|
833
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(workspaceRoot, filePath);
|
|
834
|
+
const relPath = path.relative(workspaceRoot, absPath).replace(/\\/g, '/');
|
|
835
|
+
const repoRelPath = this.toRepoRelativePath(absPath);
|
|
836
|
+
// E.g., "{harrir-web}/app/page.tsx" -> "graph/harrir-web/app/page.json"
|
|
837
|
+
const diskRelPath = repoRelPath.replace(/^\{([^}]+)\}/, '$1').replace(/\.[^/.]+$/, '.json');
|
|
838
|
+
const graphJsonPath = path.join(workspaceRoot, 'graph', diskRelPath);
|
|
839
|
+
// Get all active nodes in this file
|
|
840
|
+
const stmtNodes = this.db.prepare(`
|
|
841
|
+
SELECT * FROM nodes
|
|
842
|
+
WHERE deprecated = 0 AND (file_path = ? OR file_path LIKE ? OR file_path LIKE ? OR file_path LIKE ?)
|
|
843
|
+
`);
|
|
844
|
+
const nodes = stmtNodes.all(absPath, `%${relPath}%`, `%${absPath}%`, `%${relPath}`);
|
|
845
|
+
if (nodes.length === 0) {
|
|
846
|
+
// If no nodes left, delete the JSON file if it exists
|
|
847
|
+
if (fs.existsSync(graphJsonPath)) {
|
|
848
|
+
fs.unlinkSync(graphJsonPath);
|
|
849
|
+
}
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
// Collect all connections where source node is one of these nodes
|
|
853
|
+
const nodeIds = nodes.map(n => n.id);
|
|
854
|
+
const connections = [];
|
|
855
|
+
if (nodeIds.length > 0) {
|
|
856
|
+
const stmtConn = this.db.prepare(`
|
|
857
|
+
SELECT * FROM node_connections
|
|
858
|
+
WHERE source_node_id = ?
|
|
859
|
+
`);
|
|
860
|
+
for (const id of nodeIds) {
|
|
861
|
+
const conns = stmtConn.all(id);
|
|
862
|
+
connections.push(...conns);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// Format data
|
|
866
|
+
const data = {
|
|
867
|
+
file_path: repoRelPath,
|
|
868
|
+
nodes: nodes.map(n => ({
|
|
869
|
+
id: n.id,
|
|
870
|
+
name: n.name,
|
|
871
|
+
type: n.type,
|
|
872
|
+
signature: n.signature
|
|
873
|
+
})),
|
|
874
|
+
connections: connections.map(c => ({
|
|
875
|
+
source_node_id: c.source_node_id,
|
|
876
|
+
target_node_id: c.target_node_id
|
|
877
|
+
}))
|
|
878
|
+
};
|
|
879
|
+
fs.mkdirSync(path.dirname(graphJsonPath), { recursive: true });
|
|
880
|
+
fs.writeFileSync(graphJsonPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
881
|
+
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
console.warn('⚠️ SQLite warning: Failed to write graph JSON to disk:', err);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
540
886
|
}
|
|
541
887
|
exports.DevMindDatabase = DevMindDatabase;
|
|
542
888
|
//# sourceMappingURL=database.js.map
|