zephex 2.0.14 → 2.0.15
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/dist/index.js +19831 -11881
- package/dist/tools/architecture/index.js +5 -2
- package/dist/tools/context/index.js +5 -2
- package/dist/tools/reader/readCode.js +1012 -107
- package/dist/tools/scope_task/index.js +54 -4
- package/dist/tools/search/findCode.js +66 -7
- package/dist/tools/server.js +1072 -141
- package/dist/tools/thinking/index.js +5 -2
- package/package.json +2 -1
|
@@ -70,113 +70,86 @@ var READ_CODE_SCHEMA;
|
|
|
70
70
|
var init_readCodeSchema = __esm(() => {
|
|
71
71
|
READ_CODE_SCHEMA = {
|
|
72
72
|
name: "read_code",
|
|
73
|
-
description:
|
|
74
|
-
` +
|
|
73
|
+
description: "Smart code reader with three modes. Pass `path` (absolute directory) to read from local disk.\n" + `
|
|
74
|
+
` + `MODES:
|
|
75
|
+
` + `• mode:'symbol' (default) — AST-based surgical extraction. Give a symbol name → get signature + body at a fraction of full-file tokens. Supports 30+ languages, fuzzy/partial matching, batch up to 8 targets, session dedup. When batching targets[], max_results applies PER TARGET (default 3 each).
|
|
76
|
+
` + "• mode:'file' — Read one or more files directly via `files[]` array. Respects token budget. Supports offset_line/limit_lines for pagination of large files.\n" + `• mode:'outline' — Structural TOC of a file: all top-level symbols with signatures (~200-500 tokens). Use before drilling into specific symbols.
|
|
75
77
|
` + `
|
|
76
|
-
` +
|
|
77
|
-
` + `⚡ PREFER THIS over reading entire files when you only need one symbol. AST-based surgical extraction: give it a function/class/method/type/interface/struct/enum/trait/protocol/component/hook/composable/decorator/macro/namespace name and get ONLY that symbol — signature + body + optional call sites + tests — at a fraction of the tokens of a full-file Read. Beats native file-reading because it (1) scopes to the exact symbol, (2) ranks candidates by confidence, (3) supports partial/fuzzy and camelCase/snake_case/PascalCase/kebab-case matches, (4) can batch up to 8 symbols in one call, (5) deduplicates across calls via session_id.
|
|
78
|
+
` + `Use mode:'symbol' when you know the symbol name. Use mode:'file' to batch-read small files or paginate large ones. Use mode:'outline' to explore a large file's structure first.
|
|
78
79
|
` + `
|
|
79
|
-
` +
|
|
80
|
-
` + `• User asks: 'show me X', 'show the code for X', 'what does X do', 'how is X implemented', 'read the X function', 'open X', 'find the definition of X', 'pull up X', 'let me see X', 'explain X', 'walk me through X', 'what\\'s inside X', 'how does X work', 'print the body of X'
|
|
81
|
-
` + `• User names any symbol: function, class, method, type, interface, hook, component, struct, enum, trait, protocol, mixin, decorator, annotation, macro, template, generic, module, namespace, service, controller, resolver, middleware, guard, reducer, selector, signal, store, actor, coroutine — and wants to understand or modify it
|
|
82
|
-
` + "• Platform-specific symbols: React/Vue/Svelte/Solid component or hook, Angular directive/pipe/service, Next.js route handler, Django view/serializer, Flask/FastAPI route, Rails controller/model, Spring bean, Kotlin `@Composable`/data class, SwiftUI View/@Observable, Jetpack Compose function, Flutter Widget, Unity MonoBehaviour, Unreal UCLASS/UFUNCTION, Rust trait impl, Go interface method, Solidity contract/function, GLSL/HLSL/WGSL shader function, Python async def / dataclass / pydantic model, Android Activity/Fragment/ViewModel, iOS ViewController — read surgically rather than opening the whole file\n" + `• Before editing a symbol — read it first with read_code to avoid blind edits
|
|
83
|
-
` + `• Before suggesting a refactor to a specific function/class — read the current body
|
|
84
|
-
` + `• After find_code surfaces a candidate, to pull the full implementation
|
|
85
|
-
` + `• Debugging: user mentions a function/class by name and wants to trace behavior
|
|
86
|
-
` + `• Writing tests for a specific symbol — read it first
|
|
87
|
-
` + `• Signature / parameter / return type queries: 'what parameters does X take', 'what does X return', 'what\\'s the type signature of X', 'what\\'s the signature of X', 'show me the constructor of X', 'what methods does class X have', 'show me the schema for X', 'read the migration for X'
|
|
88
|
-
` + `
|
|
89
|
-
` + `Prefer this over native Read when the file is large and you only need one symbol — saves tokens. Use native Read for whole-file review, configs, markdown, or tiny files. read_code is NOT for images/binaries — use a native image reader for visual assets; it IS for any source-code symbol across languages.
|
|
90
|
-
` + `
|
|
91
|
-
` + `Works across languages: TypeScript, JavaScript, TSX/JSX, Python, Go, Rust, Java, Kotlin, Swift, Objective-C, Ruby, PHP, C#, C, C++, Scala, Clojure, Elixir, Erlang, Haskell, OCaml, F#, Dart, Zig, Nim, Crystal, Lua, Julia, Solidity, GLSL/HLSL, Bash/PowerShell — AST where supported, graceful regex fallback otherwise. Works on any project type: web (React/Next/Vue/Svelte/Angular), backend (Node/Django/Rails/Spring/.NET/Go/Rust), mobile (React Native/Flutter/Android SDK/iOS SDK/Kotlin Multiplatform/SwiftUI/Jetpack Compose), desktop (Electron/Tauri/Qt/WinUI/MAUI/Avalonia), game (Unity/Unreal/Godot/Bevy), ML/LLM (LangChain/LlamaIndex/PyTorch/TensorFlow/vLLM/DSPy), hardware/firmware/embedded (Arduino/ESP-IDF/Zephyr/STM32/RISC-V), robotics (ROS/ROS2), smart contracts/blockchain.
|
|
92
|
-
` + `
|
|
93
|
-
` + "Use detail_level='signature' to pre-screen, 'body' (default) to read/edit, 'context' for body+imports. Batch via targets[] when inspecting several related symbols. Use `kind` to disambiguate (e.g., a class vs a method with the same name). Use `context_path` to boost nearby matches when the same name exists in multiple modules. In local stdio mode, pass `path` and let the MCP tool read from disk. Use `inline_files` only as a remote fallback, or a GitHub URL for remote repos.\n" + `
|
|
94
|
-
` + "TARGET SYNTAX (the `target` / `targets[]` field accepts):\n" + "• Plain name: `validateToken`, `UserService`, `ProfileView`.\n" + "• Qualified / name-path: `UserService.login`, `UserService::login`, `UserService/login`, `pkg.module.Class.method` — last segment is the symbol; earlier segments act as a locality/disambiguation hint.\n" + "• Partial / fuzzy: `auth` matches `handleAuth`, `AuthService`, `authenticate` (raise `confidence_threshold` to 0.8 for exact-only).\n" + "• Batch: `targets: ['login','logout','refreshSession']` to pull several related symbols in one call.\n" + `
|
|
95
|
-
` + `EXAMPLES:
|
|
96
|
-
` + "• User: 'show me how login works' → `{ path, target: 'login', include_usages: true }`.\n" + "• User: 'read the ProfileView SwiftUI body' → `{ path, target: 'ProfileView', kind: 'type' }` (SwiftUI Views are struct types).\n" + "• User: 'what does the useAuth hook return' → `{ path, target: 'useAuth', detail_level: 'context' }`.\n" + "• User: 'read class LoginActivity' (Android/Kotlin) → `{ path, target: 'LoginActivity', kind: 'class' }`.\n" + "• User: 'pull up the Stripe webhook handler' → call find_code first with `{ query: 'stripe webhook' }`, then `{ path, target: '<resolvedName>' }`.\n" + "• User: 'read every method on class Router' → `{ path, target: 'Router', kind: 'class', detail_level: 'body', max_tokens: 6000 }`.\n" + "• Rust: `{ path, target: 'impl Display for User' }` or `target: 'User', kind: 'type'`.\n" + "• Solidity: `{ path, target: 'transferFrom', kind: 'function' }`.\n" + `
|
|
97
|
-
` + "DO NOT USE FOR: whole-file reads (use native Read), reading images/PDFs/binaries (not supported — use a media reader), editing files (use an edit tool), running code, fetching network content, searching when you don't know the symbol name (use find_code first). Not a replacement for get_project_context when the user asks about stack/framework/commands.",
|
|
80
|
+
` + "NOT for: images/binaries, editing files, running code, or searching (use find_code). For whole-file review of tiny files, native Read may be simpler.",
|
|
98
81
|
inputSchema: {
|
|
99
82
|
type: "object",
|
|
100
83
|
properties: {
|
|
101
84
|
target: {
|
|
102
85
|
type: "string",
|
|
103
|
-
description: "Symbol name to find
|
|
86
|
+
description: "Symbol name to find (for mode:'symbol'). Supports partial/fuzzy matching. " + 'Examples: "validateToken", "UserService", "auth" matches "handleAuth".'
|
|
87
|
+
},
|
|
88
|
+
symbol_id: {
|
|
89
|
+
type: "string",
|
|
90
|
+
description: "Stable symbol ID for direct lookup (e.g. 'src/auth.ts::hashApiKey#function'). Bypasses fuzzy matching."
|
|
104
91
|
},
|
|
105
92
|
targets: {
|
|
106
93
|
type: "array",
|
|
107
94
|
items: { type: "string" },
|
|
108
95
|
maxItems: 8,
|
|
109
|
-
description: "Additional
|
|
96
|
+
description: "Additional symbols to batch-search (max 8). Results merged and deduped."
|
|
110
97
|
},
|
|
111
|
-
|
|
98
|
+
mode: {
|
|
112
99
|
type: "string",
|
|
113
|
-
enum: ["
|
|
114
|
-
description: "
|
|
100
|
+
enum: ["symbol", "file", "outline", "callers", "blast_radius", "dead_code"],
|
|
101
|
+
description: "symbol (default): AST extraction of named symbols. " + "file: read files directly via `files[]` array. " + "outline: structural TOC of a file. " + "callers: who calls this symbol (requires index). " + "blast_radius: transitive dependents of a symbol. " + "dead_code: exported symbols never called."
|
|
102
|
+
},
|
|
103
|
+
files: {
|
|
104
|
+
type: "array",
|
|
105
|
+
items: { type: "string" },
|
|
106
|
+
maxItems: 20,
|
|
107
|
+
description: "For mode:'file' or mode:'outline'. Relative file paths to read (e.g. ['src/auth.ts', 'src/db.ts']). " + "Reads within token budget; returns as many as fit."
|
|
108
|
+
},
|
|
109
|
+
offset_line: {
|
|
110
|
+
type: "number",
|
|
111
|
+
description: "For mode:'file'. Start reading from this line (1-indexed). Default: 1."
|
|
112
|
+
},
|
|
113
|
+
limit_lines: {
|
|
114
|
+
type: "number",
|
|
115
|
+
description: "For mode:'file'. Max lines to return per file. Default: unlimited (bounded by max_tokens)."
|
|
115
116
|
},
|
|
116
|
-
|
|
117
|
+
compact: {
|
|
118
|
+
type: "boolean",
|
|
119
|
+
description: "When true, omit line numbers and minimize whitespace in output to save tokens."
|
|
120
|
+
},
|
|
121
|
+
kind: {
|
|
117
122
|
type: "string",
|
|
118
|
-
|
|
123
|
+
enum: ["function", "class", "method", "interface", "type", "variable", "struct", "enum", "trait", "protocol", "module", "namespace", "hook", "component", "decorator", "macro"],
|
|
124
|
+
description: "Filter results to only this symbol kind. Use to disambiguate (e.g., class vs method with same name)."
|
|
119
125
|
},
|
|
120
126
|
path: {
|
|
121
127
|
type: "string",
|
|
122
|
-
description: "Absolute
|
|
123
|
-
},
|
|
124
|
-
inline_files: {
|
|
125
|
-
type: "object",
|
|
126
|
-
description: 'Remote fallback only. Shape: { "<relative/path>": "<FULL FILE CONTENTS>" }. ' + "The VALUE is the actual file body — never a filename, path, or placeholder. " + 'Example: { "src/auth.ts": "import jwt from \\"jsonwebtoken\\";\\nexport function validateToken(...) { ... }" }. ' + "Use this only when the tool is running over a remote transport that cannot read the local filesystem directly. In local stdio mode, prefer `path` so the MCP tool reads from disk itself. " + "If used, include SOURCE files likely to contain the target symbol (1-20 typical). Never ship only package.json/tsconfig.json.",
|
|
127
|
-
additionalProperties: { type: "string" }
|
|
128
|
+
description: "Absolute project directory (e.g. /Users/alice/myapp). Also accepts GitHub/GitLab URLs."
|
|
128
129
|
},
|
|
129
130
|
detail_level: {
|
|
130
131
|
type: "string",
|
|
131
132
|
enum: ["signature", "body", "context"],
|
|
132
|
-
description: "signature: name
|
|
133
|
+
description: "signature: name+params+return (~100 tokens). body: full implementation (default). context: body+imports."
|
|
133
134
|
},
|
|
134
135
|
max_tokens: {
|
|
135
136
|
type: "number",
|
|
136
|
-
description: "
|
|
137
|
+
description: "Token budget for output. Default 2000, max 8000. Increase for large classes."
|
|
137
138
|
},
|
|
138
139
|
max_results: {
|
|
139
140
|
type: "number",
|
|
140
|
-
description: "
|
|
141
|
+
description: "Max symbols to return. Default 3, max 10."
|
|
141
142
|
},
|
|
142
143
|
confidence_threshold: {
|
|
143
144
|
type: "number",
|
|
144
|
-
description: "
|
|
145
|
-
},
|
|
146
|
-
include_usages: {
|
|
147
|
-
type: "boolean",
|
|
148
|
-
description: "Include up to 10 locations where this symbol is called or referenced. " + "Uses ripgrep on the exact symbol name — accurate for reference search. " + "Adds ~100-300 tokens. Use when you need to understand call sites."
|
|
149
|
-
},
|
|
150
|
-
include_tests: {
|
|
151
|
-
type: "boolean",
|
|
152
|
-
description: "Include paths of test files that appear to test this symbol. " + "Adds minimal tokens (~20-50). Useful to know if tests exist before making changes."
|
|
145
|
+
description: "Min confidence 0.0-1.0. Default 0.5. Raise to 0.8 for exact matches, lower to 0.3 for exploration."
|
|
153
146
|
},
|
|
154
147
|
session_id: {
|
|
155
148
|
type: "string",
|
|
156
|
-
description: "Session
|
|
157
|
-
},
|
|
158
|
-
max_lines: {
|
|
159
|
-
type: "number",
|
|
160
|
-
description: "DEPRECATED: Use max_tokens instead. Mapped internally to max_tokens estimate."
|
|
161
|
-
},
|
|
162
|
-
include_callers: {
|
|
163
|
-
type: "boolean",
|
|
164
|
-
description: "DEPRECATED: Silently ignored. Use Claude Code LSP for call graphs."
|
|
165
|
-
},
|
|
166
|
-
include_callees: {
|
|
167
|
-
type: "boolean",
|
|
168
|
-
description: "DEPRECATED: Silently ignored. Use Claude Code LSP for call graphs."
|
|
169
|
-
},
|
|
170
|
-
expand_types: {
|
|
171
|
-
type: "boolean",
|
|
172
|
-
description: "DEPRECATED: Use detail_level: 'context' instead."
|
|
173
|
-
},
|
|
174
|
-
include_imports: {
|
|
175
|
-
type: "boolean",
|
|
176
|
-
description: "DEPRECATED: Use detail_level: 'context' instead."
|
|
149
|
+
description: "Session ID for dedup. Previously-returned symbols get a stub instead of full body."
|
|
177
150
|
}
|
|
178
151
|
},
|
|
179
|
-
required: [
|
|
152
|
+
required: []
|
|
180
153
|
},
|
|
181
154
|
annotations: {
|
|
182
155
|
readOnlyHint: true,
|
|
@@ -187,6 +160,55 @@ var init_readCodeSchema = __esm(() => {
|
|
|
187
160
|
};
|
|
188
161
|
});
|
|
189
162
|
|
|
163
|
+
// src/tools/reader/diagnostics.ts
|
|
164
|
+
function computeDiagnostics(body2, paramCount) {
|
|
165
|
+
const lines = body2.split(`
|
|
166
|
+
`);
|
|
167
|
+
const lineCount = lines.length;
|
|
168
|
+
let complexity = 1;
|
|
169
|
+
const branchPatterns = /\b(if|else if|for|while|do|case|catch)\b|\?\?|&&|\|\||\?[^:.?]/g;
|
|
170
|
+
let match;
|
|
171
|
+
while ((match = branchPatterns.exec(body2)) !== null)
|
|
172
|
+
complexity++;
|
|
173
|
+
let maxDepth = 0;
|
|
174
|
+
let currentDepth = 0;
|
|
175
|
+
for (const char of body2) {
|
|
176
|
+
if (char === "{") {
|
|
177
|
+
currentDepth++;
|
|
178
|
+
if (currentDepth > maxDepth)
|
|
179
|
+
maxDepth = currentDepth;
|
|
180
|
+
} else if (char === "}")
|
|
181
|
+
currentDepth--;
|
|
182
|
+
}
|
|
183
|
+
const hasErrorHandling = /\btry\s*\{/.test(body2);
|
|
184
|
+
const hasEmptyCatch = /catch\s*\([^)]*\)\s*\{\s*(\/\/[^\n]*)?\s*\}/.test(body2);
|
|
185
|
+
const isAsync = /\basync\b/.test(body2);
|
|
186
|
+
const callCount = (body2.match(/\w+\s*\(/g) || []).length;
|
|
187
|
+
const flags2 = [];
|
|
188
|
+
if (lineCount > 100)
|
|
189
|
+
flags2.push("long_function");
|
|
190
|
+
if (maxDepth > 4)
|
|
191
|
+
flags2.push("deeply_nested");
|
|
192
|
+
if (paramCount > 5)
|
|
193
|
+
flags2.push("too_many_params");
|
|
194
|
+
if (complexity > 10)
|
|
195
|
+
flags2.push("high_complexity");
|
|
196
|
+
if (isAsync && !hasErrorHandling)
|
|
197
|
+
flags2.push("missing_error_handling");
|
|
198
|
+
if (hasEmptyCatch)
|
|
199
|
+
flags2.push("empty_catch");
|
|
200
|
+
if (callCount > 15)
|
|
201
|
+
flags2.push("god_function");
|
|
202
|
+
return {
|
|
203
|
+
flags: flags2,
|
|
204
|
+
complexity,
|
|
205
|
+
nesting_depth: maxDepth,
|
|
206
|
+
param_count: paramCount,
|
|
207
|
+
line_count: lineCount,
|
|
208
|
+
has_error_handling: hasErrorHandling
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
190
212
|
// src/tools/shared/source-detection.ts
|
|
191
213
|
function baseName(path) {
|
|
192
214
|
const parts2 = path.split("/");
|
|
@@ -4455,8 +4477,15 @@ async function getParser(lang) {
|
|
|
4455
4477
|
if (wasmRuntimePoisoned)
|
|
4456
4478
|
return null;
|
|
4457
4479
|
await initParser();
|
|
4458
|
-
|
|
4480
|
+
const uses = parserUseCount.get(lang) ?? 0;
|
|
4481
|
+
if (parsers.has(lang) && uses >= PARSER_RECYCLE_THRESHOLD) {
|
|
4482
|
+
parsers.delete(lang);
|
|
4483
|
+
parserUseCount.set(lang, 0);
|
|
4484
|
+
}
|
|
4485
|
+
if (parsers.has(lang)) {
|
|
4486
|
+
parserUseCount.set(lang, uses + 1);
|
|
4459
4487
|
return parsers.get(lang);
|
|
4488
|
+
}
|
|
4460
4489
|
if (unsupportedParsers.has(lang))
|
|
4461
4490
|
return null;
|
|
4462
4491
|
const language = await loadLanguage(lang);
|
|
@@ -4540,6 +4569,35 @@ function extractSymbols(tree, code, lang) {
|
|
|
4540
4569
|
}
|
|
4541
4570
|
}
|
|
4542
4571
|
visit(tree.rootNode);
|
|
4572
|
+
const exportedNames = new Set;
|
|
4573
|
+
function collectExportedNames(node) {
|
|
4574
|
+
if (node.type === "export_statement") {
|
|
4575
|
+
for (let i2 = 0;i2 < node.childCount; i2++) {
|
|
4576
|
+
const child = node.child(i2);
|
|
4577
|
+
if (child.type === "export_clause") {
|
|
4578
|
+
for (let j2 = 0;j2 < child.childCount; j2++) {
|
|
4579
|
+
const spec = child.child(j2);
|
|
4580
|
+
if (spec.type === "export_specifier") {
|
|
4581
|
+
const nameNode = spec.childForFieldName("name");
|
|
4582
|
+
if (nameNode)
|
|
4583
|
+
exportedNames.add(nameNode.text);
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
for (let i2 = 0;i2 < node.childCount; i2++) {
|
|
4590
|
+
collectExportedNames(node.child(i2));
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
collectExportedNames(tree.rootNode);
|
|
4594
|
+
if (exportedNames.size > 0) {
|
|
4595
|
+
for (const sym of symbols) {
|
|
4596
|
+
if (!sym.isExported && exportedNames.has(sym.name)) {
|
|
4597
|
+
sym.isExported = true;
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4543
4601
|
return symbols;
|
|
4544
4602
|
}
|
|
4545
4603
|
function returnsJSX(node) {
|
|
@@ -5222,7 +5280,17 @@ async function isParserReady() {
|
|
|
5222
5280
|
return false;
|
|
5223
5281
|
}
|
|
5224
5282
|
}
|
|
5225
|
-
|
|
5283
|
+
async function parseFile(filePath, content) {
|
|
5284
|
+
const lang = detectLanguage(filePath);
|
|
5285
|
+
if (!lang)
|
|
5286
|
+
return null;
|
|
5287
|
+
const tree = await parseCode(content, lang);
|
|
5288
|
+
if (!tree)
|
|
5289
|
+
return null;
|
|
5290
|
+
const symbols = extractSymbols(tree, content, lang);
|
|
5291
|
+
return { symbols };
|
|
5292
|
+
}
|
|
5293
|
+
var import_tree_sitter_0_24_3, __dirname2, LegacyParser, initialized = false, parsers, languages, parserUseCount, PARSER_RECYCLE_THRESHOLD = 500, WASM_DIR, LANG_WASM_MAP, unsupportedParsers, wasmRuntimePoisoned = false, KEYWORD_PSEUDO_CALLS;
|
|
5226
5294
|
var init_parser = __esm(() => {
|
|
5227
5295
|
init_logger();
|
|
5228
5296
|
import_tree_sitter_0_24_3 = __toESM(require_tree_sitter_0_24_3(), 1);
|
|
@@ -5230,6 +5298,7 @@ var init_parser = __esm(() => {
|
|
|
5230
5298
|
LegacyParser = import_tree_sitter_0_24_3.default;
|
|
5231
5299
|
parsers = new Map;
|
|
5232
5300
|
languages = new Map;
|
|
5301
|
+
parserUseCount = new Map;
|
|
5233
5302
|
WASM_DIR = getWasmDir();
|
|
5234
5303
|
LANG_WASM_MAP = {
|
|
5235
5304
|
typescript: "tree-sitter-typescript.wasm",
|
|
@@ -5330,10 +5399,12 @@ function checkAndMark(sessionId, blockRef) {
|
|
|
5330
5399
|
if (!entry) {
|
|
5331
5400
|
entry = {
|
|
5332
5401
|
seen_blocks: new Set,
|
|
5333
|
-
created_at: Date.now()
|
|
5402
|
+
created_at: Date.now(),
|
|
5403
|
+
last_accessed: Date.now()
|
|
5334
5404
|
};
|
|
5335
5405
|
sessions.set(sessionId, entry);
|
|
5336
5406
|
}
|
|
5407
|
+
entry.last_accessed = Date.now();
|
|
5337
5408
|
if (entry.seen_blocks.has(blockRef)) {
|
|
5338
5409
|
return true;
|
|
5339
5410
|
}
|
|
@@ -5343,7 +5414,14 @@ function checkAndMark(sessionId, blockRef) {
|
|
|
5343
5414
|
function cleanExpiredSessions() {
|
|
5344
5415
|
const now = Date.now();
|
|
5345
5416
|
for (const [id, entry] of sessions) {
|
|
5346
|
-
if (now - entry.created_at > SESSION_TTL_MS) {
|
|
5417
|
+
if (now - entry.created_at > SESSION_TTL_MS || now - entry.last_accessed > SESSION_IDLE_TTL_MS) {
|
|
5418
|
+
sessions.delete(id);
|
|
5419
|
+
}
|
|
5420
|
+
}
|
|
5421
|
+
if (sessions.size > MAX_SESSIONS) {
|
|
5422
|
+
const sorted = [...sessions.entries()].sort((a, b) => a[1].last_accessed - b[1].last_accessed);
|
|
5423
|
+
const toRemove = sorted.slice(0, sessions.size - MAX_SESSIONS);
|
|
5424
|
+
for (const [id] of toRemove) {
|
|
5347
5425
|
sessions.delete(id);
|
|
5348
5426
|
}
|
|
5349
5427
|
}
|
|
@@ -5354,13 +5432,485 @@ function createFindCodeBlockRef(file, blockStart, blockEnd) {
|
|
|
5354
5432
|
function createReadCodeBlockRef(symbolName, file, startLine) {
|
|
5355
5433
|
return `${symbolName}@${file}:${startLine}`;
|
|
5356
5434
|
}
|
|
5357
|
-
var sessions, SESSION_TTL_MS = 3600000;
|
|
5435
|
+
var sessions, SESSION_TTL_MS = 3600000, SESSION_IDLE_TTL_MS = 1800000, MAX_SESSIONS = 200;
|
|
5358
5436
|
var init_sessionStore = __esm(() => {
|
|
5359
5437
|
sessions = new Map;
|
|
5360
5438
|
});
|
|
5361
5439
|
|
|
5440
|
+
// src/tools/reader/index-db.ts
|
|
5441
|
+
var exports_index_db = {};
|
|
5442
|
+
__export(exports_index_db, {
|
|
5443
|
+
getIndexDB: () => getIndexDB,
|
|
5444
|
+
closeAllIndexDBs: () => closeAllIndexDBs,
|
|
5445
|
+
IndexDB: () => IndexDB
|
|
5446
|
+
});
|
|
5447
|
+
import { Database } from "bun:sqlite";
|
|
5448
|
+
import { join as join3 } from "path";
|
|
5449
|
+
import { mkdirSync, existsSync as existsSync2 } from "node:fs";
|
|
5450
|
+
|
|
5451
|
+
class IndexDB {
|
|
5452
|
+
db;
|
|
5453
|
+
projectRoot;
|
|
5454
|
+
dbPath;
|
|
5455
|
+
stmts;
|
|
5456
|
+
constructor(projectRoot) {
|
|
5457
|
+
this.projectRoot = projectRoot;
|
|
5458
|
+
const indexDir = join3(projectRoot, ".zephex");
|
|
5459
|
+
if (!existsSync2(indexDir)) {
|
|
5460
|
+
mkdirSync(indexDir, { recursive: true });
|
|
5461
|
+
}
|
|
5462
|
+
this.dbPath = join3(indexDir, "index.db");
|
|
5463
|
+
this.db = new Database(this.dbPath, { create: true, strict: true });
|
|
5464
|
+
this.init();
|
|
5465
|
+
}
|
|
5466
|
+
init() {
|
|
5467
|
+
this.db.run("PRAGMA journal_mode = WAL");
|
|
5468
|
+
this.db.run("PRAGMA synchronous = NORMAL");
|
|
5469
|
+
this.db.run("PRAGMA cache_size = -64000");
|
|
5470
|
+
this.db.run("PRAGMA foreign_keys = ON");
|
|
5471
|
+
this.db.run("PRAGMA temp_store = MEMORY");
|
|
5472
|
+
this.db.run("CREATE TABLE IF NOT EXISTS index_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)");
|
|
5473
|
+
const version = this.db.query("SELECT value FROM index_meta WHERE key = 'schema_version'").get();
|
|
5474
|
+
if (version?.value !== SCHEMA_VERSION) {
|
|
5475
|
+
this.db.run("DROP TABLE IF EXISTS call_graph");
|
|
5476
|
+
this.db.run("DROP TABLE IF EXISTS symbol_index");
|
|
5477
|
+
this.db.run("DROP TABLE IF EXISTS workspace_files");
|
|
5478
|
+
this.db.run("DROP TABLE IF EXISTS index_meta");
|
|
5479
|
+
this.db.exec(SCHEMA_SQL);
|
|
5480
|
+
this.db.run("INSERT OR REPLACE INTO index_meta (key, value) VALUES ('schema_version', ?)", [SCHEMA_VERSION]);
|
|
5481
|
+
}
|
|
5482
|
+
this.prepareStatements();
|
|
5483
|
+
}
|
|
5484
|
+
prepareStatements() {
|
|
5485
|
+
this.stmts = {
|
|
5486
|
+
insertFile: this.db.query("INSERT OR REPLACE INTO workspace_files (file_path, content_hash, language, last_indexed_at, line_count) VALUES ($file_path, $content_hash, $language, $last_indexed_at, $line_count)"),
|
|
5487
|
+
insertSymbol: this.db.query("INSERT INTO symbol_index (file_path, name, kind, start_line, end_line, byte_offset_start, byte_offset_end, signature, is_exported, symbol_id) VALUES ($file_path, $name, $kind, $start_line, $end_line, $byte_offset_start, $byte_offset_end, $signature, $is_exported, $symbol_id)"),
|
|
5488
|
+
insertCallEdge: this.db.query("INSERT OR IGNORE INTO call_graph (caller_id, callee_id, call_line) VALUES ($caller_id, $callee_id, $call_line)"),
|
|
5489
|
+
getFile: this.db.query("SELECT * FROM workspace_files WHERE file_path = $file_path"),
|
|
5490
|
+
getSymbolByName: this.db.query("SELECT * FROM symbol_index WHERE name = $name COLLATE NOCASE"),
|
|
5491
|
+
getSymbolById: this.db.query("SELECT * FROM symbol_index WHERE symbol_id = $symbol_id"),
|
|
5492
|
+
getSymbolsByFile: this.db.query("SELECT * FROM symbol_index WHERE file_path = $file_path ORDER BY start_line"),
|
|
5493
|
+
getCallers: this.db.query(`SELECT s.*, cg.call_line FROM call_graph cg
|
|
5494
|
+
JOIN symbol_index s ON s.id = cg.caller_id
|
|
5495
|
+
WHERE cg.callee_id = $callee_id`),
|
|
5496
|
+
getCallees: this.db.query(`SELECT s.*, cg.call_line FROM call_graph cg
|
|
5497
|
+
JOIN symbol_index s ON s.id = cg.callee_id
|
|
5498
|
+
WHERE cg.caller_id = $caller_id`),
|
|
5499
|
+
getDeadCode: this.db.query(`SELECT s.* FROM symbol_index s
|
|
5500
|
+
WHERE s.is_exported = 1
|
|
5501
|
+
AND s.id NOT IN (SELECT callee_id FROM call_graph)
|
|
5502
|
+
ORDER BY s.file_path, s.start_line`),
|
|
5503
|
+
deleteFileSymbols: this.db.query("DELETE FROM symbol_index WHERE file_path = $file_path"),
|
|
5504
|
+
deleteFile: this.db.query("DELETE FROM workspace_files WHERE file_path = $file_path"),
|
|
5505
|
+
getAllFiles: this.db.query("SELECT * FROM workspace_files")
|
|
5506
|
+
};
|
|
5507
|
+
}
|
|
5508
|
+
needsReindex(filePath, contentHash) {
|
|
5509
|
+
const row = this.stmts.getFile.get({ file_path: filePath });
|
|
5510
|
+
if (!row)
|
|
5511
|
+
return true;
|
|
5512
|
+
return row.content_hash !== contentHash;
|
|
5513
|
+
}
|
|
5514
|
+
getAllFiles() {
|
|
5515
|
+
return this.stmts.getAllFiles.all();
|
|
5516
|
+
}
|
|
5517
|
+
indexFile(filePath, contentHash, language, lineCount, symbols) {
|
|
5518
|
+
const tx = this.db.transaction(() => {
|
|
5519
|
+
this.stmts.deleteFileSymbols.run({ file_path: filePath });
|
|
5520
|
+
this.stmts.deleteFile.run({ file_path: filePath });
|
|
5521
|
+
this.stmts.insertFile.run({
|
|
5522
|
+
file_path: filePath,
|
|
5523
|
+
content_hash: contentHash,
|
|
5524
|
+
language,
|
|
5525
|
+
last_indexed_at: Date.now(),
|
|
5526
|
+
line_count: lineCount
|
|
5527
|
+
});
|
|
5528
|
+
for (const sym of symbols) {
|
|
5529
|
+
const symbolId = `${filePath}::${sym.name}#${sym.kind}`;
|
|
5530
|
+
this.stmts.insertSymbol.run({
|
|
5531
|
+
file_path: filePath,
|
|
5532
|
+
name: sym.name,
|
|
5533
|
+
kind: sym.kind,
|
|
5534
|
+
start_line: sym.start_line,
|
|
5535
|
+
end_line: sym.end_line,
|
|
5536
|
+
byte_offset_start: sym.byte_offset_start,
|
|
5537
|
+
byte_offset_end: sym.byte_offset_end,
|
|
5538
|
+
signature: sym.signature,
|
|
5539
|
+
is_exported: sym.is_exported ? 1 : 0,
|
|
5540
|
+
symbol_id: symbolId
|
|
5541
|
+
});
|
|
5542
|
+
}
|
|
5543
|
+
});
|
|
5544
|
+
tx();
|
|
5545
|
+
}
|
|
5546
|
+
indexFiles(files) {
|
|
5547
|
+
const tx = this.db.transaction(() => {
|
|
5548
|
+
for (const file of files) {
|
|
5549
|
+
this.stmts.deleteFileSymbols.run({ file_path: file.filePath });
|
|
5550
|
+
this.stmts.deleteFile.run({ file_path: file.filePath });
|
|
5551
|
+
this.stmts.insertFile.run({
|
|
5552
|
+
file_path: file.filePath,
|
|
5553
|
+
content_hash: file.contentHash,
|
|
5554
|
+
language: file.language,
|
|
5555
|
+
last_indexed_at: Date.now(),
|
|
5556
|
+
line_count: file.lineCount
|
|
5557
|
+
});
|
|
5558
|
+
for (const sym of file.symbols) {
|
|
5559
|
+
const symbolId = `${file.filePath}::${sym.name}#${sym.kind}`;
|
|
5560
|
+
this.stmts.insertSymbol.run({
|
|
5561
|
+
file_path: file.filePath,
|
|
5562
|
+
name: sym.name,
|
|
5563
|
+
kind: sym.kind,
|
|
5564
|
+
start_line: sym.start_line,
|
|
5565
|
+
end_line: sym.end_line,
|
|
5566
|
+
byte_offset_start: sym.byte_offset_start,
|
|
5567
|
+
byte_offset_end: sym.byte_offset_end,
|
|
5568
|
+
signature: sym.signature,
|
|
5569
|
+
is_exported: sym.is_exported ? 1 : 0,
|
|
5570
|
+
symbol_id: symbolId
|
|
5571
|
+
});
|
|
5572
|
+
}
|
|
5573
|
+
}
|
|
5574
|
+
});
|
|
5575
|
+
tx();
|
|
5576
|
+
}
|
|
5577
|
+
findByName(name2) {
|
|
5578
|
+
return this.stmts.getSymbolByName.all({ name: name2 });
|
|
5579
|
+
}
|
|
5580
|
+
findById(symbolId) {
|
|
5581
|
+
return this.stmts.getSymbolById.get({ symbol_id: symbolId }) ?? null;
|
|
5582
|
+
}
|
|
5583
|
+
getFileSymbols(filePath) {
|
|
5584
|
+
return this.stmts.getSymbolsByFile.all({ file_path: filePath });
|
|
5585
|
+
}
|
|
5586
|
+
findCallers(symbolId, maxDepth = 3) {
|
|
5587
|
+
const results = [];
|
|
5588
|
+
const visited = new Set;
|
|
5589
|
+
let frontier = [symbolId];
|
|
5590
|
+
for (let depth = 1;depth <= maxDepth && frontier.length > 0; depth++) {
|
|
5591
|
+
const nextFrontier = [];
|
|
5592
|
+
for (const id of frontier) {
|
|
5593
|
+
if (visited.has(id))
|
|
5594
|
+
continue;
|
|
5595
|
+
visited.add(id);
|
|
5596
|
+
const callers = this.stmts.getCallers.all({ callee_id: id });
|
|
5597
|
+
for (const caller of callers) {
|
|
5598
|
+
if (!visited.has(caller.id)) {
|
|
5599
|
+
results.push({ ...caller, depth });
|
|
5600
|
+
nextFrontier.push(caller.id);
|
|
5601
|
+
}
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
frontier = nextFrontier;
|
|
5605
|
+
}
|
|
5606
|
+
return results;
|
|
5607
|
+
}
|
|
5608
|
+
findBlastRadius(symbolId) {
|
|
5609
|
+
return this.findCallers(symbolId, 10);
|
|
5610
|
+
}
|
|
5611
|
+
findDeadCode() {
|
|
5612
|
+
return this.stmts.getDeadCode.all();
|
|
5613
|
+
}
|
|
5614
|
+
searchSymbols(query, limit = 20) {
|
|
5615
|
+
const exact = this.db.query("SELECT * FROM symbol_index WHERE name = $q COLLATE NOCASE LIMIT $limit").all({ q: query, limit });
|
|
5616
|
+
if (exact.length > 0)
|
|
5617
|
+
return exact;
|
|
5618
|
+
const prefix = this.db.query("SELECT * FROM symbol_index WHERE name LIKE $q COLLATE NOCASE LIMIT $limit").all({ q: `${query}%`, limit });
|
|
5619
|
+
if (prefix.length > 0)
|
|
5620
|
+
return prefix;
|
|
5621
|
+
return this.db.query("SELECT * FROM symbol_index WHERE name LIKE $q COLLATE NOCASE LIMIT $limit").all({ q: `%${query}%`, limit });
|
|
5622
|
+
}
|
|
5623
|
+
insertCallEdges(edges) {
|
|
5624
|
+
const tx = this.db.transaction(() => {
|
|
5625
|
+
for (const edge of edges) {
|
|
5626
|
+
this.stmts.insertCallEdge.run({
|
|
5627
|
+
caller_id: edge.caller_id,
|
|
5628
|
+
callee_id: edge.callee_id,
|
|
5629
|
+
call_line: edge.call_line
|
|
5630
|
+
});
|
|
5631
|
+
}
|
|
5632
|
+
});
|
|
5633
|
+
tx();
|
|
5634
|
+
}
|
|
5635
|
+
removeStaleFiles(existingPaths) {
|
|
5636
|
+
const allFiles = this.getAllFiles();
|
|
5637
|
+
let removed = 0;
|
|
5638
|
+
const tx = this.db.transaction(() => {
|
|
5639
|
+
for (const file of allFiles) {
|
|
5640
|
+
if (!existingPaths.has(file.file_path)) {
|
|
5641
|
+
this.stmts.deleteFileSymbols.run({ file_path: file.file_path });
|
|
5642
|
+
this.stmts.deleteFile.run({ file_path: file.file_path });
|
|
5643
|
+
removed++;
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
});
|
|
5647
|
+
tx();
|
|
5648
|
+
return removed;
|
|
5649
|
+
}
|
|
5650
|
+
getStats() {
|
|
5651
|
+
const files = this.db.query("SELECT COUNT(*) as c FROM workspace_files").get().c;
|
|
5652
|
+
const symbols = this.db.query("SELECT COUNT(*) as c FROM symbol_index").get().c;
|
|
5653
|
+
const edges = this.db.query("SELECT COUNT(*) as c FROM call_graph").get().c;
|
|
5654
|
+
return { files, symbols, edges };
|
|
5655
|
+
}
|
|
5656
|
+
close() {
|
|
5657
|
+
this.db.close(false);
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
function getIndexDB(projectRoot) {
|
|
5661
|
+
let db = indexCache.get(projectRoot);
|
|
5662
|
+
if (!db) {
|
|
5663
|
+
db = new IndexDB(projectRoot);
|
|
5664
|
+
indexCache.set(projectRoot, db);
|
|
5665
|
+
}
|
|
5666
|
+
return db;
|
|
5667
|
+
}
|
|
5668
|
+
function closeAllIndexDBs() {
|
|
5669
|
+
for (const db of indexCache.values()) {
|
|
5670
|
+
db.close();
|
|
5671
|
+
}
|
|
5672
|
+
indexCache.clear();
|
|
5673
|
+
}
|
|
5674
|
+
var SCHEMA_SQL = `
|
|
5675
|
+
CREATE TABLE IF NOT EXISTS workspace_files (
|
|
5676
|
+
file_path TEXT PRIMARY KEY,
|
|
5677
|
+
content_hash TEXT NOT NULL,
|
|
5678
|
+
language TEXT NOT NULL,
|
|
5679
|
+
last_indexed_at INTEGER NOT NULL,
|
|
5680
|
+
line_count INTEGER NOT NULL DEFAULT 0
|
|
5681
|
+
);
|
|
5682
|
+
|
|
5683
|
+
CREATE TABLE IF NOT EXISTS symbol_index (
|
|
5684
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
5685
|
+
file_path TEXT NOT NULL,
|
|
5686
|
+
name TEXT NOT NULL,
|
|
5687
|
+
kind TEXT NOT NULL,
|
|
5688
|
+
start_line INTEGER NOT NULL,
|
|
5689
|
+
end_line INTEGER NOT NULL,
|
|
5690
|
+
byte_offset_start INTEGER NOT NULL,
|
|
5691
|
+
byte_offset_end INTEGER NOT NULL,
|
|
5692
|
+
signature TEXT,
|
|
5693
|
+
is_exported INTEGER NOT NULL DEFAULT 0,
|
|
5694
|
+
symbol_id TEXT NOT NULL,
|
|
5695
|
+
FOREIGN KEY (file_path) REFERENCES workspace_files(file_path) ON DELETE CASCADE
|
|
5696
|
+
);
|
|
5697
|
+
|
|
5698
|
+
CREATE TABLE IF NOT EXISTS call_graph (
|
|
5699
|
+
caller_id INTEGER NOT NULL,
|
|
5700
|
+
callee_id INTEGER NOT NULL,
|
|
5701
|
+
call_line INTEGER NOT NULL,
|
|
5702
|
+
PRIMARY KEY (caller_id, callee_id, call_line),
|
|
5703
|
+
FOREIGN KEY (caller_id) REFERENCES symbol_index(id) ON DELETE CASCADE,
|
|
5704
|
+
FOREIGN KEY (callee_id) REFERENCES symbol_index(id) ON DELETE CASCADE
|
|
5705
|
+
);
|
|
5706
|
+
|
|
5707
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbol_index(name);
|
|
5708
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_name_kind ON symbol_index(name, kind);
|
|
5709
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbol_index(file_path);
|
|
5710
|
+
CREATE INDEX IF NOT EXISTS idx_symbols_id ON symbol_index(symbol_id);
|
|
5711
|
+
CREATE INDEX IF NOT EXISTS idx_call_graph_callee ON call_graph(callee_id);
|
|
5712
|
+
CREATE INDEX IF NOT EXISTS idx_call_graph_caller ON call_graph(caller_id);
|
|
5713
|
+
|
|
5714
|
+
CREATE TABLE IF NOT EXISTS index_meta (
|
|
5715
|
+
key TEXT PRIMARY KEY,
|
|
5716
|
+
value TEXT NOT NULL
|
|
5717
|
+
);
|
|
5718
|
+
`, SCHEMA_VERSION = "2", indexCache;
|
|
5719
|
+
var init_index_db = __esm(() => {
|
|
5720
|
+
indexCache = new Map;
|
|
5721
|
+
});
|
|
5722
|
+
|
|
5723
|
+
// src/tools/reader/indexer.ts
|
|
5724
|
+
var exports_indexer = {};
|
|
5725
|
+
__export(exports_indexer, {
|
|
5726
|
+
hasIndex: () => hasIndex,
|
|
5727
|
+
buildIndex: () => buildIndex
|
|
5728
|
+
});
|
|
5729
|
+
import { join as join4 } from "path";
|
|
5730
|
+
async function buildIndex(projectRoot) {
|
|
5731
|
+
const start2 = Date.now();
|
|
5732
|
+
const db = getIndexDB(projectRoot);
|
|
5733
|
+
const filePaths = await discoverFiles(projectRoot);
|
|
5734
|
+
const existingPaths = new Set(filePaths.map((f) => f.relativePath));
|
|
5735
|
+
db.removeStaleFiles(existingPaths);
|
|
5736
|
+
const toIndex = [];
|
|
5737
|
+
let fromCache = 0;
|
|
5738
|
+
for (const file of filePaths) {
|
|
5739
|
+
const content = await readFileContent(file.absolutePath);
|
|
5740
|
+
if (!content)
|
|
5741
|
+
continue;
|
|
5742
|
+
const hash = hashContent(content);
|
|
5743
|
+
if (!db.needsReindex(file.relativePath, hash)) {
|
|
5744
|
+
fromCache++;
|
|
5745
|
+
continue;
|
|
5746
|
+
}
|
|
5747
|
+
const lang = detectLanguage(file.relativePath);
|
|
5748
|
+
if (!lang)
|
|
5749
|
+
continue;
|
|
5750
|
+
toIndex.push({
|
|
5751
|
+
relativePath: file.relativePath,
|
|
5752
|
+
absolutePath: file.absolutePath,
|
|
5753
|
+
content,
|
|
5754
|
+
hash,
|
|
5755
|
+
language: lang
|
|
5756
|
+
});
|
|
5757
|
+
}
|
|
5758
|
+
const fileBatch = [];
|
|
5759
|
+
let totalSymbols = 0;
|
|
5760
|
+
for (const file of toIndex) {
|
|
5761
|
+
try {
|
|
5762
|
+
const parsed = await parseFile(file.relativePath, file.content);
|
|
5763
|
+
if (!parsed)
|
|
5764
|
+
continue;
|
|
5765
|
+
const lineCount = file.content.split(`
|
|
5766
|
+
`).length;
|
|
5767
|
+
const symbols = parsed.symbols.map((sym) => {
|
|
5768
|
+
const lines = file.content.split(`
|
|
5769
|
+
`);
|
|
5770
|
+
let byteStart = 0;
|
|
5771
|
+
for (let i2 = 0;i2 < sym.startLine - 1 && i2 < lines.length; i2++) {
|
|
5772
|
+
byteStart += lines[i2].length + 1;
|
|
5773
|
+
}
|
|
5774
|
+
let byteEnd = byteStart;
|
|
5775
|
+
for (let i2 = sym.startLine - 1;i2 < sym.endLine && i2 < lines.length; i2++) {
|
|
5776
|
+
byteEnd += lines[i2].length + 1;
|
|
5777
|
+
}
|
|
5778
|
+
return {
|
|
5779
|
+
name: sym.name,
|
|
5780
|
+
kind: sym.kind,
|
|
5781
|
+
start_line: sym.startLine,
|
|
5782
|
+
end_line: sym.endLine,
|
|
5783
|
+
byte_offset_start: byteStart,
|
|
5784
|
+
byte_offset_end: byteEnd,
|
|
5785
|
+
signature: sym.signature || null,
|
|
5786
|
+
is_exported: sym.isExported ?? false
|
|
5787
|
+
};
|
|
5788
|
+
});
|
|
5789
|
+
totalSymbols += symbols.length;
|
|
5790
|
+
fileBatch.push({
|
|
5791
|
+
filePath: file.relativePath,
|
|
5792
|
+
contentHash: file.hash,
|
|
5793
|
+
language: file.language,
|
|
5794
|
+
lineCount,
|
|
5795
|
+
symbols
|
|
5796
|
+
});
|
|
5797
|
+
} catch (err2) {
|
|
5798
|
+
logger.warn(`Indexer: failed to parse ${file.relativePath}: ${err2.message}`);
|
|
5799
|
+
}
|
|
5800
|
+
}
|
|
5801
|
+
if (fileBatch.length > 0) {
|
|
5802
|
+
db.indexFiles(fileBatch);
|
|
5803
|
+
}
|
|
5804
|
+
return {
|
|
5805
|
+
files_scanned: filePaths.length,
|
|
5806
|
+
files_indexed: fileBatch.length,
|
|
5807
|
+
files_skipped: filePaths.length - fileBatch.length - fromCache,
|
|
5808
|
+
symbols_indexed: totalSymbols,
|
|
5809
|
+
duration_ms: Date.now() - start2,
|
|
5810
|
+
from_cache: fromCache
|
|
5811
|
+
};
|
|
5812
|
+
}
|
|
5813
|
+
function hasIndex(projectRoot) {
|
|
5814
|
+
const db = getIndexDB(projectRoot);
|
|
5815
|
+
const stats = db.getStats();
|
|
5816
|
+
return stats.files > 0;
|
|
5817
|
+
}
|
|
5818
|
+
async function discoverFiles(root) {
|
|
5819
|
+
const files = [];
|
|
5820
|
+
const glob = new Bun.Glob("**/*");
|
|
5821
|
+
for await (const entry of glob.scan({ cwd: root, onlyFiles: true })) {
|
|
5822
|
+
if (files.length >= MAX_FILES)
|
|
5823
|
+
break;
|
|
5824
|
+
const parts2 = entry.split("/");
|
|
5825
|
+
if (parts2.some((p) => EXCLUDE_DIRS.has(p)))
|
|
5826
|
+
continue;
|
|
5827
|
+
const ext = "." + (entry.split(".").pop()?.toLowerCase() || "");
|
|
5828
|
+
if (!SOURCE_EXTENSIONS2.has(ext))
|
|
5829
|
+
continue;
|
|
5830
|
+
files.push({
|
|
5831
|
+
relativePath: entry,
|
|
5832
|
+
absolutePath: join4(root, entry)
|
|
5833
|
+
});
|
|
5834
|
+
}
|
|
5835
|
+
return files;
|
|
5836
|
+
}
|
|
5837
|
+
async function readFileContent(absolutePath) {
|
|
5838
|
+
try {
|
|
5839
|
+
const file = Bun.file(absolutePath);
|
|
5840
|
+
const size = file.size;
|
|
5841
|
+
if (size > MAX_FILE_SIZE)
|
|
5842
|
+
return null;
|
|
5843
|
+
return await file.text();
|
|
5844
|
+
} catch {
|
|
5845
|
+
return null;
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
function hashContent(content) {
|
|
5849
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
5850
|
+
hasher.update(content);
|
|
5851
|
+
return hasher.digest("hex");
|
|
5852
|
+
}
|
|
5853
|
+
var MAX_FILES = 2000, MAX_FILE_SIZE = 1e6, EXCLUDE_DIRS, SOURCE_EXTENSIONS2;
|
|
5854
|
+
var init_indexer = __esm(() => {
|
|
5855
|
+
init_index_db();
|
|
5856
|
+
init_parser();
|
|
5857
|
+
init_logger();
|
|
5858
|
+
EXCLUDE_DIRS = new Set([
|
|
5859
|
+
"node_modules",
|
|
5860
|
+
".git",
|
|
5861
|
+
"dist",
|
|
5862
|
+
"build",
|
|
5863
|
+
".next",
|
|
5864
|
+
".nuxt",
|
|
5865
|
+
".output",
|
|
5866
|
+
"coverage",
|
|
5867
|
+
"__pycache__",
|
|
5868
|
+
".venv",
|
|
5869
|
+
"venv",
|
|
5870
|
+
"target",
|
|
5871
|
+
"vendor",
|
|
5872
|
+
".zephex",
|
|
5873
|
+
".cache",
|
|
5874
|
+
".turbo",
|
|
5875
|
+
"out"
|
|
5876
|
+
]);
|
|
5877
|
+
SOURCE_EXTENSIONS2 = new Set([
|
|
5878
|
+
".ts",
|
|
5879
|
+
".tsx",
|
|
5880
|
+
".js",
|
|
5881
|
+
".jsx",
|
|
5882
|
+
".mjs",
|
|
5883
|
+
".cjs",
|
|
5884
|
+
".py",
|
|
5885
|
+
".go",
|
|
5886
|
+
".java",
|
|
5887
|
+
".rb",
|
|
5888
|
+
".php",
|
|
5889
|
+
".rs",
|
|
5890
|
+
".cs",
|
|
5891
|
+
".cpp",
|
|
5892
|
+
".cc",
|
|
5893
|
+
".c",
|
|
5894
|
+
".h",
|
|
5895
|
+
".hpp",
|
|
5896
|
+
".sh",
|
|
5897
|
+
".kt",
|
|
5898
|
+
".kts",
|
|
5899
|
+
".swift",
|
|
5900
|
+
".scala",
|
|
5901
|
+
".dart",
|
|
5902
|
+
".ex",
|
|
5903
|
+
".exs",
|
|
5904
|
+
".zig",
|
|
5905
|
+
".lua",
|
|
5906
|
+
".vue",
|
|
5907
|
+
".svelte",
|
|
5908
|
+
".astro"
|
|
5909
|
+
]);
|
|
5910
|
+
});
|
|
5911
|
+
|
|
5362
5912
|
// src/tools/reader/readCode.ts
|
|
5363
|
-
import { isAbsolute as isAbsolute2, normalize, relative } from "path";
|
|
5913
|
+
import { isAbsolute as isAbsolute2, normalize, relative, join as join5 } from "path";
|
|
5364
5914
|
import { access as access2, realpath, stat } from "fs/promises";
|
|
5365
5915
|
function iterativeUrlDecode(input) {
|
|
5366
5916
|
let decoded = input;
|
|
@@ -5576,7 +6126,18 @@ function extractDirectImports(fileContent) {
|
|
|
5576
6126
|
}
|
|
5577
6127
|
function isBinaryContent(content) {
|
|
5578
6128
|
const check = content.slice(0, 512);
|
|
5579
|
-
|
|
6129
|
+
if (check.includes("\x00"))
|
|
6130
|
+
return true;
|
|
6131
|
+
const head = check.slice(0, 8);
|
|
6132
|
+
if (head.startsWith("PNG") || head.startsWith("ÿØÿ") || head.startsWith("GIF8") || head.startsWith("PK\x03\x04") || head.startsWith("ELF") || head.startsWith("MZ") || head.startsWith("Êþº¾") || head.startsWith("\x1F") || head.startsWith("RIFF") || head.startsWith("%PDF"))
|
|
6133
|
+
return true;
|
|
6134
|
+
let nonPrintable = 0;
|
|
6135
|
+
for (let i2 = 0;i2 < check.length; i2++) {
|
|
6136
|
+
const c = check.charCodeAt(i2);
|
|
6137
|
+
if (c < 8 || c > 13 && c < 32 && c !== 27)
|
|
6138
|
+
nonPrintable++;
|
|
6139
|
+
}
|
|
6140
|
+
return nonPrintable / check.length > 0.1;
|
|
5580
6141
|
}
|
|
5581
6142
|
function isBinaryFile(filePath) {
|
|
5582
6143
|
const ext = "." + (filePath.split(".").pop()?.toLowerCase() || "");
|
|
@@ -5781,7 +6342,7 @@ function addLineNumbers(body2, startLine) {
|
|
|
5781
6342
|
}).join(`
|
|
5782
6343
|
`);
|
|
5783
6344
|
}
|
|
5784
|
-
function extractSymbolContent(symbol, detailLevel, fileContent) {
|
|
6345
|
+
function extractSymbolContent(symbol, detailLevel, fileContent, compact) {
|
|
5785
6346
|
const signature = extractSignature(symbol);
|
|
5786
6347
|
const base = {
|
|
5787
6348
|
name: symbol.name,
|
|
@@ -5802,14 +6363,16 @@ function extractSymbolContent(symbol, detailLevel, fileContent) {
|
|
|
5802
6363
|
base.token_estimate = estimateTokens(signature);
|
|
5803
6364
|
return base;
|
|
5804
6365
|
case "body": {
|
|
5805
|
-
const numberedBody = addLineNumbers(symbol.body, symbol.startLine);
|
|
6366
|
+
const numberedBody = compact ? symbol.body : addLineNumbers(symbol.body, symbol.startLine);
|
|
5806
6367
|
base.body = numberedBody;
|
|
5807
6368
|
base.token_estimate = estimateTokens(numberedBody);
|
|
6369
|
+
base.diagnostics = computeDiagnostics(symbol.body, symbol.params?.length ?? 0);
|
|
5808
6370
|
return base;
|
|
5809
6371
|
}
|
|
5810
6372
|
case "context": {
|
|
5811
|
-
const numberedBody = addLineNumbers(symbol.body, symbol.startLine);
|
|
6373
|
+
const numberedBody = compact ? symbol.body : addLineNumbers(symbol.body, symbol.startLine);
|
|
5812
6374
|
base.body = numberedBody;
|
|
6375
|
+
base.diagnostics = computeDiagnostics(symbol.body, symbol.params?.length ?? 0);
|
|
5813
6376
|
base.direct_imports = extractDirectImports(fileContent);
|
|
5814
6377
|
const contextText = numberedBody + `
|
|
5815
6378
|
` + base.direct_imports.join(`
|
|
@@ -5850,8 +6413,8 @@ async function findTestFiles(symbolName, symbolFile, filesToSearch) {
|
|
|
5850
6413
|
async function scanLocalDirectory(dirPath) {
|
|
5851
6414
|
const { readFile } = await import("fs/promises");
|
|
5852
6415
|
const files = {};
|
|
5853
|
-
const
|
|
5854
|
-
const
|
|
6416
|
+
const MAX_FILES2 = 400;
|
|
6417
|
+
const MAX_FILE_SIZE2 = 1048576;
|
|
5855
6418
|
const priorityPatterns = [
|
|
5856
6419
|
"src/**/*",
|
|
5857
6420
|
"lib/**/*",
|
|
@@ -5877,7 +6440,7 @@ async function scanLocalDirectory(dirPath) {
|
|
|
5877
6440
|
return false;
|
|
5878
6441
|
try {
|
|
5879
6442
|
const stats = await stat(filePath);
|
|
5880
|
-
if (!stats.isFile() || stats.size >
|
|
6443
|
+
if (!stats.isFile() || stats.size > MAX_FILE_SIZE2 || stats.size === 0)
|
|
5881
6444
|
return false;
|
|
5882
6445
|
const content = await readFile(filePath, "utf-8");
|
|
5883
6446
|
if (isBinaryContent(content))
|
|
@@ -5891,26 +6454,307 @@ async function scanLocalDirectory(dirPath) {
|
|
|
5891
6454
|
}
|
|
5892
6455
|
}
|
|
5893
6456
|
for (const pattern of priorityPatterns) {
|
|
5894
|
-
if (fileCount >=
|
|
6457
|
+
if (fileCount >= MAX_FILES2)
|
|
5895
6458
|
break;
|
|
5896
6459
|
const glob = new Bun.Glob(pattern);
|
|
5897
6460
|
for await (const filePath of glob.scan({ cwd: dirPath, absolute: true })) {
|
|
5898
|
-
if (fileCount >=
|
|
6461
|
+
if (fileCount >= MAX_FILES2)
|
|
5899
6462
|
break;
|
|
5900
6463
|
await ingest(filePath);
|
|
5901
6464
|
}
|
|
5902
6465
|
}
|
|
5903
|
-
if (fileCount <
|
|
6466
|
+
if (fileCount < MAX_FILES2) {
|
|
5904
6467
|
const glob = new Bun.Glob(fallbackPattern);
|
|
5905
6468
|
for await (const filePath of glob.scan({ cwd: dirPath, absolute: true })) {
|
|
5906
|
-
if (fileCount >=
|
|
6469
|
+
if (fileCount >= MAX_FILES2)
|
|
5907
6470
|
break;
|
|
5908
6471
|
await ingest(filePath);
|
|
5909
6472
|
}
|
|
5910
6473
|
}
|
|
5911
6474
|
return files;
|
|
5912
6475
|
}
|
|
6476
|
+
async function handleFileMode(params, filesToSearch) {
|
|
6477
|
+
const maxTokens = Math.min(params.max_tokens ?? DEFAULT_MAX_TOKENS, MAX_TOKENS_LIMIT);
|
|
6478
|
+
const requestedFiles = params.files ?? [];
|
|
6479
|
+
const offsetLine = Math.max(1, params.offset_line ?? 1);
|
|
6480
|
+
const limitLines = params.limit_lines;
|
|
6481
|
+
const compact = params.compact ?? false;
|
|
6482
|
+
if (requestedFiles.length === 0) {
|
|
6483
|
+
throw new ReadCodeError("mode:'file' requires a `files` array with at least one path", -32602);
|
|
6484
|
+
}
|
|
6485
|
+
const readResults = await Promise.all(requestedFiles.map(async (filePath) => {
|
|
6486
|
+
const content = filesToSearch[filePath] ?? filesToSearch[filePath.startsWith("/") ? filePath.slice(1) : filePath];
|
|
6487
|
+
if (!content) {
|
|
6488
|
+
return {
|
|
6489
|
+
file: filePath,
|
|
6490
|
+
content: `// File not found: ${filePath}`,
|
|
6491
|
+
total_lines: 0,
|
|
6492
|
+
returned_lines: 0,
|
|
6493
|
+
offset: offsetLine,
|
|
6494
|
+
has_more: false,
|
|
6495
|
+
token_estimate: 10,
|
|
6496
|
+
truncated: false
|
|
6497
|
+
};
|
|
6498
|
+
}
|
|
6499
|
+
const allLines = content.split(`
|
|
6500
|
+
`);
|
|
6501
|
+
const totalLines = allLines.length;
|
|
6502
|
+
const startIdx = offsetLine - 1;
|
|
6503
|
+
let endIdx = totalLines;
|
|
6504
|
+
if (limitLines)
|
|
6505
|
+
endIdx = Math.min(startIdx + limitLines, totalLines);
|
|
6506
|
+
const slicedLines = allLines.slice(startIdx, endIdx);
|
|
6507
|
+
let outputContent;
|
|
6508
|
+
if (compact) {
|
|
6509
|
+
outputContent = slicedLines.join(`
|
|
6510
|
+
`);
|
|
6511
|
+
} else {
|
|
6512
|
+
const padWidth = String(startIdx + slicedLines.length).length;
|
|
6513
|
+
outputContent = slicedLines.map((line, i2) => `${String(startIdx + i2 + 1).padStart(padWidth)} | ${line}`).join(`
|
|
6514
|
+
`);
|
|
6515
|
+
}
|
|
6516
|
+
const tokenEst = estimateTokens(outputContent);
|
|
6517
|
+
const hasMore = startIdx + slicedLines.length < totalLines;
|
|
6518
|
+
return {
|
|
6519
|
+
file: filePath,
|
|
6520
|
+
content: outputContent,
|
|
6521
|
+
total_lines: totalLines,
|
|
6522
|
+
returned_lines: slicedLines.length,
|
|
6523
|
+
offset: offsetLine,
|
|
6524
|
+
has_more: hasMore,
|
|
6525
|
+
token_estimate: tokenEst,
|
|
6526
|
+
truncated: false
|
|
6527
|
+
};
|
|
6528
|
+
}));
|
|
6529
|
+
const results = [];
|
|
6530
|
+
const remaining = [];
|
|
6531
|
+
let totalTokens = 0;
|
|
6532
|
+
for (const entry of readResults) {
|
|
6533
|
+
if (totalTokens + entry.token_estimate > maxTokens && results.length > 0) {
|
|
6534
|
+
remaining.push(entry.file);
|
|
6535
|
+
continue;
|
|
6536
|
+
}
|
|
6537
|
+
if (totalTokens + entry.token_estimate > maxTokens) {
|
|
6538
|
+
const availableTokens = maxTokens - totalTokens;
|
|
6539
|
+
const availableChars = Math.floor(availableTokens * CHARS_PER_TOKEN);
|
|
6540
|
+
entry.content = entry.content.slice(0, availableChars);
|
|
6541
|
+
entry.returned_lines = entry.content.split(`
|
|
6542
|
+
`).length;
|
|
6543
|
+
entry.token_estimate = estimateTokens(entry.content);
|
|
6544
|
+
entry.truncated = true;
|
|
6545
|
+
}
|
|
6546
|
+
results.push(entry);
|
|
6547
|
+
totalTokens += entry.token_estimate;
|
|
6548
|
+
}
|
|
6549
|
+
return {
|
|
6550
|
+
mode: "file",
|
|
6551
|
+
files: results,
|
|
6552
|
+
total_tokens_returned: totalTokens,
|
|
6553
|
+
remaining: remaining.length > 0 ? remaining : undefined
|
|
6554
|
+
};
|
|
6555
|
+
}
|
|
6556
|
+
async function handleOutlineMode(params, filesToSearch) {
|
|
6557
|
+
const requestedFiles = params.files ?? [];
|
|
6558
|
+
if (requestedFiles.length === 0) {
|
|
6559
|
+
throw new ReadCodeError("mode:'outline' requires a `files` array with at least one path", -32602);
|
|
6560
|
+
}
|
|
6561
|
+
const filePath = requestedFiles[0];
|
|
6562
|
+
const content = filesToSearch[filePath] ?? filesToSearch[filePath.startsWith("/") ? filePath.slice(1) : filePath];
|
|
6563
|
+
if (!content) {
|
|
6564
|
+
throw new ReadCodeError(`File not found: ${filePath}`, -32602);
|
|
6565
|
+
}
|
|
6566
|
+
const totalLines = content.split(`
|
|
6567
|
+
`).length;
|
|
6568
|
+
const symbols = [];
|
|
6569
|
+
const lang = detectLanguage(filePath);
|
|
6570
|
+
if (lang) {
|
|
6571
|
+
try {
|
|
6572
|
+
const cached = await getOrParse(filePath, content);
|
|
6573
|
+
for (const sym of cached.symbols) {
|
|
6574
|
+
if (sym.name === "anonymous" || sym.kind === "method")
|
|
6575
|
+
continue;
|
|
6576
|
+
symbols.push({
|
|
6577
|
+
name: sym.name,
|
|
6578
|
+
kind: sym.kind,
|
|
6579
|
+
line: sym.startLine,
|
|
6580
|
+
end_line: sym.endLine,
|
|
6581
|
+
signature: sym.signature || sym.body.split(`
|
|
6582
|
+
`)[0]?.trim().slice(0, 200) || sym.name,
|
|
6583
|
+
is_exported: sym.isExported ?? false
|
|
6584
|
+
});
|
|
6585
|
+
}
|
|
6586
|
+
} catch {}
|
|
6587
|
+
}
|
|
6588
|
+
if (symbols.length === 0) {
|
|
6589
|
+
const lines = content.split(`
|
|
6590
|
+
`);
|
|
6591
|
+
const defPattern = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\*?|class|interface|type|enum|struct|trait|const|let|var|def|fn|pub\s+fn|func|module|namespace)\s+(\w+)/;
|
|
6592
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
6593
|
+
const match = lines[i2]?.match(defPattern);
|
|
6594
|
+
if (match) {
|
|
6595
|
+
symbols.push({
|
|
6596
|
+
name: match[1],
|
|
6597
|
+
kind: "function",
|
|
6598
|
+
line: i2 + 1,
|
|
6599
|
+
end_line: i2 + 1,
|
|
6600
|
+
signature: lines[i2].trim().slice(0, 200),
|
|
6601
|
+
is_exported: /^export\b/.test(lines[i2])
|
|
6602
|
+
});
|
|
6603
|
+
}
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
const topLevel = [];
|
|
6607
|
+
for (let i2 = 0;i2 < symbols.length; i2++) {
|
|
6608
|
+
const sym = symbols[i2];
|
|
6609
|
+
const isNested = symbols.some((other, j2) => j2 !== i2 && other.line < sym.line && other.end_line > sym.end_line);
|
|
6610
|
+
if (!isNested)
|
|
6611
|
+
topLevel.push(sym);
|
|
6612
|
+
}
|
|
6613
|
+
const outputText = topLevel.map((s) => `${s.line}: ${s.signature}`).join(`
|
|
6614
|
+
`);
|
|
6615
|
+
return {
|
|
6616
|
+
mode: "outline",
|
|
6617
|
+
file: filePath,
|
|
6618
|
+
total_lines: totalLines,
|
|
6619
|
+
symbols: topLevel,
|
|
6620
|
+
total_tokens_returned: estimateTokens(outputText)
|
|
6621
|
+
};
|
|
6622
|
+
}
|
|
5913
6623
|
async function handleReadCode(params) {
|
|
6624
|
+
const mode = params.mode ?? "symbol";
|
|
6625
|
+
if (mode === "callers" || mode === "blast_radius" || mode === "dead_code") {
|
|
6626
|
+
if (!params.path || isRemoteGitUrl(params.path)) {
|
|
6627
|
+
throw new ReadCodeError(`mode:'${mode}' requires a local 'path' with an existing index`, -32602);
|
|
6628
|
+
}
|
|
6629
|
+
const validatedPath = await validatePath(params.path);
|
|
6630
|
+
const { getIndexDB: getIndexDB2 } = await Promise.resolve().then(() => (init_index_db(), exports_index_db));
|
|
6631
|
+
const { hasIndex: hasIndex2 } = await Promise.resolve().then(() => (init_indexer(), exports_indexer));
|
|
6632
|
+
if (!hasIndex2(validatedPath)) {
|
|
6633
|
+
throw new ReadCodeError(`No index found. Call read_code with mode:'symbol' first to build the index.`, -32602);
|
|
6634
|
+
}
|
|
6635
|
+
const db = getIndexDB2(validatedPath);
|
|
6636
|
+
if (mode === "dead_code") {
|
|
6637
|
+
const dead = db.findDeadCode();
|
|
6638
|
+
return {
|
|
6639
|
+
mode: "dead_code",
|
|
6640
|
+
symbols: dead.slice(0, 50).map((s) => ({
|
|
6641
|
+
name: s.name,
|
|
6642
|
+
kind: s.kind,
|
|
6643
|
+
file: s.file_path,
|
|
6644
|
+
line: s.start_line,
|
|
6645
|
+
end_line: s.end_line,
|
|
6646
|
+
symbol_id: s.symbol_id,
|
|
6647
|
+
is_exported: s.is_exported === 1,
|
|
6648
|
+
signature: s.signature || s.name
|
|
6649
|
+
})),
|
|
6650
|
+
total_found: dead.length
|
|
6651
|
+
};
|
|
6652
|
+
}
|
|
6653
|
+
if (!params.target && !params.symbol_id) {
|
|
6654
|
+
throw new ReadCodeError(`mode:'${mode}' requires a 'target' or 'symbol_id'`, -32602);
|
|
6655
|
+
}
|
|
6656
|
+
let targetSym = null;
|
|
6657
|
+
if (params.symbol_id) {
|
|
6658
|
+
const found = db.findById(params.symbol_id);
|
|
6659
|
+
if (found)
|
|
6660
|
+
targetSym = found;
|
|
6661
|
+
}
|
|
6662
|
+
if (!targetSym && params.target) {
|
|
6663
|
+
const matches = db.findByName(params.target);
|
|
6664
|
+
if (matches.length > 0)
|
|
6665
|
+
targetSym = matches[0];
|
|
6666
|
+
}
|
|
6667
|
+
if (!targetSym) {
|
|
6668
|
+
throw new ReadCodeError(`Symbol not found in index: ${params.target || params.symbol_id}`, -32602);
|
|
6669
|
+
}
|
|
6670
|
+
const depth = mode === "blast_radius" ? 10 : 3;
|
|
6671
|
+
const results = db.findCallers(targetSym.id, depth);
|
|
6672
|
+
return {
|
|
6673
|
+
mode,
|
|
6674
|
+
target: targetSym.name,
|
|
6675
|
+
target_symbol_id: targetSym.symbol_id,
|
|
6676
|
+
callers: results.map((r) => ({
|
|
6677
|
+
name: r.name,
|
|
6678
|
+
kind: r.kind,
|
|
6679
|
+
file: r.file_path,
|
|
6680
|
+
line: r.start_line,
|
|
6681
|
+
call_line: r.call_line,
|
|
6682
|
+
depth: r.depth,
|
|
6683
|
+
symbol_id: r.symbol_id
|
|
6684
|
+
})),
|
|
6685
|
+
total_found: results.length
|
|
6686
|
+
};
|
|
6687
|
+
}
|
|
6688
|
+
if (mode === "file" || mode === "outline") {
|
|
6689
|
+
let filesToSearch2;
|
|
6690
|
+
if (params.inline_files && Object.keys(params.inline_files).length > 0) {
|
|
6691
|
+
filesToSearch2 = params.inline_files;
|
|
6692
|
+
} else if (params.path) {
|
|
6693
|
+
if (isRemoteGitUrl(params.path)) {
|
|
6694
|
+
return await withResolvedPath(params.path, async (localPath) => {
|
|
6695
|
+
const files = await scanLocalDirectory(localPath);
|
|
6696
|
+
if (mode === "file")
|
|
6697
|
+
return handleFileMode(params, files);
|
|
6698
|
+
return handleOutlineMode(params, files);
|
|
6699
|
+
});
|
|
6700
|
+
}
|
|
6701
|
+
const validatedPath = await validatePath(params.path);
|
|
6702
|
+
filesToSearch2 = await scanLocalDirectory(validatedPath);
|
|
6703
|
+
} else {
|
|
6704
|
+
throw new ReadCodeError("Either 'path' or 'inline_files' is required", -32602);
|
|
6705
|
+
}
|
|
6706
|
+
if (mode === "file")
|
|
6707
|
+
return handleFileMode(params, filesToSearch2);
|
|
6708
|
+
return handleOutlineMode(params, filesToSearch2);
|
|
6709
|
+
}
|
|
6710
|
+
if (!params.target && !params.symbol_id) {
|
|
6711
|
+
throw new ReadCodeError("mode:'symbol' requires a `target` or `symbol_id` parameter", -32602);
|
|
6712
|
+
}
|
|
6713
|
+
if (params.symbol_id && params.path && !isRemoteGitUrl(params.path)) {
|
|
6714
|
+
try {
|
|
6715
|
+
const { getIndexDB: getIndexDB2 } = await Promise.resolve().then(() => (init_index_db(), exports_index_db));
|
|
6716
|
+
const { hasIndex: hasIndex2 } = await Promise.resolve().then(() => (init_indexer(), exports_indexer));
|
|
6717
|
+
const validatedPath = await validatePath(params.path);
|
|
6718
|
+
if (hasIndex2(validatedPath)) {
|
|
6719
|
+
const db = getIndexDB2(validatedPath);
|
|
6720
|
+
const sym = db.findById(params.symbol_id);
|
|
6721
|
+
if (sym) {
|
|
6722
|
+
const abs = join5(validatedPath, sym.file_path);
|
|
6723
|
+
const content = await Bun.file(abs).text();
|
|
6724
|
+
const lines = content.split(`
|
|
6725
|
+
`);
|
|
6726
|
+
const body2 = lines.slice(sym.start_line - 1, sym.end_line).join(`
|
|
6727
|
+
`);
|
|
6728
|
+
const numberedBody = params.compact ? body2 : addLineNumbers(body2, sym.start_line);
|
|
6729
|
+
return {
|
|
6730
|
+
target: sym.name,
|
|
6731
|
+
symbols: [{
|
|
6732
|
+
name: sym.name,
|
|
6733
|
+
kind: sym.kind,
|
|
6734
|
+
file: sym.file_path,
|
|
6735
|
+
line: sym.start_line,
|
|
6736
|
+
end_line: sym.end_line,
|
|
6737
|
+
language: detectLanguage(sym.file_path) || "unknown",
|
|
6738
|
+
signature: sym.signature || lines[sym.start_line - 1]?.trim().slice(0, 200) || sym.name,
|
|
6739
|
+
confidence: 1,
|
|
6740
|
+
is_definition: true,
|
|
6741
|
+
is_exported: sym.is_exported === 1,
|
|
6742
|
+
body: numberedBody,
|
|
6743
|
+
token_estimate: estimateTokens(numberedBody),
|
|
6744
|
+
truncated: false,
|
|
6745
|
+
symbol_id: sym.symbol_id
|
|
6746
|
+
}],
|
|
6747
|
+
total_found: 1,
|
|
6748
|
+
returned_count: 1,
|
|
6749
|
+
truncated: false,
|
|
6750
|
+
total_tokens_returned: estimateTokens(numberedBody),
|
|
6751
|
+
tokens_saved_vs_full_files: estimateTokens(content) - estimateTokens(numberedBody),
|
|
6752
|
+
session_dedup_skipped: 0
|
|
6753
|
+
};
|
|
6754
|
+
}
|
|
6755
|
+
}
|
|
6756
|
+
} catch {}
|
|
6757
|
+
}
|
|
5914
6758
|
let maxTokens = params.max_tokens ?? DEFAULT_MAX_TOKENS;
|
|
5915
6759
|
if (params.max_lines && !params.max_tokens) {
|
|
5916
6760
|
maxTokens = Math.ceil(params.max_lines * 12);
|
|
@@ -5937,7 +6781,15 @@ async function handleReadCode(params) {
|
|
|
5937
6781
|
include_tests = false,
|
|
5938
6782
|
session_id
|
|
5939
6783
|
} = params;
|
|
5940
|
-
|
|
6784
|
+
if (!target) {
|
|
6785
|
+
const fallbackTarget = params.symbol_id?.split("::")[1]?.split("#")[0];
|
|
6786
|
+
if (!fallbackTarget) {
|
|
6787
|
+
throw new ReadCodeError("mode:'symbol' requires a `target` or valid `symbol_id`", -32602);
|
|
6788
|
+
}
|
|
6789
|
+
params.target = fallbackTarget;
|
|
6790
|
+
}
|
|
6791
|
+
const effectiveTarget = target || params.symbol_id?.split("::")[1]?.split("#")[0] || "";
|
|
6792
|
+
const allTargets = [effectiveTarget, ...targets || []].slice(0, 9);
|
|
5941
6793
|
const validatedTargets = allTargets.map(validateTarget);
|
|
5942
6794
|
const clampedMaxResults = Math.min(Math.max(1, max_results), MAX_RESULTS_LIMIT);
|
|
5943
6795
|
const clampedThreshold = Math.min(Math.max(0, confidence_threshold), 1);
|
|
@@ -5964,7 +6816,40 @@ async function handleReadCode(params) {
|
|
|
5964
6816
|
return result;
|
|
5965
6817
|
} else {
|
|
5966
6818
|
const validatedPath = await validatePath(projectPath);
|
|
5967
|
-
|
|
6819
|
+
if (params.use_index !== false) {
|
|
6820
|
+
try {
|
|
6821
|
+
const { hasIndex: hasIndex2, buildIndex: buildIndex2 } = await Promise.resolve().then(() => (init_indexer(), exports_indexer));
|
|
6822
|
+
const { getIndexDB: getIndexDB2 } = await Promise.resolve().then(() => (init_index_db(), exports_index_db));
|
|
6823
|
+
if (!hasIndex2(validatedPath)) {
|
|
6824
|
+
buildIndex2(validatedPath).catch((err2) => logger.warn(`Index build failed: ${err2.message}`));
|
|
6825
|
+
} else {
|
|
6826
|
+
const db = getIndexDB2(validatedPath);
|
|
6827
|
+
const indexHits = [];
|
|
6828
|
+
for (const t of validatedTargets) {
|
|
6829
|
+
const matches = db.searchSymbols(t, clampedMaxResults);
|
|
6830
|
+
for (const m of matches) {
|
|
6831
|
+
if (!kind || m.kind === kind) {
|
|
6832
|
+
indexHits.push({ file_path: m.file_path, name: m.name });
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
if (indexHits.length > 0) {
|
|
6837
|
+
const uniqueFiles = [...new Set(indexHits.map((h) => h.file_path))];
|
|
6838
|
+
filesToSearch = {};
|
|
6839
|
+
for (const fp of uniqueFiles.slice(0, 50)) {
|
|
6840
|
+
try {
|
|
6841
|
+
const abs = join5(validatedPath, fp);
|
|
6842
|
+
const content = await Bun.file(abs).text();
|
|
6843
|
+
filesToSearch[fp] = content;
|
|
6844
|
+
} catch {}
|
|
6845
|
+
}
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
} catch {}
|
|
6849
|
+
}
|
|
6850
|
+
if (!filesToSearch || Object.keys(filesToSearch).length === 0) {
|
|
6851
|
+
filesToSearch = await scanLocalDirectory(validatedPath);
|
|
6852
|
+
}
|
|
5968
6853
|
if (Object.keys(filesToSearch).length === 0) {
|
|
5969
6854
|
throw new ReadCodeError(`No supported source files found in ${projectPath}. Supported extensions: .ts, .tsx, .js, .jsx, .mjs, .cjs, .py, .go, .java, .rb, .php, .rs, .cs, .cpp, .cc, .c, .h, .hpp, .sh, .kt, .swift, .scala, .dart, .ex, .exs, .zig, .lua, .vue, .svelte, .astro, .sql, .prisma, .graphql, .proto`, -32602);
|
|
5970
6855
|
}
|
|
@@ -6043,18 +6928,36 @@ async function handleReadCode(params) {
|
|
|
6043
6928
|
filteredMatches = allMatches.filter((s) => s.kind === kind);
|
|
6044
6929
|
}
|
|
6045
6930
|
const primaryTarget = validatedTargets[0];
|
|
6046
|
-
const
|
|
6047
|
-
|
|
6931
|
+
const isBatched = validatedTargets.length > 1;
|
|
6932
|
+
let scoredMatches;
|
|
6933
|
+
if (isBatched) {
|
|
6934
|
+
const perTargetResults = [];
|
|
6935
|
+
const usedKeys = new Set;
|
|
6048
6936
|
for (const t of validatedTargets) {
|
|
6049
|
-
const
|
|
6050
|
-
|
|
6051
|
-
|
|
6937
|
+
const targetMatches = filteredMatches.map((symbol) => ({
|
|
6938
|
+
...symbol,
|
|
6939
|
+
confidence: computeConfidence(symbol, t, context_path)
|
|
6940
|
+
})).filter((s) => s.confidence >= clampedThreshold).sort((a, b) => b.confidence - a.confidence).slice(0, clampedMaxResults);
|
|
6941
|
+
for (const m of targetMatches) {
|
|
6942
|
+
const key = `${m.name}::${m.file}::${m.startLine}`;
|
|
6943
|
+
if (!usedKeys.has(key)) {
|
|
6944
|
+
usedKeys.add(key);
|
|
6945
|
+
perTargetResults.push(m);
|
|
6946
|
+
}
|
|
6947
|
+
}
|
|
6052
6948
|
}
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6949
|
+
scoredMatches = perTargetResults.sort((a, b) => b.confidence - a.confidence);
|
|
6950
|
+
} else {
|
|
6951
|
+
scoredMatches = filteredMatches.map((symbol) => {
|
|
6952
|
+
let bestConfidence = 0;
|
|
6953
|
+
for (const t of validatedTargets) {
|
|
6954
|
+
const conf = computeConfidence(symbol, t, context_path);
|
|
6955
|
+
if (conf > bestConfidence)
|
|
6956
|
+
bestConfidence = conf;
|
|
6957
|
+
}
|
|
6958
|
+
return { ...symbol, confidence: bestConfidence };
|
|
6959
|
+
}).filter((s) => s.confidence >= clampedThreshold).sort((a, b) => b.confidence - a.confidence).slice(0, clampedMaxResults);
|
|
6960
|
+
}
|
|
6058
6961
|
const totalFileTokens = estimateTotalFileTokens(filesToSearch);
|
|
6059
6962
|
if (scoredMatches.length === 0) {
|
|
6060
6963
|
const insufficientHint = buildInsufficientSourceHint(primaryTarget, inline_files, { tool: "read_code" });
|
|
@@ -6100,7 +7003,7 @@ async function handleReadCode(params) {
|
|
|
6100
7003
|
continue;
|
|
6101
7004
|
}
|
|
6102
7005
|
const fileContent = filesToSearch[symbol.file] ?? "";
|
|
6103
|
-
const extracted = extractSymbolContent(symbol, detail_level, fileContent);
|
|
7006
|
+
const extracted = extractSymbolContent(symbol, detail_level, fileContent, params.compact);
|
|
6104
7007
|
extracted.confidence = symbol.confidence;
|
|
6105
7008
|
if (params.max_lines && extracted.body) {
|
|
6106
7009
|
const bodyLines = extracted.body.split(`
|
|
@@ -6134,13 +7037,15 @@ async function handleReadCode(params) {
|
|
|
6134
7037
|
totalTokensUsed += extracted.token_estimate;
|
|
6135
7038
|
resultSymbols.push(extracted);
|
|
6136
7039
|
}
|
|
7040
|
+
const shouldIncludeUsages = include_usages || detail_level === "context";
|
|
7041
|
+
const shouldIncludeTests = include_tests || detail_level === "context";
|
|
6137
7042
|
for (const symbol of resultSymbols) {
|
|
6138
7043
|
if (symbol.already_returned_this_session)
|
|
6139
7044
|
continue;
|
|
6140
|
-
if (
|
|
7045
|
+
if (shouldIncludeUsages) {
|
|
6141
7046
|
symbol.usages = await findUsages(symbol.name, filesToSearch, 10);
|
|
6142
7047
|
}
|
|
6143
|
-
if (
|
|
7048
|
+
if (shouldIncludeTests) {
|
|
6144
7049
|
symbol.tests = await findTestFiles(symbol.name, symbol.file, filesToSearch);
|
|
6145
7050
|
}
|
|
6146
7051
|
}
|
|
@@ -6176,13 +7081,13 @@ async function handleRemoteRepo(target, url, options) {
|
|
|
6176
7081
|
];
|
|
6177
7082
|
let fileCount = 0;
|
|
6178
7083
|
let skippedCount = 0;
|
|
6179
|
-
const
|
|
6180
|
-
const
|
|
7084
|
+
const MAX_FILES2 = 200;
|
|
7085
|
+
const MAX_FILE_SIZE2 = 1048576;
|
|
6181
7086
|
for await (const filePath of glob.scan({
|
|
6182
7087
|
cwd: localPath,
|
|
6183
7088
|
absolute: true
|
|
6184
7089
|
})) {
|
|
6185
|
-
if (fileCount >=
|
|
7090
|
+
if (fileCount >= MAX_FILES2) {
|
|
6186
7091
|
skippedCount++;
|
|
6187
7092
|
continue;
|
|
6188
7093
|
}
|
|
@@ -6191,7 +7096,7 @@ async function handleRemoteRepo(target, url, options) {
|
|
|
6191
7096
|
continue;
|
|
6192
7097
|
try {
|
|
6193
7098
|
const stats = await stat(filePath);
|
|
6194
|
-
if (stats.size >
|
|
7099
|
+
if (stats.size > MAX_FILE_SIZE2)
|
|
6195
7100
|
continue;
|
|
6196
7101
|
const content = await Bun.file(filePath).text();
|
|
6197
7102
|
if (isBinaryContent(content))
|
|
@@ -6217,7 +7122,7 @@ async function handleRemoteRepo(target, url, options) {
|
|
|
6217
7122
|
session_id: options.session_id
|
|
6218
7123
|
});
|
|
6219
7124
|
if (skippedCount > 0 && !result.error_hint) {
|
|
6220
|
-
result.error_hint = `Warning: Scanned ${
|
|
7125
|
+
result.error_hint = `Warning: Scanned ${MAX_FILES2} of ${MAX_FILES2 + skippedCount} source files. ${skippedCount} files were skipped. If the symbol wasn't found, try using inline_files with the specific file content, or narrow with context_path.`;
|
|
6221
7126
|
}
|
|
6222
7127
|
return result;
|
|
6223
7128
|
});
|