@side-quest/kit 0.1.0 → 0.2.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 (127) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +54 -352
  3. package/dist/cli.d.ts +14 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +156 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/index.d.ts +8 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +8 -2509
  10. package/dist/index.js.map +1 -0
  11. package/dist/lib/ast/index.d.ts +11 -0
  12. package/dist/lib/ast/index.d.ts.map +1 -0
  13. package/dist/lib/ast/index.js +15 -0
  14. package/dist/lib/ast/index.js.map +1 -0
  15. package/dist/lib/ast/languages.d.ts +55 -0
  16. package/dist/lib/ast/languages.d.ts.map +1 -0
  17. package/dist/lib/ast/languages.js +146 -0
  18. package/dist/lib/ast/languages.js.map +1 -0
  19. package/dist/lib/ast/pattern.d.ts +84 -0
  20. package/dist/lib/ast/pattern.d.ts.map +1 -0
  21. package/dist/lib/ast/pattern.js +268 -0
  22. package/dist/lib/ast/pattern.js.map +1 -0
  23. package/dist/lib/ast/searcher.d.ts +89 -0
  24. package/dist/lib/ast/searcher.d.ts.map +1 -0
  25. package/dist/lib/ast/searcher.js +316 -0
  26. package/dist/lib/ast/searcher.js.map +1 -0
  27. package/dist/lib/ast/types.d.ts +93 -0
  28. package/dist/lib/ast/types.d.ts.map +1 -0
  29. package/dist/lib/ast/types.js +23 -0
  30. package/dist/lib/ast/types.js.map +1 -0
  31. package/dist/lib/commands/callers.d.ts +20 -0
  32. package/dist/lib/commands/callers.d.ts.map +1 -0
  33. package/dist/lib/commands/callers.js +162 -0
  34. package/dist/lib/commands/callers.js.map +1 -0
  35. package/dist/lib/commands/find.d.ts +15 -0
  36. package/dist/lib/commands/find.d.ts.map +1 -0
  37. package/dist/lib/commands/find.js +113 -0
  38. package/dist/lib/commands/find.js.map +1 -0
  39. package/dist/lib/commands/overview.d.ts +6 -0
  40. package/dist/lib/commands/overview.d.ts.map +1 -0
  41. package/dist/lib/commands/overview.js +52 -0
  42. package/dist/lib/commands/overview.js.map +1 -0
  43. package/dist/lib/commands/prime.d.ts +16 -0
  44. package/dist/lib/commands/prime.d.ts.map +1 -0
  45. package/dist/lib/commands/prime.js +168 -0
  46. package/dist/lib/commands/prime.js.map +1 -0
  47. package/dist/lib/commands/search.d.ts +20 -0
  48. package/dist/lib/commands/search.d.ts.map +1 -0
  49. package/dist/lib/commands/search.js +111 -0
  50. package/dist/lib/commands/search.js.map +1 -0
  51. package/dist/lib/errors.d.ts +80 -0
  52. package/dist/lib/errors.d.ts.map +1 -0
  53. package/dist/lib/errors.js +189 -0
  54. package/dist/lib/errors.js.map +1 -0
  55. package/dist/lib/formatters/output.d.ts +5 -0
  56. package/dist/lib/formatters/output.d.ts.map +1 -0
  57. package/dist/lib/formatters/output.js +5 -0
  58. package/dist/lib/formatters/output.js.map +1 -0
  59. package/dist/lib/formatters.d.ts +29 -0
  60. package/dist/lib/formatters.d.ts.map +1 -0
  61. package/dist/lib/formatters.js +141 -0
  62. package/dist/lib/formatters.js.map +1 -0
  63. package/dist/lib/index-tools.d.ts +108 -0
  64. package/dist/lib/index-tools.d.ts.map +1 -0
  65. package/dist/lib/index-tools.js +311 -0
  66. package/dist/lib/index-tools.js.map +1 -0
  67. package/dist/lib/index.d.ts +21 -0
  68. package/dist/lib/index.d.ts.map +1 -0
  69. package/dist/lib/index.js +42 -0
  70. package/dist/lib/index.js.map +1 -0
  71. package/dist/lib/kit-wrapper.d.ts +70 -0
  72. package/dist/lib/kit-wrapper.d.ts.map +1 -0
  73. package/dist/lib/kit-wrapper.js +462 -0
  74. package/dist/lib/kit-wrapper.js.map +1 -0
  75. package/dist/lib/logger.d.ts +28 -0
  76. package/dist/lib/logger.d.ts.map +1 -0
  77. package/dist/lib/logger.js +39 -0
  78. package/dist/lib/logger.js.map +1 -0
  79. package/dist/lib/types.d.ts +179 -0
  80. package/dist/lib/types.d.ts.map +1 -0
  81. package/dist/lib/types.js +48 -0
  82. package/dist/lib/types.js.map +1 -0
  83. package/dist/lib/utils/args.d.ts +40 -0
  84. package/dist/lib/utils/args.d.ts.map +1 -0
  85. package/dist/lib/utils/args.js +58 -0
  86. package/dist/lib/utils/args.js.map +1 -0
  87. package/dist/lib/utils/git.d.ts +23 -0
  88. package/dist/lib/utils/git.d.ts.map +1 -0
  89. package/dist/lib/utils/git.js +50 -0
  90. package/dist/lib/utils/git.js.map +1 -0
  91. package/dist/lib/utils/index-parser.d.ts +155 -0
  92. package/dist/lib/utils/index-parser.d.ts.map +1 -0
  93. package/dist/lib/utils/index-parser.js +252 -0
  94. package/dist/lib/utils/index-parser.js.map +1 -0
  95. package/dist/lib/validators.d.ts +138 -0
  96. package/dist/lib/validators.d.ts.map +1 -0
  97. package/dist/lib/validators.js +302 -0
  98. package/dist/lib/validators.js.map +1 -0
  99. package/dist/mcp/index.d.ts +19 -0
  100. package/dist/mcp/index.d.ts.map +1 -0
  101. package/dist/mcp/index.js +769 -0
  102. package/dist/mcp/index.js.map +1 -0
  103. package/package.json +5 -2
  104. package/src/cli.ts +170 -0
  105. package/src/lib/ast/index.ts +32 -0
  106. package/src/lib/ast/languages.ts +172 -0
  107. package/src/lib/ast/pattern.ts +299 -0
  108. package/src/lib/ast/searcher.ts +381 -0
  109. package/src/lib/ast/types.ts +99 -0
  110. package/src/lib/commands/callers.ts +226 -0
  111. package/src/lib/commands/find.ts +159 -0
  112. package/src/lib/commands/overview.ts +73 -0
  113. package/src/lib/commands/prime.ts +271 -0
  114. package/src/lib/commands/search.ts +146 -0
  115. package/src/lib/errors.ts +221 -0
  116. package/src/lib/formatters/output.ts +9 -0
  117. package/src/lib/formatters.ts +189 -0
  118. package/src/lib/index-tools.ts +471 -0
  119. package/src/lib/index.ts +122 -0
  120. package/src/lib/kit-wrapper.ts +675 -0
  121. package/src/lib/logger.ts +57 -0
  122. package/src/lib/types.ts +228 -0
  123. package/src/lib/utils/args.ts +72 -0
  124. package/src/lib/utils/git.ts +65 -0
  125. package/src/lib/utils/index-parser.ts +350 -0
  126. package/src/lib/validators.ts +437 -0
  127. package/src/mcp/index.ts +144 -79
