@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.
@@ -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
+ }