@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 +62 -0
- package/dist/handlers/tool-handlers.js +11 -5
- 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/watcher.js +97 -1
- package/package.json +3 -2
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}** (
|
|
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}** (
|
|
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} (
|
|
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
|
+
}
|
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
|
/**
|
package/dist/helpers/config.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
//
|
|
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.
|
|
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",
|