@claudetools/tools 0.7.0 → 0.7.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/dist/cli.js +62 -0
- package/dist/codedna/generators/base.d.ts +11 -2
- package/dist/codedna/generators/base.js +91 -8
- package/dist/codedna/generators/react-frontend.js +2 -1
- package/dist/codedna/parser.d.ts +6 -0
- package/dist/codedna/parser.js +7 -0
- package/dist/codedna/registry.d.ts +23 -17
- package/dist/codedna/registry.js +103 -263
- package/dist/codedna/template-engine.js +23 -0
- package/dist/codedna/types.d.ts +22 -0
- package/dist/handlers/codedna-handlers.d.ts +219 -6
- package/dist/handlers/codedna-handlers.js +379 -11
- package/dist/handlers/tool-handlers.js +56 -7
- package/dist/helpers/codebase-mapper.d.ts +66 -0
- package/dist/helpers/codebase-mapper.js +634 -0
- package/dist/helpers/config.d.ts +1 -1
- package/dist/helpers/config.js +39 -2
- package/dist/helpers/workers.js +60 -7
- package/dist/templates/orchestrator-prompt.js +15 -7
- package/dist/templates/worker-prompt.js +24 -31
- package/dist/tools.js +101 -2
- package/dist/watcher.js +97 -1
- package/package.json +3 -2
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codebase Mapper
|
|
3
|
+
*
|
|
4
|
+
* Client-side AST analysis to generate codebase maps.
|
|
5
|
+
* Runs in the MCP server and uploads results to the API.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
8
|
+
import { join, relative, extname, basename, dirname } from 'path';
|
|
9
|
+
import * as ts from 'typescript';
|
|
10
|
+
import { API_BASE_URL } from './config.js';
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// File Discovery
|
|
13
|
+
// =============================================================================
|
|
14
|
+
const IGNORED_DIRS = new Set([
|
|
15
|
+
'node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.turbo',
|
|
16
|
+
'__pycache__', '.venv', 'venv', '.tox', 'target', '.idea', '.vscode',
|
|
17
|
+
]);
|
|
18
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
19
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
20
|
+
]);
|
|
21
|
+
function discoverFiles(root) {
|
|
22
|
+
const files = [];
|
|
23
|
+
function walk(dir) {
|
|
24
|
+
const entries = readdirSync(dir);
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (entry.startsWith('.') || IGNORED_DIRS.has(entry))
|
|
27
|
+
continue;
|
|
28
|
+
const fullPath = join(dir, entry);
|
|
29
|
+
const stat = statSync(fullPath);
|
|
30
|
+
if (stat.isDirectory()) {
|
|
31
|
+
walk(fullPath);
|
|
32
|
+
}
|
|
33
|
+
else if (stat.isFile() && SUPPORTED_EXTENSIONS.has(extname(entry))) {
|
|
34
|
+
files.push(fullPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
walk(root);
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// AST Analysis
|
|
43
|
+
// =============================================================================
|
|
44
|
+
function analyzeFile(filePath, root) {
|
|
45
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
46
|
+
const relativePath = relative(root, filePath);
|
|
47
|
+
const lines = content.split('\n').length;
|
|
48
|
+
const symbols = [];
|
|
49
|
+
const imports = [];
|
|
50
|
+
const exports = [];
|
|
51
|
+
// Parse with TypeScript
|
|
52
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, filePath.endsWith('.tsx') || filePath.endsWith('.jsx')
|
|
53
|
+
? ts.ScriptKind.TSX
|
|
54
|
+
: ts.ScriptKind.TS);
|
|
55
|
+
function visit(node) {
|
|
56
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
57
|
+
// Track imports
|
|
58
|
+
if (ts.isImportDeclaration(node)) {
|
|
59
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
60
|
+
if (ts.isStringLiteral(moduleSpecifier)) {
|
|
61
|
+
imports.push(moduleSpecifier.text);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Track exports
|
|
65
|
+
if (ts.isExportDeclaration(node)) {
|
|
66
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
67
|
+
if (moduleSpecifier && ts.isStringLiteral(moduleSpecifier)) {
|
|
68
|
+
exports.push(moduleSpecifier.text);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Check for export modifier using canHaveModifiers
|
|
72
|
+
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
73
|
+
const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
74
|
+
// Functions
|
|
75
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
76
|
+
symbols.push({
|
|
77
|
+
name: node.name.text,
|
|
78
|
+
type: 'function',
|
|
79
|
+
exported: isExported,
|
|
80
|
+
line,
|
|
81
|
+
signature: getSignature(node),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Arrow functions and const declarations
|
|
85
|
+
if (ts.isVariableStatement(node)) {
|
|
86
|
+
const varModifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
87
|
+
const isConstExported = varModifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
88
|
+
for (const decl of node.declarationList.declarations) {
|
|
89
|
+
if (ts.isIdentifier(decl.name)) {
|
|
90
|
+
const name = decl.name.text;
|
|
91
|
+
let symbolType = 'const';
|
|
92
|
+
// Detect React components (PascalCase + returns JSX)
|
|
93
|
+
if (/^[A-Z]/.test(name) && decl.initializer) {
|
|
94
|
+
if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
|
|
95
|
+
symbolType = 'component';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Detect hooks
|
|
99
|
+
if (name.startsWith('use') && name.length > 3) {
|
|
100
|
+
symbolType = 'function';
|
|
101
|
+
}
|
|
102
|
+
symbols.push({
|
|
103
|
+
name,
|
|
104
|
+
type: symbolType,
|
|
105
|
+
exported: isConstExported,
|
|
106
|
+
line: sourceFile.getLineAndCharacterOfPosition(decl.getStart()).line + 1,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Classes
|
|
112
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
113
|
+
symbols.push({
|
|
114
|
+
name: node.name.text,
|
|
115
|
+
type: 'class',
|
|
116
|
+
exported: isExported,
|
|
117
|
+
line,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// Interfaces
|
|
121
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
122
|
+
symbols.push({
|
|
123
|
+
name: node.name.text,
|
|
124
|
+
type: 'interface',
|
|
125
|
+
exported: isExported,
|
|
126
|
+
line,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Type aliases
|
|
130
|
+
if (ts.isTypeAliasDeclaration(node)) {
|
|
131
|
+
symbols.push({
|
|
132
|
+
name: node.name.text,
|
|
133
|
+
type: 'type',
|
|
134
|
+
exported: isExported,
|
|
135
|
+
line,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Enums
|
|
139
|
+
if (ts.isEnumDeclaration(node)) {
|
|
140
|
+
symbols.push({
|
|
141
|
+
name: node.name.text,
|
|
142
|
+
type: 'enum',
|
|
143
|
+
exported: isExported,
|
|
144
|
+
line,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
ts.forEachChild(node, visit);
|
|
148
|
+
}
|
|
149
|
+
visit(sourceFile);
|
|
150
|
+
return {
|
|
151
|
+
path: filePath,
|
|
152
|
+
relativePath,
|
|
153
|
+
role: detectFileRole(relativePath, symbols),
|
|
154
|
+
symbols,
|
|
155
|
+
imports,
|
|
156
|
+
exports,
|
|
157
|
+
dependencies: [], // Populated later
|
|
158
|
+
dependents: [], // Populated later
|
|
159
|
+
linesOfCode: lines,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function getSignature(node) {
|
|
163
|
+
if (!node.parameters.length && !node.type)
|
|
164
|
+
return undefined;
|
|
165
|
+
const params = node.parameters
|
|
166
|
+
.map((p) => {
|
|
167
|
+
const name = p.name.getText();
|
|
168
|
+
const type = p.type?.getText() || 'any';
|
|
169
|
+
return `${name}: ${type}`;
|
|
170
|
+
})
|
|
171
|
+
.join(', ');
|
|
172
|
+
const returnType = node.type?.getText() || 'void';
|
|
173
|
+
return `(${params}) => ${returnType}`;
|
|
174
|
+
}
|
|
175
|
+
function detectFileRole(relativePath, symbols) {
|
|
176
|
+
const path = relativePath.toLowerCase();
|
|
177
|
+
const fileName = basename(path, extname(path));
|
|
178
|
+
// Test files
|
|
179
|
+
if (path.includes('.test.') || path.includes('.spec.') || path.includes('__tests__')) {
|
|
180
|
+
return 'test';
|
|
181
|
+
}
|
|
182
|
+
// Config files
|
|
183
|
+
if (fileName.includes('config') || fileName.includes('settings')) {
|
|
184
|
+
return 'config';
|
|
185
|
+
}
|
|
186
|
+
// Entry points
|
|
187
|
+
if (['index', 'main', 'app'].includes(fileName) && !path.includes('/')) {
|
|
188
|
+
return 'entry-point';
|
|
189
|
+
}
|
|
190
|
+
// Type definitions
|
|
191
|
+
if (path.includes('/types') || fileName.endsWith('.d') || path.includes('types.ts')) {
|
|
192
|
+
return 'type-definitions';
|
|
193
|
+
}
|
|
194
|
+
// Route handlers (Next.js, Remix, etc.)
|
|
195
|
+
if (path.includes('/routes/') || path.includes('/pages/') || path.includes('/app/')) {
|
|
196
|
+
return 'route-handler';
|
|
197
|
+
}
|
|
198
|
+
// API endpoints
|
|
199
|
+
if (path.includes('/api/') || path.includes('/handlers/')) {
|
|
200
|
+
return 'api-endpoint';
|
|
201
|
+
}
|
|
202
|
+
// MCP tools
|
|
203
|
+
if (path.includes('/tools') || path.includes('tool-handlers')) {
|
|
204
|
+
return 'mcp-tool';
|
|
205
|
+
}
|
|
206
|
+
// Components
|
|
207
|
+
if (path.includes('/components/') || symbols.some((s) => s.type === 'component')) {
|
|
208
|
+
return 'component';
|
|
209
|
+
}
|
|
210
|
+
// Hooks
|
|
211
|
+
if (path.includes('/hooks/') || symbols.some((s) => s.name.startsWith('use'))) {
|
|
212
|
+
return 'hook';
|
|
213
|
+
}
|
|
214
|
+
// Models
|
|
215
|
+
if (path.includes('/models/') || path.includes('/entities/') || path.includes('/schema')) {
|
|
216
|
+
return 'model';
|
|
217
|
+
}
|
|
218
|
+
// Services
|
|
219
|
+
if (path.includes('/services/') || path.includes('/providers/')) {
|
|
220
|
+
return 'service';
|
|
221
|
+
}
|
|
222
|
+
// Extractors
|
|
223
|
+
if (path.includes('/extraction/') || path.includes('/extractors/')) {
|
|
224
|
+
return 'extractor';
|
|
225
|
+
}
|
|
226
|
+
// Utilities
|
|
227
|
+
if (path.includes('/utils/') || path.includes('/helpers/') || path.includes('/lib/')) {
|
|
228
|
+
return 'utility';
|
|
229
|
+
}
|
|
230
|
+
return 'unknown';
|
|
231
|
+
}
|
|
232
|
+
// =============================================================================
|
|
233
|
+
// Dependency Analysis
|
|
234
|
+
// =============================================================================
|
|
235
|
+
function buildDependencyGraph(files, root) {
|
|
236
|
+
// Build a map from relative import paths to file entries
|
|
237
|
+
const filesByPath = {};
|
|
238
|
+
for (const file of Object.values(files)) {
|
|
239
|
+
// Add with and without extension
|
|
240
|
+
const relPath = file.relativePath;
|
|
241
|
+
filesByPath[relPath] = relPath;
|
|
242
|
+
filesByPath[relPath.replace(/\.[^.]+$/, '')] = relPath;
|
|
243
|
+
// Add index files
|
|
244
|
+
if (basename(relPath).startsWith('index.')) {
|
|
245
|
+
filesByPath[dirname(relPath)] = relPath;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Resolve dependencies
|
|
249
|
+
for (const file of Object.values(files)) {
|
|
250
|
+
const fileDir = dirname(file.relativePath);
|
|
251
|
+
for (const imp of file.imports) {
|
|
252
|
+
// Skip external packages
|
|
253
|
+
if (!imp.startsWith('.') && !imp.startsWith('/'))
|
|
254
|
+
continue;
|
|
255
|
+
// Resolve relative import
|
|
256
|
+
let resolved = imp.startsWith('.')
|
|
257
|
+
? join(fileDir, imp).replace(/\\/g, '/')
|
|
258
|
+
: imp;
|
|
259
|
+
// Normalize
|
|
260
|
+
resolved = resolved.replace(/^\.\//, '');
|
|
261
|
+
// Find matching file
|
|
262
|
+
const target = filesByPath[resolved];
|
|
263
|
+
if (target && target !== file.relativePath) {
|
|
264
|
+
file.dependencies.push(target);
|
|
265
|
+
// Add reverse dependency
|
|
266
|
+
const targetFile = files[target];
|
|
267
|
+
if (targetFile) {
|
|
268
|
+
targetFile.dependents.push(file.relativePath);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// =============================================================================
|
|
275
|
+
// Call Graph Building
|
|
276
|
+
// =============================================================================
|
|
277
|
+
function buildCallGraph(files) {
|
|
278
|
+
const callGraph = {};
|
|
279
|
+
const reverseCallGraph = {};
|
|
280
|
+
// Build symbol lookup
|
|
281
|
+
const symbolFiles = {};
|
|
282
|
+
for (const file of Object.values(files)) {
|
|
283
|
+
for (const symbol of file.symbols) {
|
|
284
|
+
if (symbol.exported) {
|
|
285
|
+
symbolFiles[symbol.name] = file.relativePath;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// For now, use a simple heuristic based on dependencies
|
|
290
|
+
// Full call graph would require more sophisticated AST analysis
|
|
291
|
+
for (const file of Object.values(files)) {
|
|
292
|
+
for (const symbol of file.symbols) {
|
|
293
|
+
if (symbol.type === 'function' || symbol.type === 'component') {
|
|
294
|
+
callGraph[symbol.name] = [];
|
|
295
|
+
reverseCallGraph[symbol.name] = [];
|
|
296
|
+
// Infer calls from dependencies
|
|
297
|
+
for (const dep of file.dependencies) {
|
|
298
|
+
const depFile = files[dep];
|
|
299
|
+
if (depFile) {
|
|
300
|
+
for (const depSymbol of depFile.symbols) {
|
|
301
|
+
if (depSymbol.exported && (depSymbol.type === 'function' || depSymbol.type === 'component')) {
|
|
302
|
+
callGraph[symbol.name].push(depSymbol.name);
|
|
303
|
+
if (!reverseCallGraph[depSymbol.name]) {
|
|
304
|
+
reverseCallGraph[depSymbol.name] = [];
|
|
305
|
+
}
|
|
306
|
+
reverseCallGraph[depSymbol.name].push(symbol.name);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { callGraph, reverseCallGraph };
|
|
315
|
+
}
|
|
316
|
+
// =============================================================================
|
|
317
|
+
// Directory Analysis
|
|
318
|
+
// =============================================================================
|
|
319
|
+
function analyzeDirectories(files, root) {
|
|
320
|
+
const directories = {};
|
|
321
|
+
const dirFiles = {};
|
|
322
|
+
// Group files by directory
|
|
323
|
+
for (const file of Object.values(files)) {
|
|
324
|
+
const dir = dirname(file.relativePath) || '.';
|
|
325
|
+
if (!dirFiles[dir]) {
|
|
326
|
+
dirFiles[dir] = [];
|
|
327
|
+
}
|
|
328
|
+
dirFiles[dir].push(file);
|
|
329
|
+
}
|
|
330
|
+
// Analyze each directory
|
|
331
|
+
for (const [dir, filesInDir] of Object.entries(dirFiles)) {
|
|
332
|
+
const languages = {};
|
|
333
|
+
const keyFiles = [];
|
|
334
|
+
for (const file of filesInDir) {
|
|
335
|
+
const ext = extname(file.relativePath);
|
|
336
|
+
languages[ext] = (languages[ext] || 0) + 1;
|
|
337
|
+
// Key files are entry points or have many exports
|
|
338
|
+
if (file.role === 'entry-point' || file.symbols.filter((s) => s.exported).length > 5) {
|
|
339
|
+
keyFiles.push(file.relativePath);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const primaryLang = Object.entries(languages)
|
|
343
|
+
.sort((a, b) => b[1] - a[1])[0]?.[0] || 'unknown';
|
|
344
|
+
directories[dir] = {
|
|
345
|
+
path: dir,
|
|
346
|
+
role: detectDirectoryRole(dir, filesInDir),
|
|
347
|
+
fileCount: filesInDir.length,
|
|
348
|
+
primaryLanguage: primaryLang,
|
|
349
|
+
keyFiles: keyFiles.slice(0, 3),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return directories;
|
|
353
|
+
}
|
|
354
|
+
function detectDirectoryRole(dir, files) {
|
|
355
|
+
const dirLower = dir.toLowerCase();
|
|
356
|
+
if (dirLower.includes('component'))
|
|
357
|
+
return 'components';
|
|
358
|
+
if (dirLower.includes('hook'))
|
|
359
|
+
return 'hooks';
|
|
360
|
+
if (dirLower.includes('route') || dirLower.includes('page'))
|
|
361
|
+
return 'routes';
|
|
362
|
+
if (dirLower.includes('api') || dirLower.includes('handler'))
|
|
363
|
+
return 'api';
|
|
364
|
+
if (dirLower.includes('util') || dirLower.includes('helper') || dirLower.includes('lib'))
|
|
365
|
+
return 'utilities';
|
|
366
|
+
if (dirLower.includes('type'))
|
|
367
|
+
return 'types';
|
|
368
|
+
if (dirLower.includes('test') || dirLower.includes('spec'))
|
|
369
|
+
return 'tests';
|
|
370
|
+
if (dirLower.includes('config'))
|
|
371
|
+
return 'configuration';
|
|
372
|
+
if (dirLower.includes('model') || dirLower.includes('schema'))
|
|
373
|
+
return 'models';
|
|
374
|
+
if (dirLower.includes('service') || dirLower.includes('provider'))
|
|
375
|
+
return 'services';
|
|
376
|
+
// Check file roles
|
|
377
|
+
const roleCounts = {};
|
|
378
|
+
for (const file of files) {
|
|
379
|
+
roleCounts[file.role] = (roleCounts[file.role] || 0) + 1;
|
|
380
|
+
}
|
|
381
|
+
const topRole = Object.entries(roleCounts).sort((a, b) => b[1] - a[1])[0];
|
|
382
|
+
return topRole ? topRole[0] : 'misc';
|
|
383
|
+
}
|
|
384
|
+
// =============================================================================
|
|
385
|
+
// Framework Detection
|
|
386
|
+
// =============================================================================
|
|
387
|
+
function detectFrameworks(root) {
|
|
388
|
+
const frameworks = [];
|
|
389
|
+
try {
|
|
390
|
+
const pkgPath = join(root, 'package.json');
|
|
391
|
+
if (existsSync(pkgPath)) {
|
|
392
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
393
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
394
|
+
if (deps.next)
|
|
395
|
+
frameworks.push('Next.js');
|
|
396
|
+
if (deps.react)
|
|
397
|
+
frameworks.push('React');
|
|
398
|
+
if (deps.vue)
|
|
399
|
+
frameworks.push('Vue');
|
|
400
|
+
if (deps.svelte)
|
|
401
|
+
frameworks.push('Svelte');
|
|
402
|
+
if (deps['@tanstack/react-query'])
|
|
403
|
+
frameworks.push('TanStack Query');
|
|
404
|
+
if (deps.express)
|
|
405
|
+
frameworks.push('Express');
|
|
406
|
+
if (deps.hono)
|
|
407
|
+
frameworks.push('Hono');
|
|
408
|
+
if (deps.fastify)
|
|
409
|
+
frameworks.push('Fastify');
|
|
410
|
+
if (deps.drizzle)
|
|
411
|
+
frameworks.push('Drizzle');
|
|
412
|
+
if (deps.prisma)
|
|
413
|
+
frameworks.push('Prisma');
|
|
414
|
+
if (deps.zod)
|
|
415
|
+
frameworks.push('Zod');
|
|
416
|
+
if (deps.typescript)
|
|
417
|
+
frameworks.push('TypeScript');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// Ignore package.json errors
|
|
422
|
+
}
|
|
423
|
+
return frameworks;
|
|
424
|
+
}
|
|
425
|
+
// =============================================================================
|
|
426
|
+
// Markdown Generation
|
|
427
|
+
// =============================================================================
|
|
428
|
+
function generateMarkdown(index) {
|
|
429
|
+
let md = `# Codebase Map: ${basename(index.projectRoot)}\n\n`;
|
|
430
|
+
md += `> Generated ${index.generatedAt}\n\n`;
|
|
431
|
+
// Summary
|
|
432
|
+
md += `## Summary\n\n`;
|
|
433
|
+
md += `- **Files:** ${index.summary.totalFiles}\n`;
|
|
434
|
+
md += `- **Symbols:** ${index.summary.totalSymbols}\n`;
|
|
435
|
+
md += `- **Frameworks:** ${index.summary.frameworks.join(', ') || 'None detected'}\n\n`;
|
|
436
|
+
// Languages
|
|
437
|
+
md += `### Languages\n\n`;
|
|
438
|
+
for (const [lang, count] of Object.entries(index.summary.languages)) {
|
|
439
|
+
md += `- ${lang}: ${count} files\n`;
|
|
440
|
+
}
|
|
441
|
+
md += '\n';
|
|
442
|
+
// Directory structure
|
|
443
|
+
md += `## Structure\n\n`;
|
|
444
|
+
const sortedDirs = Object.values(index.directories).sort((a, b) => a.path.localeCompare(b.path));
|
|
445
|
+
for (const dir of sortedDirs) {
|
|
446
|
+
const indent = ' '.repeat(dir.path.split('/').length - 1);
|
|
447
|
+
md += `${indent}- **${dir.path}/** (${dir.role}, ${dir.fileCount} files)\n`;
|
|
448
|
+
if (dir.keyFiles.length > 0) {
|
|
449
|
+
for (const keyFile of dir.keyFiles) {
|
|
450
|
+
md += `${indent} - 📄 ${basename(keyFile)}\n`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
md += '\n';
|
|
455
|
+
// Key entry points
|
|
456
|
+
const entryPoints = Object.values(index.files).filter((f) => f.role === 'entry-point' || f.symbols.filter((s) => s.exported).length > 10);
|
|
457
|
+
if (entryPoints.length > 0) {
|
|
458
|
+
md += `## Entry Points\n\n`;
|
|
459
|
+
for (const file of entryPoints.slice(0, 10)) {
|
|
460
|
+
const exportedSymbols = file.symbols.filter((s) => s.exported);
|
|
461
|
+
md += `### ${file.relativePath}\n`;
|
|
462
|
+
md += `- **Role:** ${file.role}\n`;
|
|
463
|
+
md += `- **Lines:** ${file.linesOfCode}\n`;
|
|
464
|
+
md += `- **Exports:** ${exportedSymbols.length}\n\n`;
|
|
465
|
+
if (exportedSymbols.length > 0 && exportedSymbols.length <= 20) {
|
|
466
|
+
for (const sym of exportedSymbols) {
|
|
467
|
+
md += ` - \`${sym.name}\` (${sym.type})\n`;
|
|
468
|
+
}
|
|
469
|
+
md += '\n';
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Key symbols
|
|
474
|
+
const exportedSymbols = Object.entries(index.symbols)
|
|
475
|
+
.filter(([_, locs]) => locs.some((l) => l.exported))
|
|
476
|
+
.sort((a, b) => b[1].length - a[1].length)
|
|
477
|
+
.slice(0, 30);
|
|
478
|
+
if (exportedSymbols.length > 0) {
|
|
479
|
+
md += `## Key Symbols\n\n`;
|
|
480
|
+
md += `| Symbol | Type | Location |\n`;
|
|
481
|
+
md += `|--------|------|----------|\n`;
|
|
482
|
+
for (const [name, locs] of exportedSymbols) {
|
|
483
|
+
const loc = locs[0];
|
|
484
|
+
md += `| \`${name}\` | ${loc.type} | ${loc.file}:${loc.line} |\n`;
|
|
485
|
+
}
|
|
486
|
+
md += '\n';
|
|
487
|
+
}
|
|
488
|
+
return md;
|
|
489
|
+
}
|
|
490
|
+
// =============================================================================
|
|
491
|
+
// Main Export
|
|
492
|
+
// =============================================================================
|
|
493
|
+
export async function generateCodebaseMap(projectRoot, projectId, apiKey) {
|
|
494
|
+
try {
|
|
495
|
+
// Discover and analyze files
|
|
496
|
+
const filePaths = discoverFiles(projectRoot);
|
|
497
|
+
if (filePaths.length === 0) {
|
|
498
|
+
return { success: false, error: 'No supported files found in project' };
|
|
499
|
+
}
|
|
500
|
+
const files = {};
|
|
501
|
+
const languages = {};
|
|
502
|
+
let totalSymbols = 0;
|
|
503
|
+
for (const filePath of filePaths) {
|
|
504
|
+
try {
|
|
505
|
+
const entry = analyzeFile(filePath, projectRoot);
|
|
506
|
+
files[entry.relativePath] = entry;
|
|
507
|
+
const ext = extname(entry.relativePath);
|
|
508
|
+
languages[ext] = (languages[ext] || 0) + 1;
|
|
509
|
+
totalSymbols += entry.symbols.length;
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
// Skip files that can't be parsed
|
|
513
|
+
console.error(`Failed to analyze ${filePath}:`, err);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Build dependency graph
|
|
517
|
+
buildDependencyGraph(files, projectRoot);
|
|
518
|
+
// Build call graph
|
|
519
|
+
const { callGraph, reverseCallGraph } = buildCallGraph(files);
|
|
520
|
+
// Analyze directories
|
|
521
|
+
const directories = analyzeDirectories(files, projectRoot);
|
|
522
|
+
// Detect frameworks
|
|
523
|
+
const frameworks = detectFrameworks(projectRoot);
|
|
524
|
+
// Build symbols index
|
|
525
|
+
const symbols = {};
|
|
526
|
+
for (const file of Object.values(files)) {
|
|
527
|
+
for (const sym of file.symbols) {
|
|
528
|
+
if (!symbols[sym.name]) {
|
|
529
|
+
symbols[sym.name] = [];
|
|
530
|
+
}
|
|
531
|
+
symbols[sym.name].push({
|
|
532
|
+
file: file.relativePath,
|
|
533
|
+
line: sym.line,
|
|
534
|
+
type: sym.type,
|
|
535
|
+
exported: sym.exported,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Build index
|
|
540
|
+
const index = {
|
|
541
|
+
version: '1.0.0',
|
|
542
|
+
generatedAt: new Date().toISOString(),
|
|
543
|
+
projectRoot,
|
|
544
|
+
summary: {
|
|
545
|
+
totalFiles: Object.keys(files).length,
|
|
546
|
+
totalSymbols,
|
|
547
|
+
languages,
|
|
548
|
+
frameworks,
|
|
549
|
+
},
|
|
550
|
+
directories,
|
|
551
|
+
files,
|
|
552
|
+
symbols,
|
|
553
|
+
callGraph,
|
|
554
|
+
reverseCallGraph,
|
|
555
|
+
};
|
|
556
|
+
// Generate markdown
|
|
557
|
+
const markdown = generateMarkdown(index);
|
|
558
|
+
// Upload to API
|
|
559
|
+
const response = await fetch(`${API_BASE_URL}/api/v1/codebase/${projectId}/generate`, {
|
|
560
|
+
method: 'POST',
|
|
561
|
+
headers: {
|
|
562
|
+
'Content-Type': 'application/json',
|
|
563
|
+
Authorization: `Bearer ${apiKey}`,
|
|
564
|
+
},
|
|
565
|
+
body: JSON.stringify({ index, markdown }),
|
|
566
|
+
});
|
|
567
|
+
if (!response.ok) {
|
|
568
|
+
const text = await response.text();
|
|
569
|
+
return { success: false, error: `API error: ${response.status} - ${text}` };
|
|
570
|
+
}
|
|
571
|
+
return { success: true };
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
return { success: false, error: String(err) };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Generate a codebase map locally without uploading
|
|
579
|
+
* Useful for previewing or debugging
|
|
580
|
+
*/
|
|
581
|
+
export function generateCodebaseMapLocal(projectRoot) {
|
|
582
|
+
const filePaths = discoverFiles(projectRoot);
|
|
583
|
+
const files = {};
|
|
584
|
+
const languages = {};
|
|
585
|
+
let totalSymbols = 0;
|
|
586
|
+
for (const filePath of filePaths) {
|
|
587
|
+
try {
|
|
588
|
+
const entry = analyzeFile(filePath, projectRoot);
|
|
589
|
+
files[entry.relativePath] = entry;
|
|
590
|
+
const ext = extname(entry.relativePath);
|
|
591
|
+
languages[ext] = (languages[ext] || 0) + 1;
|
|
592
|
+
totalSymbols += entry.symbols.length;
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
// Skip files that can't be parsed
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
buildDependencyGraph(files, projectRoot);
|
|
599
|
+
const { callGraph, reverseCallGraph } = buildCallGraph(files);
|
|
600
|
+
const directories = analyzeDirectories(files, projectRoot);
|
|
601
|
+
const frameworks = detectFrameworks(projectRoot);
|
|
602
|
+
const symbols = {};
|
|
603
|
+
for (const file of Object.values(files)) {
|
|
604
|
+
for (const sym of file.symbols) {
|
|
605
|
+
if (!symbols[sym.name]) {
|
|
606
|
+
symbols[sym.name] = [];
|
|
607
|
+
}
|
|
608
|
+
symbols[sym.name].push({
|
|
609
|
+
file: file.relativePath,
|
|
610
|
+
line: sym.line,
|
|
611
|
+
type: sym.type,
|
|
612
|
+
exported: sym.exported,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const index = {
|
|
617
|
+
version: '1.0.0',
|
|
618
|
+
generatedAt: new Date().toISOString(),
|
|
619
|
+
projectRoot,
|
|
620
|
+
summary: {
|
|
621
|
+
totalFiles: Object.keys(files).length,
|
|
622
|
+
totalSymbols,
|
|
623
|
+
languages,
|
|
624
|
+
frameworks,
|
|
625
|
+
},
|
|
626
|
+
directories,
|
|
627
|
+
files,
|
|
628
|
+
symbols,
|
|
629
|
+
callGraph,
|
|
630
|
+
reverseCallGraph,
|
|
631
|
+
};
|
|
632
|
+
const markdown = generateMarkdown(index);
|
|
633
|
+
return { index, markdown };
|
|
634
|
+
}
|
package/dist/helpers/config.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export declare function resolveProjectId(): string;
|
|
|
34
34
|
export declare function resolveProjectIdAsync(): Promise<string>;
|
|
35
35
|
/**
|
|
36
36
|
* Gets the default project ID (synchronous version)
|
|
37
|
-
*
|
|
37
|
+
* Checks session-context.md first, then config, then falls back to cwd resolution
|
|
38
38
|
*/
|
|
39
39
|
export declare function getDefaultProjectId(): string;
|
|
40
40
|
/**
|