@colbymchenry/codegraph-darwin-x64 0.9.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dist/bin/codegraph.d.ts +1 -0
- package/lib/dist/bin/codegraph.d.ts.map +1 -1
- package/lib/dist/bin/codegraph.js +246 -3
- package/lib/dist/bin/codegraph.js.map +1 -1
- package/lib/dist/context/index.d.ts.map +1 -1
- package/lib/dist/context/index.js +7 -0
- package/lib/dist/context/index.js.map +1 -1
- package/lib/dist/db/index.d.ts.map +1 -1
- package/lib/dist/db/index.js +2 -1
- package/lib/dist/db/index.js.map +1 -1
- package/lib/dist/db/migrations.d.ts +1 -1
- package/lib/dist/db/migrations.d.ts.map +1 -1
- package/lib/dist/db/migrations.js +10 -1
- package/lib/dist/db/migrations.js.map +1 -1
- package/lib/dist/db/queries.d.ts +43 -0
- package/lib/dist/db/queries.d.ts.map +1 -1
- package/lib/dist/db/queries.js +103 -7
- package/lib/dist/db/queries.js.map +1 -1
- package/lib/dist/db/schema.sql +1 -0
- package/lib/dist/db/sqlite-adapter.d.ts +7 -0
- package/lib/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/lib/dist/db/sqlite-adapter.js +3 -0
- package/lib/dist/db/sqlite-adapter.js.map +1 -1
- package/lib/dist/directory.d.ts +34 -2
- package/lib/dist/directory.d.ts.map +1 -1
- package/lib/dist/directory.js +129 -35
- package/lib/dist/directory.js.map +1 -1
- package/lib/dist/extraction/astro-extractor.d.ts +79 -0
- package/lib/dist/extraction/astro-extractor.d.ts.map +1 -0
- package/lib/dist/extraction/astro-extractor.js +320 -0
- package/lib/dist/extraction/astro-extractor.js.map +1 -0
- package/lib/dist/extraction/extraction-version.d.ts +25 -0
- package/lib/dist/extraction/extraction-version.d.ts.map +1 -0
- package/lib/dist/extraction/extraction-version.js +28 -0
- package/lib/dist/extraction/extraction-version.js.map +1 -0
- package/lib/dist/extraction/function-ref.d.ts +118 -0
- package/lib/dist/extraction/function-ref.d.ts.map +1 -0
- package/lib/dist/extraction/function-ref.js +727 -0
- package/lib/dist/extraction/function-ref.js.map +1 -0
- package/lib/dist/extraction/generated-detection.d.ts.map +1 -1
- package/lib/dist/extraction/generated-detection.js +3 -0
- package/lib/dist/extraction/generated-detection.js.map +1 -1
- package/lib/dist/extraction/grammars.d.ts +7 -1
- package/lib/dist/extraction/grammars.d.ts.map +1 -1
- package/lib/dist/extraction/grammars.js +52 -4
- package/lib/dist/extraction/grammars.js.map +1 -1
- package/lib/dist/extraction/index.d.ts +34 -0
- package/lib/dist/extraction/index.d.ts.map +1 -1
- package/lib/dist/extraction/index.js +346 -62
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/extraction/languages/c-cpp.d.ts +8 -0
- package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/lib/dist/extraction/languages/c-cpp.js +87 -28
- package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
- package/lib/dist/extraction/languages/csharp.d.ts +22 -0
- package/lib/dist/extraction/languages/csharp.d.ts.map +1 -1
- package/lib/dist/extraction/languages/csharp.js +84 -2
- package/lib/dist/extraction/languages/csharp.js.map +1 -1
- package/lib/dist/extraction/languages/dart.d.ts.map +1 -1
- package/lib/dist/extraction/languages/dart.js +161 -1
- package/lib/dist/extraction/languages/dart.js.map +1 -1
- package/lib/dist/extraction/languages/go.d.ts.map +1 -1
- package/lib/dist/extraction/languages/go.js +43 -2
- package/lib/dist/extraction/languages/go.js.map +1 -1
- package/lib/dist/extraction/languages/index.d.ts.map +1 -1
- package/lib/dist/extraction/languages/index.js +2 -0
- package/lib/dist/extraction/languages/index.js.map +1 -1
- package/lib/dist/extraction/languages/java.d.ts.map +1 -1
- package/lib/dist/extraction/languages/java.js +42 -1
- package/lib/dist/extraction/languages/java.js.map +1 -1
- package/lib/dist/extraction/languages/javascript.d.ts.map +1 -1
- package/lib/dist/extraction/languages/javascript.js +16 -0
- package/lib/dist/extraction/languages/javascript.js.map +1 -1
- package/lib/dist/extraction/languages/kotlin.d.ts.map +1 -1
- package/lib/dist/extraction/languages/kotlin.js +69 -0
- package/lib/dist/extraction/languages/kotlin.js.map +1 -1
- package/lib/dist/extraction/languages/objc.d.ts.map +1 -1
- package/lib/dist/extraction/languages/objc.js +42 -0
- package/lib/dist/extraction/languages/objc.js.map +1 -1
- package/lib/dist/extraction/languages/pascal.d.ts.map +1 -1
- package/lib/dist/extraction/languages/pascal.js +11 -0
- package/lib/dist/extraction/languages/pascal.js.map +1 -1
- package/lib/dist/extraction/languages/php.d.ts.map +1 -1
- package/lib/dist/extraction/languages/php.js +90 -1
- package/lib/dist/extraction/languages/php.js.map +1 -1
- package/lib/dist/extraction/languages/r.d.ts +3 -0
- package/lib/dist/extraction/languages/r.d.ts.map +1 -0
- package/lib/dist/extraction/languages/r.js +314 -0
- package/lib/dist/extraction/languages/r.js.map +1 -0
- package/lib/dist/extraction/languages/ruby.d.ts.map +1 -1
- package/lib/dist/extraction/languages/ruby.js +35 -0
- package/lib/dist/extraction/languages/ruby.js.map +1 -1
- package/lib/dist/extraction/languages/rust.d.ts.map +1 -1
- package/lib/dist/extraction/languages/rust.js +35 -2
- package/lib/dist/extraction/languages/rust.js.map +1 -1
- package/lib/dist/extraction/languages/scala.d.ts.map +1 -1
- package/lib/dist/extraction/languages/scala.js +61 -1
- package/lib/dist/extraction/languages/scala.js.map +1 -1
- package/lib/dist/extraction/languages/swift.d.ts.map +1 -1
- package/lib/dist/extraction/languages/swift.js +61 -0
- package/lib/dist/extraction/languages/swift.js.map +1 -1
- package/lib/dist/extraction/languages/typescript.d.ts +13 -0
- package/lib/dist/extraction/languages/typescript.d.ts.map +1 -1
- package/lib/dist/extraction/languages/typescript.js +38 -0
- package/lib/dist/extraction/languages/typescript.js.map +1 -1
- package/lib/dist/extraction/liquid-extractor.d.ts +7 -0
- package/lib/dist/extraction/liquid-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/liquid-extractor.js +53 -9
- package/lib/dist/extraction/liquid-extractor.js.map +1 -1
- package/lib/dist/extraction/razor-extractor.d.ts +42 -0
- package/lib/dist/extraction/razor-extractor.d.ts.map +1 -0
- package/lib/dist/extraction/razor-extractor.js +285 -0
- package/lib/dist/extraction/razor-extractor.js.map +1 -0
- package/lib/dist/extraction/svelte-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/svelte-extractor.js +6 -3
- package/lib/dist/extraction/svelte-extractor.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter-helpers.js +59 -10
- package/lib/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-types.d.ts +33 -0
- package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.d.ts +211 -0
- package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.js +1681 -49
- package/lib/dist/extraction/tree-sitter.js.map +1 -1
- package/lib/dist/extraction/vue-extractor.d.ts +15 -0
- package/lib/dist/extraction/vue-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/vue-extractor.js +94 -3
- package/lib/dist/extraction/vue-extractor.js.map +1 -1
- package/lib/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-r.wasm +0 -0
- package/lib/dist/graph/queries.d.ts.map +1 -1
- package/lib/dist/graph/queries.js +13 -40
- package/lib/dist/graph/queries.js.map +1 -1
- package/lib/dist/graph/traversal.d.ts.map +1 -1
- package/lib/dist/graph/traversal.js +16 -4
- package/lib/dist/graph/traversal.js.map +1 -1
- package/lib/dist/index.d.ts +34 -2
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +90 -8
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +52 -2
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/installer/instructions-template.d.ts +34 -11
- package/lib/dist/installer/instructions-template.d.ts.map +1 -1
- package/lib/dist/installer/instructions-template.js +44 -12
- package/lib/dist/installer/instructions-template.js.map +1 -1
- package/lib/dist/installer/targets/claude.d.ts.map +1 -1
- package/lib/dist/installer/targets/claude.js +6 -10
- package/lib/dist/installer/targets/claude.js.map +1 -1
- package/lib/dist/installer/targets/codex.js +4 -6
- package/lib/dist/installer/targets/codex.js.map +1 -1
- package/lib/dist/installer/targets/gemini.js +4 -6
- package/lib/dist/installer/targets/gemini.js.map +1 -1
- package/lib/dist/installer/targets/opencode.d.ts +9 -1
- package/lib/dist/installer/targets/opencode.d.ts.map +1 -1
- package/lib/dist/installer/targets/opencode.js +91 -40
- package/lib/dist/installer/targets/opencode.js.map +1 -1
- package/lib/dist/installer/targets/shared.d.ts +14 -0
- package/lib/dist/installer/targets/shared.d.ts.map +1 -1
- package/lib/dist/installer/targets/shared.js +16 -0
- package/lib/dist/installer/targets/shared.js.map +1 -1
- package/lib/dist/mcp/daemon.d.ts +60 -1
- package/lib/dist/mcp/daemon.d.ts.map +1 -1
- package/lib/dist/mcp/daemon.js +221 -8
- package/lib/dist/mcp/daemon.js.map +1 -1
- package/lib/dist/mcp/dynamic-boundaries.d.ts +41 -0
- package/lib/dist/mcp/dynamic-boundaries.d.ts.map +1 -0
- package/lib/dist/mcp/dynamic-boundaries.js +359 -0
- package/lib/dist/mcp/dynamic-boundaries.js.map +1 -0
- package/lib/dist/mcp/index.d.ts.map +1 -1
- package/lib/dist/mcp/index.js +18 -9
- package/lib/dist/mcp/index.js.map +1 -1
- package/lib/dist/mcp/ppid-watchdog.d.ts +44 -0
- package/lib/dist/mcp/ppid-watchdog.d.ts.map +1 -0
- package/lib/dist/mcp/ppid-watchdog.js +27 -0
- package/lib/dist/mcp/ppid-watchdog.js.map +1 -0
- package/lib/dist/mcp/proxy.d.ts +6 -0
- package/lib/dist/mcp/proxy.d.ts.map +1 -1
- package/lib/dist/mcp/proxy.js +153 -24
- package/lib/dist/mcp/proxy.js.map +1 -1
- package/lib/dist/mcp/server-instructions.d.ts +12 -1
- package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
- package/lib/dist/mcp/server-instructions.js +43 -16
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/session.d.ts +2 -0
- package/lib/dist/mcp/session.d.ts.map +1 -1
- package/lib/dist/mcp/session.js +49 -2
- package/lib/dist/mcp/session.js.map +1 -1
- package/lib/dist/mcp/stdin-teardown.d.ts +27 -0
- package/lib/dist/mcp/stdin-teardown.d.ts.map +1 -0
- package/lib/dist/mcp/stdin-teardown.js +49 -0
- package/lib/dist/mcp/stdin-teardown.js.map +1 -0
- package/lib/dist/mcp/tools.d.ts +71 -0
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +703 -85
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/mcp/transport.d.ts.map +1 -1
- package/lib/dist/mcp/transport.js +18 -2
- package/lib/dist/mcp/transport.js.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.d.ts +3 -3
- package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.js +549 -21
- package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
- package/lib/dist/resolution/frameworks/astro.d.ts +9 -0
- package/lib/dist/resolution/frameworks/astro.d.ts.map +1 -0
- package/lib/dist/resolution/frameworks/astro.js +169 -0
- package/lib/dist/resolution/frameworks/astro.js.map +1 -0
- package/lib/dist/resolution/frameworks/expo-modules.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/expo-modules.js +6 -1
- package/lib/dist/resolution/frameworks/expo-modules.js.map +1 -1
- package/lib/dist/resolution/frameworks/index.d.ts +1 -0
- package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/index.js +5 -1
- package/lib/dist/resolution/frameworks/index.js.map +1 -1
- package/lib/dist/resolution/frameworks/java.js +6 -1
- package/lib/dist/resolution/frameworks/java.js.map +1 -1
- package/lib/dist/resolution/frameworks/python.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/python.js +7 -3
- package/lib/dist/resolution/frameworks/python.js.map +1 -1
- package/lib/dist/resolution/frameworks/react-native.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/react-native.js +53 -3
- package/lib/dist/resolution/frameworks/react-native.js.map +1 -1
- package/lib/dist/resolution/frameworks/react.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/react.js +15 -3
- package/lib/dist/resolution/frameworks/react.js.map +1 -1
- package/lib/dist/resolution/frameworks/svelte.js +5 -1
- package/lib/dist/resolution/frameworks/svelte.js.map +1 -1
- package/lib/dist/resolution/frameworks/vue.js +24 -27
- package/lib/dist/resolution/frameworks/vue.js.map +1 -1
- package/lib/dist/resolution/import-resolver.d.ts +10 -0
- package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/import-resolver.js +564 -2
- package/lib/dist/resolution/import-resolver.js.map +1 -1
- package/lib/dist/resolution/index.d.ts +80 -0
- package/lib/dist/resolution/index.d.ts.map +1 -1
- package/lib/dist/resolution/index.js +457 -7
- package/lib/dist/resolution/index.js.map +1 -1
- package/lib/dist/resolution/name-matcher.d.ts +61 -0
- package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
- package/lib/dist/resolution/name-matcher.js +590 -14
- package/lib/dist/resolution/name-matcher.js.map +1 -1
- package/lib/dist/resolution/types.d.ts +27 -3
- package/lib/dist/resolution/types.d.ts.map +1 -1
- package/lib/dist/resolution/workspace-packages.d.ts +48 -0
- package/lib/dist/resolution/workspace-packages.d.ts.map +1 -0
- package/lib/dist/resolution/workspace-packages.js +208 -0
- package/lib/dist/resolution/workspace-packages.js.map +1 -0
- package/lib/dist/search/query-utils.d.ts +17 -1
- package/lib/dist/search/query-utils.d.ts.map +1 -1
- package/lib/dist/search/query-utils.js +79 -10
- package/lib/dist/search/query-utils.js.map +1 -1
- package/lib/dist/sync/watcher.d.ts +124 -32
- package/lib/dist/sync/watcher.d.ts.map +1 -1
- package/lib/dist/sync/watcher.js +326 -111
- package/lib/dist/sync/watcher.js.map +1 -1
- package/lib/dist/telemetry/index.d.ts +146 -0
- package/lib/dist/telemetry/index.d.ts.map +1 -0
- package/lib/dist/telemetry/index.js +544 -0
- package/lib/dist/telemetry/index.js.map +1 -0
- package/lib/dist/types.d.ts +17 -2
- package/lib/dist/types.d.ts.map +1 -1
- package/lib/dist/types.js +3 -0
- package/lib/dist/types.js.map +1 -1
- package/lib/dist/upgrade/index.d.ts +132 -0
- package/lib/dist/upgrade/index.d.ts.map +1 -0
- package/lib/dist/upgrade/index.js +462 -0
- package/lib/dist/upgrade/index.js.map +1 -0
- package/lib/dist/utils.d.ts +30 -24
- package/lib/dist/utils.d.ts.map +1 -1
- package/lib/dist/utils.js +64 -48
- package/lib/dist/utils.js.map +1 -1
- package/lib/node_modules/.package-lock.json +1 -29
- package/lib/package.json +1 -2
- package/package.json +1 -1
- package/lib/node_modules/chokidar/LICENSE +0 -21
- package/lib/node_modules/chokidar/README.md +0 -305
- package/lib/node_modules/chokidar/esm/handler.d.ts +0 -90
- package/lib/node_modules/chokidar/esm/handler.js +0 -629
- package/lib/node_modules/chokidar/esm/index.d.ts +0 -215
- package/lib/node_modules/chokidar/esm/index.js +0 -798
- package/lib/node_modules/chokidar/esm/package.json +0 -1
- package/lib/node_modules/chokidar/handler.d.ts +0 -90
- package/lib/node_modules/chokidar/handler.js +0 -635
- package/lib/node_modules/chokidar/index.d.ts +0 -215
- package/lib/node_modules/chokidar/index.js +0 -804
- package/lib/node_modules/chokidar/package.json +0 -69
- package/lib/node_modules/readdirp/LICENSE +0 -21
- package/lib/node_modules/readdirp/README.md +0 -120
- package/lib/node_modules/readdirp/esm/index.d.ts +0 -108
- package/lib/node_modules/readdirp/esm/index.js +0 -257
- package/lib/node_modules/readdirp/esm/package.json +0 -1
- package/lib/node_modules/readdirp/index.d.ts +0 -108
- package/lib/node_modules/readdirp/index.js +0 -263
- package/lib/node_modules/readdirp/package.json +0 -70
package/lib/dist/mcp/tools.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Defines the tools exposed by the CodeGraph MCP server.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.ToolHandler = exports.tools = void 0;
|
|
8
|
+
exports.ToolHandler = exports.tools = exports.PathRefusalError = exports.NotIndexedError = void 0;
|
|
9
9
|
exports.getExploreBudget = getExploreBudget;
|
|
10
10
|
exports.getExploreOutputBudget = getExploreOutputBudget;
|
|
11
11
|
exports.formatStaleBanner = formatStaleBanner;
|
|
@@ -23,6 +23,28 @@ const query_utils_1 = require("../search/query-utils");
|
|
|
23
23
|
const fs_1 = require("fs");
|
|
24
24
|
const utils_1 = require("../utils");
|
|
25
25
|
const generated_detection_1 = require("../extraction/generated-detection");
|
|
26
|
+
const dynamic_boundaries_1 = require("./dynamic-boundaries");
|
|
27
|
+
/**
|
|
28
|
+
* An expected, recoverable "codegraph can't serve this" condition — most
|
|
29
|
+
* importantly a project with no index. The dispatch catch converts these to
|
|
30
|
+
* SUCCESS-shaped responses (guidance text, NO isError): an `isError: true`
|
|
31
|
+
* early in a session teaches the agent the toolset is broken and it stops
|
|
32
|
+
* calling codegraph entirely (observed repeatedly), which is exactly wrong
|
|
33
|
+
* for conditions the agent can simply work around (use built-in tools for
|
|
34
|
+
* that codebase / pass projectPath). isError is reserved for "stop trying"
|
|
35
|
+
* cases: security refusals ({@link PathRefusalError}) and genuine
|
|
36
|
+
* malfunctions.
|
|
37
|
+
*/
|
|
38
|
+
class NotIndexedError extends Error {
|
|
39
|
+
}
|
|
40
|
+
exports.NotIndexedError = NotIndexedError;
|
|
41
|
+
/**
|
|
42
|
+
* A security refusal (sensitive system path). Stays `isError: true` WITHOUT
|
|
43
|
+
* retry guidance — abandoning this path is the desired agent reaction.
|
|
44
|
+
*/
|
|
45
|
+
class PathRefusalError extends Error {
|
|
46
|
+
}
|
|
47
|
+
exports.PathRefusalError = PathRefusalError;
|
|
26
48
|
const path_1 = require("path");
|
|
27
49
|
/** Maximum output length to prevent context bloat (characters) */
|
|
28
50
|
const MAX_OUTPUT_LENGTH = 15000;
|
|
@@ -310,6 +332,10 @@ exports.tools = [
|
|
|
310
332
|
type: 'string',
|
|
311
333
|
description: 'Name of the function, method, or class to find callers for',
|
|
312
334
|
},
|
|
335
|
+
file: {
|
|
336
|
+
type: 'string',
|
|
337
|
+
description: 'Narrow to the definition in this file (path or suffix) when several same-named symbols exist (e.g. one UserService per app in a monorepo)',
|
|
338
|
+
},
|
|
313
339
|
limit: {
|
|
314
340
|
type: 'number',
|
|
315
341
|
description: 'Maximum number of callers to return (default: 20)',
|
|
@@ -330,6 +356,10 @@ exports.tools = [
|
|
|
330
356
|
type: 'string',
|
|
331
357
|
description: 'Name of the function, method, or class to find callees for',
|
|
332
358
|
},
|
|
359
|
+
file: {
|
|
360
|
+
type: 'string',
|
|
361
|
+
description: 'Narrow to the definition in this file (path or suffix) when several same-named symbols exist',
|
|
362
|
+
},
|
|
333
363
|
limit: {
|
|
334
364
|
type: 'number',
|
|
335
365
|
description: 'Maximum number of callees to return (default: 20)',
|
|
@@ -350,6 +380,10 @@ exports.tools = [
|
|
|
350
380
|
type: 'string',
|
|
351
381
|
description: 'Name of the symbol to analyze impact for',
|
|
352
382
|
},
|
|
383
|
+
file: {
|
|
384
|
+
type: 'string',
|
|
385
|
+
description: 'Narrow to the definition in this file (path or suffix) when several same-named symbols exist',
|
|
386
|
+
},
|
|
353
387
|
depth: {
|
|
354
388
|
type: 'number',
|
|
355
389
|
description: 'How many levels of dependencies to traverse (default: 2)',
|
|
@@ -362,41 +396,54 @@ exports.tools = [
|
|
|
362
396
|
},
|
|
363
397
|
{
|
|
364
398
|
name: 'codegraph_node',
|
|
365
|
-
description: '
|
|
399
|
+
description: 'Two modes. (1) READ A FILE — use INSTEAD of the Read tool: pass `file` (a path or basename) with no `symbol` and it returns that file\'s current on-disk source with line numbers, exactly the shape Read gives you (`<n>\\t<line>`, safe to Edit from), narrowable with `offset`/`limit` just like Read — PLUS a one-line note of which files depend on it. Same bytes as Read, faster (served from the index), with the blast radius attached. Use it whenever you would Read a source file. (2) ONE SYMBOL you can name — its location, signature, verbatim source (includeCode=true) and caller/callee trail in one call, so before changing it you see what calls it and what your edit would break. For an AMBIGUOUS name it returns EVERY matching definition\'s body in one call (so you never Read a file to find the right overload); pass `file`/`line` to pin one. Use codegraph_explore for several related symbols or the full flow.',
|
|
366
400
|
inputSchema: {
|
|
367
401
|
type: 'object',
|
|
368
402
|
properties: {
|
|
369
403
|
symbol: {
|
|
370
404
|
type: 'string',
|
|
371
|
-
description: 'Name of the symbol to
|
|
405
|
+
description: 'Name of the symbol to read (symbol mode). Omit it and pass `file` alone to read a whole file like Read.',
|
|
372
406
|
},
|
|
373
407
|
includeCode: {
|
|
374
408
|
type: 'boolean',
|
|
375
|
-
description: '
|
|
409
|
+
description: 'Symbol mode: include the symbol\'s full body (default: false). Ignored in file mode, which always returns source unless `symbolsOnly` is set.',
|
|
376
410
|
default: false,
|
|
377
411
|
},
|
|
378
412
|
file: {
|
|
379
413
|
type: 'string',
|
|
380
|
-
description: '
|
|
414
|
+
description: 'A file path or basename (e.g. "harness.rs", "src/auth/session.ts"). Pass it ALONE (no symbol) to READ the file like the Read tool — its full source with line numbers + which files depend on it. Or pass it WITH a symbol to disambiguate an overloaded name to the definition in this file.',
|
|
415
|
+
},
|
|
416
|
+
offset: {
|
|
417
|
+
type: 'number',
|
|
418
|
+
description: 'File mode: 1-based line to start reading from, exactly like Read\'s offset. Defaults to the start of the file.',
|
|
419
|
+
},
|
|
420
|
+
limit: {
|
|
421
|
+
type: 'number',
|
|
422
|
+
description: 'File mode: maximum number of lines to return, exactly like Read\'s limit. Defaults to the whole file (capped at 2000 lines, like Read).',
|
|
423
|
+
},
|
|
424
|
+
symbolsOnly: {
|
|
425
|
+
type: 'boolean',
|
|
426
|
+
description: 'File mode: return just the file\'s symbol map + dependents (a cheap structural overview) instead of its source.',
|
|
427
|
+
default: false,
|
|
381
428
|
},
|
|
382
429
|
line: {
|
|
383
430
|
type: 'number',
|
|
384
|
-
description: '
|
|
431
|
+
description: 'Symbol mode only: disambiguate to the definition at/around this line (use with the file:line a trail showed you).',
|
|
385
432
|
},
|
|
386
433
|
projectPath: projectPathProperty,
|
|
387
434
|
},
|
|
388
|
-
required: [
|
|
435
|
+
required: [],
|
|
389
436
|
},
|
|
390
437
|
},
|
|
391
438
|
{
|
|
392
439
|
name: 'codegraph_explore',
|
|
393
|
-
description: 'PRIMARY TOOL — call FIRST for almost any question: how does X work, architecture, a bug, where/what is X,
|
|
440
|
+
description: 'PRIMARY TOOL — call FIRST for almost any question OR before an edit: how does X work, architecture, a bug, where/what is X, surveying an area, or the symbols you are about to change. Returns the verbatim source of the relevant symbols grouped by file in ONE capped call (Read-equivalent — treat the shown source as already Read; do NOT re-open those files), plus the call path among them. Query can be a natural-language question OR a bag of symbol/file names. Usually the ONLY call you need — more accurate context, in far fewer tokens and round-trips than a search/Read/Grep loop.',
|
|
394
441
|
inputSchema: {
|
|
395
442
|
type: 'object',
|
|
396
443
|
properties: {
|
|
397
444
|
query: {
|
|
398
445
|
type: 'string',
|
|
399
|
-
description: 'Symbol names, file names, or short code terms to explore (e.g., "AuthService loginUser session-manager", "GraphTraverser BFS impact traversal.ts").
|
|
446
|
+
description: 'Symbol names, file names, or short code terms to explore (e.g., "AuthService loginUser session-manager", "GraphTraverser BFS impact traversal.ts"). For a flow question, name the symbols spanning the flow (e.g. "mutateElement renderScene"). A natural-language question works too — no prior codegraph_search needed.',
|
|
400
447
|
},
|
|
401
448
|
maxFiles: {
|
|
402
449
|
type: 'number',
|
|
@@ -460,11 +507,35 @@ exports.tools = [
|
|
|
460
507
|
*/
|
|
461
508
|
function getStaticTools() {
|
|
462
509
|
const raw = process.env.CODEGRAPH_MCP_TOOLS;
|
|
463
|
-
if (!raw || !raw.trim())
|
|
464
|
-
return exports.tools;
|
|
510
|
+
if (!raw || !raw.trim()) {
|
|
511
|
+
return exports.tools.filter(t => DEFAULT_MCP_TOOLS.has(t.name.replace(/^codegraph_/, '')));
|
|
512
|
+
}
|
|
465
513
|
const allow = new Set(raw.split(',').map(s => s.trim().replace(/^codegraph_/, '')).filter(Boolean));
|
|
466
514
|
return allow.size ? exports.tools.filter(t => allow.has(t.name.replace(/^codegraph_/, ''))) : exports.tools;
|
|
467
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* The MCP tools served by DEFAULT (short names). The other defined tools
|
|
518
|
+
* (callees, impact, files, status) remain fully functional — handlers stay,
|
|
519
|
+
* the library API and CLI are untouched, and `CODEGRAPH_MCP_TOOLS` re-enables
|
|
520
|
+
* any of them — they just aren't LISTED to agents anymore.
|
|
521
|
+
*
|
|
522
|
+
* Evidence for the cut (the "adapt the tool to the agent" principle —
|
|
523
|
+
* fewer tools = fewer mis-picks, and presence itself steers):
|
|
524
|
+
* - `codegraph_impact` appears in ZERO recorded eval runs ever — its
|
|
525
|
+
* blast-radius info already arrives inline on explore (the "Blast radius"
|
|
526
|
+
* section) and node (the dependents note), so agents never need the
|
|
527
|
+
* standalone tool.
|
|
528
|
+
* - `codegraph_callees` is redundant by construction: a symbol's body (which
|
|
529
|
+
* node returns) IS its callee list, plus the caller/callee trail.
|
|
530
|
+
* - `codegraph_files` / `codegraph_status`: the tiny-repo audit (see
|
|
531
|
+
* getTools) found they "reduce to one grep"; staleness banners already
|
|
532
|
+
* inline the pending-sync info on every read tool, and the CLI covers
|
|
533
|
+
* diagnostics.
|
|
534
|
+
* - `codegraph_callers` stays: exhaustive call-site enumeration (every
|
|
535
|
+
* caller with file:line, callback registrations labeled, one section per
|
|
536
|
+
* same-named definition) is the one job explore/node don't replicate.
|
|
537
|
+
*/
|
|
538
|
+
const DEFAULT_MCP_TOOLS = new Set(['explore', 'node', 'search', 'callers']);
|
|
468
539
|
/**
|
|
469
540
|
* Tool handler that executes tools against a CodeGraph instance
|
|
470
541
|
*
|
|
@@ -553,18 +624,22 @@ class ToolHandler {
|
|
|
553
624
|
*/
|
|
554
625
|
getTools() {
|
|
555
626
|
const allow = this.toolAllowlist();
|
|
627
|
+
// No explicit allowlist → the default 4-tool surface (see
|
|
628
|
+
// DEFAULT_MCP_TOOLS for the evidence). An allowlist replaces the
|
|
629
|
+
// default entirely, so any defined tool can be re-enabled.
|
|
556
630
|
let visible = allow
|
|
557
631
|
? exports.tools.filter(t => allow.has(t.name.replace(/^codegraph_/, '')))
|
|
558
|
-
: exports.tools;
|
|
632
|
+
: exports.tools.filter(t => DEFAULT_MCP_TOOLS.has(t.name.replace(/^codegraph_/, '')));
|
|
559
633
|
if (!this.cg)
|
|
560
634
|
return visible;
|
|
561
635
|
try {
|
|
562
636
|
const stats = this.cg.getStats();
|
|
563
637
|
const budget = getExploreBudget(stats.fileCount);
|
|
564
638
|
// Tiny-repo tool gating: on projects under TINY_REPO_FILE_THRESHOLD
|
|
565
|
-
// files, only expose the
|
|
566
|
-
//
|
|
567
|
-
//
|
|
639
|
+
// files, only expose the core trio (search, node, explore) — one
|
|
640
|
+
// below even the 4-tool default: at this scale callers, too, reduces
|
|
641
|
+
// to one grep. (Historical note: the audit below ran when context and
|
|
642
|
+
// trace still existed; its "5 core tools" are today's trio.)
|
|
568
643
|
//
|
|
569
644
|
// n=2 audits ruled out cutting below 5 tools:
|
|
570
645
|
// - 3-tool gate (search + context + trace): cost regressed on
|
|
@@ -619,13 +694,15 @@ class ToolHandler {
|
|
|
619
694
|
if (!projectPath) {
|
|
620
695
|
if (!this.cg) {
|
|
621
696
|
const searched = this.defaultProjectHint ?? process.cwd();
|
|
622
|
-
throw new
|
|
697
|
+
throw new NotIndexedError('No CodeGraph project is loaded for this session.\n' +
|
|
623
698
|
`Searched for a .codegraph/ directory starting from: ${searched}\n` +
|
|
624
|
-
'
|
|
699
|
+
'If this project IS indexed, this is a working-directory detection issue: ' +
|
|
625
700
|
"the MCP client launched the server outside your project and didn't report the " +
|
|
626
701
|
'workspace root. Fix it either way:\n' +
|
|
627
702
|
' • Pass projectPath to the tool call, e.g. projectPath: "/absolute/path/to/your/project"\n' +
|
|
628
|
-
' • Or add --path to the server\'s MCP config args: ["serve", "--mcp", "--path", "/absolute/path/to/your/project"]'
|
|
703
|
+
' • Or add --path to the server\'s MCP config args: ["serve", "--mcp", "--path", "/absolute/path/to/your/project"]\n' +
|
|
704
|
+
'If the project simply has no index, continue with your built-in tools (Read/Grep/Glob) ' +
|
|
705
|
+
"and don't call codegraph again this session — the user can run 'codegraph init' to enable it.");
|
|
629
706
|
}
|
|
630
707
|
return this.cg;
|
|
631
708
|
}
|
|
@@ -641,13 +718,16 @@ class ToolHandler {
|
|
|
641
718
|
if ((0, fs_1.existsSync)(projectPath)) {
|
|
642
719
|
const pathError = (0, utils_1.validateProjectPath)(projectPath);
|
|
643
720
|
if (pathError) {
|
|
644
|
-
throw new
|
|
721
|
+
throw new PathRefusalError(pathError);
|
|
645
722
|
}
|
|
646
723
|
}
|
|
647
724
|
// Walk up parent directories to find nearest .codegraph/
|
|
648
725
|
const resolvedRoot = (0, directory_1.findNearestCodeGraphRoot)(projectPath);
|
|
649
726
|
if (!resolvedRoot) {
|
|
650
|
-
throw new
|
|
727
|
+
throw new NotIndexedError(`The project at ${projectPath} isn't indexed with codegraph (no .codegraph/ directory found ` +
|
|
728
|
+
'walking up from it), so codegraph cannot query it. Use your built-in tools (Read/Grep/Glob) ' +
|
|
729
|
+
"for that codebase instead, and don't call codegraph for it again this session. " +
|
|
730
|
+
"Indexing is the user's decision — they can run 'codegraph init' in that project to enable it.");
|
|
651
731
|
}
|
|
652
732
|
// If the path resolves to the default project, reuse the already-open
|
|
653
733
|
// default instance rather than opening a SECOND connection to the same DB.
|
|
@@ -926,7 +1006,19 @@ class ToolHandler {
|
|
|
926
1006
|
return this.withStalenessNotice(withWorktree, args.projectPath);
|
|
927
1007
|
}
|
|
928
1008
|
catch (err) {
|
|
929
|
-
|
|
1009
|
+
// Expected condition, not a malfunction: answer as a SUCCESS so the
|
|
1010
|
+
// agent keeps trusting the toolset for projects that ARE indexed.
|
|
1011
|
+
// (An isError here teaches session-long abandonment — see NotIndexedError.)
|
|
1012
|
+
if (err instanceof NotIndexedError) {
|
|
1013
|
+
return this.textResult(err.message);
|
|
1014
|
+
}
|
|
1015
|
+
// Security refusal: a clean error, no retry encouragement.
|
|
1016
|
+
if (err instanceof PathRefusalError) {
|
|
1017
|
+
return this.errorResult(err.message);
|
|
1018
|
+
}
|
|
1019
|
+
return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
1020
|
+
'This is an internal codegraph error — retry the call once; if it persists, ' +
|
|
1021
|
+
'continue without codegraph for this task.');
|
|
930
1022
|
}
|
|
931
1023
|
}
|
|
932
1024
|
/**
|
|
@@ -937,7 +1029,11 @@ class ToolHandler {
|
|
|
937
1029
|
if (typeof query !== 'string')
|
|
938
1030
|
return query;
|
|
939
1031
|
const cg = this.getCodeGraph(args.projectPath);
|
|
940
|
-
const
|
|
1032
|
+
const rawKind = args.kind;
|
|
1033
|
+
// The schema enum says 'type' (what agents naturally reach for); the
|
|
1034
|
+
// NodeKind is 'type_alias'. Without the mapping, kind: "type" silently
|
|
1035
|
+
// matched nothing — a filter value we advertise must work.
|
|
1036
|
+
const kind = rawKind === 'type' ? 'type_alias' : rawKind;
|
|
941
1037
|
const rawLimit = Number(args.limit) || 10;
|
|
942
1038
|
const limit = (0, utils_1.clamp)(rawLimit, 1, 100);
|
|
943
1039
|
const results = cg.searchNodes(query, {
|
|
@@ -958,6 +1054,43 @@ class ToolHandler {
|
|
|
958
1054
|
const formatted = this.formatSearchResults(ranked);
|
|
959
1055
|
return this.textResult(this.truncateOutput(formatted));
|
|
960
1056
|
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Group symbol matches into DISTINCT DEFINITIONS — one group per
|
|
1059
|
+
* (filePath, qualifiedName), so same-file overloads stay together while
|
|
1060
|
+
* unrelated same-named classes across a monorepo's apps (#764: one
|
|
1061
|
+
* `UserService` per NestJS app) are kept apart. Optionally narrowed by a
|
|
1062
|
+
* `file` path/suffix first.
|
|
1063
|
+
*/
|
|
1064
|
+
groupDefinitions(nodes, fileFilter) {
|
|
1065
|
+
let pool = nodes;
|
|
1066
|
+
let filteredOut = false;
|
|
1067
|
+
if (fileFilter) {
|
|
1068
|
+
const wanted = fileFilter.replace(/^\.\//, '');
|
|
1069
|
+
const narrowed = pool.filter((n) => n.filePath === wanted || n.filePath.endsWith(wanted) || n.filePath.endsWith(`/${wanted}`));
|
|
1070
|
+
if (narrowed.length > 0) {
|
|
1071
|
+
pool = narrowed;
|
|
1072
|
+
}
|
|
1073
|
+
else {
|
|
1074
|
+
filteredOut = true;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const byDef = new Map();
|
|
1078
|
+
for (const n of pool) {
|
|
1079
|
+
const key = `${n.filePath}|${n.qualifiedName}`;
|
|
1080
|
+
const group = byDef.get(key);
|
|
1081
|
+
if (group)
|
|
1082
|
+
group.push(n);
|
|
1083
|
+
else
|
|
1084
|
+
byDef.set(key, [n]);
|
|
1085
|
+
}
|
|
1086
|
+
return { groups: [...byDef.values()], filteredOut };
|
|
1087
|
+
}
|
|
1088
|
+
/** Section heading for one distinct definition in grouped output. */
|
|
1089
|
+
definitionHeading(group) {
|
|
1090
|
+
const head = group[0];
|
|
1091
|
+
const line = head.startLine ? `:${head.startLine}` : '';
|
|
1092
|
+
return `### ${head.qualifiedName} (${head.kind}) — ${head.filePath}${line}`;
|
|
1093
|
+
}
|
|
961
1094
|
/**
|
|
962
1095
|
* Handle codegraph_callers
|
|
963
1096
|
*/
|
|
@@ -967,26 +1100,64 @@ class ToolHandler {
|
|
|
967
1100
|
return symbol;
|
|
968
1101
|
const cg = this.getCodeGraph(args.projectPath);
|
|
969
1102
|
const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
|
|
1103
|
+
const fileFilter = typeof args.file === 'string' ? args.file : undefined;
|
|
970
1104
|
const allMatches = this.findAllSymbols(cg, symbol);
|
|
971
1105
|
if (allMatches.nodes.length === 0) {
|
|
972
1106
|
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
|
973
1107
|
}
|
|
974
|
-
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1108
|
+
const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter);
|
|
1109
|
+
const filterNote = filteredOut
|
|
1110
|
+
? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.`
|
|
1111
|
+
: '';
|
|
1112
|
+
const collect = (defNodes) => {
|
|
1113
|
+
const seen = new Set();
|
|
1114
|
+
const callers = [];
|
|
1115
|
+
const labels = new Map();
|
|
1116
|
+
for (const node of defNodes) {
|
|
1117
|
+
for (const c of cg.getCallers(node.id)) {
|
|
1118
|
+
if (!seen.has(c.node.id)) {
|
|
1119
|
+
seen.add(c.node.id);
|
|
1120
|
+
callers.push(c.node);
|
|
1121
|
+
const label = this.edgeLabel(c.edge);
|
|
1122
|
+
if (label)
|
|
1123
|
+
labels.set(c.node.id, label);
|
|
1124
|
+
}
|
|
982
1125
|
}
|
|
983
1126
|
}
|
|
1127
|
+
return { callers, labels };
|
|
1128
|
+
};
|
|
1129
|
+
// Single definition (or same-file overloads): the familiar flat list.
|
|
1130
|
+
if (groups.length === 1) {
|
|
1131
|
+
const { callers, labels } = collect(groups[0]);
|
|
1132
|
+
if (callers.length === 0) {
|
|
1133
|
+
return this.textResult(`No callers found for "${symbol}"${allMatches.note}${filterNote}`);
|
|
1134
|
+
}
|
|
1135
|
+
// A successful `file` narrowing makes the multi-symbol aggregation note
|
|
1136
|
+
// stale — suppress it.
|
|
1137
|
+
const note = fileFilter && !filteredOut ? '' : allMatches.note;
|
|
1138
|
+
const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels) + note + filterNote;
|
|
1139
|
+
return this.textResult(this.truncateOutput(formatted));
|
|
1140
|
+
}
|
|
1141
|
+
// Multiple DISTINCT definitions (#764): one section per definition so an
|
|
1142
|
+
// agent never mistakes one app's callers for another's. Narrow with
|
|
1143
|
+
// `file` to focus a single definition.
|
|
1144
|
+
const lines = [
|
|
1145
|
+
`## Callers of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)`,
|
|
1146
|
+
];
|
|
1147
|
+
for (const group of groups) {
|
|
1148
|
+
const { callers, labels } = collect(group);
|
|
1149
|
+
lines.push('', this.definitionHeading(group));
|
|
1150
|
+
if (callers.length === 0) {
|
|
1151
|
+
lines.push('- (no callers)');
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
for (const node of callers.slice(0, limit)) {
|
|
1155
|
+
const location = node.startLine ? `:${node.startLine}` : '';
|
|
1156
|
+
const label = labels.get(node.id);
|
|
1157
|
+
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
|
|
1158
|
+
}
|
|
984
1159
|
}
|
|
985
|
-
|
|
986
|
-
return this.textResult(`No callers found for "${symbol}"${allMatches.note}`);
|
|
987
|
-
}
|
|
988
|
-
const formatted = this.formatNodeList(allCallers.slice(0, limit), `Callers of ${symbol}`) + allMatches.note;
|
|
989
|
-
return this.textResult(this.truncateOutput(formatted));
|
|
1160
|
+
return this.textResult(this.truncateOutput(lines.join('\n') + filterNote));
|
|
990
1161
|
}
|
|
991
1162
|
/**
|
|
992
1163
|
* Handle codegraph_callees
|
|
@@ -997,26 +1168,61 @@ class ToolHandler {
|
|
|
997
1168
|
return symbol;
|
|
998
1169
|
const cg = this.getCodeGraph(args.projectPath);
|
|
999
1170
|
const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
|
|
1171
|
+
const fileFilter = typeof args.file === 'string' ? args.file : undefined;
|
|
1000
1172
|
const allMatches = this.findAllSymbols(cg, symbol);
|
|
1001
1173
|
if (allMatches.nodes.length === 0) {
|
|
1002
1174
|
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
|
1003
1175
|
}
|
|
1004
|
-
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1176
|
+
const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter);
|
|
1177
|
+
const filterNote = filteredOut
|
|
1178
|
+
? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.`
|
|
1179
|
+
: '';
|
|
1180
|
+
const collect = (defNodes) => {
|
|
1181
|
+
const seen = new Set();
|
|
1182
|
+
const callees = [];
|
|
1183
|
+
const labels = new Map();
|
|
1184
|
+
for (const node of defNodes) {
|
|
1185
|
+
for (const c of cg.getCallees(node.id)) {
|
|
1186
|
+
if (!seen.has(c.node.id)) {
|
|
1187
|
+
seen.add(c.node.id);
|
|
1188
|
+
callees.push(c.node);
|
|
1189
|
+
const label = this.edgeLabel(c.edge);
|
|
1190
|
+
if (label)
|
|
1191
|
+
labels.set(c.node.id, label);
|
|
1192
|
+
}
|
|
1012
1193
|
}
|
|
1013
1194
|
}
|
|
1195
|
+
return { callees, labels };
|
|
1196
|
+
};
|
|
1197
|
+
if (groups.length === 1) {
|
|
1198
|
+
const { callees, labels } = collect(groups[0]);
|
|
1199
|
+
if (callees.length === 0) {
|
|
1200
|
+
return this.textResult(`No callees found for "${symbol}"${allMatches.note}${filterNote}`);
|
|
1201
|
+
}
|
|
1202
|
+
// A successful `file` narrowing makes the multi-symbol aggregation note
|
|
1203
|
+
// stale — suppress it.
|
|
1204
|
+
const note = fileFilter && !filteredOut ? '' : allMatches.note;
|
|
1205
|
+
const formatted = this.formatNodeList(callees.slice(0, limit), `Callees of ${symbol}`, labels) + note + filterNote;
|
|
1206
|
+
return this.textResult(this.truncateOutput(formatted));
|
|
1207
|
+
}
|
|
1208
|
+
// Multiple DISTINCT definitions (#764): per-definition sections.
|
|
1209
|
+
const lines = [
|
|
1210
|
+
`## Callees of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)`,
|
|
1211
|
+
];
|
|
1212
|
+
for (const group of groups) {
|
|
1213
|
+
const { callees, labels } = collect(group);
|
|
1214
|
+
lines.push('', this.definitionHeading(group));
|
|
1215
|
+
if (callees.length === 0) {
|
|
1216
|
+
lines.push('- (no callees)');
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
for (const node of callees.slice(0, limit)) {
|
|
1220
|
+
const location = node.startLine ? `:${node.startLine}` : '';
|
|
1221
|
+
const label = labels.get(node.id);
|
|
1222
|
+
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
|
|
1223
|
+
}
|
|
1014
1224
|
}
|
|
1015
|
-
|
|
1016
|
-
return this.textResult(`No callees found for "${symbol}"${allMatches.note}`);
|
|
1017
|
-
}
|
|
1018
|
-
const formatted = this.formatNodeList(allCallees.slice(0, limit), `Callees of ${symbol}`) + allMatches.note;
|
|
1019
|
-
return this.textResult(this.truncateOutput(formatted));
|
|
1225
|
+
return this.textResult(this.truncateOutput(lines.join('\n') + filterNote));
|
|
1020
1226
|
}
|
|
1021
1227
|
/**
|
|
1022
1228
|
* Handle codegraph_impact
|
|
@@ -1027,34 +1233,51 @@ class ToolHandler {
|
|
|
1027
1233
|
return symbol;
|
|
1028
1234
|
const cg = this.getCodeGraph(args.projectPath);
|
|
1029
1235
|
const depth = (0, utils_1.clamp)(args.depth || 2, 1, 10);
|
|
1236
|
+
const fileFilter = typeof args.file === 'string' ? args.file : undefined;
|
|
1030
1237
|
const allMatches = this.findAllSymbols(cg, symbol);
|
|
1031
1238
|
if (allMatches.nodes.length === 0) {
|
|
1032
1239
|
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
|
1033
1240
|
}
|
|
1034
|
-
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1241
|
+
const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter);
|
|
1242
|
+
const filterNote = filteredOut
|
|
1243
|
+
? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.`
|
|
1244
|
+
: '';
|
|
1245
|
+
const impactOf = (defNodes) => {
|
|
1246
|
+
const mergedNodes = new Map();
|
|
1247
|
+
const mergedEdges = [];
|
|
1248
|
+
const seenEdges = new Set();
|
|
1249
|
+
for (const node of defNodes) {
|
|
1250
|
+
const impact = cg.getImpactRadius(node.id, depth);
|
|
1251
|
+
for (const [id, n] of impact.nodes) {
|
|
1252
|
+
mergedNodes.set(id, n);
|
|
1253
|
+
}
|
|
1254
|
+
for (const e of impact.edges) {
|
|
1255
|
+
const key = `${e.source}->${e.target}:${e.kind}`;
|
|
1256
|
+
if (!seenEdges.has(key)) {
|
|
1257
|
+
seenEdges.add(key);
|
|
1258
|
+
mergedEdges.push(e);
|
|
1259
|
+
}
|
|
1048
1260
|
}
|
|
1049
1261
|
}
|
|
1050
|
-
|
|
1051
|
-
const mergedImpact = {
|
|
1052
|
-
nodes: mergedNodes,
|
|
1053
|
-
edges: mergedEdges,
|
|
1054
|
-
roots: allMatches.nodes.map(n => n.id),
|
|
1262
|
+
return { nodes: mergedNodes, edges: mergedEdges, roots: defNodes.map((n) => n.id) };
|
|
1055
1263
|
};
|
|
1056
|
-
|
|
1057
|
-
|
|
1264
|
+
// Single definition (or same-file overloads): the familiar merged report.
|
|
1265
|
+
if (groups.length === 1) {
|
|
1266
|
+
const formatted = this.formatImpact(symbol, impactOf(groups[0])) + (fileFilter && !filteredOut ? "" : allMatches.note) + filterNote;
|
|
1267
|
+
return this.textResult(this.truncateOutput(formatted));
|
|
1268
|
+
}
|
|
1269
|
+
// Multiple DISTINCT definitions (#764): a blast radius PER definition —
|
|
1270
|
+
// merging unrelated same-named classes (one UserService per monorepo app)
|
|
1271
|
+
// overstated impact and confused agents. Narrow with `file`.
|
|
1272
|
+
const sections = [
|
|
1273
|
+
`## Impact of ${symbol} — ${groups.length} distinct definitions (each with its own blast radius; narrow with \`file\`)`,
|
|
1274
|
+
];
|
|
1275
|
+
for (const group of groups) {
|
|
1276
|
+
const head = group[0];
|
|
1277
|
+
const line = head.startLine ? `:${head.startLine}` : '';
|
|
1278
|
+
sections.push('', this.formatImpact(`${head.qualifiedName} (${head.filePath}${line})`, impactOf(group)));
|
|
1279
|
+
}
|
|
1280
|
+
return this.textResult(this.truncateOutput(sections.join('\n') + filterNote));
|
|
1058
1281
|
}
|
|
1059
1282
|
/**
|
|
1060
1283
|
* Describe a synthesized (dynamic-dispatch) edge for human output: how the
|
|
@@ -1147,7 +1370,7 @@ class ToolHandler {
|
|
|
1147
1370
|
// names (Class.method / Class::method) — the agent's most precise input,
|
|
1148
1371
|
// resolved exactly by findAllSymbols. (The old strip mangled Class.method
|
|
1149
1372
|
// into Class, throwing the method away.)
|
|
1150
|
-
const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte)$/i;
|
|
1373
|
+
const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte|astro)$/i;
|
|
1151
1374
|
const tokens = [...new Set(query.split(/[\s,()[\]]+/)
|
|
1152
1375
|
.map((t) => t.replace(FILE_EXT, '').trim())
|
|
1153
1376
|
.filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)))].slice(0, 16);
|
|
@@ -1168,6 +1391,10 @@ class ToolHandler {
|
|
|
1168
1391
|
// (`as_sql`, 110 defs across every Expression/Compiler subclass) is NOT here,
|
|
1169
1392
|
// so naming it doesn't keep every backend variant full and flood the budget.
|
|
1170
1393
|
const uniqueNamedNodeIds = new Set();
|
|
1394
|
+
// token → resolved node ids: drives the token-coverage check that gates
|
|
1395
|
+
// the dynamic-boundary scan (a token is covered when ANY of its nodes
|
|
1396
|
+
// lands on the main chain — overloads off the chain don't count against).
|
|
1397
|
+
const tokenNodes = new Map();
|
|
1171
1398
|
for (const t of tokens) {
|
|
1172
1399
|
const cands = this.findAllSymbols(cg, t).nodes.filter((n) => CALLABLE.has(n.kind));
|
|
1173
1400
|
// A qualified or otherwise-specific name (<=3 hits) keeps all; an
|
|
@@ -1180,7 +1407,9 @@ class ToolHandler {
|
|
|
1180
1407
|
const container = segs.length >= 2 ? segs[segs.length - 2] : '';
|
|
1181
1408
|
return !!container && segPool.has(container);
|
|
1182
1409
|
});
|
|
1183
|
-
|
|
1410
|
+
const kept = pick.slice(0, 6);
|
|
1411
|
+
tokenNodes.set(t, kept.map((n) => n.id));
|
|
1412
|
+
for (const n of kept) {
|
|
1184
1413
|
named.set(n.id, n);
|
|
1185
1414
|
if (specific)
|
|
1186
1415
|
uniqueNamedNodeIds.add(n.id);
|
|
@@ -1188,8 +1417,19 @@ class ToolHandler {
|
|
|
1188
1417
|
if (named.size > 40)
|
|
1189
1418
|
break;
|
|
1190
1419
|
}
|
|
1191
|
-
if (named.size < 2)
|
|
1192
|
-
|
|
1420
|
+
if (named.size < 2) {
|
|
1421
|
+
// The agent named a flow but only one side resolved (the other end is
|
|
1422
|
+
// anonymous / runtime-registered / not extracted). The resolved side's
|
|
1423
|
+
// body may still hold the dynamic-dispatch site that EXPLAINS the gap —
|
|
1424
|
+
// surface that instead of silently returning nothing.
|
|
1425
|
+
if (named.size === 0)
|
|
1426
|
+
return EMPTY;
|
|
1427
|
+
const boundaries = this.buildDynamicBoundaries(cg, [...named.values()], named);
|
|
1428
|
+
if (!boundaries)
|
|
1429
|
+
return EMPTY;
|
|
1430
|
+
const text = boundaries + '> Full source for these symbols is below.\n';
|
|
1431
|
+
return { text, pathNodeIds: new Set(), namedNodeIds: new Set(named.keys()), uniqueNamedNodeIds };
|
|
1432
|
+
}
|
|
1193
1433
|
const MAX_HOPS = 7;
|
|
1194
1434
|
let best = null;
|
|
1195
1435
|
// BFS the full call graph (incl. synth edges) from each named seed, but
|
|
@@ -1237,6 +1477,43 @@ class ToolHandler {
|
|
|
1237
1477
|
}
|
|
1238
1478
|
const hasMain = !!best && best.length >= 3;
|
|
1239
1479
|
const pathIds = new Set((best ?? []).map((s) => s.node.id));
|
|
1480
|
+
// Dynamic-boundary scan (#687) — fires ONLY when the flow the agent
|
|
1481
|
+
// asked about did not fully connect: some token resolved to nodes but
|
|
1482
|
+
// none of them sit on the main chain (or there is no chain at all). A
|
|
1483
|
+
// healthy flow skips this entirely. Scan order: the chain's dead end
|
|
1484
|
+
// first (where the partial flow stops), then the disconnected symbols,
|
|
1485
|
+
// agent-specific (unique-named) ones first.
|
|
1486
|
+
let boundaryText = '';
|
|
1487
|
+
{
|
|
1488
|
+
const uncovered = [];
|
|
1489
|
+
if (!hasMain) {
|
|
1490
|
+
// No rendered chain — but a 2-node chain still CONNECTS its two
|
|
1491
|
+
// endpoints (e.g. via one synthesized hop, surfaced below as a
|
|
1492
|
+
// dynamic-dispatch link). Only nodes off that short chain are
|
|
1493
|
+
// unexplained breaks worth scanning.
|
|
1494
|
+
for (const n of named.values())
|
|
1495
|
+
if (!pathIds.has(n.id))
|
|
1496
|
+
uncovered.push(n);
|
|
1497
|
+
}
|
|
1498
|
+
else {
|
|
1499
|
+
for (const ids of tokenNodes.values()) {
|
|
1500
|
+
if (ids.length === 0 || ids.some((id) => pathIds.has(id)))
|
|
1501
|
+
continue;
|
|
1502
|
+
for (const id of ids) {
|
|
1503
|
+
const n = named.get(id);
|
|
1504
|
+
if (n)
|
|
1505
|
+
uncovered.push(n);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if (uncovered.length > 0) {
|
|
1510
|
+
const scanList = [];
|
|
1511
|
+
if (hasMain)
|
|
1512
|
+
scanList.push(best[best.length - 1].node);
|
|
1513
|
+
scanList.push(...uncovered.sort((a, b) => (uniqueNamedNodeIds.has(b.id) ? 1 : 0) - (uniqueNamedNodeIds.has(a.id) ? 1 : 0)));
|
|
1514
|
+
boundaryText = this.buildDynamicBoundaries(cg, scanList, named);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1240
1517
|
// Supplementary: dynamic-dispatch (synthesized) edges incident to a NAMED
|
|
1241
1518
|
// symbol — the indirect hops an agent would otherwise grep/Read to
|
|
1242
1519
|
// reconstruct ("where do the appended `validators` actually run?"). The
|
|
@@ -1255,8 +1532,13 @@ class ToolHandler {
|
|
|
1255
1532
|
break;
|
|
1256
1533
|
if (edge.provenance !== 'heuristic' || other.id === n.id)
|
|
1257
1534
|
continue;
|
|
1258
|
-
|
|
1259
|
-
|
|
1535
|
+
// "Already in the main chain" only applies when a chain RENDERS
|
|
1536
|
+
// (hasMain). A 2-node chain populates pathIds but renders nothing,
|
|
1537
|
+
// so a direct synthesized hop between two named symbols (custom
|
|
1538
|
+
// EventBus emit→handler, #687) was invisible — too short for Flow,
|
|
1539
|
+
// skipped here as in-chain. Surface it.
|
|
1540
|
+
if (hasMain && pathIds.has(edge.source) && pathIds.has(edge.target))
|
|
1541
|
+
continue;
|
|
1260
1542
|
const src = edge.source === n.id ? n : other;
|
|
1261
1543
|
const tgt = edge.source === n.id ? other : n;
|
|
1262
1544
|
const key = `${src.name}>${tgt.name}`;
|
|
@@ -1267,7 +1549,7 @@ class ToolHandler {
|
|
|
1267
1549
|
synthLines.push(`- ${src.name} → ${tgt.name} [${note ? note.compact : edge.kind}]`);
|
|
1268
1550
|
}
|
|
1269
1551
|
}
|
|
1270
|
-
if (!hasMain && synthLines.length === 0)
|
|
1552
|
+
if (!hasMain && synthLines.length === 0 && !boundaryText)
|
|
1271
1553
|
return EMPTY;
|
|
1272
1554
|
const out = [];
|
|
1273
1555
|
if (hasMain) {
|
|
@@ -1285,6 +1567,8 @@ class ToolHandler {
|
|
|
1285
1567
|
if (synthLines.length) {
|
|
1286
1568
|
out.push('## Dynamic-dispatch links among your symbols', '(synthesized — the indirect hops grep/Read would reconstruct; the `@file:line` is the wiring site)', '', ...synthLines, '');
|
|
1287
1569
|
}
|
|
1570
|
+
if (boundaryText)
|
|
1571
|
+
out.push(boundaryText);
|
|
1288
1572
|
out.push('> Full source for these symbols is below — the call flow among them, followed by their bodies.', '');
|
|
1289
1573
|
// namedNodeIds = every callable the agent explicitly named (a superset of
|
|
1290
1574
|
// the spine). A file holding one is something the agent asked to SEE, so it
|
|
@@ -1297,6 +1581,163 @@ class ToolHandler {
|
|
|
1297
1581
|
return EMPTY;
|
|
1298
1582
|
}
|
|
1299
1583
|
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Dynamic-boundary surfacing (#687): when the flow among the agent's named
|
|
1586
|
+
* symbols does not fully connect, scan the disconnected symbols' bodies for
|
|
1587
|
+
* dynamic-dispatch sites (computed member calls, getattr, reflection, typed
|
|
1588
|
+
* message buses, runtime-keyed emits) and ANNOUNCE the boundary — the exact
|
|
1589
|
+
* site, the form, and (when a key is statically visible) candidate targets —
|
|
1590
|
+
* instead of guessing edges. The answer to "how does A reach B" when no
|
|
1591
|
+
* static path exists IS the dispatch site: that's where the flow continues
|
|
1592
|
+
* at runtime. Query-time, deterministic, zero graph mutation; a fully
|
|
1593
|
+
* connected flow never reaches this method.
|
|
1594
|
+
*/
|
|
1595
|
+
buildDynamicBoundaries(cg, scanList, named) {
|
|
1596
|
+
const MAX_NOTES = 4; // boundary bullets per explore
|
|
1597
|
+
const MAX_SCAN = 8; // bodies scanned
|
|
1598
|
+
const MAX_TOTAL_CHARS = 200_000;
|
|
1599
|
+
let projectRoot;
|
|
1600
|
+
try {
|
|
1601
|
+
projectRoot = cg.getProjectRoot();
|
|
1602
|
+
}
|
|
1603
|
+
catch {
|
|
1604
|
+
return '';
|
|
1605
|
+
}
|
|
1606
|
+
const notes = [];
|
|
1607
|
+
const seenNode = new Set();
|
|
1608
|
+
const seenSite = new Set();
|
|
1609
|
+
let scanned = 0, charsScanned = 0;
|
|
1610
|
+
for (const node of scanList) {
|
|
1611
|
+
if (notes.length >= MAX_NOTES || scanned >= MAX_SCAN || charsScanned > MAX_TOTAL_CHARS)
|
|
1612
|
+
break;
|
|
1613
|
+
if (seenNode.has(node.id) || !node.startLine || !node.endLine)
|
|
1614
|
+
continue;
|
|
1615
|
+
seenNode.add(node.id);
|
|
1616
|
+
const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, node.filePath);
|
|
1617
|
+
if (!absPath || !(0, fs_1.existsSync)(absPath))
|
|
1618
|
+
continue;
|
|
1619
|
+
let content;
|
|
1620
|
+
try {
|
|
1621
|
+
content = (0, fs_1.readFileSync)(absPath, 'utf-8');
|
|
1622
|
+
}
|
|
1623
|
+
catch {
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
const body = content.split('\n').slice(node.startLine - 1, node.endLine).join('\n');
|
|
1627
|
+
scanned++;
|
|
1628
|
+
charsScanned += body.length;
|
|
1629
|
+
for (const m of (0, dynamic_boundaries_1.scanDynamicDispatch)(body, node.language || '', node.startLine)) {
|
|
1630
|
+
if (notes.length >= MAX_NOTES)
|
|
1631
|
+
break;
|
|
1632
|
+
const siteKey = `${node.filePath}:${m.line}:${m.form}`;
|
|
1633
|
+
if (seenSite.has(siteKey))
|
|
1634
|
+
continue;
|
|
1635
|
+
seenSite.add(siteKey);
|
|
1636
|
+
const more = m.moreSites ? ` (+${m.moreSites} more such site${m.moreSites > 1 ? 's' : ''} in this body)` : '';
|
|
1637
|
+
notes.push(`- \`${node.name}\` (${node.filePath}:${m.line}) — ${m.label}: \`${m.snippet}\`${more}`);
|
|
1638
|
+
if (m.key) {
|
|
1639
|
+
const cand = this.boundaryCandidates(cg, m.key, !!m.keyIsType, named, node.id);
|
|
1640
|
+
if (cand)
|
|
1641
|
+
notes.push(` ${cand}`);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (notes.length === 0)
|
|
1646
|
+
return '';
|
|
1647
|
+
return [
|
|
1648
|
+
'## Dynamic boundaries (the static path ends at runtime dispatch)',
|
|
1649
|
+
'',
|
|
1650
|
+
...notes,
|
|
1651
|
+
'',
|
|
1652
|
+
'> These sites choose their call target at runtime (registry / bus / reflection) — the site shown IS where the flow continues. To follow it, run codegraph_explore or codegraph_node on a candidate; source for the sites above is included below.',
|
|
1653
|
+
'',
|
|
1654
|
+
].join('\n');
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Shortlist candidate runtime targets for a dispatch key surfaced by
|
|
1658
|
+
* {@link buildDynamicBoundaries}. Exact conventional names first (`save` →
|
|
1659
|
+
* `onSave`/`handleSave`; `CreateCmd` → `CreateCmdHandler`), then FTS, with a
|
|
1660
|
+
* normalized-containment post-filter (FTS camel-splitting is fuzzier than a
|
|
1661
|
+
* candidate list should be). Symbols the agent already named sort first and
|
|
1662
|
+
* are marked — that's the "you were right, here's the wiring" case.
|
|
1663
|
+
*/
|
|
1664
|
+
boundaryCandidates(cg, key, keyIsType, named, selfId) {
|
|
1665
|
+
const CALLABLE = new Set(['method', 'function', 'component', 'constructor', 'class']);
|
|
1666
|
+
const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
1667
|
+
const keyNorm = norm(key);
|
|
1668
|
+
if (keyNorm.length < 3)
|
|
1669
|
+
return '';
|
|
1670
|
+
const cands = new Map();
|
|
1671
|
+
const consider = (n) => {
|
|
1672
|
+
if (!n || n.id === selfId || !CALLABLE.has(n.kind) || cands.has(n.id))
|
|
1673
|
+
return;
|
|
1674
|
+
const nameNorm = norm(n.name || '');
|
|
1675
|
+
if (nameNorm.length < 3)
|
|
1676
|
+
return;
|
|
1677
|
+
if (!nameNorm.includes(keyNorm) && !keyNorm.includes(nameNorm))
|
|
1678
|
+
return;
|
|
1679
|
+
cands.set(n.id, n);
|
|
1680
|
+
};
|
|
1681
|
+
const cap = key.charAt(0).toUpperCase() + key.slice(1);
|
|
1682
|
+
const probes = keyIsType
|
|
1683
|
+
? [`${key}Handler`, key]
|
|
1684
|
+
: [key, `on${cap}`, `handle${cap}`, `${key}Handler`, `handle_${key}`];
|
|
1685
|
+
for (const p of probes) {
|
|
1686
|
+
try {
|
|
1687
|
+
for (const n of cg.getNodesByName(p))
|
|
1688
|
+
consider(n);
|
|
1689
|
+
}
|
|
1690
|
+
catch { /* exact probe miss is fine */ }
|
|
1691
|
+
}
|
|
1692
|
+
let raw = 0;
|
|
1693
|
+
try {
|
|
1694
|
+
const results = cg.searchNodes(key, { limit: 12 });
|
|
1695
|
+
raw = results.length;
|
|
1696
|
+
for (const r of results)
|
|
1697
|
+
consider(r.node);
|
|
1698
|
+
}
|
|
1699
|
+
catch { /* FTS syntax edge — exact probes already ran */ }
|
|
1700
|
+
if (cands.size === 0) {
|
|
1701
|
+
return raw >= 12 && key.length < 5 ? `key \`${key}\` is too generic to shortlist (${raw}+ matches)` : '';
|
|
1702
|
+
}
|
|
1703
|
+
// A constructor candidate duplicates its class: extractors emit ctors as
|
|
1704
|
+
// METHOD nodes named like the class (C#/Java `Foo::Foo`) — keep the class.
|
|
1705
|
+
const all = [...cands.values()];
|
|
1706
|
+
const classKey = new Set(all.filter((n) => n.kind === 'class').map((n) => `${n.name}|${n.filePath}`));
|
|
1707
|
+
const namedNames = new Set([...named.values()].map((n) => n.name));
|
|
1708
|
+
const isNamed = (n) => named.has(n.id) || namedNames.has(n.name); // the flow's named set holds callables only — transfer the mark to the class
|
|
1709
|
+
const list = all
|
|
1710
|
+
.filter((n) => !(n.kind !== 'class' && classKey.has(`${n.name}|${n.filePath}`)))
|
|
1711
|
+
.sort((a, b) => (isNamed(b) ? 1 : 0) - (isNamed(a) ? 1 : 0))
|
|
1712
|
+
.slice(0, 4)
|
|
1713
|
+
.map((n) => {
|
|
1714
|
+
// Typed-bus convention: the runtime target is the candidate class's
|
|
1715
|
+
// Handle/Execute/Consume method — name the exact node, not just the class.
|
|
1716
|
+
let display = n.qualifiedName || n.name;
|
|
1717
|
+
let at = `${n.filePath}:${n.startLine}`;
|
|
1718
|
+
if (keyIsType && n.kind === 'class') {
|
|
1719
|
+
try {
|
|
1720
|
+
const HANDLER_METHODS = /^(handle|handleAsync|execute|executeAsync|consume|consumeAsync|run|__invoke)$/i;
|
|
1721
|
+
const method = cg.getOutgoingEdges(n.id)
|
|
1722
|
+
.filter((e) => e.kind === 'contains')
|
|
1723
|
+
.map((e) => { try {
|
|
1724
|
+
return cg.getNode(e.target);
|
|
1725
|
+
}
|
|
1726
|
+
catch {
|
|
1727
|
+
return null;
|
|
1728
|
+
} })
|
|
1729
|
+
.find((c) => !!c && c.kind === 'method' && HANDLER_METHODS.test(c.name));
|
|
1730
|
+
if (method) {
|
|
1731
|
+
display = `${n.name}.${method.name}`;
|
|
1732
|
+
at = `${method.filePath}:${method.startLine}`;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
catch { /* class without resolvable members — show the class itself */ }
|
|
1736
|
+
}
|
|
1737
|
+
return `\`${display}\` (${at})${isNamed(n) ? ' ← you named this' : ''}`;
|
|
1738
|
+
});
|
|
1739
|
+
return `candidates for key \`${key}\`: ${list.join(', ')}`;
|
|
1740
|
+
}
|
|
1300
1741
|
/**
|
|
1301
1742
|
* Compact "blast radius" for the entry symbols of an explore result: who
|
|
1302
1743
|
* depends on each (callers) and which test files cover it — LOCATIONS ONLY,
|
|
@@ -1527,7 +1968,7 @@ class ToolHandler {
|
|
|
1527
1968
|
// agent explicitly named is in the subgraph and its file is scored.
|
|
1528
1969
|
const namedSeedIds = new Set();
|
|
1529
1970
|
{
|
|
1530
|
-
const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte)$/i;
|
|
1971
|
+
const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte|astro)$/i;
|
|
1531
1972
|
const CALLABLE = new Set(['method', 'function', 'component', 'constructor']);
|
|
1532
1973
|
const isTestPath = (p) => /(^|\/)(tests?|specs?|__tests__|testdata|mocks?|fixtures?)\//i.test(p) || /\.(test|spec)\.[a-z]+$/i.test(p);
|
|
1533
1974
|
const bodyLines = (n) => Math.max(0, (n.endLine ?? n.startLine) - n.startLine);
|
|
@@ -1538,8 +1979,12 @@ class ToolHandler {
|
|
|
1538
1979
|
// agent writes "DataRequest task validate", the `task`/`validate` it wants
|
|
1539
1980
|
// are DataRequest's, NOT the same-named overloads in Validation.swift /
|
|
1540
1981
|
// Concurrency.swift / the abstract base. Used below to bias overloaded
|
|
1541
|
-
// names toward the file/class the query also names.
|
|
1542
|
-
|
|
1982
|
+
// names toward the file/class the query also names. EXCLUDE the project
|
|
1983
|
+
// name (a PascalCase token a user naturally includes) — it names the whole
|
|
1984
|
+
// repo, so biasing toward it just pulls overloads to whichever stack
|
|
1985
|
+
// embeds it, re-burying the rest (#720).
|
|
1986
|
+
const projectNameTokens = cg.getProjectNameTokens();
|
|
1987
|
+
const typeTokens = tokens.filter((o) => /^[A-Z][A-Za-z0-9]{3,}/.test(o) && !projectNameTokens.has((0, query_utils_1.normalizeNameToken)(o)));
|
|
1543
1988
|
const inNamedContext = (n) => typeTokens.some((ct) => {
|
|
1544
1989
|
const lc = ct.toLowerCase();
|
|
1545
1990
|
return n.filePath.toLowerCase().includes(lc) || n.qualifiedName.toLowerCase().includes(lc);
|
|
@@ -1596,6 +2041,12 @@ class ToolHandler {
|
|
|
1596
2041
|
// Skip import/export nodes — they add noise without information
|
|
1597
2042
|
if (node.kind === 'import' || node.kind === 'export')
|
|
1598
2043
|
continue;
|
|
2044
|
+
// SECURITY (#383): never render the on-disk source of a config-leaf
|
|
2045
|
+
// (Spring application.{yml,properties} key) — its line is `key = <secret>`,
|
|
2046
|
+
// so whole-file/cluster rendering here would push secrets into context
|
|
2047
|
+
// unbidden. The key still appears in the flow/symbol listing above.
|
|
2048
|
+
if ((0, utils_1.isConfigLeafNode)(node))
|
|
2049
|
+
continue;
|
|
1599
2050
|
const group = fileGroups.get(node.filePath) || { nodes: [], score: 0 };
|
|
1600
2051
|
group.nodes.push(node);
|
|
1601
2052
|
// Score: a NAMED-SEED node (a symbol the agent named that FTS missed, now
|
|
@@ -2394,14 +2845,26 @@ class ToolHandler {
|
|
|
2394
2845
|
* Handle codegraph_node
|
|
2395
2846
|
*/
|
|
2396
2847
|
async handleNode(args) {
|
|
2397
|
-
const symbol = this.validateString(args.symbol, 'symbol');
|
|
2398
|
-
if (typeof symbol !== 'string')
|
|
2399
|
-
return symbol;
|
|
2400
2848
|
const cg = this.getCodeGraph(args.projectPath);
|
|
2401
2849
|
// Default to false to minimize context usage
|
|
2402
2850
|
const includeCode = args.includeCode === true;
|
|
2403
2851
|
const fileHint = typeof args.file === 'string' && args.file.trim() ? args.file.trim() : undefined;
|
|
2404
2852
|
const lineHint = typeof args.line === 'number' && args.line > 0 ? args.line : undefined;
|
|
2853
|
+
const offset = typeof args.offset === 'number' && args.offset > 0 ? Math.floor(args.offset) : undefined;
|
|
2854
|
+
const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.floor(args.limit) : undefined;
|
|
2855
|
+
const symbolsOnly = args.symbolsOnly === true;
|
|
2856
|
+
const symbolRaw = typeof args.symbol === 'string' ? args.symbol.trim() : '';
|
|
2857
|
+
// FILE READ MODE: a `file` with no `symbol` reads that file like the Read
|
|
2858
|
+
// tool — its current on-disk source with line numbers, narrowable with
|
|
2859
|
+
// `offset`/`limit` exactly as Read does — PLUS a one-line blast-radius
|
|
2860
|
+
// header (which files depend on it). `symbolsOnly` returns just the
|
|
2861
|
+
// structural map instead. Backed by the index: same bytes Read gives you.
|
|
2862
|
+
if (!symbolRaw && fileHint) {
|
|
2863
|
+
return this.handleFileView(cg, fileHint, { offset, limit, symbolsOnly });
|
|
2864
|
+
}
|
|
2865
|
+
const symbol = this.validateString(args.symbol, 'symbol');
|
|
2866
|
+
if (typeof symbol !== 'string')
|
|
2867
|
+
return symbol;
|
|
2405
2868
|
let matches = this.findSymbolMatches(cg, symbol);
|
|
2406
2869
|
if (matches.length === 0) {
|
|
2407
2870
|
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
|
@@ -2487,6 +2950,140 @@ class ToolHandler {
|
|
|
2487
2950
|
}
|
|
2488
2951
|
return this.textResult(this.truncateOutput(out.join('\n')));
|
|
2489
2952
|
}
|
|
2953
|
+
/**
|
|
2954
|
+
* FILE READ MODE: resolve `fileArg` (path or basename) to an indexed file and
|
|
2955
|
+
* read it like the Read tool — its current on-disk source with line numbers,
|
|
2956
|
+
* narrowable with `offset`/`limit` exactly as Read's are — preceded by a
|
|
2957
|
+
* one-line blast-radius header (which files depend on it). `symbolsOnly`
|
|
2958
|
+
* returns just the structural map (symbols + dependents) instead of source.
|
|
2959
|
+
*
|
|
2960
|
+
* Parity goal: the numbered source block is byte-for-byte the shape Read
|
|
2961
|
+
* returns (`<n>\t<line>`, no padding), so the agent treats it as a Read — only
|
|
2962
|
+
* faster (served from the index) and with the blast radius attached. Security:
|
|
2963
|
+
* yaml/properties files are summarized by key, never dumped (#383); reads go
|
|
2964
|
+
* through validatePathWithinRoot (#527).
|
|
2965
|
+
*/
|
|
2966
|
+
async handleFileView(cg, fileArg, opts = {}) {
|
|
2967
|
+
const normalize = (p) => p.replace(/\\/g, '/').replace(/^(?:\.?\/+)+/, '').replace(/\/+$/, '');
|
|
2968
|
+
const wantLower = normalize(fileArg).toLowerCase();
|
|
2969
|
+
const allFiles = cg.getFiles();
|
|
2970
|
+
if (allFiles.length === 0)
|
|
2971
|
+
return this.textResult('No files indexed. Run `codegraph index` first.');
|
|
2972
|
+
let resolved = allFiles.find((f) => f.path.toLowerCase() === wantLower);
|
|
2973
|
+
let candidates = [];
|
|
2974
|
+
if (!resolved) {
|
|
2975
|
+
candidates = allFiles.filter((f) => f.path.toLowerCase().endsWith('/' + wantLower));
|
|
2976
|
+
if (candidates.length === 1)
|
|
2977
|
+
resolved = candidates[0];
|
|
2978
|
+
}
|
|
2979
|
+
if (!resolved && candidates.length === 0) {
|
|
2980
|
+
candidates = allFiles.filter((f) => f.path.toLowerCase().includes(wantLower));
|
|
2981
|
+
if (candidates.length === 1)
|
|
2982
|
+
resolved = candidates[0];
|
|
2983
|
+
}
|
|
2984
|
+
if (!resolved && candidates.length > 1) {
|
|
2985
|
+
return this.textResult([`"${fileArg}" matches ${candidates.length} indexed files — pass a longer path:`, '',
|
|
2986
|
+
...candidates.slice(0, 25).map((f) => `- ${f.path}`)].join('\n'));
|
|
2987
|
+
}
|
|
2988
|
+
if (!resolved) {
|
|
2989
|
+
return this.textResult(`No indexed file matches "${fileArg}". Codegraph indexes source files; configs/docs it doesn't parse won't appear — Read those directly.`);
|
|
2990
|
+
}
|
|
2991
|
+
const filePath = resolved.path;
|
|
2992
|
+
const nodes = cg.getNodesInFile(filePath)
|
|
2993
|
+
.filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export')
|
|
2994
|
+
.sort((a, b) => a.startLine - b.startLine);
|
|
2995
|
+
const dependents = cg.getFileDependents(filePath);
|
|
2996
|
+
// Compact, one-line blast radius (codegraph's value-add over a plain Read).
|
|
2997
|
+
const depSummary = dependents.length
|
|
2998
|
+
? `used by ${dependents.length} file${dependents.length === 1 ? '' : 's'}: ${dependents.slice(0, 8).join(', ')}${dependents.length > 8 ? `, +${dependents.length - 8} more` : ''}`
|
|
2999
|
+
: 'no other indexed file depends on it';
|
|
3000
|
+
// Symbol-map renderer — for symbolsOnly, the config fallback, and read errors.
|
|
3001
|
+
const symbolMap = (heading, limit = 200) => {
|
|
3002
|
+
const lines = [heading];
|
|
3003
|
+
for (const n of nodes.slice(0, limit)) {
|
|
3004
|
+
const sig = n.signature ? ` ${n.signature.replace(/\s+/g, ' ').trim()}` : '';
|
|
3005
|
+
lines.push(`- \`${n.name}\` (${n.kind})${sig} — :${n.startLine}`);
|
|
3006
|
+
}
|
|
3007
|
+
if (nodes.length > limit)
|
|
3008
|
+
lines.push(`- … +${nodes.length - limit} more`);
|
|
3009
|
+
return lines;
|
|
3010
|
+
};
|
|
3011
|
+
// symbolsOnly → the cheap structural overview, no source.
|
|
3012
|
+
if (opts.symbolsOnly) {
|
|
3013
|
+
const out = [`**${filePath}** — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, ''];
|
|
3014
|
+
if (nodes.length)
|
|
3015
|
+
out.push(...symbolMap('### Symbols'));
|
|
3016
|
+
else
|
|
3017
|
+
out.push('_No indexed symbols in this file._');
|
|
3018
|
+
out.push('', '> Drop `symbolsOnly` (or pass `offset`/`limit`) to read the source, like Read.');
|
|
3019
|
+
return this.textResult(this.truncateOutput(out.join('\n')));
|
|
3020
|
+
}
|
|
3021
|
+
// SECURITY (#383): never dump a raw config/data file — a yaml/properties
|
|
3022
|
+
// line is `key: <secret>`. Summarize by key and point to a real Read.
|
|
3023
|
+
if (utils_1.CONFIG_LEAF_LANGUAGES.has(resolved.language)) {
|
|
3024
|
+
const out = [`**${filePath}** — configuration/data file, ${depSummary}`, ''];
|
|
3025
|
+
if (nodes.length)
|
|
3026
|
+
out.push(...symbolMap('### Keys (values withheld for safety)'));
|
|
3027
|
+
out.push('', '> Values may be secrets, so codegraph indexes keys only. Read the file directly if you need a value.');
|
|
3028
|
+
return this.textResult(this.truncateOutput(out.join('\n')));
|
|
3029
|
+
}
|
|
3030
|
+
// Read the current bytes from disk through the security chokepoint
|
|
3031
|
+
// (validatePathWithinRoot: blocks `../` traversal and symlink escapes, #527).
|
|
3032
|
+
const abs = (0, utils_1.validatePathWithinRoot)(cg.getProjectRoot(), filePath);
|
|
3033
|
+
let content = null;
|
|
3034
|
+
if (abs) {
|
|
3035
|
+
try {
|
|
3036
|
+
content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
3037
|
+
}
|
|
3038
|
+
catch {
|
|
3039
|
+
content = null;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (content === null) {
|
|
3043
|
+
const out = [`**${filePath}** — could not read from disk (it may have moved since indexing). ${depSummary}`, ''];
|
|
3044
|
+
if (nodes.length)
|
|
3045
|
+
out.push(...symbolMap('### Symbols'));
|
|
3046
|
+
out.push('', `> Read \`${filePath}\` directly for its current content.`);
|
|
3047
|
+
return this.textResult(this.truncateOutput(out.join('\n')));
|
|
3048
|
+
}
|
|
3049
|
+
// Split exactly as Read does — keep the trailing empty line a final newline
|
|
3050
|
+
// produces (Read numbers it too), so line numbers line up byte-for-byte.
|
|
3051
|
+
const fileLines = content.split('\n');
|
|
3052
|
+
const total = fileLines.length;
|
|
3053
|
+
// Read-parity windowing: `offset`/`limit` mean exactly what they do on Read
|
|
3054
|
+
// (1-based start line; max line count). Default: the whole file, capped like
|
|
3055
|
+
// Read at 2000 lines and bounded by a char budget that tracks explore's
|
|
3056
|
+
// proven-safe ~38k response ceiling. Overflow is stated explicitly (Read
|
|
3057
|
+
// paginates too) — never the silent 15k truncateOutput chop.
|
|
3058
|
+
const CHAR_BUDGET = 38000;
|
|
3059
|
+
const DEFAULT_LIMIT = 2000;
|
|
3060
|
+
const offset = Math.max(1, opts.offset ?? 1);
|
|
3061
|
+
if (offset > total) {
|
|
3062
|
+
return this.textResult(`**${filePath}** has ${total} line${total === 1 ? '' : 's'} — offset ${offset} is past the end. ${depSummary}`);
|
|
3063
|
+
}
|
|
3064
|
+
const maxLines = Math.max(1, opts.limit ?? DEFAULT_LIMIT);
|
|
3065
|
+
const start = offset - 1; // 0-based
|
|
3066
|
+
const header = `**${filePath}** — ${total} lines, ${nodes.length} symbol${nodes.length === 1 ? '' : 's'} · ${depSummary}`;
|
|
3067
|
+
// Numbered lines, byte-for-byte Read's shape: `<n>\t<line>`, no left-pad.
|
|
3068
|
+
const numbered = [];
|
|
3069
|
+
let used = header.length + 8;
|
|
3070
|
+
let i = start;
|
|
3071
|
+
for (; i < total && numbered.length < maxLines; i++) {
|
|
3072
|
+
const ln = `${i + 1}\t${fileLines[i]}`;
|
|
3073
|
+
if (used + ln.length + 1 > CHAR_BUDGET && numbered.length > 0)
|
|
3074
|
+
break;
|
|
3075
|
+
numbered.push(ln);
|
|
3076
|
+
used += ln.length + 1;
|
|
3077
|
+
}
|
|
3078
|
+
const shownEnd = start + numbered.length;
|
|
3079
|
+
const complete = offset === 1 && shownEnd >= total;
|
|
3080
|
+
const out = [header, '', ...numbered];
|
|
3081
|
+
if (!complete) {
|
|
3082
|
+
out.push('', `(lines ${offset}–${shownEnd} of ${total} — pass \`offset\`/\`limit\` for another range, or \`codegraph_node <symbol>\` for one symbol in full)`);
|
|
3083
|
+
}
|
|
3084
|
+
// Self-bounded to CHAR_BUDGET — do NOT route through truncateOutput (15k).
|
|
3085
|
+
return this.textResult(out.join('\n'));
|
|
3086
|
+
}
|
|
2490
3087
|
/** Render one symbol: details + (optional) body/outline + its caller/callee trail. */
|
|
2491
3088
|
async renderNodeSection(cg, node, includeCode) {
|
|
2492
3089
|
let code = null;
|
|
@@ -2955,15 +3552,36 @@ class ToolHandler {
|
|
|
2955
3552
|
}
|
|
2956
3553
|
return lines.join('\n');
|
|
2957
3554
|
}
|
|
2958
|
-
formatNodeList(nodes, title) {
|
|
3555
|
+
formatNodeList(nodes, title, labels) {
|
|
2959
3556
|
const lines = [`## ${title} (${nodes.length} found)`, ''];
|
|
2960
3557
|
for (const node of nodes) {
|
|
2961
3558
|
const location = node.startLine ? `:${node.startLine}` : '';
|
|
2962
|
-
// Compact: just name, kind, location
|
|
2963
|
-
|
|
3559
|
+
// Compact: just name, kind, location — plus the relationship when it
|
|
3560
|
+
// isn't a plain call (callback registration, instantiation, …).
|
|
3561
|
+
const label = labels?.get(node.id);
|
|
3562
|
+
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
|
|
2964
3563
|
}
|
|
2965
3564
|
return lines.join('\n');
|
|
2966
3565
|
}
|
|
3566
|
+
/**
|
|
3567
|
+
* Relationship label for a non-`calls` edge in callers/callees lists. A
|
|
3568
|
+
* function-as-value edge (#756) is the high-signal one: `callers(cb)`
|
|
3569
|
+
* showing "via callback registration" tells the agent this is where the
|
|
3570
|
+
* callback is WIRED, not where it's invoked.
|
|
3571
|
+
*/
|
|
3572
|
+
edgeLabel(edge) {
|
|
3573
|
+
if (edge.kind === 'calls')
|
|
3574
|
+
return null;
|
|
3575
|
+
if (edge.metadata?.fnRef === true)
|
|
3576
|
+
return 'callback registration';
|
|
3577
|
+
if (edge.kind === 'instantiates')
|
|
3578
|
+
return 'instantiation';
|
|
3579
|
+
if (edge.kind === 'imports')
|
|
3580
|
+
return 'import';
|
|
3581
|
+
if (edge.kind === 'references')
|
|
3582
|
+
return 'reference';
|
|
3583
|
+
return edge.kind;
|
|
3584
|
+
}
|
|
2967
3585
|
formatImpact(symbol, impact) {
|
|
2968
3586
|
const nodeCount = impact.nodes.size;
|
|
2969
3587
|
// Compact format: just list affected symbols grouped by file
|