namnam-skills 1.0.0 → 1.0.2

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/src/indexer.js ADDED
@@ -0,0 +1,944 @@
1
+ /**
2
+ * Codebase Indexing System
3
+ *
4
+ * Provides deep code understanding through:
5
+ * - File scanning and hashing (change detection)
6
+ * - Symbol extraction (functions, classes, exports)
7
+ * - Import/dependency graph
8
+ * - Pattern detection
9
+ * - AI-ready context generation
10
+ */
11
+
12
+ import fs from 'fs-extra';
13
+ import path from 'path';
14
+ import crypto from 'crypto';
15
+
16
+ // Constants
17
+ const INDEX_DIR = 'index';
18
+ const META_FILE = 'meta.json';
19
+ const FILES_FILE = 'files.json';
20
+ const SYMBOLS_FILE = 'symbols.json';
21
+ const IMPORTS_FILE = 'imports.json';
22
+ const PATTERNS_FILE = 'patterns.json';
23
+ const SUMMARIES_DIR = 'summaries';
24
+
25
+ // File patterns to index
26
+ const INDEXABLE_EXTENSIONS = [
27
+ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
28
+ '.py', '.rb', '.go', '.rs', '.java', '.kt',
29
+ '.c', '.cpp', '.h', '.hpp', '.cs',
30
+ '.vue', '.svelte', '.astro',
31
+ '.json', '.yaml', '.yml', '.toml',
32
+ '.md', '.mdx',
33
+ '.css', '.scss', '.less',
34
+ '.html', '.xml',
35
+ '.sql', '.graphql', '.prisma'
36
+ ];
37
+
38
+ // Directories to skip
39
+ const SKIP_DIRS = [
40
+ 'node_modules', '.git', '.svn', '.hg',
41
+ 'dist', 'build', 'out', '.next', '.nuxt',
42
+ 'coverage', '.nyc_output',
43
+ '__pycache__', '.pytest_cache',
44
+ 'vendor', 'target',
45
+ '.claude', '.cursor', '.vscode'
46
+ ];
47
+
48
+ // Max file size to index (1MB)
49
+ const MAX_FILE_SIZE = 1024 * 1024;
50
+
51
+ /**
52
+ * Get index directory path
53
+ */
54
+ export function getIndexDir(cwd = process.cwd()) {
55
+ return path.join(cwd, '.claude', INDEX_DIR);
56
+ }
57
+
58
+ /**
59
+ * Check if index exists
60
+ */
61
+ export async function hasIndex(cwd = process.cwd()) {
62
+ const metaPath = path.join(getIndexDir(cwd), META_FILE);
63
+ return await fs.pathExists(metaPath);
64
+ }
65
+
66
+ /**
67
+ * Get index metadata
68
+ */
69
+ export async function getIndexMeta(cwd = process.cwd()) {
70
+ const metaPath = path.join(getIndexDir(cwd), META_FILE);
71
+ if (!(await fs.pathExists(metaPath))) {
72
+ return null;
73
+ }
74
+ return await fs.readJson(metaPath);
75
+ }
76
+
77
+ /**
78
+ * Calculate file hash for change detection
79
+ */
80
+ function hashFile(content) {
81
+ return crypto.createHash('md5').update(content).digest('hex');
82
+ }
83
+
84
+ /**
85
+ * Check if file should be indexed
86
+ */
87
+ function shouldIndexFile(filePath) {
88
+ const ext = path.extname(filePath).toLowerCase();
89
+ return INDEXABLE_EXTENSIONS.includes(ext);
90
+ }
91
+
92
+ /**
93
+ * Check if directory should be skipped
94
+ */
95
+ function shouldSkipDir(dirName) {
96
+ return SKIP_DIRS.includes(dirName) || dirName.startsWith('.');
97
+ }
98
+
99
+ /**
100
+ * Scan directory for files
101
+ */
102
+ async function scanDirectory(dir, baseDir = dir) {
103
+ const files = [];
104
+ const items = await fs.readdir(dir, { withFileTypes: true });
105
+
106
+ for (const item of items) {
107
+ const fullPath = path.join(dir, item.name);
108
+ const relativePath = path.relative(baseDir, fullPath);
109
+
110
+ if (item.isDirectory()) {
111
+ if (!shouldSkipDir(item.name)) {
112
+ const subFiles = await scanDirectory(fullPath, baseDir);
113
+ files.push(...subFiles);
114
+ }
115
+ } else if (item.isFile() && shouldIndexFile(item.name)) {
116
+ const stats = await fs.stat(fullPath);
117
+ if (stats.size <= MAX_FILE_SIZE) {
118
+ files.push({
119
+ path: relativePath.replace(/\\/g, '/'),
120
+ fullPath,
121
+ size: stats.size,
122
+ mtime: stats.mtime.toISOString()
123
+ });
124
+ }
125
+ }
126
+ }
127
+
128
+ return files;
129
+ }
130
+
131
+ /**
132
+ * Extract symbols from JavaScript/TypeScript file
133
+ */
134
+ function extractJsSymbols(content, filePath) {
135
+ const symbols = {
136
+ functions: [],
137
+ classes: [],
138
+ exports: [],
139
+ variables: [],
140
+ types: []
141
+ };
142
+
143
+ // Function declarations
144
+ const funcRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g;
145
+ let match;
146
+ while ((match = funcRegex.exec(content)) !== null) {
147
+ symbols.functions.push({
148
+ name: match[1],
149
+ line: content.substring(0, match.index).split('\n').length,
150
+ exported: match[0].includes('export')
151
+ });
152
+ }
153
+
154
+ // Arrow functions (const name = () => or const name = async () =>)
155
+ const arrowRegex = /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g;
156
+ while ((match = arrowRegex.exec(content)) !== null) {
157
+ symbols.functions.push({
158
+ name: match[1],
159
+ line: content.substring(0, match.index).split('\n').length,
160
+ exported: match[0].includes('export'),
161
+ arrow: true
162
+ });
163
+ }
164
+
165
+ // Class declarations
166
+ const classRegex = /(?:export\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?/g;
167
+ while ((match = classRegex.exec(content)) !== null) {
168
+ symbols.classes.push({
169
+ name: match[1],
170
+ extends: match[2] || null,
171
+ line: content.substring(0, match.index).split('\n').length,
172
+ exported: match[0].includes('export')
173
+ });
174
+ }
175
+
176
+ // Export statements
177
+ const exportRegex = /export\s+(?:default\s+)?(?:const|let|var|function|class|type|interface)?\s*(\w+)/g;
178
+ while ((match = exportRegex.exec(content)) !== null) {
179
+ if (!symbols.exports.includes(match[1])) {
180
+ symbols.exports.push(match[1]);
181
+ }
182
+ }
183
+
184
+ // Named exports
185
+ const namedExportRegex = /export\s*\{\s*([^}]+)\s*\}/g;
186
+ while ((match = namedExportRegex.exec(content)) !== null) {
187
+ const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim());
188
+ symbols.exports.push(...names.filter(n => n && !symbols.exports.includes(n)));
189
+ }
190
+
191
+ // TypeScript types and interfaces
192
+ const typeRegex = /(?:export\s+)?(?:type|interface)\s+(\w+)/g;
193
+ while ((match = typeRegex.exec(content)) !== null) {
194
+ symbols.types.push({
195
+ name: match[1],
196
+ line: content.substring(0, match.index).split('\n').length,
197
+ exported: match[0].includes('export')
198
+ });
199
+ }
200
+
201
+ return symbols;
202
+ }
203
+
204
+ /**
205
+ * Extract imports from JavaScript/TypeScript file
206
+ */
207
+ function extractJsImports(content, filePath) {
208
+ const imports = [];
209
+
210
+ // ES6 imports
211
+ const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s*,?\s*)*\s*from\s*['"]([^'"]+)['"]/g;
212
+ let match;
213
+ while ((match = importRegex.exec(content)) !== null) {
214
+ imports.push({
215
+ source: match[1],
216
+ line: content.substring(0, match.index).split('\n').length,
217
+ isRelative: match[1].startsWith('.'),
218
+ isPackage: !match[1].startsWith('.')
219
+ });
220
+ }
221
+
222
+ // CommonJS require
223
+ const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
224
+ while ((match = requireRegex.exec(content)) !== null) {
225
+ imports.push({
226
+ source: match[1],
227
+ line: content.substring(0, match.index).split('\n').length,
228
+ isRelative: match[1].startsWith('.'),
229
+ isPackage: !match[1].startsWith('.'),
230
+ commonjs: true
231
+ });
232
+ }
233
+
234
+ return imports;
235
+ }
236
+
237
+ /**
238
+ * Extract symbols from Python file
239
+ */
240
+ function extractPySymbols(content, filePath) {
241
+ const symbols = {
242
+ functions: [],
243
+ classes: [],
244
+ exports: [],
245
+ variables: []
246
+ };
247
+
248
+ // Function definitions
249
+ const funcRegex = /^(?:async\s+)?def\s+(\w+)\s*\(/gm;
250
+ let match;
251
+ while ((match = funcRegex.exec(content)) !== null) {
252
+ symbols.functions.push({
253
+ name: match[1],
254
+ line: content.substring(0, match.index).split('\n').length,
255
+ exported: !match[1].startsWith('_')
256
+ });
257
+ }
258
+
259
+ // Class definitions
260
+ const classRegex = /^class\s+(\w+)(?:\s*\(([^)]*)\))?/gm;
261
+ while ((match = classRegex.exec(content)) !== null) {
262
+ symbols.classes.push({
263
+ name: match[1],
264
+ extends: match[2] || null,
265
+ line: content.substring(0, match.index).split('\n').length,
266
+ exported: !match[1].startsWith('_')
267
+ });
268
+ }
269
+
270
+ return symbols;
271
+ }
272
+
273
+ /**
274
+ * Extract symbols based on file type
275
+ */
276
+ function extractSymbols(content, filePath) {
277
+ const ext = path.extname(filePath).toLowerCase();
278
+
279
+ if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext)) {
280
+ return extractJsSymbols(content, filePath);
281
+ } else if (ext === '.py') {
282
+ return extractPySymbols(content, filePath);
283
+ }
284
+
285
+ return { functions: [], classes: [], exports: [], variables: [], types: [] };
286
+ }
287
+
288
+ /**
289
+ * Extract imports based on file type
290
+ */
291
+ function extractImports(content, filePath) {
292
+ const ext = path.extname(filePath).toLowerCase();
293
+
294
+ if (['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext)) {
295
+ return extractJsImports(content, filePath);
296
+ }
297
+
298
+ return [];
299
+ }
300
+
301
+ /**
302
+ * Detect code patterns and conventions
303
+ */
304
+ function detectPatterns(filesData) {
305
+ const patterns = {
306
+ framework: null,
307
+ language: 'javascript',
308
+ styling: null,
309
+ testing: null,
310
+ packageManager: null,
311
+ conventions: {
312
+ naming: null,
313
+ fileStructure: []
314
+ }
315
+ };
316
+
317
+ const fileNames = filesData.map(f => f.path.toLowerCase());
318
+ const hasFile = (name) => fileNames.some(f => f.includes(name));
319
+
320
+ // Detect framework
321
+ if (hasFile('next.config')) patterns.framework = 'Next.js';
322
+ else if (hasFile('nuxt.config')) patterns.framework = 'Nuxt';
323
+ else if (hasFile('vite.config')) patterns.framework = 'Vite';
324
+ else if (hasFile('angular.json')) patterns.framework = 'Angular';
325
+ else if (hasFile('vue.config')) patterns.framework = 'Vue';
326
+ else if (hasFile('svelte.config')) patterns.framework = 'Svelte';
327
+ else if (hasFile('remix.config')) patterns.framework = 'Remix';
328
+ else if (hasFile('astro.config')) patterns.framework = 'Astro';
329
+
330
+ // Detect language
331
+ const tsFiles = filesData.filter(f => f.path.endsWith('.ts') || f.path.endsWith('.tsx'));
332
+ const jsFiles = filesData.filter(f => f.path.endsWith('.js') || f.path.endsWith('.jsx'));
333
+ if (tsFiles.length > jsFiles.length) patterns.language = 'typescript';
334
+
335
+ // Detect styling
336
+ if (hasFile('tailwind.config')) patterns.styling = 'Tailwind CSS';
337
+ else if (fileNames.some(f => f.endsWith('.scss'))) patterns.styling = 'SCSS';
338
+ else if (fileNames.some(f => f.endsWith('.less'))) patterns.styling = 'Less';
339
+ else if (fileNames.some(f => f.includes('.module.css'))) patterns.styling = 'CSS Modules';
340
+ else if (fileNames.some(f => f.includes('styled'))) patterns.styling = 'Styled Components';
341
+
342
+ // Detect testing
343
+ if (hasFile('jest.config')) patterns.testing = 'Jest';
344
+ else if (hasFile('vitest.config')) patterns.testing = 'Vitest';
345
+ else if (hasFile('playwright.config')) patterns.testing = 'Playwright';
346
+ else if (hasFile('cypress.config')) patterns.testing = 'Cypress';
347
+
348
+ // Detect package manager
349
+ if (hasFile('pnpm-lock.yaml')) patterns.packageManager = 'pnpm';
350
+ else if (hasFile('yarn.lock')) patterns.packageManager = 'yarn';
351
+ else if (hasFile('bun.lockb')) patterns.packageManager = 'bun';
352
+ else if (hasFile('package-lock.json')) patterns.packageManager = 'npm';
353
+
354
+ // Detect file structure conventions
355
+ const dirs = [...new Set(filesData.map(f => f.path.split('/')[0]))];
356
+ patterns.conventions.fileStructure = dirs.filter(d => !d.includes('.'));
357
+
358
+ return patterns;
359
+ }
360
+
361
+ /**
362
+ * Generate file summary for AI context
363
+ */
364
+ function generateFileSummary(filePath, content, symbols, imports) {
365
+ const lines = content.split('\n').length;
366
+ const ext = path.extname(filePath);
367
+
368
+ let summary = `## ${filePath}\n\n`;
369
+ summary += `**Type:** ${ext} | **Lines:** ${lines}\n\n`;
370
+
371
+ if (symbols.exports.length > 0) {
372
+ summary += `**Exports:** ${symbols.exports.join(', ')}\n\n`;
373
+ }
374
+
375
+ if (symbols.functions.length > 0) {
376
+ summary += `**Functions:**\n`;
377
+ symbols.functions.slice(0, 10).forEach(f => {
378
+ summary += `- \`${f.name}\` (line ${f.line})${f.exported ? ' [exported]' : ''}\n`;
379
+ });
380
+ if (symbols.functions.length > 10) {
381
+ summary += `- ... and ${symbols.functions.length - 10} more\n`;
382
+ }
383
+ summary += '\n';
384
+ }
385
+
386
+ if (symbols.classes.length > 0) {
387
+ summary += `**Classes:**\n`;
388
+ symbols.classes.forEach(c => {
389
+ summary += `- \`${c.name}\`${c.extends ? ` extends ${c.extends}` : ''} (line ${c.line})\n`;
390
+ });
391
+ summary += '\n';
392
+ }
393
+
394
+ if (imports.length > 0) {
395
+ const packages = imports.filter(i => i.isPackage).map(i => i.source);
396
+ const relatives = imports.filter(i => i.isRelative).map(i => i.source);
397
+
398
+ if (packages.length > 0) {
399
+ summary += `**Dependencies:** ${[...new Set(packages)].slice(0, 10).join(', ')}\n`;
400
+ }
401
+ if (relatives.length > 0) {
402
+ summary += `**Local imports:** ${relatives.slice(0, 5).join(', ')}${relatives.length > 5 ? '...' : ''}\n`;
403
+ }
404
+ }
405
+
406
+ return summary;
407
+ }
408
+
409
+ /**
410
+ * Build the full codebase index
411
+ */
412
+ export async function buildIndex(cwd = process.cwd(), options = {}) {
413
+ const { onProgress = () => {} } = options;
414
+
415
+ const indexDir = getIndexDir(cwd);
416
+ const summariesDir = path.join(indexDir, SUMMARIES_DIR);
417
+
418
+ // Create directories
419
+ await fs.ensureDir(indexDir);
420
+ await fs.ensureDir(summariesDir);
421
+
422
+ onProgress({ phase: 'scanning', message: 'Scanning files...' });
423
+
424
+ // Scan all files
425
+ const scannedFiles = await scanDirectory(cwd);
426
+
427
+ onProgress({
428
+ phase: 'scanning',
429
+ message: `Found ${scannedFiles.length} files`,
430
+ total: scannedFiles.length
431
+ });
432
+
433
+ // Process each file
434
+ const filesIndex = [];
435
+ const allSymbols = {};
436
+ const allImports = {};
437
+ let processed = 0;
438
+
439
+ for (const file of scannedFiles) {
440
+ try {
441
+ const content = await fs.readFile(file.fullPath, 'utf-8');
442
+ const hash = hashFile(content);
443
+
444
+ // Extract symbols and imports
445
+ const symbols = extractSymbols(content, file.path);
446
+ const imports = extractImports(content, file.path);
447
+
448
+ // Store file info
449
+ filesIndex.push({
450
+ path: file.path,
451
+ size: file.size,
452
+ hash,
453
+ mtime: file.mtime,
454
+ lines: content.split('\n').length,
455
+ hasSymbols: symbols.functions.length > 0 || symbols.classes.length > 0
456
+ });
457
+
458
+ // Store symbols
459
+ if (symbols.functions.length > 0 || symbols.classes.length > 0 || symbols.types.length > 0) {
460
+ allSymbols[file.path] = symbols;
461
+ }
462
+
463
+ // Store imports
464
+ if (imports.length > 0) {
465
+ allImports[file.path] = imports;
466
+ }
467
+
468
+ // Generate summary
469
+ const summary = generateFileSummary(file.path, content, symbols, imports);
470
+ const summaryPath = path.join(summariesDir, file.path.replace(/\//g, '_').replace(/\\/g, '_') + '.md');
471
+ await fs.writeFile(summaryPath, summary);
472
+
473
+ processed++;
474
+ if (processed % 50 === 0) {
475
+ onProgress({
476
+ phase: 'indexing',
477
+ message: `Indexed ${processed}/${scannedFiles.length} files`,
478
+ current: processed,
479
+ total: scannedFiles.length
480
+ });
481
+ }
482
+ } catch (err) {
483
+ // Skip files that can't be read
484
+ console.error(`Skipping ${file.path}: ${err.message}`);
485
+ }
486
+ }
487
+
488
+ // Detect patterns
489
+ onProgress({ phase: 'analyzing', message: 'Analyzing patterns...' });
490
+ const patterns = detectPatterns(filesIndex);
491
+
492
+ // Build dependency graph
493
+ const dependencyGraph = buildDependencyGraph(allImports, filesIndex);
494
+
495
+ // Save all index files
496
+ onProgress({ phase: 'saving', message: 'Saving index...' });
497
+
498
+ const meta = {
499
+ version: '1.0.0',
500
+ createdAt: new Date().toISOString(),
501
+ updatedAt: new Date().toISOString(),
502
+ stats: {
503
+ totalFiles: filesIndex.length,
504
+ totalLines: filesIndex.reduce((sum, f) => sum + f.lines, 0),
505
+ filesWithSymbols: Object.keys(allSymbols).length,
506
+ totalFunctions: Object.values(allSymbols).reduce((sum, s) => sum + s.functions.length, 0),
507
+ totalClasses: Object.values(allSymbols).reduce((sum, s) => sum + s.classes.length, 0)
508
+ },
509
+ patterns
510
+ };
511
+
512
+ await fs.writeJson(path.join(indexDir, META_FILE), meta, { spaces: 2 });
513
+ await fs.writeJson(path.join(indexDir, FILES_FILE), filesIndex, { spaces: 2 });
514
+ await fs.writeJson(path.join(indexDir, SYMBOLS_FILE), allSymbols, { spaces: 2 });
515
+ await fs.writeJson(path.join(indexDir, IMPORTS_FILE), allImports, { spaces: 2 });
516
+ await fs.writeJson(path.join(indexDir, PATTERNS_FILE), patterns, { spaces: 2 });
517
+
518
+ onProgress({
519
+ phase: 'complete',
520
+ message: `Index complete: ${filesIndex.length} files, ${meta.stats.totalFunctions} functions, ${meta.stats.totalClasses} classes`
521
+ });
522
+
523
+ return meta;
524
+ }
525
+
526
+ /**
527
+ * Build dependency graph from imports
528
+ */
529
+ function buildDependencyGraph(imports, files) {
530
+ const graph = {
531
+ nodes: files.map(f => f.path),
532
+ edges: []
533
+ };
534
+
535
+ for (const [filePath, fileImports] of Object.entries(imports)) {
536
+ for (const imp of fileImports) {
537
+ if (imp.isRelative) {
538
+ // Resolve relative import
539
+ const dir = path.dirname(filePath);
540
+ let resolved = path.join(dir, imp.source).replace(/\\/g, '/');
541
+
542
+ // Try with extensions
543
+ const possiblePaths = [
544
+ resolved,
545
+ resolved + '.js',
546
+ resolved + '.ts',
547
+ resolved + '.jsx',
548
+ resolved + '.tsx',
549
+ resolved + '/index.js',
550
+ resolved + '/index.ts'
551
+ ];
552
+
553
+ const target = possiblePaths.find(p => files.some(f => f.path === p));
554
+ if (target) {
555
+ graph.edges.push({
556
+ from: filePath,
557
+ to: target
558
+ });
559
+ }
560
+ }
561
+ }
562
+ }
563
+
564
+ return graph;
565
+ }
566
+
567
+ /**
568
+ * Check for changes since last index
569
+ */
570
+ export async function checkIndexChanges(cwd = process.cwd()) {
571
+ const meta = await getIndexMeta(cwd);
572
+ if (!meta) return { hasChanges: true, reason: 'no_index' };
573
+
574
+ const filesPath = path.join(getIndexDir(cwd), FILES_FILE);
575
+ if (!(await fs.pathExists(filesPath))) {
576
+ return { hasChanges: true, reason: 'missing_files_index' };
577
+ }
578
+
579
+ const indexedFiles = await fs.readJson(filesPath);
580
+ const currentFiles = await scanDirectory(cwd);
581
+
582
+ // Check for new, modified, or deleted files
583
+ const indexedPaths = new Set(indexedFiles.map(f => f.path));
584
+ const currentPaths = new Set(currentFiles.map(f => f.path));
585
+
586
+ const newFiles = currentFiles.filter(f => !indexedPaths.has(f.path));
587
+ const deletedFiles = indexedFiles.filter(f => !currentPaths.has(f.path));
588
+
589
+ // Check for modified files
590
+ const modifiedFiles = [];
591
+ for (const current of currentFiles) {
592
+ const indexed = indexedFiles.find(f => f.path === current.path);
593
+ if (indexed && indexed.mtime !== current.mtime) {
594
+ modifiedFiles.push(current.path);
595
+ }
596
+ }
597
+
598
+ if (newFiles.length > 0 || deletedFiles.length > 0 || modifiedFiles.length > 0) {
599
+ return {
600
+ hasChanges: true,
601
+ reason: 'files_changed',
602
+ newFiles: newFiles.map(f => f.path),
603
+ deletedFiles: deletedFiles.map(f => f.path),
604
+ modifiedFiles
605
+ };
606
+ }
607
+
608
+ return { hasChanges: false };
609
+ }
610
+
611
+ /**
612
+ * Search symbols across codebase
613
+ */
614
+ export async function searchSymbols(query, cwd = process.cwd()) {
615
+ const symbolsPath = path.join(getIndexDir(cwd), SYMBOLS_FILE);
616
+ if (!(await fs.pathExists(symbolsPath))) {
617
+ return [];
618
+ }
619
+
620
+ const allSymbols = await fs.readJson(symbolsPath);
621
+ const results = [];
622
+ const queryLower = query.toLowerCase();
623
+
624
+ for (const [filePath, symbols] of Object.entries(allSymbols)) {
625
+ // Search functions
626
+ for (const func of symbols.functions || []) {
627
+ if (func.name.toLowerCase().includes(queryLower)) {
628
+ results.push({
629
+ type: 'function',
630
+ name: func.name,
631
+ file: filePath,
632
+ line: func.line,
633
+ exported: func.exported
634
+ });
635
+ }
636
+ }
637
+
638
+ // Search classes
639
+ for (const cls of symbols.classes || []) {
640
+ if (cls.name.toLowerCase().includes(queryLower)) {
641
+ results.push({
642
+ type: 'class',
643
+ name: cls.name,
644
+ file: filePath,
645
+ line: cls.line,
646
+ extends: cls.extends,
647
+ exported: cls.exported
648
+ });
649
+ }
650
+ }
651
+
652
+ // Search types
653
+ for (const type of symbols.types || []) {
654
+ if (type.name.toLowerCase().includes(queryLower)) {
655
+ results.push({
656
+ type: 'type',
657
+ name: type.name,
658
+ file: filePath,
659
+ line: type.line,
660
+ exported: type.exported
661
+ });
662
+ }
663
+ }
664
+ }
665
+
666
+ return results;
667
+ }
668
+
669
+ /**
670
+ * Get files that import a specific file
671
+ */
672
+ export async function getFileImporters(targetFile, cwd = process.cwd()) {
673
+ const importsPath = path.join(getIndexDir(cwd), IMPORTS_FILE);
674
+ if (!(await fs.pathExists(importsPath))) {
675
+ return [];
676
+ }
677
+
678
+ const allImports = await fs.readJson(importsPath);
679
+ const importers = [];
680
+
681
+ for (const [filePath, imports] of Object.entries(allImports)) {
682
+ for (const imp of imports) {
683
+ if (imp.isRelative) {
684
+ const dir = path.dirname(filePath);
685
+ const resolved = path.join(dir, imp.source).replace(/\\/g, '/');
686
+ if (resolved === targetFile || targetFile.startsWith(resolved)) {
687
+ importers.push({ file: filePath, line: imp.line });
688
+ }
689
+ }
690
+ }
691
+ }
692
+
693
+ return importers;
694
+ }
695
+
696
+ /**
697
+ * Get files that a specific file imports
698
+ */
699
+ export async function getFileDependencies(sourceFile, cwd = process.cwd()) {
700
+ const importsPath = path.join(getIndexDir(cwd), IMPORTS_FILE);
701
+ if (!(await fs.pathExists(importsPath))) {
702
+ return [];
703
+ }
704
+
705
+ const allImports = await fs.readJson(importsPath);
706
+ return allImports[sourceFile] || [];
707
+ }
708
+
709
+ /**
710
+ * Generate context for AI consumption
711
+ */
712
+ export async function generateAIContext(options = {}, cwd = process.cwd()) {
713
+ const {
714
+ files = [], // Specific files to include
715
+ query = null, // Search query to find relevant files
716
+ maxTokens = 50000, // Approximate max tokens
717
+ includeImports = true,
718
+ includeSummaries = true
719
+ } = options;
720
+
721
+ const indexDir = getIndexDir(cwd);
722
+ const meta = await getIndexMeta(cwd);
723
+
724
+ if (!meta) {
725
+ return { error: 'No index found. Run indexing first.' };
726
+ }
727
+
728
+ let context = `# Codebase Context\n\n`;
729
+ context += `**Project:** ${path.basename(cwd)}\n`;
730
+ context += `**Framework:** ${meta.patterns?.framework || 'Unknown'}\n`;
731
+ context += `**Language:** ${meta.patterns?.language || 'Unknown'}\n`;
732
+ context += `**Files:** ${meta.stats.totalFiles} | **Lines:** ${meta.stats.totalLines}\n\n`;
733
+
734
+ // Add pattern info
735
+ if (meta.patterns) {
736
+ context += `## Detected Patterns\n\n`;
737
+ if (meta.patterns.styling) context += `- **Styling:** ${meta.patterns.styling}\n`;
738
+ if (meta.patterns.testing) context += `- **Testing:** ${meta.patterns.testing}\n`;
739
+ if (meta.patterns.packageManager) context += `- **Package Manager:** ${meta.patterns.packageManager}\n`;
740
+ context += '\n';
741
+ }
742
+
743
+ // Add file summaries
744
+ if (includeSummaries) {
745
+ const summariesDir = path.join(indexDir, SUMMARIES_DIR);
746
+
747
+ if (files.length > 0) {
748
+ // Include specific files
749
+ context += `## Requested Files\n\n`;
750
+ for (const file of files) {
751
+ const summaryPath = path.join(summariesDir, file.replace(/\//g, '_').replace(/\\/g, '_') + '.md');
752
+ if (await fs.pathExists(summaryPath)) {
753
+ const summary = await fs.readFile(summaryPath, 'utf-8');
754
+ context += summary + '\n---\n\n';
755
+ }
756
+ }
757
+ } else if (query) {
758
+ // Search and include relevant files
759
+ const results = await searchSymbols(query, cwd);
760
+ const relevantFiles = [...new Set(results.map(r => r.file))].slice(0, 20);
761
+
762
+ context += `## Relevant Files (query: "${query}")\n\n`;
763
+ for (const file of relevantFiles) {
764
+ const summaryPath = path.join(summariesDir, file.replace(/\//g, '_').replace(/\\/g, '_') + '.md');
765
+ if (await fs.pathExists(summaryPath)) {
766
+ const summary = await fs.readFile(summaryPath, 'utf-8');
767
+ context += summary + '\n---\n\n';
768
+ }
769
+ }
770
+ }
771
+ }
772
+
773
+ return {
774
+ context,
775
+ meta,
776
+ tokenEstimate: Math.ceil(context.length / 4)
777
+ };
778
+ }
779
+
780
+ /**
781
+ * Quick stats about the index
782
+ */
783
+ export async function getIndexStats(cwd = process.cwd()) {
784
+ const meta = await getIndexMeta(cwd);
785
+ if (!meta) return null;
786
+
787
+ return {
788
+ ...meta.stats,
789
+ patterns: meta.patterns,
790
+ lastUpdated: meta.updatedAt,
791
+ indexAge: Date.now() - new Date(meta.updatedAt).getTime()
792
+ };
793
+ }
794
+
795
+ /**
796
+ * Incremental index update - only process changed files
797
+ */
798
+ export async function updateIndexIncremental(changedFiles, cwd = process.cwd(), options = {}) {
799
+ const { onProgress = () => {} } = options;
800
+
801
+ const indexDir = getIndexDir(cwd);
802
+ const summariesDir = path.join(indexDir, SUMMARIES_DIR);
803
+
804
+ // Load existing index data
805
+ const filesPath = path.join(indexDir, FILES_FILE);
806
+ const symbolsPath = path.join(indexDir, SYMBOLS_FILE);
807
+ const importsPath = path.join(indexDir, IMPORTS_FILE);
808
+
809
+ if (!(await fs.pathExists(filesPath))) {
810
+ // No existing index, do full build
811
+ return await buildIndex(cwd, options);
812
+ }
813
+
814
+ let filesIndex = await fs.readJson(filesPath);
815
+ let allSymbols = await fs.readJson(symbolsPath);
816
+ let allImports = await fs.readJson(importsPath);
817
+
818
+ onProgress({ phase: 'incremental', message: `Processing ${changedFiles.length} changed files...` });
819
+
820
+ let processed = 0;
821
+
822
+ for (const change of changedFiles) {
823
+ const { path: filePath, fullPath, type } = change;
824
+
825
+ if (type === 'delete' || type === 'rename') {
826
+ // Remove from index
827
+ filesIndex = filesIndex.filter(f => f.path !== filePath);
828
+ delete allSymbols[filePath];
829
+ delete allImports[filePath];
830
+
831
+ // Remove summary
832
+ const summaryPath = path.join(summariesDir, filePath.replace(/\//g, '_').replace(/\\/g, '_') + '.md');
833
+ if (await fs.pathExists(summaryPath)) {
834
+ await fs.remove(summaryPath);
835
+ }
836
+ } else {
837
+ // Add or update file
838
+ try {
839
+ const resolvedPath = fullPath || path.join(cwd, filePath);
840
+
841
+ if (!(await fs.pathExists(resolvedPath))) {
842
+ // File was deleted
843
+ filesIndex = filesIndex.filter(f => f.path !== filePath);
844
+ delete allSymbols[filePath];
845
+ delete allImports[filePath];
846
+ continue;
847
+ }
848
+
849
+ const content = await fs.readFile(resolvedPath, 'utf-8');
850
+ const stats = await fs.stat(resolvedPath);
851
+ const hash = crypto.createHash('md5').update(content).digest('hex');
852
+
853
+ // Extract symbols and imports
854
+ const symbols = extractSymbols(content, filePath);
855
+ const imports = extractImports(content, filePath);
856
+
857
+ // Update file entry
858
+ const existingIdx = filesIndex.findIndex(f => f.path === filePath);
859
+ const fileEntry = {
860
+ path: filePath,
861
+ size: stats.size,
862
+ hash,
863
+ mtime: stats.mtime.toISOString(),
864
+ lines: content.split('\n').length,
865
+ hasSymbols: symbols.functions.length > 0 || symbols.classes.length > 0
866
+ };
867
+
868
+ if (existingIdx >= 0) {
869
+ filesIndex[existingIdx] = fileEntry;
870
+ } else {
871
+ filesIndex.push(fileEntry);
872
+ }
873
+
874
+ // Update symbols and imports
875
+ if (symbols.functions.length > 0 || symbols.classes.length > 0 || symbols.types.length > 0) {
876
+ allSymbols[filePath] = symbols;
877
+ } else {
878
+ delete allSymbols[filePath];
879
+ }
880
+
881
+ if (imports.length > 0) {
882
+ allImports[filePath] = imports;
883
+ } else {
884
+ delete allImports[filePath];
885
+ }
886
+
887
+ // Update summary
888
+ const summary = generateFileSummary(filePath, content, symbols, imports);
889
+ const summaryPath = path.join(summariesDir, filePath.replace(/\//g, '_').replace(/\\/g, '_') + '.md');
890
+ await fs.writeFile(summaryPath, summary);
891
+
892
+ } catch (err) {
893
+ // Skip files that can't be read
894
+ console.error(`Skipping ${filePath}: ${err.message}`);
895
+ }
896
+ }
897
+
898
+ processed++;
899
+ onProgress({
900
+ phase: 'incremental',
901
+ message: `Processed ${processed}/${changedFiles.length}`,
902
+ current: processed,
903
+ total: changedFiles.length
904
+ });
905
+ }
906
+
907
+ // Re-detect patterns
908
+ onProgress({ phase: 'analyzing', message: 'Analyzing patterns...' });
909
+ const patterns = detectPatterns(filesIndex);
910
+
911
+ // Update meta
912
+ const meta = {
913
+ version: '1.0.0',
914
+ createdAt: (await getIndexMeta(cwd))?.createdAt || new Date().toISOString(),
915
+ updatedAt: new Date().toISOString(),
916
+ stats: {
917
+ totalFiles: filesIndex.length,
918
+ totalLines: filesIndex.reduce((sum, f) => sum + f.lines, 0),
919
+ filesWithSymbols: Object.keys(allSymbols).length,
920
+ totalFunctions: Object.values(allSymbols).reduce((sum, s) => sum + s.functions.length, 0),
921
+ totalClasses: Object.values(allSymbols).reduce((sum, s) => sum + s.classes.length, 0)
922
+ },
923
+ patterns,
924
+ lastIncrementalUpdate: {
925
+ timestamp: new Date().toISOString(),
926
+ filesProcessed: changedFiles.length
927
+ }
928
+ };
929
+
930
+ // Save updated index
931
+ onProgress({ phase: 'saving', message: 'Saving index...' });
932
+ await fs.writeJson(path.join(indexDir, META_FILE), meta, { spaces: 2 });
933
+ await fs.writeJson(filesPath, filesIndex, { spaces: 2 });
934
+ await fs.writeJson(symbolsPath, allSymbols, { spaces: 2 });
935
+ await fs.writeJson(importsPath, allImports, { spaces: 2 });
936
+ await fs.writeJson(path.join(indexDir, PATTERNS_FILE), patterns, { spaces: 2 });
937
+
938
+ onProgress({
939
+ phase: 'complete',
940
+ message: `Incremental update complete: ${changedFiles.length} files processed`
941
+ });
942
+
943
+ return meta;
944
+ }