@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.
- package/CHANGELOG.md +36 -0
- package/README.md +54 -352
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +156 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -2509
- package/dist/index.js.map +1 -0
- package/dist/lib/ast/index.d.ts +11 -0
- package/dist/lib/ast/index.d.ts.map +1 -0
- package/dist/lib/ast/index.js +15 -0
- package/dist/lib/ast/index.js.map +1 -0
- package/dist/lib/ast/languages.d.ts +55 -0
- package/dist/lib/ast/languages.d.ts.map +1 -0
- package/dist/lib/ast/languages.js +146 -0
- package/dist/lib/ast/languages.js.map +1 -0
- package/dist/lib/ast/pattern.d.ts +84 -0
- package/dist/lib/ast/pattern.d.ts.map +1 -0
- package/dist/lib/ast/pattern.js +268 -0
- package/dist/lib/ast/pattern.js.map +1 -0
- package/dist/lib/ast/searcher.d.ts +89 -0
- package/dist/lib/ast/searcher.d.ts.map +1 -0
- package/dist/lib/ast/searcher.js +316 -0
- package/dist/lib/ast/searcher.js.map +1 -0
- package/dist/lib/ast/types.d.ts +93 -0
- package/dist/lib/ast/types.d.ts.map +1 -0
- package/dist/lib/ast/types.js +23 -0
- package/dist/lib/ast/types.js.map +1 -0
- package/dist/lib/commands/callers.d.ts +20 -0
- package/dist/lib/commands/callers.d.ts.map +1 -0
- package/dist/lib/commands/callers.js +162 -0
- package/dist/lib/commands/callers.js.map +1 -0
- package/dist/lib/commands/find.d.ts +15 -0
- package/dist/lib/commands/find.d.ts.map +1 -0
- package/dist/lib/commands/find.js +113 -0
- package/dist/lib/commands/find.js.map +1 -0
- package/dist/lib/commands/overview.d.ts +6 -0
- package/dist/lib/commands/overview.d.ts.map +1 -0
- package/dist/lib/commands/overview.js +52 -0
- package/dist/lib/commands/overview.js.map +1 -0
- package/dist/lib/commands/prime.d.ts +16 -0
- package/dist/lib/commands/prime.d.ts.map +1 -0
- package/dist/lib/commands/prime.js +168 -0
- package/dist/lib/commands/prime.js.map +1 -0
- package/dist/lib/commands/search.d.ts +20 -0
- package/dist/lib/commands/search.d.ts.map +1 -0
- package/dist/lib/commands/search.js +111 -0
- package/dist/lib/commands/search.js.map +1 -0
- package/dist/lib/errors.d.ts +80 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +189 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/formatters/output.d.ts +5 -0
- package/dist/lib/formatters/output.d.ts.map +1 -0
- package/dist/lib/formatters/output.js +5 -0
- package/dist/lib/formatters/output.js.map +1 -0
- package/dist/lib/formatters.d.ts +29 -0
- package/dist/lib/formatters.d.ts.map +1 -0
- package/dist/lib/formatters.js +141 -0
- package/dist/lib/formatters.js.map +1 -0
- package/dist/lib/index-tools.d.ts +108 -0
- package/dist/lib/index-tools.d.ts.map +1 -0
- package/dist/lib/index-tools.js +311 -0
- package/dist/lib/index-tools.js.map +1 -0
- package/dist/lib/index.d.ts +21 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +42 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/kit-wrapper.d.ts +70 -0
- package/dist/lib/kit-wrapper.d.ts.map +1 -0
- package/dist/lib/kit-wrapper.js +462 -0
- package/dist/lib/kit-wrapper.js.map +1 -0
- package/dist/lib/logger.d.ts +28 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +39 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/types.d.ts +179 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +48 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utils/args.d.ts +40 -0
- package/dist/lib/utils/args.d.ts.map +1 -0
- package/dist/lib/utils/args.js +58 -0
- package/dist/lib/utils/args.js.map +1 -0
- package/dist/lib/utils/git.d.ts +23 -0
- package/dist/lib/utils/git.d.ts.map +1 -0
- package/dist/lib/utils/git.js +50 -0
- package/dist/lib/utils/git.js.map +1 -0
- package/dist/lib/utils/index-parser.d.ts +155 -0
- package/dist/lib/utils/index-parser.d.ts.map +1 -0
- package/dist/lib/utils/index-parser.js +252 -0
- package/dist/lib/utils/index-parser.js.map +1 -0
- package/dist/lib/validators.d.ts +138 -0
- package/dist/lib/validators.d.ts.map +1 -0
- package/dist/lib/validators.js +302 -0
- package/dist/lib/validators.js.map +1 -0
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +769 -0
- package/dist/mcp/index.js.map +1 -0
- package/package.json +5 -2
- package/src/cli.ts +170 -0
- package/src/lib/ast/index.ts +32 -0
- package/src/lib/ast/languages.ts +172 -0
- package/src/lib/ast/pattern.ts +299 -0
- package/src/lib/ast/searcher.ts +381 -0
- package/src/lib/ast/types.ts +99 -0
- package/src/lib/commands/callers.ts +226 -0
- package/src/lib/commands/find.ts +159 -0
- package/src/lib/commands/overview.ts +73 -0
- package/src/lib/commands/prime.ts +271 -0
- package/src/lib/commands/search.ts +146 -0
- package/src/lib/errors.ts +221 -0
- package/src/lib/formatters/output.ts +9 -0
- package/src/lib/formatters.ts +189 -0
- package/src/lib/index-tools.ts +471 -0
- package/src/lib/index.ts +122 -0
- package/src/lib/kit-wrapper.ts +675 -0
- package/src/lib/logger.ts +57 -0
- package/src/lib/types.ts +228 -0
- package/src/lib/utils/args.ts +72 -0
- package/src/lib/utils/git.ts +65 -0
- package/src/lib/utils/index-parser.ts +350 -0
- package/src/lib/validators.ts +437 -0
- package/src/mcp/index.ts +144 -79
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prime command - Generate or refresh PROJECT_INDEX.json
|
|
3
|
+
*
|
|
4
|
+
* Migrated from prime-reporter.ts to integrate with kit-index CLI.
|
|
5
|
+
* Supports dual output formats (markdown with colors, or JSON).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { getFileAgeHours, getFileSizeMB } from '@side-quest/core/fs'
|
|
10
|
+
import {
|
|
11
|
+
ensureCommandAvailable,
|
|
12
|
+
spawnWithTimeout,
|
|
13
|
+
} from '@side-quest/core/spawn'
|
|
14
|
+
import { color, OutputFormat } from '../formatters/output'
|
|
15
|
+
import { getTargetDir, INDEX_FILE, MAX_AGE_HOURS } from '../utils/git.js'
|
|
16
|
+
|
|
17
|
+
interface IndexStats {
|
|
18
|
+
files: number
|
|
19
|
+
symbols: number
|
|
20
|
+
hasTree: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse index stats from PROJECT_INDEX.json
|
|
25
|
+
*/
|
|
26
|
+
async function parseIndexStats(indexPath: string): Promise<IndexStats> {
|
|
27
|
+
const file = Bun.file(indexPath)
|
|
28
|
+
const json = (await file.json()) as {
|
|
29
|
+
files: unknown[]
|
|
30
|
+
symbols: Record<string, unknown[]>
|
|
31
|
+
file_tree?: unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const symbolCount = Object.values(json.symbols).reduce(
|
|
35
|
+
(sum, arr) => sum + arr.length,
|
|
36
|
+
0,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
files: json.files.length,
|
|
41
|
+
symbols: symbolCount,
|
|
42
|
+
hasTree: json.file_tree !== undefined,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate the index using kit CLI
|
|
48
|
+
*/
|
|
49
|
+
async function generateIndex(
|
|
50
|
+
targetDir: string,
|
|
51
|
+
indexPath: string,
|
|
52
|
+
): Promise<{ durationSec: number }> {
|
|
53
|
+
const kitCmd = ensureCommandAvailable('kit')
|
|
54
|
+
const startTime = Date.now()
|
|
55
|
+
const result = await spawnWithTimeout(
|
|
56
|
+
[kitCmd, 'index', targetDir, '-o', indexPath],
|
|
57
|
+
60_000,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if (result.timedOut) {
|
|
61
|
+
throw new Error('kit index timed out')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (result.exitCode !== 0) {
|
|
65
|
+
throw new Error(`kit index failed: ${result.stderr}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const durationSec = (Date.now() - startTime) / 1000
|
|
69
|
+
return { durationSec }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Report existing index (markdown format)
|
|
74
|
+
*/
|
|
75
|
+
async function reportExistingMarkdown(
|
|
76
|
+
ageHours: number,
|
|
77
|
+
indexPath: string,
|
|
78
|
+
targetDir: string,
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
const stats = await parseIndexStats(indexPath)
|
|
81
|
+
const size = getFileSizeMB(indexPath)
|
|
82
|
+
|
|
83
|
+
console.log(color('cyan', '\n📊 PROJECT_INDEX.json exists\n'))
|
|
84
|
+
console.log(color('dim', `Location: ${targetDir}`))
|
|
85
|
+
console.log(color('dim', `Age: ${ageHours.toFixed(1)} hours`))
|
|
86
|
+
console.log(color('dim', `Files: ${stats.files}`))
|
|
87
|
+
console.log(color('dim', `Symbols: ${stats.symbols}`))
|
|
88
|
+
console.log(color('dim', `Size: ${size} MB`))
|
|
89
|
+
console.log(
|
|
90
|
+
color(
|
|
91
|
+
'yellow',
|
|
92
|
+
'\n⚠️ Index is less than 24 hours old. Use --force to regenerate.',
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Report existing index (JSON format)
|
|
99
|
+
*/
|
|
100
|
+
async function reportExistingJSON(
|
|
101
|
+
ageHours: number,
|
|
102
|
+
indexPath: string,
|
|
103
|
+
targetDir: string,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
const stats = await parseIndexStats(indexPath)
|
|
106
|
+
const sizeMB = getFileSizeMB(indexPath)
|
|
107
|
+
|
|
108
|
+
console.log(
|
|
109
|
+
JSON.stringify(
|
|
110
|
+
{
|
|
111
|
+
status: 'exists',
|
|
112
|
+
location: targetDir,
|
|
113
|
+
ageHours: Number.parseFloat(ageHours.toFixed(1)),
|
|
114
|
+
files: stats.files,
|
|
115
|
+
symbols: stats.symbols,
|
|
116
|
+
size: `${sizeMB} MB`,
|
|
117
|
+
message: 'Index is less than 24 hours old. Use --force to regenerate.',
|
|
118
|
+
},
|
|
119
|
+
null,
|
|
120
|
+
2,
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Report successful generation (markdown format)
|
|
127
|
+
*/
|
|
128
|
+
async function reportSuccessMarkdown(
|
|
129
|
+
durationSec: number,
|
|
130
|
+
indexPath: string,
|
|
131
|
+
targetDir: string,
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const stats = await parseIndexStats(indexPath)
|
|
134
|
+
const sizeMB = getFileSizeMB(indexPath)
|
|
135
|
+
|
|
136
|
+
console.log(
|
|
137
|
+
color('green', '\n✅ PROJECT_INDEX.json generated successfully\n'),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
console.log(color('cyan', 'Stats:'))
|
|
141
|
+
console.log(` ${color('dim', '•')} Location: ${color('blue', targetDir)}`)
|
|
142
|
+
console.log(
|
|
143
|
+
` ${color('dim', '•')} Files indexed: ${color('blue', stats.files.toString())}`,
|
|
144
|
+
)
|
|
145
|
+
console.log(
|
|
146
|
+
` ${color('dim', '•')} Symbols extracted: ${color('blue', stats.symbols.toString())}`,
|
|
147
|
+
)
|
|
148
|
+
console.log(
|
|
149
|
+
` ${color('dim', '•')} Index size: ${color('blue', `${sizeMB} MB`)}`,
|
|
150
|
+
)
|
|
151
|
+
console.log(
|
|
152
|
+
` ${color('dim', '•')} Time taken: ${color('blue', `${durationSec.toFixed(1)}s`)}`,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
console.log(color('cyan', '\nYou can now use:'))
|
|
156
|
+
console.log(
|
|
157
|
+
` ${color('dim', '•')} ${color('magenta', 'bun run src/cli.ts find <symbol>')} - Find symbol definitions`,
|
|
158
|
+
)
|
|
159
|
+
console.log(
|
|
160
|
+
` ${color('dim', '•')} ${color('magenta', 'bun run src/cli.ts callers <fn>')} - Find who calls a function`,
|
|
161
|
+
)
|
|
162
|
+
console.log(
|
|
163
|
+
` ${color('dim', '•')} ${color('magenta', 'bun run src/cli.ts overview <file>')} - Get file symbol summary`,
|
|
164
|
+
)
|
|
165
|
+
console.log(
|
|
166
|
+
` ${color('dim', '•')} ${color('magenta', 'bun run src/cli.ts stats')} - Codebase overview`,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Report successful generation (JSON format)
|
|
172
|
+
*/
|
|
173
|
+
async function reportSuccessJSON(
|
|
174
|
+
durationSec: number,
|
|
175
|
+
indexPath: string,
|
|
176
|
+
targetDir: string,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
const stats = await parseIndexStats(indexPath)
|
|
179
|
+
const sizeMB = getFileSizeMB(indexPath)
|
|
180
|
+
|
|
181
|
+
console.log(
|
|
182
|
+
JSON.stringify(
|
|
183
|
+
{
|
|
184
|
+
success: true,
|
|
185
|
+
location: targetDir,
|
|
186
|
+
stats: {
|
|
187
|
+
files: stats.files,
|
|
188
|
+
symbols: stats.symbols,
|
|
189
|
+
hasTree: stats.hasTree,
|
|
190
|
+
size: `${sizeMB} MB`,
|
|
191
|
+
durationSec: Number.parseFloat(durationSec.toFixed(1)),
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
null,
|
|
195
|
+
2,
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Execute prime command
|
|
202
|
+
*
|
|
203
|
+
* @param force - Force regenerate even if index is fresh
|
|
204
|
+
* @param format - Output format (markdown or JSON)
|
|
205
|
+
* @param customPath - Optional custom path to index (defaults to git root or CWD)
|
|
206
|
+
*/
|
|
207
|
+
export async function executePrime(
|
|
208
|
+
force: boolean,
|
|
209
|
+
format: OutputFormat,
|
|
210
|
+
customPath?: string,
|
|
211
|
+
): Promise<void> {
|
|
212
|
+
try {
|
|
213
|
+
// Determine target directory and index path
|
|
214
|
+
const targetDir = await getTargetDir(customPath)
|
|
215
|
+
const indexPath = join(targetDir, INDEX_FILE)
|
|
216
|
+
|
|
217
|
+
const ageHours = getFileAgeHours(indexPath)
|
|
218
|
+
|
|
219
|
+
// Check if index exists and is fresh
|
|
220
|
+
if (ageHours !== null && ageHours <= MAX_AGE_HOURS && !force) {
|
|
221
|
+
if (format === OutputFormat.JSON) {
|
|
222
|
+
await reportExistingJSON(ageHours, indexPath, targetDir)
|
|
223
|
+
} else {
|
|
224
|
+
await reportExistingMarkdown(ageHours, indexPath, targetDir)
|
|
225
|
+
}
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Generate progress indicator (markdown only)
|
|
230
|
+
if (format === OutputFormat.MARKDOWN) {
|
|
231
|
+
console.log(color('blue', '▶ Generating index...'))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Generate new index
|
|
235
|
+
const { durationSec } = await generateIndex(targetDir, indexPath)
|
|
236
|
+
|
|
237
|
+
// Report success
|
|
238
|
+
if (format === OutputFormat.JSON) {
|
|
239
|
+
await reportSuccessJSON(durationSec, indexPath, targetDir)
|
|
240
|
+
} else {
|
|
241
|
+
await reportSuccessMarkdown(durationSec, indexPath, targetDir)
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (format === OutputFormat.JSON) {
|
|
245
|
+
console.error(
|
|
246
|
+
JSON.stringify(
|
|
247
|
+
{
|
|
248
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
249
|
+
isError: true,
|
|
250
|
+
},
|
|
251
|
+
null,
|
|
252
|
+
2,
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
} else {
|
|
256
|
+
console.error(color('red', '\n❌ Error:'), error)
|
|
257
|
+
|
|
258
|
+
// Check if kit is installed
|
|
259
|
+
try {
|
|
260
|
+
ensureCommandAvailable('kit')
|
|
261
|
+
} catch {
|
|
262
|
+
console.error(color('yellow', '\n💡 Kit CLI not found. Install with:'))
|
|
263
|
+
console.error(color('dim', ' uv tool install cased-kit'))
|
|
264
|
+
console.error(color('dim', ' # or'))
|
|
265
|
+
console.error(color('dim', ' pipx install cased-kit'))
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
process.exit(1)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic search command - Natural language code search
|
|
3
|
+
*
|
|
4
|
+
* Uses kit semantic search to find code by meaning rather than exact text.
|
|
5
|
+
* Gracefully falls back to grep if ML dependencies unavailable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { color, OutputFormat } from '../formatters/output'
|
|
9
|
+
import { executeKitSemantic } from '../kit-wrapper'
|
|
10
|
+
import type { SemanticMatch, SemanticOptions, SemanticResult } from '../types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execute semantic search command
|
|
14
|
+
*
|
|
15
|
+
* Performs natural language search over codebase using vector embeddings.
|
|
16
|
+
* Shows warning with install instructions if semantic unavailable.
|
|
17
|
+
*
|
|
18
|
+
* @param query - Natural language search query
|
|
19
|
+
* @param options - Search options (path, topK, chunkBy, buildIndex)
|
|
20
|
+
* @param format - Output format (markdown or JSON)
|
|
21
|
+
*/
|
|
22
|
+
export async function executeSearch(
|
|
23
|
+
query: string,
|
|
24
|
+
options: Omit<SemanticOptions, 'query'>,
|
|
25
|
+
format: OutputFormat,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
try {
|
|
28
|
+
// Execute semantic search
|
|
29
|
+
const result = executeKitSemantic({ query, ...options })
|
|
30
|
+
|
|
31
|
+
// Handle errors
|
|
32
|
+
if ('error' in result) {
|
|
33
|
+
if (format === OutputFormat.JSON) {
|
|
34
|
+
console.error(
|
|
35
|
+
JSON.stringify(
|
|
36
|
+
{
|
|
37
|
+
error: result.error,
|
|
38
|
+
query,
|
|
39
|
+
isError: true,
|
|
40
|
+
},
|
|
41
|
+
null,
|
|
42
|
+
2,
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
} else {
|
|
46
|
+
console.error(color('red', '\n❌ Error:'), result.error, '\n')
|
|
47
|
+
|
|
48
|
+
// Show install hint if semantic unavailable
|
|
49
|
+
if ('hint' in result && result.hint) {
|
|
50
|
+
console.error(color('yellow', '💡 Tip:'), result.hint, '\n')
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Show fallback warning if grep was used
|
|
57
|
+
if (result.fallback && format === OutputFormat.MARKDOWN) {
|
|
58
|
+
console.log(
|
|
59
|
+
color(
|
|
60
|
+
'yellow',
|
|
61
|
+
'\n⚠️ Semantic search unavailable - using grep fallback\n',
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
if (result.installHint) {
|
|
65
|
+
console.log(color('dim', result.installHint), '\n')
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Output results
|
|
70
|
+
if (format === OutputFormat.JSON) {
|
|
71
|
+
console.log(JSON.stringify(result, null, 2))
|
|
72
|
+
} else {
|
|
73
|
+
console.log(formatMarkdown(result))
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (format === OutputFormat.JSON) {
|
|
77
|
+
console.error(
|
|
78
|
+
JSON.stringify(
|
|
79
|
+
{
|
|
80
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
81
|
+
query,
|
|
82
|
+
isError: true,
|
|
83
|
+
},
|
|
84
|
+
null,
|
|
85
|
+
2,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
} else {
|
|
89
|
+
console.error(
|
|
90
|
+
color('red', '\n❌ Error:'),
|
|
91
|
+
error instanceof Error ? error.message : error,
|
|
92
|
+
'\n',
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
process.exit(1)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format semantic results as markdown
|
|
101
|
+
*/
|
|
102
|
+
function formatMarkdown(result: SemanticResult): string {
|
|
103
|
+
const { query, count, matches, fallback } = result
|
|
104
|
+
|
|
105
|
+
if (count === 0) {
|
|
106
|
+
return color('yellow', `\n⚠️ No matches found for query: "${query}"\n`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let output = color('cyan', `\n🔍 Found ${count} semantic match(es) for: `)
|
|
110
|
+
output += color('blue', `"${query}"`)
|
|
111
|
+
if (fallback) {
|
|
112
|
+
output += color('dim', ' (grep fallback)')
|
|
113
|
+
}
|
|
114
|
+
output += '\n\n'
|
|
115
|
+
|
|
116
|
+
// Group by file
|
|
117
|
+
const byFile = new Map<string, SemanticMatch[]>()
|
|
118
|
+
for (const match of matches) {
|
|
119
|
+
if (!byFile.has(match.file)) {
|
|
120
|
+
byFile.set(match.file, [])
|
|
121
|
+
}
|
|
122
|
+
byFile.get(match.file)?.push(match)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Output each file's matches
|
|
126
|
+
for (const [file, fileMatches] of byFile.entries()) {
|
|
127
|
+
output += color('dim', `${file}:\n`)
|
|
128
|
+
for (const match of fileMatches) {
|
|
129
|
+
const lineInfo = match.startLine
|
|
130
|
+
? `L${match.startLine}${match.endLine ? `-${match.endLine}` : ''}`
|
|
131
|
+
: ''
|
|
132
|
+
const scoreStr = `(${(match.score * 100).toFixed(1)}%)`
|
|
133
|
+
|
|
134
|
+
output += ` ${color('dim', lineInfo)} ${color('green', scoreStr)}\n`
|
|
135
|
+
|
|
136
|
+
// Show chunk preview (first 2 lines max)
|
|
137
|
+
const lines = match.chunk.split('\n').slice(0, 2)
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
output += ` ${color('dim', line.trim())}\n`
|
|
140
|
+
}
|
|
141
|
+
output += '\n'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return output
|
|
146
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kit Plugin Error Taxonomy
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive error handling with clear recovery instructions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
detectErrorFromOutput as coreDetectErrorFromOutput,
|
|
9
|
+
type ErrorPattern,
|
|
10
|
+
isTimeoutOutput,
|
|
11
|
+
PluginError,
|
|
12
|
+
} from '@side-quest/core/instrumentation'
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Error Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Kit error types with associated recovery strategies.
|
|
20
|
+
*/
|
|
21
|
+
export enum KitErrorType {
|
|
22
|
+
/** Kit CLI not found in PATH */
|
|
23
|
+
KitNotInstalled = 'KitNotInstalled',
|
|
24
|
+
/** Invalid repository path */
|
|
25
|
+
InvalidPath = 'InvalidPath',
|
|
26
|
+
/** Invalid input (regex, glob, etc.) */
|
|
27
|
+
InvalidInput = 'InvalidInput',
|
|
28
|
+
/** Semantic search not available (missing ML deps) */
|
|
29
|
+
SemanticNotAvailable = 'SemanticNotAvailable',
|
|
30
|
+
/** Semantic search index not yet built for repository */
|
|
31
|
+
SemanticIndexNotBuilt = 'SemanticIndexNotBuilt',
|
|
32
|
+
/** Too many results returned */
|
|
33
|
+
TooManyResults = 'TooManyResults',
|
|
34
|
+
/** Kit command failed */
|
|
35
|
+
KitCommandFailed = 'KitCommandFailed',
|
|
36
|
+
/** Failed to parse Kit output */
|
|
37
|
+
OutputParseError = 'OutputParseError',
|
|
38
|
+
/** Operation timed out */
|
|
39
|
+
Timeout = 'Timeout',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Error Messages & Recovery Hints
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* User-facing error messages with recovery hints.
|
|
48
|
+
*/
|
|
49
|
+
export const ERROR_MESSAGES: Record<
|
|
50
|
+
KitErrorType,
|
|
51
|
+
{ message: string; hint: string }
|
|
52
|
+
> = {
|
|
53
|
+
[KitErrorType.KitNotInstalled]: {
|
|
54
|
+
message: 'Kit CLI is not installed or not found in PATH.',
|
|
55
|
+
hint: 'Install Kit with: uv tool install cased-kit',
|
|
56
|
+
},
|
|
57
|
+
[KitErrorType.InvalidPath]: {
|
|
58
|
+
message: 'The specified path does not exist or is not accessible.',
|
|
59
|
+
hint: 'Check the path exists and you have read permissions.',
|
|
60
|
+
},
|
|
61
|
+
[KitErrorType.InvalidInput]: {
|
|
62
|
+
message: 'Invalid input provided.',
|
|
63
|
+
hint: 'Check your search pattern or options for syntax errors.',
|
|
64
|
+
},
|
|
65
|
+
[KitErrorType.SemanticNotAvailable]: {
|
|
66
|
+
message: 'Semantic search is not available.',
|
|
67
|
+
hint: `Install ML dependencies: pip install 'cased-kit[ml]' or uv tool install 'cased-kit[ml]'`,
|
|
68
|
+
},
|
|
69
|
+
[KitErrorType.SemanticIndexNotBuilt]: {
|
|
70
|
+
message: 'Vector index has not been built for this repository yet.',
|
|
71
|
+
hint: 'Build it by running the CLI command provided in the error details. After building (one-time), semantic search will be fast and cached.',
|
|
72
|
+
},
|
|
73
|
+
[KitErrorType.TooManyResults]: {
|
|
74
|
+
message: 'Query returned too many results.',
|
|
75
|
+
hint: 'Try a more specific query or use --max-results to limit output.',
|
|
76
|
+
},
|
|
77
|
+
[KitErrorType.KitCommandFailed]: {
|
|
78
|
+
message: 'Kit command failed to execute.',
|
|
79
|
+
hint: 'Check the error details below for more information.',
|
|
80
|
+
},
|
|
81
|
+
[KitErrorType.OutputParseError]: {
|
|
82
|
+
message: 'Failed to parse Kit output.',
|
|
83
|
+
hint: 'This may be a bug. Check logs for details.',
|
|
84
|
+
},
|
|
85
|
+
[KitErrorType.Timeout]: {
|
|
86
|
+
message:
|
|
87
|
+
'Operation timed out (this may take longer on large repositories).',
|
|
88
|
+
hint: 'Try again—the vector index will be cached. If it times out again, clear .kit/vector_db and rebuild with build_index: true.',
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Error Class
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Custom error class for Kit operations.
|
|
98
|
+
*
|
|
99
|
+
* Extends PluginError to provide Kit-specific error handling with:
|
|
100
|
+
* - Typed error classification (KitErrorType enum)
|
|
101
|
+
* - User-friendly messages with recovery hints
|
|
102
|
+
* - Structured JSON serialization for MCP responses
|
|
103
|
+
*/
|
|
104
|
+
export class KitError extends PluginError<KitErrorType> {
|
|
105
|
+
constructor(type: KitErrorType, details?: string, stderr?: string) {
|
|
106
|
+
const errorInfo = ERROR_MESSAGES[type]
|
|
107
|
+
super('KitError', type, errorInfo.message, errorInfo.hint, details, stderr)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Semantic Search Fallback
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Install hint shown when semantic search is unavailable.
|
|
117
|
+
*/
|
|
118
|
+
export const SEMANTIC_INSTALL_HINT = `
|
|
119
|
+
Semantic search is not available. Using text search instead.
|
|
120
|
+
|
|
121
|
+
To enable semantic search, install the ML dependencies:
|
|
122
|
+
pip install 'cased-kit[ml]'
|
|
123
|
+
|
|
124
|
+
Or if using uv:
|
|
125
|
+
uv tool install 'cased-kit[ml]'
|
|
126
|
+
`.trim()
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if an error indicates semantic search is unavailable.
|
|
130
|
+
* @param stderr - Standard error output from Kit
|
|
131
|
+
*/
|
|
132
|
+
export function isSemanticUnavailableError(stderr: string): boolean {
|
|
133
|
+
const patterns = [
|
|
134
|
+
'sentence-transformers',
|
|
135
|
+
'chromadb',
|
|
136
|
+
'semantic search',
|
|
137
|
+
'vector index',
|
|
138
|
+
'embedding',
|
|
139
|
+
]
|
|
140
|
+
const lowerStderr = stderr.toLowerCase()
|
|
141
|
+
return patterns.some((p) => lowerStderr.includes(p))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if an error indicates a timeout occurred.
|
|
146
|
+
*
|
|
147
|
+
* Re-exports the core utility for backward compatibility.
|
|
148
|
+
*
|
|
149
|
+
* @param stderr - Standard error output
|
|
150
|
+
*/
|
|
151
|
+
export const isTimeoutError = isTimeoutOutput
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Error Detection Helpers
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Kit-specific error patterns for subprocess output detection.
|
|
159
|
+
*
|
|
160
|
+
* These patterns are tested in order by detectErrorType().
|
|
161
|
+
*/
|
|
162
|
+
const KIT_ERROR_PATTERNS: ErrorPattern<KitErrorType>[] = [
|
|
163
|
+
// Semantic search unavailable (must be first - most specific)
|
|
164
|
+
{
|
|
165
|
+
type: KitErrorType.SemanticNotAvailable,
|
|
166
|
+
patterns: [
|
|
167
|
+
'sentence-transformers',
|
|
168
|
+
'chromadb',
|
|
169
|
+
'semantic search',
|
|
170
|
+
'vector index',
|
|
171
|
+
'embedding',
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
// Path errors
|
|
175
|
+
{
|
|
176
|
+
type: KitErrorType.InvalidPath,
|
|
177
|
+
patterns: ['no such file', 'not found', 'does not exist'],
|
|
178
|
+
},
|
|
179
|
+
// Timeout errors
|
|
180
|
+
{
|
|
181
|
+
type: KitErrorType.Timeout,
|
|
182
|
+
patterns: ['timeout', 'timed out', 'etimedout'],
|
|
183
|
+
},
|
|
184
|
+
// Too many results
|
|
185
|
+
{
|
|
186
|
+
type: KitErrorType.TooManyResults,
|
|
187
|
+
patterns: ['too many', 'limit'],
|
|
188
|
+
},
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Detect error type from Kit stderr output.
|
|
193
|
+
*
|
|
194
|
+
* Uses the generic core pattern matcher with Kit-specific patterns.
|
|
195
|
+
*
|
|
196
|
+
* @param stderr - Standard error output from Kit
|
|
197
|
+
* @param _exitCode - Exit code from Kit process (currently unused)
|
|
198
|
+
*/
|
|
199
|
+
export function detectErrorType(
|
|
200
|
+
stderr: string,
|
|
201
|
+
_exitCode: number,
|
|
202
|
+
): KitErrorType {
|
|
203
|
+
return coreDetectErrorFromOutput(
|
|
204
|
+
stderr,
|
|
205
|
+
KIT_ERROR_PATTERNS,
|
|
206
|
+
KitErrorType.KitCommandFailed, // Default
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a KitError from process output.
|
|
212
|
+
* @param stderr - Standard error output
|
|
213
|
+
* @param exitCode - Process exit code
|
|
214
|
+
*/
|
|
215
|
+
export function createErrorFromOutput(
|
|
216
|
+
stderr: string,
|
|
217
|
+
exitCode: number,
|
|
218
|
+
): KitError {
|
|
219
|
+
const errorType = detectErrorType(stderr, exitCode)
|
|
220
|
+
return new KitError(errorType, undefined, stderr.trim())
|
|
221
|
+
}
|