@@ -0,0 +1,299 @@
1
+ /**
2
+ * AST Pattern
3
+ *
4
+ * Compiles and matches AST search patterns against tree-sitter nodes.
5
+ * Ported from Kit's ASTPattern class in ast_search.py.
6
+ */
7
+
8
+ import { getAstLogger } from '../logger.js'
9
+ import type { SyntaxNode } from './languages.js'
10
+ import { type PatternCriteria, SearchMode } from './types.js'
11
+
12
+ /**
13
+ * Node types that represent function definitions across languages.
14
+ */
15
+ const FUNCTION_NODE_TYPES = [
16
+ // TypeScript/JavaScript
17
+ 'function_declaration',
18
+ 'function_expression',
19
+ 'arrow_function',
20
+ 'method_definition',
21
+ 'generator_function_declaration',
22
+ // Python
23
+ 'function_definition',
24
+ ]
25
+
26
+ /**
27
+ * Node types that represent class definitions across languages.
28
+ */
29
+ const CLASS_NODE_TYPES = [
30
+ // TypeScript/JavaScript
31
+ 'class_declaration',
32
+ 'class_expression',
33
+ // Python
34
+ 'class_definition',
35
+ ]
36
+
37
+ /**
38
+ * Node types that represent try/catch statements.
39
+ */
40
+ const TRY_NODE_TYPES = [
41
+ 'try_statement', // TypeScript/JavaScript and Python
42
+ ]
43
+
44
+ /**
45
+ * ASTPattern compiles search patterns and matches them against AST nodes.
46
+ *
47
+ * Supports two modes:
48
+ * - `simple`: Natural language patterns like "async function", "class"
49
+ * - `pattern`: JSON object criteria like {"type": "function_declaration"}
50
+ */
51
+ export class ASTPattern {
52
+ readonly pattern: string
53
+ readonly mode: SearchMode
54
+
55
+ // Simple mode flags (parsed from natural language)
56
+ private isAsync = false
57
+ private isDef = false
58
+ private isClass = false
59
+ private isTry = false
60
+ private isImport = false
61
+ private isExport = false
62
+
63
+ // Pattern mode criteria (parsed from JSON)
64
+ private criteria: PatternCriteria = {}
65
+
66
+ /**
67
+ * Create a new AST pattern.
68
+ *
69
+ * @param pattern - The search pattern string
70
+ * @param mode - The search mode (simple or pattern)
71
+ */
72
+ constructor(pattern: string, mode: SearchMode = SearchMode.SIMPLE) {
73
+ this.pattern = pattern
74
+ this.mode = mode
75
+ this.compile()
76
+ }
77
+
78
+ /**
79
+ * Compile the pattern based on mode.
80
+ */
81
+ private compile(): void {
82
+ if (this.mode === SearchMode.SIMPLE) {
83
+ this.compileSimple()
84
+ } else if (this.mode === SearchMode.PATTERN) {
85
+ this.compilePattern()
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Compile simple mode pattern from natural language.
91
+ * Parses keywords like "async", "function", "class", etc.
92
+ */
93
+ private compileSimple(): void {
94
+ const lower = this.pattern.toLowerCase()
95
+
96
+ // Check for keywords
97
+ this.isAsync = lower.includes('async')
98
+ this.isDef =
99
+ lower.includes('function') ||
100
+ lower.includes('def') ||
101
+ lower.includes('method')
102
+ this.isClass = lower.includes('class')
103
+ this.isTry = lower.includes('try') || lower.includes('catch')
104
+ this.isImport = lower.includes('import')
105
+ this.isExport = lower.includes('export')
106
+
107
+ // If no specific keywords matched, treat as text search
108
+ if (
109
+ !this.isAsync &&
110
+ !this.isDef &&
111
+ !this.isClass &&
112
+ !this.isTry &&
113
+ !this.isImport &&
114
+ !this.isExport
115
+ ) {
116
+ // Fall back to text matching mode
117
+ this.criteria = { textMatch: this.pattern }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Compile pattern mode from JSON criteria.
123
+ */
124
+ private compilePattern(): void {
125
+ try {
126
+ this.criteria = JSON.parse(this.pattern)
127
+ } catch (error) {
128
+ // If JSON parsing fails, treat as text match
129
+ // Log at debug level since this is often intentional (user typed plain text)
130
+ const logger = getAstLogger()
131
+ logger.debug('Pattern JSON parse failed, using text match fallback', {
132
+ pattern: this.pattern,
133
+ error: error instanceof Error ? error.message : String(error),
134
+ })
135
+ this.criteria = { textMatch: this.pattern }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Check if a node matches this pattern.
141
+ *
142
+ * @param node - The tree-sitter syntax node
143
+ * @param source - The source code string
144
+ * @returns True if the node matches
145
+ */
146
+ matches(node: SyntaxNode, source: string): boolean {
147
+ if (this.mode === SearchMode.SIMPLE) {
148
+ return this.matchesSimple(node, source)
149
+ } else if (this.mode === SearchMode.PATTERN) {
150
+ return this.matchesPattern(node, source)
151
+ }
152
+ return false
153
+ }
154
+
155
+ /**
156
+ * Match a node against simple mode pattern.
157
+ */
158
+ private matchesSimple(node: SyntaxNode, source: string): boolean {
159
+ const nodeType = node.type
160
+
161
+ // If we're looking for specific constructs
162
+ if (
163
+ this.isDef ||
164
+ this.isClass ||
165
+ this.isTry ||
166
+ this.isImport ||
167
+ this.isExport
168
+ ) {
169
+ // Check construct type
170
+ if (this.isDef && !this.isFunctionNode(nodeType)) return false
171
+ if (this.isClass && !this.isClassNode(nodeType)) return false
172
+ if (this.isTry && !TRY_NODE_TYPES.includes(nodeType)) return false
173
+ if (this.isImport && !nodeType.includes('import')) return false
174
+ if (this.isExport && !nodeType.includes('export')) return false
175
+
176
+ // Check async modifier (only for functions)
177
+ if (this.isAsync && this.isDef) {
178
+ if (!this.hasAsyncModifier(node)) return false
179
+ }
180
+
181
+ return true
182
+ }
183
+
184
+ // Text match fallback
185
+ if (this.criteria.textMatch) {
186
+ const text = this.getNodeText(node, source)
187
+ return text.includes(this.criteria.textMatch)
188
+ }
189
+
190
+ return false
191
+ }
192
+
193
+ /**
194
+ * Match a node against pattern mode criteria.
195
+ */
196
+ private matchesPattern(node: SyntaxNode, source: string): boolean {
197
+ // Match node type
198
+ if (this.criteria.type && node.type !== this.criteria.type) {
199
+ return false
200
+ }
201
+
202
+ // Match async modifier
203
+ if (this.criteria.async !== undefined) {
204
+ if (this.hasAsyncModifier(node) !== this.criteria.async) {
205
+ return false
206
+ }
207
+ }
208
+
209
+ // Match symbol name
210
+ if (this.criteria.name) {
211
+ const name = this.getNodeName(node, source)
212
+ if (name !== this.criteria.name) {
213
+ return false
214
+ }
215
+ }
216
+
217
+ // Match text content
218
+ if (this.criteria.textMatch) {
219
+ const text = this.getNodeText(node, source)
220
+ if (!text.includes(this.criteria.textMatch)) {
221
+ return false
222
+ }
223
+ }
224
+
225
+ // If no criteria specified, don't match anything
226
+ if (
227
+ !this.criteria.type &&
228
+ !this.criteria.async &&
229
+ !this.criteria.name &&
230
+ !this.criteria.textMatch
231
+ ) {
232
+ return false
233
+ }
234
+
235
+ return true
236
+ }
237
+
238
+ /**
239
+ * Check if a node type is a function node.
240
+ */
241
+ private isFunctionNode(type: string): boolean {
242
+ return FUNCTION_NODE_TYPES.includes(type)
243
+ }
244
+
245
+ /**
246
+ * Check if a node type is a class node.
247
+ */
248
+ private isClassNode(type: string): boolean {
249
+ return CLASS_NODE_TYPES.includes(type)
250
+ }
251
+
252
+ /**
253
+ * Check if a node has an async modifier.
254
+ * Works for both direct 'async' child and keyword in text.
255
+ */
256
+ private hasAsyncModifier(node: SyntaxNode): boolean {
257
+ // Check for 'async' child node (TypeScript/JavaScript)
258
+ for (const child of node.children) {
259
+ if (child && child.type === 'async') return true
260
+ }
261
+
262
+ // Check for async keyword in first few characters
263
+ // This handles cases where async is part of the node text
264
+ const firstPart = node.text?.slice(0, 10) ?? ''
265
+ return firstPart.includes('async')
266
+ }
267
+
268
+ /**
269
+ * Get the name of a named node (function, class, etc.)
270
+ */
271
+ private getNodeName(node: SyntaxNode, source: string): string | undefined {
272
+ // Look for identifier or name child
273
+ for (const child of node.children) {
274
+ if (!child) continue
275
+ if (
276
+ child.type === 'identifier' ||
277
+ child.type === 'type_identifier' ||
278
+ child.type === 'property_identifier'
279
+ ) {
280
+ return source.substring(child.startIndex, child.endIndex)
281
+ }
282
+ }
283
+
284
+ // Check named children
285
+ const nameChild = node.childForFieldName('name')
286
+ if (nameChild) {
287
+ return source.substring(nameChild.startIndex, nameChild.endIndex)
288
+ }
289
+
290
+ return undefined
291
+ }
292
+
293
+ /**
294
+ * Get the text content of a node.
295
+ */
296
+ private getNodeText(node: SyntaxNode, source: string): string {
297
+ return source.substring(node.startIndex, node.endIndex)
298
+ }
299
+ }
@@ -0,0 +1,381 @@
1
+ /**
2
+ * AST Searcher
3
+ *
4
+ * Searches files using tree-sitter AST patterns.
5
+ * Ported from Kit's ASTSearcher class in ast_search.py.
6
+ *
7
+ * Performance optimizations:
8
+ * - Uses fs/promises for non-blocking I/O
9
+ * - Parses files in parallel chunks to maximize throughput
10
+ */
11
+
12
+ import { readdir, readFile, stat } from 'node:fs/promises'
13
+ import { join, relative } from 'node:path'
14
+ import { processInParallelChunks } from '@side-quest/core/concurrency'
15
+ import { getAstLogger } from '../logger.js'
16
+ import { getDefaultKitPath } from '../types.js'
17
+ import type { SyntaxNode } from './languages.js'
18
+ import { detectLanguage, getParser, isSupported } from './languages.js'
19
+ import { ASTPattern } from './pattern.js'
20
+ import {
21
+ type ASTMatch,
22
+ type ASTMatchContext,
23
+ type ASTSearchOptions,
24
+ type ASTSearchResult,
25
+ SearchMode,
26
+ } from './types.js'
27
+
28
+ /**
29
+ * Maximum file size to parse (5MB).
30
+ * Larger files are skipped to prevent memory issues.
31
+ */
32
+ const MAX_FILE_SIZE = 5 * 1024 * 1024
33
+
34
+ /**
35
+ * Maximum text length to include in match results.
36
+ */
37
+ const MAX_TEXT_LENGTH = 500
38
+
39
+ /**
40
+ * Number of files to parse in parallel.
41
+ * Balances throughput vs memory usage.
42
+ */
43
+ const PARALLEL_CHUNK_SIZE = 10
44
+
45
+ /**
46
+ * Directories to skip during traversal.
47
+ */
48
+ const SKIP_DIRS = new Set([
49
+ 'node_modules',
50
+ '.git',
51
+ '.svn',
52
+ '.hg',
53
+ '__pycache__',
54
+ '.pytest_cache',
55
+ '.mypy_cache',
56
+ 'dist',
57
+ 'build',
58
+ '.next',
59
+ '.nuxt',
60
+ 'coverage',
61
+ '.coverage',
62
+ 'venv',
63
+ '.venv',
64
+ 'env',
65
+ '.env',
66
+ ])
67
+
68
+ /**
69
+ * ASTSearcher performs AST-based code search using tree-sitter.
70
+ *
71
+ * It traverses the file system, parses supported files, and matches
72
+ * AST nodes against the given pattern.
73
+ */
74
+ export class ASTSearcher {
75
+ private readonly repoPath: string
76
+
77
+ /**
78
+ * Create a new AST searcher.
79
+ *
80
+ * @param repoPath - Repository path to search (defaults to KIT_DEFAULT_PATH or cwd)
81
+ */
82
+ constructor(repoPath?: string) {
83
+ this.repoPath = repoPath ?? getDefaultKitPath()
84
+ }
85
+
86
+ /**
87
+ * Search for AST patterns in the repository.
88
+ *
89
+ * Files are parsed in parallel chunks for better performance.
90
+ *
91
+ * @param options - Search options
92
+ * @returns Search results with matching AST nodes
93
+ */
94
+ async searchPattern(options: ASTSearchOptions): Promise<ASTSearchResult> {
95
+ const {
96
+ pattern,
97
+ mode = SearchMode.SIMPLE,
98
+ filePattern,
99
+ maxResults = 100,
100
+ } = options
101
+
102
+ const astPattern = new ASTPattern(pattern, mode)
103
+
104
+ // Get all files to search (async directory traversal)
105
+ const files = await this.getMatchingFiles(filePattern)
106
+
107
+ // Debug logging for MCP issues
108
+ const logger = getAstLogger()
109
+ logger.debug('File search results', {
110
+ fileCount: files.length,
111
+ repoPath: this.repoPath,
112
+ filePattern,
113
+ firstFiles: files.slice(0, 3),
114
+ })
115
+
116
+ // Process files in parallel chunks using core utility
117
+ const matches = await processInParallelChunks({
118
+ items: files,
119
+ chunkSize: PARALLEL_CHUNK_SIZE,
120
+ maxResults,
121
+ processor: (filePath) => this.searchFile(filePath, astPattern),
122
+ onError: (filePath, error) => {
123
+ logger.error('Error parsing file', {
124
+ filePath,
125
+ error: error.message,
126
+ })
127
+ return []
128
+ },
129
+ })
130
+
131
+ return {
132
+ count: matches.length,
133
+ matches,
134
+ pattern,
135
+ mode,
136
+ path: this.repoPath,
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Get all files matching the pattern.
142
+ *
143
+ * @param filePattern - Optional glob pattern to filter files
144
+ * @returns Array of absolute file paths
145
+ */
146
+ private async getMatchingFiles(filePattern?: string): Promise<string[]> {
147
+ const files: string[] = []
148
+ await this.walkDirectory(this.repoPath, files, filePattern)
149
+ return files
150
+ }
151
+
152
+ /**
153
+ * Recursively walk directory and collect matching files.
154
+ * Uses async fs methods to avoid blocking the event loop.
155
+ */
156
+ private async walkDirectory(
157
+ dir: string,
158
+ files: string[],
159
+ filePattern?: string,
160
+ ): Promise<void> {
161
+ const logger = getAstLogger()
162
+
163
+ let entries: string[]
164
+ try {
165
+ entries = await readdir(dir)
166
+ } catch (error) {
167
+ // Directory doesn't exist or can't be read
168
+ logger.warn('Failed to read directory', {
169
+ dir,
170
+ error: error instanceof Error ? error.message : String(error),
171
+ })
172
+ return
173
+ }
174
+
175
+ // Process entries in parallel for faster traversal
176
+ await Promise.all(
177
+ entries.map(async (entry) => {
178
+ // Skip hidden and ignored directories
179
+ if (SKIP_DIRS.has(entry) || entry.startsWith('.')) return
180
+
181
+ const fullPath = join(dir, entry)
182
+
183
+ let stats: Awaited<ReturnType<typeof stat>>
184
+ try {
185
+ stats = await stat(fullPath)
186
+ } catch (error) {
187
+ logger.debug('Failed to stat entry', {
188
+ path: fullPath,
189
+ error: error instanceof Error ? error.message : String(error),
190
+ })
191
+ return
192
+ }
193
+
194
+ if (stats.isDirectory()) {
195
+ await this.walkDirectory(fullPath, files, filePattern)
196
+ } else if (stats.isFile()) {
197
+ // Skip large files
198
+ if (stats.size > MAX_FILE_SIZE) {
199
+ logger.warn('Skipping large file', {
200
+ path: fullPath,
201
+ size: stats.size,
202
+ maxSize: MAX_FILE_SIZE,
203
+ })
204
+ return
205
+ }
206
+
207
+ // Check if file is supported for parsing
208
+ if (!isSupported(fullPath)) return
209
+
210
+ // Apply file pattern filter if provided
211
+ if (filePattern && !this.matchesFilePattern(fullPath, filePattern)) {
212
+ return
213
+ }
214
+
215
+ files.push(fullPath)
216
+ }
217
+ }),
218
+ )
219
+ }
220
+
221
+ /**
222
+ * Check if a file matches the given pattern.
223
+ * Uses Bun.Glob for proper glob pattern matching.
224
+ */
225
+ private matchesFilePattern(filePath: string, pattern: string): boolean {
226
+ const relativePath = relative(this.repoPath, filePath)
227
+
228
+ // Use Bun.Glob for proper glob matching
229
+ const glob = new Bun.Glob(pattern)
230
+ return glob.match(relativePath)
231
+ }
232
+
233
+ /**
234
+ * Search a single file for pattern matches.
235
+ *
236
+ * @param filePath - Absolute path to the file
237
+ * @param pattern - AST pattern to match
238
+ * @returns Array of matches found in the file
239
+ */
240
+ private async searchFile(
241
+ filePath: string,
242
+ pattern: ASTPattern,
243
+ ): Promise<ASTMatch[]> {
244
+ const language = detectLanguage(filePath)
245
+ if (!language) return []
246
+
247
+ const parser = await getParser(language)
248
+ const source = await readFile(filePath, 'utf8')
249
+ const tree = parser.parse(source)
250
+
251
+ // Handle parse failure
252
+ if (!tree) return []
253
+
254
+ const matches: ASTMatch[] = []
255
+ this.searchNode(tree.rootNode, source, pattern, filePath, matches)
256
+ return matches
257
+ }
258
+
259
+ /**
260
+ * Recursively search AST nodes for pattern matches.
261
+ */
262
+ private searchNode(
263
+ node: SyntaxNode,
264
+ source: string,
265
+ pattern: ASTPattern,
266
+ filePath: string,
267
+ matches: ASTMatch[],
268
+ ): void {
269
+ // Check if this node matches the pattern
270
+ if (pattern.matches(node, source)) {
271
+ const match = this.createMatch(node, source, filePath)
272
+ matches.push(match)
273
+ }
274
+
275
+ // Recurse into children
276
+ for (let i = 0; i < node.childCount; i++) {
277
+ const child = node.child(i)
278
+ if (child) {
279
+ this.searchNode(child, source, pattern, filePath, matches)
280
+ }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Create an ASTMatch from a matching node.
286
+ */
287
+ private createMatch(
288
+ node: SyntaxNode,
289
+ source: string,
290
+ filePath: string,
291
+ ): ASTMatch {
292
+ return {
293
+ file: relative(this.repoPath, filePath),
294
+ line: node.startPosition.row + 1,
295
+ column: node.startPosition.column,
296
+ nodeType: node.type,
297
+ text: this.truncateText(source.substring(node.startIndex, node.endIndex)),
298
+ context: this.getContext(node, source),
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Get context information about a matched node.
304
+ */
305
+ private getContext(node: SyntaxNode, source: string): ASTMatchContext {
306
+ const context: ASTMatchContext = { nodeType: node.type }
307
+
308
+ // Walk up the tree to find parent function or class
309
+ let parent = node.parent
310
+ while (parent) {
311
+ if (this.isFunctionNode(parent.type)) {
312
+ context.parentFunction = this.getNodeName(parent, source)
313
+ break
314
+ }
315
+ if (this.isClassNode(parent.type)) {
316
+ context.parentClass = this.getNodeName(parent, source)
317
+ break
318
+ }
319
+ parent = parent.parent
320
+ }
321
+
322
+ return context
323
+ }
324
+
325
+ /**
326
+ * Get the name of a named node (function, class, etc.).
327
+ */
328
+ private getNodeName(node: SyntaxNode, source: string): string | undefined {
329
+ // Try named children first
330
+ const nameChild = node.childForFieldName('name')
331
+ if (nameChild) {
332
+ return source.substring(nameChild.startIndex, nameChild.endIndex)
333
+ }
334
+
335
+ // Look for identifier children
336
+ for (let i = 0; i < node.childCount; i++) {
337
+ const child = node.child(i)
338
+ if (
339
+ child &&
340
+ (child.type === 'identifier' || child.type === 'type_identifier')
341
+ ) {
342
+ return source.substring(child.startIndex, child.endIndex)
343
+ }
344
+ }
345
+
346
+ return undefined
347
+ }
348
+
349
+ /**
350
+ * Check if a node type is a function.
351
+ */
352
+ private isFunctionNode(type: string): boolean {
353
+ return [
354
+ 'function_declaration',
355
+ 'function_definition',
356
+ 'function_expression',
357
+ 'arrow_function',
358
+ 'method_definition',
359
+ 'generator_function_declaration',
360
+ ].includes(type)
361
+ }
362
+
363
+ /**
364
+ * Check if a node type is a class.
365
+ */
366
+ private isClassNode(type: string): boolean {
367
+ return [
368
+ 'class_declaration',
369
+ 'class_definition',
370
+ 'class_expression',
371
+ ].includes(type)
372
+ }
373
+
374
+ /**
375
+ * Truncate text to maximum length.
376
+ */
377
+ private truncateText(text: string): string {
378
+ if (text.length <= MAX_TEXT_LENGTH) return text
379
+ return `${text.slice(0, MAX_TEXT_LENGTH - 3)}...`
380
+ }
381
+ }