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