agent-dir 0.5.0 → 1.1.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/README.md +40 -421
- package/SECURITY.md +39 -0
- package/assets/agent-dir-banner.svg +38 -0
- package/dist/bin/agent-dir.js +9 -7
- package/dist/src/auth.js +3 -1
- package/dist/src/codegraph.d.ts +1 -0
- package/dist/src/codegraph.js +12 -0
- package/dist/src/config.js +0 -2
- package/dist/src/logging.d.ts +1 -1
- package/dist/src/logging.js +9 -69
- package/dist/src/mcp.d.ts +2 -1
- package/dist/src/mcp.js +191 -653
- package/dist/src/server.d.ts +2 -2
- package/dist/src/server.js +45 -146
- package/dist/src/telemetry/privacy.js +2 -0
- package/dist/src/telemetry/types.d.ts +2 -0
- package/dist/src/tools/code.d.ts +0 -12
- package/dist/src/tools/code.js +9 -29
- package/dist/src/tools/diagnostics.js +3 -1
- package/dist/src/tools/files.d.ts +2 -21
- package/dist/src/tools/files.js +85 -70
- package/dist/src/tools/git.d.ts +0 -7
- package/dist/src/tools/git.js +0 -64
- package/dist/src/tools/metadata.d.ts +0 -1
- package/dist/src/tools/metadata.js +0 -22
- package/dist/src/tools/skills.d.ts +1 -0
- package/dist/src/tools/skills.js +65 -0
- package/dist/src/types.d.ts +1 -2
- package/docs/CONFIGURATION.md +154 -0
- package/docs/MCP.md +106 -0
- package/docs/RELEASING.md +204 -0
- package/docs/telemetry.md +109 -0
- package/package.json +4 -1
package/dist/src/mcp.js
CHANGED
|
@@ -4,24 +4,23 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import { hostname, release, userInfo } from "node:os";
|
|
5
5
|
import { CodeGraphIntegration } from "./codegraph.js";
|
|
6
6
|
import { TelemetryRecorder } from "./telemetry/recorder.js";
|
|
7
|
-
import {
|
|
7
|
+
import { findFiles, findReferences, findSymbol, searchCode } from "./tools/code.js";
|
|
8
8
|
import { runCommand } from "./tools/commands.js";
|
|
9
9
|
import { diagnostics } from "./tools/diagnostics.js";
|
|
10
|
-
import { applyFileChanges,
|
|
11
|
-
import {
|
|
12
|
-
import { fileInfo, packageInfo, projectOverview } from "./tools/metadata.js";
|
|
10
|
+
import { applyFileChanges, readFile, readRange } from "./tools/files.js";
|
|
11
|
+
import { fileInfo, projectOverview } from "./tools/metadata.js";
|
|
13
12
|
import { runNpm } from "./tools/npm.js";
|
|
14
|
-
import { getSkill, listSkillResources, listSkills, readSkillDirectory, readSkillResource, skillResourceUrisForPath, } from "./tools/skills.js";
|
|
13
|
+
import { getSkill, invalidateSkillDiscoveryCache, listSkillResources, listSkills, readSkillDirectory, readSkillResource, skillResourceUrisForPath, } from "./tools/skills.js";
|
|
15
14
|
const require = createRequire(import.meta.url);
|
|
16
15
|
const packageVersion = require("../../package.json").version;
|
|
17
16
|
const PROTOCOL_VERSION = "2026-07-28";
|
|
18
|
-
const LEGACY_PROTOCOL_VERSION = "2025-11-25";
|
|
19
17
|
const SERVER_INFO = { name: "agent-dir", version: packageVersion };
|
|
20
18
|
const SERVER_INFO_META = { "io.modelcontextprotocol/serverInfo": SERVER_INFO };
|
|
21
19
|
const PROTOCOL_META = "io.modelcontextprotocol/protocolVersion";
|
|
22
20
|
const CLIENT_CAPABILITIES_META = "io.modelcontextprotocol/clientCapabilities";
|
|
23
21
|
const PAGE_SIZE = 50;
|
|
24
|
-
const
|
|
22
|
+
const DISCOVERY_CACHE_TTL_MS = 300_000;
|
|
23
|
+
const DEFAULT_OUTPUT_BYTES = 8_000;
|
|
25
24
|
const INSTRUCTIONS_URI = "agent-dir://instructions";
|
|
26
25
|
const CAPABILITIES_URI = "agent-dir://capabilities";
|
|
27
26
|
const SUBSCRIPTION_ID_META = "io.modelcontextprotocol/subscriptionId";
|
|
@@ -36,7 +35,6 @@ const arrayOutput = (items) => ({
|
|
|
36
35
|
type: "array",
|
|
37
36
|
items,
|
|
38
37
|
});
|
|
39
|
-
const fileEntrySchema = objectOutput({ path: stringSchema, type: { type: "string", enum: ["file", "directory"] } }, ["path", "type"]);
|
|
40
38
|
const searchMatchSchema = objectOutput({
|
|
41
39
|
path: stringSchema,
|
|
42
40
|
line: { type: "integer" },
|
|
@@ -49,37 +47,25 @@ const commandResultSchema = objectOutput({
|
|
|
49
47
|
stdout: stringSchema,
|
|
50
48
|
stderr: stringSchema,
|
|
51
49
|
}, ["command", "exitCode", "stdout", "stderr"]);
|
|
52
|
-
const codeGraphTool = tool("codegraph_explore", "
|
|
50
|
+
const codeGraphTool = tool("codegraph_explore", "Use first for most non-trivial code-understanding tasks: explain behavior, trace call/data flow, map symbol relationships, or assess change impact. Prefer this over repeated grep/read loops for structural questions. Use search_code/find_symbol/read_range for exact-text lookup and targeted follow-up. Treat returned source as read; avoid re-verifying without a concrete gap. Output is capped; large responses return a preview.", {
|
|
53
51
|
query: { type: "string", minLength: 1 },
|
|
54
|
-
maxFiles: { type: "integer", minimum: 1, maximum: 100 },
|
|
52
|
+
maxFiles: { type: "integer", minimum: 1, maximum: 100, default: 10 },
|
|
53
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
55
54
|
}, { type: "object" }, ["query"]);
|
|
56
55
|
const baseTools = [
|
|
57
|
-
tool("list_files", "List all visible project files and directories recursively up to the server's discovery depth. Prefer project_overview, find_files, search_code, or list_dirs for targeted discovery; use this only when broad recursive discovery is actually needed.", {
|
|
58
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
59
|
-
}, arrayOutput(fileEntrySchema)),
|
|
60
|
-
tool("list_dirs", "List files and directories directly inside one or more project directories.", {
|
|
61
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
62
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
63
|
-
}, arrayOutput(objectOutput({ path: stringSchema, entries: arrayOutput(fileEntrySchema) }, [
|
|
64
|
-
"path",
|
|
65
|
-
"entries",
|
|
66
|
-
])), ["paths"]),
|
|
67
56
|
tool("read_range", "Read a bounded line range from one UTF-8 project file.", {
|
|
68
57
|
path: pathSchema,
|
|
69
58
|
startLine: { type: "integer", minimum: 1 },
|
|
70
59
|
endLine: { type: "integer", minimum: 1 },
|
|
71
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
60
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
72
61
|
}, objectOutput({
|
|
73
62
|
path: stringSchema,
|
|
74
63
|
startLine: { type: "integer" },
|
|
75
64
|
endLine: { type: "integer" },
|
|
76
65
|
content: stringSchema,
|
|
66
|
+
truncated: { type: "boolean" },
|
|
77
67
|
}, ["path", "startLine", "endLine", "content"]), ["path", "startLine", "endLine"]),
|
|
78
|
-
tool("
|
|
79
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
80
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
81
|
-
}, arrayOutput(objectOutput({ path: stringSchema, content: stringSchema }, ["path", "content"])), ["paths"]),
|
|
82
|
-
tool("apply_changes", "Apply multiple file writes, exact patches, and deletes as one preflighted transaction. Use this for related multi-file edits when partial changes would be undesirable; supports dry-run previews.", {
|
|
68
|
+
tool("apply_changes", "Atomically apply related writes, exact patches, and deletes. Returns only per-file status; never returns file contents or diffs. Use dryRun=true to preview changes.", {
|
|
83
69
|
dryRun: { type: "boolean", default: false },
|
|
84
70
|
changes: {
|
|
85
71
|
type: "array",
|
|
@@ -110,72 +96,24 @@ const baseTools = [
|
|
|
110
96
|
changed: { type: "boolean" },
|
|
111
97
|
dryRun: { type: "boolean" },
|
|
112
98
|
}, ["kind", "path", "changed"])), ["changes"]),
|
|
113
|
-
tool("
|
|
114
|
-
dryRun: { type: "boolean", default: false },
|
|
115
|
-
files: {
|
|
116
|
-
type: "array",
|
|
117
|
-
items: objectOutput({ path: pathSchema, content: stringSchema }, ["path", "content"]),
|
|
118
|
-
minItems: 1,
|
|
119
|
-
},
|
|
120
|
-
}, arrayOutput(objectOutput({ path: stringSchema, written: { type: "boolean" } }, ["path", "written"])), ["files"]),
|
|
121
|
-
tool("patch_files", "Apply exact text replacements to one or more UTF-8 files. Prefer this for targeted edits instead of rewriting whole files; batch related patches in one call.", {
|
|
122
|
-
dryRun: { type: "boolean", default: false },
|
|
123
|
-
files: {
|
|
124
|
-
type: "array",
|
|
125
|
-
items: objectOutput({
|
|
126
|
-
path: pathSchema,
|
|
127
|
-
patches: {
|
|
128
|
-
type: "array",
|
|
129
|
-
items: objectOutput({
|
|
130
|
-
search: stringSchema,
|
|
131
|
-
replace: stringSchema,
|
|
132
|
-
count: { type: "integer", minimum: 1 },
|
|
133
|
-
}, ["search", "replace"]),
|
|
134
|
-
minItems: 1,
|
|
135
|
-
},
|
|
136
|
-
}, ["path", "patches"]),
|
|
137
|
-
minItems: 1,
|
|
138
|
-
},
|
|
139
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
140
|
-
}, arrayOutput(objectOutput({ path: stringSchema, applied: { type: "integer" }, content: stringSchema }, [
|
|
141
|
-
"path",
|
|
142
|
-
"applied",
|
|
143
|
-
"content",
|
|
144
|
-
])), ["files"]),
|
|
145
|
-
tool("delete_files", "Delete one or more project files.", {
|
|
146
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
147
|
-
dryRun: { type: "boolean", default: false },
|
|
148
|
-
}, arrayOutput(objectOutput({ path: stringSchema, deleted: { type: "boolean" } }, ["path", "deleted"])), ["paths"]),
|
|
149
|
-
tool("search_files", "Search text across visible project files. Prefer this over broad file reads; use search_code for source-only searches and keep maxResults tight.", {
|
|
150
|
-
query: stringSchema,
|
|
151
|
-
regex: { type: "boolean", default: false },
|
|
152
|
-
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
153
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
154
|
-
}, arrayOutput(searchMatchSchema), ["query"]),
|
|
155
|
-
tool("find_files", "Find project files by a simple glob pattern using * and ?. Prefer this over list_files when you know the filename pattern.", {
|
|
99
|
+
tool("find_files", "Find project paths by a simple glob pattern using * and ?. Returns {items, truncated}; use this for targeted filename discovery.", {
|
|
156
100
|
pattern: stringSchema,
|
|
157
101
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
158
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
159
|
-
}, arrayOutput(stringSchema),
|
|
160
|
-
|
|
102
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
103
|
+
}, objectOutput({ items: arrayOutput(stringSchema), truncated: { type: "boolean" } }, [
|
|
104
|
+
"items",
|
|
105
|
+
"truncated",
|
|
106
|
+
]), ["pattern"]),
|
|
107
|
+
tool("search_code", "Search source files by text or regex within an 8 KB default budget. Binary files and files over 1 MB are skipped; start narrow and expand only when needed.", {
|
|
161
108
|
query: stringSchema,
|
|
162
109
|
regex: { type: "boolean", default: false },
|
|
163
110
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
164
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
111
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
165
112
|
}, arrayOutput(searchMatchSchema), ["query"]),
|
|
166
|
-
tool("find_symbol", "Find
|
|
113
|
+
tool("find_symbol", "Find symbol definitions and locations. Use read_range for source context and find_references to trace usage.", {
|
|
167
114
|
symbol: stringSchema,
|
|
168
115
|
maxResults: { type: "integer", minimum: 1, maximum: 500, default: 100 },
|
|
169
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
170
|
-
}, arrayOutput(objectOutput({
|
|
171
|
-
...searchMatchSchema.properties,
|
|
172
|
-
symbol: stringSchema,
|
|
173
|
-
kind: stringSchema,
|
|
174
|
-
}, ["path", "line", "column", "text", "symbol", "kind"])), ["symbol"]),
|
|
175
|
-
tool("find_definition", "Find likely definitions of a symbol. Use this before reading a large file when you need one implementation.", {
|
|
176
|
-
symbol: stringSchema,
|
|
177
|
-
maxResults: { type: "integer", minimum: 1, maximum: 500, default: 100 },
|
|
178
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
116
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
179
117
|
}, arrayOutput(objectOutput({
|
|
180
118
|
...searchMatchSchema.properties,
|
|
181
119
|
symbol: stringSchema,
|
|
@@ -184,137 +122,30 @@ const baseTools = [
|
|
|
184
122
|
tool("find_references", "Find source-code references to a symbol. Use this instead of searching entire files when tracing usage of a known symbol.", {
|
|
185
123
|
symbol: stringSchema,
|
|
186
124
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
187
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
125
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
188
126
|
}, arrayOutput(searchMatchSchema), ["symbol"]),
|
|
189
|
-
tool("find_imports", "Find import and require statements in source files.", {
|
|
190
|
-
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
191
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
192
|
-
}, arrayOutput(objectOutput({
|
|
193
|
-
...searchMatchSchema.properties,
|
|
194
|
-
module: stringSchema,
|
|
195
|
-
kind: { type: "string", enum: ["import", "require"] },
|
|
196
|
-
}, ["path", "line", "column", "text", "module", "kind"]))),
|
|
197
|
-
tool("find_exports", "Find exported declarations in source files.", {
|
|
198
|
-
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
199
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
200
|
-
}, arrayOutput(objectOutput({
|
|
201
|
-
...searchMatchSchema.properties,
|
|
202
|
-
symbol: stringSchema,
|
|
203
|
-
kind: { type: "string", const: "export" },
|
|
204
|
-
}, ["path", "line", "column", "text", "symbol", "kind"]))),
|
|
205
|
-
tool("git_status", "Show Git working-tree status and branch information. Use this as the lightweight first Git check; do not run a full diff unless changes need inspection.", {
|
|
206
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
207
|
-
}, commandResultSchema),
|
|
208
|
-
tool("git_stage", "Stage one or more Git paths.", {
|
|
209
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
210
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
211
|
-
}, commandResultSchema, ["paths"]),
|
|
212
|
-
tool("git_unstage", "Unstage one or more Git paths without changing their working-tree contents.", {
|
|
213
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
214
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
215
|
-
}, commandResultSchema, ["paths"]),
|
|
216
|
-
tool("git_commit", "Create a Git commit with the currently staged changes.", {
|
|
217
|
-
message: stringSchema,
|
|
218
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
219
|
-
}, commandResultSchema, ["message"]),
|
|
220
|
-
tool("git_restore", "Restore one or more Git paths from the index, discarding unstaged working-tree changes.", {
|
|
221
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
222
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
223
|
-
}, commandResultSchema, ["paths"]),
|
|
224
|
-
tool("git_push", "Push the current Git branch to a remote.", {
|
|
225
|
-
remote: stringSchema,
|
|
226
|
-
branch: stringSchema,
|
|
227
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
228
|
-
}, commandResultSchema),
|
|
229
|
-
tool("git_diff", "Show a read-only Git diff, optionally staged or limited to a path. Prefer path-scoped or staged diffs over dumping the entire repository diff.", {
|
|
230
|
-
staged: { type: "boolean", default: false },
|
|
231
|
-
path: pathSchema,
|
|
232
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
233
|
-
}, commandResultSchema),
|
|
234
|
-
tool("git_log", "Show recent Git commits in a compact, read-only form. Keep the limit small and scope by path when history context is local.", {
|
|
235
|
-
limit: { type: "integer", minimum: 1, maximum: 200, default: 20 },
|
|
236
|
-
path: pathSchema,
|
|
237
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
238
|
-
}, commandResultSchema),
|
|
239
|
-
tool("locate", "Locate project code or files in one call. Use kind auto, code, text, or file.", {
|
|
240
|
-
query: stringSchema,
|
|
241
|
-
kind: { type: "string", enum: ["auto", "code", "text", "file"], default: "auto" },
|
|
242
|
-
regex: { type: "boolean", default: false },
|
|
243
|
-
maxResults: { type: "integer", minimum: 1, maximum: 100, default: 20 },
|
|
244
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
245
|
-
}, { type: "object", additionalProperties: true }, ["query"]),
|
|
246
|
-
tool("inspect_symbol", "Inspect a known symbol in one call, returning its definition, bounded source context, and optionally references.", {
|
|
247
|
-
symbol: stringSchema,
|
|
248
|
-
includeReferences: { type: "boolean", default: false },
|
|
249
|
-
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
250
|
-
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
251
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
252
|
-
}, { type: "object", additionalProperties: true }, ["symbol"]),
|
|
253
|
-
tool("inspect", "Inspect a project target in one call. Use kind auto, file, symbol, or code; returns bounded context and optional symbol references.", {
|
|
254
|
-
target: stringSchema,
|
|
255
|
-
kind: { type: "string", enum: ["auto", "file", "symbol", "code"], default: "auto" },
|
|
256
|
-
includeReferences: { type: "boolean", default: false },
|
|
257
|
-
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
258
|
-
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
259
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
260
|
-
}, { type: "object", additionalProperties: true }, ["target"]),
|
|
261
127
|
tool("project_context", "Return a compact LLM-oriented project snapshot combining project overview, package metadata, and active execution policy. Prefer this over separate orientation calls.", {
|
|
262
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
128
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
263
129
|
}, { type: "object", additionalProperties: true }),
|
|
264
|
-
tool("
|
|
265
|
-
query: stringSchema,
|
|
266
|
-
regex: { type: "boolean", default: false },
|
|
267
|
-
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
268
|
-
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
269
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
270
|
-
}, { type: "object", additionalProperties: true }, ["query"]),
|
|
271
|
-
tool("validate", "Run a unified project validation pass: lightweight diagnostics first, then explicitly requested allowed npm scripts. Use diagnostics for cheap checks and scripts only when executable validation is needed.", {
|
|
130
|
+
tool("validate", "Run a unified project validation pass: lightweight diagnostics first, then explicitly requested allowed npm scripts. Use level=diagnostics for cheap checks and request scripts only when executable validation is needed.", {
|
|
272
131
|
level: { type: "string", enum: ["diagnostics", "scripts", "full"], default: "diagnostics" },
|
|
273
|
-
scripts: { type: "array", items: stringSchema, minItems: 1 },
|
|
132
|
+
scripts: { type: "array", items: stringSchema, minItems: 1, maxItems: 10 },
|
|
274
133
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
275
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
276
|
-
}, { type: "object", additionalProperties: true }),
|
|
277
|
-
tool("git_changes", "Return a compact Git working-tree snapshot combining status and an optional diff. Prefer this over separate git_status and git_diff calls when reviewing changes.", {
|
|
278
|
-
includeDiff: { type: "boolean", default: false },
|
|
279
|
-
path: pathSchema,
|
|
280
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
134
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
281
135
|
}, { type: "object", additionalProperties: true }),
|
|
282
|
-
tool("project_overview", "Summarize project structure, languages, package managers, project metadata files, and Git state in one lightweight call. Prefer this before broad discovery.", {}, objectOutput({
|
|
283
|
-
root: stringSchema,
|
|
284
|
-
files: { type: "integer" },
|
|
285
|
-
directories: { type: "integer" },
|
|
286
|
-
languages: { type: "array" },
|
|
287
|
-
packageManagers: { type: "array", items: stringSchema },
|
|
288
|
-
projectFiles: { type: "array", items: stringSchema },
|
|
289
|
-
git: { type: "object" },
|
|
290
|
-
}, ["root", "files", "directories", "languages", "packageManagers", "projectFiles", "git"])),
|
|
291
|
-
tool("package_info", "Read normalized metadata from package.json when present.", {}, { type: "object", additionalProperties: true }),
|
|
292
|
-
tool("allowed_commands", "Show the npm scripts and commands explicitly allowed by the active server configuration.", {}, objectOutput({
|
|
293
|
-
npmScripts: { type: "array", items: stringSchema },
|
|
294
|
-
commands: { type: "array", items: stringSchema },
|
|
295
|
-
}, ["npmScripts", "commands"])),
|
|
296
136
|
tool("file_info", "Return filesystem metadata for a project path.", { path: pathSchema }, { type: "object", additionalProperties: true }, ["path"]),
|
|
297
|
-
tool("diagnostics", "Run lightweight project-independent diagnostics without invoking tests, linters, or typecheckers. Prefer this before expensive validation when checking basic file/JSON/conflict issues.", {
|
|
298
|
-
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
299
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
300
|
-
}, arrayOutput(objectOutput({
|
|
301
|
-
severity: { type: "string", enum: ["error", "warning", "info"] },
|
|
302
|
-
code: stringSchema,
|
|
303
|
-
path: stringSchema,
|
|
304
|
-
line: { type: "integer" },
|
|
305
|
-
message: stringSchema,
|
|
306
|
-
}, ["severity", "code", "message"]))),
|
|
307
137
|
tool("run_npm_batch", "Run one or more explicitly allowed npm scripts sequentially. Prefer the narrowest relevant script; batch independent scripts in one call and avoid running full check/test when a targeted check answers the question.", {
|
|
308
|
-
scripts: { type: "array", items: stringSchema, minItems: 1 },
|
|
309
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
138
|
+
scripts: { type: "array", items: stringSchema, minItems: 1, maxItems: 10 },
|
|
139
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
310
140
|
}, arrayOutput(commandResultSchema), ["scripts"]),
|
|
311
141
|
tool("run_command_batch", "Run one or more explicitly allowed commands sequentially without a shell. Prefer dedicated Agent Dir tools over generic commands because they return structured, bounded results with less token overhead.", {
|
|
312
142
|
commands: {
|
|
313
143
|
type: "array",
|
|
314
144
|
items: objectOutput({ command: stringSchema, args: { type: "array", items: stringSchema } }, ["command"]),
|
|
315
145
|
minItems: 1,
|
|
146
|
+
maxItems: 10,
|
|
316
147
|
},
|
|
317
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
148
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
318
149
|
}, arrayOutput(commandResultSchema), ["commands"]),
|
|
319
150
|
];
|
|
320
151
|
function tool(name, description, properties, outputSchema, required = []) {
|
|
@@ -331,8 +162,8 @@ function tool(name, description, properties, outputSchema, required = []) {
|
|
|
331
162
|
outputSchema,
|
|
332
163
|
};
|
|
333
164
|
}
|
|
334
|
-
export function createMcpHandler(root, commandConfig = {}, telemetry = { level: "none" }) {
|
|
335
|
-
const codeGraph = new CodeGraphIntegration(root);
|
|
165
|
+
export function createMcpHandler(root, commandConfig = {}, telemetry = { level: "none" }, codeGraphIntegration) {
|
|
166
|
+
const codeGraph = codeGraphIntegration ?? new CodeGraphIntegration(root);
|
|
336
167
|
const telemetryRecorder = new TelemetryRecorder({
|
|
337
168
|
level: telemetry.level,
|
|
338
169
|
persist: telemetry.persist ?? false,
|
|
@@ -349,13 +180,20 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
349
180
|
let sessionOutputBytes = 0;
|
|
350
181
|
let sessionEstimatedOutputTokens = 0;
|
|
351
182
|
const seenOutputFingerprints = new Set();
|
|
183
|
+
let cachedTools;
|
|
184
|
+
const getAvailableTools = async () => {
|
|
185
|
+
if (cachedTools && cachedTools.expiresAt > Date.now())
|
|
186
|
+
return cachedTools.value;
|
|
187
|
+
const value = await availableTools(codeGraph);
|
|
188
|
+
cachedTools = { expiresAt: Date.now() + DISCOVERY_CACHE_TTL_MS, value };
|
|
189
|
+
return value;
|
|
190
|
+
};
|
|
352
191
|
const telemetryProjectPromise = telemetry.level === "detailed" || telemetry.level === "diagnostic"
|
|
353
192
|
? buildTelemetryProjectContext(root, codeGraph).catch(() => undefined)
|
|
354
193
|
: undefined;
|
|
355
194
|
const npmAllowed = commandConfig.npm?.allowedScripts ?? [];
|
|
356
195
|
const allowedCommands = commandConfig.commands ?? [];
|
|
357
196
|
const blacklistedCommands = commandConfig.blacklistedCommands ?? [];
|
|
358
|
-
const gitEnabled = commandConfig.git ?? allowedCommands.includes("git");
|
|
359
197
|
const subscriptions = new Set();
|
|
360
198
|
const notify = (event) => {
|
|
361
199
|
for (const subscription of subscriptions) {
|
|
@@ -387,6 +225,17 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
387
225
|
};
|
|
388
226
|
const handler = async (request) => {
|
|
389
227
|
const requestStartedAt = performance.now();
|
|
228
|
+
let responseBytes;
|
|
229
|
+
let estimatedResponseTokens;
|
|
230
|
+
const trackedJson = (value, status = 200) => {
|
|
231
|
+
const serialized = JSON.stringify(value);
|
|
232
|
+
responseBytes = Buffer.byteLength(serialized, "utf8");
|
|
233
|
+
estimatedResponseTokens = estimateTokens(serialized);
|
|
234
|
+
return new Response(serialized, {
|
|
235
|
+
status,
|
|
236
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
237
|
+
});
|
|
238
|
+
};
|
|
390
239
|
if (sessionStartedAt === undefined)
|
|
391
240
|
sessionStartedAt = requestStartedAt;
|
|
392
241
|
sessionRequestCount += 1;
|
|
@@ -401,6 +250,8 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
401
250
|
method,
|
|
402
251
|
success: mcpSuccess,
|
|
403
252
|
durationMs: performance.now() - requestStartedAt,
|
|
253
|
+
...(responseBytes === undefined ? {} : { responseBytes }),
|
|
254
|
+
...(estimatedResponseTokens === undefined ? {} : { estimatedResponseTokens }),
|
|
404
255
|
...(telemetryRequestErrorCategory
|
|
405
256
|
? { errorCategory: telemetryRequestErrorCategory }
|
|
406
257
|
: {}),
|
|
@@ -445,64 +296,19 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
445
296
|
}
|
|
446
297
|
const params = message.params ?? {};
|
|
447
298
|
runtime = telemetryRuntime(params);
|
|
448
|
-
|
|
449
|
-
const legacyInitialize = message.method === "initialize";
|
|
450
|
-
const legacyRequest = !legacyInitialize &&
|
|
451
|
-
request.headers.get("mcp-protocol-version") === LEGACY_PROTOCOL_VERSION &&
|
|
452
|
-
!isRecord(params._meta);
|
|
453
|
-
if (legacyInitialize) {
|
|
454
|
-
const protocolHeader = request.headers.get("mcp-protocol-version");
|
|
455
|
-
if (protocolHeader !== null && protocolHeader !== LEGACY_PROTOCOL_VERSION) {
|
|
456
|
-
telemetryRequestErrorCategory = "invalid_input";
|
|
457
|
-
recordMcpRequest();
|
|
458
|
-
return jsonRpcError(message.id ?? null, -32020, "MCP-Protocol-Version does not match the initialize protocol version.", 400);
|
|
459
|
-
}
|
|
460
|
-
if (requestedProtocolVersion !== LEGACY_PROTOCOL_VERSION) {
|
|
461
|
-
telemetryRequestErrorCategory = "invalid_input";
|
|
462
|
-
recordMcpRequest();
|
|
463
|
-
return jsonRpcError(message.id ?? null, -32602, "Unsupported protocol version.", 400, {
|
|
464
|
-
supported: [LEGACY_PROTOCOL_VERSION],
|
|
465
|
-
requested: requestedProtocolVersion,
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
if (!isRecord(params.capabilities) || !isRecord(params.clientInfo)) {
|
|
469
|
-
telemetryRequestErrorCategory = "invalid_input";
|
|
470
|
-
recordMcpRequest();
|
|
471
|
-
return jsonRpcError(message.id ?? null, -32602, "Invalid initialize params.", 400);
|
|
472
|
-
}
|
|
473
|
-
mcpSuccess = true;
|
|
474
|
-
recordMcpRequest();
|
|
475
|
-
return json({
|
|
476
|
-
jsonrpc: "2.0",
|
|
477
|
-
id: message.id,
|
|
478
|
-
result: {
|
|
479
|
-
protocolVersion: LEGACY_PROTOCOL_VERSION,
|
|
480
|
-
capabilities: capabilities(),
|
|
481
|
-
serverInfo: SERVER_INFO,
|
|
482
|
-
instructions: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, await codeGraph.capability()),
|
|
483
|
-
},
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
if (legacyRequest) {
|
|
487
|
-
if (request.headers.get("mcp-protocol-version") !== LEGACY_PROTOCOL_VERSION) {
|
|
488
|
-
telemetryRequestErrorCategory = "invalid_input";
|
|
489
|
-
recordMcpRequest();
|
|
490
|
-
return jsonRpcError(message.id ?? null, -32020, "MCP-Protocol-Version must be 2025-11-25 for legacy requests.", 400);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
else if (!isRecord(params._meta)) {
|
|
299
|
+
if (!isRecord(params._meta)) {
|
|
494
300
|
telemetryRequestErrorCategory = "invalid_input";
|
|
495
301
|
recordMcpRequest();
|
|
496
302
|
return jsonRpcError(message.id ?? null, -32602, "Missing required request metadata.", 400);
|
|
497
303
|
}
|
|
498
|
-
const meta =
|
|
304
|
+
const meta = params._meta;
|
|
499
305
|
const version = meta[PROTOCOL_META];
|
|
500
|
-
if (
|
|
306
|
+
if (typeof version !== "string") {
|
|
501
307
|
telemetryRequestErrorCategory = "invalid_input";
|
|
502
308
|
recordMcpRequest();
|
|
503
309
|
return jsonRpcError(message.id ?? null, -32602, "Missing required protocol version metadata.", 400);
|
|
504
310
|
}
|
|
505
|
-
if (
|
|
311
|
+
if (version !== PROTOCOL_VERSION) {
|
|
506
312
|
telemetryRequestErrorCategory = "invalid_input";
|
|
507
313
|
recordMcpRequest();
|
|
508
314
|
return jsonRpcError(message.id ?? null, -32022, "Unsupported protocol version.", 400, {
|
|
@@ -510,7 +316,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
510
316
|
requested: version,
|
|
511
317
|
});
|
|
512
318
|
}
|
|
513
|
-
if (!
|
|
319
|
+
if (!isRecord(meta[CLIENT_CAPABILITIES_META])) {
|
|
514
320
|
telemetryRequestErrorCategory = "invalid_input";
|
|
515
321
|
recordMcpRequest();
|
|
516
322
|
return jsonRpcError(message.id ?? null, -32602, "Missing required client capabilities metadata.", 400);
|
|
@@ -519,7 +325,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
519
325
|
switch (message.method) {
|
|
520
326
|
case "server/discover":
|
|
521
327
|
mcpSuccess = true;
|
|
522
|
-
return
|
|
328
|
+
return trackedJson({
|
|
523
329
|
jsonrpc: "2.0",
|
|
524
330
|
id: message.id,
|
|
525
331
|
result: {
|
|
@@ -527,23 +333,23 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
527
333
|
supportedVersions: [PROTOCOL_VERSION],
|
|
528
334
|
capabilities: capabilities(),
|
|
529
335
|
_meta: SERVER_INFO_META,
|
|
530
|
-
instructions:
|
|
336
|
+
instructions: buildDiscoveryInstructions(npmAllowed, allowedCommands, await codeGraph.capability()),
|
|
531
337
|
codeGraph: await codeGraph.capability(),
|
|
532
|
-
ttlMs:
|
|
338
|
+
ttlMs: DISCOVERY_CACHE_TTL_MS,
|
|
533
339
|
cacheScope: "private",
|
|
534
340
|
},
|
|
535
341
|
});
|
|
536
342
|
case "tools/list": {
|
|
537
|
-
const page = paginate(await
|
|
343
|
+
const page = paginate(await getAvailableTools(), params.cursor);
|
|
538
344
|
mcpSuccess = true;
|
|
539
|
-
return
|
|
345
|
+
return trackedJson({
|
|
540
346
|
jsonrpc: "2.0",
|
|
541
347
|
id: message.id,
|
|
542
348
|
result: {
|
|
543
349
|
resultType: "complete",
|
|
544
350
|
tools: page.items,
|
|
545
351
|
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
546
|
-
ttlMs:
|
|
352
|
+
ttlMs: DISCOVERY_CACHE_TTL_MS,
|
|
547
353
|
cacheScope: "private",
|
|
548
354
|
_meta: SERVER_INFO_META,
|
|
549
355
|
},
|
|
@@ -555,7 +361,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
555
361
|
return jsonRpcError(message.id ?? null, -32602, "tools/call name must be a non-empty string.", 400);
|
|
556
362
|
}
|
|
557
363
|
const toolName = params.name;
|
|
558
|
-
const toolDefinition = (await
|
|
364
|
+
const toolDefinition = (await getAvailableTools()).find((item) => item.name === toolName);
|
|
559
365
|
if (!toolDefinition)
|
|
560
366
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
561
367
|
if (params.arguments !== undefined && !isRecord(params.arguments)) {
|
|
@@ -571,7 +377,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
571
377
|
sessionToolCallCount += 1;
|
|
572
378
|
const startedAt = performance.now();
|
|
573
379
|
try {
|
|
574
|
-
const result = await callTool(toolName, argumentsValue, root, npmAllowed, allowedCommands, blacklistedCommands,
|
|
380
|
+
const result = await callTool(toolName, argumentsValue, root, npmAllowed, allowedCommands, blacklistedCommands, notify, request.signal, codeGraph);
|
|
575
381
|
const durationMs = performance.now() - startedAt;
|
|
576
382
|
const count = resultCount(result);
|
|
577
383
|
const serializedResult = JSON.stringify(result);
|
|
@@ -612,7 +418,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
612
418
|
}, runtime, telemetryProject);
|
|
613
419
|
}
|
|
614
420
|
mcpSuccess = true;
|
|
615
|
-
return
|
|
421
|
+
return trackedJson({
|
|
616
422
|
jsonrpc: "2.0",
|
|
617
423
|
id: message.id,
|
|
618
424
|
result,
|
|
@@ -678,14 +484,14 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
678
484
|
];
|
|
679
485
|
const page = paginate(resources, params.cursor);
|
|
680
486
|
mcpSuccess = true;
|
|
681
|
-
return
|
|
487
|
+
return trackedJson({
|
|
682
488
|
jsonrpc: "2.0",
|
|
683
489
|
id: message.id,
|
|
684
490
|
result: {
|
|
685
491
|
resultType: "complete",
|
|
686
492
|
resources: page.items,
|
|
687
493
|
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
688
|
-
ttlMs:
|
|
494
|
+
ttlMs: DISCOVERY_CACHE_TTL_MS,
|
|
689
495
|
cacheScope: "private",
|
|
690
496
|
_meta: SERVER_INFO_META,
|
|
691
497
|
},
|
|
@@ -696,7 +502,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
696
502
|
return jsonRpcError(message.id ?? null, -32602, "resources/read uri must be a non-empty string.", 400);
|
|
697
503
|
if (params.uri === INSTRUCTIONS_URI) {
|
|
698
504
|
mcpSuccess = true;
|
|
699
|
-
return
|
|
505
|
+
return trackedJson({
|
|
700
506
|
jsonrpc: "2.0",
|
|
701
507
|
id: message.id,
|
|
702
508
|
result: {
|
|
@@ -705,7 +511,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
705
511
|
{
|
|
706
512
|
uri: INSTRUCTIONS_URI,
|
|
707
513
|
mimeType: "text/markdown",
|
|
708
|
-
text: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands,
|
|
514
|
+
text: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, await codeGraph.capability()),
|
|
709
515
|
},
|
|
710
516
|
],
|
|
711
517
|
ttlMs: 0,
|
|
@@ -716,7 +522,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
716
522
|
}
|
|
717
523
|
if (params.uri === CAPABILITIES_URI) {
|
|
718
524
|
mcpSuccess = true;
|
|
719
|
-
return
|
|
525
|
+
return trackedJson({
|
|
720
526
|
jsonrpc: "2.0",
|
|
721
527
|
id: message.id,
|
|
722
528
|
result: {
|
|
@@ -725,7 +531,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
725
531
|
{
|
|
726
532
|
uri: CAPABILITIES_URI,
|
|
727
533
|
mimeType: "application/json",
|
|
728
|
-
text: JSON.stringify(buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands,
|
|
534
|
+
text: JSON.stringify(buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands, await codeGraph.capability()), null, 2),
|
|
729
535
|
},
|
|
730
536
|
],
|
|
731
537
|
ttlMs: 0,
|
|
@@ -736,7 +542,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
736
542
|
}
|
|
737
543
|
const value = await readSkillResource(root, params.uri);
|
|
738
544
|
mcpSuccess = true;
|
|
739
|
-
return
|
|
545
|
+
return trackedJson({
|
|
740
546
|
jsonrpc: "2.0",
|
|
741
547
|
id: message.id,
|
|
742
548
|
result: {
|
|
@@ -754,7 +560,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
754
560
|
const resources = await readSkillDirectory(root, params.uri);
|
|
755
561
|
const page = paginate(resources, params.cursor);
|
|
756
562
|
mcpSuccess = true;
|
|
757
|
-
return
|
|
563
|
+
return trackedJson({
|
|
758
564
|
jsonrpc: "2.0",
|
|
759
565
|
id: message.id,
|
|
760
566
|
result: {
|
|
@@ -770,7 +576,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
770
576
|
case "skills/list": {
|
|
771
577
|
const page = paginate(await listSkills(root), params.cursor);
|
|
772
578
|
mcpSuccess = true;
|
|
773
|
-
return
|
|
579
|
+
return trackedJson({
|
|
774
580
|
jsonrpc: "2.0",
|
|
775
581
|
id: message.id,
|
|
776
582
|
result: {
|
|
@@ -792,7 +598,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
792
598
|
if (typeof params.uri !== "string" || params.uri.length === 0)
|
|
793
599
|
return jsonRpcError(message.id ?? null, -32602, "skills/get uri must be a non-empty string.", 400);
|
|
794
600
|
mcpSuccess = true;
|
|
795
|
-
return
|
|
601
|
+
return trackedJson({
|
|
796
602
|
jsonrpc: "2.0",
|
|
797
603
|
id: message.id,
|
|
798
604
|
result: {
|
|
@@ -811,7 +617,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
811
617
|
catch (error) {
|
|
812
618
|
telemetryRequestErrorCategory = classifyTelemetryError(error);
|
|
813
619
|
if (message.method === "tools/call")
|
|
814
|
-
return
|
|
620
|
+
return trackedJson({
|
|
815
621
|
jsonrpc: "2.0",
|
|
816
622
|
id: message.id,
|
|
817
623
|
result: {
|
|
@@ -875,16 +681,23 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
875
681
|
};
|
|
876
682
|
return handler;
|
|
877
683
|
}
|
|
878
|
-
async function availableTools(codeGraph
|
|
684
|
+
async function availableTools(codeGraph) {
|
|
879
685
|
const capability = await codeGraph.capability();
|
|
880
|
-
|
|
686
|
+
return capability.status === "available" ||
|
|
881
687
|
capability.status === "startup_failed" ||
|
|
882
688
|
capability.status === "runtime_failed"
|
|
883
689
|
? [...baseTools, codeGraphTool]
|
|
884
690
|
: baseTools;
|
|
885
|
-
return gitEnabled ? tools : tools.filter((item) => !item.name.startsWith("git_"));
|
|
886
691
|
}
|
|
887
|
-
function
|
|
692
|
+
function buildDiscoveryInstructions(npmAllowed, allowedCommands, codeGraph) {
|
|
693
|
+
const scripts = npmAllowed.length ? npmAllowed.join(", ") : "none";
|
|
694
|
+
const commands = allowedCommands.length ? allowedCommands.join(", ") : "none";
|
|
695
|
+
const graph = codeGraph.status === "available"
|
|
696
|
+
? " For non-trivial code understanding, call codegraph_explore first for behavior, call/data flow, symbol relationships, or change impact; avoid duplicating results with grep/read loops."
|
|
697
|
+
: "";
|
|
698
|
+
return `Use Agent Dir as the primary project interface. Minimize calls and output. Start with project_context; use find_files/search_code/find_symbol/find_references for targeted discovery, read_range for bounded context, and apply_changes for atomic edits. Use validate for diagnostics and allowed npm scripts; use run_command_batch for allowlisted commands, including Git when allowed.${graph} Batch related operations. Avoid recursive listings, whole-file reads, full diffs, and repeated discovery. Allowed npm scripts: ${scripts}. Allowed commands: ${commands}.`;
|
|
699
|
+
}
|
|
700
|
+
function buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, codeGraph) {
|
|
888
701
|
const npm = npmAllowed.length ? npmAllowed.map((item) => `- ${item}`).join("\n") : "- none";
|
|
889
702
|
const commands = allowedCommands.length
|
|
890
703
|
? allowedCommands.map((item) => `- ${item}`).join("\n")
|
|
@@ -895,44 +708,46 @@ function buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands
|
|
|
895
708
|
const codeGraphSection = codeGraph.status === "available" ||
|
|
896
709
|
codeGraph.status === "startup_failed" ||
|
|
897
710
|
codeGraph.status === "runtime_failed"
|
|
898
|
-
? "\n\n## CodeGraph\n- CodeGraph is an optional code-intelligence backend for this project.\n-
|
|
711
|
+
? "\n\n## CodeGraph\n- CodeGraph is an optional code-intelligence backend for this project.\n- REQUIRED FIRST STEP for non-trivial code understanding: call `codegraph_explore` before grep/search/read loops to explain behavior, trace call/data flow, map symbol relationships, or assess change impact. - Treat returned source as already read; do not re-run searches without a concrete gap.\n- Agent Dir pins CodeGraph to this project root; do not attempt to select another project.\n- Current status: " +
|
|
899
712
|
codeGraph.status +
|
|
900
713
|
(codeGraph.detail ? ` — ${codeGraph.detail}` : "") +
|
|
901
714
|
"."
|
|
902
715
|
: "";
|
|
903
|
-
return `# Agent Dir operating instructions\n\nUse Agent Dir as the primary project interface. Optimize for correctness with the minimum necessary tool calls, filesystem reads, command output, and context.\n\n## Efficiency rules\n- Start with the narrowest operation that can answer the question. Do not dump the repository, large files, or full command output when a targeted operation is sufficient.\n- Prefer project_context for initial orientation; it combines project structure, package metadata, and execution policy in one call.
|
|
716
|
+
return `# Agent Dir operating instructions\n\nUse Agent Dir as the primary project interface. Optimize for correctness with the minimum necessary tool calls, filesystem reads, command output, and context.\n\n## Efficiency rules\n- Start with the narrowest operation that can answer the question. Do not dump the repository, large files, or full command output when a targeted operation is sufficient.\n- Prefer project_context for initial orientation; it combines project structure, package metadata, and execution policy in one call. \n- Prefer find_files for known filename patterns\n- Prefer search_code before reading files. Search first, then read only the relevant ranges.\n- Prefer find_symbol/find_references for known symbols instead of scanning source files manually.\n- Prefer read_range for a bounded section.\n- Prefer apply_changes for atomic writes, patches, and deletions. Do not rewrite an entire file when a small exact replacement is sufficient.\n- Batch related reads, writes, patches, searches, and commands into one tool call when practical.\n- Keep maxResults small unless the initial result is insufficient.\n- Prefer dedicated Agent Dir tools over run_command_batch because dedicated tools return structured, bounded results.\n- Prefer validate for lightweight diagnostics and explicitly requested npm scripts.\n- Run the narrowest relevant validation after a change; escalate only when required by the task or release workflow.\n- Never repeat a successful discovery/read just because another tool can provide the same information.\n\n## Preferred workflow\n1. Orient: project_context.\n2. Locate: find_files/search_code/find_symbol as appropriate.\n3. Read: read_range.\n4. Modify: apply_changes for atomic writes, patches, and deletes.\n5. Validate: use validate for diagnostics or explicitly allowed npm scripts for executable checks.\n6. Review: use allowlisted Git commands through run_command_batch when \`git\` is enabled.\n\n## Execution policy\nAllowed npm scripts:\n${npm}\n\nAllowed executables:\n${commands}\n\nBlacklisted command prefixes:\n${blacklist}\n\nOnly use commands from the execution policy. A blacklisted command prefix overrides an allowed executable. Do not attempt to bypass it with shells or alternate executables.\n\n## Git operations\nUse \`run_command_batch\` for Git commands only when \`git\` is in the allowed executable list. The command allowlist and blacklist still apply; avoid destructive operations unless explicitly requested.\n\n## Tool selection\n- Discovery: project_context > find_files > search_code.\n- Code location: find_symbol > find_references > search_code.\n- File reading: read_range.\n- Editing: apply_changes for atomic multi-file edits.\n- Validation: validate > targeted npm script > full check/test.\n- Git operations: run_command_batch with the allowed \`git\` executable, respecting the configured blacklist.\n\nThe execution policy and this guidance are generated from the running Agent Dir configuration; do not maintain a separate client-specific copy.${codeGraphSection}`;
|
|
904
717
|
}
|
|
905
|
-
function buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands,
|
|
718
|
+
function buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands, codeGraph) {
|
|
906
719
|
return {
|
|
907
720
|
version: packageVersion,
|
|
908
|
-
tools:
|
|
909
|
-
.
|
|
910
|
-
.
|
|
721
|
+
tools: [
|
|
722
|
+
...baseTools.map((item) => item.name),
|
|
723
|
+
...(codeGraph.status === "available" ? ["codegraph_explore"] : []),
|
|
724
|
+
],
|
|
911
725
|
resources: [INSTRUCTIONS_URI, CAPABILITIES_URI],
|
|
912
726
|
execution: {
|
|
913
727
|
npmScripts: [...npmAllowed],
|
|
914
728
|
commands: [...allowedCommands],
|
|
915
729
|
blacklistedCommands: [...blacklistedCommands],
|
|
916
|
-
git: gitEnabled,
|
|
917
730
|
},
|
|
918
731
|
efficiency: {
|
|
919
732
|
preferred: {
|
|
920
733
|
orientation: "project_context",
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
discovery: ["find_files", "
|
|
924
|
-
symbolNavigation: ["
|
|
925
|
-
reading: ["read_range"
|
|
926
|
-
editing: ["apply_changes"
|
|
927
|
-
validation: ["validate", "
|
|
928
|
-
gitInspection: ["git_changes", "git_status", "git_diff", "git_log"],
|
|
734
|
+
...(codeGraph.status === "available" ? { structuralAnalysis: "codegraph_explore" } : {}),
|
|
735
|
+
focusedReading: "read_range",
|
|
736
|
+
discovery: ["find_files", "search_code"],
|
|
737
|
+
symbolNavigation: ["find_symbol", "find_references"],
|
|
738
|
+
reading: ["read_range"],
|
|
739
|
+
editing: ["apply_changes"],
|
|
740
|
+
validation: ["validate", "targeted npm script"],
|
|
929
741
|
},
|
|
930
742
|
avoid: [
|
|
931
743
|
"recursive discovery when targeted search is enough",
|
|
932
744
|
"whole-file reads when a line range is enough",
|
|
933
745
|
"full-repository diffs when a path-scoped diff is enough",
|
|
934
746
|
"expensive validation when a targeted check answers the question",
|
|
935
|
-
"generic command execution when a dedicated Agent Dir tool exists",
|
|
747
|
+
"generic command execution when a dedicated Agent Dir tool exists, except allowlisted Git operations",
|
|
748
|
+
...(codeGraph.status === "available"
|
|
749
|
+
? ["repeated grep/read loops for structural questions already answered by CodeGraph"]
|
|
750
|
+
: []),
|
|
936
751
|
],
|
|
937
752
|
},
|
|
938
753
|
};
|
|
@@ -944,7 +759,7 @@ function capabilities() {
|
|
|
944
759
|
extensions: { "io.modelcontextprotocol/skills": { directoryRead: true } },
|
|
945
760
|
};
|
|
946
761
|
}
|
|
947
|
-
async function callTool(name, args, root, npmAllowed, allowedCommands, blacklistedCommands,
|
|
762
|
+
async function callTool(name, args, root, npmAllowed, allowedCommands, blacklistedCommands, notify, signal, codeGraph) {
|
|
948
763
|
let structuredContent;
|
|
949
764
|
if (name === "codegraph_explore") {
|
|
950
765
|
if (!codeGraph)
|
|
@@ -954,41 +769,28 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
954
769
|
if (args.maxFiles !== undefined &&
|
|
955
770
|
(!Number.isInteger(args.maxFiles) || Number(args.maxFiles) < 1 || Number(args.maxFiles) > 100))
|
|
956
771
|
throw new Error("maxFiles must be an integer between 1 and 100.");
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
772
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
773
|
+
const result = await codeGraph.explore(args.query, args.maxFiles === undefined ? 10 : Number(args.maxFiles));
|
|
774
|
+
const serialized = JSON.stringify(result);
|
|
775
|
+
if (Buffer.byteLength(serialized, "utf8") <= maxBytes)
|
|
776
|
+
return result;
|
|
777
|
+
return {
|
|
778
|
+
truncated: true,
|
|
779
|
+
originalBytes: Buffer.byteLength(serialized, "utf8"),
|
|
780
|
+
message: "CodeGraph response exceeded the output budget. Narrow the query or lower maxFiles.",
|
|
781
|
+
preview: truncateText(serialized, Math.min(2_000, maxBytes - 256)).text,
|
|
782
|
+
};
|
|
961
783
|
}
|
|
962
784
|
switch (name) {
|
|
963
|
-
case "list_files": {
|
|
964
|
-
const items = await listFiles(root);
|
|
965
|
-
structuredContent = boundedItems(items, outputBytesArg(args.maxBytes)).items;
|
|
966
|
-
break;
|
|
967
|
-
}
|
|
968
|
-
case "list_dirs": {
|
|
969
|
-
const items = await listDirs(root, stringArray(args.paths, "paths"));
|
|
970
|
-
structuredContent = boundedItems(items, outputBytesArg(args.maxBytes)).items;
|
|
971
|
-
break;
|
|
972
|
-
}
|
|
973
785
|
case "read_range": {
|
|
974
|
-
const result = await readRange(root, String(args.path), Number(args.startLine), Number(args.endLine));
|
|
975
|
-
const bounded = truncateText(result.content, outputBytesArg(args.maxBytes));
|
|
976
|
-
structuredContent = { ...result, content: bounded.text };
|
|
977
|
-
break;
|
|
978
|
-
}
|
|
979
|
-
case "read_files": {
|
|
980
786
|
const maxBytes = outputBytesArg(args.maxBytes);
|
|
981
|
-
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
if (bounded.truncated || remaining <= 0)
|
|
989
|
-
break;
|
|
990
|
-
}
|
|
991
|
-
structuredContent = output;
|
|
787
|
+
const result = await readRange(root, String(args.path), Number(args.startLine), Number(args.endLine), Math.max(1, maxBytes - 256));
|
|
788
|
+
const bounded = truncateText(result.content, Math.max(1, maxBytes - 256));
|
|
789
|
+
structuredContent = {
|
|
790
|
+
...result,
|
|
791
|
+
content: bounded.text,
|
|
792
|
+
truncated: result.truncated || bounded.truncated,
|
|
793
|
+
};
|
|
992
794
|
break;
|
|
993
795
|
}
|
|
994
796
|
case "apply_changes": {
|
|
@@ -1020,247 +822,39 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
1020
822
|
const before = new Map();
|
|
1021
823
|
for (const p of affected)
|
|
1022
824
|
before.set(p, await skillResourceUrisForPath(root, p));
|
|
1023
|
-
|
|
1024
|
-
|
|
825
|
+
const applied = await applyFileChanges(root, changes, dryRun);
|
|
826
|
+
structuredContent = applied.map((item) => {
|
|
827
|
+
if (!isRecord(item))
|
|
828
|
+
return item;
|
|
829
|
+
const { content: _content, previousContent: _previousContent, ...summary } = item;
|
|
830
|
+
return summary;
|
|
831
|
+
});
|
|
832
|
+
if (!dryRun) {
|
|
833
|
+
invalidateSkillDiscoveryCache(root);
|
|
1025
834
|
for (const p of affected)
|
|
1026
835
|
await notifyResourceChange(notify, root, p, before.get(p) ?? []);
|
|
1027
|
-
break;
|
|
1028
|
-
}
|
|
1029
|
-
case "write_files": {
|
|
1030
|
-
const dryRun = args.dryRun === true;
|
|
1031
|
-
const output = [];
|
|
1032
|
-
for (const file of objectArray(args.files, "files")) {
|
|
1033
|
-
const p = String(file.path);
|
|
1034
|
-
const before = await skillResourceUrisForPath(root, p);
|
|
1035
|
-
if (!dryRun) {
|
|
1036
|
-
await writeFile(root, p, String(file.content));
|
|
1037
|
-
await notifyResourceChange(notify, root, p, before);
|
|
1038
|
-
}
|
|
1039
|
-
output.push({ path: p, written: true, ...(dryRun ? { dryRun: true } : {}) });
|
|
1040
836
|
}
|
|
1041
|
-
structuredContent = output;
|
|
1042
837
|
break;
|
|
1043
838
|
}
|
|
1044
|
-
case "
|
|
1045
|
-
const
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
})),
|
|
1053
|
-
}));
|
|
1054
|
-
const before = new Map();
|
|
1055
|
-
for (const file of files)
|
|
1056
|
-
before.set(file.path, await skillResourceUrisForPath(root, file.path));
|
|
1057
|
-
if (dryRun) {
|
|
1058
|
-
structuredContent = await patchFilesDryRun(root, files);
|
|
1059
|
-
}
|
|
1060
|
-
else {
|
|
1061
|
-
structuredContent = await patchFiles(root, files);
|
|
1062
|
-
}
|
|
1063
|
-
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1064
|
-
if (Array.isArray(structuredContent)) {
|
|
1065
|
-
structuredContent = boundedItems(structuredContent.map((item) => {
|
|
1066
|
-
if (!isRecord(item) || typeof item.content !== "string")
|
|
1067
|
-
return item;
|
|
1068
|
-
return { ...item, content: truncateText(item.content, maxBytes).text };
|
|
1069
|
-
}), maxBytes).items;
|
|
1070
|
-
}
|
|
1071
|
-
if (!dryRun) {
|
|
1072
|
-
for (const file of files)
|
|
1073
|
-
await notifyResourceChange(notify, root, file.path, before.get(file.path) ?? []);
|
|
1074
|
-
}
|
|
1075
|
-
break;
|
|
1076
|
-
}
|
|
1077
|
-
case "delete_files": {
|
|
1078
|
-
const dryRun = args.dryRun === true;
|
|
1079
|
-
const output = [];
|
|
1080
|
-
for (const p of stringArray(args.paths, "paths")) {
|
|
1081
|
-
const before = await skillResourceUrisForPath(root, p);
|
|
1082
|
-
if (!dryRun) {
|
|
1083
|
-
await deleteFile(root, p);
|
|
1084
|
-
await notifyResourceChange(notify, root, p, before);
|
|
1085
|
-
}
|
|
1086
|
-
output.push({ path: p, deleted: true, ...(dryRun ? { dryRun: true } : {}) });
|
|
1087
|
-
}
|
|
1088
|
-
structuredContent = output;
|
|
839
|
+
case "find_files": {
|
|
840
|
+
const maxResults = numberArg(args.maxResults, 200, 1000);
|
|
841
|
+
const results = await findFiles(root, String(args.pattern), maxResults);
|
|
842
|
+
const bounded = boundedItems(results, outputBytesArg(args.maxBytes));
|
|
843
|
+
structuredContent = {
|
|
844
|
+
items: bounded.items,
|
|
845
|
+
truncated: bounded.truncated || results.length >= maxResults,
|
|
846
|
+
};
|
|
1089
847
|
break;
|
|
1090
848
|
}
|
|
1091
|
-
case "search_files":
|
|
1092
|
-
structuredContent = await searchFiles(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
1093
|
-
break;
|
|
1094
|
-
case "find_files":
|
|
1095
|
-
structuredContent = boundedItems(await findFiles(root, String(args.pattern), numberArg(args.maxResults, 200, 1000)), outputBytesArg(args.maxBytes)).items;
|
|
1096
|
-
break;
|
|
1097
849
|
case "search_code":
|
|
1098
850
|
structuredContent = await searchCode(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
1099
851
|
break;
|
|
1100
852
|
case "find_symbol":
|
|
1101
853
|
structuredContent = await findSymbol(root, String(args.symbol), numberArg(args.maxResults, 100, 500), outputBytesArg(args.maxBytes));
|
|
1102
854
|
break;
|
|
1103
|
-
case "find_definition":
|
|
1104
|
-
structuredContent = await findDefinition(root, String(args.symbol), numberArg(args.maxResults, 100, 500), outputBytesArg(args.maxBytes));
|
|
1105
|
-
break;
|
|
1106
855
|
case "find_references":
|
|
1107
856
|
structuredContent = await findReferences(root, String(args.symbol), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
1108
857
|
break;
|
|
1109
|
-
case "find_imports":
|
|
1110
|
-
structuredContent = await findImports(root, numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
1111
|
-
break;
|
|
1112
|
-
case "find_exports":
|
|
1113
|
-
structuredContent = await findExports(root, numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
1114
|
-
break;
|
|
1115
|
-
case "git_status":
|
|
1116
|
-
structuredContent = boundCommandResult(await gitStatus(root), outputBytesArg(args.maxBytes));
|
|
1117
|
-
break;
|
|
1118
|
-
case "git_stage":
|
|
1119
|
-
structuredContent = boundCommandResult(await gitStage(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
1120
|
-
break;
|
|
1121
|
-
case "git_unstage":
|
|
1122
|
-
structuredContent = boundCommandResult(await gitUnstage(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
1123
|
-
break;
|
|
1124
|
-
case "git_commit":
|
|
1125
|
-
structuredContent = boundCommandResult(await gitCommit(root, String(args.message)), outputBytesArg(args.maxBytes));
|
|
1126
|
-
break;
|
|
1127
|
-
case "git_restore":
|
|
1128
|
-
structuredContent = boundCommandResult(await gitRestore(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
1129
|
-
break;
|
|
1130
|
-
case "git_push":
|
|
1131
|
-
structuredContent = boundCommandResult(await gitPush(root, args.remote === undefined ? undefined : String(args.remote), args.branch === undefined ? undefined : String(args.branch)), outputBytesArg(args.maxBytes));
|
|
1132
|
-
break;
|
|
1133
|
-
case "git_diff":
|
|
1134
|
-
structuredContent = boundCommandResult(await gitDiff(root, Boolean(args.staged), args.path === undefined ? undefined : String(args.path)), outputBytesArg(args.maxBytes));
|
|
1135
|
-
break;
|
|
1136
|
-
case "git_log":
|
|
1137
|
-
structuredContent = boundCommandResult(await gitLog(root, numberArg(args.limit, 20, 200), args.path === undefined ? undefined : String(args.path)), outputBytesArg(args.maxBytes));
|
|
1138
|
-
break;
|
|
1139
|
-
case "locate": {
|
|
1140
|
-
const query = String(args.query);
|
|
1141
|
-
const kind = typeof args.kind === "string" ? args.kind : "auto";
|
|
1142
|
-
const regex = args.regex === true;
|
|
1143
|
-
const maxResults = numberArg(args.maxResults, 20, 100);
|
|
1144
|
-
const resolvedKind = kind === "auto"
|
|
1145
|
-
? /[*?]/.test(query)
|
|
1146
|
-
? "file"
|
|
1147
|
-
: /\.(ts|tsx|js|jsx|py|go|rs|java|json|md|yml|yaml|css|html)$/.test(query)
|
|
1148
|
-
? "file"
|
|
1149
|
-
: "code"
|
|
1150
|
-
: kind;
|
|
1151
|
-
const items = resolvedKind === "file"
|
|
1152
|
-
? await findFiles(root, query, maxResults)
|
|
1153
|
-
: resolvedKind === "text"
|
|
1154
|
-
? await searchFiles(root, query, regex, maxResults)
|
|
1155
|
-
: await searchCode(root, query, regex, maxResults);
|
|
1156
|
-
const bounded = boundedItems(items, outputBytesArg(args.maxBytes));
|
|
1157
|
-
structuredContent = {
|
|
1158
|
-
query,
|
|
1159
|
-
kind: resolvedKind,
|
|
1160
|
-
items: bounded.items,
|
|
1161
|
-
count: bounded.items.length,
|
|
1162
|
-
truncated: bounded.truncated || items.length >= maxResults,
|
|
1163
|
-
};
|
|
1164
|
-
break;
|
|
1165
|
-
}
|
|
1166
|
-
case "inspect_symbol": {
|
|
1167
|
-
const symbol = String(args.symbol);
|
|
1168
|
-
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
1169
|
-
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
1170
|
-
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
1171
|
-
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
1172
|
-
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1173
|
-
const definitions = await findDefinition(root, symbol, maxResults, maxBytes);
|
|
1174
|
-
const definitionContext = await Promise.all(definitions.map(async (definition) => {
|
|
1175
|
-
const range = await readRange(root, definition.path, Math.max(1, definition.line - contextLines), definition.line + contextLines);
|
|
1176
|
-
return { ...definition, context: range.content };
|
|
1177
|
-
}));
|
|
1178
|
-
const references = args.includeReferences === true
|
|
1179
|
-
? await findReferences(root, symbol, maxResults, maxBytes)
|
|
1180
|
-
: undefined;
|
|
1181
|
-
const result = {
|
|
1182
|
-
symbol,
|
|
1183
|
-
definitions: definitionContext,
|
|
1184
|
-
...(references ? { references } : {}),
|
|
1185
|
-
};
|
|
1186
|
-
const bounded = truncateText(JSON.stringify(result), maxBytes);
|
|
1187
|
-
structuredContent = bounded.truncated
|
|
1188
|
-
? { symbol, result: bounded.text, truncated: true }
|
|
1189
|
-
: result;
|
|
1190
|
-
break;
|
|
1191
|
-
}
|
|
1192
|
-
case "inspect": {
|
|
1193
|
-
const target = String(args.target);
|
|
1194
|
-
const requestedKind = typeof args.kind === "string" ? args.kind : "auto";
|
|
1195
|
-
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
1196
|
-
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
1197
|
-
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
1198
|
-
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
1199
|
-
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1200
|
-
let kind = requestedKind;
|
|
1201
|
-
if (kind === "auto") {
|
|
1202
|
-
try {
|
|
1203
|
-
await readFile(root, target);
|
|
1204
|
-
kind = "file";
|
|
1205
|
-
}
|
|
1206
|
-
catch {
|
|
1207
|
-
kind = /[*?]/.test(target) ? "code" : "symbol";
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
if (kind === "file") {
|
|
1211
|
-
const content = await readFile(root, target);
|
|
1212
|
-
const boundedContent = truncateText(content, Math.max(1, maxBytes - 128));
|
|
1213
|
-
const result = {
|
|
1214
|
-
target,
|
|
1215
|
-
kind,
|
|
1216
|
-
path: target,
|
|
1217
|
-
content: boundedContent.text,
|
|
1218
|
-
truncated: boundedContent.truncated,
|
|
1219
|
-
};
|
|
1220
|
-
structuredContent = result;
|
|
1221
|
-
break;
|
|
1222
|
-
}
|
|
1223
|
-
if (kind === "symbol") {
|
|
1224
|
-
const definitions = await findDefinition(root, target, maxResults, maxBytes);
|
|
1225
|
-
const definitionsWithContext = await Promise.all(definitions.map(async (definition) => {
|
|
1226
|
-
const range = await readRange(root, definition.path, Math.max(1, definition.line - contextLines), definition.line + contextLines);
|
|
1227
|
-
return { ...definition, context: range.content };
|
|
1228
|
-
}));
|
|
1229
|
-
const references = args.includeReferences === true
|
|
1230
|
-
? await findReferences(root, target, maxResults, maxBytes)
|
|
1231
|
-
: undefined;
|
|
1232
|
-
const result = {
|
|
1233
|
-
target,
|
|
1234
|
-
kind,
|
|
1235
|
-
definitions: definitionsWithContext,
|
|
1236
|
-
...(references ? { references } : {}),
|
|
1237
|
-
};
|
|
1238
|
-
const bounded = truncateText(JSON.stringify(result), maxBytes);
|
|
1239
|
-
structuredContent = bounded.truncated
|
|
1240
|
-
? { target, kind, result: bounded.text, truncated: true }
|
|
1241
|
-
: result;
|
|
1242
|
-
break;
|
|
1243
|
-
}
|
|
1244
|
-
const matches = await searchCode(root, target, false, maxResults, maxBytes);
|
|
1245
|
-
const relevant = await Promise.all(matches.map(async (match) => {
|
|
1246
|
-
const range = await readRange(root, match.path, Math.max(1, match.line - contextLines), match.line + contextLines);
|
|
1247
|
-
return {
|
|
1248
|
-
...match,
|
|
1249
|
-
context: range.content,
|
|
1250
|
-
startLine: range.startLine,
|
|
1251
|
-
endLine: range.endLine,
|
|
1252
|
-
};
|
|
1253
|
-
}));
|
|
1254
|
-
const bounded = boundedItems(relevant, maxBytes);
|
|
1255
|
-
structuredContent = {
|
|
1256
|
-
target,
|
|
1257
|
-
kind: "code",
|
|
1258
|
-
matches: bounded.items,
|
|
1259
|
-
count: bounded.items.length,
|
|
1260
|
-
truncated: bounded.truncated || relevant.length >= maxResults,
|
|
1261
|
-
};
|
|
1262
|
-
break;
|
|
1263
|
-
}
|
|
1264
858
|
case "project_context": {
|
|
1265
859
|
const overview = await projectOverview(root);
|
|
1266
860
|
let pkg = { path: "package.json", exists: false };
|
|
@@ -1283,39 +877,12 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
1283
877
|
...overview,
|
|
1284
878
|
package: pkg,
|
|
1285
879
|
execution: { npmScripts: [...npmAllowed], commands: [...allowedCommands] },
|
|
1286
|
-
}), maxBytes);
|
|
880
|
+
}), Math.max(1, maxBytes - 128));
|
|
1287
881
|
structuredContent = bounded.truncated
|
|
1288
882
|
? { result: bounded.text, truncated: true }
|
|
1289
883
|
: JSON.parse(bounded.text);
|
|
1290
884
|
break;
|
|
1291
885
|
}
|
|
1292
|
-
case "read_relevant": {
|
|
1293
|
-
const query = String(args.query);
|
|
1294
|
-
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
1295
|
-
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
1296
|
-
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
1297
|
-
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
1298
|
-
const matches = await searchCode(root, query, Boolean(args.regex), maxResults, outputBytesArg(args.maxBytes));
|
|
1299
|
-
const relevant = await Promise.all(matches.map(async (match) => {
|
|
1300
|
-
const range = await readRange(root, match.path, Math.max(1, match.line - contextLines), match.line + contextLines);
|
|
1301
|
-
return {
|
|
1302
|
-
...match,
|
|
1303
|
-
context: range.content,
|
|
1304
|
-
startLine: range.startLine,
|
|
1305
|
-
endLine: range.endLine,
|
|
1306
|
-
};
|
|
1307
|
-
}));
|
|
1308
|
-
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1309
|
-
const bounded = boundedItems(relevant, Math.max(1, maxBytes - 128));
|
|
1310
|
-
const result = {
|
|
1311
|
-
query,
|
|
1312
|
-
matches: bounded.items,
|
|
1313
|
-
count: relevant.length,
|
|
1314
|
-
truncated: bounded.truncated || relevant.length >= maxResults,
|
|
1315
|
-
};
|
|
1316
|
-
structuredContent = result;
|
|
1317
|
-
break;
|
|
1318
|
-
}
|
|
1319
886
|
case "validate": {
|
|
1320
887
|
const level = typeof args.level === "string" ? args.level : "diagnostics";
|
|
1321
888
|
const maxResults = numberArg(args.maxResults, 200, 1000);
|
|
@@ -1328,6 +895,8 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
1328
895
|
: level === "full"
|
|
1329
896
|
? ["check", "test"].filter((script) => npmAllowed.includes(script))
|
|
1330
897
|
: [];
|
|
898
|
+
if (scripts.length > 10)
|
|
899
|
+
throw new Error("validate accepts at most 10 scripts per call.");
|
|
1331
900
|
const checks = [
|
|
1332
901
|
{
|
|
1333
902
|
kind: "diagnostics",
|
|
@@ -1347,60 +916,55 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
1347
916
|
if (result.exitCode !== 0)
|
|
1348
917
|
break;
|
|
1349
918
|
}
|
|
1350
|
-
|
|
919
|
+
const validation = {
|
|
1351
920
|
level,
|
|
1352
921
|
ok: checks.every((check) => check.ok === true),
|
|
1353
922
|
checks,
|
|
1354
923
|
scriptsRequested: scripts,
|
|
1355
924
|
};
|
|
925
|
+
const serialized = JSON.stringify(validation);
|
|
926
|
+
if (Buffer.byteLength(serialized, "utf8") <= maxBytes) {
|
|
927
|
+
structuredContent = validation;
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
const perCheckBudget = Math.max(256, Math.floor((maxBytes - 256) / checks.length));
|
|
931
|
+
const compactChecks = checks.map((check) => {
|
|
932
|
+
if (check.kind === "npm" && isRecord(check.result))
|
|
933
|
+
return { ...check, result: boundCommandResult(check.result, perCheckBudget) };
|
|
934
|
+
if (check.kind === "diagnostics" && Array.isArray(check.issues))
|
|
935
|
+
return { ...check, issues: boundedItems(check.issues, perCheckBudget).items };
|
|
936
|
+
return check;
|
|
937
|
+
});
|
|
938
|
+
const compact = { ...validation, checks: compactChecks, truncated: true };
|
|
939
|
+
const compactJson = JSON.stringify(compact);
|
|
940
|
+
if (Buffer.byteLength(compactJson, "utf8") <= maxBytes) {
|
|
941
|
+
structuredContent = compact;
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
structuredContent = {
|
|
945
|
+
level,
|
|
946
|
+
ok: validation.ok,
|
|
947
|
+
truncated: true,
|
|
948
|
+
checks: checks.map((check) => ({
|
|
949
|
+
kind: check.kind,
|
|
950
|
+
...(typeof check.script === "string" ? { script: check.script } : {}),
|
|
951
|
+
ok: check.ok,
|
|
952
|
+
...(typeof check.count === "number" ? { count: check.count } : {}),
|
|
953
|
+
})),
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
}
|
|
1356
957
|
break;
|
|
1357
958
|
}
|
|
1358
|
-
case "git_changes": {
|
|
1359
|
-
const status = await gitStatus(root);
|
|
1360
|
-
const includeDiff = Boolean(args.includeDiff);
|
|
1361
|
-
const path = args.path === undefined ? undefined : String(args.path);
|
|
1362
|
-
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1363
|
-
const diff = includeDiff ? await gitDiff(root, false, path) : undefined;
|
|
1364
|
-
const statusBytes = Buffer.byteLength(JSON.stringify(status), "utf8");
|
|
1365
|
-
const diffBudget = Math.max(0, maxBytes - statusBytes - 64);
|
|
1366
|
-
const boundedDiff = diff ? truncateText(diff.stdout, diffBudget) : undefined;
|
|
1367
|
-
const result = {
|
|
1368
|
-
status,
|
|
1369
|
-
...(diff
|
|
1370
|
-
? {
|
|
1371
|
-
diff: {
|
|
1372
|
-
...diff,
|
|
1373
|
-
stdout: boundedDiff?.text ?? "",
|
|
1374
|
-
truncated: boundedDiff?.truncated ?? false,
|
|
1375
|
-
},
|
|
1376
|
-
}
|
|
1377
|
-
: {}),
|
|
1378
|
-
};
|
|
1379
|
-
structuredContent = result;
|
|
1380
|
-
break;
|
|
1381
|
-
}
|
|
1382
|
-
case "project_overview":
|
|
1383
|
-
structuredContent = await projectOverview(root);
|
|
1384
|
-
break;
|
|
1385
|
-
case "package_info":
|
|
1386
|
-
structuredContent = await packageInfo(root);
|
|
1387
|
-
break;
|
|
1388
|
-
case "allowed_commands":
|
|
1389
|
-
structuredContent = {
|
|
1390
|
-
npmScripts: [...npmAllowed],
|
|
1391
|
-
commands: [...allowedCommands],
|
|
1392
|
-
blacklistedCommands: [...blacklistedCommands],
|
|
1393
|
-
};
|
|
1394
|
-
break;
|
|
1395
959
|
case "file_info":
|
|
1396
960
|
structuredContent = await fileInfo(root, String(args.path));
|
|
1397
961
|
break;
|
|
1398
|
-
case "diagnostics":
|
|
1399
|
-
structuredContent = boundedItems(await diagnostics(root, numberArg(args.maxResults, 200, 1000)), outputBytesArg(args.maxBytes)).items;
|
|
1400
|
-
break;
|
|
1401
962
|
case "run_npm_batch": {
|
|
1402
963
|
const output = [];
|
|
1403
|
-
|
|
964
|
+
const scripts = stringArray(args.scripts, "scripts");
|
|
965
|
+
if (scripts.length > 10)
|
|
966
|
+
throw new Error("run_npm_batch accepts at most 10 scripts per call.");
|
|
967
|
+
for (const script of scripts) {
|
|
1404
968
|
const value = await runNpm(root, script, 120000, npmAllowed, signal);
|
|
1405
969
|
output.push(value);
|
|
1406
970
|
if (value.exitCode !== 0)
|
|
@@ -1428,7 +992,10 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
1428
992
|
}
|
|
1429
993
|
case "run_command_batch": {
|
|
1430
994
|
const output = [];
|
|
1431
|
-
|
|
995
|
+
const commands = objectArray(args.commands, "commands");
|
|
996
|
+
if (commands.length > 10)
|
|
997
|
+
throw new Error("run_command_batch accepts at most 10 commands per call.");
|
|
998
|
+
for (const item of commands) {
|
|
1432
999
|
const value = await runCommand(root, String(item.command), Array.isArray(item.args) ? item.args.map(String) : [], allowedCommands, blacklistedCommands, 120000, signal);
|
|
1433
1000
|
output.push(value);
|
|
1434
1001
|
if (value.exitCode !== 0)
|
|
@@ -1684,7 +1251,7 @@ function toolTelemetryMetrics(toolName, result, seenOutputFingerprints) {
|
|
|
1684
1251
|
const duplicateOutputBytes = seenOutputFingerprints.has(outputFingerprint) ? outputBytes : 0;
|
|
1685
1252
|
seenOutputFingerprints.add(outputFingerprint);
|
|
1686
1253
|
return {
|
|
1687
|
-
...(paths.size > 0 && /^(write|patch|delete|read|file
|
|
1254
|
+
...(paths.size > 0 && /^(write|patch|delete|read|file)/.test(toolName)
|
|
1688
1255
|
? { affectedFiles: paths.size }
|
|
1689
1256
|
: {}),
|
|
1690
1257
|
...(truncated ? { truncated: true } : {}),
|
|
@@ -1703,8 +1270,6 @@ function commandTelemetryDescriptor(toolName) {
|
|
|
1703
1270
|
return { family: "command", operation: "batch" };
|
|
1704
1271
|
if (toolName === "validate")
|
|
1705
1272
|
return { family: "npm", operation: "validate" };
|
|
1706
|
-
if (toolName.startsWith("git_"))
|
|
1707
|
-
return { family: "git", operation: toolName.slice("git_".length) };
|
|
1708
1273
|
return undefined;
|
|
1709
1274
|
}
|
|
1710
1275
|
async function buildTelemetryProjectContext(root, codeGraph) {
|
|
@@ -1774,8 +1339,8 @@ function objectArray(value, name) {
|
|
|
1774
1339
|
}
|
|
1775
1340
|
function outputBytesArg(value) {
|
|
1776
1341
|
const result = value === undefined ? DEFAULT_OUTPUT_BYTES : Number(value);
|
|
1777
|
-
if (!Number.isInteger(result) || result < 1024 || result >
|
|
1778
|
-
throw new Error("maxBytes must be an integer between 1024 and
|
|
1342
|
+
if (!Number.isInteger(result) || result < 1024 || result > 64_000)
|
|
1343
|
+
throw new Error("maxBytes must be an integer between 1024 and 64000.");
|
|
1779
1344
|
return result;
|
|
1780
1345
|
}
|
|
1781
1346
|
function truncateText(value, maxBytes) {
|
|
@@ -1874,33 +1439,6 @@ function decodeCursor(value) {
|
|
|
1874
1439
|
throw new Error("Invalid pagination cursor.");
|
|
1875
1440
|
return offset;
|
|
1876
1441
|
}
|
|
1877
|
-
async function patchFilesDryRun(root, files) {
|
|
1878
|
-
return Promise.all(files.map(async ({ path: relativePath, patches }) => {
|
|
1879
|
-
let content = await readFile(root, relativePath);
|
|
1880
|
-
for (const patch of patches) {
|
|
1881
|
-
if (!patch.search)
|
|
1882
|
-
throw new Error("Patch search text cannot be empty.");
|
|
1883
|
-
const occurrences = content.split(patch.search).length - 1;
|
|
1884
|
-
const count = patch.count ?? 1;
|
|
1885
|
-
if (occurrences === 0)
|
|
1886
|
-
throw new Error(`Patch text was not found in '${relativePath}'.`);
|
|
1887
|
-
if (!Number.isInteger(count) || count < 1)
|
|
1888
|
-
throw new Error("Patch count must be a positive integer.");
|
|
1889
|
-
if (occurrences < count)
|
|
1890
|
-
throw new Error(`Patch text occurs ${occurrences} time(s), but ${count} replacement(s) were requested.`);
|
|
1891
|
-
let offset = 0;
|
|
1892
|
-
for (let index = 0; index < count; index += 1) {
|
|
1893
|
-
const position = content.indexOf(patch.search, offset);
|
|
1894
|
-
content =
|
|
1895
|
-
content.slice(0, position) +
|
|
1896
|
-
patch.replace +
|
|
1897
|
-
content.slice(position + patch.search.length);
|
|
1898
|
-
offset = position + patch.replace.length;
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
return { path: relativePath, applied: patches.length, content, dryRun: true };
|
|
1902
|
-
}));
|
|
1903
|
-
}
|
|
1904
1442
|
async function notifyResourceChange(notify, root, pathValue, before) {
|
|
1905
1443
|
const after = await skillResourceUrisForPath(root, pathValue);
|
|
1906
1444
|
if (before.length === 0 && after.length === 0)
|