namnam-skills 1.0.0 → 1.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.
- package/package.json +2 -1
- package/src/cli.js +544 -0
- package/src/conversation.js +447 -0
- package/src/indexer.js +793 -0
- package/src/templates/core/namnam.md +493 -237
- package/src/templates/platforms/AGENTS.md +47 -0
- package/src/templates/core/code-review.md +0 -70
- package/src/templates/core/git-commit.md +0 -57
- package/src/templates/core/git-push.md +0 -53
- package/src/templates/core/git-status.md +0 -48
- package/src/templates/core/validate-and-fix.md +0 -69
package/src/indexer.js
ADDED
|
@@ -0,0 +1,793 @@
|
|
|
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
|
+
}
|