agent-dir 0.4.1 → 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/README.md +40 -374
- package/SECURITY.md +39 -0
- package/assets/agent-dir-banner.svg +38 -0
- package/dist/bin/agent-dir.js +105 -51
- package/dist/src/auth.js +3 -1
- package/dist/src/config.js +31 -3
- package/dist/src/logging.d.ts +1 -1
- package/dist/src/logging.js +13 -70
- package/dist/src/mcp.js +304 -665
- package/dist/src/server.d.ts +1 -1
- package/dist/src/server.js +41 -146
- package/dist/src/setup.js +67 -6
- package/dist/src/telemetry/privacy.js +48 -0
- package/dist/src/telemetry/recorder.js +8 -17
- package/dist/src/telemetry/storage.d.ts +9 -0
- package/dist/src/telemetry/storage.js +133 -0
- package/dist/src/telemetry/types.d.ts +24 -0
- package/dist/src/tools/code.d.ts +0 -12
- package/dist/src/tools/code.js +9 -29
- package/dist/src/tools/commands.d.ts +7 -0
- package/dist/src/tools/commands.js +63 -5
- 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 +10 -68
- package/dist/src/tools/metadata.d.ts +0 -1
- package/dist/src/tools/metadata.js +0 -22
- package/dist/src/tools/npm.js +7 -5
- package/dist/src/tools/skills.d.ts +1 -0
- package/dist/src/tools/skills.js +65 -0
- package/dist/src/tunnel.js +35 -18
- package/dist/src/types.d.ts +16 -4
- package/docs/CONFIGURATION.md +154 -0
- package/docs/MCP.md +106 -0
- package/docs/RELEASING.md +213 -0
- package/docs/telemetry.md +109 -0
- package/package.json +4 -1
package/dist/src/mcp.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
|
-
import { hostname, userInfo } from "node:os";
|
|
4
|
+
import { hostname, release, userInfo } from "node:os";
|
|
4
5
|
import { CodeGraphIntegration } from "./codegraph.js";
|
|
5
6
|
import { TelemetryRecorder } from "./telemetry/recorder.js";
|
|
6
|
-
import {
|
|
7
|
+
import { findFiles, findReferences, findSymbol, searchCode } from "./tools/code.js";
|
|
7
8
|
import { runCommand } from "./tools/commands.js";
|
|
8
9
|
import { diagnostics } from "./tools/diagnostics.js";
|
|
9
|
-
import { applyFileChanges,
|
|
10
|
-
import {
|
|
11
|
-
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";
|
|
12
12
|
import { runNpm } from "./tools/npm.js";
|
|
13
|
-
import { getSkill, listSkillResources, listSkills, readSkillDirectory, readSkillResource, skillResourceUrisForPath, } from "./tools/skills.js";
|
|
13
|
+
import { getSkill, invalidateSkillDiscoveryCache, listSkillResources, listSkills, readSkillDirectory, readSkillResource, skillResourceUrisForPath, } from "./tools/skills.js";
|
|
14
14
|
const require = createRequire(import.meta.url);
|
|
15
15
|
const packageVersion = require("../../package.json").version;
|
|
16
16
|
const PROTOCOL_VERSION = "2026-07-28";
|
|
17
|
-
const LEGACY_PROTOCOL_VERSION = "2025-11-25";
|
|
18
17
|
const SERVER_INFO = { name: "agent-dir", version: packageVersion };
|
|
19
18
|
const SERVER_INFO_META = { "io.modelcontextprotocol/serverInfo": SERVER_INFO };
|
|
20
19
|
const PROTOCOL_META = "io.modelcontextprotocol/protocolVersion";
|
|
21
20
|
const CLIENT_CAPABILITIES_META = "io.modelcontextprotocol/clientCapabilities";
|
|
22
21
|
const PAGE_SIZE = 50;
|
|
23
|
-
const
|
|
22
|
+
const DISCOVERY_CACHE_TTL_MS = 300_000;
|
|
23
|
+
const DEFAULT_OUTPUT_BYTES = 8_000;
|
|
24
24
|
const INSTRUCTIONS_URI = "agent-dir://instructions";
|
|
25
25
|
const CAPABILITIES_URI = "agent-dir://capabilities";
|
|
26
26
|
const SUBSCRIPTION_ID_META = "io.modelcontextprotocol/subscriptionId";
|
|
@@ -35,7 +35,6 @@ const arrayOutput = (items) => ({
|
|
|
35
35
|
type: "array",
|
|
36
36
|
items,
|
|
37
37
|
});
|
|
38
|
-
const fileEntrySchema = objectOutput({ path: stringSchema, type: { type: "string", enum: ["file", "directory"] } }, ["path", "type"]);
|
|
39
38
|
const searchMatchSchema = objectOutput({
|
|
40
39
|
path: stringSchema,
|
|
41
40
|
line: { type: "integer" },
|
|
@@ -48,37 +47,25 @@ const commandResultSchema = objectOutput({
|
|
|
48
47
|
stdout: stringSchema,
|
|
49
48
|
stderr: stringSchema,
|
|
50
49
|
}, ["command", "exitCode", "stdout", "stderr"]);
|
|
51
|
-
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.", {
|
|
52
51
|
query: { type: "string", minLength: 1 },
|
|
53
|
-
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 },
|
|
54
54
|
}, { type: "object" }, ["query"]);
|
|
55
55
|
const baseTools = [
|
|
56
|
-
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.", {
|
|
57
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
58
|
-
}, arrayOutput(fileEntrySchema)),
|
|
59
|
-
tool("list_dirs", "List files and directories directly inside one or more project directories.", {
|
|
60
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
61
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
62
|
-
}, arrayOutput(objectOutput({ path: stringSchema, entries: arrayOutput(fileEntrySchema) }, [
|
|
63
|
-
"path",
|
|
64
|
-
"entries",
|
|
65
|
-
])), ["paths"]),
|
|
66
56
|
tool("read_range", "Read a bounded line range from one UTF-8 project file.", {
|
|
67
57
|
path: pathSchema,
|
|
68
58
|
startLine: { type: "integer", minimum: 1 },
|
|
69
59
|
endLine: { type: "integer", minimum: 1 },
|
|
70
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
60
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
71
61
|
}, objectOutput({
|
|
72
62
|
path: stringSchema,
|
|
73
63
|
startLine: { type: "integer" },
|
|
74
64
|
endLine: { type: "integer" },
|
|
75
65
|
content: stringSchema,
|
|
66
|
+
truncated: { type: "boolean" },
|
|
76
67
|
}, ["path", "startLine", "endLine", "content"]), ["path", "startLine", "endLine"]),
|
|
77
|
-
tool("
|
|
78
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
79
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
80
|
-
}, arrayOutput(objectOutput({ path: stringSchema, content: stringSchema }, ["path", "content"])), ["paths"]),
|
|
81
|
-
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.", {
|
|
82
69
|
dryRun: { type: "boolean", default: false },
|
|
83
70
|
changes: {
|
|
84
71
|
type: "array",
|
|
@@ -109,72 +96,24 @@ const baseTools = [
|
|
|
109
96
|
changed: { type: "boolean" },
|
|
110
97
|
dryRun: { type: "boolean" },
|
|
111
98
|
}, ["kind", "path", "changed"])), ["changes"]),
|
|
112
|
-
tool("
|
|
113
|
-
dryRun: { type: "boolean", default: false },
|
|
114
|
-
files: {
|
|
115
|
-
type: "array",
|
|
116
|
-
items: objectOutput({ path: pathSchema, content: stringSchema }, ["path", "content"]),
|
|
117
|
-
minItems: 1,
|
|
118
|
-
},
|
|
119
|
-
}, arrayOutput(objectOutput({ path: stringSchema, written: { type: "boolean" } }, ["path", "written"])), ["files"]),
|
|
120
|
-
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.", {
|
|
121
|
-
dryRun: { type: "boolean", default: false },
|
|
122
|
-
files: {
|
|
123
|
-
type: "array",
|
|
124
|
-
items: objectOutput({
|
|
125
|
-
path: pathSchema,
|
|
126
|
-
patches: {
|
|
127
|
-
type: "array",
|
|
128
|
-
items: objectOutput({
|
|
129
|
-
search: stringSchema,
|
|
130
|
-
replace: stringSchema,
|
|
131
|
-
count: { type: "integer", minimum: 1 },
|
|
132
|
-
}, ["search", "replace"]),
|
|
133
|
-
minItems: 1,
|
|
134
|
-
},
|
|
135
|
-
}, ["path", "patches"]),
|
|
136
|
-
minItems: 1,
|
|
137
|
-
},
|
|
138
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
139
|
-
}, arrayOutput(objectOutput({ path: stringSchema, applied: { type: "integer" }, content: stringSchema }, [
|
|
140
|
-
"path",
|
|
141
|
-
"applied",
|
|
142
|
-
"content",
|
|
143
|
-
])), ["files"]),
|
|
144
|
-
tool("delete_files", "Delete one or more project files.", {
|
|
145
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
146
|
-
dryRun: { type: "boolean", default: false },
|
|
147
|
-
}, arrayOutput(objectOutput({ path: stringSchema, deleted: { type: "boolean" } }, ["path", "deleted"])), ["paths"]),
|
|
148
|
-
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.", {
|
|
149
|
-
query: stringSchema,
|
|
150
|
-
regex: { type: "boolean", default: false },
|
|
151
|
-
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
152
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
153
|
-
}, arrayOutput(searchMatchSchema), ["query"]),
|
|
154
|
-
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.", {
|
|
155
100
|
pattern: stringSchema,
|
|
156
101
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
157
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
158
|
-
}, arrayOutput(stringSchema),
|
|
159
|
-
|
|
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.", {
|
|
160
108
|
query: stringSchema,
|
|
161
109
|
regex: { type: "boolean", default: false },
|
|
162
110
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
163
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
111
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
164
112
|
}, arrayOutput(searchMatchSchema), ["query"]),
|
|
165
|
-
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.", {
|
|
166
114
|
symbol: stringSchema,
|
|
167
115
|
maxResults: { type: "integer", minimum: 1, maximum: 500, default: 100 },
|
|
168
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
169
|
-
}, arrayOutput(objectOutput({
|
|
170
|
-
...searchMatchSchema.properties,
|
|
171
|
-
symbol: stringSchema,
|
|
172
|
-
kind: stringSchema,
|
|
173
|
-
}, ["path", "line", "column", "text", "symbol", "kind"])), ["symbol"]),
|
|
174
|
-
tool("find_definition", "Find likely definitions of a symbol. Use this before reading a large file when you need one implementation.", {
|
|
175
|
-
symbol: stringSchema,
|
|
176
|
-
maxResults: { type: "integer", minimum: 1, maximum: 500, default: 100 },
|
|
177
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
116
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
178
117
|
}, arrayOutput(objectOutput({
|
|
179
118
|
...searchMatchSchema.properties,
|
|
180
119
|
symbol: stringSchema,
|
|
@@ -183,137 +122,30 @@ const baseTools = [
|
|
|
183
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.", {
|
|
184
123
|
symbol: stringSchema,
|
|
185
124
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
186
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
125
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
187
126
|
}, arrayOutput(searchMatchSchema), ["symbol"]),
|
|
188
|
-
tool("find_imports", "Find import and require statements in source files.", {
|
|
189
|
-
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
190
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
191
|
-
}, arrayOutput(objectOutput({
|
|
192
|
-
...searchMatchSchema.properties,
|
|
193
|
-
module: stringSchema,
|
|
194
|
-
kind: { type: "string", enum: ["import", "require"] },
|
|
195
|
-
}, ["path", "line", "column", "text", "module", "kind"]))),
|
|
196
|
-
tool("find_exports", "Find exported declarations in source files.", {
|
|
197
|
-
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
198
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
199
|
-
}, arrayOutput(objectOutput({
|
|
200
|
-
...searchMatchSchema.properties,
|
|
201
|
-
symbol: stringSchema,
|
|
202
|
-
kind: { type: "string", const: "export" },
|
|
203
|
-
}, ["path", "line", "column", "text", "symbol", "kind"]))),
|
|
204
|
-
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.", {
|
|
205
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
206
|
-
}, commandResultSchema),
|
|
207
|
-
tool("git_stage", "Stage one or more Git paths.", {
|
|
208
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
209
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
210
|
-
}, commandResultSchema, ["paths"]),
|
|
211
|
-
tool("git_unstage", "Unstage one or more Git paths without changing their working-tree contents.", {
|
|
212
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
213
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
214
|
-
}, commandResultSchema, ["paths"]),
|
|
215
|
-
tool("git_commit", "Create a Git commit with the currently staged changes.", {
|
|
216
|
-
message: stringSchema,
|
|
217
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
218
|
-
}, commandResultSchema, ["message"]),
|
|
219
|
-
tool("git_restore", "Restore one or more Git paths from the index, discarding unstaged working-tree changes.", {
|
|
220
|
-
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
221
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
222
|
-
}, commandResultSchema, ["paths"]),
|
|
223
|
-
tool("git_push", "Push the current Git branch to a remote.", {
|
|
224
|
-
remote: stringSchema,
|
|
225
|
-
branch: stringSchema,
|
|
226
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
227
|
-
}, commandResultSchema),
|
|
228
|
-
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.", {
|
|
229
|
-
staged: { type: "boolean", default: false },
|
|
230
|
-
path: pathSchema,
|
|
231
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
232
|
-
}, commandResultSchema),
|
|
233
|
-
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.", {
|
|
234
|
-
limit: { type: "integer", minimum: 1, maximum: 200, default: 20 },
|
|
235
|
-
path: pathSchema,
|
|
236
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
237
|
-
}, commandResultSchema),
|
|
238
|
-
tool("locate", "Locate project code or files in one call. Use kind auto, code, text, or file.", {
|
|
239
|
-
query: stringSchema,
|
|
240
|
-
kind: { type: "string", enum: ["auto", "code", "text", "file"], default: "auto" },
|
|
241
|
-
regex: { type: "boolean", default: false },
|
|
242
|
-
maxResults: { type: "integer", minimum: 1, maximum: 100, default: 20 },
|
|
243
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
244
|
-
}, { type: "object", additionalProperties: true }, ["query"]),
|
|
245
|
-
tool("inspect_symbol", "Inspect a known symbol in one call, returning its definition, bounded source context, and optionally references.", {
|
|
246
|
-
symbol: stringSchema,
|
|
247
|
-
includeReferences: { type: "boolean", default: false },
|
|
248
|
-
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
249
|
-
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
250
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
251
|
-
}, { type: "object", additionalProperties: true }, ["symbol"]),
|
|
252
|
-
tool("inspect", "Inspect a project target in one call. Use kind auto, file, symbol, or code; returns bounded context and optional symbol references.", {
|
|
253
|
-
target: stringSchema,
|
|
254
|
-
kind: { type: "string", enum: ["auto", "file", "symbol", "code"], default: "auto" },
|
|
255
|
-
includeReferences: { type: "boolean", default: false },
|
|
256
|
-
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
257
|
-
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
258
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
259
|
-
}, { type: "object", additionalProperties: true }, ["target"]),
|
|
260
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.", {
|
|
261
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
128
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
262
129
|
}, { type: "object", additionalProperties: true }),
|
|
263
|
-
tool("
|
|
264
|
-
query: stringSchema,
|
|
265
|
-
regex: { type: "boolean", default: false },
|
|
266
|
-
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
267
|
-
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
268
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
269
|
-
}, { type: "object", additionalProperties: true }, ["query"]),
|
|
270
|
-
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.", {
|
|
271
131
|
level: { type: "string", enum: ["diagnostics", "scripts", "full"], default: "diagnostics" },
|
|
272
|
-
scripts: { type: "array", items: stringSchema, minItems: 1 },
|
|
132
|
+
scripts: { type: "array", items: stringSchema, minItems: 1, maxItems: 10 },
|
|
273
133
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
274
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
275
|
-
}, { type: "object", additionalProperties: true }),
|
|
276
|
-
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.", {
|
|
277
|
-
includeDiff: { type: "boolean", default: false },
|
|
278
|
-
path: pathSchema,
|
|
279
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
134
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
280
135
|
}, { type: "object", additionalProperties: true }),
|
|
281
|
-
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({
|
|
282
|
-
root: stringSchema,
|
|
283
|
-
files: { type: "integer" },
|
|
284
|
-
directories: { type: "integer" },
|
|
285
|
-
languages: { type: "array" },
|
|
286
|
-
packageManagers: { type: "array", items: stringSchema },
|
|
287
|
-
projectFiles: { type: "array", items: stringSchema },
|
|
288
|
-
git: { type: "object" },
|
|
289
|
-
}, ["root", "files", "directories", "languages", "packageManagers", "projectFiles", "git"])),
|
|
290
|
-
tool("package_info", "Read normalized metadata from package.json when present.", {}, { type: "object", additionalProperties: true }),
|
|
291
|
-
tool("allowed_commands", "Show the npm scripts and commands explicitly allowed by the active server configuration.", {}, objectOutput({
|
|
292
|
-
npmScripts: { type: "array", items: stringSchema },
|
|
293
|
-
commands: { type: "array", items: stringSchema },
|
|
294
|
-
}, ["npmScripts", "commands"])),
|
|
295
136
|
tool("file_info", "Return filesystem metadata for a project path.", { path: pathSchema }, { type: "object", additionalProperties: true }, ["path"]),
|
|
296
|
-
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.", {
|
|
297
|
-
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
298
|
-
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
299
|
-
}, arrayOutput(objectOutput({
|
|
300
|
-
severity: { type: "string", enum: ["error", "warning", "info"] },
|
|
301
|
-
code: stringSchema,
|
|
302
|
-
path: stringSchema,
|
|
303
|
-
line: { type: "integer" },
|
|
304
|
-
message: stringSchema,
|
|
305
|
-
}, ["severity", "code", "message"]))),
|
|
306
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.", {
|
|
307
|
-
scripts: { type: "array", items: stringSchema, minItems: 1 },
|
|
308
|
-
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 },
|
|
309
140
|
}, arrayOutput(commandResultSchema), ["scripts"]),
|
|
310
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.", {
|
|
311
142
|
commands: {
|
|
312
143
|
type: "array",
|
|
313
144
|
items: objectOutput({ command: stringSchema, args: { type: "array", items: stringSchema } }, ["command"]),
|
|
314
145
|
minItems: 1,
|
|
146
|
+
maxItems: 10,
|
|
315
147
|
},
|
|
316
|
-
maxBytes: { type: "integer", minimum: 1024, maximum:
|
|
148
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 64000, default: DEFAULT_OUTPUT_BYTES },
|
|
317
149
|
}, arrayOutput(commandResultSchema), ["commands"]),
|
|
318
150
|
];
|
|
319
151
|
function tool(name, description, properties, outputSchema, required = []) {
|
|
@@ -345,13 +177,23 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
345
177
|
let sessionToolCallCount = 0;
|
|
346
178
|
let sessionCommandCallCount = 0;
|
|
347
179
|
let sessionRecorded = false;
|
|
180
|
+
let sessionOutputBytes = 0;
|
|
181
|
+
let sessionEstimatedOutputTokens = 0;
|
|
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
|
+
};
|
|
348
191
|
const telemetryProjectPromise = telemetry.level === "detailed" || telemetry.level === "diagnostic"
|
|
349
192
|
? buildTelemetryProjectContext(root, codeGraph).catch(() => undefined)
|
|
350
193
|
: undefined;
|
|
351
194
|
const npmAllowed = commandConfig.npm?.allowedScripts ?? [];
|
|
352
195
|
const allowedCommands = commandConfig.commands ?? [];
|
|
353
196
|
const blacklistedCommands = commandConfig.blacklistedCommands ?? [];
|
|
354
|
-
const gitEnabled = commandConfig.git ?? allowedCommands.includes("git");
|
|
355
197
|
const subscriptions = new Set();
|
|
356
198
|
const notify = (event) => {
|
|
357
199
|
for (const subscription of subscriptions) {
|
|
@@ -383,6 +225,17 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
383
225
|
};
|
|
384
226
|
const handler = async (request) => {
|
|
385
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
|
+
};
|
|
386
239
|
if (sessionStartedAt === undefined)
|
|
387
240
|
sessionStartedAt = requestStartedAt;
|
|
388
241
|
sessionRequestCount += 1;
|
|
@@ -397,6 +250,8 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
397
250
|
method,
|
|
398
251
|
success: mcpSuccess,
|
|
399
252
|
durationMs: performance.now() - requestStartedAt,
|
|
253
|
+
...(responseBytes === undefined ? {} : { responseBytes }),
|
|
254
|
+
...(estimatedResponseTokens === undefined ? {} : { estimatedResponseTokens }),
|
|
400
255
|
...(telemetryRequestErrorCategory
|
|
401
256
|
? { errorCategory: telemetryRequestErrorCategory }
|
|
402
257
|
: {}),
|
|
@@ -441,64 +296,19 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
441
296
|
}
|
|
442
297
|
const params = message.params ?? {};
|
|
443
298
|
runtime = telemetryRuntime(params);
|
|
444
|
-
|
|
445
|
-
const legacyInitialize = message.method === "initialize";
|
|
446
|
-
const legacyRequest = !legacyInitialize &&
|
|
447
|
-
request.headers.get("mcp-protocol-version") === LEGACY_PROTOCOL_VERSION &&
|
|
448
|
-
!isRecord(params._meta);
|
|
449
|
-
if (legacyInitialize) {
|
|
450
|
-
const protocolHeader = request.headers.get("mcp-protocol-version");
|
|
451
|
-
if (protocolHeader !== null && protocolHeader !== LEGACY_PROTOCOL_VERSION) {
|
|
452
|
-
telemetryRequestErrorCategory = "invalid_input";
|
|
453
|
-
recordMcpRequest();
|
|
454
|
-
return jsonRpcError(message.id ?? null, -32020, "MCP-Protocol-Version does not match the initialize protocol version.", 400);
|
|
455
|
-
}
|
|
456
|
-
if (requestedProtocolVersion !== LEGACY_PROTOCOL_VERSION) {
|
|
457
|
-
telemetryRequestErrorCategory = "invalid_input";
|
|
458
|
-
recordMcpRequest();
|
|
459
|
-
return jsonRpcError(message.id ?? null, -32602, "Unsupported protocol version.", 400, {
|
|
460
|
-
supported: [LEGACY_PROTOCOL_VERSION],
|
|
461
|
-
requested: requestedProtocolVersion,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
if (!isRecord(params.capabilities) || !isRecord(params.clientInfo)) {
|
|
465
|
-
telemetryRequestErrorCategory = "invalid_input";
|
|
466
|
-
recordMcpRequest();
|
|
467
|
-
return jsonRpcError(message.id ?? null, -32602, "Invalid initialize params.", 400);
|
|
468
|
-
}
|
|
469
|
-
mcpSuccess = true;
|
|
470
|
-
recordMcpRequest();
|
|
471
|
-
return json({
|
|
472
|
-
jsonrpc: "2.0",
|
|
473
|
-
id: message.id,
|
|
474
|
-
result: {
|
|
475
|
-
protocolVersion: LEGACY_PROTOCOL_VERSION,
|
|
476
|
-
capabilities: capabilities(),
|
|
477
|
-
serverInfo: SERVER_INFO,
|
|
478
|
-
instructions: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, gitEnabled, await codeGraph.capability()),
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
if (legacyRequest) {
|
|
483
|
-
if (request.headers.get("mcp-protocol-version") !== LEGACY_PROTOCOL_VERSION) {
|
|
484
|
-
telemetryRequestErrorCategory = "invalid_input";
|
|
485
|
-
recordMcpRequest();
|
|
486
|
-
return jsonRpcError(message.id ?? null, -32020, "MCP-Protocol-Version must be 2025-11-25 for legacy requests.", 400);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
else if (!isRecord(params._meta)) {
|
|
299
|
+
if (!isRecord(params._meta)) {
|
|
490
300
|
telemetryRequestErrorCategory = "invalid_input";
|
|
491
301
|
recordMcpRequest();
|
|
492
302
|
return jsonRpcError(message.id ?? null, -32602, "Missing required request metadata.", 400);
|
|
493
303
|
}
|
|
494
|
-
const meta =
|
|
304
|
+
const meta = params._meta;
|
|
495
305
|
const version = meta[PROTOCOL_META];
|
|
496
|
-
if (
|
|
306
|
+
if (typeof version !== "string") {
|
|
497
307
|
telemetryRequestErrorCategory = "invalid_input";
|
|
498
308
|
recordMcpRequest();
|
|
499
309
|
return jsonRpcError(message.id ?? null, -32602, "Missing required protocol version metadata.", 400);
|
|
500
310
|
}
|
|
501
|
-
if (
|
|
311
|
+
if (version !== PROTOCOL_VERSION) {
|
|
502
312
|
telemetryRequestErrorCategory = "invalid_input";
|
|
503
313
|
recordMcpRequest();
|
|
504
314
|
return jsonRpcError(message.id ?? null, -32022, "Unsupported protocol version.", 400, {
|
|
@@ -506,7 +316,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
506
316
|
requested: version,
|
|
507
317
|
});
|
|
508
318
|
}
|
|
509
|
-
if (!
|
|
319
|
+
if (!isRecord(meta[CLIENT_CAPABILITIES_META])) {
|
|
510
320
|
telemetryRequestErrorCategory = "invalid_input";
|
|
511
321
|
recordMcpRequest();
|
|
512
322
|
return jsonRpcError(message.id ?? null, -32602, "Missing required client capabilities metadata.", 400);
|
|
@@ -515,7 +325,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
515
325
|
switch (message.method) {
|
|
516
326
|
case "server/discover":
|
|
517
327
|
mcpSuccess = true;
|
|
518
|
-
return
|
|
328
|
+
return trackedJson({
|
|
519
329
|
jsonrpc: "2.0",
|
|
520
330
|
id: message.id,
|
|
521
331
|
result: {
|
|
@@ -523,23 +333,23 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
523
333
|
supportedVersions: [PROTOCOL_VERSION],
|
|
524
334
|
capabilities: capabilities(),
|
|
525
335
|
_meta: SERVER_INFO_META,
|
|
526
|
-
instructions:
|
|
336
|
+
instructions: buildDiscoveryInstructions(npmAllowed, allowedCommands, await codeGraph.capability()),
|
|
527
337
|
codeGraph: await codeGraph.capability(),
|
|
528
|
-
ttlMs:
|
|
338
|
+
ttlMs: DISCOVERY_CACHE_TTL_MS,
|
|
529
339
|
cacheScope: "private",
|
|
530
340
|
},
|
|
531
341
|
});
|
|
532
342
|
case "tools/list": {
|
|
533
|
-
const page = paginate(await
|
|
343
|
+
const page = paginate(await getAvailableTools(), params.cursor);
|
|
534
344
|
mcpSuccess = true;
|
|
535
|
-
return
|
|
345
|
+
return trackedJson({
|
|
536
346
|
jsonrpc: "2.0",
|
|
537
347
|
id: message.id,
|
|
538
348
|
result: {
|
|
539
349
|
resultType: "complete",
|
|
540
350
|
tools: page.items,
|
|
541
351
|
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
542
|
-
ttlMs:
|
|
352
|
+
ttlMs: DISCOVERY_CACHE_TTL_MS,
|
|
543
353
|
cacheScope: "private",
|
|
544
354
|
_meta: SERVER_INFO_META,
|
|
545
355
|
},
|
|
@@ -551,7 +361,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
551
361
|
return jsonRpcError(message.id ?? null, -32602, "tools/call name must be a non-empty string.", 400);
|
|
552
362
|
}
|
|
553
363
|
const toolName = params.name;
|
|
554
|
-
const toolDefinition = (await
|
|
364
|
+
const toolDefinition = (await getAvailableTools()).find((item) => item.name === toolName);
|
|
555
365
|
if (!toolDefinition)
|
|
556
366
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
557
367
|
if (params.arguments !== undefined && !isRecord(params.arguments)) {
|
|
@@ -567,19 +377,33 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
567
377
|
sessionToolCallCount += 1;
|
|
568
378
|
const startedAt = performance.now();
|
|
569
379
|
try {
|
|
570
|
-
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);
|
|
571
381
|
const durationMs = performance.now() - startedAt;
|
|
572
382
|
const count = resultCount(result);
|
|
383
|
+
const serializedResult = JSON.stringify(result);
|
|
384
|
+
const outputBytes = Buffer.byteLength(serializedResult, "utf8");
|
|
385
|
+
const estimatedOutputTokens = estimateTokens(serializedResult);
|
|
386
|
+
sessionOutputBytes += outputBytes;
|
|
387
|
+
sessionEstimatedOutputTokens += estimatedOutputTokens;
|
|
573
388
|
const runtime = telemetryRuntime(params);
|
|
574
389
|
telemetryRecorder.record({
|
|
575
390
|
event: "tool_call",
|
|
576
391
|
tool: toolName,
|
|
392
|
+
requestIdHash: telemetryHash(String(message.id)),
|
|
577
393
|
success: true,
|
|
578
394
|
durationMs,
|
|
579
395
|
inputBytes: Buffer.byteLength(JSON.stringify(argumentsValue), "utf8"),
|
|
580
|
-
outputBytes
|
|
396
|
+
outputBytes,
|
|
397
|
+
estimatedInputTokens: estimateTokens(JSON.stringify(argumentsValue)),
|
|
398
|
+
estimatedOutputTokens,
|
|
399
|
+
cumulativeOutputBytes: sessionOutputBytes,
|
|
400
|
+
cumulativeEstimatedOutputTokens: sessionEstimatedOutputTokens,
|
|
401
|
+
inputFingerprint: telemetryHash(JSON.stringify({ toolName, argumentsValue })),
|
|
402
|
+
...(telemetry.level === "diagnostic"
|
|
403
|
+
? { diagnosticDetails: diagnosticToolDetails(argumentsValue, result) }
|
|
404
|
+
: {}),
|
|
581
405
|
...(count === undefined ? {} : { resultCount: count }),
|
|
582
|
-
...toolTelemetryMetrics(toolName, result),
|
|
406
|
+
...toolTelemetryMetrics(toolName, result, seenOutputFingerprints),
|
|
583
407
|
}, runtime, telemetryProject);
|
|
584
408
|
const command = commandTelemetryDescriptor(toolName);
|
|
585
409
|
if (command) {
|
|
@@ -590,11 +414,11 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
590
414
|
operation: command.operation,
|
|
591
415
|
success: true,
|
|
592
416
|
durationMs,
|
|
593
|
-
outputBytes
|
|
417
|
+
outputBytes,
|
|
594
418
|
}, runtime, telemetryProject);
|
|
595
419
|
}
|
|
596
420
|
mcpSuccess = true;
|
|
597
|
-
return
|
|
421
|
+
return trackedJson({
|
|
598
422
|
jsonrpc: "2.0",
|
|
599
423
|
id: message.id,
|
|
600
424
|
result,
|
|
@@ -607,9 +431,21 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
607
431
|
telemetryRecorder.record({
|
|
608
432
|
event: "tool_call",
|
|
609
433
|
tool: toolName,
|
|
434
|
+
requestIdHash: telemetryHash(String(message.id)),
|
|
610
435
|
success: false,
|
|
611
436
|
durationMs,
|
|
612
437
|
inputBytes: Buffer.byteLength(JSON.stringify(argumentsValue), "utf8"),
|
|
438
|
+
estimatedInputTokens: estimateTokens(JSON.stringify(argumentsValue)),
|
|
439
|
+
inputFingerprint: telemetryHash(JSON.stringify({ toolName, argumentsValue })),
|
|
440
|
+
...(telemetry.level === "diagnostic"
|
|
441
|
+
? {
|
|
442
|
+
diagnosticDetails: diagnosticToolDetails(argumentsValue, {
|
|
443
|
+
error: error instanceof Error
|
|
444
|
+
? { name: error.name, message: error.message, stack: error.stack }
|
|
445
|
+
: String(error),
|
|
446
|
+
}),
|
|
447
|
+
}
|
|
448
|
+
: {}),
|
|
613
449
|
errorCategory,
|
|
614
450
|
}, runtime, telemetryProject);
|
|
615
451
|
const command = commandTelemetryDescriptor(toolName);
|
|
@@ -648,14 +484,14 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
648
484
|
];
|
|
649
485
|
const page = paginate(resources, params.cursor);
|
|
650
486
|
mcpSuccess = true;
|
|
651
|
-
return
|
|
487
|
+
return trackedJson({
|
|
652
488
|
jsonrpc: "2.0",
|
|
653
489
|
id: message.id,
|
|
654
490
|
result: {
|
|
655
491
|
resultType: "complete",
|
|
656
492
|
resources: page.items,
|
|
657
493
|
...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
|
|
658
|
-
ttlMs:
|
|
494
|
+
ttlMs: DISCOVERY_CACHE_TTL_MS,
|
|
659
495
|
cacheScope: "private",
|
|
660
496
|
_meta: SERVER_INFO_META,
|
|
661
497
|
},
|
|
@@ -666,7 +502,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
666
502
|
return jsonRpcError(message.id ?? null, -32602, "resources/read uri must be a non-empty string.", 400);
|
|
667
503
|
if (params.uri === INSTRUCTIONS_URI) {
|
|
668
504
|
mcpSuccess = true;
|
|
669
|
-
return
|
|
505
|
+
return trackedJson({
|
|
670
506
|
jsonrpc: "2.0",
|
|
671
507
|
id: message.id,
|
|
672
508
|
result: {
|
|
@@ -675,7 +511,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
675
511
|
{
|
|
676
512
|
uri: INSTRUCTIONS_URI,
|
|
677
513
|
mimeType: "text/markdown",
|
|
678
|
-
text: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands,
|
|
514
|
+
text: buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands, await codeGraph.capability()),
|
|
679
515
|
},
|
|
680
516
|
],
|
|
681
517
|
ttlMs: 0,
|
|
@@ -686,7 +522,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
686
522
|
}
|
|
687
523
|
if (params.uri === CAPABILITIES_URI) {
|
|
688
524
|
mcpSuccess = true;
|
|
689
|
-
return
|
|
525
|
+
return trackedJson({
|
|
690
526
|
jsonrpc: "2.0",
|
|
691
527
|
id: message.id,
|
|
692
528
|
result: {
|
|
@@ -695,7 +531,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
695
531
|
{
|
|
696
532
|
uri: CAPABILITIES_URI,
|
|
697
533
|
mimeType: "application/json",
|
|
698
|
-
text: JSON.stringify(buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands,
|
|
534
|
+
text: JSON.stringify(buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands, await codeGraph.capability()), null, 2),
|
|
699
535
|
},
|
|
700
536
|
],
|
|
701
537
|
ttlMs: 0,
|
|
@@ -706,7 +542,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
706
542
|
}
|
|
707
543
|
const value = await readSkillResource(root, params.uri);
|
|
708
544
|
mcpSuccess = true;
|
|
709
|
-
return
|
|
545
|
+
return trackedJson({
|
|
710
546
|
jsonrpc: "2.0",
|
|
711
547
|
id: message.id,
|
|
712
548
|
result: {
|
|
@@ -724,7 +560,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
724
560
|
const resources = await readSkillDirectory(root, params.uri);
|
|
725
561
|
const page = paginate(resources, params.cursor);
|
|
726
562
|
mcpSuccess = true;
|
|
727
|
-
return
|
|
563
|
+
return trackedJson({
|
|
728
564
|
jsonrpc: "2.0",
|
|
729
565
|
id: message.id,
|
|
730
566
|
result: {
|
|
@@ -740,7 +576,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
740
576
|
case "skills/list": {
|
|
741
577
|
const page = paginate(await listSkills(root), params.cursor);
|
|
742
578
|
mcpSuccess = true;
|
|
743
|
-
return
|
|
579
|
+
return trackedJson({
|
|
744
580
|
jsonrpc: "2.0",
|
|
745
581
|
id: message.id,
|
|
746
582
|
result: {
|
|
@@ -762,7 +598,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
762
598
|
if (typeof params.uri !== "string" || params.uri.length === 0)
|
|
763
599
|
return jsonRpcError(message.id ?? null, -32602, "skills/get uri must be a non-empty string.", 400);
|
|
764
600
|
mcpSuccess = true;
|
|
765
|
-
return
|
|
601
|
+
return trackedJson({
|
|
766
602
|
jsonrpc: "2.0",
|
|
767
603
|
id: message.id,
|
|
768
604
|
result: {
|
|
@@ -781,7 +617,7 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
781
617
|
catch (error) {
|
|
782
618
|
telemetryRequestErrorCategory = classifyTelemetryError(error);
|
|
783
619
|
if (message.method === "tools/call")
|
|
784
|
-
return
|
|
620
|
+
return trackedJson({
|
|
785
621
|
jsonrpc: "2.0",
|
|
786
622
|
id: message.id,
|
|
787
623
|
result: {
|
|
@@ -845,16 +681,23 @@ export function createMcpHandler(root, commandConfig = {}, telemetry = { level:
|
|
|
845
681
|
};
|
|
846
682
|
return handler;
|
|
847
683
|
}
|
|
848
|
-
async function availableTools(codeGraph
|
|
684
|
+
async function availableTools(codeGraph) {
|
|
849
685
|
const capability = await codeGraph.capability();
|
|
850
|
-
|
|
686
|
+
return capability.status === "available" ||
|
|
851
687
|
capability.status === "startup_failed" ||
|
|
852
688
|
capability.status === "runtime_failed"
|
|
853
689
|
? [...baseTools, codeGraphTool]
|
|
854
690
|
: baseTools;
|
|
855
|
-
return gitEnabled ? tools : tools.filter((item) => !item.name.startsWith("git_"));
|
|
856
691
|
}
|
|
857
|
-
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) {
|
|
858
701
|
const npm = npmAllowed.length ? npmAllowed.map((item) => `- ${item}`).join("\n") : "- none";
|
|
859
702
|
const commands = allowedCommands.length
|
|
860
703
|
? allowedCommands.map((item) => `- ${item}`).join("\n")
|
|
@@ -865,44 +708,46 @@ function buildAgentInstructions(npmAllowed, allowedCommands, blacklistedCommands
|
|
|
865
708
|
const codeGraphSection = codeGraph.status === "available" ||
|
|
866
709
|
codeGraph.status === "startup_failed" ||
|
|
867
710
|
codeGraph.status === "runtime_failed"
|
|
868
|
-
? "\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: " +
|
|
869
712
|
codeGraph.status +
|
|
870
713
|
(codeGraph.detail ? ` — ${codeGraph.detail}` : "") +
|
|
871
714
|
"."
|
|
872
715
|
: "";
|
|
873
|
-
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}`;
|
|
874
717
|
}
|
|
875
|
-
function buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands,
|
|
718
|
+
function buildAgentCapabilities(npmAllowed, allowedCommands, blacklistedCommands, codeGraph) {
|
|
876
719
|
return {
|
|
877
720
|
version: packageVersion,
|
|
878
|
-
tools:
|
|
879
|
-
.
|
|
880
|
-
.
|
|
721
|
+
tools: [
|
|
722
|
+
...baseTools.map((item) => item.name),
|
|
723
|
+
...(codeGraph.status === "available" ? ["codegraph_explore"] : []),
|
|
724
|
+
],
|
|
881
725
|
resources: [INSTRUCTIONS_URI, CAPABILITIES_URI],
|
|
882
726
|
execution: {
|
|
883
727
|
npmScripts: [...npmAllowed],
|
|
884
728
|
commands: [...allowedCommands],
|
|
885
729
|
blacklistedCommands: [...blacklistedCommands],
|
|
886
|
-
git: gitEnabled,
|
|
887
730
|
},
|
|
888
731
|
efficiency: {
|
|
889
732
|
preferred: {
|
|
890
733
|
orientation: "project_context",
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
discovery: ["find_files", "
|
|
894
|
-
symbolNavigation: ["
|
|
895
|
-
reading: ["read_range"
|
|
896
|
-
editing: ["apply_changes"
|
|
897
|
-
validation: ["validate", "
|
|
898
|
-
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"],
|
|
899
741
|
},
|
|
900
742
|
avoid: [
|
|
901
743
|
"recursive discovery when targeted search is enough",
|
|
902
744
|
"whole-file reads when a line range is enough",
|
|
903
745
|
"full-repository diffs when a path-scoped diff is enough",
|
|
904
746
|
"expensive validation when a targeted check answers the question",
|
|
905
|
-
"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
|
+
: []),
|
|
906
751
|
],
|
|
907
752
|
},
|
|
908
753
|
};
|
|
@@ -914,7 +759,7 @@ function capabilities() {
|
|
|
914
759
|
extensions: { "io.modelcontextprotocol/skills": { directoryRead: true } },
|
|
915
760
|
};
|
|
916
761
|
}
|
|
917
|
-
async function callTool(name, args, root, npmAllowed, allowedCommands, blacklistedCommands,
|
|
762
|
+
async function callTool(name, args, root, npmAllowed, allowedCommands, blacklistedCommands, notify, signal, codeGraph) {
|
|
918
763
|
let structuredContent;
|
|
919
764
|
if (name === "codegraph_explore") {
|
|
920
765
|
if (!codeGraph)
|
|
@@ -924,41 +769,28 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
924
769
|
if (args.maxFiles !== undefined &&
|
|
925
770
|
(!Number.isInteger(args.maxFiles) || Number(args.maxFiles) < 1 || Number(args.maxFiles) > 100))
|
|
926
771
|
throw new Error("maxFiles must be an integer between 1 and 100.");
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
+
};
|
|
931
783
|
}
|
|
932
784
|
switch (name) {
|
|
933
|
-
case "list_files": {
|
|
934
|
-
const items = await listFiles(root);
|
|
935
|
-
structuredContent = boundedItems(items, outputBytesArg(args.maxBytes)).items;
|
|
936
|
-
break;
|
|
937
|
-
}
|
|
938
|
-
case "list_dirs": {
|
|
939
|
-
const items = await listDirs(root, stringArray(args.paths, "paths"));
|
|
940
|
-
structuredContent = boundedItems(items, outputBytesArg(args.maxBytes)).items;
|
|
941
|
-
break;
|
|
942
|
-
}
|
|
943
785
|
case "read_range": {
|
|
944
|
-
const result = await readRange(root, String(args.path), Number(args.startLine), Number(args.endLine));
|
|
945
|
-
const bounded = truncateText(result.content, outputBytesArg(args.maxBytes));
|
|
946
|
-
structuredContent = { ...result, content: bounded.text };
|
|
947
|
-
break;
|
|
948
|
-
}
|
|
949
|
-
case "read_files": {
|
|
950
786
|
const maxBytes = outputBytesArg(args.maxBytes);
|
|
951
|
-
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
if (bounded.truncated || remaining <= 0)
|
|
959
|
-
break;
|
|
960
|
-
}
|
|
961
|
-
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
|
+
};
|
|
962
794
|
break;
|
|
963
795
|
}
|
|
964
796
|
case "apply_changes": {
|
|
@@ -990,247 +822,39 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
990
822
|
const before = new Map();
|
|
991
823
|
for (const p of affected)
|
|
992
824
|
before.set(p, await skillResourceUrisForPath(root, p));
|
|
993
|
-
|
|
994
|
-
|
|
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);
|
|
995
834
|
for (const p of affected)
|
|
996
835
|
await notifyResourceChange(notify, root, p, before.get(p) ?? []);
|
|
997
|
-
break;
|
|
998
|
-
}
|
|
999
|
-
case "write_files": {
|
|
1000
|
-
const dryRun = args.dryRun === true;
|
|
1001
|
-
const output = [];
|
|
1002
|
-
for (const file of objectArray(args.files, "files")) {
|
|
1003
|
-
const p = String(file.path);
|
|
1004
|
-
const before = await skillResourceUrisForPath(root, p);
|
|
1005
|
-
if (!dryRun) {
|
|
1006
|
-
await writeFile(root, p, String(file.content));
|
|
1007
|
-
await notifyResourceChange(notify, root, p, before);
|
|
1008
|
-
}
|
|
1009
|
-
output.push({ path: p, written: true, ...(dryRun ? { dryRun: true } : {}) });
|
|
1010
836
|
}
|
|
1011
|
-
structuredContent = output;
|
|
1012
837
|
break;
|
|
1013
838
|
}
|
|
1014
|
-
case "
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
})),
|
|
1023
|
-
}));
|
|
1024
|
-
const before = new Map();
|
|
1025
|
-
for (const file of files)
|
|
1026
|
-
before.set(file.path, await skillResourceUrisForPath(root, file.path));
|
|
1027
|
-
if (dryRun) {
|
|
1028
|
-
structuredContent = await patchFilesDryRun(root, files);
|
|
1029
|
-
}
|
|
1030
|
-
else {
|
|
1031
|
-
structuredContent = await patchFiles(root, files);
|
|
1032
|
-
}
|
|
1033
|
-
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1034
|
-
if (Array.isArray(structuredContent)) {
|
|
1035
|
-
structuredContent = boundedItems(structuredContent.map((item) => {
|
|
1036
|
-
if (!isRecord(item) || typeof item.content !== "string")
|
|
1037
|
-
return item;
|
|
1038
|
-
return { ...item, content: truncateText(item.content, maxBytes).text };
|
|
1039
|
-
}), maxBytes).items;
|
|
1040
|
-
}
|
|
1041
|
-
if (!dryRun) {
|
|
1042
|
-
for (const file of files)
|
|
1043
|
-
await notifyResourceChange(notify, root, file.path, before.get(file.path) ?? []);
|
|
1044
|
-
}
|
|
1045
|
-
break;
|
|
1046
|
-
}
|
|
1047
|
-
case "delete_files": {
|
|
1048
|
-
const dryRun = args.dryRun === true;
|
|
1049
|
-
const output = [];
|
|
1050
|
-
for (const p of stringArray(args.paths, "paths")) {
|
|
1051
|
-
const before = await skillResourceUrisForPath(root, p);
|
|
1052
|
-
if (!dryRun) {
|
|
1053
|
-
await deleteFile(root, p);
|
|
1054
|
-
await notifyResourceChange(notify, root, p, before);
|
|
1055
|
-
}
|
|
1056
|
-
output.push({ path: p, deleted: true, ...(dryRun ? { dryRun: true } : {}) });
|
|
1057
|
-
}
|
|
1058
|
-
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
|
+
};
|
|
1059
847
|
break;
|
|
1060
848
|
}
|
|
1061
|
-
case "search_files":
|
|
1062
|
-
structuredContent = await searchFiles(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
1063
|
-
break;
|
|
1064
|
-
case "find_files":
|
|
1065
|
-
structuredContent = boundedItems(await findFiles(root, String(args.pattern), numberArg(args.maxResults, 200, 1000)), outputBytesArg(args.maxBytes)).items;
|
|
1066
|
-
break;
|
|
1067
849
|
case "search_code":
|
|
1068
850
|
structuredContent = await searchCode(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
1069
851
|
break;
|
|
1070
852
|
case "find_symbol":
|
|
1071
853
|
structuredContent = await findSymbol(root, String(args.symbol), numberArg(args.maxResults, 100, 500), outputBytesArg(args.maxBytes));
|
|
1072
854
|
break;
|
|
1073
|
-
case "find_definition":
|
|
1074
|
-
structuredContent = await findDefinition(root, String(args.symbol), numberArg(args.maxResults, 100, 500), outputBytesArg(args.maxBytes));
|
|
1075
|
-
break;
|
|
1076
855
|
case "find_references":
|
|
1077
856
|
structuredContent = await findReferences(root, String(args.symbol), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
1078
857
|
break;
|
|
1079
|
-
case "find_imports":
|
|
1080
|
-
structuredContent = await findImports(root, numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
1081
|
-
break;
|
|
1082
|
-
case "find_exports":
|
|
1083
|
-
structuredContent = await findExports(root, numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
1084
|
-
break;
|
|
1085
|
-
case "git_status":
|
|
1086
|
-
structuredContent = boundCommandResult(await gitStatus(root), outputBytesArg(args.maxBytes));
|
|
1087
|
-
break;
|
|
1088
|
-
case "git_stage":
|
|
1089
|
-
structuredContent = boundCommandResult(await gitStage(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
1090
|
-
break;
|
|
1091
|
-
case "git_unstage":
|
|
1092
|
-
structuredContent = boundCommandResult(await gitUnstage(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
1093
|
-
break;
|
|
1094
|
-
case "git_commit":
|
|
1095
|
-
structuredContent = boundCommandResult(await gitCommit(root, String(args.message)), outputBytesArg(args.maxBytes));
|
|
1096
|
-
break;
|
|
1097
|
-
case "git_restore":
|
|
1098
|
-
structuredContent = boundCommandResult(await gitRestore(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
1099
|
-
break;
|
|
1100
|
-
case "git_push":
|
|
1101
|
-
structuredContent = boundCommandResult(await gitPush(root, args.remote === undefined ? undefined : String(args.remote), args.branch === undefined ? undefined : String(args.branch)), outputBytesArg(args.maxBytes));
|
|
1102
|
-
break;
|
|
1103
|
-
case "git_diff":
|
|
1104
|
-
structuredContent = boundCommandResult(await gitDiff(root, Boolean(args.staged), args.path === undefined ? undefined : String(args.path)), outputBytesArg(args.maxBytes));
|
|
1105
|
-
break;
|
|
1106
|
-
case "git_log":
|
|
1107
|
-
structuredContent = boundCommandResult(await gitLog(root, numberArg(args.limit, 20, 200), args.path === undefined ? undefined : String(args.path)), outputBytesArg(args.maxBytes));
|
|
1108
|
-
break;
|
|
1109
|
-
case "locate": {
|
|
1110
|
-
const query = String(args.query);
|
|
1111
|
-
const kind = typeof args.kind === "string" ? args.kind : "auto";
|
|
1112
|
-
const regex = args.regex === true;
|
|
1113
|
-
const maxResults = numberArg(args.maxResults, 20, 100);
|
|
1114
|
-
const resolvedKind = kind === "auto"
|
|
1115
|
-
? /[*?]/.test(query)
|
|
1116
|
-
? "file"
|
|
1117
|
-
: /\.(ts|tsx|js|jsx|py|go|rs|java|json|md|yml|yaml|css|html)$/.test(query)
|
|
1118
|
-
? "file"
|
|
1119
|
-
: "code"
|
|
1120
|
-
: kind;
|
|
1121
|
-
const items = resolvedKind === "file"
|
|
1122
|
-
? await findFiles(root, query, maxResults)
|
|
1123
|
-
: resolvedKind === "text"
|
|
1124
|
-
? await searchFiles(root, query, regex, maxResults)
|
|
1125
|
-
: await searchCode(root, query, regex, maxResults);
|
|
1126
|
-
const bounded = boundedItems(items, outputBytesArg(args.maxBytes));
|
|
1127
|
-
structuredContent = {
|
|
1128
|
-
query,
|
|
1129
|
-
kind: resolvedKind,
|
|
1130
|
-
items: bounded.items,
|
|
1131
|
-
count: bounded.items.length,
|
|
1132
|
-
truncated: bounded.truncated || items.length >= maxResults,
|
|
1133
|
-
};
|
|
1134
|
-
break;
|
|
1135
|
-
}
|
|
1136
|
-
case "inspect_symbol": {
|
|
1137
|
-
const symbol = String(args.symbol);
|
|
1138
|
-
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
1139
|
-
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
1140
|
-
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
1141
|
-
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
1142
|
-
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1143
|
-
const definitions = await findDefinition(root, symbol, maxResults, maxBytes);
|
|
1144
|
-
const definitionContext = await Promise.all(definitions.map(async (definition) => {
|
|
1145
|
-
const range = await readRange(root, definition.path, Math.max(1, definition.line - contextLines), definition.line + contextLines);
|
|
1146
|
-
return { ...definition, context: range.content };
|
|
1147
|
-
}));
|
|
1148
|
-
const references = args.includeReferences === true
|
|
1149
|
-
? await findReferences(root, symbol, maxResults, maxBytes)
|
|
1150
|
-
: undefined;
|
|
1151
|
-
const result = {
|
|
1152
|
-
symbol,
|
|
1153
|
-
definitions: definitionContext,
|
|
1154
|
-
...(references ? { references } : {}),
|
|
1155
|
-
};
|
|
1156
|
-
const bounded = truncateText(JSON.stringify(result), maxBytes);
|
|
1157
|
-
structuredContent = bounded.truncated
|
|
1158
|
-
? { symbol, result: bounded.text, truncated: true }
|
|
1159
|
-
: result;
|
|
1160
|
-
break;
|
|
1161
|
-
}
|
|
1162
|
-
case "inspect": {
|
|
1163
|
-
const target = String(args.target);
|
|
1164
|
-
const requestedKind = typeof args.kind === "string" ? args.kind : "auto";
|
|
1165
|
-
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
1166
|
-
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
1167
|
-
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
1168
|
-
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
1169
|
-
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1170
|
-
let kind = requestedKind;
|
|
1171
|
-
if (kind === "auto") {
|
|
1172
|
-
try {
|
|
1173
|
-
await readFile(root, target);
|
|
1174
|
-
kind = "file";
|
|
1175
|
-
}
|
|
1176
|
-
catch {
|
|
1177
|
-
kind = /[*?]/.test(target) ? "code" : "symbol";
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
if (kind === "file") {
|
|
1181
|
-
const content = await readFile(root, target);
|
|
1182
|
-
const boundedContent = truncateText(content, Math.max(1, maxBytes - 128));
|
|
1183
|
-
const result = {
|
|
1184
|
-
target,
|
|
1185
|
-
kind,
|
|
1186
|
-
path: target,
|
|
1187
|
-
content: boundedContent.text,
|
|
1188
|
-
truncated: boundedContent.truncated,
|
|
1189
|
-
};
|
|
1190
|
-
structuredContent = result;
|
|
1191
|
-
break;
|
|
1192
|
-
}
|
|
1193
|
-
if (kind === "symbol") {
|
|
1194
|
-
const definitions = await findDefinition(root, target, maxResults, maxBytes);
|
|
1195
|
-
const definitionsWithContext = await Promise.all(definitions.map(async (definition) => {
|
|
1196
|
-
const range = await readRange(root, definition.path, Math.max(1, definition.line - contextLines), definition.line + contextLines);
|
|
1197
|
-
return { ...definition, context: range.content };
|
|
1198
|
-
}));
|
|
1199
|
-
const references = args.includeReferences === true
|
|
1200
|
-
? await findReferences(root, target, maxResults, maxBytes)
|
|
1201
|
-
: undefined;
|
|
1202
|
-
const result = {
|
|
1203
|
-
target,
|
|
1204
|
-
kind,
|
|
1205
|
-
definitions: definitionsWithContext,
|
|
1206
|
-
...(references ? { references } : {}),
|
|
1207
|
-
};
|
|
1208
|
-
const bounded = truncateText(JSON.stringify(result), maxBytes);
|
|
1209
|
-
structuredContent = bounded.truncated
|
|
1210
|
-
? { target, kind, result: bounded.text, truncated: true }
|
|
1211
|
-
: result;
|
|
1212
|
-
break;
|
|
1213
|
-
}
|
|
1214
|
-
const matches = await searchCode(root, target, false, maxResults, maxBytes);
|
|
1215
|
-
const relevant = await Promise.all(matches.map(async (match) => {
|
|
1216
|
-
const range = await readRange(root, match.path, Math.max(1, match.line - contextLines), match.line + contextLines);
|
|
1217
|
-
return {
|
|
1218
|
-
...match,
|
|
1219
|
-
context: range.content,
|
|
1220
|
-
startLine: range.startLine,
|
|
1221
|
-
endLine: range.endLine,
|
|
1222
|
-
};
|
|
1223
|
-
}));
|
|
1224
|
-
const bounded = boundedItems(relevant, maxBytes);
|
|
1225
|
-
structuredContent = {
|
|
1226
|
-
target,
|
|
1227
|
-
kind: "code",
|
|
1228
|
-
matches: bounded.items,
|
|
1229
|
-
count: bounded.items.length,
|
|
1230
|
-
truncated: bounded.truncated || relevant.length >= maxResults,
|
|
1231
|
-
};
|
|
1232
|
-
break;
|
|
1233
|
-
}
|
|
1234
858
|
case "project_context": {
|
|
1235
859
|
const overview = await projectOverview(root);
|
|
1236
860
|
let pkg = { path: "package.json", exists: false };
|
|
@@ -1253,39 +877,12 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
1253
877
|
...overview,
|
|
1254
878
|
package: pkg,
|
|
1255
879
|
execution: { npmScripts: [...npmAllowed], commands: [...allowedCommands] },
|
|
1256
|
-
}), maxBytes);
|
|
880
|
+
}), Math.max(1, maxBytes - 128));
|
|
1257
881
|
structuredContent = bounded.truncated
|
|
1258
882
|
? { result: bounded.text, truncated: true }
|
|
1259
883
|
: JSON.parse(bounded.text);
|
|
1260
884
|
break;
|
|
1261
885
|
}
|
|
1262
|
-
case "read_relevant": {
|
|
1263
|
-
const query = String(args.query);
|
|
1264
|
-
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
1265
|
-
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
1266
|
-
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
1267
|
-
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
1268
|
-
const matches = await searchCode(root, query, Boolean(args.regex), maxResults, outputBytesArg(args.maxBytes));
|
|
1269
|
-
const relevant = await Promise.all(matches.map(async (match) => {
|
|
1270
|
-
const range = await readRange(root, match.path, Math.max(1, match.line - contextLines), match.line + contextLines);
|
|
1271
|
-
return {
|
|
1272
|
-
...match,
|
|
1273
|
-
context: range.content,
|
|
1274
|
-
startLine: range.startLine,
|
|
1275
|
-
endLine: range.endLine,
|
|
1276
|
-
};
|
|
1277
|
-
}));
|
|
1278
|
-
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1279
|
-
const bounded = boundedItems(relevant, Math.max(1, maxBytes - 128));
|
|
1280
|
-
const result = {
|
|
1281
|
-
query,
|
|
1282
|
-
matches: bounded.items,
|
|
1283
|
-
count: relevant.length,
|
|
1284
|
-
truncated: bounded.truncated || relevant.length >= maxResults,
|
|
1285
|
-
};
|
|
1286
|
-
structuredContent = result;
|
|
1287
|
-
break;
|
|
1288
|
-
}
|
|
1289
886
|
case "validate": {
|
|
1290
887
|
const level = typeof args.level === "string" ? args.level : "diagnostics";
|
|
1291
888
|
const maxResults = numberArg(args.maxResults, 200, 1000);
|
|
@@ -1298,6 +895,8 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
1298
895
|
: level === "full"
|
|
1299
896
|
? ["check", "test"].filter((script) => npmAllowed.includes(script))
|
|
1300
897
|
: [];
|
|
898
|
+
if (scripts.length > 10)
|
|
899
|
+
throw new Error("validate accepts at most 10 scripts per call.");
|
|
1301
900
|
const checks = [
|
|
1302
901
|
{
|
|
1303
902
|
kind: "diagnostics",
|
|
@@ -1317,60 +916,55 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
1317
916
|
if (result.exitCode !== 0)
|
|
1318
917
|
break;
|
|
1319
918
|
}
|
|
1320
|
-
|
|
919
|
+
const validation = {
|
|
1321
920
|
level,
|
|
1322
921
|
ok: checks.every((check) => check.ok === true),
|
|
1323
922
|
checks,
|
|
1324
923
|
scriptsRequested: scripts,
|
|
1325
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
|
+
}
|
|
1326
957
|
break;
|
|
1327
958
|
}
|
|
1328
|
-
case "git_changes": {
|
|
1329
|
-
const status = await gitStatus(root);
|
|
1330
|
-
const includeDiff = Boolean(args.includeDiff);
|
|
1331
|
-
const path = args.path === undefined ? undefined : String(args.path);
|
|
1332
|
-
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1333
|
-
const diff = includeDiff ? await gitDiff(root, false, path) : undefined;
|
|
1334
|
-
const statusBytes = Buffer.byteLength(JSON.stringify(status), "utf8");
|
|
1335
|
-
const diffBudget = Math.max(0, maxBytes - statusBytes - 64);
|
|
1336
|
-
const boundedDiff = diff ? truncateText(diff.stdout, diffBudget) : undefined;
|
|
1337
|
-
const result = {
|
|
1338
|
-
status,
|
|
1339
|
-
...(diff
|
|
1340
|
-
? {
|
|
1341
|
-
diff: {
|
|
1342
|
-
...diff,
|
|
1343
|
-
stdout: boundedDiff?.text ?? "",
|
|
1344
|
-
truncated: boundedDiff?.truncated ?? false,
|
|
1345
|
-
},
|
|
1346
|
-
}
|
|
1347
|
-
: {}),
|
|
1348
|
-
};
|
|
1349
|
-
structuredContent = result;
|
|
1350
|
-
break;
|
|
1351
|
-
}
|
|
1352
|
-
case "project_overview":
|
|
1353
|
-
structuredContent = await projectOverview(root);
|
|
1354
|
-
break;
|
|
1355
|
-
case "package_info":
|
|
1356
|
-
structuredContent = await packageInfo(root);
|
|
1357
|
-
break;
|
|
1358
|
-
case "allowed_commands":
|
|
1359
|
-
structuredContent = {
|
|
1360
|
-
npmScripts: [...npmAllowed],
|
|
1361
|
-
commands: [...allowedCommands],
|
|
1362
|
-
blacklistedCommands: [...blacklistedCommands],
|
|
1363
|
-
};
|
|
1364
|
-
break;
|
|
1365
959
|
case "file_info":
|
|
1366
960
|
structuredContent = await fileInfo(root, String(args.path));
|
|
1367
961
|
break;
|
|
1368
|
-
case "diagnostics":
|
|
1369
|
-
structuredContent = boundedItems(await diagnostics(root, numberArg(args.maxResults, 200, 1000)), outputBytesArg(args.maxBytes)).items;
|
|
1370
|
-
break;
|
|
1371
962
|
case "run_npm_batch": {
|
|
1372
963
|
const output = [];
|
|
1373
|
-
|
|
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) {
|
|
1374
968
|
const value = await runNpm(root, script, 120000, npmAllowed, signal);
|
|
1375
969
|
output.push(value);
|
|
1376
970
|
if (value.exitCode !== 0)
|
|
@@ -1398,7 +992,10 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, blacklist
|
|
|
1398
992
|
}
|
|
1399
993
|
case "run_command_batch": {
|
|
1400
994
|
const output = [];
|
|
1401
|
-
|
|
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) {
|
|
1402
999
|
const value = await runCommand(root, String(item.command), Array.isArray(item.args) ? item.args.map(String) : [], allowedCommands, blacklistedCommands, 120000, signal);
|
|
1403
1000
|
output.push(value);
|
|
1404
1001
|
if (value.exitCode !== 0)
|
|
@@ -1554,6 +1151,8 @@ function telemetryRuntime(params) {
|
|
|
1554
1151
|
...(typeof clientInfo?.name === "string" ? { mcpClientName: clientInfo.name } : {}),
|
|
1555
1152
|
...(typeof clientInfo?.version === "string" ? { mcpClientVersion: clientInfo.version } : {}),
|
|
1556
1153
|
nodeVersion: process.versions.node,
|
|
1154
|
+
...linuxDistribution(),
|
|
1155
|
+
kernelVersion: release(),
|
|
1557
1156
|
hostname: hostname(),
|
|
1558
1157
|
username: userInfo().username,
|
|
1559
1158
|
processId: process.pid,
|
|
@@ -1562,37 +1161,106 @@ function telemetryRuntime(params) {
|
|
|
1562
1161
|
memoryRssBytes: process.memoryUsage().rss,
|
|
1563
1162
|
};
|
|
1564
1163
|
}
|
|
1565
|
-
function
|
|
1164
|
+
function linuxDistribution() {
|
|
1165
|
+
if (process.platform !== "linux")
|
|
1166
|
+
return {};
|
|
1167
|
+
try {
|
|
1168
|
+
const values = Object.fromEntries(readFileSync("/etc/os-release", "utf8")
|
|
1169
|
+
.split("\n")
|
|
1170
|
+
.map((line) => line.match(/^([A-Z_]+)=(.*)$/))
|
|
1171
|
+
.filter((match) => match !== null)
|
|
1172
|
+
.map((match) => [match[1], match[2]?.replace(/^"|"$/g, "")]));
|
|
1173
|
+
return {
|
|
1174
|
+
...(values.ID ? { distroId: values.ID } : {}),
|
|
1175
|
+
...(values.PRETTY_NAME ? { distroName: values.PRETTY_NAME } : {}),
|
|
1176
|
+
...(values.VERSION_ID ? { distroVersion: values.VERSION_ID } : {}),
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
catch {
|
|
1180
|
+
return {};
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function diagnosticToolDetails(input, output) {
|
|
1184
|
+
const serialize = (value) => {
|
|
1185
|
+
try {
|
|
1186
|
+
const json = JSON.stringify(value);
|
|
1187
|
+
if (json === undefined)
|
|
1188
|
+
return null;
|
|
1189
|
+
if (Buffer.byteLength(json, "utf8") <= 64_000)
|
|
1190
|
+
return value;
|
|
1191
|
+
return {
|
|
1192
|
+
truncated: true,
|
|
1193
|
+
byteLength: Buffer.byteLength(json, "utf8"),
|
|
1194
|
+
preview: json.slice(0, 64_000),
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
catch {
|
|
1198
|
+
return { serializationFailed: true };
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
return { input: serialize(input), output: serialize(output) };
|
|
1202
|
+
}
|
|
1203
|
+
function telemetryHash(value) {
|
|
1204
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1205
|
+
}
|
|
1206
|
+
// A rough byte-based proxy, not a model-tokenizer count or Claude billing measurement.
|
|
1207
|
+
function estimateTokens(value) {
|
|
1208
|
+
return Math.ceil(Buffer.byteLength(value, "utf8") / 4);
|
|
1209
|
+
}
|
|
1210
|
+
function toolTelemetryMetrics(toolName, result, seenOutputFingerprints) {
|
|
1566
1211
|
const structured = result.structuredContent;
|
|
1567
1212
|
if (!Array.isArray(structured) && !isRecord(structured))
|
|
1568
1213
|
return {};
|
|
1569
1214
|
const paths = new Set();
|
|
1215
|
+
const files = new Map();
|
|
1570
1216
|
let truncated = false;
|
|
1571
1217
|
let paginated = false;
|
|
1218
|
+
let textOutputBytes = 0;
|
|
1572
1219
|
const visit = (value) => {
|
|
1220
|
+
if (typeof value === "string") {
|
|
1221
|
+
textOutputBytes += Buffer.byteLength(value, "utf8");
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (Array.isArray(value)) {
|
|
1225
|
+
for (const child of value)
|
|
1226
|
+
visit(child);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1573
1229
|
if (!isRecord(value))
|
|
1574
1230
|
return;
|
|
1575
|
-
if (typeof value.path === "string")
|
|
1231
|
+
if (typeof value.path === "string") {
|
|
1576
1232
|
paths.add(value.path);
|
|
1233
|
+
const content = typeof value.content === "string" ? value.content : undefined;
|
|
1234
|
+
files.set(value.path, {
|
|
1235
|
+
pathHash: telemetryHash(value.path),
|
|
1236
|
+
...(content === undefined ? {} : { contentHash: telemetryHash(content) }),
|
|
1237
|
+
bytes: content === undefined ? 0 : Buffer.byteLength(content, "utf8"),
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1577
1240
|
if (value.truncated === true)
|
|
1578
1241
|
truncated = true;
|
|
1579
1242
|
if (value.nextCursor !== undefined)
|
|
1580
1243
|
paginated = true;
|
|
1581
|
-
for (const child of Object.values(value))
|
|
1582
|
-
|
|
1583
|
-
for (const item of child)
|
|
1584
|
-
visit(item);
|
|
1585
|
-
else if (isRecord(child))
|
|
1586
|
-
visit(child);
|
|
1587
|
-
}
|
|
1244
|
+
for (const child of Object.values(value))
|
|
1245
|
+
visit(child);
|
|
1588
1246
|
};
|
|
1589
1247
|
visit(structured);
|
|
1248
|
+
const outputJson = JSON.stringify(result);
|
|
1249
|
+
const outputFingerprint = telemetryHash(outputJson);
|
|
1250
|
+
const outputBytes = Buffer.byteLength(outputJson, "utf8");
|
|
1251
|
+
const duplicateOutputBytes = seenOutputFingerprints.has(outputFingerprint) ? outputBytes : 0;
|
|
1252
|
+
seenOutputFingerprints.add(outputFingerprint);
|
|
1590
1253
|
return {
|
|
1591
|
-
...(paths.size > 0 && /^(write|patch|delete|read|file
|
|
1254
|
+
...(paths.size > 0 && /^(write|patch|delete|read|file)/.test(toolName)
|
|
1592
1255
|
? { affectedFiles: paths.size }
|
|
1593
1256
|
: {}),
|
|
1594
1257
|
...(truncated ? { truncated: true } : {}),
|
|
1595
1258
|
...(paginated ? { paginated: true } : {}),
|
|
1259
|
+
outputFingerprint,
|
|
1260
|
+
duplicateOutputBytes,
|
|
1261
|
+
textOutputBytes,
|
|
1262
|
+
uniqueFileCount: files.size,
|
|
1263
|
+
...(files.size ? { files: [...files.values()].slice(0, 100) } : {}),
|
|
1596
1264
|
};
|
|
1597
1265
|
}
|
|
1598
1266
|
function commandTelemetryDescriptor(toolName) {
|
|
@@ -1602,8 +1270,6 @@ function commandTelemetryDescriptor(toolName) {
|
|
|
1602
1270
|
return { family: "command", operation: "batch" };
|
|
1603
1271
|
if (toolName === "validate")
|
|
1604
1272
|
return { family: "npm", operation: "validate" };
|
|
1605
|
-
if (toolName.startsWith("git_"))
|
|
1606
|
-
return { family: "git", operation: toolName.slice("git_".length) };
|
|
1607
1273
|
return undefined;
|
|
1608
1274
|
}
|
|
1609
1275
|
async function buildTelemetryProjectContext(root, codeGraph) {
|
|
@@ -1673,8 +1339,8 @@ function objectArray(value, name) {
|
|
|
1673
1339
|
}
|
|
1674
1340
|
function outputBytesArg(value) {
|
|
1675
1341
|
const result = value === undefined ? DEFAULT_OUTPUT_BYTES : Number(value);
|
|
1676
|
-
if (!Number.isInteger(result) || result < 1024 || result >
|
|
1677
|
-
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.");
|
|
1678
1344
|
return result;
|
|
1679
1345
|
}
|
|
1680
1346
|
function truncateText(value, maxBytes) {
|
|
@@ -1773,33 +1439,6 @@ function decodeCursor(value) {
|
|
|
1773
1439
|
throw new Error("Invalid pagination cursor.");
|
|
1774
1440
|
return offset;
|
|
1775
1441
|
}
|
|
1776
|
-
async function patchFilesDryRun(root, files) {
|
|
1777
|
-
return Promise.all(files.map(async ({ path: relativePath, patches }) => {
|
|
1778
|
-
let content = await readFile(root, relativePath);
|
|
1779
|
-
for (const patch of patches) {
|
|
1780
|
-
if (!patch.search)
|
|
1781
|
-
throw new Error("Patch search text cannot be empty.");
|
|
1782
|
-
const occurrences = content.split(patch.search).length - 1;
|
|
1783
|
-
const count = patch.count ?? 1;
|
|
1784
|
-
if (occurrences === 0)
|
|
1785
|
-
throw new Error(`Patch text was not found in '${relativePath}'.`);
|
|
1786
|
-
if (!Number.isInteger(count) || count < 1)
|
|
1787
|
-
throw new Error("Patch count must be a positive integer.");
|
|
1788
|
-
if (occurrences < count)
|
|
1789
|
-
throw new Error(`Patch text occurs ${occurrences} time(s), but ${count} replacement(s) were requested.`);
|
|
1790
|
-
let offset = 0;
|
|
1791
|
-
for (let index = 0; index < count; index += 1) {
|
|
1792
|
-
const position = content.indexOf(patch.search, offset);
|
|
1793
|
-
content =
|
|
1794
|
-
content.slice(0, position) +
|
|
1795
|
-
patch.replace +
|
|
1796
|
-
content.slice(position + patch.search.length);
|
|
1797
|
-
offset = position + patch.replace.length;
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
return { path: relativePath, applied: patches.length, content, dryRun: true };
|
|
1801
|
-
}));
|
|
1802
|
-
}
|
|
1803
1442
|
async function notifyResourceChange(notify, root, pathValue, before) {
|
|
1804
1443
|
const after = await skillResourceUrisForPath(root, pathValue);
|
|
1805
1444
|
if (before.length === 0 && after.length === 0)
|