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.
Files changed (133) hide show
  1. package/.claude-plugin/plugin.json +22 -0
  2. package/.mcp.json +8 -0
  3. package/LICENSE +21 -0
  4. package/README.md +419 -0
  5. package/commands/clast-status.md +9 -0
  6. package/dist/config/defaults.d.ts +19 -0
  7. package/dist/config/defaults.d.ts.map +1 -0
  8. package/dist/config/defaults.js +44 -0
  9. package/dist/config/defaults.js.map +1 -0
  10. package/dist/config/index.d.ts +5 -0
  11. package/dist/config/index.d.ts.map +1 -0
  12. package/dist/config/index.js +48 -0
  13. package/dist/config/index.js.map +1 -0
  14. package/dist/db/index.d.ts +16 -0
  15. package/dist/db/index.d.ts.map +1 -0
  16. package/dist/db/index.js +54 -0
  17. package/dist/db/index.js.map +1 -0
  18. package/dist/db/queries.d.ts +44 -0
  19. package/dist/db/queries.d.ts.map +1 -0
  20. package/dist/db/queries.js +251 -0
  21. package/dist/db/queries.js.map +1 -0
  22. package/dist/db/schema.d.ts +3 -0
  23. package/dist/db/schema.d.ts.map +1 -0
  24. package/dist/db/schema.js +94 -0
  25. package/dist/db/schema.js.map +1 -0
  26. package/dist/db/types.d.ts +44 -0
  27. package/dist/db/types.d.ts.map +1 -0
  28. package/dist/db/types.js +2 -0
  29. package/dist/db/types.js.map +1 -0
  30. package/dist/llm/comment-generator.d.ts +26 -0
  31. package/dist/llm/comment-generator.d.ts.map +1 -0
  32. package/dist/llm/comment-generator.js +106 -0
  33. package/dist/llm/comment-generator.js.map +1 -0
  34. package/dist/llm/index.d.ts +24 -0
  35. package/dist/llm/index.d.ts.map +1 -0
  36. package/dist/llm/index.js +54 -0
  37. package/dist/llm/index.js.map +1 -0
  38. package/dist/llm/prompts.d.ts +9 -0
  39. package/dist/llm/prompts.d.ts.map +1 -0
  40. package/dist/llm/prompts.js +79 -0
  41. package/dist/llm/prompts.js.map +1 -0
  42. package/dist/parser/call-resolver.d.ts +15 -0
  43. package/dist/parser/call-resolver.d.ts.map +1 -0
  44. package/dist/parser/call-resolver.js +16 -0
  45. package/dist/parser/call-resolver.js.map +1 -0
  46. package/dist/parser/extractor.d.ts +17 -0
  47. package/dist/parser/extractor.d.ts.map +1 -0
  48. package/dist/parser/extractor.js +254 -0
  49. package/dist/parser/extractor.js.map +1 -0
  50. package/dist/parser/index.d.ts +24 -0
  51. package/dist/parser/index.d.ts.map +1 -0
  52. package/dist/parser/index.js +190 -0
  53. package/dist/parser/index.js.map +1 -0
  54. package/dist/parser/languages.d.ts +11 -0
  55. package/dist/parser/languages.d.ts.map +1 -0
  56. package/dist/parser/languages.js +74 -0
  57. package/dist/parser/languages.js.map +1 -0
  58. package/dist/server/index.d.ts +3 -0
  59. package/dist/server/index.d.ts.map +1 -0
  60. package/dist/server/index.js +88 -0
  61. package/dist/server/index.js.map +1 -0
  62. package/dist/server/register-tools.d.ts +7 -0
  63. package/dist/server/register-tools.d.ts.map +1 -0
  64. package/dist/server/register-tools.js +17 -0
  65. package/dist/server/register-tools.js.map +1 -0
  66. package/dist/server/tools/clast-call-graph.d.ts +4 -0
  67. package/dist/server/tools/clast-call-graph.d.ts.map +1 -0
  68. package/dist/server/tools/clast-call-graph.js +70 -0
  69. package/dist/server/tools/clast-call-graph.js.map +1 -0
  70. package/dist/server/tools/clast-comment.d.ts +5 -0
  71. package/dist/server/tools/clast-comment.d.ts.map +1 -0
  72. package/dist/server/tools/clast-comment.js +64 -0
  73. package/dist/server/tools/clast-comment.js.map +1 -0
  74. package/dist/server/tools/clast-file-summary.d.ts +4 -0
  75. package/dist/server/tools/clast-file-summary.d.ts.map +1 -0
  76. package/dist/server/tools/clast-file-summary.js +42 -0
  77. package/dist/server/tools/clast-file-summary.js.map +1 -0
  78. package/dist/server/tools/clast-get-context.d.ts +4 -0
  79. package/dist/server/tools/clast-get-context.d.ts.map +1 -0
  80. package/dist/server/tools/clast-get-context.js +80 -0
  81. package/dist/server/tools/clast-get-context.js.map +1 -0
  82. package/dist/server/tools/clast-reindex.d.ts +5 -0
  83. package/dist/server/tools/clast-reindex.d.ts.map +1 -0
  84. package/dist/server/tools/clast-reindex.js +36 -0
  85. package/dist/server/tools/clast-reindex.js.map +1 -0
  86. package/dist/server/tools/clast-search.d.ts +4 -0
  87. package/dist/server/tools/clast-search.d.ts.map +1 -0
  88. package/dist/server/tools/clast-search.js +29 -0
  89. package/dist/server/tools/clast-search.js.map +1 -0
  90. package/dist/server/tools/clast-status.d.ts +5 -0
  91. package/dist/server/tools/clast-status.d.ts.map +1 -0
  92. package/dist/server/tools/clast-status.js +23 -0
  93. package/dist/server/tools/clast-status.js.map +1 -0
  94. package/dist/types.d.ts +52 -0
  95. package/dist/types.d.ts.map +1 -0
  96. package/dist/types.js +2 -0
  97. package/dist/types.js.map +1 -0
  98. package/dist/watcher/gitignore.d.ts +9 -0
  99. package/dist/watcher/gitignore.d.ts.map +1 -0
  100. package/dist/watcher/gitignore.js +38 -0
  101. package/dist/watcher/gitignore.js.map +1 -0
  102. package/dist/watcher/index.d.ts +23 -0
  103. package/dist/watcher/index.d.ts.map +1 -0
  104. package/dist/watcher/index.js +111 -0
  105. package/dist/watcher/index.js.map +1 -0
  106. package/package.json +65 -0
  107. package/skills/clast-explore/SKILL.md +41 -0
  108. package/src/config/defaults.ts +61 -0
  109. package/src/config/index.ts +55 -0
  110. package/src/db/index.ts +63 -0
  111. package/src/db/queries.ts +332 -0
  112. package/src/db/schema.ts +106 -0
  113. package/src/db/types.ts +47 -0
  114. package/src/llm/comment-generator.ts +131 -0
  115. package/src/llm/index.ts +80 -0
  116. package/src/llm/prompts.ts +95 -0
  117. package/src/parser/call-resolver.ts +17 -0
  118. package/src/parser/extractor.ts +303 -0
  119. package/src/parser/index.ts +226 -0
  120. package/src/parser/languages.ts +86 -0
  121. package/src/server/index.ts +109 -0
  122. package/src/server/register-tools.ts +30 -0
  123. package/src/server/tools/clast-call-graph.ts +82 -0
  124. package/src/server/tools/clast-comment.ts +76 -0
  125. package/src/server/tools/clast-file-summary.ts +59 -0
  126. package/src/server/tools/clast-get-context.ts +97 -0
  127. package/src/server/tools/clast-reindex.ts +52 -0
  128. package/src/server/tools/clast-search.ts +38 -0
  129. package/src/server/tools/clast-status.ts +37 -0
  130. package/src/types.ts +69 -0
  131. package/src/watcher/gitignore.ts +42 -0
  132. package/src/watcher/index.ts +131 -0
  133. 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
+ }