@side-quest/kit 0.0.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 +36 -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,675 @@
1
+ /**
2
+ * Kit CLI Wrapper
3
+ *
4
+ * Pure functions for executing Kit CLI commands with proper error handling.
5
+ * Uses Bun.spawnSync via shared helpers for synchronous execution to fit MCP tool patterns.
6
+ */
7
+
8
+ import { join } from 'node:path'
9
+ import { TimeoutError, withTimeout } from '@side-quest/core/concurrency'
10
+ import {
11
+ ensureCacheDir,
12
+ isCachePopulated,
13
+ withTempJsonFileSync,
14
+ } from '@side-quest/core/fs'
15
+ import {
16
+ buildEnhancedPath,
17
+ ensureCommandAvailable,
18
+ spawnSyncCollect,
19
+ } from '@side-quest/core/spawn'
20
+
21
+ import {
22
+ AST_SEARCH_TIMEOUT,
23
+ ASTSearcher,
24
+ type ASTSearchOptions,
25
+ type ASTSearchResult,
26
+ } from './ast/index.js'
27
+ import {
28
+ createErrorFromOutput,
29
+ isSemanticUnavailableError,
30
+ isTimeoutError,
31
+ KitError,
32
+ KitErrorType,
33
+ SEMANTIC_INSTALL_HINT,
34
+ } from './errors.js'
35
+ import {
36
+ astLogger,
37
+ createCorrelationId,
38
+ grepLogger,
39
+ semanticLogger,
40
+ usagesLogger,
41
+ } from './logger.js'
42
+ import type {
43
+ GrepMatch,
44
+ GrepOptions,
45
+ GrepResult,
46
+ KitResult,
47
+ SemanticMatch,
48
+ SemanticOptions,
49
+ SemanticResult,
50
+ SymbolUsage,
51
+ UsagesOptions,
52
+ UsagesResult,
53
+ } from './types.js'
54
+ import {
55
+ GREP_TIMEOUT,
56
+ getDefaultKitPath,
57
+ SEMANTIC_TIMEOUT,
58
+ USAGES_TIMEOUT,
59
+ } from './types.js'
60
+
61
+ // ============================================================================
62
+ // Kit CLI Execution
63
+ // ============================================================================
64
+
65
+ /**
66
+ * Check if Kit CLI is installed and available in PATH.
67
+ * @returns True if kit command is available
68
+ */
69
+ export function isKitInstalled(): boolean {
70
+ try {
71
+ ensureCommandAvailable('kit')
72
+ return true
73
+ } catch {
74
+ return false
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Get Kit CLI version.
80
+ * @returns Version string or null if not installed
81
+ */
82
+ export function getKitVersion(): string | null {
83
+ try {
84
+ const result = spawnSyncCollect(['kit', '--version'], {
85
+ env: {
86
+ ...process.env,
87
+ PATH: buildEnhancedPath(),
88
+ },
89
+ })
90
+ if (result.exitCode === 0 && result.stdout) {
91
+ return result.stdout.trim()
92
+ }
93
+ return null
94
+ } catch {
95
+ return null
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Execute a Kit CLI command.
101
+ * @param args - Arguments to pass to kit
102
+ * @param options - Execution options
103
+ * @returns Execution result with stdout, stderr, and exit code
104
+ */
105
+ function executeKit(
106
+ args: string[],
107
+ options: {
108
+ timeout?: number
109
+ cwd?: string
110
+ } = {},
111
+ ): { stdout: string; stderr: string; exitCode: number } {
112
+ const { cwd } = options
113
+
114
+ const result = spawnSyncCollect(['kit', ...args], {
115
+ env: {
116
+ ...process.env,
117
+ PATH: buildEnhancedPath(),
118
+ },
119
+ ...(cwd && { cwd }),
120
+ })
121
+
122
+ return {
123
+ stdout: result.stdout || '',
124
+ stderr: result.stderr || '',
125
+ exitCode: result.exitCode ?? 1,
126
+ }
127
+ }
128
+
129
+ // ============================================================================
130
+ // Grep Execution
131
+ // ============================================================================
132
+
133
+ /**
134
+ * Raw grep match as returned by Kit CLI.
135
+ */
136
+ interface RawGrepMatch {
137
+ file: string
138
+ line_number: number
139
+ line_content: string
140
+ }
141
+
142
+ /**
143
+ * Execute kit grep command.
144
+ * @param options - Grep options
145
+ * @returns Grep result or error
146
+ */
147
+ export function executeKitGrep(options: GrepOptions): KitResult<GrepResult> {
148
+ const cid = createCorrelationId()
149
+ const startTime = Date.now()
150
+
151
+ // Check if Kit is installed
152
+ if (!isKitInstalled()) {
153
+ grepLogger.error('Kit not installed', { cid })
154
+ return new KitError(KitErrorType.KitNotInstalled).toJSON()
155
+ }
156
+
157
+ const {
158
+ pattern,
159
+ path = getDefaultKitPath(),
160
+ caseSensitive = true,
161
+ include,
162
+ exclude,
163
+ maxResults = 100,
164
+ directory,
165
+ } = options
166
+
167
+ // Build command arguments
168
+ const args: string[] = ['grep', path, pattern]
169
+
170
+ // Add options
171
+ if (!caseSensitive) {
172
+ args.push('--ignore-case')
173
+ }
174
+
175
+ if (include) {
176
+ args.push('--include', include)
177
+ }
178
+
179
+ if (exclude) {
180
+ args.push('--exclude', exclude)
181
+ }
182
+
183
+ args.push('--max-results', String(maxResults))
184
+
185
+ if (directory) {
186
+ args.push('--directory', directory)
187
+ }
188
+
189
+ grepLogger.info('Executing kit grep', {
190
+ cid,
191
+ pattern,
192
+ path,
193
+ args,
194
+ })
195
+
196
+ try {
197
+ // Use temp file for JSON output with automatic cleanup
198
+ const rawMatches = withTempJsonFileSync<RawGrepMatch[]>(
199
+ `kit-grep-${cid}`,
200
+ (tempFile) => {
201
+ args.push('--output', tempFile)
202
+ const result = executeKit(args, { timeout: GREP_TIMEOUT })
203
+ return {
204
+ exitCode: result.exitCode,
205
+ stderr: result.stderr,
206
+ }
207
+ },
208
+ )
209
+
210
+ // Transform to our format
211
+ const matches: GrepMatch[] = rawMatches.map((m) => ({
212
+ file: m.file,
213
+ line: m.line_number,
214
+ content: m.line_content,
215
+ }))
216
+
217
+ grepLogger.info('Grep completed', {
218
+ cid,
219
+ pattern,
220
+ matchCount: matches.length,
221
+ durationMs: Date.now() - startTime,
222
+ })
223
+
224
+ return {
225
+ count: matches.length,
226
+ matches,
227
+ pattern,
228
+ path,
229
+ }
230
+ } catch (error) {
231
+ const message = error instanceof Error ? error.message : 'Unknown error'
232
+ grepLogger.error('Grep threw exception', { cid, error: message })
233
+
234
+ // Check if this is a Kit CLI error from withTempJsonFileSync
235
+ if (message.includes('Operation failed with exit code')) {
236
+ const exitCode = Number.parseInt(
237
+ message.match(/exit code (\d+)/)?.[1] || '1',
238
+ 10,
239
+ )
240
+ const stderr = message.split(': ').slice(2).join(': ') || message
241
+ return createErrorFromOutput(stderr, exitCode).toJSON()
242
+ }
243
+
244
+ return new KitError(KitErrorType.KitCommandFailed, message).toJSON()
245
+ }
246
+ }
247
+
248
+ // ============================================================================
249
+ // Semantic Search Execution
250
+ // ============================================================================
251
+
252
+ /**
253
+ * Raw semantic match as returned by Kit CLI.
254
+ */
255
+ interface RawSemanticMatch {
256
+ file: string
257
+ code: string
258
+ name?: string
259
+ type?: string
260
+ score: number
261
+ start_line?: number
262
+ end_line?: number
263
+ }
264
+
265
+ /**
266
+ * Execute kit semantic search command.
267
+ * @param options - Semantic search options
268
+ * @returns Semantic result or error (with fallback to grep)
269
+ */
270
+ export function executeKitSemantic(
271
+ options: SemanticOptions,
272
+ ): KitResult<SemanticResult> {
273
+ const cid = createCorrelationId()
274
+ const startTime = Date.now()
275
+
276
+ // Check if Kit is installed
277
+ if (!isKitInstalled()) {
278
+ semanticLogger.error('Kit not installed', { cid })
279
+ return new KitError(KitErrorType.KitNotInstalled).toJSON()
280
+ }
281
+
282
+ const {
283
+ query,
284
+ path = getDefaultKitPath(),
285
+ topK = 5,
286
+ chunkBy = 'symbols',
287
+ buildIndex = false,
288
+ } = options
289
+
290
+ // Pre-flight check: if index not built and not forcing build, tell user to build it first
291
+ if (!buildIndex && !isSemanticIndexBuilt(path)) {
292
+ semanticLogger.info('Semantic index not built, instructing user to build', {
293
+ cid,
294
+ path,
295
+ })
296
+
297
+ const buildCommand = `kit search-semantic "${path}" "${query}" --build-index`
298
+ const error = new KitError(
299
+ KitErrorType.SemanticIndexNotBuilt,
300
+ `To use semantic search, build the vector index with:\n\n ${buildCommand}\n\nAfter building (one-time), semantic search will be fast and cached.`,
301
+ )
302
+ return error.toJSON()
303
+ }
304
+
305
+ // Get global cache directory for this repo's vector index
306
+ const persistDir = getSemanticCacheDir(path)
307
+
308
+ // Build command arguments
309
+ const args: string[] = [
310
+ 'search-semantic',
311
+ path,
312
+ query,
313
+ '--top-k',
314
+ String(topK),
315
+ '--format',
316
+ 'json',
317
+ '--chunk-by',
318
+ chunkBy,
319
+ '--persist-dir',
320
+ persistDir,
321
+ ]
322
+
323
+ if (buildIndex) {
324
+ args.push('--build-index')
325
+ }
326
+
327
+ semanticLogger.info('Executing kit semantic search', {
328
+ cid,
329
+ query,
330
+ path,
331
+ topK,
332
+ chunkBy,
333
+ persistDir,
334
+ })
335
+
336
+ try {
337
+ const result = executeKit(args, { timeout: SEMANTIC_TIMEOUT })
338
+
339
+ // Check for semantic search unavailable (ML deps not installed)
340
+ // Note: kit writes error messages to stdout, not stderr
341
+ const combinedOutput = `${result.stdout}\n${result.stderr}`
342
+ if (result.exitCode !== 0 && isSemanticUnavailableError(combinedOutput)) {
343
+ semanticLogger.warn('Semantic search unavailable, falling back to grep', {
344
+ cid,
345
+ output: combinedOutput.slice(0, 200),
346
+ })
347
+
348
+ // Fall back to grep search
349
+ return fallbackToGrep(query, path, topK, cid)
350
+ }
351
+
352
+ // Check for timeout - DO NOT fall back to grep as it would also timeout
353
+ if (result.exitCode !== 0 && isTimeoutError(combinedOutput)) {
354
+ semanticLogger.warn('Semantic search timed out on large repository', {
355
+ cid,
356
+ query,
357
+ durationMs: Date.now() - startTime,
358
+ })
359
+ return new KitError(
360
+ KitErrorType.Timeout,
361
+ `Semantic search timed out after ${SEMANTIC_TIMEOUT}ms. On first run, building the vector index may take longer. Try again to use the cached index.`,
362
+ ).toJSON()
363
+ }
364
+
365
+ // Check for other errors
366
+ if (result.exitCode !== 0) {
367
+ semanticLogger.error('Semantic search failed', {
368
+ cid,
369
+ exitCode: result.exitCode,
370
+ output: combinedOutput.slice(0, 500),
371
+ durationMs: Date.now() - startTime,
372
+ })
373
+ return createErrorFromOutput(combinedOutput, result.exitCode).toJSON()
374
+ }
375
+
376
+ // Parse JSON output
377
+ let rawMatches: RawSemanticMatch[]
378
+ try {
379
+ rawMatches = JSON.parse(result.stdout)
380
+ } catch {
381
+ semanticLogger.error('Failed to parse semantic output', {
382
+ cid,
383
+ stdout: result.stdout,
384
+ })
385
+ return new KitError(
386
+ KitErrorType.OutputParseError,
387
+ 'Failed to parse semantic search JSON output',
388
+ ).toJSON()
389
+ }
390
+
391
+ // Transform to our format
392
+ const matches: SemanticMatch[] = rawMatches.map((m) => ({
393
+ file: m.file,
394
+ chunk: m.code,
395
+ score: m.score,
396
+ startLine: m.start_line,
397
+ endLine: m.end_line,
398
+ }))
399
+
400
+ semanticLogger.info('Semantic search completed', {
401
+ cid,
402
+ query,
403
+ matchCount: matches.length,
404
+ durationMs: Date.now() - startTime,
405
+ })
406
+
407
+ return {
408
+ count: matches.length,
409
+ matches,
410
+ query,
411
+ path,
412
+ }
413
+ } catch (error) {
414
+ const message = error instanceof Error ? error.message : 'Unknown error'
415
+ semanticLogger.error('Semantic search threw exception', {
416
+ cid,
417
+ error: message,
418
+ })
419
+ return new KitError(KitErrorType.KitCommandFailed, message).toJSON()
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Fall back to grep when semantic search is unavailable.
425
+ */
426
+ function fallbackToGrep(
427
+ query: string,
428
+ path: string,
429
+ limit: number,
430
+ cid: string,
431
+ ): KitResult<SemanticResult> {
432
+ // Extract keywords from the query for grep
433
+ const keywords = query
434
+ .split(/\s+/)
435
+ .filter((w) => w.length > 2)
436
+ .slice(0, 3)
437
+
438
+ const pattern = keywords.join('|')
439
+
440
+ semanticLogger.info('Fallback grep search', { cid, pattern, path })
441
+
442
+ const grepResult = executeKitGrep({
443
+ pattern,
444
+ path,
445
+ maxResults: limit,
446
+ caseSensitive: false,
447
+ })
448
+
449
+ if ('error' in grepResult) {
450
+ return grepResult
451
+ }
452
+
453
+ // Convert grep matches to semantic format
454
+ // Score decreases by 0.05 per result, with minimum of 0.1 to avoid negative scores
455
+ const matches: SemanticMatch[] = grepResult.matches.map((m, idx) => ({
456
+ file: m.file,
457
+ chunk: m.content,
458
+ score: Math.max(0.1, 1 - idx * 0.05),
459
+ startLine: m.line,
460
+ endLine: m.line,
461
+ }))
462
+
463
+ return {
464
+ count: matches.length,
465
+ matches,
466
+ query,
467
+ path,
468
+ fallback: true,
469
+ installHint: SEMANTIC_INSTALL_HINT,
470
+ }
471
+ }
472
+
473
+ // ============================================================================
474
+ // Symbol Usages Execution
475
+ // ============================================================================
476
+
477
+ /**
478
+ * Raw symbol usage as returned by Kit CLI.
479
+ */
480
+ interface RawSymbolUsage {
481
+ file: string
482
+ type: string
483
+ name: string
484
+ line: number | null
485
+ context: string | null
486
+ }
487
+
488
+ /**
489
+ * Execute kit usages command to find symbol definitions.
490
+ * @param options - Usages options
491
+ * @returns Usages result or error
492
+ */
493
+ export function executeKitUsages(
494
+ options: UsagesOptions,
495
+ ): KitResult<UsagesResult> {
496
+ const cid = createCorrelationId()
497
+ const startTime = Date.now()
498
+
499
+ // Check if Kit is installed
500
+ if (!isKitInstalled()) {
501
+ usagesLogger.error('Kit not installed', { cid })
502
+ return new KitError(KitErrorType.KitNotInstalled).toJSON()
503
+ }
504
+
505
+ const { path = getDefaultKitPath(), symbolName, symbolType } = options
506
+
507
+ if (!symbolName || symbolName.trim() === '') {
508
+ return new KitError(
509
+ KitErrorType.InvalidInput,
510
+ 'Symbol name is required',
511
+ ).toJSON()
512
+ }
513
+
514
+ // Build command arguments
515
+ const args: string[] = ['usages', path, symbolName.trim()]
516
+
517
+ if (symbolType) {
518
+ args.push('--type', symbolType)
519
+ }
520
+
521
+ usagesLogger.info('Executing kit usages', {
522
+ cid,
523
+ path,
524
+ symbolName,
525
+ symbolType,
526
+ args,
527
+ })
528
+
529
+ try {
530
+ // Use temp file for JSON output with automatic cleanup
531
+ const rawUsages = withTempJsonFileSync<RawSymbolUsage[]>(
532
+ `kit-usages-${cid}`,
533
+ (tempFile) => {
534
+ const argsWithOutput = [...args, '--output', tempFile]
535
+ const result = executeKit(argsWithOutput, { timeout: USAGES_TIMEOUT })
536
+ return {
537
+ exitCode: result.exitCode,
538
+ stderr: result.stderr,
539
+ }
540
+ },
541
+ )
542
+
543
+ // Transform to our format
544
+ const usages: SymbolUsage[] = rawUsages.map((u) => ({
545
+ file: u.file,
546
+ type: u.type,
547
+ name: u.name,
548
+ line: u.line,
549
+ context: u.context,
550
+ }))
551
+
552
+ usagesLogger.info('Usages completed', {
553
+ cid,
554
+ symbolName,
555
+ usageCount: usages.length,
556
+ durationMs: Date.now() - startTime,
557
+ })
558
+
559
+ return {
560
+ count: usages.length,
561
+ usages,
562
+ symbolName: symbolName.trim(),
563
+ path,
564
+ }
565
+ } catch (error) {
566
+ const message = error instanceof Error ? error.message : 'Unknown error'
567
+ usagesLogger.error('Usages threw exception', { cid, error: message })
568
+
569
+ // Check if this is a Kit CLI error from withTempJsonFileSync
570
+ if (message.includes('Operation failed with exit code')) {
571
+ const exitCode = Number.parseInt(
572
+ message.match(/exit code (\d+)/)?.[1] || '1',
573
+ 10,
574
+ )
575
+ const stderr = message.split(': ').slice(2).join(': ') || message
576
+ return createErrorFromOutput(stderr, exitCode).toJSON()
577
+ }
578
+
579
+ return new KitError(KitErrorType.KitCommandFailed, message).toJSON()
580
+ }
581
+ }
582
+
583
+ // ============================================================================
584
+ // AST Search Execution (tree-sitter powered)
585
+ // ============================================================================
586
+
587
+ /**
588
+ * Execute AST-based code search using tree-sitter.
589
+ *
590
+ * Unlike other Kit commands, this uses an internal tree-sitter
591
+ * implementation rather than shelling out to the Kit CLI.
592
+ *
593
+ * @param options - AST search options
594
+ * @returns AST search result or error
595
+ */
596
+ export async function executeAstSearch(
597
+ options: ASTSearchOptions,
598
+ ): Promise<KitResult<ASTSearchResult>> {
599
+ const cid = createCorrelationId()
600
+ const startTime = Date.now()
601
+
602
+ const { pattern, mode, filePattern, path, maxResults } = options
603
+
604
+ astLogger.info('Executing AST search', {
605
+ cid,
606
+ pattern,
607
+ mode,
608
+ filePattern,
609
+ path,
610
+ maxResults,
611
+ })
612
+
613
+ try {
614
+ const searcher = new ASTSearcher(path)
615
+
616
+ // Use core timeout utility
617
+ const result = await withTimeout(
618
+ searcher.searchPattern(options),
619
+ AST_SEARCH_TIMEOUT,
620
+ 'AST search timed out',
621
+ )
622
+
623
+ astLogger.info('AST search completed', {
624
+ cid,
625
+ pattern,
626
+ matchCount: result.count,
627
+ durationMs: Date.now() - startTime,
628
+ })
629
+
630
+ return result
631
+ } catch (error) {
632
+ const message = error instanceof Error ? error.message : 'Unknown error'
633
+ const timeoutInfo =
634
+ error instanceof TimeoutError ? ` after ${error.timeoutMs}ms` : ''
635
+ astLogger.error('AST search failed', {
636
+ cid,
637
+ error: message + timeoutInfo,
638
+ })
639
+ return new KitError(KitErrorType.KitCommandFailed, message).toJSON()
640
+ }
641
+ }
642
+
643
+ // ============================================================================
644
+ // Utilities
645
+ // ============================================================================
646
+
647
+ /**
648
+ * Get the persist directory for a repo's semantic search vector index.
649
+ * Creates the directory if it doesn't exist.
650
+ *
651
+ * Per-repo caching strategy: Each repository gets its own .kit/vector_db/
652
+ * directory for isolated, portable vector indexes. This ensures:
653
+ * - Cache is scoped to the repo being searched
654
+ * - No cross-contamination between different repos
655
+ * - Cache travels with the repo context in Claude Code sessions
656
+ * - Easy cleanup (delete .kit/ when done with project)
657
+ *
658
+ * Structure: <repo-path>/.kit/vector_db/
659
+ *
660
+ * @param repoPath - Absolute path to the repository
661
+ * @returns Path to the persist directory for this repo's vector index
662
+ */
663
+ export function getSemanticCacheDir(repoPath: string): string {
664
+ return ensureCacheDir(repoPath, 'vector_db')
665
+ }
666
+
667
+ /**
668
+ * Check if semantic search vector index has been built for a repository.
669
+ * @param repoPath - Path to the repository
670
+ * @returns True if vector index exists and has been built
671
+ */
672
+ export function isSemanticIndexBuilt(repoPath: string): boolean {
673
+ const cacheDir = join(repoPath, '.kit', 'vector_db')
674
+ return isCachePopulated(cacheDir)
675
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Kit Plugin Logger
3
+ *
4
+ * JSONL logging with LogTape for observability and debugging.
5
+ * Uses @sidequest/core logging factory for consistent log location.
6
+ *
7
+ * Log location: ~/.claude/logs/kit.jsonl
8
+ *
9
+ * Logging Level Convention:
10
+ * - DEBUG: Detailed diagnostic info (file counts, cache hits, parameter echo)
11
+ * - INFO: Normal operation events (start/complete, results summary)
12
+ * - WARN: Degraded operation (fallbacks, skipped files, soft failures)
13
+ * - ERROR: Operation failures (exceptions, command failures, parse errors)
14
+ */
15
+
16
+ import {
17
+ createCorrelationId,
18
+ createPluginLogger,
19
+ } from '@side-quest/core/logging'
20
+
21
+ const {
22
+ initLogger,
23
+ rootLogger: logger,
24
+ getSubsystemLogger,
25
+ logDir,
26
+ logFile,
27
+ } = createPluginLogger({
28
+ name: 'kit',
29
+ subsystems: ['grep', 'semantic', 'usages', 'ast'],
30
+ })
31
+
32
+ // ============================================================================
33
+ // Exports
34
+ // ============================================================================
35
+
36
+ export { createCorrelationId, initLogger, logDir, logFile, logger }
37
+
38
+ /** Grep subsystem logger */
39
+ export const grepLogger = getSubsystemLogger('grep')
40
+
41
+ /** Semantic search subsystem logger */
42
+ export const semanticLogger = getSubsystemLogger('semantic')
43
+
44
+ /** Usages subsystem logger */
45
+ export const usagesLogger = getSubsystemLogger('usages')
46
+
47
+ /** AST search subsystem logger */
48
+ export const astLogger = getSubsystemLogger('ast')
49
+
50
+ // ============================================================================
51
+ // Legacy getter functions (for backwards compatibility)
52
+ // ============================================================================
53
+
54
+ /** @deprecated Use astLogger directly */
55
+ export function getAstLogger() {
56
+ return astLogger
57
+ }