devsmind-mcp 1.2.2 → 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/README.md +55 -47
- package/dist/cli/runner.js +7 -3
- package/dist/cli/runner.js.map +1 -1
- package/dist/db/database.d.ts +8 -0
- package/dist/db/database.js +360 -55
- package/dist/db/database.js.map +1 -1
- package/dist/mcp/server.js +16 -6
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
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);
|
|
@@ -133,23 +146,44 @@ class DevMindDatabase {
|
|
|
133
146
|
`);
|
|
134
147
|
stmt.run(node.id, node.type, node.name, node.file_path, node.signature || null);
|
|
135
148
|
}
|
|
149
|
+
this.writeGraphToDisk(node.file_path);
|
|
136
150
|
}
|
|
137
151
|
getNode(id) {
|
|
138
152
|
const stmt = this.db.prepare('SELECT * FROM nodes WHERE id = ?');
|
|
139
|
-
|
|
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;
|
|
140
164
|
}
|
|
141
165
|
deleteNode(id) {
|
|
166
|
+
const node = this.getNode(id);
|
|
167
|
+
const resolvedId = node ? node.id : id;
|
|
142
168
|
const stmt = this.db.prepare('DELETE FROM nodes WHERE id = ?');
|
|
143
|
-
stmt.run(
|
|
169
|
+
stmt.run(resolvedId);
|
|
170
|
+
if (node && node.file_path) {
|
|
171
|
+
this.writeGraphToDisk(node.file_path);
|
|
172
|
+
}
|
|
144
173
|
}
|
|
145
174
|
deprecateNode(id) {
|
|
175
|
+
const node = this.getNode(id);
|
|
176
|
+
const resolvedId = node ? node.id : id;
|
|
146
177
|
const updateStmt = this.db.prepare('UPDATE nodes SET deprecated = 1 WHERE id = ?');
|
|
147
178
|
const deleteConnStmt = this.db.prepare('DELETE FROM node_connections WHERE source_node_id = ? OR target_node_id = ?');
|
|
148
179
|
const tx = this.db.transaction(() => {
|
|
149
|
-
updateStmt.run(
|
|
150
|
-
deleteConnStmt.run(
|
|
180
|
+
updateStmt.run(resolvedId);
|
|
181
|
+
deleteConnStmt.run(resolvedId, resolvedId);
|
|
151
182
|
});
|
|
152
183
|
tx();
|
|
184
|
+
if (node && node.file_path) {
|
|
185
|
+
this.writeGraphToDisk(node.file_path);
|
|
186
|
+
}
|
|
153
187
|
}
|
|
154
188
|
renameNode(oldId, newId, newName) {
|
|
155
189
|
const node = this.getNode(oldId);
|
|
@@ -181,6 +215,9 @@ class DevMindDatabase {
|
|
|
181
215
|
deleteOldStmt.run(oldId);
|
|
182
216
|
});
|
|
183
217
|
runTx();
|
|
218
|
+
if (node.file_path) {
|
|
219
|
+
this.writeGraphToDisk(node.file_path);
|
|
220
|
+
}
|
|
184
221
|
}
|
|
185
222
|
finally {
|
|
186
223
|
this.db.pragma('foreign_keys = ON');
|
|
@@ -188,29 +225,42 @@ class DevMindDatabase {
|
|
|
188
225
|
}
|
|
189
226
|
// --- Connection Operations ---
|
|
190
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');
|
|
191
233
|
try {
|
|
192
234
|
const stmt = this.db.prepare(`
|
|
193
235
|
INSERT OR IGNORE INTO node_connections (source_node_id, target_node_id)
|
|
194
236
|
VALUES (?, ?)
|
|
195
237
|
`);
|
|
196
|
-
stmt.run(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (err instanceof Error && err.message.includes('FOREIGN KEY')) {
|
|
200
|
-
// Ignore foreign key violations (e.g. target node defined in a file not indexed yet, or external library)
|
|
201
|
-
return;
|
|
238
|
+
stmt.run(resolvedSrc, resolvedTgt);
|
|
239
|
+
if (srcNode && srcNode.file_path) {
|
|
240
|
+
this.writeGraphToDisk(srcNode.file_path);
|
|
202
241
|
}
|
|
203
|
-
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
this.db.pragma('foreign_keys = ON');
|
|
204
245
|
}
|
|
205
246
|
}
|
|
206
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;
|
|
207
252
|
const stmt = this.db.prepare(`
|
|
208
253
|
DELETE FROM node_connections
|
|
209
254
|
WHERE source_node_id = ? AND target_node_id = ?
|
|
210
255
|
`);
|
|
211
|
-
stmt.run(
|
|
256
|
+
stmt.run(resolvedSrc, resolvedTgt);
|
|
257
|
+
if (srcNode && srcNode.file_path) {
|
|
258
|
+
this.writeGraphToDisk(srcNode.file_path);
|
|
259
|
+
}
|
|
212
260
|
}
|
|
213
261
|
getConnections(nodeId) {
|
|
262
|
+
const node = this.getNode(nodeId);
|
|
263
|
+
const resolvedId = node ? node.id : nodeId;
|
|
214
264
|
const usesStmt = this.db.prepare(`
|
|
215
265
|
SELECT n.* FROM nodes n
|
|
216
266
|
JOIN node_connections c ON n.id = c.target_node_id
|
|
@@ -222,75 +272,64 @@ class DevMindDatabase {
|
|
|
222
272
|
WHERE c.target_node_id = ?
|
|
223
273
|
`);
|
|
224
274
|
return {
|
|
225
|
-
uses: usesStmt.all(
|
|
226
|
-
usedBy: usedByStmt.all(
|
|
275
|
+
uses: usesStmt.all(resolvedId),
|
|
276
|
+
usedBy: usedByStmt.all(resolvedId)
|
|
227
277
|
};
|
|
228
278
|
}
|
|
229
279
|
// --- History Operations ---
|
|
230
280
|
getLatestHistory(nodeId) {
|
|
281
|
+
const node = this.getNode(nodeId);
|
|
282
|
+
const resolvedId = node ? node.id : nodeId;
|
|
231
283
|
const stmt = this.db.prepare(`
|
|
232
|
-
SELECT
|
|
284
|
+
SELECT id, node_id, session_id, created_at, updated_at FROM history
|
|
233
285
|
WHERE node_id = ?
|
|
234
286
|
ORDER BY updated_at DESC
|
|
235
287
|
LIMIT 1
|
|
236
288
|
`);
|
|
237
|
-
const row = stmt.get(
|
|
289
|
+
const row = stmt.get(resolvedId);
|
|
238
290
|
if (!row)
|
|
239
291
|
return null;
|
|
240
|
-
return
|
|
241
|
-
...row,
|
|
242
|
-
code_snapshot: decompressText(row.code_snapshot),
|
|
243
|
-
reasoning: decompressText(row.reasoning)
|
|
244
|
-
};
|
|
292
|
+
return this.populateHistoryFromDisk(row);
|
|
245
293
|
}
|
|
246
294
|
listHistory(nodeId) {
|
|
295
|
+
const node = this.getNode(nodeId);
|
|
296
|
+
const resolvedId = node ? node.id : nodeId;
|
|
247
297
|
const stmt = this.db.prepare(`
|
|
248
298
|
SELECT id, node_id, session_id, created_at, updated_at
|
|
249
299
|
FROM history
|
|
250
300
|
WHERE node_id = ?
|
|
251
301
|
ORDER BY updated_at DESC
|
|
252
302
|
`);
|
|
253
|
-
return stmt.all(
|
|
303
|
+
return stmt.all(resolvedId);
|
|
254
304
|
}
|
|
255
305
|
getHistoryEntry(id) {
|
|
256
|
-
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 = ?');
|
|
257
307
|
const row = stmt.get(id);
|
|
258
308
|
if (!row)
|
|
259
309
|
return null;
|
|
260
|
-
return
|
|
261
|
-
...row,
|
|
262
|
-
code_snapshot: decompressText(row.code_snapshot),
|
|
263
|
-
reasoning: decompressText(row.reasoning)
|
|
264
|
-
};
|
|
310
|
+
return this.populateHistoryFromDisk(row);
|
|
265
311
|
}
|
|
266
312
|
getFullHistory(nodeId) {
|
|
313
|
+
const node = this.getNode(nodeId);
|
|
314
|
+
const resolvedId = node ? node.id : nodeId;
|
|
267
315
|
const stmt = this.db.prepare(`
|
|
268
|
-
SELECT
|
|
316
|
+
SELECT id, node_id, session_id, created_at, updated_at
|
|
269
317
|
FROM history
|
|
270
318
|
WHERE node_id = ?
|
|
271
319
|
ORDER BY updated_at DESC
|
|
272
320
|
`);
|
|
273
|
-
const rows = stmt.all(
|
|
274
|
-
return rows.map(row => (
|
|
275
|
-
...row,
|
|
276
|
-
code_snapshot: decompressText(row.code_snapshot),
|
|
277
|
-
reasoning: decompressText(row.reasoning)
|
|
278
|
-
}));
|
|
321
|
+
const rows = stmt.all(resolvedId);
|
|
322
|
+
return rows.map(row => this.populateHistoryFromDisk(row));
|
|
279
323
|
}
|
|
280
324
|
getLatestCode(nodeId) {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
ORDER BY updated_at DESC
|
|
286
|
-
LIMIT 1
|
|
287
|
-
`);
|
|
288
|
-
const row = stmt.get(nodeId);
|
|
289
|
-
if (!row)
|
|
325
|
+
const node = this.getNode(nodeId);
|
|
326
|
+
const resolvedId = node ? node.id : nodeId;
|
|
327
|
+
const history = this.getLatestHistory(resolvedId);
|
|
328
|
+
if (!history)
|
|
290
329
|
return null;
|
|
291
330
|
return {
|
|
292
|
-
updated_at:
|
|
293
|
-
code_snapshot:
|
|
331
|
+
updated_at: history.updated_at,
|
|
332
|
+
code_snapshot: history.code_snapshot
|
|
294
333
|
};
|
|
295
334
|
}
|
|
296
335
|
getGraph(nodeId, maxDepth = 6) {
|
|
@@ -364,11 +403,13 @@ class DevMindDatabase {
|
|
|
364
403
|
}
|
|
365
404
|
updateHistory(params) {
|
|
366
405
|
const { node_id, code_snapshot, reasoning } = params;
|
|
406
|
+
const node = this.getNode(node_id);
|
|
407
|
+
const resolvedId = node ? node.id : node_id;
|
|
367
408
|
const formattedReasoning = formatReasoning(reasoning);
|
|
368
409
|
const nowStr = new Date().toISOString();
|
|
369
410
|
const compressedCode = compressText(code_snapshot);
|
|
370
411
|
// 1-hour session boundary rule check
|
|
371
|
-
const latest = this.getLatestHistory(
|
|
412
|
+
const latest = this.getLatestHistory(resolvedId);
|
|
372
413
|
if (latest) {
|
|
373
414
|
const lastUpdate = new Date(latest.updated_at).getTime();
|
|
374
415
|
const nowTime = new Date(nowStr).getTime();
|
|
@@ -377,10 +418,12 @@ class DevMindDatabase {
|
|
|
377
418
|
if (diffMs < 3600000) {
|
|
378
419
|
const updateStmt = this.db.prepare(`
|
|
379
420
|
UPDATE history
|
|
380
|
-
SET code_snapshot =
|
|
421
|
+
SET code_snapshot = '', reasoning = '', updated_at = ?
|
|
381
422
|
WHERE id = ?
|
|
382
423
|
`);
|
|
383
|
-
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);
|
|
384
427
|
return {
|
|
385
428
|
...latest,
|
|
386
429
|
code_snapshot,
|
|
@@ -394,12 +437,14 @@ class DevMindDatabase {
|
|
|
394
437
|
const sessionId = params.session_id || crypto.randomUUID();
|
|
395
438
|
const insertStmt = this.db.prepare(`
|
|
396
439
|
INSERT INTO history (id, node_id, session_id, created_at, updated_at, code_snapshot, reasoning)
|
|
397
|
-
VALUES (?, ?, ?, ?, ?,
|
|
440
|
+
VALUES (?, ?, ?, ?, ?, '', '')
|
|
398
441
|
`);
|
|
399
|
-
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);
|
|
400
445
|
return {
|
|
401
446
|
id: newId,
|
|
402
|
-
node_id,
|
|
447
|
+
node_id: resolvedId,
|
|
403
448
|
session_id: sessionId,
|
|
404
449
|
created_at: nowStr,
|
|
405
450
|
updated_at: nowStr,
|
|
@@ -527,12 +572,13 @@ class DevMindDatabase {
|
|
|
527
572
|
pruneSpuriousNodes(workspaceRoot) {
|
|
528
573
|
const spuriousNames = new Set([
|
|
529
574
|
'promise', 'map', 'set', 'json', 'console', 'error', 'object', 'function', 'array', 'string', 'number', 'boolean', 'regexp', 'date', 'math',
|
|
530
|
-
'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'
|
|
531
577
|
]);
|
|
532
|
-
// Get nodes with
|
|
578
|
+
// Get all active nodes (including those with history) to check for missing files or spurious names
|
|
533
579
|
const stmt = this.db.prepare(`
|
|
534
580
|
SELECT id, name, file_path FROM nodes
|
|
535
|
-
WHERE deprecated = 0
|
|
581
|
+
WHERE deprecated = 0
|
|
536
582
|
`);
|
|
537
583
|
const candidates = stmt.all();
|
|
538
584
|
const idsToDelete = [];
|
|
@@ -565,10 +611,12 @@ class DevMindDatabase {
|
|
|
565
611
|
if (idsToDelete.length > 0) {
|
|
566
612
|
const updateStmt = this.db.prepare('UPDATE nodes SET deprecated = 1 WHERE id = ?');
|
|
567
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 = ?');
|
|
568
615
|
const deprecateTx = this.db.transaction((ids) => {
|
|
569
616
|
for (const id of ids) {
|
|
570
617
|
updateStmt.run(id);
|
|
571
618
|
deleteConnStmt.run(id, id);
|
|
619
|
+
deleteHistoryStmt.run(id);
|
|
572
620
|
}
|
|
573
621
|
});
|
|
574
622
|
deprecateTx(idsToDelete);
|
|
@@ -578,6 +626,263 @@ class DevMindDatabase {
|
|
|
578
626
|
prunedNodes: namesDeleted
|
|
579
627
|
};
|
|
580
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
|
+
}
|
|
581
886
|
}
|
|
582
887
|
exports.DevMindDatabase = DevMindDatabase;
|
|
583
888
|
//# sourceMappingURL=database.js.map
|