@stupidloud/codegraph 0.7.20 → 0.9.5
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/README.md +127 -106
- package/dist/bin/codegraph.d.ts +4 -0
- package/dist/bin/codegraph.d.ts.map +1 -1
- package/dist/bin/codegraph.js +327 -8
- package/dist/bin/codegraph.js.map +1 -1
- package/dist/bin/node-version-check.d.ts +17 -0
- package/dist/bin/node-version-check.d.ts.map +1 -1
- package/dist/bin/node-version-check.js +37 -0
- package/dist/bin/node-version-check.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -11
- package/dist/config.js.map +1 -1
- package/dist/db/index.d.ts +30 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +75 -25
- package/dist/db/index.js.map +1 -1
- package/dist/db/queries.d.ts +16 -0
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +80 -27
- package/dist/db/queries.js.map +1 -1
- package/dist/db/sqlite-adapter.d.ts +17 -23
- package/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/dist/db/sqlite-adapter.js +51 -174
- package/dist/db/sqlite-adapter.js.map +1 -1
- package/dist/extraction/grammars.d.ts +7 -1
- package/dist/extraction/grammars.d.ts.map +1 -1
- package/dist/extraction/grammars.js +42 -2
- package/dist/extraction/grammars.js.map +1 -1
- package/dist/extraction/index.d.ts +9 -14
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +131 -124
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/index.d.ts.map +1 -1
- package/dist/extraction/languages/index.js +4 -0
- package/dist/extraction/languages/index.js.map +1 -1
- package/dist/extraction/languages/lua.d.ts +3 -0
- package/dist/extraction/languages/lua.d.ts.map +1 -0
- package/dist/extraction/languages/lua.js +150 -0
- package/dist/extraction/languages/lua.js.map +1 -0
- package/dist/extraction/languages/luau.d.ts +3 -0
- package/dist/extraction/languages/luau.d.ts.map +1 -0
- package/dist/extraction/languages/luau.js +37 -0
- package/dist/extraction/languages/luau.js.map +1 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +38 -0
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-luau.wasm +0 -0
- package/dist/extraction/wasm-runtime-flags.d.ts +38 -0
- package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -0
- package/dist/extraction/wasm-runtime-flags.js +105 -0
- package/dist/extraction/wasm-runtime-flags.js.map +1 -0
- package/dist/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +71 -36
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -18
- package/dist/index.js.map +1 -1
- package/dist/installer/config-writer.d.ts.map +1 -1
- package/dist/installer/config-writer.js +3 -1
- package/dist/installer/config-writer.js.map +1 -1
- package/dist/installer/index.d.ts +66 -2
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +195 -5
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/instructions-template.d.ts +2 -2
- package/dist/installer/instructions-template.d.ts.map +1 -1
- package/dist/installer/instructions-template.js +4 -2
- package/dist/installer/instructions-template.js.map +1 -1
- package/dist/installer/targets/claude.d.ts +26 -6
- package/dist/installer/targets/claude.d.ts.map +1 -1
- package/dist/installer/targets/claude.js +165 -10
- package/dist/installer/targets/claude.js.map +1 -1
- package/dist/installer/targets/cursor.d.ts.map +1 -1
- package/dist/installer/targets/cursor.js +57 -3
- package/dist/installer/targets/cursor.js.map +1 -1
- package/dist/installer/targets/hermes.d.ts +18 -0
- package/dist/installer/targets/hermes.d.ts.map +1 -0
- package/dist/installer/targets/hermes.js +305 -0
- package/dist/installer/targets/hermes.js.map +1 -0
- package/dist/installer/targets/registry.d.ts.map +1 -1
- package/dist/installer/targets/registry.js +2 -0
- package/dist/installer/targets/registry.js.map +1 -1
- package/dist/installer/targets/types.d.ts +1 -1
- package/dist/installer/targets/types.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +12 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +213 -18
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server-instructions.d.ts +1 -1
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +15 -0
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/tools.d.ts +25 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +221 -30
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/transport.d.ts +17 -0
- package/dist/mcp/transport.d.ts.map +1 -1
- package/dist/mcp/transport.js +63 -0
- package/dist/mcp/transport.js.map +1 -1
- package/dist/resolution/frameworks/drupal.d.ts +51 -0
- package/dist/resolution/frameworks/drupal.d.ts.map +1 -0
- package/dist/resolution/frameworks/drupal.js +335 -0
- package/dist/resolution/frameworks/drupal.js.map +1 -0
- package/dist/resolution/frameworks/index.d.ts +2 -0
- package/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/dist/resolution/frameworks/index.js +9 -1
- package/dist/resolution/frameworks/index.js.map +1 -1
- package/dist/resolution/frameworks/nestjs.d.ts +26 -0
- package/dist/resolution/frameworks/nestjs.d.ts.map +1 -0
- package/dist/resolution/frameworks/nestjs.js +374 -0
- package/dist/resolution/frameworks/nestjs.js.map +1 -0
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +40 -7
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/lru-cache.d.ts +24 -0
- package/dist/resolution/lru-cache.d.ts.map +1 -0
- package/dist/resolution/lru-cache.js +62 -0
- package/dist/resolution/lru-cache.js.map +1 -0
- package/dist/sync/git-hooks.d.ts +45 -0
- package/dist/sync/git-hooks.d.ts.map +1 -0
- package/dist/sync/git-hooks.js +223 -0
- package/dist/sync/git-hooks.js.map +1 -0
- package/dist/sync/index.d.ts +4 -0
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +12 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/watch-policy.d.ts +48 -0
- package/dist/sync/watch-policy.d.ts.map +1 -0
- package/dist/sync/watch-policy.js +124 -0
- package/dist/sync/watch-policy.js.map +1 -0
- package/dist/sync/watcher.d.ts +2 -4
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +14 -6
- package/dist/sync/watcher.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -1
- package/dist/utils.js +1 -1
- package/package.json +4 -4
- package/scripts/add-lang/bench.sh +60 -0
- package/scripts/add-lang/check-grammar.mjs +75 -0
- package/scripts/add-lang/dump-ast.mjs +103 -0
- package/scripts/add-lang/verify-extraction.mjs +70 -0
- package/scripts/agent-eval/audit.sh +68 -0
- package/scripts/agent-eval/itrun.sh +1 -1
- package/scripts/agent-eval/run-all.sh +67 -0
- package/scripts/build-bundle.sh +118 -0
- package/scripts/npm-shim.js +246 -0
- package/scripts/pack-npm.sh +95 -0
- package/scripts/patch-tree-sitter-dart.js +0 -112
- package/scripts/release.sh +0 -68
package/dist/mcp/tools.js
CHANGED
|
@@ -47,9 +47,22 @@ const fs_1 = require("fs");
|
|
|
47
47
|
const utils_1 = require("../utils");
|
|
48
48
|
const os_1 = require("os");
|
|
49
49
|
const path_1 = require("path");
|
|
50
|
-
const db_1 = require("../db");
|
|
51
50
|
/** Maximum output length to prevent context bloat (characters) */
|
|
52
51
|
const MAX_OUTPUT_LENGTH = 15000;
|
|
52
|
+
/**
|
|
53
|
+
* Maximum length for free-form string inputs (query, task, symbol).
|
|
54
|
+
* Bounds memory and CPU when a buggy or hostile MCP client sends a
|
|
55
|
+
* huge payload — without this an attacker could ship a 100MB string
|
|
56
|
+
* and force a full FTS5 scan / OOM the server. 10 000 characters is
|
|
57
|
+
* far beyond any realistic legitimate query.
|
|
58
|
+
*/
|
|
59
|
+
const MAX_INPUT_LENGTH = 10_000;
|
|
60
|
+
/**
|
|
61
|
+
* Maximum length for path-like string inputs (projectPath, path
|
|
62
|
+
* filter, glob pattern). Paths beyond a few thousand chars are
|
|
63
|
+
* never legitimate and signal abuse or a bug upstream.
|
|
64
|
+
*/
|
|
65
|
+
const MAX_PATH_LENGTH = 4_096;
|
|
53
66
|
/**
|
|
54
67
|
* Rust path roots that have no file-system equivalent — `crate` is the
|
|
55
68
|
* current crate, `super` is the parent module, `self` is the current
|
|
@@ -58,6 +71,15 @@ const MAX_OUTPUT_LENGTH = 15000;
|
|
|
58
71
|
* same as `configurator::stage_apply::run`.
|
|
59
72
|
*/
|
|
60
73
|
const RUST_PATH_PREFIXES = new Set(['crate', 'super', 'self']);
|
|
74
|
+
/**
|
|
75
|
+
* Node kinds that contain other symbols. For these, `codegraph_node` with
|
|
76
|
+
* `includeCode=true` returns a structural outline (member names + signatures
|
|
77
|
+
* + line numbers) instead of the full body, which for a large class is a
|
|
78
|
+
* multi-thousand-character wall of source that bloats the agent's context.
|
|
79
|
+
*/
|
|
80
|
+
const CONTAINER_NODE_KINDS = new Set([
|
|
81
|
+
'class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'namespace', 'module',
|
|
82
|
+
]);
|
|
61
83
|
/** Last `::` / `.` / `/`-separated segment of a qualified symbol. */
|
|
62
84
|
function lastQualifierPart(symbol) {
|
|
63
85
|
const parts = symbol.split(/::|[./]/).filter((p) => p.length > 0);
|
|
@@ -96,12 +118,12 @@ function getExploreOutputBudget(fileCount) {
|
|
|
96
118
|
}
|
|
97
119
|
if (fileCount < 5000) {
|
|
98
120
|
return {
|
|
99
|
-
maxOutputChars:
|
|
100
|
-
defaultMaxFiles:
|
|
101
|
-
maxCharsPerFile:
|
|
102
|
-
gapThreshold:
|
|
103
|
-
maxSymbolsInFileHeader:
|
|
104
|
-
maxEdgesPerRelationshipKind:
|
|
121
|
+
maxOutputChars: 13000,
|
|
122
|
+
defaultMaxFiles: 6,
|
|
123
|
+
maxCharsPerFile: 2500,
|
|
124
|
+
gapThreshold: 10,
|
|
125
|
+
maxSymbolsInFileHeader: 8,
|
|
126
|
+
maxEdgesPerRelationshipKind: 8,
|
|
105
127
|
includeRelationships: true,
|
|
106
128
|
includeAdditionalFiles: true,
|
|
107
129
|
includeCompletenessSignal: true,
|
|
@@ -168,15 +190,50 @@ function numberSourceLines(slice, firstLineNumber) {
|
|
|
168
190
|
/**
|
|
169
191
|
* Mark a Claude session as having consulted MCP tools.
|
|
170
192
|
* This enables Grep/Glob/Bash commands that would otherwise be blocked.
|
|
193
|
+
*
|
|
194
|
+
* Why the explicit openSync + O_NOFOLLOW dance instead of plain writeFileSync:
|
|
195
|
+
* tmpdir() is world-writable on Linux (mode 1777), so on a shared multi-user
|
|
196
|
+
* machine any other local user can pre-create `codegraph-consulted-<hash>` as
|
|
197
|
+
* a symlink pointing at a file the victim owns. The old `writeFileSync` would
|
|
198
|
+
* happily follow that link and overwrite the target's contents with the ISO
|
|
199
|
+
* timestamp string (CWE-59). The session-id hash provides the predictability
|
|
200
|
+
* gate, but it's defense-in-depth: if a session id ever surfaces in logs,
|
|
201
|
+
* argv, or telemetry the attack becomes trivial, and the right fix is to not
|
|
202
|
+
* follow links from /tmp paths in the first place.
|
|
171
203
|
*/
|
|
172
204
|
function markSessionConsulted(sessionId) {
|
|
173
205
|
try {
|
|
174
206
|
const hash = (0, crypto_1.createHash)('md5').update(sessionId).digest('hex').slice(0, 16);
|
|
175
207
|
const markerPath = (0, path_1.join)((0, os_1.tmpdir)(), `codegraph-consulted-${hash}`);
|
|
176
|
-
|
|
208
|
+
// Refuse to follow a pre-planted symlink at the marker path (CWE-59).
|
|
209
|
+
// O_NOFOLLOW (below) is the atomic, TOCTOU-free guard on POSIX, but it is
|
|
210
|
+
// `undefined` on Windows (libuv ignores it), so the bitwise-OR silently
|
|
211
|
+
// drops it and openSync would follow the link. This lstat check closes that
|
|
212
|
+
// gap cross-platform; ENOENT (path is free) falls through to create it.
|
|
213
|
+
try {
|
|
214
|
+
if ((0, fs_1.lstatSync)(markerPath).isSymbolicLink())
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// No existing entry (or stat failed) — nothing to refuse; proceed.
|
|
219
|
+
}
|
|
220
|
+
// O_NOFOLLOW makes openSync throw ELOOP if markerPath is already a symlink.
|
|
221
|
+
// O_CREAT + O_TRUNC keep the original "create-or-overwrite" semantics, and
|
|
222
|
+
// mode 0o600 prevents readback by other local users (the marker payload is
|
|
223
|
+
// benign, but narrowing the exposure costs nothing).
|
|
224
|
+
const flags = fs_1.constants.O_WRONLY | fs_1.constants.O_CREAT | fs_1.constants.O_TRUNC | fs_1.constants.O_NOFOLLOW;
|
|
225
|
+
const fd = (0, fs_1.openSync)(markerPath, flags, 0o600);
|
|
226
|
+
try {
|
|
227
|
+
(0, fs_1.writeSync)(fd, new Date().toISOString());
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
(0, fs_1.closeSync)(fd);
|
|
231
|
+
}
|
|
177
232
|
}
|
|
178
233
|
catch {
|
|
179
|
-
// Silently fail - don't break MCP on marker write failure
|
|
234
|
+
// Silently fail - don't break MCP on marker write failure. ELOOP from a
|
|
235
|
+
// planted symlink lands here too, which is the intended behavior: refuse
|
|
236
|
+
// to write rather than overwrite an attacker-chosen target.
|
|
180
237
|
}
|
|
181
238
|
}
|
|
182
239
|
/**
|
|
@@ -222,7 +279,7 @@ exports.tools = [
|
|
|
222
279
|
},
|
|
223
280
|
{
|
|
224
281
|
name: 'codegraph_context',
|
|
225
|
-
description: 'PRIMARY TOOL
|
|
282
|
+
description: 'PRIMARY TOOL — call this FIRST for any "how does X work", architecture, feature, or bug-context question. Composes search + node + callers + callees and returns entry points, related symbols, and key code in ONE call — usually enough to answer with no further search/Read/Grep. Prefer this over chaining codegraph_search + codegraph_node, and over codegraph_explore. NOTE: provides CODE context, not product requirements; for new features still clarify UX/edge cases with the user.',
|
|
226
283
|
inputSchema: {
|
|
227
284
|
type: 'object',
|
|
228
285
|
properties: {
|
|
@@ -307,7 +364,7 @@ exports.tools = [
|
|
|
307
364
|
},
|
|
308
365
|
{
|
|
309
366
|
name: 'codegraph_node',
|
|
310
|
-
description: 'Get detailed
|
|
367
|
+
description: 'Get detailed info about ONE symbol (location, signature, docstring). Pass includeCode=true for source: a function/method returns its body; a class/interface/struct/enum returns a compact member OUTLINE (fields + method signatures + line numbers), not every method body — Read or codegraph_node a specific member for its body. Keep includeCode=false to minimize context. For SEVERAL related symbols, make ONE codegraph_explore (or codegraph_context) call instead of many node calls — repeated node calls each re-read the whole context and cost far more.',
|
|
311
368
|
inputSchema: {
|
|
312
369
|
type: 'object',
|
|
313
370
|
properties: {
|
|
@@ -327,7 +384,7 @@ exports.tools = [
|
|
|
327
384
|
},
|
|
328
385
|
{
|
|
329
386
|
name: 'codegraph_explore',
|
|
330
|
-
description: 'PRIMARY TOOL for understanding questions
|
|
387
|
+
description: 'PRIMARY TOOL for understanding questions and inspecting SEVERAL related symbols in ONE capped call. Returns relevant source grouped by file (contiguous, line-numbered sections) plus a relationship map. Use it after codegraph_context when you need actual source for the surfaced symbols, or query it directly with specific symbol/file/code terms. Strongly prefer it over many codegraph_node or Read calls. For a pinpoint "where is X defined" lookup, use codegraph_search instead.',
|
|
331
388
|
inputSchema: {
|
|
332
389
|
type: 'object',
|
|
333
390
|
properties: {
|
|
@@ -399,6 +456,9 @@ class ToolHandler {
|
|
|
399
456
|
cg;
|
|
400
457
|
// Cache of opened CodeGraph instances for cross-project queries
|
|
401
458
|
projectCache = new Map();
|
|
459
|
+
// The directory the server last searched for a default project. Surfaced in
|
|
460
|
+
// the "not initialized" error so users can see why detection missed.
|
|
461
|
+
defaultProjectHint = null;
|
|
402
462
|
constructor(cg) {
|
|
403
463
|
this.cg = cg;
|
|
404
464
|
}
|
|
@@ -408,6 +468,13 @@ class ToolHandler {
|
|
|
408
468
|
setDefaultCodeGraph(cg) {
|
|
409
469
|
this.cg = cg;
|
|
410
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Record the directory the server tried to resolve the default project from.
|
|
473
|
+
* Used only to make the "no default project" error actionable.
|
|
474
|
+
*/
|
|
475
|
+
setDefaultProjectHint(searchedPath) {
|
|
476
|
+
this.defaultProjectHint = searchedPath;
|
|
477
|
+
}
|
|
411
478
|
/**
|
|
412
479
|
* Whether a default CodeGraph instance is available
|
|
413
480
|
*/
|
|
@@ -451,7 +518,14 @@ class ToolHandler {
|
|
|
451
518
|
getCodeGraph(projectPath) {
|
|
452
519
|
if (!projectPath) {
|
|
453
520
|
if (!this.cg) {
|
|
454
|
-
|
|
521
|
+
const searched = this.defaultProjectHint ?? process.cwd();
|
|
522
|
+
throw new Error('No CodeGraph project is loaded for this session.\n' +
|
|
523
|
+
`Searched for a .codegraph/ directory starting from: ${searched}\n` +
|
|
524
|
+
'The index is likely fine — this is a working-directory detection issue: ' +
|
|
525
|
+
"the MCP client launched the server outside your project and didn't report the " +
|
|
526
|
+
'workspace root. Fix it either way:\n' +
|
|
527
|
+
' • Pass projectPath to the tool call, e.g. projectPath: "/absolute/path/to/your/project"\n' +
|
|
528
|
+
' • Or add --path to the server\'s MCP config args: ["serve", "--mcp", "--path", "/absolute/path/to/your/project"]');
|
|
455
529
|
}
|
|
456
530
|
return this.cg;
|
|
457
531
|
}
|
|
@@ -459,11 +533,32 @@ class ToolHandler {
|
|
|
459
533
|
if (this.projectCache.has(projectPath)) {
|
|
460
534
|
return this.projectCache.get(projectPath);
|
|
461
535
|
}
|
|
536
|
+
// Reject sensitive system directories before opening. Only validate a
|
|
537
|
+
// path that actually exists — a nested or not-yet-created sub-path of a
|
|
538
|
+
// real project must still be allowed to resolve UP to its .codegraph/
|
|
539
|
+
// root below (issue #238), so we don't run the existence-checking
|
|
540
|
+
// validator on paths that are meant to walk up.
|
|
541
|
+
if ((0, fs_1.existsSync)(projectPath)) {
|
|
542
|
+
const pathError = (0, utils_1.validateProjectPath)(projectPath);
|
|
543
|
+
if (pathError) {
|
|
544
|
+
throw new Error(pathError);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
462
547
|
// Walk up parent directories to find nearest .codegraph/
|
|
463
548
|
const resolvedRoot = (0, index_1.findNearestCodeGraphRoot)(projectPath);
|
|
464
549
|
if (!resolvedRoot) {
|
|
465
550
|
throw new Error(`CodeGraph not initialized in ${projectPath}. Run 'codegraph init' in that project first.`);
|
|
466
551
|
}
|
|
552
|
+
// If the path resolves to the default project, reuse the already-open
|
|
553
|
+
// default instance rather than opening a SECOND connection to the same DB.
|
|
554
|
+
// A duplicate connection serializes reads against the watcher's auto-sync
|
|
555
|
+
// writes; on the wasm backend (no WAL) that surfaces as intermittent
|
|
556
|
+
// "database is locked" on concurrent tool calls. See issue #238. Deliberately
|
|
557
|
+
// not cached under projectPath — the server owns and closes the default
|
|
558
|
+
// instance, so routing it through projectCache.closeAll() would double-close it.
|
|
559
|
+
if (this.cg && this.cg.getProjectRoot() === resolvedRoot) {
|
|
560
|
+
return this.cg;
|
|
561
|
+
}
|
|
467
562
|
// Check if we already have this resolved root cached (different path, same project)
|
|
468
563
|
if (this.projectCache.has(resolvedRoot)) {
|
|
469
564
|
const cg = this.projectCache.get(resolvedRoot);
|
|
@@ -489,12 +584,35 @@ class ToolHandler {
|
|
|
489
584
|
this.projectCache.clear();
|
|
490
585
|
}
|
|
491
586
|
/**
|
|
492
|
-
* Validate that a value is a non-empty string
|
|
587
|
+
* Validate that a value is a non-empty string within length bounds.
|
|
588
|
+
*
|
|
589
|
+
* The `maxLength` cap protects against MCP clients that ship huge
|
|
590
|
+
* payloads (10MB+ query strings either by accident or maliciously).
|
|
591
|
+
* Without this, a single oversized input can pin the FTS5 index or
|
|
592
|
+
* exhaust memory before any real work runs.
|
|
493
593
|
*/
|
|
494
|
-
validateString(value, name) {
|
|
594
|
+
validateString(value, name, maxLength = MAX_INPUT_LENGTH) {
|
|
495
595
|
if (typeof value !== 'string' || value.length === 0) {
|
|
496
596
|
return this.errorResult(`${name} must be a non-empty string`);
|
|
497
597
|
}
|
|
598
|
+
if (value.length > maxLength) {
|
|
599
|
+
return this.errorResult(`${name} exceeds maximum length of ${maxLength} characters (got ${value.length})`);
|
|
600
|
+
}
|
|
601
|
+
return value;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Validate an optional path-like string input. Returns the value if
|
|
605
|
+
* valid (or undefined), or a ToolResult with the error.
|
|
606
|
+
*/
|
|
607
|
+
validateOptionalPath(value, name) {
|
|
608
|
+
if (value === undefined || value === null)
|
|
609
|
+
return undefined;
|
|
610
|
+
if (typeof value !== 'string') {
|
|
611
|
+
return this.errorResult(`${name} must be a string`);
|
|
612
|
+
}
|
|
613
|
+
if (value.length > MAX_PATH_LENGTH) {
|
|
614
|
+
return this.errorResult(`${name} exceeds maximum length of ${MAX_PATH_LENGTH} characters (got ${value.length})`);
|
|
615
|
+
}
|
|
498
616
|
return value;
|
|
499
617
|
}
|
|
500
618
|
/**
|
|
@@ -502,6 +620,26 @@ class ToolHandler {
|
|
|
502
620
|
*/
|
|
503
621
|
async execute(toolName, args) {
|
|
504
622
|
try {
|
|
623
|
+
// Cross-cutting input validation. All tools accept an optional
|
|
624
|
+
// `projectPath` and most accept either `query`, `task`, or
|
|
625
|
+
// `symbol` — bound their lengths centrally so individual handlers
|
|
626
|
+
// can stay focused on tool-specific logic.
|
|
627
|
+
const pathCheck = this.validateOptionalPath(args.projectPath, 'projectPath');
|
|
628
|
+
if (typeof pathCheck === 'object' && pathCheck !== undefined) {
|
|
629
|
+
return pathCheck;
|
|
630
|
+
}
|
|
631
|
+
// The `path` and `pattern` properties used by codegraph_files are
|
|
632
|
+
// also path-shaped — apply the same cap.
|
|
633
|
+
if (args.path !== undefined) {
|
|
634
|
+
const check = this.validateOptionalPath(args.path, 'path');
|
|
635
|
+
if (typeof check === 'object' && check !== undefined)
|
|
636
|
+
return check;
|
|
637
|
+
}
|
|
638
|
+
if (args.pattern !== undefined) {
|
|
639
|
+
const check = this.validateOptionalPath(args.pattern, 'pattern');
|
|
640
|
+
if (typeof check === 'object' && check !== undefined)
|
|
641
|
+
return check;
|
|
642
|
+
}
|
|
505
643
|
switch (toolName) {
|
|
506
644
|
case 'codegraph_search':
|
|
507
645
|
return await this.handleSearch(args);
|
|
@@ -577,10 +715,10 @@ class ToolHandler {
|
|
|
577
715
|
: '';
|
|
578
716
|
// buildContext returns string when format is 'markdown'
|
|
579
717
|
if (typeof context === 'string') {
|
|
580
|
-
return this.textResult(context + reminder);
|
|
718
|
+
return this.textResult(this.truncateOutput(context + reminder));
|
|
581
719
|
}
|
|
582
720
|
// If it returns TaskContext, format it
|
|
583
|
-
return this.textResult(this.formatTaskContext(context) + reminder);
|
|
721
|
+
return this.textResult(this.truncateOutput(this.formatTaskContext(context) + reminder));
|
|
584
722
|
}
|
|
585
723
|
/**
|
|
586
724
|
* Heuristic to detect if a query looks like a feature request
|
|
@@ -1123,7 +1261,20 @@ class ToolHandler {
|
|
|
1123
1261
|
// Stats unavailable — skip budget note
|
|
1124
1262
|
}
|
|
1125
1263
|
}
|
|
1126
|
-
|
|
1264
|
+
// Hard-cap to the adaptive budget. The per-file loop bounds the source
|
|
1265
|
+
// sections, but the relationship map, additional-files list, and
|
|
1266
|
+
// completeness/budget notes can still push the assembled output past
|
|
1267
|
+
// maxOutputChars (observed 30k against a 28k tier cap). A fat explore
|
|
1268
|
+
// payload persists in the agent's context and is re-read as cache-input
|
|
1269
|
+
// on every subsequent turn, so the overrun is paid many times over.
|
|
1270
|
+
const output = lines.join('\n');
|
|
1271
|
+
if (output.length > budget.maxOutputChars) {
|
|
1272
|
+
const cut = output.slice(0, budget.maxOutputChars);
|
|
1273
|
+
const lastNewline = cut.lastIndexOf('\n');
|
|
1274
|
+
const safe = lastNewline > budget.maxOutputChars * 0.8 ? cut.slice(0, lastNewline) : cut;
|
|
1275
|
+
return this.textResult(safe + '\n\n... (explore output truncated to budget — use codegraph_node or Read for more)');
|
|
1276
|
+
}
|
|
1277
|
+
return this.textResult(output);
|
|
1127
1278
|
}
|
|
1128
1279
|
/**
|
|
1129
1280
|
* Handle codegraph_node
|
|
@@ -1140,10 +1291,22 @@ class ToolHandler {
|
|
|
1140
1291
|
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
|
1141
1292
|
}
|
|
1142
1293
|
let code = null;
|
|
1294
|
+
let outline = null;
|
|
1143
1295
|
if (includeCode) {
|
|
1144
|
-
|
|
1296
|
+
// For container symbols (class/interface/struct/…), the full body is the
|
|
1297
|
+
// sum of every method body — a wall of source (e.g. a 10k-char class)
|
|
1298
|
+
// that bloats context and is rarely needed in full. Return a structural
|
|
1299
|
+
// outline (members + signatures + line numbers) instead; the agent can
|
|
1300
|
+
// Read or codegraph_node a specific method for its body. Leaf symbols
|
|
1301
|
+
// (function/method/etc.) return their full body as before.
|
|
1302
|
+
if (CONTAINER_NODE_KINDS.has(match.node.kind)) {
|
|
1303
|
+
outline = this.buildContainerOutline(cg, match.node);
|
|
1304
|
+
}
|
|
1305
|
+
if (!outline) {
|
|
1306
|
+
code = await cg.getCode(match.node.id);
|
|
1307
|
+
}
|
|
1145
1308
|
}
|
|
1146
|
-
const formatted = this.formatNodeDetails(match.node, code) + match.note;
|
|
1309
|
+
const formatted = this.formatNodeDetails(match.node, code, outline) + match.note;
|
|
1147
1310
|
return this.textResult(this.truncateOutput(formatted));
|
|
1148
1311
|
}
|
|
1149
1312
|
/**
|
|
@@ -1160,16 +1323,20 @@ class ToolHandler {
|
|
|
1160
1323
|
`**Total edges:** ${stats.edgeCount}`,
|
|
1161
1324
|
`**Database size:** ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`,
|
|
1162
1325
|
];
|
|
1163
|
-
// Surface the active SQLite backend
|
|
1164
|
-
//
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1326
|
+
// Surface the active SQLite backend (node:sqlite, Node's built-in real
|
|
1327
|
+
// SQLite — full WAL + FTS5, no native build).
|
|
1328
|
+
lines.push(`**Backend:** node:sqlite (Node built-in) — full WAL + FTS5`);
|
|
1329
|
+
// Effective journal mode. 'wal' ⇒ concurrent reads never block on a writer;
|
|
1330
|
+
// anything else ⇒ they can ("database is locked"). node:sqlite supports WAL
|
|
1331
|
+
// everywhere, so a non-wal mode means the filesystem can't (network/
|
|
1332
|
+
// virtualized mounts, WSL2 /mnt). See issue #238.
|
|
1333
|
+
const journalMode = cg.getJournalMode();
|
|
1334
|
+
if (journalMode === 'wal') {
|
|
1335
|
+
lines.push(`**Journal mode:** wal (concurrent reads safe)`);
|
|
1169
1336
|
}
|
|
1170
1337
|
else {
|
|
1171
|
-
lines.push(`**
|
|
1172
|
-
`
|
|
1338
|
+
lines.push(`**Journal mode:** ⚠ ${journalMode || 'unknown'} — WAL not active, so reads ` +
|
|
1339
|
+
`can block on a concurrent write (WAL appears unsupported on this filesystem)`);
|
|
1173
1340
|
}
|
|
1174
1341
|
lines.push('', '### Nodes by Kind:');
|
|
1175
1342
|
for (const [kind, count] of Object.entries(stats.nodesByKind)) {
|
|
@@ -1520,7 +1687,28 @@ class ToolHandler {
|
|
|
1520
1687
|
}
|
|
1521
1688
|
return lines.join('\n');
|
|
1522
1689
|
}
|
|
1523
|
-
|
|
1690
|
+
/**
|
|
1691
|
+
* Build a compact structural outline of a container symbol from its
|
|
1692
|
+
* indexed children (methods, fields, properties, …) — name, kind,
|
|
1693
|
+
* line number, and signature — so the agent gets the shape of a class
|
|
1694
|
+
* without the full source of every method. Returns '' when the container
|
|
1695
|
+
* has no indexed children, so the caller can fall back to full source.
|
|
1696
|
+
*/
|
|
1697
|
+
buildContainerOutline(cg, node) {
|
|
1698
|
+
const children = cg.getChildren(node.id)
|
|
1699
|
+
.filter(c => c.kind !== 'import' && c.kind !== 'export')
|
|
1700
|
+
.sort((a, b) => (a.startLine ?? 0) - (b.startLine ?? 0));
|
|
1701
|
+
if (children.length === 0)
|
|
1702
|
+
return '';
|
|
1703
|
+
const lines = [`**Members (${children.length}):**`, ''];
|
|
1704
|
+
for (const c of children) {
|
|
1705
|
+
const loc = c.startLine ? `:${c.startLine}` : '';
|
|
1706
|
+
const sig = c.signature ? ` — \`${c.signature}\`` : '';
|
|
1707
|
+
lines.push(`- ${c.name} (${c.kind})${loc}${sig}`);
|
|
1708
|
+
}
|
|
1709
|
+
return lines.join('\n');
|
|
1710
|
+
}
|
|
1711
|
+
formatNodeDetails(node, code, outline) {
|
|
1524
1712
|
const location = node.startLine ? `:${node.startLine}` : '';
|
|
1525
1713
|
const lines = [
|
|
1526
1714
|
`## ${node.name} (${node.kind})`,
|
|
@@ -1534,7 +1722,10 @@ class ToolHandler {
|
|
|
1534
1722
|
if (node.docstring && node.docstring.length < 200) {
|
|
1535
1723
|
lines.push('', node.docstring);
|
|
1536
1724
|
}
|
|
1537
|
-
if (
|
|
1725
|
+
if (outline) {
|
|
1726
|
+
lines.push('', outline, '', `> Structural outline only. Read \`${node.filePath}\` or call codegraph_node on a specific member for its body.`);
|
|
1727
|
+
}
|
|
1728
|
+
else if (code) {
|
|
1538
1729
|
lines.push('', '```' + node.language, code, '```');
|
|
1539
1730
|
}
|
|
1540
1731
|
return lines.join('\n');
|