agent-dir 0.5.0 → 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/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 { findDefinition, findExports, findFiles, findImports, findReferences, findSymbol, searchCode, searchFiles, } from "./tools/code.js";
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, deleteFile, listDirs, listFiles, patchFiles, readFile, readRange, writeFile, } from "./tools/files.js";
11
- import { gitCommit, gitDiff, gitLog, gitPush, gitRestore, gitStage, gitStatus, gitUnstage, } from "./tools/git.js";
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 DEFAULT_OUTPUT_BYTES = 32_000;
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", "Explore the current project through its optional CodeGraph MCP index. Returns CodeGraph native structured tool results for structural, flow, and symbol questions.", {
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: 1000000, default: DEFAULT_OUTPUT_BYTES },
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("read_files", "Read one or more UTF-8 project files in one call. Prefer read_range when only a bounded section is needed; batch related files here to reduce round trips.", {
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("write_files", "Create or completely replace one or more UTF-8 project files.", {
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: 1000000, default: DEFAULT_OUTPUT_BYTES },
159
- }, arrayOutput(stringSchema), ["pattern"]),
160
- tool("search_code", "Search source-like project files for text or a regular expression. Prefer this over reading files broadly; start with a narrow query and expand only if needed.", {
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: 1000000, default: DEFAULT_OUTPUT_BYTES },
111
+ maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
165
112
  }, arrayOutput(searchMatchSchema), ["query"]),
166
- tool("find_symbol", "Find likely symbol definitions across source files. Prefer this over reading whole modules when locating an implementation.", {
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: 1000000, default: DEFAULT_OUTPUT_BYTES },
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: 1000000, default: DEFAULT_OUTPUT_BYTES },
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: 1000000, default: DEFAULT_OUTPUT_BYTES },
128
+ maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
263
129
  }, { type: "object", additionalProperties: true }),
