@claudetools/tools 0.7.1 → 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 CHANGED
@@ -10,6 +10,7 @@ import { dirname, join } from 'node:path';
10
10
  import { runSetup, runUninstall, runInit, runCleanup } from './setup.js';
11
11
  import { startServer } from './index.js';
12
12
  import { startWatcher, stopWatcher, watcherStatus } from './watcher.js';
13
+ import { generateCodebaseMap, generateCodebaseMapLocal } from './helpers/codebase-mapper.js';
13
14
  // Get version from package.json
14
15
  const __filename = fileURLToPath(import.meta.url);
15
16
  const __dirname = dirname(__filename);
@@ -53,6 +54,8 @@ Commands:
53
54
  watch Start the file watcher daemon
54
55
  watch --stop Stop the watcher daemon
55
56
  watch --status Check watcher status
57
+ map Generate codebase map for current project
58
+ map --local Generate map locally without uploading
56
59
 
57
60
  Running without options starts the MCP server.
58
61
 
@@ -103,6 +106,65 @@ else if (positionals[0] === 'watch') {
103
106
  });
104
107
  }
105
108
  }
109
+ else if (positionals[0] === 'map') {
110
+ // Handle map command
111
+ const mapArgs = process.argv.slice(3); // Get args after 'map'
112
+ const isLocal = mapArgs.includes('--local');
113
+ (async () => {
114
+ const cwd = process.cwd();
115
+ if (isLocal) {
116
+ console.log(`Generating codebase map for: ${cwd}`);
117
+ const { index, markdown } = generateCodebaseMapLocal(cwd);
118
+ console.log(`\nGenerated map with ${index.summary.totalFiles} files, ${index.summary.totalSymbols} symbols`);
119
+ console.log(`\n--- CODEBASE MAP ---\n`);
120
+ console.log(markdown);
121
+ }
122
+ else {
123
+ // Load config to get API key and project ID
124
+ const { existsSync: exists, readFileSync: read } = await import('fs');
125
+ const { join: pathJoin, basename } = await import('path');
126
+ const { homedir } = await import('os');
127
+ const configPath = pathJoin(homedir(), '.claudetools', 'config.json');
128
+ const projectsPath = pathJoin(homedir(), '.claudetools', 'projects.json');
129
+ if (!exists(configPath)) {
130
+ console.error('Not configured. Run: claudetools --setup');
131
+ process.exit(1);
132
+ }
133
+ const config = JSON.parse(read(configPath, 'utf-8'));
134
+ if (!config.apiKey) {
135
+ console.error('No API key configured. Run: claudetools --setup');
136
+ process.exit(1);
137
+ }
138
+ // Find project ID for current directory
139
+ let projectId = null;
140
+ if (exists(projectsPath)) {
141
+ const projects = JSON.parse(read(projectsPath, 'utf-8'));
142
+ const binding = projects.bindings?.find((b) => b.local_path === cwd);
143
+ if (binding) {
144
+ projectId = binding.project_id;
145
+ }
146
+ }
147
+ if (!projectId) {
148
+ console.error('Current directory is not a registered project.');
149
+ console.error('Run: claudetools init');
150
+ process.exit(1);
151
+ }
152
+ console.log(`Generating codebase map for: ${basename(cwd)} (${projectId})`);
153
+ const result = await generateCodebaseMap(cwd, projectId, config.apiKey);
154
+ if (result.success) {
155
+ console.log('Codebase map generated and uploaded successfully!');
156
+ console.log(`View at: ${config.apiUrl || 'https://api.claudetools.dev'}/api/v1/codebase/${projectId}/map`);
157
+ }
158
+ else {
159
+ console.error('Failed to generate map:', result.error);
160
+ process.exit(1);
161
+ }
162
+ }
163
+ })().catch((error) => {
164
+ console.error('Map generation failed:', error);
165
+ process.exit(1);
166
+ });
167
+ }
106
168
  else {
107
169
  // Start MCP server
108
170
  startServer();
@@ -758,7 +758,7 @@ export function registerToolHandlers(server) {
758
758
  blocked: '🚫', review: '👀', done: '✅', cancelled: '❌'
759
759
  };
760
760
  const emoji = statusEmoji[t.status] || '📝';
761
- let line = `- ${emoji} **${t.title}** (${t.id.slice(0, 8)}...)`;
761
+ let line = `- ${emoji} **${t.title}** (\`${t.id}\`)`;
762
762
  line += ` [${t.status}]`;
763
763
  if (t.priority !== 'medium')
764
764
  line += ` [${t.priority}]`;
@@ -817,12 +817,12 @@ export function registerToolHandlers(server) {
817
817
  }
818
818
  if (data.parent) {
819
819
  output += `\n## Parent\n`;
820
- output += `- **${data.parent.title}** (${data.parent.id.slice(0, 8)}...) [${data.parent.status}]\n`;
820
+ output += `- **${data.parent.title}** (\`${data.parent.id}\`) [${data.parent.status}]\n`;
821
821
  }
822
822
  if (data.subtasks?.length) {
823
823
  output += `\n## Subtasks (${data.subtasks.length})\n`;
824
824
  data.subtasks.forEach(s => {
825
- output += `- ${s.title} (${s.id.slice(0, 8)}...) [${s.status}]\n`;
825
+ output += `- ${s.title} (\`${s.id}\`) [${s.status}]\n`;
826
826
  });
827
827
  }
828
828
  if (data.context?.length) {
@@ -1430,14 +1430,20 @@ export function registerToolHandlers(server) {
1430
1430
  // =========================================================================
1431
1431
  case 'codebase_map': {
1432
1432
  // Use raw fetch since this endpoint returns markdown, not JSON
1433
+ // But we still need auth header
1434
+ const { getConfig } = await import('../helpers/config-manager.js');
1435
+ const config = getConfig();
1436
+ const apiKey = config.apiKey || process.env.CLAUDETOOLS_API_KEY || process.env.MEMORY_API_KEY;
1433
1437
  const url = `${API_BASE_URL}/api/v1/codebase/${projectId}/map`;
1434
- const response = await fetch(url);
1438
+ const response = await fetch(url, {
1439
+ headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {},
1440
+ });
1435
1441
  mcpLogger.toolResult(name, response.ok, timer());
1436
1442
  if (!response.ok) {
1437
1443
  return {
1438
1444
  content: [{
1439
1445
  type: 'text',
1440
- text: `# No Codebase Map Found\n\nThe codebase map hasn't been generated yet. Use the file watcher or API to generate one.`,
1446
+ text: `# No Codebase Map Found\n\nThe codebase map hasn't been generated yet. Use the file watcher or API to generate one.\n\n**Debug:** \`${url}\` returned ${response.status}`,
1441
1447
  }],
1442
1448
  };
1443
1449
  }
@@ -0,0 +1,66 @@
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
+ export interface FileSymbol {
8
+ name: string;
9
+ type: 'function' | 'class' | 'interface' | 'type' | 'const' | 'enum' | 'component';
10
+ exported: boolean;
11
+ line: number;
12
+ signature?: string;
13
+ description?: string;
14
+ }
15
+ export interface FileEntry {
16
+ path: string;
17
+ relativePath: string;
18
+ role: FileRole;
19
+ symbols: FileSymbol[];
20
+ imports: string[];
21
+ exports: string[];
22
+ dependencies: string[];
23
+ dependents: string[];
24
+ linesOfCode: number;
25
+ }
26
+ export type FileRole = 'entry-point' | 'route-handler' | 'api-endpoint' | 'mcp-tool' | 'component' | 'hook' | 'utility' | 'type-definitions' | 'config' | 'test' | 'model' | 'service' | 'extractor' | 'unknown';
27
+ export interface DirectoryEntry {
28
+ path: string;
29
+ role: string;
30
+ fileCount: number;
31
+ primaryLanguage: string;
32
+ keyFiles: string[];
33
+ }
34
+ export interface CodebaseIndex {
35
+ version: string;
36
+ generatedAt: string;
37
+ projectRoot: string;
38
+ summary: {
39
+ totalFiles: number;
40
+ totalSymbols: number;
41
+ languages: Record<string, number>;
42
+ frameworks: string[];
43
+ };
44
+ directories: Record<string, DirectoryEntry>;
45
+ files: Record<string, FileEntry>;
46
+ symbols: Record<string, {
47
+ file: string;
48
+ line: number;
49
+ type: string;
50
+ exported: boolean;
51
+ }[]>;
52
+ callGraph: Record<string, string[]>;
53
+ reverseCallGraph: Record<string, string[]>;
54
+ }
55
+ export declare function generateCodebaseMap(projectRoot: string, projectId: string, apiKey: string): Promise<{
56
+ success: boolean;
57
+ error?: string;
58
+ }>;
59
+ /**
60
+ * Generate a codebase map locally without uploading
61
+ * Useful for previewing or debugging
62
+ */
63
+ export declare function generateCodebaseMapLocal(projectRoot: string): {
64
+ index: CodebaseIndex;
65
+ markdown: string;
66
+ };
@@ -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
+ }
@@ -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
- * Throws if not already resolved - call resolveProjectIdAsync() first
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
  /**
@@ -3,6 +3,7 @@
3
3
  // =============================================================================
4
4
  import { mcpLogger } from '../logger.js';
5
5
  import * as fs from 'fs';
6
+ import * as path from 'path';
6
7
  import { getConfig, getConfigDir, getProjectsPath, } from './config-manager.js';
7
8
  import { getOrRegisterProject } from './project-registration.js';
8
9
  // Configuration - now loaded from config manager
@@ -10,6 +11,28 @@ const config = getConfig();
10
11
  export const API_BASE_URL = config.apiUrl;
11
12
  export const CLAUDETOOLS_DIR = getConfigDir();
12
13
  export const PROJECTS_FILE = getProjectsPath();
14
+ const SESSION_CONTEXT_FILE = path.join(CLAUDETOOLS_DIR, 'session-context.md');
15
+ /**
16
+ * Read project ID from session-context.md (written by session-start hook)
17
+ * This is the most reliable source since it's set by Claude Code's hook system
18
+ */
19
+ function getProjectIdFromSessionContext() {
20
+ try {
21
+ if (!fs.existsSync(SESSION_CONTEXT_FILE)) {
22
+ return null;
23
+ }
24
+ const content = fs.readFileSync(SESSION_CONTEXT_FILE, 'utf-8');
25
+ // Parse: **Project ID:** proj_xxxxx
26
+ const match = content.match(/\*\*Project ID:\*\*\s*(proj_[a-f0-9]{20})/);
27
+ if (match) {
28
+ return match[1];
29
+ }
30
+ return null;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
13
36
  // Cache for resolved project ID
14
37
  let resolvedProjectId = null;
15
38
  /**
@@ -93,6 +116,14 @@ export async function resolveProjectIdAsync() {
93
116
  mcpLogger.debug('MEMORY', `Using project ID from env: ${resolvedProjectId}`);
94
117
  return resolvedProjectId;
95
118
  }
119
+ // Check session-context.md (written by Claude Code's session-start hook)
120
+ // This is the most reliable source since MCP server's cwd isn't the project dir
121
+ const sessionProjectId = getProjectIdFromSessionContext();
122
+ if (sessionProjectId) {
123
+ resolvedProjectId = sessionProjectId;
124
+ mcpLogger.info('MEMORY', `Project resolved from session context: ${resolvedProjectId}`);
125
+ return resolvedProjectId;
126
+ }
96
127
  const cwd = process.cwd();
97
128
  // Check projects.json cache
98
129
  const binding = findProjectBinding(cwd);
@@ -134,13 +165,19 @@ export async function resolveProjectIdAsync() {
134
165
  let _defaultProjectId = null;
135
166
  /**
136
167
  * Gets the default project ID (synchronous version)
137
- * Throws if not already resolved - call resolveProjectIdAsync() first
168
+ * Checks session-context.md first, then config, then falls back to cwd resolution
138
169
  */
139
170
  export function getDefaultProjectId() {
140
171
  if (_defaultProjectId) {
141
172
  return _defaultProjectId;
142
173
  }
143
- // Try config first
174
+ // Check session-context.md first (most reliable for MCP server)
175
+ const sessionProjectId = getProjectIdFromSessionContext();
176
+ if (sessionProjectId) {
177
+ _defaultProjectId = sessionProjectId;
178
+ return _defaultProjectId;
179
+ }
180
+ // Try config second
144
181
  if (config.defaultProjectId) {
145
182
  if (!isValidProjectId(config.defaultProjectId)) {
146
183
  // Legacy format - convert to local_ format instead of throwing
package/dist/watcher.js CHANGED
@@ -4,7 +4,9 @@
4
4
  // Monitors project directories for changes and syncs with the API
5
5
  import { homedir } from 'os';
6
6
  import { join } from 'path';
7
- import { existsSync, readFileSync, writeFileSync, unlinkSync, watch, readdirSync, statSync } from 'fs';
7
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, watch, readdirSync, statSync, mkdirSync } from 'fs';
8
+ import { getProjectTemplate, SECTION_START } from './templates/claude-md.js';
9
+ import { generateCodebaseMap } from './helpers/codebase-mapper.js';
8
10
  // -----------------------------------------------------------------------------
9
11
  // Constants
10
12
  // -----------------------------------------------------------------------------
@@ -136,6 +138,94 @@ function discoverProjects(directories) {
136
138
  }
137
139
  return projects;
138
140
  }
141
+ /**
142
+ * Ensures .claude/CLAUDE.md exists for a project
143
+ * Creates it if missing, using the project template
144
+ */
145
+ function ensureProjectClaudeMd(projectPath, projectId, projectName) {
146
+ const claudeDir = join(projectPath, '.claude');
147
+ const claudeMdPath = join(claudeDir, 'CLAUDE.md');
148
+ // Skip if CLAUDE.md already exists with claudetools section
149
+ if (existsSync(claudeMdPath)) {
150
+ try {
151
+ const content = readFileSync(claudeMdPath, 'utf-8');
152
+ if (content.includes(SECTION_START)) {
153
+ return false; // Already has claudetools section
154
+ }
155
+ // Has CLAUDE.md but no claudetools section - append it
156
+ const template = getProjectTemplate(projectId, projectName);
157
+ writeFileSync(claudeMdPath, content.trimEnd() + '\n' + template);
158
+ log('info', `Added ClaudeTools section to existing CLAUDE.md: ${projectPath}`);
159
+ return true;
160
+ }
161
+ catch {
162
+ return false;
163
+ }
164
+ }
165
+ // Create .claude directory if needed
166
+ if (!existsSync(claudeDir)) {
167
+ try {
168
+ mkdirSync(claudeDir, { recursive: true });
169
+ }
170
+ catch (err) {
171
+ log('warn', `Could not create .claude directory for ${projectPath}: ${err}`);
172
+ return false;
173
+ }
174
+ }
175
+ // Create CLAUDE.md with template
176
+ try {
177
+ const template = getProjectTemplate(projectId, projectName);
178
+ writeFileSync(claudeMdPath, template);
179
+ log('info', `Created CLAUDE.md for project: ${projectPath}`);
180
+ return true;
181
+ }
182
+ catch (err) {
183
+ log('warn', `Could not create CLAUDE.md for ${projectPath}: ${err}`);
184
+ return false;
185
+ }
186
+ }
187
+ /**
188
+ * Ensures all synced projects have CLAUDE.md files
189
+ */
190
+ function ensureAllProjectsHaveClaudeMd(bindings) {
191
+ let created = 0;
192
+ for (const binding of bindings) {
193
+ if (binding.local_path && binding.project_id) {
194
+ if (ensureProjectClaudeMd(binding.local_path, binding.project_id, binding.project_name || 'Unknown')) {
195
+ created++;
196
+ }
197
+ }
198
+ }
199
+ if (created > 0) {
200
+ log('info', `Created CLAUDE.md for ${created} projects`);
201
+ }
202
+ }
203
+ /**
204
+ * Generate codebase maps for all synced projects
205
+ */
206
+ async function generateMapsForProjects(bindings, apiKey) {
207
+ let generated = 0;
208
+ for (const binding of bindings) {
209
+ if (binding.local_path && binding.project_id) {
210
+ try {
211
+ log('info', `Generating codebase map for ${binding.project_name || binding.local_path}...`);
212
+ const result = await generateCodebaseMap(binding.local_path, binding.project_id, apiKey);
213
+ if (result.success) {
214
+ generated++;
215
+ }
216
+ else {
217
+ log('warn', `Map generation failed for ${binding.project_name}: ${result.error}`);
218
+ }
219
+ }
220
+ catch (err) {
221
+ log('warn', `Map generation error for ${binding.project_name}: ${err}`);
222
+ }
223
+ }
224
+ }
225
+ if (generated > 0) {
226
+ log('info', `Generated codebase maps for ${generated} projects`);
227
+ }
228
+ }
139
229
  // -----------------------------------------------------------------------------
140
230
  // API Sync
141
231
  // -----------------------------------------------------------------------------
@@ -166,6 +256,12 @@ async function syncProjectsWithAPI(config, system, projects) {
166
256
  projectsData.bindings = data.bindings;
167
257
  projectsData.last_sync = new Date().toISOString();
168
258
  writeFileSync(PROJECTS_FILE, JSON.stringify(projectsData, null, 2));
259
+ // Auto-create CLAUDE.md for all synced projects
260
+ ensureAllProjectsHaveClaudeMd(data.bindings);
261
+ // Generate codebase maps for all projects (async, don't block)
262
+ generateMapsForProjects(data.bindings, config.apiKey).catch((err) => {
263
+ log('warn', `Map generation failed: ${err}`);
264
+ });
169
265
  }
170
266
  log('info', `Synced ${projects.length} projects with API`);
171
267
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudetools/tools",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Persistent AI memory, task management, and codebase intelligence for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -59,7 +59,8 @@
59
59
  "chalk": "^5.6.2",
60
60
  "nunjucks": "^3.2.4",
61
61
  "ora": "^9.0.0",
62
- "prompts": "^2.4.2"
62
+ "prompts": "^2.4.2",
63
+ "typescript": "^5.3.0"
63
64
  },
64
65
  "devDependencies": {
65
66
  "@types/node": "^20.10.0",