@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,189 @@
1
+ /**
2
+ * Kit Plugin Formatters
3
+ *
4
+ * Response formatters for MCP tool output in markdown and JSON formats.
5
+ */
6
+
7
+ import { truncate } from '@side-quest/core/utils'
8
+ import type {
9
+ ErrorResult,
10
+ KitResult,
11
+ SemanticResult,
12
+ UsagesResult,
13
+ } from './types.js'
14
+ import { isError, ResponseFormat } from './types.js'
15
+
16
+ // ============================================================================
17
+ // Semantic Formatters
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Format semantic search results for display.
22
+ * @param result - Semantic result or error
23
+ * @param format - Output format (markdown or json)
24
+ * @returns Formatted string
25
+ */
26
+ export function formatSemanticResults(
27
+ result: KitResult<SemanticResult>,
28
+ format: ResponseFormat = ResponseFormat.MARKDOWN,
29
+ ): string {
30
+ if (isError(result)) {
31
+ return formatError(result, format)
32
+ }
33
+
34
+ if (format === ResponseFormat.JSON) {
35
+ return JSON.stringify(result, null, 2)
36
+ }
37
+
38
+ // Markdown format
39
+ const lines: string[] = []
40
+
41
+ lines.push(`## Semantic Search Results`)
42
+ lines.push('')
43
+
44
+ // Show fallback notice if applicable
45
+ if (result.fallback && result.installHint) {
46
+ lines.push(`> **Note:** ${result.installHint.split('\n')[0]}`)
47
+ lines.push('>')
48
+ lines.push(
49
+ '> Using text search fallback. Results may be less relevant than semantic search.',
50
+ )
51
+ lines.push('')
52
+ }
53
+
54
+ lines.push(`Found **${result.count}** matches for query: _"${result.query}"_`)
55
+ lines.push('')
56
+
57
+ if (result.matches.length === 0) {
58
+ lines.push('_No matches found._')
59
+ return lines.join('\n')
60
+ }
61
+
62
+ result.matches.forEach((match, i) => {
63
+ const score = (match.score * 100).toFixed(1)
64
+ const lineInfo =
65
+ match.startLine && match.endLine
66
+ ? `:${match.startLine}-${match.endLine}`
67
+ : match.startLine
68
+ ? `:${match.startLine}`
69
+ : ''
70
+
71
+ lines.push(`### ${i + 1}. ${match.file}${lineInfo} (${score}% relevance)`)
72
+ lines.push('')
73
+ lines.push('```')
74
+ lines.push(truncate(match.chunk, 500))
75
+ lines.push('```')
76
+ lines.push('')
77
+ })
78
+
79
+ return lines.join('\n')
80
+ }
81
+
82
+ // ============================================================================
83
+ // Error Formatters
84
+ // ============================================================================
85
+
86
+ /**
87
+ * Format an error result.
88
+ * @param error - Error result
89
+ * @param format - Output format
90
+ * @returns Formatted string
91
+ */
92
+ export function formatError(
93
+ error: ErrorResult,
94
+ format: ResponseFormat = ResponseFormat.MARKDOWN,
95
+ ): string {
96
+ if (format === ResponseFormat.JSON) {
97
+ return JSON.stringify(error, null, 2)
98
+ }
99
+
100
+ const lines: string[] = []
101
+ lines.push(`## Error`)
102
+ lines.push('')
103
+ lines.push(`**${error.error}**`)
104
+
105
+ if (error.hint) {
106
+ lines.push('')
107
+ lines.push(`**Hint:** ${error.hint}`)
108
+ }
109
+
110
+ return lines.join('\n')
111
+ }
112
+
113
+ // ============================================================================
114
+ // Usages Formatters
115
+ // ============================================================================
116
+
117
+ /**
118
+ * Format symbol usages results for display.
119
+ * @param result - Usages result or error
120
+ * @param format - Output format (markdown or json)
121
+ * @returns Formatted string
122
+ */
123
+ export function formatUsagesResults(
124
+ result: KitResult<UsagesResult>,
125
+ format: ResponseFormat = ResponseFormat.MARKDOWN,
126
+ ): string {
127
+ if (isError(result)) {
128
+ return formatError(result, format)
129
+ }
130
+
131
+ if (format === ResponseFormat.JSON) {
132
+ return JSON.stringify(result, null, 2)
133
+ }
134
+
135
+ // Markdown format
136
+ const lines: string[] = []
137
+
138
+ lines.push(`## Symbol Definitions`)
139
+ lines.push('')
140
+ lines.push(
141
+ `Found **${result.count}** definition(s) for \`${result.symbolName}\``,
142
+ )
143
+ lines.push('')
144
+
145
+ if (result.usages.length === 0) {
146
+ lines.push('_No definitions found._')
147
+ return lines.join('\n')
148
+ }
149
+
150
+ for (const usage of result.usages) {
151
+ const icon = getSymbolTypeIcon(usage.type)
152
+ const lineInfo = usage.line ? `:${usage.line}` : ''
153
+
154
+ lines.push(`### ${icon} ${usage.name}`)
155
+ lines.push('')
156
+ lines.push(`- **Type:** ${usage.type}`)
157
+ lines.push(`- **File:** \`${usage.file}${lineInfo}\``)
158
+
159
+ if (usage.context) {
160
+ lines.push('')
161
+ lines.push('```')
162
+ lines.push(usage.context)
163
+ lines.push('```')
164
+ }
165
+
166
+ lines.push('')
167
+ }
168
+
169
+ return lines.join('\n')
170
+ }
171
+
172
+ /**
173
+ * Get an icon for a symbol type (usages variant).
174
+ */
175
+ function getSymbolTypeIcon(type: string): string {
176
+ const icons: Record<string, string> = {
177
+ function: '📦',
178
+ class: '📚',
179
+ method: '🔧',
180
+ property: '🏷️',
181
+ variable: '📌',
182
+ constant: '🔒',
183
+ type: '📝',
184
+ interface: '📋',
185
+ enum: '📊',
186
+ module: '📁',
187
+ }
188
+ return icons[type.toLowerCase()] ?? '•'
189
+ }
@@ -0,0 +1,471 @@
1
+ /**
2
+ * PROJECT_INDEX.json based tools for token-efficient codebase queries
3
+ *
4
+ * These tools read from a pre-built index instead of scanning files,
5
+ * providing significant token savings for LLM workflows.
6
+ */
7
+
8
+ import {
9
+ findSymbol,
10
+ findSymbolFuzzy,
11
+ getComplexityHotspots,
12
+ getFileSymbols,
13
+ getSymbolTypeDistribution,
14
+ loadProjectIndex,
15
+ } from './utils/index-parser'
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ export interface IndexFindResult {
22
+ query: string
23
+ matchType: 'exact' | 'fuzzy'
24
+ count: number
25
+ results: Array<{
26
+ file: string
27
+ name: string
28
+ type: string
29
+ line: number
30
+ code: string
31
+ score?: number
32
+ }>
33
+ }
34
+
35
+ export interface IndexStatsResult {
36
+ files: number
37
+ totalSymbols: number
38
+ distribution: Record<string, number>
39
+ hotspots: Array<{ directory: string; symbolCount: number }>
40
+ }
41
+
42
+ export interface IndexOverviewResult {
43
+ file: string
44
+ symbolCount: number
45
+ symbols: Array<{
46
+ name: string
47
+ type: string
48
+ line: number
49
+ }>
50
+ }
51
+
52
+ export interface IndexError {
53
+ error: string
54
+ isError: true
55
+ }
56
+
57
+ // ============================================================================
58
+ // Find Tool - Symbol lookup from index
59
+ // ============================================================================
60
+
61
+ /**
62
+ * Find symbol definitions from PROJECT_INDEX.json
63
+ *
64
+ * @param symbolName - Symbol name to search for
65
+ * @param indexPath - Optional path to index file or directory
66
+ * @returns Symbol search results
67
+ */
68
+ export async function executeIndexFind(
69
+ symbolName: string,
70
+ indexPath?: string,
71
+ ): Promise<IndexFindResult | IndexError> {
72
+ try {
73
+ const index = await loadProjectIndex(indexPath)
74
+
75
+ // Try exact match first
76
+ let results = findSymbol(index, symbolName)
77
+ let matchType: 'exact' | 'fuzzy' = 'exact'
78
+
79
+ // Fall back to fuzzy search if no exact match
80
+ if (results.length === 0) {
81
+ const fuzzyResults = findSymbolFuzzy(index, symbolName)
82
+ matchType = 'fuzzy'
83
+ results = fuzzyResults.map(({ file, symbol }) => ({ file, symbol }))
84
+
85
+ return {
86
+ query: symbolName,
87
+ matchType,
88
+ count: fuzzyResults.length,
89
+ results: fuzzyResults.map(({ file, symbol, score }) => ({
90
+ file,
91
+ name: symbol.name,
92
+ type: symbol.type,
93
+ line: symbol.start_line,
94
+ code: symbol.code,
95
+ score,
96
+ })),
97
+ }
98
+ }
99
+
100
+ return {
101
+ query: symbolName,
102
+ matchType,
103
+ count: results.length,
104
+ results: results.map(({ file, symbol }) => ({
105
+ file,
106
+ name: symbol.name,
107
+ type: symbol.type,
108
+ line: symbol.start_line,
109
+ code: symbol.code,
110
+ })),
111
+ }
112
+ } catch (error) {
113
+ return {
114
+ error: error instanceof Error ? error.message : 'Unknown error',
115
+ isError: true,
116
+ }
117
+ }
118
+ }
119
+
120
+ // ============================================================================
121
+ // Stats Tool - Codebase metrics from index
122
+ // ============================================================================
123
+
124
+ /**
125
+ * Get codebase statistics from PROJECT_INDEX.json
126
+ *
127
+ * @param indexPath - Optional path to index file or directory
128
+ * @param topN - Number of top hotspots to return
129
+ * @returns Codebase statistics
130
+ */
131
+ export async function executeIndexStats(
132
+ indexPath?: string,
133
+ topN = 5,
134
+ ): Promise<IndexStatsResult | IndexError> {
135
+ try {
136
+ const index = await loadProjectIndex(indexPath)
137
+
138
+ const distribution = getSymbolTypeDistribution(index)
139
+ const hotspots = getComplexityHotspots(index, topN)
140
+
141
+ const totalSymbols = Object.values(distribution).reduce(
142
+ (sum, count) => sum + count,
143
+ 0,
144
+ )
145
+
146
+ return {
147
+ files: Object.keys(index.symbols).length,
148
+ totalSymbols,
149
+ distribution,
150
+ hotspots,
151
+ }
152
+ } catch (error) {
153
+ return {
154
+ error: error instanceof Error ? error.message : 'Unknown error',
155
+ isError: true,
156
+ }
157
+ }
158
+ }
159
+
160
+ // ============================================================================
161
+ // Overview Tool - File symbols from index
162
+ // ============================================================================
163
+
164
+ /**
165
+ * Get all symbols in a file from PROJECT_INDEX.json
166
+ *
167
+ * @param filePath - File path to get symbols for
168
+ * @param indexPath - Optional path to index file or directory
169
+ * @returns File symbols
170
+ */
171
+ export async function executeIndexOverview(
172
+ filePath: string,
173
+ indexPath?: string,
174
+ ): Promise<IndexOverviewResult | IndexError> {
175
+ try {
176
+ const index = await loadProjectIndex(indexPath)
177
+ const symbols = getFileSymbols(index, filePath)
178
+
179
+ return {
180
+ file: filePath,
181
+ symbolCount: symbols.length,
182
+ symbols: symbols.map((s) => ({
183
+ name: s.name,
184
+ type: s.type,
185
+ line: s.start_line,
186
+ })),
187
+ }
188
+ } catch (error) {
189
+ return {
190
+ error: error instanceof Error ? error.message : 'Unknown error',
191
+ isError: true,
192
+ }
193
+ }
194
+ }
195
+
196
+ // ============================================================================
197
+ // Prime Tool - Generate/refresh index
198
+ // ============================================================================
199
+
200
+ import { join } from 'node:path'
201
+ import { getFileAgeHours, getFileSizeMB } from '@side-quest/core/fs'
202
+ import {
203
+ ensureCommandAvailable,
204
+ spawnWithTimeout,
205
+ } from '@side-quest/core/spawn'
206
+ import { getTargetDir, INDEX_FILE, MAX_AGE_HOURS } from './utils/git.js'
207
+
208
+ export interface IndexPrimeResult {
209
+ success: true
210
+ location: string
211
+ files: number
212
+ symbols: number
213
+ size: string
214
+ durationSec: number
215
+ }
216
+
217
+ export interface IndexPrimeExistsResult {
218
+ status: 'exists'
219
+ location: string
220
+ ageHours: number
221
+ files: number
222
+ symbols: number
223
+ size: string
224
+ message: string
225
+ }
226
+
227
+ /**
228
+ * Generate or refresh PROJECT_INDEX.json
229
+ *
230
+ * @param force - Force regenerate even if index is fresh
231
+ * @param customPath - Optional custom path to index
232
+ * @returns Prime result
233
+ */
234
+ export async function executeIndexPrime(
235
+ force = false,
236
+ customPath?: string,
237
+ ): Promise<IndexPrimeResult | IndexPrimeExistsResult | IndexError> {
238
+ try {
239
+ const targetDir = await getTargetDir(customPath)
240
+ const kitCmd = ensureCommandAvailable('kit')
241
+ const indexPath = join(targetDir, INDEX_FILE)
242
+
243
+ // Check if index exists and is fresh
244
+ const ageHours = getFileAgeHours(indexPath)
245
+ if (ageHours !== null && ageHours <= MAX_AGE_HOURS && !force) {
246
+ const index = await loadProjectIndex(indexPath)
247
+ const symbolCount = Object.values(index.symbols).reduce(
248
+ (sum, symbols) => sum + symbols.length,
249
+ 0,
250
+ )
251
+ const sizeMB = getFileSizeMB(indexPath)
252
+
253
+ return {
254
+ status: 'exists',
255
+ location: targetDir,
256
+ ageHours: Number.parseFloat(ageHours.toFixed(1)),
257
+ files: Object.keys(index.symbols).length,
258
+ symbols: symbolCount,
259
+ size: `${sizeMB} MB`,
260
+ message:
261
+ 'Index is less than 24 hours old. Use force=true to regenerate.',
262
+ }
263
+ }
264
+
265
+ // Generate new index
266
+ const startTime = Date.now()
267
+ const result = await spawnWithTimeout(
268
+ [kitCmd, 'index', targetDir, '-o', indexPath],
269
+ 60_000,
270
+ )
271
+
272
+ if (result.timedOut) {
273
+ return {
274
+ error: 'kit index timed out',
275
+ isError: true,
276
+ }
277
+ }
278
+
279
+ if (result.exitCode !== 0) {
280
+ return {
281
+ error: `kit index failed: ${result.stderr}`,
282
+ isError: true,
283
+ }
284
+ }
285
+
286
+ const durationSec = (Date.now() - startTime) / 1000
287
+
288
+ // Parse generated index
289
+ const index = await loadProjectIndex(indexPath)
290
+ const symbolCount = Object.values(index.symbols).reduce(
291
+ (sum, symbols) => sum + symbols.length,
292
+ 0,
293
+ )
294
+ const sizeMB = getFileSizeMB(indexPath)
295
+
296
+ return {
297
+ success: true,
298
+ location: targetDir,
299
+ files: Object.keys(index.symbols).length,
300
+ symbols: symbolCount,
301
+ size: `${sizeMB} MB`,
302
+ durationSec: Number.parseFloat(durationSec.toFixed(1)),
303
+ }
304
+ } catch (error) {
305
+ return {
306
+ error: error instanceof Error ? error.message : 'Unknown error',
307
+ isError: true,
308
+ }
309
+ }
310
+ }
311
+
312
+ // ============================================================================
313
+ // Format utilities for MCP responses
314
+ // ============================================================================
315
+
316
+ import type { ResponseFormat } from './types.js'
317
+
318
+ /**
319
+ * Format index find results
320
+ */
321
+ export function formatIndexFindResults(
322
+ result: IndexFindResult | IndexError,
323
+ format: ResponseFormat,
324
+ ): string {
325
+ if ('isError' in result) {
326
+ return format === 'json'
327
+ ? JSON.stringify(result, null, 2)
328
+ : `**Error:** ${result.error}`
329
+ }
330
+
331
+ if (format === 'json') {
332
+ return JSON.stringify(result, null, 2)
333
+ }
334
+
335
+ if (result.count === 0) {
336
+ return `No symbols found matching: ${result.query}`
337
+ }
338
+
339
+ let output = `Found ${result.count} ${result.matchType} match(es) for "${result.query}":\n\n`
340
+
341
+ for (const r of result.results) {
342
+ output += `- **${r.name}** (${r.type}) in \`${r.file}:${r.line}\``
343
+ if (r.score !== undefined) {
344
+ output += ` [score: ${r.score}]`
345
+ }
346
+ output += '\n'
347
+ }
348
+
349
+ return output
350
+ }
351
+
352
+ /**
353
+ * Format index stats results
354
+ */
355
+ export function formatIndexStatsResults(
356
+ result: IndexStatsResult | IndexError,
357
+ format: ResponseFormat,
358
+ ): string {
359
+ if ('isError' in result) {
360
+ return format === 'json'
361
+ ? JSON.stringify(result, null, 2)
362
+ : `**Error:** ${result.error}`
363
+ }
364
+
365
+ if (format === 'json') {
366
+ return JSON.stringify(result, null, 2)
367
+ }
368
+
369
+ let output = `## Codebase Statistics\n\n`
370
+ output += `- **Files:** ${result.files}\n`
371
+ output += `- **Total Symbols:** ${result.totalSymbols}\n\n`
372
+
373
+ output += `### Symbol Distribution\n`
374
+ for (const [type, count] of Object.entries(result.distribution)) {
375
+ if (count > 0) {
376
+ output += `- ${type}: ${count}\n`
377
+ }
378
+ }
379
+
380
+ output += `\n### Complexity Hotspots\n`
381
+ for (const { directory, symbolCount } of result.hotspots) {
382
+ output += `- \`${directory}\`: ${symbolCount} symbols\n`
383
+ }
384
+
385
+ return output
386
+ }
387
+
388
+ /**
389
+ * Format index overview results
390
+ */
391
+ export function formatIndexOverviewResults(
392
+ result: IndexOverviewResult | IndexError,
393
+ format: ResponseFormat,
394
+ ): string {
395
+ if ('isError' in result) {
396
+ return format === 'json'
397
+ ? JSON.stringify(result, null, 2)
398
+ : `**Error:** ${result.error}`
399
+ }
400
+
401
+ if (format === 'json') {
402
+ return JSON.stringify(result, null, 2)
403
+ }
404
+
405
+ if (result.symbolCount === 0) {
406
+ return `No symbols found in: ${result.file}`
407
+ }
408
+
409
+ let output = `## ${result.file}\n\n`
410
+ output += `**${result.symbolCount} symbol(s)**\n\n`
411
+
412
+ // Group by type
413
+ const byType: Record<string, typeof result.symbols> = {}
414
+ for (const sym of result.symbols) {
415
+ if (!byType[sym.type]) {
416
+ byType[sym.type] = []
417
+ }
418
+ byType[sym.type]!.push(sym)
419
+ }
420
+
421
+ for (const [type, symbols] of Object.entries(byType)) {
422
+ output += `### ${type}s\n`
423
+ for (const sym of symbols) {
424
+ output += `- \`${sym.name}\` (line ${sym.line})\n`
425
+ }
426
+ output += '\n'
427
+ }
428
+
429
+ return output
430
+ }
431
+
432
+ /**
433
+ * Format index prime results
434
+ */
435
+ export function formatIndexPrimeResults(
436
+ result: IndexPrimeResult | IndexPrimeExistsResult | IndexError,
437
+ format: ResponseFormat,
438
+ ): string {
439
+ if ('isError' in result) {
440
+ return format === 'json'
441
+ ? JSON.stringify(result, null, 2)
442
+ : `**Error:** ${result.error}`
443
+ }
444
+
445
+ if (format === 'json') {
446
+ return JSON.stringify(result, null, 2)
447
+ }
448
+
449
+ if ('status' in result && result.status === 'exists') {
450
+ return (
451
+ `## Index Already Exists\n\n` +
452
+ `- **Location:** ${result.location}\n` +
453
+ `- **Age:** ${result.ageHours} hours\n` +
454
+ `- **Files:** ${result.files}\n` +
455
+ `- **Symbols:** ${result.symbols}\n` +
456
+ `- **Size:** ${result.size}\n\n` +
457
+ `> ${result.message}`
458
+ )
459
+ }
460
+
461
+ // TypeScript narrowing: at this point result is IndexPrimeResult
462
+ const primeResult = result as IndexPrimeResult
463
+ return (
464
+ `## Index Generated Successfully\n\n` +
465
+ `- **Location:** ${primeResult.location}\n` +
466
+ `- **Files:** ${primeResult.files}\n` +
467
+ `- **Symbols:** ${primeResult.symbols}\n` +
468
+ `- **Size:** ${primeResult.size}\n` +
469
+ `- **Duration:** ${primeResult.durationSec}s`
470
+ )
471
+ }