neuronlayer 0.1.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.
Files changed (78) hide show
  1. package/CONTRIBUTING.md +127 -0
  2. package/LICENSE +21 -0
  3. package/README.md +305 -0
  4. package/dist/index.js +38016 -0
  5. package/esbuild.config.js +26 -0
  6. package/package.json +63 -0
  7. package/src/cli/commands.ts +382 -0
  8. package/src/core/adr-exporter.ts +253 -0
  9. package/src/core/architecture/architecture-enforcement.ts +228 -0
  10. package/src/core/architecture/duplicate-detector.ts +288 -0
  11. package/src/core/architecture/index.ts +6 -0
  12. package/src/core/architecture/pattern-learner.ts +306 -0
  13. package/src/core/architecture/pattern-library.ts +403 -0
  14. package/src/core/architecture/pattern-validator.ts +324 -0
  15. package/src/core/change-intelligence/bug-correlator.ts +444 -0
  16. package/src/core/change-intelligence/change-intelligence.ts +221 -0
  17. package/src/core/change-intelligence/change-tracker.ts +334 -0
  18. package/src/core/change-intelligence/fix-suggester.ts +340 -0
  19. package/src/core/change-intelligence/index.ts +5 -0
  20. package/src/core/code-verifier.ts +843 -0
  21. package/src/core/confidence/confidence-scorer.ts +251 -0
  22. package/src/core/confidence/conflict-checker.ts +289 -0
  23. package/src/core/confidence/index.ts +5 -0
  24. package/src/core/confidence/source-tracker.ts +263 -0
  25. package/src/core/confidence/warning-detector.ts +241 -0
  26. package/src/core/context-rot/compaction.ts +284 -0
  27. package/src/core/context-rot/context-health.ts +243 -0
  28. package/src/core/context-rot/context-rot-prevention.ts +213 -0
  29. package/src/core/context-rot/critical-context.ts +221 -0
  30. package/src/core/context-rot/drift-detector.ts +255 -0
  31. package/src/core/context-rot/index.ts +7 -0
  32. package/src/core/context.ts +263 -0
  33. package/src/core/decision-extractor.ts +339 -0
  34. package/src/core/decisions.ts +69 -0
  35. package/src/core/deja-vu.ts +421 -0
  36. package/src/core/engine.ts +1455 -0
  37. package/src/core/feature-context.ts +726 -0
  38. package/src/core/ghost-mode.ts +412 -0
  39. package/src/core/learning.ts +485 -0
  40. package/src/core/living-docs/activity-tracker.ts +296 -0
  41. package/src/core/living-docs/architecture-generator.ts +428 -0
  42. package/src/core/living-docs/changelog-generator.ts +348 -0
  43. package/src/core/living-docs/component-generator.ts +230 -0
  44. package/src/core/living-docs/doc-engine.ts +110 -0
  45. package/src/core/living-docs/doc-validator.ts +282 -0
  46. package/src/core/living-docs/index.ts +8 -0
  47. package/src/core/project-manager.ts +297 -0
  48. package/src/core/summarizer.ts +267 -0
  49. package/src/core/test-awareness/change-validator.ts +499 -0
  50. package/src/core/test-awareness/index.ts +5 -0
  51. package/src/index.ts +49 -0
  52. package/src/indexing/ast.ts +563 -0
  53. package/src/indexing/embeddings.ts +85 -0
  54. package/src/indexing/indexer.ts +245 -0
  55. package/src/indexing/watcher.ts +78 -0
  56. package/src/server/gateways/aggregator.ts +374 -0
  57. package/src/server/gateways/index.ts +473 -0
  58. package/src/server/gateways/memory-ghost.ts +343 -0
  59. package/src/server/gateways/memory-query.ts +452 -0
  60. package/src/server/gateways/memory-record.ts +346 -0
  61. package/src/server/gateways/memory-review.ts +410 -0
  62. package/src/server/gateways/memory-status.ts +517 -0
  63. package/src/server/gateways/memory-verify.ts +392 -0
  64. package/src/server/gateways/router.ts +434 -0
  65. package/src/server/gateways/types.ts +610 -0
  66. package/src/server/mcp.ts +154 -0
  67. package/src/server/resources.ts +85 -0
  68. package/src/server/tools.ts +2261 -0
  69. package/src/storage/database.ts +262 -0
  70. package/src/storage/tier1.ts +135 -0
  71. package/src/storage/tier2.ts +764 -0
  72. package/src/storage/tier3.ts +123 -0
  73. package/src/types/documentation.ts +619 -0
  74. package/src/types/index.ts +222 -0
  75. package/src/utils/config.ts +193 -0
  76. package/src/utils/files.ts +117 -0
  77. package/src/utils/time.ts +37 -0
  78. package/src/utils/tokens.ts +52 -0
