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.
@@ -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 stmt = this.db.prepare(`
110
- INSERT INTO nodes (id, type, name, file_path, signature)
111
- VALUES (?, ?, ?, ?, ?)
112
- ON CONFLICT(id) DO UPDATE SET
113
- type = excluded.type,
114
- name = excluded.name,
115
- file_path = excluded.file_path,
116
- signature = COALESCE(excluded.signature, nodes.signature),
117
- deprecated = 0
118
- `);
119
- stmt.run(node.id, node.type, node.name, node.file_path, node.signature || null);
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
- return stmt.get(id) || null;
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(id);
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(id);
134
- deleteConnStmt.run(id, id);
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(sourceNodeId, targetNodeId);
181
- }
182
- catch (err) {
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
- throw err;
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(sourceNodeId, targetNodeId);
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(nodeId),
210
- usedBy: usedByStmt.all(nodeId)
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 * FROM history
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(nodeId);
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(nodeId);
303
+ return stmt.all(resolvedId);
238
304
  }
239
305
  getHistoryEntry(id) {
240
- const stmt = this.db.prepare('SELECT * FROM history WHERE id = ?');
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(nodeId);
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 stmt = this.db.prepare(`
266
- SELECT code_snapshot, updated_at
267
- FROM history
268
- WHERE node_id = ?
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: row.updated_at,
277
- code_snapshot: decompressText(row.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(node_id);
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 = ?, reasoning = ?, updated_at = ?
421
+ SET code_snapshot = '', reasoning = '', updated_at = ?
365
422
  WHERE id = ?
366
423
  `);
367
- updateStmt.run(compressedCode, formattedReasoning, nowStr, latest.id);
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, node_id, sessionId, nowStr, nowStr, compressedCode, formattedReasoning);
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
- // SQLite datetime('now', '-24 hours') style modifier
414
- return stmt.all(`-${hours} hours`);
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 0 history entries that are not already deprecated
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 AND id NOT IN (SELECT DISTINCT node_id FROM history)
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 resolvedPath = path.isAbsolute(node.file_path)
513
- ? node.file_path
514
- : path.resolve(workspaceRoot, node.file_path);
515
- if (!fs.existsSync(resolvedPath)) {
516
- fileMissing = true;
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