devsmind-mcp 1.2.2 → 2.0.1

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);
@@ -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
- 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;
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(id);
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(id);
150
- deleteConnStmt.run(id, id);
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(sourceNodeId, targetNodeId);
197
- }
198
- catch (err) {
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
- throw err;
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(sourceNodeId, targetNodeId);
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(nodeId),
226
- usedBy: usedByStmt.all(nodeId)
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 * FROM history
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(nodeId);
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(nodeId);
303
+ return stmt.all(resolvedId);
254
304
  }
255
305
  getHistoryEntry(id) {
256
- 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 = ?');
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(nodeId);
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 stmt = this.db.prepare(`
282
- SELECT code_snapshot, updated_at
283
- FROM history
284
- WHERE node_id = ?
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: row.updated_at,
293
- code_snapshot: decompressText(row.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(node_id);
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 = ?, reasoning = ?, updated_at = ?
421
+ SET code_snapshot = '', reasoning = '', updated_at = ?
381
422
  WHERE id = ?
382
423
  `);
383
- 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);
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, 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);
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 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
533
579
  const stmt = this.db.prepare(`
534
580
  SELECT id, name, file_path FROM nodes
535
- WHERE deprecated = 0 AND id NOT IN (SELECT DISTINCT node_id FROM history)
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