clast-ai 0.1.0
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/.claude-plugin/plugin.json +22 -0
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +419 -0
- package/commands/clast-status.md +9 -0
- package/dist/config/defaults.d.ts +19 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +44 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +48 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/index.d.ts +16 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +54 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/queries.d.ts +44 -0
- package/dist/db/queries.d.ts.map +1 -0
- package/dist/db/queries.js +251 -0
- package/dist/db/queries.js.map +1 -0
- package/dist/db/schema.d.ts +3 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +94 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/types.d.ts +44 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/llm/comment-generator.d.ts +26 -0
- package/dist/llm/comment-generator.d.ts.map +1 -0
- package/dist/llm/comment-generator.js +106 -0
- package/dist/llm/comment-generator.js.map +1 -0
- package/dist/llm/index.d.ts +24 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +54 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/prompts.d.ts +9 -0
- package/dist/llm/prompts.d.ts.map +1 -0
- package/dist/llm/prompts.js +79 -0
- package/dist/llm/prompts.js.map +1 -0
- package/dist/parser/call-resolver.d.ts +15 -0
- package/dist/parser/call-resolver.d.ts.map +1 -0
- package/dist/parser/call-resolver.js +16 -0
- package/dist/parser/call-resolver.js.map +1 -0
- package/dist/parser/extractor.d.ts +17 -0
- package/dist/parser/extractor.d.ts.map +1 -0
- package/dist/parser/extractor.js +254 -0
- package/dist/parser/extractor.js.map +1 -0
- package/dist/parser/index.d.ts +24 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +190 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/languages.d.ts +11 -0
- package/dist/parser/languages.d.ts.map +1 -0
- package/dist/parser/languages.js +74 -0
- package/dist/parser/languages.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +88 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/register-tools.d.ts +7 -0
- package/dist/server/register-tools.d.ts.map +1 -0
- package/dist/server/register-tools.js +17 -0
- package/dist/server/register-tools.js.map +1 -0
- package/dist/server/tools/clast-call-graph.d.ts +4 -0
- package/dist/server/tools/clast-call-graph.d.ts.map +1 -0
- package/dist/server/tools/clast-call-graph.js +70 -0
- package/dist/server/tools/clast-call-graph.js.map +1 -0
- package/dist/server/tools/clast-comment.d.ts +5 -0
- package/dist/server/tools/clast-comment.d.ts.map +1 -0
- package/dist/server/tools/clast-comment.js +64 -0
- package/dist/server/tools/clast-comment.js.map +1 -0
- package/dist/server/tools/clast-file-summary.d.ts +4 -0
- package/dist/server/tools/clast-file-summary.d.ts.map +1 -0
- package/dist/server/tools/clast-file-summary.js +42 -0
- package/dist/server/tools/clast-file-summary.js.map +1 -0
- package/dist/server/tools/clast-get-context.d.ts +4 -0
- package/dist/server/tools/clast-get-context.d.ts.map +1 -0
- package/dist/server/tools/clast-get-context.js +80 -0
- package/dist/server/tools/clast-get-context.js.map +1 -0
- package/dist/server/tools/clast-reindex.d.ts +5 -0
- package/dist/server/tools/clast-reindex.d.ts.map +1 -0
- package/dist/server/tools/clast-reindex.js +36 -0
- package/dist/server/tools/clast-reindex.js.map +1 -0
- package/dist/server/tools/clast-search.d.ts +4 -0
- package/dist/server/tools/clast-search.d.ts.map +1 -0
- package/dist/server/tools/clast-search.js +29 -0
- package/dist/server/tools/clast-search.js.map +1 -0
- package/dist/server/tools/clast-status.d.ts +5 -0
- package/dist/server/tools/clast-status.d.ts.map +1 -0
- package/dist/server/tools/clast-status.js +23 -0
- package/dist/server/tools/clast-status.js.map +1 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/watcher/gitignore.d.ts +9 -0
- package/dist/watcher/gitignore.d.ts.map +1 -0
- package/dist/watcher/gitignore.js +38 -0
- package/dist/watcher/gitignore.js.map +1 -0
- package/dist/watcher/index.d.ts +23 -0
- package/dist/watcher/index.d.ts.map +1 -0
- package/dist/watcher/index.js +111 -0
- package/dist/watcher/index.js.map +1 -0
- package/package.json +65 -0
- package/skills/clast-explore/SKILL.md +41 -0
- package/src/config/defaults.ts +61 -0
- package/src/config/index.ts +55 -0
- package/src/db/index.ts +63 -0
- package/src/db/queries.ts +332 -0
- package/src/db/schema.ts +106 -0
- package/src/db/types.ts +47 -0
- package/src/llm/comment-generator.ts +131 -0
- package/src/llm/index.ts +80 -0
- package/src/llm/prompts.ts +95 -0
- package/src/parser/call-resolver.ts +17 -0
- package/src/parser/extractor.ts +303 -0
- package/src/parser/index.ts +226 -0
- package/src/parser/languages.ts +86 -0
- package/src/server/index.ts +109 -0
- package/src/server/register-tools.ts +30 -0
- package/src/server/tools/clast-call-graph.ts +82 -0
- package/src/server/tools/clast-comment.ts +76 -0
- package/src/server/tools/clast-file-summary.ts +59 -0
- package/src/server/tools/clast-get-context.ts +97 -0
- package/src/server/tools/clast-reindex.ts +52 -0
- package/src/server/tools/clast-search.ts +38 -0
- package/src/server/tools/clast-status.ts +37 -0
- package/src/types.ts +69 -0
- package/src/watcher/gitignore.ts +42 -0
- package/src/watcher/index.ts +131 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import TreeSitter from 'web-tree-sitter';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
4
|
+
import { join, extname, relative } from 'node:path';
|
|
5
|
+
import { getLanguageForExtension, getGrammarWasmPath, getSupportedExtensions } from './languages.js';
|
|
6
|
+
import { extractFromTree } from './extractor.js';
|
|
7
|
+
import { resolveCallEdges } from './call-resolver.js';
|
|
8
|
+
import type { ClastDatabase } from '../db/index.js';
|
|
9
|
+
import type { ClastConfig } from '../config/index.js';
|
|
10
|
+
import type { AstNodeRow } from '../db/types.js';
|
|
11
|
+
|
|
12
|
+
export class ParserManager {
|
|
13
|
+
private parser: TreeSitter | null = null;
|
|
14
|
+
private languages = new Map<string, TreeSitter.Language>();
|
|
15
|
+
private config: ClastConfig;
|
|
16
|
+
private initialized = false;
|
|
17
|
+
|
|
18
|
+
constructor(config: ClastConfig) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async init(): Promise<void> {
|
|
23
|
+
if (this.initialized) return;
|
|
24
|
+
await TreeSitter.init();
|
|
25
|
+
this.parser = new TreeSitter();
|
|
26
|
+
this.initialized = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private async getLanguage(langName: string): Promise<TreeSitter.Language | null> {
|
|
30
|
+
if (this.languages.has(langName)) {
|
|
31
|
+
return this.languages.get(langName)!;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const wasmPath = getGrammarWasmPath(langName);
|
|
35
|
+
if (!wasmPath) {
|
|
36
|
+
console.error(`[clast] No WASM grammar found for ${langName}`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const language = await TreeSitter.Language.load(wasmPath);
|
|
42
|
+
this.languages.set(langName, language);
|
|
43
|
+
return language;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(`[clast] Failed to load grammar for ${langName}:`, err);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async parseSource(source: string, langName: string): Promise<TreeSitter.Tree | null> {
|
|
51
|
+
await this.init();
|
|
52
|
+
if (!this.parser) return null;
|
|
53
|
+
|
|
54
|
+
const language = await this.getLanguage(langName);
|
|
55
|
+
if (!language) return null;
|
|
56
|
+
|
|
57
|
+
this.parser.setLanguage(language);
|
|
58
|
+
return this.parser.parse(source);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async parseAndStore(filePath: string, projectRoot: string, db: ClastDatabase): Promise<boolean> {
|
|
62
|
+
const ext = extname(filePath);
|
|
63
|
+
const langEntry = getLanguageForExtension(ext);
|
|
64
|
+
if (!langEntry) return false;
|
|
65
|
+
|
|
66
|
+
const relPath = relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
67
|
+
|
|
68
|
+
let source: string;
|
|
69
|
+
try {
|
|
70
|
+
source = readFileSync(filePath, 'utf-8');
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hash = createHash('sha256').update(source).digest('hex');
|
|
76
|
+
const existingHash = db.queries.getFileHash(relPath);
|
|
77
|
+
if (existingHash === hash) return false;
|
|
78
|
+
|
|
79
|
+
const tree = await this.parseSource(source, langEntry.name);
|
|
80
|
+
if (!tree) return false;
|
|
81
|
+
|
|
82
|
+
const result = extractFromTree(
|
|
83
|
+
tree,
|
|
84
|
+
relPath,
|
|
85
|
+
langEntry.name,
|
|
86
|
+
source,
|
|
87
|
+
this.config.maxBodySize
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Delete old data for this file and upsert file record BEFORE inserting nodes
|
|
91
|
+
// (nodes have a FK to files.file_path)
|
|
92
|
+
db.queries.deleteNodesForFile(relPath);
|
|
93
|
+
db.queries.upsertFile(relPath, hash, langEntry.name, result.nodes.length);
|
|
94
|
+
|
|
95
|
+
// Insert nodes
|
|
96
|
+
const nodeRows: Omit<AstNodeRow, 'id'>[] = result.nodes.map(n => ({
|
|
97
|
+
file_path: n.filePath,
|
|
98
|
+
name: n.name,
|
|
99
|
+
type: n.type,
|
|
100
|
+
signature: n.signature,
|
|
101
|
+
start_line: n.startLine,
|
|
102
|
+
end_line: n.endLine,
|
|
103
|
+
start_col: n.startCol,
|
|
104
|
+
end_col: n.endCol,
|
|
105
|
+
parent_id: n.parentId,
|
|
106
|
+
language: n.language,
|
|
107
|
+
body_text: n.bodyText,
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
const nodeIds = db.queries.insertNodes(nodeRows);
|
|
111
|
+
|
|
112
|
+
// Insert edges
|
|
113
|
+
const edgeRows = result.edges
|
|
114
|
+
.filter(e => nodeIds[e.callerNodeIndex] !== undefined)
|
|
115
|
+
.map(e => ({
|
|
116
|
+
caller_id: nodeIds[e.callerNodeIndex]!,
|
|
117
|
+
callee_id: null as number | null,
|
|
118
|
+
callee_name: e.calleeName,
|
|
119
|
+
file_path: relPath,
|
|
120
|
+
line: e.line,
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
if (edgeRows.length > 0) {
|
|
124
|
+
db.queries.insertEdges(edgeRows);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Insert comments
|
|
128
|
+
for (const c of result.comments) {
|
|
129
|
+
const dbId = nodeIds[c.nodeIndex];
|
|
130
|
+
if (dbId !== undefined) {
|
|
131
|
+
db.queries.insertComment(dbId, c.text, c.source);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Save DB periodically
|
|
136
|
+
db.save();
|
|
137
|
+
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async fullIndex(
|
|
142
|
+
projectRoot: string,
|
|
143
|
+
db: ClastDatabase,
|
|
144
|
+
onProgress?: (file: string, current: number, total: number) => void
|
|
145
|
+
): Promise<{ filesProcessed: number; nodesFound: number; edgesResolved: number; durationMs: number }> {
|
|
146
|
+
await this.init();
|
|
147
|
+
|
|
148
|
+
const start = Date.now();
|
|
149
|
+
const supportedExts = new Set(getSupportedExtensions(this.config.languages));
|
|
150
|
+
const files = this.collectFiles(projectRoot, supportedExts);
|
|
151
|
+
|
|
152
|
+
let filesProcessed = 0;
|
|
153
|
+
for (let i = 0; i < files.length; i++) {
|
|
154
|
+
const file = files[i]!;
|
|
155
|
+
onProgress?.(file, i + 1, files.length);
|
|
156
|
+
if (await this.parseAndStore(file, projectRoot, db)) {
|
|
157
|
+
filesProcessed++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const edgesResolved = resolveCallEdges(db.queries);
|
|
162
|
+
|
|
163
|
+
// Final save
|
|
164
|
+
db.save();
|
|
165
|
+
|
|
166
|
+
const status = db.queries.getStatus();
|
|
167
|
+
return {
|
|
168
|
+
filesProcessed,
|
|
169
|
+
nodesFound: status.total_nodes,
|
|
170
|
+
edgesResolved,
|
|
171
|
+
durationMs: Date.now() - start,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async incrementalUpdate(filePath: string, projectRoot: string, db: ClastDatabase): Promise<boolean> {
|
|
176
|
+
const parsed = await this.parseAndStore(filePath, projectRoot, db);
|
|
177
|
+
if (parsed) {
|
|
178
|
+
resolveCallEdges(db.queries);
|
|
179
|
+
db.save();
|
|
180
|
+
}
|
|
181
|
+
return parsed;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
removeFile(filePath: string, projectRoot: string, db: ClastDatabase): void {
|
|
185
|
+
const relPath = relative(projectRoot, filePath).replace(/\\/g, '/');
|
|
186
|
+
db.queries.deleteFile(relPath);
|
|
187
|
+
db.save();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private collectFiles(dir: string, supportedExts: Set<string>, depth: number = 0): string[] {
|
|
191
|
+
if (depth > 20) return [];
|
|
192
|
+
|
|
193
|
+
const files: string[] = [];
|
|
194
|
+
let entries: string[];
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
entries = readdirSync(dir);
|
|
198
|
+
} catch {
|
|
199
|
+
return files;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const entry of entries) {
|
|
203
|
+
if (this.config.ignoredPaths.includes(entry)) continue;
|
|
204
|
+
if (entry.startsWith('.')) continue;
|
|
205
|
+
|
|
206
|
+
const fullPath = join(dir, entry);
|
|
207
|
+
let stat;
|
|
208
|
+
try {
|
|
209
|
+
stat = statSync(fullPath);
|
|
210
|
+
} catch {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (stat.isDirectory()) {
|
|
215
|
+
files.push(...this.collectFiles(fullPath, supportedExts, depth + 1));
|
|
216
|
+
} else if (stat.isFile()) {
|
|
217
|
+
const ext = extname(entry);
|
|
218
|
+
if (supportedExts.has(ext)) {
|
|
219
|
+
files.push(fullPath);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return files;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { join, dirname } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
|
|
6
|
+
interface LanguageEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
extensions: string[];
|
|
9
|
+
wasmFile: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LANGUAGES: LanguageEntry[] = [
|
|
13
|
+
{ name: 'typescript', extensions: ['.ts'], wasmFile: 'tree-sitter-typescript.wasm' },
|
|
14
|
+
{ name: 'tsx', extensions: ['.tsx'], wasmFile: 'tree-sitter-tsx.wasm' },
|
|
15
|
+
{ name: 'javascript', extensions: ['.js', '.jsx', '.mjs', '.cjs'], wasmFile: 'tree-sitter-javascript.wasm' },
|
|
16
|
+
{ name: 'python', extensions: ['.py', '.pyw'], wasmFile: 'tree-sitter-python.wasm' },
|
|
17
|
+
{ name: 'java', extensions: ['.java'], wasmFile: 'tree-sitter-java.wasm' },
|
|
18
|
+
{ name: 'csharp', extensions: ['.cs'], wasmFile: 'tree-sitter-c_sharp.wasm' },
|
|
19
|
+
{ name: 'go', extensions: ['.go'], wasmFile: 'tree-sitter-go.wasm' },
|
|
20
|
+
{ name: 'rust', extensions: ['.rs'], wasmFile: 'tree-sitter-rust.wasm' },
|
|
21
|
+
{ name: 'c', extensions: ['.c', '.h'], wasmFile: 'tree-sitter-c.wasm' },
|
|
22
|
+
{ name: 'cpp', extensions: ['.cpp', '.hpp', '.cc', '.hh', '.cxx', '.hxx'], wasmFile: 'tree-sitter-cpp.wasm' },
|
|
23
|
+
{ name: 'ruby', extensions: ['.rb'], wasmFile: 'tree-sitter-ruby.wasm' },
|
|
24
|
+
{ name: 'php', extensions: ['.php'], wasmFile: 'tree-sitter-php.wasm' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Extension → language entry mapping
|
|
28
|
+
const EXT_MAP = new Map<string, LanguageEntry>();
|
|
29
|
+
for (const lang of LANGUAGES) {
|
|
30
|
+
for (const ext of lang.extensions) {
|
|
31
|
+
EXT_MAP.set(ext, lang);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getLanguageForExtension(ext: string): LanguageEntry | undefined {
|
|
36
|
+
return EXT_MAP.get(ext.toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getGrammarWasmPath(langName: string): string | null {
|
|
40
|
+
const entry = LANGUAGES.find(l => l.name === langName);
|
|
41
|
+
if (!entry) return null;
|
|
42
|
+
|
|
43
|
+
// Try to resolve from tree-sitter-wasms npm package first
|
|
44
|
+
const require = createRequire(import.meta.url);
|
|
45
|
+
try {
|
|
46
|
+
const wasmsDir = dirname(require.resolve('tree-sitter-wasms/package.json'));
|
|
47
|
+
const npmPath = join(wasmsDir, 'out', entry.wasmFile);
|
|
48
|
+
if (existsSync(npmPath)) return npmPath;
|
|
49
|
+
} catch {
|
|
50
|
+
// tree-sitter-wasms not installed, try fallbacks
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
|
|
55
|
+
// Check grammars/ directory relative to project
|
|
56
|
+
const candidates = [
|
|
57
|
+
// Custom grammars dir (env override)
|
|
58
|
+
process.env.CLAST_GRAMMARS_DIR ? join(process.env.CLAST_GRAMMARS_DIR, entry.wasmFile) : null,
|
|
59
|
+
// Plugin root
|
|
60
|
+
process.env.CLAUDE_PLUGIN_ROOT ? join(process.env.CLAUDE_PLUGIN_ROOT, 'grammars', entry.wasmFile) : null,
|
|
61
|
+
// Relative to dist/
|
|
62
|
+
join(__dirname, '..', '..', 'grammars', entry.wasmFile),
|
|
63
|
+
// CWD
|
|
64
|
+
join(process.cwd(), 'grammars', entry.wasmFile),
|
|
65
|
+
].filter(Boolean) as string[];
|
|
66
|
+
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
if (existsSync(candidate)) return candidate;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getSupportedExtensions(enabledLanguages?: string[]): string[] {
|
|
75
|
+
const result: string[] = [];
|
|
76
|
+
for (const lang of LANGUAGES) {
|
|
77
|
+
if (!enabledLanguages || enabledLanguages.includes(lang.name)) {
|
|
78
|
+
result.push(...lang.extensions);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getAllLanguageNames(): string[] {
|
|
85
|
+
return LANGUAGES.map(l => l.name);
|
|
86
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { loadConfig, resolveDbPath } from '../config/index.js';
|
|
8
|
+
import { ClastDatabase } from '../db/index.js';
|
|
9
|
+
import { ParserManager } from '../parser/index.js';
|
|
10
|
+
import { FileWatcher } from '../watcher/index.js';
|
|
11
|
+
import { LlmClient } from '../llm/index.js';
|
|
12
|
+
import { CommentGenerator } from '../llm/comment-generator.js';
|
|
13
|
+
import { registerAllTools } from './register-tools.js';
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
// Determine project root
|
|
17
|
+
const projectRoot = resolve(
|
|
18
|
+
process.env.CLAST_PROJECT_DIR ??
|
|
19
|
+
process.env.CLAUDE_PROJECT_DIR ??
|
|
20
|
+
process.cwd()
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
console.error(`[clast] Project root: ${projectRoot}`);
|
|
24
|
+
|
|
25
|
+
// Load configuration
|
|
26
|
+
const config = loadConfig(projectRoot);
|
|
27
|
+
console.error(`[clast] Config loaded. Languages: ${config.languages.join(', ')}`);
|
|
28
|
+
|
|
29
|
+
// Open database (async — sql.js uses WASM)
|
|
30
|
+
const dbPath = resolveDbPath(projectRoot, config);
|
|
31
|
+
console.error(`[clast] Database: ${dbPath}`);
|
|
32
|
+
const db = await ClastDatabase.open(dbPath);
|
|
33
|
+
|
|
34
|
+
// Create parser (async — web-tree-sitter uses WASM)
|
|
35
|
+
const parser = new ParserManager(config);
|
|
36
|
+
await parser.init();
|
|
37
|
+
|
|
38
|
+
// Create watcher
|
|
39
|
+
const watcher = new FileWatcher(projectRoot, config, db, parser);
|
|
40
|
+
|
|
41
|
+
// Create LLM client + comment generator
|
|
42
|
+
const llmClient = new LlmClient(config.llm.endpoint, config.llm.model, config.llm.apiKey);
|
|
43
|
+
const commentGen = new CommentGenerator(llmClient, db.queries, config);
|
|
44
|
+
|
|
45
|
+
if (llmClient.isConfigured) {
|
|
46
|
+
console.error(`[clast] LLM configured: ${config.llm.model} @ ${config.llm.endpoint}`);
|
|
47
|
+
} else {
|
|
48
|
+
console.error('[clast] LLM not configured (comment generation disabled)');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create MCP server
|
|
52
|
+
const server = new McpServer({
|
|
53
|
+
name: 'clast',
|
|
54
|
+
version: '0.1.0',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Register all tools
|
|
58
|
+
registerAllTools(server, db, parser, watcher, commentGen, projectRoot);
|
|
59
|
+
|
|
60
|
+
// Perform initial index
|
|
61
|
+
console.error('[clast] Starting initial index...');
|
|
62
|
+
const indexResult = await parser.fullIndex(projectRoot, db, (file, current, total) => {
|
|
63
|
+
if (current % 50 === 0 || current === total) {
|
|
64
|
+
console.error(`[clast] Indexing: ${current}/${total} files`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
console.error(
|
|
68
|
+
`[clast] Index complete: ${indexResult.filesProcessed} files, ` +
|
|
69
|
+
`${indexResult.nodesFound} nodes, ${indexResult.edgesResolved} edges resolved ` +
|
|
70
|
+
`(${indexResult.durationMs}ms)`
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Start file watcher
|
|
74
|
+
watcher.start();
|
|
75
|
+
watcher.on('indexed', ({ filePath, nodesFound }: { filePath: string; nodesFound: number }) => {
|
|
76
|
+
console.error(`[clast] Updated: ${filePath} (${nodesFound} nodes)`);
|
|
77
|
+
});
|
|
78
|
+
watcher.on('deleted', ({ filePath }: { filePath: string }) => {
|
|
79
|
+
console.error(`[clast] Removed: ${filePath}`);
|
|
80
|
+
});
|
|
81
|
+
watcher.on('error', ({ filePath, error }: { filePath: string; error: Error }) => {
|
|
82
|
+
console.error(`[clast] Error processing ${filePath}:`, error.message);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (watcher.isActive) {
|
|
86
|
+
console.error('[clast] File watcher active');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Connect to stdio transport
|
|
90
|
+
const transport = new StdioServerTransport();
|
|
91
|
+
await server.connect(transport);
|
|
92
|
+
console.error('[clast] MCP server running on stdio');
|
|
93
|
+
|
|
94
|
+
// Graceful shutdown
|
|
95
|
+
const shutdown = () => {
|
|
96
|
+
console.error('[clast] Shutting down...');
|
|
97
|
+
watcher.stop();
|
|
98
|
+
db.close();
|
|
99
|
+
process.exit(0);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
process.on('SIGINT', shutdown);
|
|
103
|
+
process.on('SIGTERM', shutdown);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
main().catch((err) => {
|
|
107
|
+
console.error('[clast] Fatal error:', err);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { ClastDatabase } from '../db/index.js';
|
|
3
|
+
import type { ParserManager } from '../parser/index.js';
|
|
4
|
+
import type { FileWatcher } from '../watcher/index.js';
|
|
5
|
+
import type { CommentGenerator } from '../llm/comment-generator.js';
|
|
6
|
+
|
|
7
|
+
import { registerSearchTool } from './tools/clast-search.js';
|
|
8
|
+
import { registerCallGraphTool } from './tools/clast-call-graph.js';
|
|
9
|
+
import { registerFileSummaryTool } from './tools/clast-file-summary.js';
|
|
10
|
+
import { registerGetContextTool } from './tools/clast-get-context.js';
|
|
11
|
+
import { registerStatusTool } from './tools/clast-status.js';
|
|
12
|
+
import { registerReindexTool } from './tools/clast-reindex.js';
|
|
13
|
+
import { registerCommentTool } from './tools/clast-comment.js';
|
|
14
|
+
|
|
15
|
+
export function registerAllTools(
|
|
16
|
+
server: McpServer,
|
|
17
|
+
db: ClastDatabase,
|
|
18
|
+
parser: ParserManager,
|
|
19
|
+
watcher: FileWatcher,
|
|
20
|
+
commentGen: CommentGenerator,
|
|
21
|
+
projectRoot: string
|
|
22
|
+
): void {
|
|
23
|
+
registerSearchTool(server, db);
|
|
24
|
+
registerCallGraphTool(server, db);
|
|
25
|
+
registerFileSummaryTool(server, db);
|
|
26
|
+
registerGetContextTool(server, db);
|
|
27
|
+
registerStatusTool(server, db, watcher);
|
|
28
|
+
registerReindexTool(server, db, parser, projectRoot);
|
|
29
|
+
registerCommentTool(server, db, commentGen);
|
|
30
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { ClastDatabase } from '../../db/index.js';
|
|
4
|
+
|
|
5
|
+
export function registerCallGraphTool(server: McpServer, db: ClastDatabase): void {
|
|
6
|
+
server.tool(
|
|
7
|
+
'clast_call_graph',
|
|
8
|
+
'Get the call graph for a function or method: what it calls (callees) and what calls it (callers). Supports configurable traversal depth.',
|
|
9
|
+
{
|
|
10
|
+
name: z.string().describe('Function or method name to trace'),
|
|
11
|
+
depth: z.number().int().min(1).max(5).default(2).describe('How many levels deep to traverse'),
|
|
12
|
+
direction: z.enum(['callers', 'callees', 'both']).default('both').describe('Direction to traverse'),
|
|
13
|
+
},
|
|
14
|
+
async ({ name, depth, direction }) => {
|
|
15
|
+
const node = db.queries.getNodeByName(name);
|
|
16
|
+
if (node.length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{
|
|
19
|
+
type: 'text' as const,
|
|
20
|
+
text: JSON.stringify({ error: `No symbol found with name "${name}"` }),
|
|
21
|
+
}],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result: Record<string, unknown> = {
|
|
26
|
+
symbol: {
|
|
27
|
+
name: node[0]!.name,
|
|
28
|
+
type: node[0]!.type,
|
|
29
|
+
filePath: node[0]!.file_path,
|
|
30
|
+
startLine: node[0]!.start_line,
|
|
31
|
+
endLine: node[0]!.end_line,
|
|
32
|
+
signature: node[0]!.signature,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (direction === 'callers' || direction === 'both') {
|
|
37
|
+
const callers = db.queries.getCallersOf(name, depth);
|
|
38
|
+
result.callers = callers.map(n => ({
|
|
39
|
+
name: n.name,
|
|
40
|
+
type: n.type,
|
|
41
|
+
filePath: n.file_path,
|
|
42
|
+
startLine: n.start_line,
|
|
43
|
+
signature: n.signature,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (direction === 'callees' || direction === 'both') {
|
|
48
|
+
const callees = db.queries.getCalleesOf(name, depth);
|
|
49
|
+
result.callees = callees.map(n => ({
|
|
50
|
+
name: n.name,
|
|
51
|
+
type: n.type,
|
|
52
|
+
filePath: n.file_path,
|
|
53
|
+
startLine: n.start_line,
|
|
54
|
+
signature: n.signature,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Also include direct call edge details for the primary node
|
|
59
|
+
if (node[0]) {
|
|
60
|
+
const edges = db.queries.getCallEdgesForNode(node[0].id);
|
|
61
|
+
result.directCalls = {
|
|
62
|
+
outgoing: edges.outgoing.map(e => ({
|
|
63
|
+
calleeName: e.callee_name,
|
|
64
|
+
line: e.line,
|
|
65
|
+
resolved: e.callee_id !== null,
|
|
66
|
+
})),
|
|
67
|
+
incoming: edges.incoming.map(e => ({
|
|
68
|
+
callerId: e.caller_id,
|
|
69
|
+
line: e.line,
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
content: [{
|
|
76
|
+
type: 'text' as const,
|
|
77
|
+
text: JSON.stringify(result, null, 2),
|
|
78
|
+
}],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { ClastDatabase } from '../../db/index.js';
|
|
4
|
+
import type { CommentGenerator } from '../../llm/comment-generator.js';
|
|
5
|
+
|
|
6
|
+
export function registerCommentTool(
|
|
7
|
+
server: McpServer,
|
|
8
|
+
db: ClastDatabase,
|
|
9
|
+
commentGen: CommentGenerator
|
|
10
|
+
): void {
|
|
11
|
+
server.tool(
|
|
12
|
+
'clast_comment',
|
|
13
|
+
'Get or generate documentation comments for a symbol. By default returns existing comments. Set generate=true to create a new comment via LLM if none exists.',
|
|
14
|
+
{
|
|
15
|
+
name: z.string().describe('Symbol name to get/generate comment for'),
|
|
16
|
+
generate: z.boolean().default(false).describe('If true and no comment exists, generate one via LLM'),
|
|
17
|
+
},
|
|
18
|
+
async ({ name, generate }) => {
|
|
19
|
+
const nodes = db.queries.getNodeByName(name);
|
|
20
|
+
if (nodes.length === 0) {
|
|
21
|
+
return {
|
|
22
|
+
content: [{
|
|
23
|
+
type: 'text' as const,
|
|
24
|
+
text: JSON.stringify({ error: `No symbol found with name "${name}"` }),
|
|
25
|
+
}],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const results = [];
|
|
30
|
+
for (const node of nodes) {
|
|
31
|
+
const comments = db.queries.getCommentsForNode(node.id);
|
|
32
|
+
const original = comments.find(c => c.source === 'original');
|
|
33
|
+
const generated = comments.find(c => c.source === 'generated');
|
|
34
|
+
|
|
35
|
+
const entry: Record<string, unknown> = {
|
|
36
|
+
name: node.name,
|
|
37
|
+
type: node.type,
|
|
38
|
+
filePath: node.file_path,
|
|
39
|
+
startLine: node.start_line,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (original) {
|
|
43
|
+
entry.comment = original.text;
|
|
44
|
+
entry.source = 'original';
|
|
45
|
+
} else if (generated) {
|
|
46
|
+
entry.comment = generated.text;
|
|
47
|
+
entry.source = 'generated';
|
|
48
|
+
} else if (generate) {
|
|
49
|
+
if (!commentGen.isAvailable) {
|
|
50
|
+
entry.error = 'LLM not configured. Set llm.model in clast.config.json.';
|
|
51
|
+
} else {
|
|
52
|
+
const result = await commentGen.generateForNode(node.id);
|
|
53
|
+
if (result) {
|
|
54
|
+
entry.comment = result;
|
|
55
|
+
entry.source = 'generated';
|
|
56
|
+
} else {
|
|
57
|
+
entry.error = 'Failed to generate comment';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
entry.comment = null;
|
|
62
|
+
entry.source = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
results.push(entry);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
content: [{
|
|
70
|
+
type: 'text' as const,
|
|
71
|
+
text: JSON.stringify(results, null, 2),
|
|
72
|
+
}],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import type { ClastDatabase } from '../../db/index.js';
|
|
4
|
+
|
|
5
|
+
export function registerFileSummaryTool(server: McpServer, db: ClastDatabase): void {
|
|
6
|
+
server.tool(
|
|
7
|
+
'clast_file_summary',
|
|
8
|
+
'Get a structured summary of all symbols in a file, grouped by type (imports, classes, functions, exports, etc.). Use instead of reading the whole file to understand its structure.',
|
|
9
|
+
{
|
|
10
|
+
filePath: z.string().describe('Relative file path (forward slashes, e.g. "src/parser/index.ts")'),
|
|
11
|
+
},
|
|
12
|
+
async ({ filePath }) => {
|
|
13
|
+
// Normalize path
|
|
14
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
15
|
+
const nodes = db.queries.getNodesByFile(normalized);
|
|
16
|
+
|
|
17
|
+
if (nodes.length === 0) {
|
|
18
|
+
return {
|
|
19
|
+
content: [{
|
|
20
|
+
type: 'text' as const,
|
|
21
|
+
text: JSON.stringify({ error: `No indexed data for file "${normalized}". Try clast_reindex first.` }),
|
|
22
|
+
}],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Group by type
|
|
27
|
+
const grouped: Record<string, Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
signature: string;
|
|
30
|
+
startLine: number;
|
|
31
|
+
endLine: number;
|
|
32
|
+
}>> = {};
|
|
33
|
+
|
|
34
|
+
for (const node of nodes) {
|
|
35
|
+
const group = grouped[node.type] ??= [];
|
|
36
|
+
group.push({
|
|
37
|
+
name: node.name,
|
|
38
|
+
signature: node.signature,
|
|
39
|
+
startLine: node.start_line,
|
|
40
|
+
endLine: node.end_line,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const summary = {
|
|
45
|
+
filePath: normalized,
|
|
46
|
+
language: nodes[0]?.language,
|
|
47
|
+
totalSymbols: nodes.length,
|
|
48
|
+
symbols: grouped,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: 'text' as const,
|
|
54
|
+
text: JSON.stringify(summary, null, 2),
|
|
55
|
+
}],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}
|