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,131 @@
1
+ import { LlmClient } from './index.js';
2
+ import { buildFunctionCommentPrompt, buildClassCommentPrompt } from './prompts.js';
3
+ import type { Queries } from '../db/queries.js';
4
+ import type { AstNodeRow } from '../db/types.js';
5
+ import type { ClastConfig } from '../config/index.js';
6
+
7
+ export class CommentGenerator {
8
+ private client: LlmClient;
9
+ private queries: Queries;
10
+ private config: ClastConfig;
11
+
12
+ constructor(client: LlmClient, queries: Queries, config: ClastConfig) {
13
+ this.client = client;
14
+ this.queries = queries;
15
+ this.config = config;
16
+ }
17
+
18
+ get isAvailable(): boolean {
19
+ return this.client.isConfigured;
20
+ }
21
+
22
+ /**
23
+ * Generate a comment for a specific node.
24
+ * Returns the generated comment text, or null if generation is disabled/failed.
25
+ */
26
+ async generateForNode(nodeId: number): Promise<string | null> {
27
+ if (!this.client.isConfigured) return null;
28
+
29
+ const node = this.queries.getNodeById(nodeId);
30
+ if (!node) return null;
31
+
32
+ // Check if original comment exists
33
+ const existingComments = this.queries.getCommentsForNode(nodeId);
34
+ const hasOriginal = existingComments.some(c => c.source === 'original');
35
+
36
+ if (hasOriginal && !this.config.llm.alwaysGenerate) {
37
+ // Original comment exists and we're not forcing generation
38
+ return existingComments.find(c => c.source === 'original')?.text ?? null;
39
+ }
40
+
41
+ try {
42
+ const prompt = this.buildPrompt(node);
43
+ const response = await this.client.complete([
44
+ { role: 'system', content: prompt.system },
45
+ { role: 'user', content: prompt.user },
46
+ ]);
47
+
48
+ const comment = response.content.trim();
49
+ if (!comment) return null;
50
+
51
+ // Remove existing generated comment if any
52
+ this.queries.deleteCommentsBySource(nodeId, 'generated');
53
+
54
+ // Store the generated comment
55
+ this.queries.insertComment(nodeId, comment, 'generated');
56
+
57
+ return comment;
58
+ } catch (err) {
59
+ console.error(`[clast] Failed to generate comment for node ${nodeId}:`, err);
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Batch generate comments for nodes without any comments.
66
+ * Returns counts of successes and failures.
67
+ */
68
+ async batchGenerate(
69
+ limit: number = 100,
70
+ onProgress?: (current: number, total: number) => void
71
+ ): Promise<{ success: number; failed: number; skipped: number }> {
72
+ if (!this.client.isConfigured) {
73
+ return { success: 0, failed: 0, skipped: 0 };
74
+ }
75
+
76
+ const nodes = this.queries.getNodesWithoutComments(limit);
77
+ let success = 0;
78
+ let failed = 0;
79
+ let skipped = 0;
80
+
81
+ // Process with concurrency limit
82
+ const concurrency = this.config.llm.maxConcurrent;
83
+ const chunks = chunkArray(nodes, concurrency);
84
+
85
+ let processed = 0;
86
+ for (const chunk of chunks) {
87
+ const results = await Promise.allSettled(
88
+ chunk.map(node => this.generateForNode(node.id))
89
+ );
90
+
91
+ for (const result of results) {
92
+ processed++;
93
+ onProgress?.(processed, nodes.length);
94
+
95
+ if (result.status === 'fulfilled') {
96
+ if (result.value) {
97
+ success++;
98
+ } else {
99
+ skipped++;
100
+ }
101
+ } else {
102
+ failed++;
103
+ }
104
+ }
105
+ }
106
+
107
+ return { success, failed, skipped };
108
+ }
109
+
110
+ private buildPrompt(node: AstNodeRow): { system: string; user: string } {
111
+ if (node.type === 'class' || node.type === 'interface' || node.type === 'struct' || node.type === 'trait') {
112
+ // Get method names for context
113
+ const children = this.queries.searchNodes(node.name, 'method', 50);
114
+ const methodNames = children
115
+ .filter(c => c.file_path === node.file_path)
116
+ .map(c => c.name);
117
+
118
+ return buildClassCommentPrompt(node.name, node.signature, methodNames, node.language);
119
+ }
120
+
121
+ return buildFunctionCommentPrompt(node.signature, node.body_text, node.language);
122
+ }
123
+ }
124
+
125
+ function chunkArray<T>(arr: T[], size: number): T[][] {
126
+ const chunks: T[][] = [];
127
+ for (let i = 0; i < arr.length; i += size) {
128
+ chunks.push(arr.slice(i, i + size));
129
+ }
130
+ return chunks;
131
+ }
@@ -0,0 +1,80 @@
1
+ export interface ChatMessage {
2
+ role: 'system' | 'user' | 'assistant';
3
+ content: string;
4
+ }
5
+
6
+ export interface LlmResponse {
7
+ content: string;
8
+ model: string;
9
+ usage?: { prompt_tokens: number; completion_tokens: number };
10
+ }
11
+
12
+ export class LlmClient {
13
+ private endpoint: string;
14
+ private model: string;
15
+ private apiKey: string;
16
+
17
+ constructor(endpoint: string, model: string, apiKey: string = '') {
18
+ // Ensure endpoint doesn't have trailing slash
19
+ this.endpoint = endpoint.replace(/\/$/, '');
20
+ this.model = model;
21
+ this.apiKey = apiKey;
22
+ }
23
+
24
+ get isConfigured(): boolean {
25
+ return this.model.length > 0;
26
+ }
27
+
28
+ async complete(
29
+ messages: ChatMessage[],
30
+ options: { temperature?: number; maxTokens?: number } = {}
31
+ ): Promise<LlmResponse> {
32
+ const { temperature = 0.3, maxTokens = 500 } = options;
33
+
34
+ const url = `${this.endpoint}/chat/completions`;
35
+ const headers: Record<string, string> = {
36
+ 'Content-Type': 'application/json',
37
+ };
38
+
39
+ if (this.apiKey) {
40
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
41
+ }
42
+
43
+ const controller = new AbortController();
44
+ const timeout = setTimeout(() => controller.abort(), 30000);
45
+
46
+ try {
47
+ const response = await fetch(url, {
48
+ method: 'POST',
49
+ headers,
50
+ body: JSON.stringify({
51
+ model: this.model,
52
+ messages,
53
+ temperature,
54
+ max_tokens: maxTokens,
55
+ }),
56
+ signal: controller.signal,
57
+ });
58
+
59
+ if (!response.ok) {
60
+ const body = await response.text().catch(() => '');
61
+ throw new Error(`LLM API error ${response.status}: ${body}`);
62
+ }
63
+
64
+ const data = await response.json() as {
65
+ choices: Array<{ message: { content: string } }>;
66
+ model: string;
67
+ usage?: { prompt_tokens: number; completion_tokens: number };
68
+ };
69
+
70
+ const content = data.choices?.[0]?.message?.content ?? '';
71
+ return {
72
+ content,
73
+ model: data.model ?? this.model,
74
+ usage: data.usage,
75
+ };
76
+ } finally {
77
+ clearTimeout(timeout);
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,95 @@
1
+ const LANGUAGE_COMMENT_STYLES: Record<string, { docPrefix: string; example: string }> = {
2
+ typescript: {
3
+ docPrefix: '/**',
4
+ example: '/** Calculates the total price including tax. */',
5
+ },
6
+ tsx: {
7
+ docPrefix: '/**',
8
+ example: '/** Calculates the total price including tax. */',
9
+ },
10
+ javascript: {
11
+ docPrefix: '/**',
12
+ example: '/** Calculates the total price including tax. */',
13
+ },
14
+ python: {
15
+ docPrefix: '"""',
16
+ example: '"""Calculates the total price including tax."""',
17
+ },
18
+ java: {
19
+ docPrefix: '/**',
20
+ example: '/** Calculates the total price including tax. */',
21
+ },
22
+ csharp: {
23
+ docPrefix: '///',
24
+ example: '/// <summary>Calculates the total price including tax.</summary>',
25
+ },
26
+ go: {
27
+ docPrefix: '//',
28
+ example: '// CalculateTotal calculates the total price including tax.',
29
+ },
30
+ rust: {
31
+ docPrefix: '///',
32
+ example: '/// Calculates the total price including tax.',
33
+ },
34
+ c: {
35
+ docPrefix: '/**',
36
+ example: '/** Calculates the total price including tax. */',
37
+ },
38
+ cpp: {
39
+ docPrefix: '/**',
40
+ example: '/** Calculates the total price including tax. */',
41
+ },
42
+ ruby: {
43
+ docPrefix: '#',
44
+ example: '# Calculates the total price including tax.',
45
+ },
46
+ php: {
47
+ docPrefix: '/**',
48
+ example: '/** Calculates the total price including tax. */',
49
+ },
50
+ };
51
+
52
+ export function buildFunctionCommentPrompt(
53
+ signature: string,
54
+ bodyText: string,
55
+ language: string
56
+ ): { system: string; user: string } {
57
+ const style = LANGUAGE_COMMENT_STYLES[language] ?? LANGUAGE_COMMENT_STYLES['typescript']!;
58
+
59
+ const system = `You are a code documentation expert. Generate a concise, clear documentation comment for the given function or method. Use the ${language} documentation style. Output ONLY the comment text (no code fences, no extra explanation). Keep it brief: 1-3 sentences describing what the function does, its parameters, and return value if applicable.`;
60
+
61
+ const user = `Language: ${language}
62
+ Comment style example: ${style.example}
63
+
64
+ Signature:
65
+ ${signature}
66
+
67
+ Body:
68
+ ${bodyText.slice(0, 1500)}
69
+
70
+ Generate a documentation comment for this function.`;
71
+
72
+ return { system, user };
73
+ }
74
+
75
+ export function buildClassCommentPrompt(
76
+ name: string,
77
+ signature: string,
78
+ methodNames: string[],
79
+ language: string
80
+ ): { system: string; user: string } {
81
+ const style = LANGUAGE_COMMENT_STYLES[language] ?? LANGUAGE_COMMENT_STYLES['typescript']!;
82
+
83
+ const system = `You are a code documentation expert. Generate a concise, clear documentation comment for the given class or type. Use the ${language} documentation style. Output ONLY the comment text (no code fences, no extra explanation). Keep it brief: 1-3 sentences describing the class purpose and responsibilities.`;
84
+
85
+ const user = `Language: ${language}
86
+ Comment style example: ${style.example}
87
+
88
+ Class: ${name}
89
+ Signature: ${signature}
90
+ Methods: ${methodNames.join(', ') || 'none'}
91
+
92
+ Generate a documentation comment for this class.`;
93
+
94
+ return { system, user };
95
+ }
@@ -0,0 +1,17 @@
1
+ import type { Queries } from '../db/queries.js';
2
+
3
+ /**
4
+ * Resolves unresolved call edges in the database.
5
+ *
6
+ * After all files are parsed and nodes inserted, call edges may have
7
+ * callee_id = null because the callee wasn't inserted yet. This resolver
8
+ * attempts to match callee_name to ast_nodes.name.
9
+ *
10
+ * Resolution strategy (best-effort):
11
+ * 1. Exact name match within the same file
12
+ * 2. Exact name match across all files
13
+ * 3. Leave unresolved if no match
14
+ */
15
+ export function resolveCallEdges(queries: Queries): number {
16
+ return queries.resolveEdges();
17
+ }
@@ -0,0 +1,303 @@
1
+ import type TreeSitter from 'web-tree-sitter';
2
+ import type { AstNode, AstNodeType } from '../types.js';
3
+
4
+ // Node types that represent extractable symbols per language
5
+ const FUNCTION_TYPES = new Set([
6
+ 'function_declaration',
7
+ 'function_definition',
8
+ 'method_definition',
9
+ 'method_declaration',
10
+ 'arrow_function',
11
+ 'function_item', // Rust
12
+ 'func_declaration', // Go
13
+ 'function_expression',
14
+ ]);
15
+
16
+ const CLASS_TYPES = new Set([
17
+ 'class_declaration',
18
+ 'class_definition',
19
+ 'struct_item', // Rust
20
+ 'struct_specifier', // C/C++
21
+ 'impl_item', // Rust
22
+ 'type_declaration', // Go
23
+ ]);
24
+
25
+ const INTERFACE_TYPES = new Set([
26
+ 'interface_declaration',
27
+ 'trait_item', // Rust
28
+ ]);
29
+
30
+ const IMPORT_TYPES = new Set([
31
+ 'import_statement',
32
+ 'import_declaration',
33
+ 'use_declaration', // Rust
34
+ 'preproc_include', // C/C++ (#include)
35
+ ]);
36
+
37
+ const EXPORT_TYPES = new Set([
38
+ 'export_statement',
39
+ 'export_declaration',
40
+ ]);
41
+
42
+ const ENUM_TYPES = new Set([
43
+ 'enum_declaration',
44
+ 'enum_item', // Rust
45
+ 'enum_specifier', // C/C++
46
+ ]);
47
+
48
+ const CALL_TYPES = new Set([
49
+ 'call_expression',
50
+ 'new_expression',
51
+ 'method_invocation', // Java
52
+ 'invocation_expression', // C#
53
+ ]);
54
+
55
+ function classifyNodeType(treeSitterType: string): AstNodeType | null {
56
+ if (FUNCTION_TYPES.has(treeSitterType)) return 'function';
57
+ if (CLASS_TYPES.has(treeSitterType)) return 'class';
58
+ if (INTERFACE_TYPES.has(treeSitterType)) return 'interface';
59
+ if (IMPORT_TYPES.has(treeSitterType)) return 'import';
60
+ if (EXPORT_TYPES.has(treeSitterType)) return 'export';
61
+ if (ENUM_TYPES.has(treeSitterType)) return 'enum';
62
+ return null;
63
+ }
64
+
65
+ function extractName(node: TreeSitter.SyntaxNode): string {
66
+ const nameNode =
67
+ node.childForFieldName('name') ??
68
+ node.childForFieldName('declarator')?.childForFieldName('name') ??
69
+ node.childForFieldName('declarator');
70
+
71
+ if (nameNode) {
72
+ return nameNode.text;
73
+ }
74
+
75
+ // Arrow functions assigned to variables
76
+ if (node.type === 'arrow_function' && node.parent) {
77
+ const parentName = node.parent.childForFieldName('name');
78
+ if (parentName) return parentName.text;
79
+ }
80
+
81
+ // Exports
82
+ if (node.type === 'export_statement') {
83
+ const decl = node.childForFieldName('declaration');
84
+ if (decl) return extractName(decl);
85
+ return '<export>';
86
+ }
87
+
88
+ // Imports
89
+ if (IMPORT_TYPES.has(node.type)) {
90
+ const source = node.childForFieldName('source') ?? node.childForFieldName('path');
91
+ if (source) return source.text.replace(/['"]/g, '');
92
+ return node.text.slice(0, 80);
93
+ }
94
+
95
+ return '<anonymous>';
96
+ }
97
+
98
+ function extractSignature(node: TreeSitter.SyntaxNode, maxLength: number = 200): string {
99
+ const text = node.text;
100
+ const braceIdx = text.indexOf('{');
101
+ const parenCloseIdx = text.indexOf(')');
102
+
103
+ let end = text.length;
104
+ if (braceIdx > 0) {
105
+ end = braceIdx;
106
+ } else if (parenCloseIdx > 0) {
107
+ end = parenCloseIdx + 1;
108
+ }
109
+
110
+ const sig = text.slice(0, end).trim();
111
+ return sig.length > maxLength ? sig.slice(0, maxLength) + '...' : sig;
112
+ }
113
+
114
+ function extractPrecedingComment(
115
+ node: TreeSitter.SyntaxNode,
116
+ source: string
117
+ ): string | null {
118
+ const lines = source.split('\n');
119
+ const nodeLine = node.startPosition.row;
120
+
121
+ const commentLines: string[] = [];
122
+ let checkLine = nodeLine - 1;
123
+
124
+ while (checkLine >= 0) {
125
+ const line = lines[checkLine]?.trim() ?? '';
126
+
127
+ if (
128
+ line.startsWith('//') ||
129
+ line.startsWith('#') ||
130
+ line.startsWith('*') ||
131
+ line.startsWith('/*') ||
132
+ line.startsWith('///') ||
133
+ line.startsWith('/**')
134
+ ) {
135
+ commentLines.unshift(line);
136
+ checkLine--;
137
+ } else if (line === '' && commentLines.length === 0) {
138
+ checkLine--;
139
+ } else {
140
+ break;
141
+ }
142
+ }
143
+
144
+ if (commentLines.length === 0) return null;
145
+
146
+ const cleaned = commentLines
147
+ .map(l => l.replace(/^\/\/\/?\s?|^\/\*\*?\s?|^\*\/?\s?|^#\s?/g, '').trim())
148
+ .filter(l => l.length > 0)
149
+ .join('\n');
150
+
151
+ return cleaned || null;
152
+ }
153
+
154
+ function extractPythonDocstring(node: TreeSitter.SyntaxNode): string | null {
155
+ const body = node.childForFieldName('body');
156
+ if (!body) return null;
157
+
158
+ const firstChild = body.firstChild;
159
+ if (!firstChild) return null;
160
+
161
+ if (
162
+ firstChild.type === 'expression_statement' &&
163
+ firstChild.firstChild?.type === 'string'
164
+ ) {
165
+ const text = firstChild.firstChild.text;
166
+ return text.replace(/^["']{3}|["']{3}$/g, '').trim();
167
+ }
168
+
169
+ return null;
170
+ }
171
+
172
+ function extractCallName(node: TreeSitter.SyntaxNode): string | null {
173
+ const fn = node.childForFieldName('function') ?? node.childForFieldName('method');
174
+ if (!fn) {
175
+ const firstChild = node.firstChild;
176
+ if (firstChild) {
177
+ if (firstChild.type === 'member_expression' || firstChild.type === 'field_expression') {
178
+ const prop = firstChild.childForFieldName('property') ?? firstChild.childForFieldName('field');
179
+ return prop?.text ?? firstChild.text;
180
+ }
181
+ return firstChild.text;
182
+ }
183
+ return null;
184
+ }
185
+
186
+ if (fn.type === 'member_expression' || fn.type === 'field_expression') {
187
+ const prop = fn.childForFieldName('property') ?? fn.childForFieldName('field');
188
+ return prop?.text ?? fn.text;
189
+ }
190
+
191
+ return fn.text;
192
+ }
193
+
194
+ export interface ExtractionResult {
195
+ nodes: AstNode[];
196
+ edges: Array<{ callerNodeIndex: number; calleeName: string; line: number }>;
197
+ comments: Array<{ nodeIndex: number; text: string; source: 'original' }>;
198
+ }
199
+
200
+ export function extractFromTree(
201
+ tree: TreeSitter.Tree,
202
+ filePath: string,
203
+ language: string,
204
+ source: string,
205
+ maxBodySize: number = 2000
206
+ ): ExtractionResult {
207
+ const nodes: AstNode[] = [];
208
+ const edges: Array<{ callerNodeIndex: number; calleeName: string; line: number }> = [];
209
+ const comments: Array<{ nodeIndex: number; text: string; source: 'original' }> = [];
210
+
211
+ const nodeIdToIndex = new Map<number, number>();
212
+
213
+ function findEnclosingFunctionIndex(node: TreeSitter.SyntaxNode): number | null {
214
+ let current = node.parent;
215
+ while (current) {
216
+ const type = classifyNodeType(current.type);
217
+ if (type === 'function') {
218
+ return nodeIdToIndex.get(current.id) ?? null;
219
+ }
220
+ current = current.parent;
221
+ }
222
+ return null;
223
+ }
224
+
225
+ function walk(node: TreeSitter.SyntaxNode, parentIndex: number | null): void {
226
+ const nodeType = classifyNodeType(node.type);
227
+ let currentIndex = parentIndex;
228
+
229
+ if (nodeType && nodeType !== 'import' && nodeType !== 'export') {
230
+ const name = extractName(node);
231
+ const signature = extractSignature(node);
232
+ const bodyText = node.text.length > maxBodySize
233
+ ? node.text.slice(0, maxBodySize) + '...'
234
+ : node.text;
235
+
236
+ const astNode: AstNode = {
237
+ filePath,
238
+ name,
239
+ type: nodeType === 'function' && parentIndex !== null ? 'method' : nodeType,
240
+ signature,
241
+ startLine: node.startPosition.row + 1,
242
+ endLine: node.endPosition.row + 1,
243
+ startCol: node.startPosition.column,
244
+ endCol: node.endPosition.column,
245
+ parentId: null,
246
+ language,
247
+ bodyText,
248
+ };
249
+
250
+ currentIndex = nodes.length;
251
+ nodes.push(astNode);
252
+ nodeIdToIndex.set(node.id, currentIndex);
253
+
254
+ const comment = language === 'python'
255
+ ? (extractPythonDocstring(node) ?? extractPrecedingComment(node, source))
256
+ : extractPrecedingComment(node, source);
257
+
258
+ if (comment) {
259
+ comments.push({ nodeIndex: currentIndex, text: comment, source: 'original' });
260
+ }
261
+ } else if (nodeType === 'import' || nodeType === 'export') {
262
+ const name = extractName(node);
263
+ nodes.push({
264
+ filePath,
265
+ name,
266
+ type: nodeType,
267
+ signature: node.text.slice(0, 200),
268
+ startLine: node.startPosition.row + 1,
269
+ endLine: node.endPosition.row + 1,
270
+ startCol: node.startPosition.column,
271
+ endCol: node.endPosition.column,
272
+ parentId: null,
273
+ language,
274
+ bodyText: '',
275
+ });
276
+ }
277
+
278
+ // Extract call expressions
279
+ if (CALL_TYPES.has(node.type)) {
280
+ const calleeName = extractCallName(node);
281
+ const enclosingFunc = findEnclosingFunctionIndex(node);
282
+ if (calleeName && enclosingFunc !== null) {
283
+ edges.push({
284
+ callerNodeIndex: enclosingFunc,
285
+ calleeName,
286
+ line: node.startPosition.row + 1,
287
+ });
288
+ }
289
+ }
290
+
291
+ // Recurse into children
292
+ for (let i = 0; i < node.childCount; i++) {
293
+ const child = node.child(i);
294
+ if (child) {
295
+ walk(child, currentIndex);
296
+ }
297
+ }
298
+ }
299
+
300
+ walk(tree.rootNode, null);
301
+
302
+ return { nodes, edges, comments };
303
+ }