@@ -0,0 +1,282 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import type Database from 'better-sqlite3';
4
+ import type { Tier2Storage } from '../../storage/tier2.js';
5
+ import type { CodeSymbol } from '../../types/index.js';
6
+ import type {
7
+ ValidationResult,
8
+ OutdatedDoc,
9
+ UndocumentedItem,
10
+ DocSuggestion
11
+ } from '../../types/documentation.js';
12
+
13
+ export class DocValidator {
14
+ private projectPath: string;
15
+ private tier2: Tier2Storage;
16
+ private db: Database.Database;
17
+
18
+ constructor(projectPath: string, tier2: Tier2Storage, db: Database.Database) {
19
+ this.projectPath = projectPath;
20
+ this.tier2 = tier2;
21
+ this.db = db;
22
+ }
23
+
24
+ async validate(): Promise<ValidationResult> {
25
+ const outdated = await this.findOutdated();
26
+ const undocumented = await this.findUndocumented();
27
+ const suggestions = this.generateSuggestions(outdated, undocumented);
28
+ const score = this.calculateScore(outdated, undocumented);
29
+
30
+ return {
31
+ isValid: score >= 70,
32
+ outdatedDocs: outdated,
33
+ undocumentedCode: undocumented,
34
+ suggestions,
35
+ score
36
+ };
37
+ }
38
+
39
+ async findOutdated(): Promise<OutdatedDoc[]> {
40
+ const outdated: OutdatedDoc[] = [];
41
+
42
+ try {
43
+ // Get all documentation records
44
+ const stmt = this.db.prepare(`
45
+ SELECT d.file_id, d.generated_at, f.path, f.last_modified
46
+ FROM documentation d
47
+ JOIN files f ON d.file_id = f.id
48
+ `);
49
+
50
+ const rows = stmt.all() as Array<{
51
+ file_id: number;
52
+ generated_at: number;
53
+ path: string;
54
+ last_modified: number;
55
+ }>;
56
+
57
+ for (const row of rows) {
58
+ // Check if code changed after docs were generated
59
+ if (row.last_modified > row.generated_at) {
60
+ const daysSinceUpdate = Math.floor(
61
+ (Date.now() / 1000 - row.generated_at) / (24 * 60 * 60)
62
+ );
63
+
64
+ outdated.push({
65
+ file: row.path,
66
+ reason: 'Code modified after documentation was generated',
67
+ lastDocUpdate: new Date(row.generated_at * 1000),
68
+ lastCodeChange: new Date(row.last_modified * 1000),
69
+ severity: daysSinceUpdate > 30 ? 'high' : daysSinceUpdate > 7 ? 'medium' : 'low'
70
+ });
71
+ }
72
+ }
73
+ } catch {
74
+ // Documentation table might not exist yet
75
+ }
76
+
77
+ return outdated;
78
+ }
79
+
80
+ async findUndocumented(options?: {
81
+ importance?: 'low' | 'medium' | 'high' | 'all';
82
+ type?: 'file' | 'function' | 'class' | 'interface' | 'all';
83
+ }): Promise<UndocumentedItem[]> {
84
+ const items: UndocumentedItem[] = [];
85
+ const files = this.tier2.getAllFiles();
86
+
87
+ for (const file of files) {
88
+ const symbols = this.tier2.getSymbolsByFile(file.id);
89
+
90
+ // Check for undocumented exported symbols
91
+ const exportedSymbols = symbols.filter(s => s.exported);
92
+
93
+ for (const symbol of exportedSymbols) {
94
+ if (!symbol.docstring || symbol.docstring.trim().length === 0) {
95
+ const importance = this.calculateImportance(file.path, symbol);
96
+
97
+ // Apply filters
98
+ if (options?.importance && options.importance !== 'all' && importance !== options.importance) {
99
+ continue;
100
+ }
101
+
102
+ const symbolType = this.mapSymbolKindToType(symbol.kind);
103
+ if (options?.type && options.type !== 'all' && symbolType !== options.type) {
104
+ continue;
105
+ }
106
+
107
+ items.push({
108
+ file: file.path,
109
+ symbol: symbol.name,
110
+ type: symbolType,
111
+ importance
112
+ });
113
+ }
114
+ }
115
+
116
+ // Check for files without any documentation
117
+ if (exportedSymbols.length === 0 && symbols.length > 0) {
118
+ // File has symbols but none exported - might be internal
119
+ continue;
120
+ }
121
+
122
+ // Check if the file has any JSDoc/docstring at file level
123
+ const hasFileDoc = symbols.some(s => s.docstring && s.lineStart <= 5);
124
+ if (!hasFileDoc && exportedSymbols.length > 0) {
125
+ const importance = this.calculateFileImportance(file.path);
126
+
127
+ if (options?.importance && options.importance !== 'all' && importance !== options.importance) {
128
+ continue;
129
+ }
130
+ if (options?.type && options.type !== 'all' && options.type !== 'file') {
131
+ continue;
132
+ }
133
+
134
+ // Only add if the file isn't already represented by undocumented symbols
135
+ const hasSymbolEntry = items.some(i => i.file === file.path);
136
+ if (!hasSymbolEntry) {
137
+ items.push({
138
+ file: file.path,
139
+ type: 'file',
140
+ importance
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ // Sort by importance
147
+ const importanceOrder = { high: 0, medium: 1, low: 2 };
148
+ items.sort((a, b) => importanceOrder[a.importance] - importanceOrder[b.importance]);
149
+
150
+ return items;
151
+ }
152
+
153
+ private calculateImportance(filePath: string, symbol: CodeSymbol): 'low' | 'medium' | 'high' {
154
+ // Check how many files depend on this file
155
+ const dependents = this.tier2.getFileDependents(filePath);
156
+
157
+ // High importance: many dependents, or it's a class/interface
158
+ if (dependents.length >= 3 || symbol.kind === 'class' || symbol.kind === 'interface') {
159
+ return 'high';
160
+ }
161
+
162
+ // Medium importance: some dependents, or it's a function
163
+ if (dependents.length >= 1 || symbol.kind === 'function') {
164
+ return 'medium';
165
+ }
166
+
167
+ return 'low';
168
+ }
169
+
170
+ private calculateFileImportance(filePath: string): 'low' | 'medium' | 'high' {
171
+ const dependents = this.tier2.getFileDependents(filePath);
172
+
173
+ // Check if it's an index/entry file
174
+ if (filePath.includes('index.') || filePath.includes('/src/')) {
175
+ return 'high';
176
+ }
177
+
178
+ if (dependents.length >= 5) return 'high';
179
+ if (dependents.length >= 2) return 'medium';
180
+ return 'low';
181
+ }
182
+
183
+ private mapSymbolKindToType(kind: string): 'file' | 'function' | 'class' | 'interface' {
184
+ switch (kind) {
185
+ case 'class':
186
+ return 'class';
187
+ case 'interface':
188
+ case 'type':
189
+ return 'interface';
190
+ case 'function':
191
+ case 'method':
192
+ return 'function';
193
+ default:
194
+ return 'function';
195
+ }
196
+ }
197
+
198
+ private generateSuggestions(
199
+ outdated: OutdatedDoc[],
200
+ undocumented: UndocumentedItem[]
201
+ ): DocSuggestion[] {
202
+ const suggestions: DocSuggestion[] = [];
203
+
204
+ // Suggestions for outdated docs
205
+ for (const doc of outdated) {
206
+ suggestions.push({
207
+ file: doc.file,
208
+ suggestion: `Update documentation - code changed ${this.formatTimeDiff(doc.lastCodeChange, doc.lastDocUpdate)} after docs`,
209
+ priority: doc.severity
210
+ });
211
+ }
212
+
213
+ // Suggestions for undocumented code
214
+ const highPriorityUndoc = undocumented.filter(u => u.importance === 'high');
215
+ for (const item of highPriorityUndoc) {
216
+ suggestions.push({
217
+ file: item.file,
218
+ suggestion: item.symbol
219
+ ? `Add documentation for exported ${item.type} '${item.symbol}'`
220
+ : `Add file-level documentation`,
221
+ priority: 'high'
222
+ });
223
+ }
224
+
225
+ // Group medium priority suggestions
226
+ const mediumPriorityCount = undocumented.filter(u => u.importance === 'medium').length;
227
+ if (mediumPriorityCount > 0) {
228
+ const files = [...new Set(undocumented.filter(u => u.importance === 'medium').map(u => u.file))];
229
+ if (files.length <= 3) {
230
+ for (const file of files) {
231
+ suggestions.push({
232
+ file,
233
+ suggestion: 'Add documentation for exported symbols',
234
+ priority: 'medium'
235
+ });
236
+ }
237
+ } else {
238
+ suggestions.push({
239
+ file: files[0]!,
240
+ suggestion: `${mediumPriorityCount} symbols across ${files.length} files need documentation`,
241
+ priority: 'medium'
242
+ });
243
+ }
244
+ }
245
+
246
+ return suggestions;
247
+ }
248
+
249
+ private formatTimeDiff(later: Date, earlier: Date): string {
250
+ const diffMs = later.getTime() - earlier.getTime();
251
+ const diffDays = Math.floor(diffMs / (24 * 60 * 60 * 1000));
252
+
253
+ if (diffDays === 0) return 'today';
254
+ if (diffDays === 1) return '1 day';
255
+ if (diffDays < 7) return `${diffDays} days`;
256
+ if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks`;
257
+ return `${Math.floor(diffDays / 30)} months`;
258
+ }
259
+
260
+ private calculateScore(outdated: OutdatedDoc[], undocumented: UndocumentedItem[]): number {
261
+ const totalFiles = this.tier2.getFileCount();
262
+ if (totalFiles === 0) return 100;
263
+
264
+ // Calculate based on:
265
+ // - % of files with outdated docs (30% weight)
266
+ // - % of high-importance items undocumented (50% weight)
267
+ // - % of medium-importance items undocumented (20% weight)
268
+
269
+ const outdatedPenalty = (outdated.length / totalFiles) * 30;
270
+
271
+ const highUndoc = undocumented.filter(u => u.importance === 'high').length;
272
+ const mediumUndoc = undocumented.filter(u => u.importance === 'medium').length;
273
+
274
+ // Assume each file has ~3 documentable items on average
275
+ const estimatedTotalItems = totalFiles * 3;
276
+ const highPenalty = estimatedTotalItems > 0 ? (highUndoc / estimatedTotalItems) * 50 : 0;
277
+ const mediumPenalty = estimatedTotalItems > 0 ? (mediumUndoc / estimatedTotalItems) * 20 : 0;
278
+
279
+ const totalPenalty = Math.min(100, outdatedPenalty + highPenalty + mediumPenalty);
280
+ return Math.round(100 - totalPenalty);
281
+ }
282
+ }
@@ -0,0 +1,8 @@
1
+ // Living Documentation Module - Barrel Export
2
+
3
+ export { LivingDocumentationEngine } from './doc-engine.js';
4
+ export { ArchitectureGenerator } from './architecture-generator.js';
5
+ export { ComponentGenerator } from './component-generator.js';
6
+ export { ChangelogGenerator } from './changelog-generator.js';
7
+ export { DocValidator } from './doc-validator.js';
8
+ export { ActivityTracker } from './activity-tracker.js';
@@ -0,0 +1,297 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
2
+ import { join, basename, resolve } from 'path';
3
+ import { createHash } from 'crypto';
4
+ import { homedir } from 'os';
5
+ import Database from 'better-sqlite3';
6
+ import type { MemoryLayerConfig } from '../types/index.js';
7
+
8
+ export interface ProjectInfo {
9
+ id: string;
10
+ name: string;
11
+ path: string;
12
+ dataDir: string;
13
+ lastAccessed: number;
14
+ totalFiles: number;
15
+ totalDecisions: number;
16
+ languages: string[];
17
+ }
18
+
19
+ export interface ProjectRegistry {
20
+ version: number;
21
+ activeProject: string | null;
22
+ projects: Record<string, ProjectInfo>;
23
+ }
24
+
25
+ export class ProjectManager {
26
+ private registryPath: string;
27
+ private registry: ProjectRegistry;
28
+ private baseDataDir: string;
29
+
30
+ constructor() {
31
+ this.baseDataDir = join(homedir(), '.memorylayer');
32
+ this.registryPath = join(this.baseDataDir, 'registry.json');
33
+
34
+ // Ensure base directory exists
35
+ if (!existsSync(this.baseDataDir)) {
36
+ mkdirSync(this.baseDataDir, { recursive: true });
37
+ }
38
+
39
+ this.registry = this.loadRegistry();
40
+ }
41
+
42
+ private loadRegistry(): ProjectRegistry {
43
+ try {
44
+ if (existsSync(this.registryPath)) {
45
+ const data = JSON.parse(readFileSync(this.registryPath, 'utf-8'));
46
+ return data;
47
+ }
48
+ } catch (error) {
49
+ console.error('Error loading registry:', error);
50
+ }
51
+
52
+ return {
53
+ version: 1,
54
+ activeProject: null,
55
+ projects: {}
56
+ };
57
+ }
58
+
59
+ private saveRegistry(): void {
60
+ try {
61
+ writeFileSync(this.registryPath, JSON.stringify(this.registry, null, 2));
62
+ } catch (error) {
63
+ console.error('Error saving registry:', error);
64
+ }
65
+ }
66
+
67
+ private generateProjectId(projectPath: string): string {
68
+ const normalizedPath = resolve(projectPath);
69
+ return createHash('md5').update(normalizedPath).digest('hex').slice(0, 12);
70
+ }
71
+
72
+ private getProjectDataDir(projectPath: string): string {
73
+ const projectId = this.generateProjectId(projectPath);
74
+ const projectName = basename(projectPath);
75
+ return join(this.baseDataDir, 'projects', `${projectName}-${projectId}`);
76
+ }
77
+
78
+ // Register a new project or update existing
79
+ registerProject(projectPath: string): ProjectInfo {
80
+ const normalizedPath = resolve(projectPath);
81
+ const projectId = this.generateProjectId(normalizedPath);
82
+ const dataDir = this.getProjectDataDir(normalizedPath);
83
+
84
+ // Check if project directory exists
85
+ if (!existsSync(normalizedPath)) {
86
+ throw new Error(`Project path does not exist: ${normalizedPath}`);
87
+ }
88
+
89
+ const existingProject = this.registry.projects[projectId];
90
+
91
+ const projectInfo: ProjectInfo = {
92
+ id: projectId,
93
+ name: basename(normalizedPath),
94
+ path: normalizedPath,
95
+ dataDir,
96
+ lastAccessed: Date.now(),
97
+ totalFiles: existingProject?.totalFiles || 0,
98
+ totalDecisions: existingProject?.totalDecisions || 0,
99
+ languages: existingProject?.languages || []
100
+ };
101
+
102
+ this.registry.projects[projectId] = projectInfo;
103
+ this.saveRegistry();
104
+
105
+ return projectInfo;
106
+ }
107
+
108
+ // Remove a project from registry
109
+ removeProject(projectId: string): boolean {
110
+ if (!this.registry.projects[projectId]) {
111
+ return false;
112
+ }
113
+
114
+ delete this.registry.projects[projectId];
115
+
116
+ if (this.registry.activeProject === projectId) {
117
+ this.registry.activeProject = null;
118
+ }
119
+
120
+ this.saveRegistry();
121
+ return true;
122
+ }
123
+
124
+ // Get all registered projects
125
+ listProjects(): ProjectInfo[] {
126
+ return Object.values(this.registry.projects)
127
+ .sort((a, b) => b.lastAccessed - a.lastAccessed);
128
+ }
129
+
130
+ // Get a specific project
131
+ getProject(projectId: string): ProjectInfo | null {
132
+ return this.registry.projects[projectId] || null;
133
+ }
134
+
135
+ // Get project by path
136
+ getProjectByPath(projectPath: string): ProjectInfo | null {
137
+ const projectId = this.generateProjectId(projectPath);
138
+ return this.registry.projects[projectId] || null;
139
+ }
140
+
141
+ // Set active project
142
+ setActiveProject(projectId: string): boolean {
143
+ if (!this.registry.projects[projectId]) {
144
+ return false;
145
+ }
146
+
147
+ this.registry.activeProject = projectId;
148
+ this.registry.projects[projectId]!.lastAccessed = Date.now();
149
+ this.saveRegistry();
150
+ return true;
151
+ }
152
+
153
+ // Get active project
154
+ getActiveProject(): ProjectInfo | null {
155
+ if (!this.registry.activeProject) {
156
+ return null;
157
+ }
158
+ return this.registry.projects[this.registry.activeProject] || null;
159
+ }
160
+
161
+ // Update project stats
162
+ updateProjectStats(projectId: string, stats: { totalFiles?: number; totalDecisions?: number; languages?: string[] }): void {
163
+ const project = this.registry.projects[projectId];
164
+ if (!project) return;
165
+
166
+ if (stats.totalFiles !== undefined) {
167
+ project.totalFiles = stats.totalFiles;
168
+ }
169
+ if (stats.totalDecisions !== undefined) {
170
+ project.totalDecisions = stats.totalDecisions;
171
+ }
172
+ if (stats.languages !== undefined) {
173
+ project.languages = stats.languages;
174
+ }
175
+
176
+ this.saveRegistry();
177
+ }
178
+
179
+ // Get config for a project
180
+ getProjectConfig(projectPath: string): MemoryLayerConfig {
181
+ const normalizedPath = resolve(projectPath);
182
+ const dataDir = this.getProjectDataDir(normalizedPath);
183
+
184
+ return {
185
+ projectPath: normalizedPath,
186
+ dataDir,
187
+ maxTokens: 6000,
188
+ embeddingModel: 'Xenova/all-MiniLM-L6-v2',
189
+ watchIgnore: [
190
+ '**/node_modules/**',
191
+ '**/.git/**',
192
+ '**/dist/**',
193
+ '**/build/**',
194
+ '**/.next/**',
195
+ '**/coverage/**',
196
+ '**/*.min.js',
197
+ '**/*.min.css',
198
+ '**/*.map',
199
+ '**/package-lock.json',
200
+ '**/yarn.lock',
201
+ '**/pnpm-lock.yaml',
202
+ '**/.env*',
203
+ '**/*.log'
204
+ ]
205
+ };
206
+ }
207
+
208
+ // Scan for projects in common locations
209
+ discoverProjects(): string[] {
210
+ const discovered: string[] = [];
211
+ const homeDir = homedir();
212
+
213
+ // Common project locations
214
+ const searchDirs = [
215
+ join(homeDir, 'projects'),
216
+ join(homeDir, 'Projects'),
217
+ join(homeDir, 'code'),
218
+ join(homeDir, 'Code'),
219
+ join(homeDir, 'dev'),
220
+ join(homeDir, 'Development'),
221
+ join(homeDir, 'workspace'),
222
+ join(homeDir, 'repos'),
223
+ join(homeDir, 'github'),
224
+ join(homeDir, 'Desktop'),
225
+ join(homeDir, 'Documents'),
226
+ ];
227
+
228
+ for (const searchDir of searchDirs) {
229
+ if (!existsSync(searchDir)) continue;
230
+
231
+ try {
232
+ const entries = readdirSync(searchDir, { withFileTypes: true });
233
+
234
+ for (const entry of entries) {
235
+ if (!entry.isDirectory()) continue;
236
+
237
+ const projectPath = join(searchDir, entry.name);
238
+
239
+ // Check if it looks like a project (has common project files)
240
+ const projectIndicators = [
241
+ 'package.json',
242
+ 'Cargo.toml',
243
+ 'go.mod',
244
+ 'requirements.txt',
245
+ 'pyproject.toml',
246
+ 'pom.xml',
247
+ 'build.gradle',
248
+ '.git'
249
+ ];
250
+
251
+ const isProject = projectIndicators.some(indicator =>
252
+ existsSync(join(projectPath, indicator))
253
+ );
254
+
255
+ if (isProject) {
256
+ discovered.push(projectPath);
257
+ }
258
+ }
259
+ } catch {
260
+ // Skip directories we can't read
261
+ }
262
+ }
263
+
264
+ return discovered;
265
+ }
266
+
267
+ // Get cross-project database connections for search
268
+ getProjectDatabases(): Array<{ project: ProjectInfo; db: Database.Database }> {
269
+ const result: Array<{ project: ProjectInfo; db: Database.Database }> = [];
270
+
271
+ for (const project of this.listProjects()) {
272
+ const dbPath = join(project.dataDir, 'memorylayer.db');
273
+
274
+ if (existsSync(dbPath)) {
275
+ try {
276
+ const db = new Database(dbPath, { readonly: true });
277
+ result.push({ project, db });
278
+ } catch {
279
+ // Skip databases we can't open
280
+ }
281
+ }
282
+ }
283
+
284
+ return result;
285
+ }
286
+
287
+ // Close all database connections
288
+ closeAllDatabases(dbs: Array<{ db: Database.Database }>): void {
289
+ for (const { db } of dbs) {
290
+ try {
291
+ db.close();
292
+ } catch {
293
+ // Ignore close errors
294
+ }
295
+ }
296
+ }
297
+ }