264
- tool("read_relevant", "Locate relevant source matches for a query and return bounded source context in one call. Prefer this when you know what you need but not the exact lines.", {
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: 1000000, default: DEFAULT_OUTPUT_BYTES },
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: 1000000, default: DEFAULT_OUTPUT_BYTES },
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: 1000000, default: DEFAULT_OUTPUT_BYTES },
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 = []) {
@@ -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
- const requestedProtocolVersion = typeof params.protocolVersion === "string" ? params.protocolVersion : undefined;
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 = isRecord(params._meta) ? params._meta : {};
304
+ const meta = params._meta;
499
305
  const version = meta[PROTOCOL_META];
500
- if (!legacyRequest && typeof version !== "string") {
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 (!legacyRequest && version !== PROTOCOL_VERSION) {
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 (!legacyRequest && !isRecord(meta[CLIENT_CAPABILITIES_META])) {
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 json({
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: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, await codeGraph.capability()),
336
+ instructions: buildDiscoveryInstructions(npmAllowed, allowedCommands, await codeGraph.capability()),
531
337
  codeGraph: await codeGraph.capability(),
532
- ttlMs: 0,
338
+ ttlMs: DISCOVERY_CACHE_TTL_MS,
533
339
  cacheScope: "private",
534
340
  },
535
341
  });
536
342
  case "tools/list": {
537
- const page = paginate(await availableTools(codeGraph, gitEnabled), params.cursor);
343
+ const page = paginate(await getAvailableTools(), params.cursor);
538
344
  mcpSuccess = true;
539
- return json({
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: 0,
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 availableTools(codeGraph, gitEnabled)).find((item) => item.name === toolName);
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, gitEnabled, notify, request.signal, codeGraph);
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 json({
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 json({
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: 0,
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 json({
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, gitEnabled, await codeGraph.capability()),
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 json({
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, gitEnabled), null, 2),
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 json({
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 json({
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 json({
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 json({
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 json({
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, gitEnabled) {
684
+ async function availableTools(codeGraph) {
879
685
  const capability = await codeGraph.capability();
880
- const tools = capability.status === "available" ||
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 buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, codeGraph) {
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- Prefer `codegraph_explore` for structural, symbol, and code-flow questions when available.\n- Agent Dir pins CodeGraph to this project root; do not attempt to select another project.\n- Current status: " +
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. Use project_overview when package metadata is unnecessary.\n- Prefer find_files for known filename patterns and list_dirs for one directory. Use list_files only when recursive discovery is genuinely required.\n- Prefer search_code/search_files before reading files. Search first, then read only the relevant ranges. Use read_relevant when one call can locate and return the needed source context.\n- Prefer find_symbol/find_definition/find_references for known symbols instead of scanning source files manually.\n- Prefer read_range for a bounded section. Use read_files to batch several already-identified files.\n- Prefer patch_files for targeted edits. 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 and Git log limits 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 diagnostics before expensive tests, linters, or typechecks when checking basic structural issues.\n- Run the narrowest relevant validation after a change; escalate only when required by the task or release workflow.\n- Before a broad Git diff, use git_status; then inspect only the relevant path or staged diff.\n- Never repeat a successful discovery/read just because another tool can provide the same information.\n\n## Preferred workflow\n1. Orient: project_overview.\n2. Locate: find_files/search_code/find_symbol/find_definition as appropriate.\n3. Read: read_range or batched read_files.\n4. Modify: patch_files for targeted changes; write_files for new/complete files.\n5. Validate: diagnostics first when applicable, then the narrowest relevant npm script.\n6. Review: git_status, then scoped git_diff when needed.\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 capability\nDedicated Git MCP tools are ${gitEnabled ? "enabled" : "disabled"} for this profile.\n\n## Tool selection\n- Discovery: project_context > locate > project_overview > find_files/list_dirs > list_files.\n- Code location: inspect_symbol > find_definition/find_symbol > locate > search_code > broad file reads.\n- File reading: read_relevant > read_range > targeted read_files > broad recursive reads.\n- Editing: patch_files > write_files for complete files.\n- Validation: diagnostics > targeted npm script > full check/test.\n- Git inspection: git_changes > git_status > scoped git_diff > full repository diff.\n\nThe execution policy and this guidance are generated from the running Agent Dir configuration; do not maintain a separate client-specific copy.${codeGraphSection}`;
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, gitEnabled) {
718
+ function buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands, codeGraph) {
906
719
  return {
907
720
  version: packageVersion,
908
- tools: baseTools
909
- .filter((item) => gitEnabled || !item.name.startsWith("git_"))
910
- .map((item) => item.name),
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
- focusedReading: "read_relevant",
922
- changeReview: "git_changes",
923
- discovery: ["find_files", "list_dirs", "search_code"],
924
- symbolNavigation: ["find_definition", "find_symbol", "find_references"],
925
- reading: ["read_range", "read_files"],
926
- editing: ["apply_changes", "patch_files", "write_files"],
927
- validation: ["validate", "diagnostics", "targeted npm script"],
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, gitEnabled, notify, signal, codeGraph) {
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
- return await codeGraph.explore(args.query, args.maxFiles === undefined ? undefined : Number(args.maxFiles));
958
- }
959
- if (name.startsWith("git_") && !gitEnabled) {
960
- throw new Error("Dedicated Git tools are disabled for this profile.");
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
- let remaining = maxBytes;
982
- const output = [];
983
- for (const p of stringArray(args.paths, "paths")) {
984
- const content = await readFile(root, p);
985
- const bounded = truncateText(content, remaining);
986
- output.push({ path: p, content: bounded.text });
987
- remaining -= Buffer.byteLength(bounded.text, "utf8");
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
- structuredContent = await applyFileChanges(root, changes, dryRun);
1024
- if (!dryRun)
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 "patch_files": {
1045
- const dryRun = args.dryRun === true;
1046
- const files = objectArray(args.files, "files").map((file) => ({
1047
- path: String(file.path),
1048
- patches: objectArray(file.patches, "patches").map((p) => ({
1049
- search: String(p.search),
1050
- replace: String(p.replace),
1051
- ...(p.count === undefined ? {} : { count: Number(p.count) }),
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
- structuredContent = {
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
- for (const script of stringArray(args.scripts, "scripts")) {
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
- for (const item of objectArray(args.commands, "commands")) {
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|git_changes|inspect)/.test(toolName)
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 > 1_000_000)
1778
- throw new Error("maxBytes must be an integer between 1024 and 1000000.");
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)