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