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