@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
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kit Plugin Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Shared types for the Kit MCP server and CLI wrapper.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Response Format
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Response format options for tool output.
|
|
13
|
+
*/
|
|
14
|
+
export enum ResponseFormat {
|
|
15
|
+
MARKDOWN = 'markdown',
|
|
16
|
+
JSON = 'json',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Grep Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A single grep match result.
|
|
25
|
+
*/
|
|
26
|
+
export interface GrepMatch {
|
|
27
|
+
/** Relative file path from repository root */
|
|
28
|
+
file: string
|
|
29
|
+
/** Line number (1-indexed) */
|
|
30
|
+
line?: number
|
|
31
|
+
/** Matched line content */
|
|
32
|
+
content: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Result of a grep search operation.
|
|
37
|
+
*/
|
|
38
|
+
export interface GrepResult {
|
|
39
|
+
/** Number of matches found */
|
|
40
|
+
count: number
|
|
41
|
+
/** Array of match objects */
|
|
42
|
+
matches: GrepMatch[]
|
|
43
|
+
/** Search pattern used */
|
|
44
|
+
pattern: string
|
|
45
|
+
/** Repository path searched */
|
|
46
|
+
path: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Options for grep search.
|
|
51
|
+
*/
|
|
52
|
+
export interface GrepOptions {
|
|
53
|
+
/** Search pattern (text or regex) */
|
|
54
|
+
pattern: string
|
|
55
|
+
/** Repository path to search */
|
|
56
|
+
path?: string
|
|
57
|
+
/** Case-sensitive search (default: true) */
|
|
58
|
+
caseSensitive?: boolean
|
|
59
|
+
/** File pattern to include (e.g., "*.py") */
|
|
60
|
+
include?: string
|
|
61
|
+
/** File pattern to exclude */
|
|
62
|
+
exclude?: string
|
|
63
|
+
/** Maximum results to return (default: 100) */
|
|
64
|
+
maxResults?: number
|
|
65
|
+
/** Subdirectory to search within */
|
|
66
|
+
directory?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Semantic Search Types
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A single semantic search match.
|
|
75
|
+
*/
|
|
76
|
+
export interface SemanticMatch {
|
|
77
|
+
/** Relative file path */
|
|
78
|
+
file: string
|
|
79
|
+
/** Code chunk that matched */
|
|
80
|
+
chunk: string
|
|
81
|
+
/** Relevance score (higher = more relevant) */
|
|
82
|
+
score: number
|
|
83
|
+
/** Start line of the chunk */
|
|
84
|
+
startLine?: number
|
|
85
|
+
/** End line of the chunk */
|
|
86
|
+
endLine?: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Result of a semantic search operation.
|
|
91
|
+
*/
|
|
92
|
+
export interface SemanticResult {
|
|
93
|
+
/** Number of matches found */
|
|
94
|
+
count: number
|
|
95
|
+
/** Array of semantic matches */
|
|
96
|
+
matches: SemanticMatch[]
|
|
97
|
+
/** Natural language query used */
|
|
98
|
+
query: string
|
|
99
|
+
/** Repository path searched */
|
|
100
|
+
path: string
|
|
101
|
+
/** Whether results came from fallback grep */
|
|
102
|
+
fallback?: boolean
|
|
103
|
+
/** Install hint if semantic search unavailable */
|
|
104
|
+
installHint?: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Options for semantic search.
|
|
109
|
+
*/
|
|
110
|
+
export interface SemanticOptions {
|
|
111
|
+
/** Natural language query */
|
|
112
|
+
query: string
|
|
113
|
+
/** Repository path to search */
|
|
114
|
+
path?: string
|
|
115
|
+
/** Number of results to return (default: 5) */
|
|
116
|
+
topK?: number
|
|
117
|
+
/** Chunking strategy: 'symbols' or 'lines' */
|
|
118
|
+
chunkBy?: 'symbols' | 'lines'
|
|
119
|
+
/** Force rebuild of vector index */
|
|
120
|
+
buildIndex?: boolean
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Symbol Usages Types
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A symbol usage/definition found by Kit.
|
|
129
|
+
*/
|
|
130
|
+
export interface SymbolUsage {
|
|
131
|
+
/** File containing the symbol */
|
|
132
|
+
file: string
|
|
133
|
+
/** Symbol type (function, class, variable, etc.) */
|
|
134
|
+
type: string
|
|
135
|
+
/** Symbol name */
|
|
136
|
+
name: string
|
|
137
|
+
/** Line number (may be null in current Kit version) */
|
|
138
|
+
line: number | null
|
|
139
|
+
/** Context around the usage */
|
|
140
|
+
context: string | null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Result of a symbol usages search.
|
|
145
|
+
*/
|
|
146
|
+
export interface UsagesResult {
|
|
147
|
+
/** Number of usages found */
|
|
148
|
+
count: number
|
|
149
|
+
/** Array of symbol usages */
|
|
150
|
+
usages: SymbolUsage[]
|
|
151
|
+
/** Symbol name searched for */
|
|
152
|
+
symbolName: string
|
|
153
|
+
/** Repository path searched */
|
|
154
|
+
path: string
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Options for symbol usages search.
|
|
159
|
+
*/
|
|
160
|
+
export interface UsagesOptions {
|
|
161
|
+
/** Repository path */
|
|
162
|
+
path?: string
|
|
163
|
+
/** Symbol name to find usages for */
|
|
164
|
+
symbolName: string
|
|
165
|
+
/** Filter by symbol type (function, class, etc.) */
|
|
166
|
+
symbolType?: string
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Generic Result Types
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Error result type.
|
|
175
|
+
*/
|
|
176
|
+
export interface ErrorResult {
|
|
177
|
+
/** Error message */
|
|
178
|
+
error: string
|
|
179
|
+
/** Optional recovery hint */
|
|
180
|
+
hint?: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Generic result type that can be success or error.
|
|
185
|
+
*/
|
|
186
|
+
export type KitResult<T> = T | ErrorResult
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Type guard for error results.
|
|
190
|
+
*/
|
|
191
|
+
export function isError<T extends object>(
|
|
192
|
+
result: KitResult<T>,
|
|
193
|
+
): result is ErrorResult {
|
|
194
|
+
return typeof result === 'object' && result !== null && 'error' in result
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================================
|
|
198
|
+
// Default Configuration
|
|
199
|
+
// ============================================================================
|
|
200
|
+
|
|
201
|
+
/** Environment variable name for configuring default path */
|
|
202
|
+
export const KIT_DEFAULT_PATH_ENV = 'KIT_DEFAULT_PATH'
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get the default path for Kit operations using cascading defaults:
|
|
206
|
+
* 1. KIT_DEFAULT_PATH environment variable (if set)
|
|
207
|
+
* 2. Current working directory (process.cwd())
|
|
208
|
+
*
|
|
209
|
+
* @returns Resolved default path for Kit operations
|
|
210
|
+
*/
|
|
211
|
+
export function getDefaultKitPath(): string {
|
|
212
|
+
return process.env[KIT_DEFAULT_PATH_ENV] || process.cwd()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Default timeout for grep operations (ms) */
|
|
216
|
+
export const GREP_TIMEOUT = 30000
|
|
217
|
+
|
|
218
|
+
/** Default timeout for semantic operations (ms) */
|
|
219
|
+
export const SEMANTIC_TIMEOUT = 60000
|
|
220
|
+
|
|
221
|
+
/** Default timeout for symbol usages operations (ms) */
|
|
222
|
+
export const USAGES_TIMEOUT = 45000
|
|
223
|
+
|
|
224
|
+
/** Default max results for grep */
|
|
225
|
+
export const DEFAULT_MAX_RESULTS = 100
|
|
226
|
+
|
|
227
|
+
/** Default top-k for semantic search */
|
|
228
|
+
export const DEFAULT_TOP_K = 5
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-line argument parsing utilities
|
|
3
|
+
*
|
|
4
|
+
* Adapted from cinema-bandit plugin's argument parsing pattern.
|
|
5
|
+
* Parses arguments in the format: <command> [positional] [--flag value]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parsed command-line arguments
|
|
10
|
+
*/
|
|
11
|
+
export interface ParsedArgs {
|
|
12
|
+
/** The subcommand (e.g., "prime", "find", "callers") */
|
|
13
|
+
command: string
|
|
14
|
+
/** Positional arguments after the command */
|
|
15
|
+
positional: string[]
|
|
16
|
+
/** Named flags (e.g., --format json, --force) */
|
|
17
|
+
flags: Record<string, string>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parses command-line arguments into a structured object.
|
|
22
|
+
*
|
|
23
|
+
* @param args - Raw arguments from process.argv.slice(2)
|
|
24
|
+
* @returns Parsed arguments with command, positional args, and flags
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* parseArgs(["prime"]);
|
|
29
|
+
* // { command: "prime", positional: [], flags: {} }
|
|
30
|
+
*
|
|
31
|
+
* parseArgs(["find", "MyFunction"]);
|
|
32
|
+
* // { command: "find", positional: ["MyFunction"], flags: {} }
|
|
33
|
+
*
|
|
34
|
+
* parseArgs(["find", "MyFunction", "--format", "json"]);
|
|
35
|
+
* // { command: "find", positional: ["MyFunction"], flags: { "format": "json" } }
|
|
36
|
+
*
|
|
37
|
+
* parseArgs(["prime", "--force"]);
|
|
38
|
+
* // { command: "prime", positional: [], flags: { "force": "true" } }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function parseArgs(args: string[]): ParsedArgs {
|
|
42
|
+
const command = args[0] ?? ''
|
|
43
|
+
const positional: string[] = []
|
|
44
|
+
const flags: Record<string, string> = {}
|
|
45
|
+
|
|
46
|
+
let i = 1
|
|
47
|
+
while (i < args.length) {
|
|
48
|
+
const arg = args[i] as string
|
|
49
|
+
|
|
50
|
+
if (arg.startsWith('--')) {
|
|
51
|
+
// Handle --flag or --flag value
|
|
52
|
+
const key = arg.slice(2)
|
|
53
|
+
const nextArg = args[i + 1]
|
|
54
|
+
|
|
55
|
+
// Check if there's a value after the flag
|
|
56
|
+
if (nextArg && !nextArg.startsWith('--')) {
|
|
57
|
+
flags[key] = nextArg
|
|
58
|
+
i += 2
|
|
59
|
+
} else {
|
|
60
|
+
// Boolean flag (no value)
|
|
61
|
+
flags[key] = 'true'
|
|
62
|
+
i++
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// Positional argument
|
|
66
|
+
positional.push(arg)
|
|
67
|
+
i++
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { command, positional, flags }
|
|
72
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import {
|
|
3
|
+
ensureCommandAvailable,
|
|
4
|
+
spawnSyncCollect,
|
|
5
|
+
spawnWithTimeout,
|
|
6
|
+
} from '@side-quest/core/spawn'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Find the git repository root directory (async).
|
|
10
|
+
* @returns The git root path, or null if not in a git repo
|
|
11
|
+
*/
|
|
12
|
+
export async function findGitRoot(): Promise<string | null> {
|
|
13
|
+
const gitCmd = ensureCommandAvailable('git')
|
|
14
|
+
const result = await spawnWithTimeout(
|
|
15
|
+
[gitCmd, 'rev-parse', '--show-toplevel'],
|
|
16
|
+
10_000,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if (result.timedOut || result.exitCode !== 0) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (result.stdout) {
|
|
24
|
+
return result.stdout.trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Find the git repository root directory (synchronous).
|
|
32
|
+
* @returns The git root path, or undefined if not in a git repo
|
|
33
|
+
*/
|
|
34
|
+
export function findGitRootSync(): string | undefined {
|
|
35
|
+
const result = spawnSyncCollect(['git', 'rev-parse', '--show-toplevel'])
|
|
36
|
+
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
37
|
+
return result.stdout.trim()
|
|
38
|
+
}
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the target directory for operations.
|
|
44
|
+
* Uses custom path if provided, falls back to git root, then cwd.
|
|
45
|
+
* @param customPath - Optional custom path to use
|
|
46
|
+
* @returns Resolved target directory path
|
|
47
|
+
*/
|
|
48
|
+
export async function getTargetDir(customPath?: string): Promise<string> {
|
|
49
|
+
if (customPath) {
|
|
50
|
+
return resolve(customPath)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const gitRoot = await findGitRoot()
|
|
54
|
+
if (gitRoot) {
|
|
55
|
+
return gitRoot
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return process.cwd()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Constants for index file management
|
|
63
|
+
*/
|
|
64
|
+
export const INDEX_FILE = 'PROJECT_INDEX.json'
|
|
65
|
+
export const MAX_AGE_HOURS = 24
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROJECT_INDEX.json parser and query utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides TypeScript-based queries to replace jq dependencies.
|
|
5
|
+
* Supports git-style directory search to find index from any subdirectory.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { dirname, join, resolve } from 'node:path'
|
|
9
|
+
import { findUpSync, pathExistsSync } from '@side-quest/core/fs'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Symbol definition from Kit CLI index
|
|
13
|
+
*/
|
|
14
|
+
export interface Symbol {
|
|
15
|
+
name: string
|
|
16
|
+
type: 'function' | 'class' | 'interface' | 'type' | 'constant' | 'variable'
|
|
17
|
+
start_line: number
|
|
18
|
+
end_line: number
|
|
19
|
+
code: string
|
|
20
|
+
file: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* File tree entry
|
|
25
|
+
*/
|
|
26
|
+
export interface FileTreeEntry {
|
|
27
|
+
path: string
|
|
28
|
+
is_dir: boolean
|
|
29
|
+
name: string
|
|
30
|
+
size: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* PROJECT_INDEX.json structure (from Kit CLI)
|
|
35
|
+
*/
|
|
36
|
+
export interface ProjectIndex {
|
|
37
|
+
file_tree: FileTreeEntry[]
|
|
38
|
+
files: FileTreeEntry[]
|
|
39
|
+
symbols: Record<string, Symbol[]>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Search up directory tree for PROJECT_INDEX.json (git-style)
|
|
44
|
+
*
|
|
45
|
+
* Note: Delegates to @sidequest/core/fs findUpSync for the file search.
|
|
46
|
+
*
|
|
47
|
+
* @param startDir - Directory to start searching from (default: process.cwd())
|
|
48
|
+
* @returns Absolute path to PROJECT_INDEX.json
|
|
49
|
+
* @throws Error if index not found
|
|
50
|
+
*/
|
|
51
|
+
export async function findProjectIndex(
|
|
52
|
+
startDir: string = process.cwd(),
|
|
53
|
+
): Promise<string> {
|
|
54
|
+
const indexPath = findUpSync('PROJECT_INDEX.json', startDir)
|
|
55
|
+
|
|
56
|
+
if (indexPath) {
|
|
57
|
+
return indexPath
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error(
|
|
61
|
+
'PROJECT_INDEX.json not found in current directory or any parent directory.\n' +
|
|
62
|
+
'Run: bun run src/cli.ts prime',
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve explicit path to PROJECT_INDEX.json
|
|
68
|
+
*
|
|
69
|
+
* Handles three cases:
|
|
70
|
+
* 1. Path to PROJECT_INDEX.json file directly
|
|
71
|
+
* 2. Path to directory containing PROJECT_INDEX.json
|
|
72
|
+
* 3. Relative path that needs resolution
|
|
73
|
+
*
|
|
74
|
+
* @param explicitPath - Explicit path provided by user
|
|
75
|
+
* @returns Absolute path to PROJECT_INDEX.json
|
|
76
|
+
* @throws Error if index not found at specified path
|
|
77
|
+
*/
|
|
78
|
+
export function resolveExplicitIndexPath(explicitPath: string): string {
|
|
79
|
+
const resolved = resolve(explicitPath)
|
|
80
|
+
|
|
81
|
+
// Case 1: Direct path to PROJECT_INDEX.json
|
|
82
|
+
if (resolved.endsWith('PROJECT_INDEX.json')) {
|
|
83
|
+
if (pathExistsSync(resolved)) {
|
|
84
|
+
return resolved
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`PROJECT_INDEX.json not found at: ${resolved}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Case 2: Directory containing PROJECT_INDEX.json
|
|
90
|
+
const indexInDir = join(resolved, 'PROJECT_INDEX.json')
|
|
91
|
+
if (pathExistsSync(indexInDir)) {
|
|
92
|
+
return indexInDir
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw new Error(
|
|
96
|
+
`PROJECT_INDEX.json not found at: ${indexInDir}\n` +
|
|
97
|
+
'Specify either:\n' +
|
|
98
|
+
' - Path to PROJECT_INDEX.json file\n' +
|
|
99
|
+
' - Directory containing PROJECT_INDEX.json',
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Load and parse PROJECT_INDEX.json
|
|
105
|
+
*
|
|
106
|
+
* @param explicitPath - Optional explicit path to index file or directory
|
|
107
|
+
* @returns Parsed project index
|
|
108
|
+
* @throws Error if index not found or invalid JSON
|
|
109
|
+
*/
|
|
110
|
+
export async function loadProjectIndex(
|
|
111
|
+
explicitPath?: string,
|
|
112
|
+
): Promise<ProjectIndex> {
|
|
113
|
+
const indexPath = explicitPath
|
|
114
|
+
? resolveExplicitIndexPath(explicitPath)
|
|
115
|
+
: await findProjectIndex()
|
|
116
|
+
const file = Bun.file(indexPath)
|
|
117
|
+
return (await file.json()) as ProjectIndex
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Find symbols by name across all files
|
|
122
|
+
*
|
|
123
|
+
* @param index - Project index
|
|
124
|
+
* @param symbolName - Symbol name to search for (exact match)
|
|
125
|
+
* @returns Array of matching symbols with their file paths
|
|
126
|
+
*/
|
|
127
|
+
export function findSymbol(
|
|
128
|
+
index: ProjectIndex,
|
|
129
|
+
symbolName: string,
|
|
130
|
+
): Array<{ file: string; symbol: Symbol }> {
|
|
131
|
+
const results: Array<{ file: string; symbol: Symbol }> = []
|
|
132
|
+
|
|
133
|
+
for (const [file, symbols] of Object.entries(index.symbols)) {
|
|
134
|
+
for (const symbol of symbols) {
|
|
135
|
+
if (symbol.name === symbolName) {
|
|
136
|
+
results.push({ file, symbol })
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return results
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Find symbols by fuzzy name matching (case-insensitive substring)
|
|
146
|
+
*
|
|
147
|
+
* @param index - Project index
|
|
148
|
+
* @param query - Search query (substring, case-insensitive)
|
|
149
|
+
* @returns Array of matching symbols with their file paths
|
|
150
|
+
*/
|
|
151
|
+
export function findSymbolFuzzy(
|
|
152
|
+
index: ProjectIndex,
|
|
153
|
+
query: string,
|
|
154
|
+
): Array<{ file: string; symbol: Symbol; score: number }> {
|
|
155
|
+
const results: Array<{ file: string; symbol: Symbol; score: number }> = []
|
|
156
|
+
const lowerQuery = query.toLowerCase()
|
|
157
|
+
|
|
158
|
+
for (const [file, symbols] of Object.entries(index.symbols)) {
|
|
159
|
+
for (const symbol of symbols) {
|
|
160
|
+
const lowerName = symbol.name.toLowerCase()
|
|
161
|
+
|
|
162
|
+
// Exact match (highest score)
|
|
163
|
+
if (lowerName === lowerQuery) {
|
|
164
|
+
results.push({ file, symbol, score: 100 })
|
|
165
|
+
}
|
|
166
|
+
// Starts with (high score)
|
|
167
|
+
else if (lowerName.startsWith(lowerQuery)) {
|
|
168
|
+
results.push({ file, symbol, score: 75 })
|
|
169
|
+
}
|
|
170
|
+
// Contains (lower score)
|
|
171
|
+
else if (lowerName.includes(lowerQuery)) {
|
|
172
|
+
results.push({ file, symbol, score: 50 })
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Sort by score descending, then by name
|
|
178
|
+
return results.sort((a, b) => {
|
|
179
|
+
if (b.score !== a.score) return b.score - a.score
|
|
180
|
+
return a.symbol.name.localeCompare(b.symbol.name)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get all symbols in a file
|
|
186
|
+
*
|
|
187
|
+
* @param index - Project index
|
|
188
|
+
* @param filePath - File path (can be relative or absolute)
|
|
189
|
+
* @returns Array of symbols in the file
|
|
190
|
+
*/
|
|
191
|
+
export function getFileSymbols(
|
|
192
|
+
index: ProjectIndex,
|
|
193
|
+
filePath: string,
|
|
194
|
+
): Symbol[] {
|
|
195
|
+
// Try exact match first
|
|
196
|
+
if (index.symbols[filePath]) {
|
|
197
|
+
return index.symbols[filePath]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Try finding by filename (if relative path provided)
|
|
201
|
+
for (const [path, symbols] of Object.entries(index.symbols)) {
|
|
202
|
+
if (path.endsWith(filePath)) {
|
|
203
|
+
return symbols
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return []
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get all files with their symbol counts
|
|
212
|
+
*
|
|
213
|
+
* @param index - Project index
|
|
214
|
+
* @returns Array of files with symbol counts
|
|
215
|
+
*/
|
|
216
|
+
export function getFileStats(
|
|
217
|
+
index: ProjectIndex,
|
|
218
|
+
): Array<{ file: string; symbolCount: number }> {
|
|
219
|
+
const stats: Array<{ file: string; symbolCount: number }> = []
|
|
220
|
+
|
|
221
|
+
for (const [file, symbols] of Object.entries(index.symbols)) {
|
|
222
|
+
stats.push({ file, symbolCount: symbols.length })
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Sort by symbol count descending
|
|
226
|
+
return stats.sort((a, b) => b.symbolCount - a.symbolCount)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get symbol distribution by type
|
|
231
|
+
*
|
|
232
|
+
* @param index - Project index
|
|
233
|
+
* @returns Record of symbol types to counts
|
|
234
|
+
*/
|
|
235
|
+
export function getSymbolTypeDistribution(
|
|
236
|
+
index: ProjectIndex,
|
|
237
|
+
): Record<string, number> {
|
|
238
|
+
const distribution: Record<string, number> = {
|
|
239
|
+
function: 0,
|
|
240
|
+
class: 0,
|
|
241
|
+
interface: 0,
|
|
242
|
+
type: 0,
|
|
243
|
+
constant: 0,
|
|
244
|
+
variable: 0,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const symbols of Object.values(index.symbols)) {
|
|
248
|
+
for (const symbol of symbols) {
|
|
249
|
+
distribution[symbol.type] = (distribution[symbol.type] || 0) + 1
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return distribution
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get all exported symbols (heuristic: uppercase or common export patterns)
|
|
258
|
+
*
|
|
259
|
+
* Note: Kit index doesn't have explicit "exported" flag, so we use naming conventions
|
|
260
|
+
*
|
|
261
|
+
* @param index - Project index
|
|
262
|
+
* @returns Array of likely exported symbols
|
|
263
|
+
*/
|
|
264
|
+
export function getExportedSymbols(
|
|
265
|
+
index: ProjectIndex,
|
|
266
|
+
): Array<{ file: string; symbol: Symbol }> {
|
|
267
|
+
const exported: Array<{ file: string; symbol: Symbol }> = []
|
|
268
|
+
|
|
269
|
+
for (const [file, symbols] of Object.entries(index.symbols)) {
|
|
270
|
+
// Skip test files
|
|
271
|
+
if (file.includes('.test.') || file.includes('.spec.')) {
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const symbol of symbols) {
|
|
276
|
+
// Heuristic: likely exported if:
|
|
277
|
+
// 1. Starts with uppercase (classes, types, interfaces)
|
|
278
|
+
// 2. Exported by naming convention
|
|
279
|
+
const firstChar = symbol.name[0]
|
|
280
|
+
if (firstChar && firstChar === firstChar.toUpperCase()) {
|
|
281
|
+
exported.push({ file, symbol })
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return exported
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Group symbols by directory
|
|
291
|
+
*
|
|
292
|
+
* @param index - Project index
|
|
293
|
+
* @returns Record of directory paths to symbol counts
|
|
294
|
+
*/
|
|
295
|
+
export function groupSymbolsByDirectory(
|
|
296
|
+
index: ProjectIndex,
|
|
297
|
+
): Record<string, number> {
|
|
298
|
+
const dirCounts: Record<string, number> = {}
|
|
299
|
+
|
|
300
|
+
for (const [file, symbols] of Object.entries(index.symbols)) {
|
|
301
|
+
const dir = dirname(file)
|
|
302
|
+
dirCounts[dir] = (dirCounts[dir] || 0) + symbols.length
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return dirCounts
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get directory with most symbols (complexity hotspot)
|
|
310
|
+
*
|
|
311
|
+
* @param index - Project index
|
|
312
|
+
* @param topN - Number of top directories to return
|
|
313
|
+
* @returns Array of directories sorted by symbol count
|
|
314
|
+
*/
|
|
315
|
+
export function getComplexityHotspots(
|
|
316
|
+
index: ProjectIndex,
|
|
317
|
+
topN = 10,
|
|
318
|
+
): Array<{ directory: string; symbolCount: number }> {
|
|
319
|
+
const dirCounts = groupSymbolsByDirectory(index)
|
|
320
|
+
|
|
321
|
+
const sorted = Object.entries(dirCounts)
|
|
322
|
+
.map(([directory, symbolCount]) => ({ directory, symbolCount }))
|
|
323
|
+
.sort((a, b) => b.symbolCount - a.symbolCount)
|
|
324
|
+
|
|
325
|
+
return sorted.slice(0, topN)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get all symbols of a specific type
|
|
330
|
+
*
|
|
331
|
+
* @param index - Project index
|
|
332
|
+
* @param type - Symbol type to filter by
|
|
333
|
+
* @returns Array of matching symbols with file paths
|
|
334
|
+
*/
|
|
335
|
+
export function getSymbolsByType(
|
|
336
|
+
index: ProjectIndex,
|
|
337
|
+
type: Symbol['type'],
|
|
338
|
+
): Array<{ file: string; symbol: Symbol }> {
|
|
339
|
+
const results: Array<{ file: string; symbol: Symbol }> = []
|
|
340
|
+
|
|
341
|
+
for (const [file, symbols] of Object.entries(index.symbols)) {
|
|
342
|
+
for (const symbol of symbols) {
|
|
343
|
+
if (symbol.type === type) {
|
|
344
|
+
results.push({ file, symbol })
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return results
|
|
350
|
+
}
|