@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,769 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Kit MCP Server (Slim)
4
+ *
5
+ * 7 focused tools for token-efficient codebase navigation using Kit CLI.
6
+ *
7
+ * Tools:
8
+ * 1. kit_prime - Generate/refresh PROJECT_INDEX.json
9
+ * 2. kit_find - Symbol lookup + file overview (merged)
10
+ * 3. kit_references - Callers + usages (merged)
11
+ * 4. kit_semantic - Vector search with grep fallback
12
+ * 5. kit_ast_search - Tree-sitter structural search
13
+ * 6. kit_context - Extract enclosing definition around file:line
14
+ * 7. kit_chunk - Split file into LLM-friendly chunks
15
+ *
16
+ * Observability: JSONL file logging to ~/.claude/logs/kit.jsonl
17
+ */
18
+ import { createCorrelationId, log, startServer, tool, z, } from '@side-quest/core/mcp';
19
+ import { wrapToolHandler } from '@side-quest/core/mcp-response';
20
+ import { buildEnhancedPath, spawnSyncCollect } from '@side-quest/core/spawn';
21
+ import { executeAstSearch, executeIndexFind, executeIndexOverview, executeIndexPrime, executeKitGrep, executeKitSemantic, executeKitUsages, formatIndexFindResults, formatIndexOverviewResults, formatIndexPrimeResults, formatSemanticResults, ResponseFormat, SearchMode, validateAstSearchInputs, validatePath, validateSemanticInputs, validateUsagesInputs, } from '../lib/index.js';
22
+ import { findGitRootSync } from '../lib/utils/git.js';
23
+ // ============================================================================
24
+ // Logger Adapter
25
+ // ============================================================================
26
+ /**
27
+ * Adapter to bridge @side-quest/core/mcp log API to wrapToolHandler Logger interface.
28
+ *
29
+ * wrapToolHandler expects: logger.info(message, properties)
30
+ * @side-quest/core/mcp provides: log.info(properties, subsystem)
31
+ */
32
+ function createLoggerAdapter(subsystem) {
33
+ return {
34
+ info: (message, properties) => {
35
+ log.info({ message, ...properties }, subsystem);
36
+ },
37
+ error: (message, properties) => {
38
+ log.error({ message, ...properties }, subsystem);
39
+ },
40
+ };
41
+ }
42
+ // ============================================================================
43
+ // 1. kit_prime - Generate/refresh PROJECT_INDEX.json
44
+ // ============================================================================
45
+ tool('kit_prime', {
46
+ description: `Generate or refresh PROJECT_INDEX.json for the codebase.
47
+
48
+ Creates a pre-built index enabling token-efficient queries:
49
+ - Indexes all symbols (functions, classes, types, etc.)
50
+ - Enables fast symbol lookup without scanning files
51
+ - Auto-detects git repository root
52
+
53
+ The index is valid for 24 hours. Use force=true to regenerate.
54
+
55
+ Requires Kit CLI: uv tool install cased-kit`,
56
+ inputSchema: {
57
+ path: z
58
+ .string()
59
+ .optional()
60
+ .describe('Directory to index (default: git root, then CWD)'),
61
+ force: z
62
+ .boolean()
63
+ .optional()
64
+ .describe('Force regenerate even if index is less than 24 hours old'),
65
+ response_format: z
66
+ .enum(['markdown', 'json'])
67
+ .optional()
68
+ .default('json')
69
+ .describe("Output format: 'markdown' or 'json' (default)"),
70
+ },
71
+ annotations: {
72
+ readOnlyHint: false,
73
+ destructiveHint: false,
74
+ idempotentHint: true,
75
+ openWorldHint: false,
76
+ },
77
+ }, wrapToolHandler(async (args, format) => {
78
+ const { path, force } = args;
79
+ const result = await executeIndexPrime(force, path);
80
+ if ('isError' in result && result.isError) {
81
+ throw new Error(result.error);
82
+ }
83
+ const responseFormat = format === ResponseFormat.JSON
84
+ ? ResponseFormat.JSON
85
+ : ResponseFormat.MARKDOWN;
86
+ return formatIndexPrimeResults(result, responseFormat);
87
+ }, {
88
+ toolName: 'kit_prime',
89
+ logger: createLoggerAdapter('symbols'),
90
+ createCid: createCorrelationId,
91
+ }));
92
+ // ============================================================================
93
+ // 2. kit_find - Symbol lookup + file overview (merged)
94
+ // ============================================================================
95
+ tool('kit_find', {
96
+ description: `Find symbol definitions or list all symbols in a file from PROJECT_INDEX.json.
97
+
98
+ Two modes:
99
+ - Symbol lookup: Pass symbol_name to find where a function/class/type is defined
100
+ - File overview: Pass file_path to see all symbols in a file without reading source
101
+
102
+ ~50x token savings compared to reading full files.
103
+
104
+ NOTE: Requires PROJECT_INDEX.json. Run kit_prime first if not present.`,
105
+ inputSchema: {
106
+ symbol_name: z
107
+ .string()
108
+ .optional()
109
+ .describe('Symbol name to search for. Example: "executeKitGrep". Provide this OR file_path.'),
110
+ file_path: z
111
+ .string()
112
+ .optional()
113
+ .describe('File path to get all symbols for (relative to repo root). Example: "src/kit-wrapper.ts". Provide this OR symbol_name.'),
114
+ index_path: z
115
+ .string()
116
+ .optional()
117
+ .describe('Path to PROJECT_INDEX.json or directory containing it (default: walks up to find it)'),
118
+ response_format: z
119
+ .enum(['markdown', 'json'])
120
+ .optional()
121
+ .default('json')
122
+ .describe("Output format: 'markdown' or 'json' (default)"),
123
+ },
124
+ annotations: {
125
+ readOnlyHint: true,
126
+ destructiveHint: false,
127
+ idempotentHint: true,
128
+ openWorldHint: false,
129
+ },
130
+ }, wrapToolHandler(async (args, format) => {
131
+ const { symbol_name, file_path, index_path } = args;
132
+ if (!symbol_name && !file_path) {
133
+ throw new Error('Either symbol_name or file_path is required. Pass symbol_name to find a definition, or file_path to list all symbols in a file.');
134
+ }
135
+ const responseFormat = format === ResponseFormat.JSON
136
+ ? ResponseFormat.JSON
137
+ : ResponseFormat.MARKDOWN;
138
+ // File overview mode
139
+ if (file_path) {
140
+ const result = await executeIndexOverview(file_path, index_path);
141
+ if ('isError' in result && result.isError) {
142
+ throw new Error(result.error);
143
+ }
144
+ return formatIndexOverviewResults(result, responseFormat);
145
+ }
146
+ // Symbol lookup mode
147
+ const result = await executeIndexFind(symbol_name, index_path);
148
+ if ('isError' in result && result.isError) {
149
+ throw new Error(result.error);
150
+ }
151
+ return formatIndexFindResults(result, responseFormat);
152
+ }, {
153
+ toolName: 'kit_find',
154
+ logger: createLoggerAdapter('symbols'),
155
+ createCid: createCorrelationId,
156
+ }));
157
+ // ============================================================================
158
+ // 3. kit_references - Callers + usages (merged)
159
+ // ============================================================================
160
+ tool('kit_references', {
161
+ description: `Find all references to a symbol -- call sites, usages, and definitions.
162
+
163
+ Three modes:
164
+ - all (default): Find all references (definitions + call sites + type usages)
165
+ - callers_only: Only call sites (filters out definitions)
166
+ - definitions_only: Only definition locations
167
+
168
+ Uses PROJECT_INDEX.json + grep for callers, Kit CLI for usages.
169
+
170
+ Requires Kit CLI: uv tool install cased-kit`,
171
+ inputSchema: {
172
+ symbol: z
173
+ .string()
174
+ .describe('Symbol name to find references for. Example: "executeFind"'),
175
+ mode: z
176
+ .enum(['all', 'callers_only', 'definitions_only'])
177
+ .optional()
178
+ .describe("Reference mode: 'all' (default), 'callers_only', or 'definitions_only'"),
179
+ symbol_type: z
180
+ .string()
181
+ .optional()
182
+ .describe('Filter by symbol type (for usages mode): "function", "class", "type", etc.'),
183
+ path: z
184
+ .string()
185
+ .optional()
186
+ .describe('Repository path to search (default: current directory)'),
187
+ response_format: z
188
+ .enum(['markdown', 'json'])
189
+ .optional()
190
+ .default('json')
191
+ .describe("Output format: 'markdown' or 'json' (default)"),
192
+ },
193
+ annotations: {
194
+ readOnlyHint: true,
195
+ destructiveHint: false,
196
+ idempotentHint: true,
197
+ openWorldHint: false,
198
+ },
199
+ }, wrapToolHandler(async (args, format) => {
200
+ const { symbol, mode = 'all', symbol_type, path, } = args;
201
+ if (mode === 'callers_only') {
202
+ // Validate symbol for callers mode (allowlist valid identifiers)
203
+ const trimmed = symbol.trim();
204
+ if (!trimmed) {
205
+ throw new Error('symbol is required and cannot be empty');
206
+ }
207
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$.<>#]*$/.test(trimmed)) {
208
+ throw new Error('symbol must be a valid identifier (letters, numbers, _, $, ., <, >, #)');
209
+ }
210
+ // Call library function directly instead of spawning CLI
211
+ // Uses kit grep to find all occurrences, then filters to call sites only
212
+ const grepResult = executeKitGrep({
213
+ pattern: trimmed,
214
+ path,
215
+ caseSensitive: true,
216
+ maxResults: 500,
217
+ });
218
+ // Handle error result
219
+ if ('error' in grepResult) {
220
+ throw new Error(`${grepResult.error}${grepResult.hint ? `\nHint: ${grepResult.hint}` : ''}`);
221
+ }
222
+ // Filter out definition patterns to show only call sites
223
+ // Heuristics: exclude lines that look like function declarations or assignments
224
+ const definitionPatterns = [
225
+ /^function\s+/, // function declarations
226
+ /^export\s+(async\s+)?function\s+/, // exported functions
227
+ /^(const|let|var)\s+\w+\s*=\s*function/, // function expressions
228
+ /^(const|let|var)\s+\w+\s*=\s*\(/, // arrow functions
229
+ /^(const|let|var)\s+\w+\s*=\s*async\s*\(/, // async arrow functions
230
+ /^async\s+function\s+/, // async function declarations
231
+ ];
232
+ const callSites = grepResult.matches.filter((match) => {
233
+ const content = match.content.trim();
234
+ return !definitionPatterns.some((pattern) => pattern.test(content));
235
+ });
236
+ // Build callers result in same format as CLI command
237
+ const callersResult = {
238
+ functionName: trimmed,
239
+ callSites: callSites.map((m) => ({
240
+ file: m.file,
241
+ line: m.line || 0,
242
+ context: m.content,
243
+ })),
244
+ count: callSites.length,
245
+ };
246
+ // Format output
247
+ if (format === ResponseFormat.JSON) {
248
+ return JSON.stringify(callersResult, null, 2);
249
+ }
250
+ // Markdown format
251
+ let markdown = `## Call Sites\n\n`;
252
+ markdown += `**Function:** \`${trimmed}\`\n`;
253
+ markdown += `**Call sites found:** ${callersResult.count}\n\n`;
254
+ if (callersResult.count === 0) {
255
+ markdown += '_No call sites found_\n';
256
+ }
257
+ else {
258
+ // Group by file
259
+ const byFile = new Map();
260
+ for (const site of callSites) {
261
+ if (!byFile.has(site.file)) {
262
+ byFile.set(site.file, []);
263
+ }
264
+ byFile.get(site.file)?.push(site);
265
+ }
266
+ for (const [file, sites] of byFile.entries()) {
267
+ markdown += `### ${file}\n\n`;
268
+ for (const site of sites) {
269
+ markdown += `- Line ${site.line || '?'}: \`${site.content.trim()}\`\n`;
270
+ }
271
+ markdown += '\n';
272
+ }
273
+ }
274
+ return markdown;
275
+ }
276
+ // Validate inputs for usages modes
277
+ const validation = validateUsagesInputs({
278
+ symbolName: symbol,
279
+ symbolType: symbol_type,
280
+ path,
281
+ });
282
+ if (!validation.valid) {
283
+ throw new Error(validation.errors.join('; '));
284
+ }
285
+ // For "all" and "definitions_only", use Kit usages
286
+ const result = executeKitUsages({
287
+ symbolName: validation.validated.symbolName,
288
+ symbolType: validation.validated.symbolType,
289
+ path: validation.validated.path,
290
+ });
291
+ if ('error' in result) {
292
+ throw new Error(`${result.error}${result.hint ? `\nHint: ${result.hint}` : ''}`);
293
+ }
294
+ // Filter to definitions only if requested
295
+ if (mode === 'definitions_only') {
296
+ result.usages = result.usages.filter((u) => u.type === 'definition' || u.type === 'export');
297
+ result.count = result.usages.length;
298
+ }
299
+ if (format === ResponseFormat.JSON) {
300
+ return JSON.stringify(result, null, 2);
301
+ }
302
+ // Format as markdown
303
+ let markdown = `## Symbol References\n\n`;
304
+ markdown += `**Symbol:** \`${result.symbolName}\`\n`;
305
+ markdown += `**Mode:** ${mode}\n`;
306
+ markdown += `**References found:** ${result.count}\n\n`;
307
+ if (result.usages.length === 0) {
308
+ markdown += '_No references found_\n';
309
+ }
310
+ else {
311
+ for (const usage of result.usages) {
312
+ markdown += `### ${usage.file}${usage.line ? `:${usage.line}` : ''}\n`;
313
+ markdown += `**Type:** \`${usage.type}\` | **Name:** \`${usage.name}\`\n`;
314
+ if (usage.context) {
315
+ markdown += `\`\`\`\n${usage.context}\n\`\`\`\n`;
316
+ }
317
+ markdown += '\n';
318
+ }
319
+ }
320
+ return markdown;
321
+ }, {
322
+ toolName: 'kit_references',
323
+ logger: createLoggerAdapter('references'),
324
+ createCid: createCorrelationId,
325
+ }));
326
+ // ============================================================================
327
+ // 4. kit_semantic - Vector search with grep fallback
328
+ // ============================================================================
329
+ tool('kit_semantic', {
330
+ description: `Semantic search using natural language queries and vector embeddings.
331
+
332
+ Find code by meaning rather than exact text matches. Great for:
333
+ - "How does authentication work?"
334
+ - "Error handling patterns"
335
+ - "Database connection logic"
336
+
337
+ NOTE: Requires ML dependencies. If unavailable, falls back to text search.
338
+ To enable: uv tool install 'cased-kit[ml]'`,
339
+ inputSchema: {
340
+ query: z
341
+ .string()
342
+ .describe('Natural language query. Example: "authentication flow logic"'),
343
+ path: z
344
+ .string()
345
+ .optional()
346
+ .describe('Repository path to search (default: current directory)'),
347
+ top_k: z
348
+ .number()
349
+ .optional()
350
+ .describe('Number of results to return (default: 5, max: 50)'),
351
+ chunk_by: z
352
+ .enum(['symbols', 'lines'])
353
+ .optional()
354
+ .describe("Chunking strategy: 'symbols' (default) or 'lines'"),
355
+ build_index: z
356
+ .boolean()
357
+ .optional()
358
+ .describe('Force rebuild of vector index (default: false)'),
359
+ response_format: z
360
+ .enum(['markdown', 'json'])
361
+ .optional()
362
+ .default('json')
363
+ .describe("Output format: 'markdown' or 'json' (default)"),
364
+ },
365
+ annotations: {
366
+ readOnlyHint: true,
367
+ destructiveHint: false,
368
+ idempotentHint: true,
369
+ openWorldHint: false,
370
+ },
371
+ }, wrapToolHandler(async (args, format) => {
372
+ const { query, path, top_k, chunk_by, build_index } = args;
373
+ // Validate semantic search inputs
374
+ const validation = validateSemanticInputs({ query, path, topK: top_k });
375
+ if (!validation.valid) {
376
+ throw new Error(validation.errors.join('; '));
377
+ }
378
+ // Call library function directly instead of spawning CLI
379
+ const result = executeKitSemantic({
380
+ query: validation.validated.query,
381
+ path: validation.validated.path,
382
+ topK: validation.validated.topK,
383
+ chunkBy: chunk_by,
384
+ buildIndex: build_index,
385
+ });
386
+ // Handle error result
387
+ if ('error' in result) {
388
+ throw new Error(`${result.error}${result.hint ? `\nHint: ${result.hint}` : ''}`);
389
+ }
390
+ // Format result using existing formatter
391
+ const responseFormat = format === ResponseFormat.JSON
392
+ ? ResponseFormat.JSON
393
+ : ResponseFormat.MARKDOWN;
394
+ return formatSemanticResults(result, responseFormat);
395
+ }, {
396
+ toolName: 'kit_semantic',
397
+ logger: createLoggerAdapter('semantic'),
398
+ createCid: createCorrelationId,
399
+ }));
400
+ // ============================================================================
401
+ // 5. kit_ast_search - Tree-sitter structural search
402
+ // ============================================================================
403
+ tool('kit_ast_search', {
404
+ description: `AST pattern search using tree-sitter for structural code matching.
405
+
406
+ Find code by structure rather than text. More precise than grep for:
407
+ - "async function" - Find all async functions
408
+ - "try catch" - Find try-catch blocks
409
+ - "React hooks" - Find useState/useEffect calls
410
+ - "class extends" - Find class inheritance
411
+
412
+ Supports TypeScript, JavaScript, and Python.
413
+
414
+ Two modes:
415
+ - simple (default): Natural language patterns like "async function"
416
+ - pattern: JSON criteria like {"type": "function_declaration", "async": true}`,
417
+ inputSchema: {
418
+ pattern: z
419
+ .string()
420
+ .describe('Search pattern. Simple mode: "async function", "try catch". Pattern mode: {"type": "function_declaration"}'),
421
+ mode: z
422
+ .enum(['simple', 'pattern'])
423
+ .optional()
424
+ .describe("Search mode: 'simple' (default) for natural language, 'pattern' for JSON criteria"),
425
+ file_pattern: z
426
+ .string()
427
+ .optional()
428
+ .describe('File glob pattern to search (default: all supported files). Example: "*.ts"'),
429
+ path: z
430
+ .string()
431
+ .optional()
432
+ .describe('Repository path to search (default: current directory)'),
433
+ max_results: z
434
+ .number()
435
+ .optional()
436
+ .describe('Maximum results to return (default: 100)'),
437
+ response_format: z
438
+ .enum(['markdown', 'json'])
439
+ .optional()
440
+ .default('json')
441
+ .describe("Output format: 'markdown' or 'json' (default)"),
442
+ },
443
+ annotations: {
444
+ readOnlyHint: true,
445
+ destructiveHint: false,
446
+ idempotentHint: true,
447
+ openWorldHint: false,
448
+ },
449
+ }, wrapToolHandler(async (args, format) => {
450
+ const { pattern, mode, file_pattern, path, max_results } = args;
451
+ // Validate AST search inputs
452
+ const validation = validateAstSearchInputs({
453
+ pattern,
454
+ mode,
455
+ filePattern: file_pattern,
456
+ path,
457
+ maxResults: max_results,
458
+ });
459
+ if (!validation.valid) {
460
+ throw new Error(validation.errors.join('; '));
461
+ }
462
+ const result = await executeAstSearch({
463
+ pattern: validation.validated.pattern,
464
+ mode: validation.validated.mode === 'pattern'
465
+ ? SearchMode.PATTERN
466
+ : SearchMode.SIMPLE,
467
+ filePattern: validation.validated.filePattern,
468
+ path: validation.validated.path,
469
+ maxResults: validation.validated.maxResults,
470
+ });
471
+ if ('error' in result) {
472
+ throw new Error(`${result.error}${result.hint ? `\nHint: ${result.hint}` : ''}`);
473
+ }
474
+ if (format === ResponseFormat.JSON) {
475
+ return JSON.stringify(result, null, 2);
476
+ }
477
+ let markdown = `## AST Search Results\n\n`;
478
+ markdown += `**Pattern:** \`${result.pattern}\`\n`;
479
+ markdown += `**Mode:** ${result.mode}\n`;
480
+ markdown += `**Matches:** ${result.count}\n\n`;
481
+ if (result.matches.length === 0) {
482
+ markdown += '_No matches found_\n';
483
+ }
484
+ else {
485
+ for (const match of result.matches) {
486
+ markdown += `### ${match.file}:${match.line}\n`;
487
+ markdown += `**Node type:** \`${match.nodeType}\`\n`;
488
+ if (match.context.parentFunction) {
489
+ markdown += `**In function:** \`${match.context.parentFunction}\`\n`;
490
+ }
491
+ if (match.context.parentClass) {
492
+ markdown += `**In class:** \`${match.context.parentClass}\`\n`;
493
+ }
494
+ markdown += `\`\`\`\n${match.text.slice(0, 300)}${match.text.length > 300 ? '...' : ''}\n\`\`\`\n\n`;
495
+ }
496
+ }
497
+ return markdown;
498
+ }, {
499
+ toolName: 'kit_ast_search',
500
+ logger: createLoggerAdapter('ast'),
501
+ createCid: createCorrelationId,
502
+ }));
503
+ // ============================================================================
504
+ // 6. kit_context - Extract enclosing definition around file:line
505
+ // ============================================================================
506
+ tool('kit_context', {
507
+ description: `Extract the full enclosing definition around a specific line in a file.
508
+
509
+ Uses Kit CLI to find the complete function/class/method that contains a given line.
510
+ Great for:
511
+ - Getting full context around a line reference
512
+ - Extracting complete function bodies without reading entire files
513
+ - Understanding code surrounding a specific location
514
+
515
+ Requires Kit CLI v3.0+: uv tool install cased-kit`,
516
+ inputSchema: {
517
+ file_path: z
518
+ .string()
519
+ .describe('Relative path to the file within the repository. Example: "src/kit-wrapper.ts"'),
520
+ line: z.number().describe('Line number to extract context around'),
521
+ path: z
522
+ .string()
523
+ .optional()
524
+ .describe('Repository path (default: git root or current directory)'),
525
+ response_format: z
526
+ .enum(['markdown', 'json'])
527
+ .optional()
528
+ .default('json')
529
+ .describe("Output format: 'markdown' or 'json' (default)"),
530
+ },
531
+ annotations: {
532
+ readOnlyHint: true,
533
+ destructiveHint: false,
534
+ idempotentHint: true,
535
+ openWorldHint: false,
536
+ },
537
+ }, wrapToolHandler(async (args, format) => {
538
+ const { file_path, line, path } = args;
539
+ // Validate file_path - no traversal, non-empty, no null bytes, relative only
540
+ const fileTrimmed = file_path.trim();
541
+ if (!fileTrimmed) {
542
+ throw new Error('file_path is required');
543
+ }
544
+ // Reject null bytes
545
+ if (fileTrimmed.includes('\x00')) {
546
+ throw new Error('file_path contains invalid characters');
547
+ }
548
+ // Reject absolute paths
549
+ if (fileTrimmed.startsWith('/') || fileTrimmed.startsWith('\\')) {
550
+ throw new Error('file_path must be a relative path');
551
+ }
552
+ // Normalize and check for directory traversal
553
+ const normalized = fileTrimmed.replace(/\\/g, '/');
554
+ if (normalized.includes('..')) {
555
+ throw new Error('file_path must not contain directory traversal');
556
+ }
557
+ if (line < 1) {
558
+ throw new Error('line must be a positive integer');
559
+ }
560
+ // Validate path param if provided
561
+ if (path) {
562
+ const pathResult = validatePath(path);
563
+ if (!pathResult.valid) {
564
+ throw new Error(pathResult.error);
565
+ }
566
+ }
567
+ const repoPath = path || findGitRootSync() || process.cwd();
568
+ const result = spawnSyncCollect(['kit', 'context', repoPath, '--', file_path, String(line)], {
569
+ env: { PATH: buildEnhancedPath() },
570
+ });
571
+ if (result.exitCode !== 0) {
572
+ throw new Error(result.stderr || `Failed to extract context for ${file_path}:${line}`);
573
+ }
574
+ const output = result.stdout.trim();
575
+ if (format === ResponseFormat.JSON) {
576
+ // Kit context outputs JSON by default
577
+ try {
578
+ const parsed = JSON.parse(output);
579
+ return JSON.stringify(parsed, null, 2);
580
+ }
581
+ catch {
582
+ return JSON.stringify({ context: output, file: file_path, line });
583
+ }
584
+ }
585
+ // Markdown format
586
+ let markdown = `## Context for ${file_path}:${line}\n\n`;
587
+ try {
588
+ const parsed = JSON.parse(output);
589
+ if (parsed.context || parsed.code) {
590
+ const code = parsed.context || parsed.code || output;
591
+ const ext = file_path.split('.').pop() || '';
592
+ const lang = { ts: 'typescript', js: 'javascript', py: 'python' }[ext] || '';
593
+ markdown += `\`\`\`${lang}\n${code}\n\`\`\`\n`;
594
+ }
595
+ else {
596
+ markdown += `\`\`\`\n${output}\n\`\`\`\n`;
597
+ }
598
+ }
599
+ catch {
600
+ markdown += `\`\`\`\n${output}\n\`\`\`\n`;
601
+ }
602
+ return markdown;
603
+ }, {
604
+ toolName: 'kit_context',
605
+ logger: createLoggerAdapter('context'),
606
+ createCid: createCorrelationId,
607
+ }));
608
+ // ============================================================================
609
+ // 7. kit_chunk - Split file into LLM-friendly chunks
610
+ // ============================================================================
611
+ tool('kit_chunk', {
612
+ description: `Split a file into LLM-friendly chunks for efficient processing.
613
+
614
+ Two strategies:
615
+ - symbols (default): Chunk at function/class boundaries (semantic)
616
+ - lines: Chunk by line count (configurable max_lines)
617
+
618
+ Great for:
619
+ - Processing large files piece by piece
620
+ - Token-efficient file analysis
621
+ - Focused code review on specific sections
622
+
623
+ Requires Kit CLI v3.0+: uv tool install cased-kit`,
624
+ inputSchema: {
625
+ file_path: z
626
+ .string()
627
+ .describe('Relative path to the file within the repository. Example: "src/kit-wrapper.ts"'),
628
+ strategy: z
629
+ .enum(['symbols', 'lines'])
630
+ .optional()
631
+ .describe("Chunking strategy: 'symbols' (default, at function boundaries) or 'lines' (by line count)"),
632
+ max_lines: z
633
+ .number()
634
+ .optional()
635
+ .describe("Maximum lines per chunk (only for 'lines' strategy, default: 50)"),
636
+ path: z
637
+ .string()
638
+ .optional()
639
+ .describe('Repository path (default: git root or current directory)'),
640
+ response_format: z
641
+ .enum(['markdown', 'json'])
642
+ .optional()
643
+ .default('json')
644
+ .describe("Output format: 'markdown' or 'json' (default)"),
645
+ },
646
+ annotations: {
647
+ readOnlyHint: true,
648
+ destructiveHint: false,
649
+ idempotentHint: true,
650
+ openWorldHint: false,
651
+ },
652
+ }, wrapToolHandler(async (args, format) => {
653
+ const { file_path, strategy = 'symbols', max_lines, path, } = args;
654
+ // Validate file_path - no traversal, non-empty, no null bytes, relative only
655
+ const fileTrimmed = file_path.trim();
656
+ if (!fileTrimmed) {
657
+ throw new Error('file_path is required');
658
+ }
659
+ // Reject null bytes
660
+ if (fileTrimmed.includes('\x00')) {
661
+ throw new Error('file_path contains invalid characters');
662
+ }
663
+ // Reject absolute paths
664
+ if (fileTrimmed.startsWith('/') || fileTrimmed.startsWith('\\')) {
665
+ throw new Error('file_path must be a relative path');
666
+ }
667
+ // Normalize and check for directory traversal
668
+ const normalized = fileTrimmed.replace(/\\/g, '/');
669
+ if (normalized.includes('..')) {
670
+ throw new Error('file_path must not contain directory traversal');
671
+ }
672
+ // Validate max_lines bounds (1-500)
673
+ if (max_lines !== undefined && (max_lines < 1 || max_lines > 500)) {
674
+ throw new Error('max_lines must be between 1 and 500');
675
+ }
676
+ // Validate path param if provided
677
+ if (path) {
678
+ const pathResult = validatePath(path);
679
+ if (!pathResult.valid) {
680
+ throw new Error(pathResult.error);
681
+ }
682
+ }
683
+ const repoPath = path || findGitRootSync() || process.cwd();
684
+ let cmd;
685
+ if (strategy === 'symbols') {
686
+ cmd = ['kit', 'chunk-symbols', repoPath, '--', file_path];
687
+ }
688
+ else {
689
+ cmd = ['kit', 'chunk-lines', repoPath];
690
+ if (max_lines) {
691
+ cmd.push('-n', String(max_lines));
692
+ }
693
+ cmd.push('--', file_path);
694
+ }
695
+ const result = spawnSyncCollect(cmd, {
696
+ env: { PATH: buildEnhancedPath() },
697
+ });
698
+ if (result.exitCode !== 0) {
699
+ throw new Error(result.stderr || `Failed to chunk ${file_path}`);
700
+ }
701
+ const output = result.stdout.trim();
702
+ if (format === ResponseFormat.JSON) {
703
+ try {
704
+ const parsed = JSON.parse(output);
705
+ return JSON.stringify(parsed, null, 2);
706
+ }
707
+ catch {
708
+ return JSON.stringify({
709
+ file: file_path,
710
+ strategy,
711
+ chunks: [output],
712
+ });
713
+ }
714
+ }
715
+ // Markdown format
716
+ let markdown = `## File Chunks: ${file_path}\n\n`;
717
+ markdown += `**Strategy:** ${strategy}\n`;
718
+ try {
719
+ const parsed = JSON.parse(output);
720
+ const chunks = Array.isArray(parsed)
721
+ ? parsed
722
+ : parsed.chunks || [parsed];
723
+ markdown += `**Chunks:** ${chunks.length}\n\n`;
724
+ for (let i = 0; i < chunks.length; i++) {
725
+ const chunk = chunks[i];
726
+ markdown += `### Chunk ${i + 1}`;
727
+ if (chunk.name || chunk.symbol) {
728
+ markdown += ` - ${chunk.name || chunk.symbol}`;
729
+ }
730
+ markdown += '\n';
731
+ if (chunk.start_line || chunk.startLine) {
732
+ markdown += `Lines ${chunk.start_line || chunk.startLine}-${chunk.end_line || chunk.endLine}\n`;
733
+ }
734
+ const code = chunk.content || chunk.code || chunk.text || JSON.stringify(chunk);
735
+ const ext = file_path.split('.').pop() || '';
736
+ const lang = { ts: 'typescript', js: 'javascript', py: 'python' }[ext] || '';
737
+ markdown += `\`\`\`${lang}\n${code}\n\`\`\`\n\n`;
738
+ }
739
+ }
740
+ catch {
741
+ markdown += `\`\`\`\n${output}\n\`\`\`\n`;
742
+ }
743
+ return markdown;
744
+ }, {
745
+ toolName: 'kit_chunk',
746
+ logger: createLoggerAdapter('chunk'),
747
+ createCid: createCorrelationId,
748
+ }));
749
+ // ============================================================================
750
+ // Start Server
751
+ // ============================================================================
752
+ if (import.meta.main) {
753
+ startServer('kit', {
754
+ version: '1.0.0',
755
+ fileLogging: {
756
+ enabled: true,
757
+ subsystems: [
758
+ 'symbols',
759
+ 'references',
760
+ 'semantic',
761
+ 'ast',
762
+ 'context',
763
+ 'chunk',
764
+ ],
765
+ level: 'debug',
766
+ },
767
+ });
768
+ }
769
+ //# sourceMappingURL=index.js.map