agent-dir 0.1.3 → 0.2.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 +38 -0
- package/dist/bin/agent-dir.js +1 -2
- package/dist/bin/agent-dir.js.map +1 -1
- package/dist/src/codegraph.d.ts +33 -0
- package/dist/src/codegraph.d.ts.map +1 -0
- package/dist/src/codegraph.js +265 -0
- package/dist/src/codegraph.js.map +1 -0
- package/dist/src/logging.d.ts.map +1 -1
- package/dist/src/logging.js +7 -0
- package/dist/src/logging.js.map +1 -1
- package/dist/src/mcp.d.ts.map +1 -1
- package/dist/src/mcp.js +775 -76
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/tools/code.d.ts +7 -7
- package/dist/src/tools/code.d.ts.map +1 -1
- package/dist/src/tools/code.js +49 -16
- package/dist/src/tools/code.js.map +1 -1
- package/dist/src/tools/files.d.ts +19 -0
- package/dist/src/tools/files.d.ts.map +1 -1
- package/dist/src/tools/files.js +119 -0
- package/dist/src/tools/files.js.map +1 -1
- package/package.json +5 -3
package/dist/src/mcp.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { CodeGraphIntegration } from "./codegraph.js";
|
|
2
3
|
import { findDefinition, findExports, findFiles, findImports, findReferences, findSymbol, searchCode, searchFiles, } from "./tools/code.js";
|
|
3
4
|
import { runCommand } from "./tools/commands.js";
|
|
4
5
|
import { diagnostics } from "./tools/diagnostics.js";
|
|
5
|
-
import { deleteFile, listDirs, listFiles, patchFiles, readFile, readRange, writeFile, } from "./tools/files.js";
|
|
6
|
+
import { applyFileChanges, deleteFile, listDirs, listFiles, patchFiles, readFile, readRange, writeFile, } from "./tools/files.js";
|
|
6
7
|
import { gitCommit, gitDiff, gitLog, gitPush, gitRestore, gitStage, gitStatus, gitUnstage, } from "./tools/git.js";
|
|
7
8
|
import { fileInfo, packageInfo, projectOverview } from "./tools/metadata.js";
|
|
8
9
|
import { runNpm } from "./tools/npm.js";
|
|
@@ -16,6 +17,9 @@ const SERVER_INFO_META = { "io.modelcontextprotocol/serverInfo": SERVER_INFO };
|
|
|
16
17
|
const PROTOCOL_META = "io.modelcontextprotocol/protocolVersion";
|
|
17
18
|
const CLIENT_CAPABILITIES_META = "io.modelcontextprotocol/clientCapabilities";
|
|
18
19
|
const PAGE_SIZE = 50;
|
|
20
|
+
const DEFAULT_OUTPUT_BYTES = 32_000;
|
|
21
|
+
const INSTRUCTIONS_URI = "agent-dir://instructions";
|
|
22
|
+
const CAPABILITIES_URI = "agent-dir://capabilities";
|
|
19
23
|
const SUBSCRIPTION_ID_META = "io.modelcontextprotocol/subscriptionId";
|
|
20
24
|
const stringSchema = { type: "string" };
|
|
21
25
|
const pathSchema = { type: "string", description: "Path relative to the exposed project root." };
|
|
@@ -41,9 +45,18 @@ const commandResultSchema = objectOutput({
|
|
|
41
45
|
stdout: stringSchema,
|
|
42
46
|
stderr: stringSchema,
|
|
43
47
|
}, ["command", "exitCode", "stdout", "stderr"]);
|
|
48
|
+
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.", {
|
|
49
|
+
query: { type: "string", minLength: 1 },
|
|
50
|
+
maxFiles: { type: "integer", minimum: 1, maximum: 100 },
|
|
51
|
+
}, { type: "object" }, ["query"]);
|
|
44
52
|
const baseTools = [
|
|
45
|
-
tool("list_files", "List all visible project files and directories recursively up to the server's discovery depth.
|
|
46
|
-
|
|
53
|
+
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.", {
|
|
54
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
55
|
+
}, arrayOutput(fileEntrySchema)),
|
|
56
|
+
tool("list_dirs", "List files and directories directly inside one or more project directories.", {
|
|
57
|
+
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
58
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
59
|
+
}, arrayOutput(objectOutput({ path: stringSchema, entries: arrayOutput(fileEntrySchema) }, [
|
|
47
60
|
"path",
|
|
48
61
|
"entries",
|
|
49
62
|
])), ["paths"]),
|
|
@@ -51,21 +64,58 @@ const baseTools = [
|
|
|
51
64
|
path: pathSchema,
|
|
52
65
|
startLine: { type: "integer", minimum: 1 },
|
|
53
66
|
endLine: { type: "integer", minimum: 1 },
|
|
67
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
54
68
|
}, objectOutput({
|
|
55
69
|
path: stringSchema,
|
|
56
70
|
startLine: { type: "integer" },
|
|
57
71
|
endLine: { type: "integer" },
|
|
58
72
|
content: stringSchema,
|
|
59
73
|
}, ["path", "startLine", "endLine", "content"]), ["path", "startLine", "endLine"]),
|
|
60
|
-
tool("read_files", "Read one or more UTF-8 project files in one call.
|
|
74
|
+
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.", {
|
|
75
|
+
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
76
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
77
|
+
}, arrayOutput(objectOutput({ path: stringSchema, content: stringSchema }, ["path", "content"])), ["paths"]),
|
|
78
|
+
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.", {
|
|
79
|
+
dryRun: { type: "boolean", default: false },
|
|
80
|
+
changes: {
|
|
81
|
+
type: "array",
|
|
82
|
+
minItems: 1,
|
|
83
|
+
items: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
kind: { type: "string", enum: ["write", "patch", "delete"] },
|
|
87
|
+
path: pathSchema,
|
|
88
|
+
content: stringSchema,
|
|
89
|
+
patches: {
|
|
90
|
+
type: "array",
|
|
91
|
+
items: objectOutput({
|
|
92
|
+
search: stringSchema,
|
|
93
|
+
replace: stringSchema,
|
|
94
|
+
count: { type: "integer", minimum: 1 },
|
|
95
|
+
}, ["search", "replace"]),
|
|
96
|
+
minItems: 1,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required: ["kind", "path"],
|
|
100
|
+
additionalProperties: false,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}, arrayOutput(objectOutput({
|
|
104
|
+
kind: { type: "string", enum: ["write", "patch", "delete"] },
|
|
105
|
+
path: stringSchema,
|
|
106
|
+
changed: { type: "boolean" },
|
|
107
|
+
dryRun: { type: "boolean" },
|
|
108
|
+
}, ["kind", "path", "changed"])), ["changes"]),
|
|
61
109
|
tool("write_files", "Create or completely replace one or more UTF-8 project files.", {
|
|
110
|
+
dryRun: { type: "boolean", default: false },
|
|
62
111
|
files: {
|
|
63
112
|
type: "array",
|
|
64
113
|
items: objectOutput({ path: pathSchema, content: stringSchema }, ["path", "content"]),
|
|
65
114
|
minItems: 1,
|
|
66
115
|
},
|
|
67
116
|
}, arrayOutput(objectOutput({ path: stringSchema, written: { type: "boolean" } }, ["path", "written"])), ["files"]),
|
|
68
|
-
tool("patch_files", "Apply exact text replacements to one or more UTF-8 files.", {
|
|
117
|
+
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.", {
|
|
118
|
+
dryRun: { type: "boolean", default: false },
|
|
69
119
|
files: {
|
|
70
120
|
type: "array",
|
|
71
121
|
items: objectOutput({
|
|
@@ -82,65 +132,150 @@ const baseTools = [
|
|
|
82
132
|
}, ["path", "patches"]),
|
|
83
133
|
minItems: 1,
|
|
84
134
|
},
|
|
135
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
85
136
|
}, arrayOutput(objectOutput({ path: stringSchema, applied: { type: "integer" }, content: stringSchema }, [
|
|
86
137
|
"path",
|
|
87
138
|
"applied",
|
|
88
139
|
"content",
|
|
89
140
|
])), ["files"]),
|
|
90
|
-
tool("delete_files", "Delete one or more project files.", {
|
|
91
|
-
|
|
141
|
+
tool("delete_files", "Delete one or more project files.", {
|
|
142
|
+
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
143
|
+
dryRun: { type: "boolean", default: false },
|
|
144
|
+
}, arrayOutput(objectOutput({ path: stringSchema, deleted: { type: "boolean" } }, ["path", "deleted"])), ["paths"]),
|
|
145
|
+
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.", {
|
|
92
146
|
query: stringSchema,
|
|
93
147
|
regex: { type: "boolean", default: false },
|
|
94
148
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
149
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
95
150
|
}, arrayOutput(searchMatchSchema), ["query"]),
|
|
96
|
-
tool("find_files", "Find project files by a simple glob pattern using * and ?.", {
|
|
151
|
+
tool("find_files", "Find project files by a simple glob pattern using * and ?. Prefer this over list_files when you know the filename pattern.", {
|
|
97
152
|
pattern: stringSchema,
|
|
98
153
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
154
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
99
155
|
}, arrayOutput(stringSchema), ["pattern"]),
|
|
100
|
-
tool("search_code", "Search source-like project files for text or a regular expression.", {
|
|
156
|
+
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.", {
|
|
101
157
|
query: stringSchema,
|
|
102
158
|
regex: { type: "boolean", default: false },
|
|
103
159
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
160
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
104
161
|
}, arrayOutput(searchMatchSchema), ["query"]),
|
|
105
|
-
tool("find_symbol", "Find likely symbol definitions across source files.", {
|
|
162
|
+
tool("find_symbol", "Find likely symbol definitions across source files. Prefer this over reading whole modules when locating an implementation.", {
|
|
106
163
|
symbol: stringSchema,
|
|
107
164
|
maxResults: { type: "integer", minimum: 1, maximum: 500, default: 100 },
|
|
165
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
108
166
|
}, arrayOutput(objectOutput({
|
|
109
167
|
...searchMatchSchema.properties,
|
|
110
168
|
symbol: stringSchema,
|
|
111
169
|
kind: stringSchema,
|
|
112
170
|
}, ["path", "line", "column", "text", "symbol", "kind"])), ["symbol"]),
|
|
113
|
-
tool("find_definition", "Find likely definitions of a symbol.", {
|
|
171
|
+
tool("find_definition", "Find likely definitions of a symbol. Use this before reading a large file when you need one implementation.", {
|
|
114
172
|
symbol: stringSchema,
|
|
115
173
|
maxResults: { type: "integer", minimum: 1, maximum: 500, default: 100 },
|
|
174
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
116
175
|
}, arrayOutput(objectOutput({
|
|
117
176
|
...searchMatchSchema.properties,
|
|
118
177
|
symbol: stringSchema,
|
|
119
178
|
kind: stringSchema,
|
|
120
179
|
}, ["path", "line", "column", "text", "symbol", "kind"])), ["symbol"]),
|
|
121
|
-
tool("find_references", "Find source-code references to a symbol.", {
|
|
180
|
+
tool("find_references", "Find source-code references to a symbol. Use this instead of searching entire files when tracing usage of a known symbol.", {
|
|
122
181
|
symbol: stringSchema,
|
|
123
182
|
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
183
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
124
184
|
}, arrayOutput(searchMatchSchema), ["symbol"]),
|
|
125
|
-
tool("find_imports", "Find import and require statements in source files.", {
|
|
185
|
+
tool("find_imports", "Find import and require statements in source files.", {
|
|
186
|
+
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
187
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
188
|
+
}, arrayOutput(objectOutput({
|
|
126
189
|
...searchMatchSchema.properties,
|
|
127
190
|
module: stringSchema,
|
|
128
191
|
kind: { type: "string", enum: ["import", "require"] },
|
|
129
192
|
}, ["path", "line", "column", "text", "module", "kind"]))),
|
|
130
|
-
tool("find_exports", "Find exported declarations in source files.", {
|
|
193
|
+
tool("find_exports", "Find exported declarations in source files.", {
|
|
194
|
+
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
195
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
196
|
+
}, arrayOutput(objectOutput({
|
|
131
197
|
...searchMatchSchema.properties,
|
|
132
198
|
symbol: stringSchema,
|
|
133
199
|
kind: { type: "string", const: "export" },
|
|
134
200
|
}, ["path", "line", "column", "text", "symbol", "kind"]))),
|
|
135
|
-
tool("git_status", "Show Git working-tree status and branch information.", {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
tool("
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
tool("
|
|
143
|
-
|
|
201
|
+
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.", {
|
|
202
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
203
|
+
}, commandResultSchema),
|
|
204
|
+
tool("git_stage", "Stage one or more Git paths.", {
|
|
205
|
+
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
206
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
207
|
+
}, commandResultSchema, ["paths"]),
|
|
208
|
+
tool("git_unstage", "Unstage one or more Git paths without changing their working-tree contents.", {
|
|
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_commit", "Create a Git commit with the currently staged changes.", {
|
|
213
|
+
message: stringSchema,
|
|
214
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
215
|
+
}, commandResultSchema, ["message"]),
|
|
216
|
+
tool("git_restore", "Restore one or more Git paths from the index, discarding unstaged working-tree changes.", {
|
|
217
|
+
paths: { type: "array", items: pathSchema, minItems: 1 },
|
|
218
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
219
|
+
}, commandResultSchema, ["paths"]),
|
|
220
|
+
tool("git_push", "Push the current Git branch to a remote.", {
|
|
221
|
+
remote: stringSchema,
|
|
222
|
+
branch: stringSchema,
|
|
223
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
224
|
+
}, commandResultSchema),
|
|
225
|
+
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.", {
|
|
226
|
+
staged: { type: "boolean", default: false },
|
|
227
|
+
path: pathSchema,
|
|
228
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
229
|
+
}, commandResultSchema),
|
|
230
|
+
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.", {
|
|
231
|
+
limit: { type: "integer", minimum: 1, maximum: 200, default: 20 },
|
|
232
|
+
path: pathSchema,
|
|
233
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
234
|
+
}, commandResultSchema),
|
|
235
|
+
tool("locate", "Locate project code or files in one call. Use kind auto, code, text, or file.", {
|
|
236
|
+
query: stringSchema,
|
|
237
|
+
kind: { type: "string", enum: ["auto", "code", "text", "file"], default: "auto" },
|
|
238
|
+
regex: { type: "boolean", default: false },
|
|
239
|
+
maxResults: { type: "integer", minimum: 1, maximum: 100, default: 20 },
|
|
240
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
241
|
+
}, { type: "object", additionalProperties: true }, ["query"]),
|
|
242
|
+
tool("inspect_symbol", "Inspect a known symbol in one call, returning its definition, bounded source context, and optionally references.", {
|
|
243
|
+
symbol: stringSchema,
|
|
244
|
+
includeReferences: { type: "boolean", default: false },
|
|
245
|
+
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
246
|
+
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
247
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
248
|
+
}, { type: "object", additionalProperties: true }, ["symbol"]),
|
|
249
|
+
tool("inspect", "Inspect a project target in one call. Use kind auto, file, symbol, or code; returns bounded context and optional symbol references.", {
|
|
250
|
+
target: stringSchema,
|
|
251
|
+
kind: { type: "string", enum: ["auto", "file", "symbol", "code"], default: "auto" },
|
|
252
|
+
includeReferences: { type: "boolean", default: false },
|
|
253
|
+
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
254
|
+
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
255
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
256
|
+
}, { type: "object", additionalProperties: true }, ["target"]),
|
|
257
|
+
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.", {
|
|
258
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
259
|
+
}, { type: "object", additionalProperties: true }),
|
|
260
|
+
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.", {
|
|
261
|
+
query: stringSchema,
|
|
262
|
+
regex: { type: "boolean", default: false },
|
|
263
|
+
contextLines: { type: "integer", minimum: 0, maximum: 20, default: 3 },
|
|
264
|
+
maxResults: { type: "integer", minimum: 1, maximum: 20, default: 5 },
|
|
265
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
266
|
+
}, { type: "object", additionalProperties: true }, ["query"]),
|
|
267
|
+
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.", {
|
|
268
|
+
level: { type: "string", enum: ["diagnostics", "scripts", "full"], default: "diagnostics" },
|
|
269
|
+
scripts: { type: "array", items: stringSchema, minItems: 1 },
|
|
270
|
+
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
271
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
272
|
+
}, { type: "object", additionalProperties: true }),
|
|
273
|
+
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.", {
|
|
274
|
+
includeDiff: { type: "boolean", default: false },
|
|
275
|
+
path: pathSchema,
|
|
276
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
277
|
+
}, { type: "object", additionalProperties: true }),
|
|
278
|
+
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({
|
|
144
279
|
root: stringSchema,
|
|
145
280
|
files: { type: "integer" },
|
|
146
281
|
directories: { type: "integer" },
|
|
@@ -155,20 +290,27 @@ const baseTools = [
|
|
|
155
290
|
commands: { type: "array", items: stringSchema },
|
|
156
291
|
}, ["npmScripts", "commands"])),
|
|
157
292
|
tool("file_info", "Return filesystem metadata for a project path.", { path: pathSchema }, { type: "object", additionalProperties: true }, ["path"]),
|
|
158
|
-
tool("diagnostics", "Run project-independent diagnostics without invoking tests, linters, or typecheckers.
|
|
293
|
+
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.", {
|
|
294
|
+
maxResults: { type: "integer", minimum: 1, maximum: 1000, default: 200 },
|
|
295
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
296
|
+
}, arrayOutput(objectOutput({
|
|
159
297
|
severity: { type: "string", enum: ["error", "warning", "info"] },
|
|
160
298
|
code: stringSchema,
|
|
161
299
|
path: stringSchema,
|
|
162
300
|
line: { type: "integer" },
|
|
163
301
|
message: stringSchema,
|
|
164
302
|
}, ["severity", "code", "message"]))),
|
|
165
|
-
tool("run_npm_batch", "Run one or more explicitly allowed npm scripts sequentially.
|
|
166
|
-
|
|
303
|
+
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.", {
|
|
304
|
+
scripts: { type: "array", items: stringSchema, minItems: 1 },
|
|
305
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
306
|
+
}, arrayOutput(commandResultSchema), ["scripts"]),
|
|
307
|
+
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.", {
|
|
167
308
|
commands: {
|
|
168
309
|
type: "array",
|
|
169
310
|
items: objectOutput({ command: stringSchema, args: { type: "array", items: stringSchema } }, ["command"]),
|
|
170
311
|
minItems: 1,
|
|
171
312
|
},
|
|
313
|
+
maxBytes: { type: "integer", minimum: 1024, maximum: 1000000, default: DEFAULT_OUTPUT_BYTES },
|
|
172
314
|
}, arrayOutput(commandResultSchema), ["commands"]),
|
|
173
315
|
];
|
|
174
316
|
function tool(name, description, properties, outputSchema, required = []) {
|
|
@@ -186,6 +328,7 @@ function tool(name, description, properties, outputSchema, required = []) {
|
|
|
186
328
|
};
|
|
187
329
|
}
|
|
188
330
|
export function createMcpHandler(root, commandConfig = {}) {
|
|
331
|
+
const codeGraph = new CodeGraphIntegration(root);
|
|
189
332
|
const npmAllowed = commandConfig.npm?.allowedScripts ?? [];
|
|
190
333
|
const allowedCommands = commandConfig.commands ?? [];
|
|
191
334
|
const subscriptions = new Set();
|
|
@@ -258,7 +401,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
258
401
|
protocolVersion: LEGACY_PROTOCOL_VERSION,
|
|
259
402
|
capabilities: capabilities(),
|
|
260
403
|
serverInfo: SERVER_INFO,
|
|
261
|
-
instructions:
|
|
404
|
+
instructions: buildAgentInstructions(npmAllowed, allowedCommands, await codeGraph.capability()),
|
|
262
405
|
},
|
|
263
406
|
});
|
|
264
407
|
}
|
|
@@ -290,13 +433,14 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
290
433
|
supportedVersions: [PROTOCOL_VERSION],
|
|
291
434
|
capabilities: capabilities(),
|
|
292
435
|
_meta: SERVER_INFO_META,
|
|
293
|
-
instructions:
|
|
436
|
+
instructions: buildAgentInstructions(npmAllowed, allowedCommands, await codeGraph.capability()),
|
|
437
|
+
codeGraph: await codeGraph.capability(),
|
|
294
438
|
ttlMs: 0,
|
|
295
439
|
cacheScope: "private",
|
|
296
440
|
},
|
|
297
441
|
});
|
|
298
442
|
case "tools/list": {
|
|
299
|
-
const page = paginate(
|
|
443
|
+
const page = paginate(await availableTools(codeGraph), params.cursor);
|
|
300
444
|
return json({
|
|
301
445
|
jsonrpc: "2.0",
|
|
302
446
|
id: message.id,
|
|
@@ -314,7 +458,7 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
314
458
|
if (typeof params.name !== "string" || params.name.length === 0)
|
|
315
459
|
return jsonRpcError(message.id ?? null, -32602, "tools/call name must be a non-empty string.", 400);
|
|
316
460
|
const toolName = params.name;
|
|
317
|
-
const toolDefinition =
|
|
461
|
+
const toolDefinition = (await availableTools(codeGraph)).find((item) => item.name === toolName);
|
|
318
462
|
if (!toolDefinition)
|
|
319
463
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
320
464
|
if (params.arguments !== undefined && !isRecord(params.arguments))
|
|
@@ -326,14 +470,28 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
326
470
|
return json({
|
|
327
471
|
jsonrpc: "2.0",
|
|
328
472
|
id: message.id,
|
|
329
|
-
result: await callTool(toolName, argumentsValue, root, npmAllowed, allowedCommands, notify, request.signal),
|
|
473
|
+
result: await callTool(toolName, argumentsValue, root, npmAllowed, allowedCommands, notify, request.signal, codeGraph),
|
|
330
474
|
});
|
|
331
475
|
}
|
|
332
476
|
case "resources/list": {
|
|
333
|
-
const resources =
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
477
|
+
const resources = [
|
|
478
|
+
{
|
|
479
|
+
uri: INSTRUCTIONS_URI,
|
|
480
|
+
name: "Agent Dir usage instructions",
|
|
481
|
+
description: "Dynamically generated, efficiency-focused instructions for using this Agent Dir server.",
|
|
482
|
+
mimeType: "text/markdown",
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
uri: CAPABILITIES_URI,
|
|
486
|
+
name: "Agent Dir capabilities",
|
|
487
|
+
description: "Machine-readable current tools and execution policy for this Agent Dir server.",
|
|
488
|
+
mimeType: "application/json",
|
|
489
|
+
},
|
|
490
|
+
...(await listSkillResources(root)).map((item) => ({
|
|
491
|
+
...item,
|
|
492
|
+
description: "Agent Skill resource",
|
|
493
|
+
})),
|
|
494
|
+
];
|
|
337
495
|
const page = paginate(resources, params.cursor);
|
|
338
496
|
return json({
|
|
339
497
|
jsonrpc: "2.0",
|
|
@@ -351,6 +509,42 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
351
509
|
case "resources/read": {
|
|
352
510
|
if (typeof params.uri !== "string" || params.uri.length === 0)
|
|
353
511
|
return jsonRpcError(message.id ?? null, -32602, "resources/read uri must be a non-empty string.", 400);
|
|
512
|
+
if (params.uri === INSTRUCTIONS_URI)
|
|
513
|
+
return json({
|
|
514
|
+
jsonrpc: "2.0",
|
|
515
|
+
id: message.id,
|
|
516
|
+
result: {
|
|
517
|
+
resultType: "complete",
|
|
518
|
+
contents: [
|
|
519
|
+
{
|
|
520
|
+
uri: INSTRUCTIONS_URI,
|
|
521
|
+
mimeType: "text/markdown",
|
|
522
|
+
text: buildAgentInstructions(npmAllowed, allowedCommands, await codeGraph.capability()),
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
ttlMs: 0,
|
|
526
|
+
cacheScope: "private",
|
|
527
|
+
_meta: SERVER_INFO_META,
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
if (params.uri === CAPABILITIES_URI)
|
|
531
|
+
return json({
|
|
532
|
+
jsonrpc: "2.0",
|
|
533
|
+
id: message.id,
|
|
534
|
+
result: {
|
|
535
|
+
resultType: "complete",
|
|
536
|
+
contents: [
|
|
537
|
+
{
|
|
538
|
+
uri: CAPABILITIES_URI,
|
|
539
|
+
mimeType: "application/json",
|
|
540
|
+
text: JSON.stringify(buildAgentCapabilities(npmAllowed, allowedCommands), null, 2),
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
ttlMs: 0,
|
|
544
|
+
cacheScope: "private",
|
|
545
|
+
_meta: SERVER_INFO_META,
|
|
546
|
+
},
|
|
547
|
+
});
|
|
354
548
|
const value = await readSkillResource(root, params.uri);
|
|
355
549
|
return json({
|
|
356
550
|
jsonrpc: "2.0",
|
|
@@ -437,7 +631,8 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
437
631
|
const code = message.method === "skills/get" ||
|
|
438
632
|
message.method === "resources/read" ||
|
|
439
633
|
message.method === "resources/directory/read" ||
|
|
440
|
-
message.method === "subscriptions/listen"
|
|
634
|
+
message.method === "subscriptions/listen" ||
|
|
635
|
+
message.method === "skills/list"
|
|
441
636
|
? -32602
|
|
442
637
|
: -32603;
|
|
443
638
|
return jsonRpcError(message.id ?? null, code, error instanceof Error ? error.message : String(error), code === -32602 ? 400 : 500);
|
|
@@ -459,9 +654,61 @@ export function createMcpHandler(root, commandConfig = {}) {
|
|
|
459
654
|
subscription.controller.close();
|
|
460
655
|
}
|
|
461
656
|
subscriptions.clear();
|
|
657
|
+
void codeGraph.close();
|
|
462
658
|
};
|
|
463
659
|
return handler;
|
|
464
660
|
}
|
|
661
|
+
async function availableTools(codeGraph) {
|
|
662
|
+
const capability = await codeGraph.capability();
|
|
663
|
+
return capability.status === "available" ||
|
|
664
|
+
capability.status === "startup_failed" ||
|
|
665
|
+
capability.status === "runtime_failed"
|
|
666
|
+
? [...baseTools, codeGraphTool]
|
|
667
|
+
: baseTools;
|
|
668
|
+
}
|
|
669
|
+
function buildAgentInstructions(npmAllowed, allowedCommands, codeGraph) {
|
|
670
|
+
const npm = npmAllowed.length ? npmAllowed.map((item) => `- ${item}`).join("\n") : "- none";
|
|
671
|
+
const commands = allowedCommands.length
|
|
672
|
+
? allowedCommands.map((item) => `- ${item}`).join("\n")
|
|
673
|
+
: "- none";
|
|
674
|
+
const codeGraphSection = codeGraph.status === "available" ||
|
|
675
|
+
codeGraph.status === "startup_failed" ||
|
|
676
|
+
codeGraph.status === "runtime_failed"
|
|
677
|
+
? "\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: " +
|
|
678
|
+
codeGraph.status +
|
|
679
|
+
(codeGraph.detail ? ` — ${codeGraph.detail}` : "") +
|
|
680
|
+
"."
|
|
681
|
+
: "";
|
|
682
|
+
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\nOnly use commands from the execution policy. Do not attempt to bypass it with shells or alternate executables.\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}`;
|
|
683
|
+
}
|
|
684
|
+
function buildAgentCapabilities(npmAllowed, allowedCommands) {
|
|
685
|
+
return {
|
|
686
|
+
version: packageVersion,
|
|
687
|
+
tools: baseTools.map((item) => item.name),
|
|
688
|
+
resources: [INSTRUCTIONS_URI, CAPABILITIES_URI],
|
|
689
|
+
execution: { npmScripts: [...npmAllowed], commands: [...allowedCommands] },
|
|
690
|
+
efficiency: {
|
|
691
|
+
preferred: {
|
|
692
|
+
orientation: "project_context",
|
|
693
|
+
focusedReading: "read_relevant",
|
|
694
|
+
changeReview: "git_changes",
|
|
695
|
+
discovery: ["find_files", "list_dirs", "search_code"],
|
|
696
|
+
symbolNavigation: ["find_definition", "find_symbol", "find_references"],
|
|
697
|
+
reading: ["read_range", "read_files"],
|
|
698
|
+
editing: ["apply_changes", "patch_files", "write_files"],
|
|
699
|
+
validation: ["validate", "diagnostics", "targeted npm script"],
|
|
700
|
+
gitInspection: ["git_changes", "git_status", "git_diff", "git_log"],
|
|
701
|
+
},
|
|
702
|
+
avoid: [
|
|
703
|
+
"recursive discovery when targeted search is enough",
|
|
704
|
+
"whole-file reads when a line range is enough",
|
|
705
|
+
"full-repository diffs when a path-scoped diff is enough",
|
|
706
|
+
"expensive validation when a targeted check answers the question",
|
|
707
|
+
"generic command execution when a dedicated Agent Dir tool exists",
|
|
708
|
+
],
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
}
|
|
465
712
|
function capabilities() {
|
|
466
713
|
return {
|
|
467
714
|
tools: { listChanged: true },
|
|
@@ -469,37 +716,102 @@ function capabilities() {
|
|
|
469
716
|
extensions: { "io.modelcontextprotocol/skills": { directoryRead: true } },
|
|
470
717
|
};
|
|
471
718
|
}
|
|
472
|
-
async function callTool(name, args, root, npmAllowed, allowedCommands, notify, signal) {
|
|
719
|
+
async function callTool(name, args, root, npmAllowed, allowedCommands, notify, signal, codeGraph) {
|
|
473
720
|
let structuredContent;
|
|
721
|
+
if (name === "codegraph_explore") {
|
|
722
|
+
if (!codeGraph)
|
|
723
|
+
throw new Error("CodeGraph integration is unavailable.");
|
|
724
|
+
if (typeof args.query !== "string" || args.query.trim().length === 0)
|
|
725
|
+
throw new Error("query must be a non-empty string.");
|
|
726
|
+
if (args.maxFiles !== undefined &&
|
|
727
|
+
(!Number.isInteger(args.maxFiles) || Number(args.maxFiles) < 1 || Number(args.maxFiles) > 100))
|
|
728
|
+
throw new Error("maxFiles must be an integer between 1 and 100.");
|
|
729
|
+
return await codeGraph.explore(args.query, args.maxFiles === undefined ? undefined : Number(args.maxFiles));
|
|
730
|
+
}
|
|
474
731
|
switch (name) {
|
|
475
|
-
case "list_files":
|
|
476
|
-
|
|
732
|
+
case "list_files": {
|
|
733
|
+
const items = await listFiles(root);
|
|
734
|
+
structuredContent = boundedItems(items, outputBytesArg(args.maxBytes)).items;
|
|
477
735
|
break;
|
|
478
|
-
|
|
479
|
-
|
|
736
|
+
}
|
|
737
|
+
case "list_dirs": {
|
|
738
|
+
const items = await listDirs(root, stringArray(args.paths, "paths"));
|
|
739
|
+
structuredContent = boundedItems(items, outputBytesArg(args.maxBytes)).items;
|
|
480
740
|
break;
|
|
481
|
-
|
|
482
|
-
|
|
741
|
+
}
|
|
742
|
+
case "read_range": {
|
|
743
|
+
const result = await readRange(root, String(args.path), Number(args.startLine), Number(args.endLine));
|
|
744
|
+
const bounded = truncateText(result.content, outputBytesArg(args.maxBytes));
|
|
745
|
+
structuredContent = { ...result, content: bounded.text };
|
|
483
746
|
break;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
747
|
+
}
|
|
748
|
+
case "read_files": {
|
|
749
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
750
|
+
let remaining = maxBytes;
|
|
751
|
+
const output = [];
|
|
752
|
+
for (const p of stringArray(args.paths, "paths")) {
|
|
753
|
+
const content = await readFile(root, p);
|
|
754
|
+
const bounded = truncateText(content, remaining);
|
|
755
|
+
output.push({ path: p, content: bounded.text });
|
|
756
|
+
remaining -= Buffer.byteLength(bounded.text, "utf8");
|
|
757
|
+
if (bounded.truncated || remaining <= 0)
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
structuredContent = output;
|
|
489
761
|
break;
|
|
762
|
+
}
|
|
763
|
+
case "apply_changes": {
|
|
764
|
+
const changes = objectArray(args.changes, "changes").map((change) => {
|
|
765
|
+
const kind = String(change.kind);
|
|
766
|
+
if (kind !== "write" && kind !== "patch" && kind !== "delete")
|
|
767
|
+
throw new Error("Change kind must be write, patch, or delete.");
|
|
768
|
+
if (kind === "write" && typeof change.content !== "string")
|
|
769
|
+
throw new Error("write changes require content.");
|
|
770
|
+
if (kind === "patch" && !Array.isArray(change.patches))
|
|
771
|
+
throw new Error("patch changes require patches.");
|
|
772
|
+
return {
|
|
773
|
+
kind,
|
|
774
|
+
path: String(change.path),
|
|
775
|
+
...(kind === "write" ? { content: String(change.content) } : {}),
|
|
776
|
+
...(kind === "patch"
|
|
777
|
+
? {
|
|
778
|
+
patches: objectArray(change.patches, "patches").map((patch) => ({
|
|
779
|
+
search: String(patch.search),
|
|
780
|
+
replace: String(patch.replace),
|
|
781
|
+
...(patch.count === undefined ? {} : { count: Number(patch.count) }),
|
|
782
|
+
})),
|
|
783
|
+
}
|
|
784
|
+
: {}),
|
|
785
|
+
};
|
|
786
|
+
});
|
|
787
|
+
const dryRun = args.dryRun === true;
|
|
788
|
+
const affected = [...new Set(changes.map((change) => change.path))];
|
|
789
|
+
const before = new Map();
|
|
790
|
+
for (const p of affected)
|
|
791
|
+
before.set(p, await skillResourceUrisForPath(root, p));
|
|
792
|
+
structuredContent = await applyFileChanges(root, changes, dryRun);
|
|
793
|
+
if (!dryRun)
|
|
794
|
+
for (const p of affected)
|
|
795
|
+
await notifyResourceChange(notify, root, p, before.get(p) ?? []);
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
490
798
|
case "write_files": {
|
|
799
|
+
const dryRun = args.dryRun === true;
|
|
491
800
|
const output = [];
|
|
492
801
|
for (const file of objectArray(args.files, "files")) {
|
|
493
802
|
const p = String(file.path);
|
|
494
803
|
const before = await skillResourceUrisForPath(root, p);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
804
|
+
if (!dryRun) {
|
|
805
|
+
await writeFile(root, p, String(file.content));
|
|
806
|
+
await notifyResourceChange(notify, root, p, before);
|
|
807
|
+
}
|
|
808
|
+
output.push({ path: p, written: true, ...(dryRun ? { dryRun: true } : {}) });
|
|
498
809
|
}
|
|
499
810
|
structuredContent = output;
|
|
500
811
|
break;
|
|
501
812
|
}
|
|
502
813
|
case "patch_files": {
|
|
814
|
+
const dryRun = args.dryRun === true;
|
|
503
815
|
const files = objectArray(args.files, "files").map((file) => ({
|
|
504
816
|
path: String(file.path),
|
|
505
817
|
patches: objectArray(file.patches, "patches").map((p) => ({
|
|
@@ -511,70 +823,331 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, notify, s
|
|
|
511
823
|
const before = new Map();
|
|
512
824
|
for (const file of files)
|
|
513
825
|
before.set(file.path, await skillResourceUrisForPath(root, file.path));
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
826
|
+
if (dryRun) {
|
|
827
|
+
structuredContent = await patchFilesDryRun(root, files);
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
structuredContent = await patchFiles(root, files);
|
|
831
|
+
}
|
|
832
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
833
|
+
if (Array.isArray(structuredContent)) {
|
|
834
|
+
structuredContent = boundedItems(structuredContent.map((item) => {
|
|
835
|
+
if (!isRecord(item) || typeof item.content !== "string")
|
|
836
|
+
return item;
|
|
837
|
+
return { ...item, content: truncateText(item.content, maxBytes).text };
|
|
838
|
+
}), maxBytes).items;
|
|
839
|
+
}
|
|
840
|
+
if (!dryRun) {
|
|
841
|
+
for (const file of files)
|
|
842
|
+
await notifyResourceChange(notify, root, file.path, before.get(file.path) ?? []);
|
|
843
|
+
}
|
|
517
844
|
break;
|
|
518
845
|
}
|
|
519
846
|
case "delete_files": {
|
|
847
|
+
const dryRun = args.dryRun === true;
|
|
520
848
|
const output = [];
|
|
521
849
|
for (const p of stringArray(args.paths, "paths")) {
|
|
522
850
|
const before = await skillResourceUrisForPath(root, p);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
851
|
+
if (!dryRun) {
|
|
852
|
+
await deleteFile(root, p);
|
|
853
|
+
await notifyResourceChange(notify, root, p, before);
|
|
854
|
+
}
|
|
855
|
+
output.push({ path: p, deleted: true, ...(dryRun ? { dryRun: true } : {}) });
|
|
526
856
|
}
|
|
527
857
|
structuredContent = output;
|
|
528
858
|
break;
|
|
529
859
|
}
|
|
530
860
|
case "search_files":
|
|
531
|
-
structuredContent = await searchFiles(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000));
|
|
861
|
+
structuredContent = await searchFiles(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
532
862
|
break;
|
|
533
863
|
case "find_files":
|
|
534
|
-
structuredContent = await findFiles(root, String(args.pattern), numberArg(args.maxResults, 200, 1000));
|
|
864
|
+
structuredContent = boundedItems(await findFiles(root, String(args.pattern), numberArg(args.maxResults, 200, 1000)), outputBytesArg(args.maxBytes)).items;
|
|
535
865
|
break;
|
|
536
866
|
case "search_code":
|
|
537
|
-
structuredContent = await searchCode(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000));
|
|
867
|
+
structuredContent = await searchCode(root, String(args.query), Boolean(args.regex), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
538
868
|
break;
|
|
539
869
|
case "find_symbol":
|
|
540
|
-
structuredContent = await findSymbol(root, String(args.symbol), numberArg(args.maxResults, 100, 500));
|
|
870
|
+
structuredContent = await findSymbol(root, String(args.symbol), numberArg(args.maxResults, 100, 500), outputBytesArg(args.maxBytes));
|
|
541
871
|
break;
|
|
542
872
|
case "find_definition":
|
|
543
|
-
structuredContent = await findDefinition(root, String(args.symbol), numberArg(args.maxResults, 100, 500));
|
|
873
|
+
structuredContent = await findDefinition(root, String(args.symbol), numberArg(args.maxResults, 100, 500), outputBytesArg(args.maxBytes));
|
|
544
874
|
break;
|
|
545
875
|
case "find_references":
|
|
546
|
-
structuredContent = await findReferences(root, String(args.symbol), numberArg(args.maxResults, 200, 1000));
|
|
876
|
+
structuredContent = await findReferences(root, String(args.symbol), numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
547
877
|
break;
|
|
548
878
|
case "find_imports":
|
|
549
|
-
structuredContent = await findImports(root, numberArg(args.maxResults, 200, 1000));
|
|
879
|
+
structuredContent = await findImports(root, numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
550
880
|
break;
|
|
551
881
|
case "find_exports":
|
|
552
|
-
structuredContent = await findExports(root, numberArg(args.maxResults, 200, 1000));
|
|
882
|
+
structuredContent = await findExports(root, numberArg(args.maxResults, 200, 1000), outputBytesArg(args.maxBytes));
|
|
553
883
|
break;
|
|
554
884
|
case "git_status":
|
|
555
|
-
structuredContent = await gitStatus(root);
|
|
885
|
+
structuredContent = boundCommandResult(await gitStatus(root), outputBytesArg(args.maxBytes));
|
|
556
886
|
break;
|
|
557
887
|
case "git_stage":
|
|
558
|
-
structuredContent = await gitStage(root, stringArray(args.paths, "paths"));
|
|
888
|
+
structuredContent = boundCommandResult(await gitStage(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
559
889
|
break;
|
|
560
890
|
case "git_unstage":
|
|
561
|
-
structuredContent = await gitUnstage(root, stringArray(args.paths, "paths"));
|
|
891
|
+
structuredContent = boundCommandResult(await gitUnstage(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
562
892
|
break;
|
|
563
893
|
case "git_commit":
|
|
564
|
-
structuredContent = await gitCommit(root, String(args.message));
|
|
894
|
+
structuredContent = boundCommandResult(await gitCommit(root, String(args.message)), outputBytesArg(args.maxBytes));
|
|
565
895
|
break;
|
|
566
896
|
case "git_restore":
|
|
567
|
-
structuredContent = await gitRestore(root, stringArray(args.paths, "paths"));
|
|
897
|
+
structuredContent = boundCommandResult(await gitRestore(root, stringArray(args.paths, "paths")), outputBytesArg(args.maxBytes));
|
|
568
898
|
break;
|
|
569
899
|
case "git_push":
|
|
570
|
-
structuredContent = await gitPush(root, args.remote === undefined ? undefined : String(args.remote), args.branch === undefined ? undefined : String(args.branch));
|
|
900
|
+
structuredContent = boundCommandResult(await gitPush(root, args.remote === undefined ? undefined : String(args.remote), args.branch === undefined ? undefined : String(args.branch)), outputBytesArg(args.maxBytes));
|
|
571
901
|
break;
|
|
572
902
|
case "git_diff":
|
|
573
|
-
structuredContent = await gitDiff(root, Boolean(args.staged), args.path === undefined ? undefined : String(args.path));
|
|
903
|
+
structuredContent = boundCommandResult(await gitDiff(root, Boolean(args.staged), args.path === undefined ? undefined : String(args.path)), outputBytesArg(args.maxBytes));
|
|
574
904
|
break;
|
|
575
905
|
case "git_log":
|
|
576
|
-
structuredContent = await gitLog(root, numberArg(args.limit, 20, 200), args.path === undefined ? undefined : String(args.path));
|
|
906
|
+
structuredContent = boundCommandResult(await gitLog(root, numberArg(args.limit, 20, 200), args.path === undefined ? undefined : String(args.path)), outputBytesArg(args.maxBytes));
|
|
907
|
+
break;
|
|
908
|
+
case "locate": {
|
|
909
|
+
const query = String(args.query);
|
|
910
|
+
const kind = typeof args.kind === "string" ? args.kind : "auto";
|
|
911
|
+
const regex = args.regex === true;
|
|
912
|
+
const maxResults = numberArg(args.maxResults, 20, 100);
|
|
913
|
+
const resolvedKind = kind === "auto"
|
|
914
|
+
? /[*?]/.test(query)
|
|
915
|
+
? "file"
|
|
916
|
+
: /\.(ts|tsx|js|jsx|py|go|rs|java|json|md|yml|yaml|css|html)$/.test(query)
|
|
917
|
+
? "file"
|
|
918
|
+
: "code"
|
|
919
|
+
: kind;
|
|
920
|
+
const items = resolvedKind === "file"
|
|
921
|
+
? await findFiles(root, query, maxResults)
|
|
922
|
+
: resolvedKind === "text"
|
|
923
|
+
? await searchFiles(root, query, regex, maxResults)
|
|
924
|
+
: await searchCode(root, query, regex, maxResults);
|
|
925
|
+
const bounded = boundedItems(items, outputBytesArg(args.maxBytes));
|
|
926
|
+
structuredContent = {
|
|
927
|
+
query,
|
|
928
|
+
kind: resolvedKind,
|
|
929
|
+
items: bounded.items,
|
|
930
|
+
count: bounded.items.length,
|
|
931
|
+
truncated: bounded.truncated || items.length >= maxResults,
|
|
932
|
+
};
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
case "inspect_symbol": {
|
|
936
|
+
const symbol = String(args.symbol);
|
|
937
|
+
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
938
|
+
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
939
|
+
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
940
|
+
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
941
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
942
|
+
const definitions = await findDefinition(root, symbol, maxResults, maxBytes);
|
|
943
|
+
const definitionContext = await Promise.all(definitions.map(async (definition) => {
|
|
944
|
+
const range = await readRange(root, definition.path, Math.max(1, definition.line - contextLines), definition.line + contextLines);
|
|
945
|
+
return { ...definition, context: range.content };
|
|
946
|
+
}));
|
|
947
|
+
const references = args.includeReferences === true
|
|
948
|
+
? await findReferences(root, symbol, maxResults, maxBytes)
|
|
949
|
+
: undefined;
|
|
950
|
+
const result = {
|
|
951
|
+
symbol,
|
|
952
|
+
definitions: definitionContext,
|
|
953
|
+
...(references ? { references } : {}),
|
|
954
|
+
};
|
|
955
|
+
const bounded = truncateText(JSON.stringify(result), maxBytes);
|
|
956
|
+
structuredContent = bounded.truncated
|
|
957
|
+
? { symbol, result: bounded.text, truncated: true }
|
|
958
|
+
: result;
|
|
577
959
|
break;
|
|
960
|
+
}
|
|
961
|
+
case "inspect": {
|
|
962
|
+
const target = String(args.target);
|
|
963
|
+
const requestedKind = typeof args.kind === "string" ? args.kind : "auto";
|
|
964
|
+
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
965
|
+
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
966
|
+
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
967
|
+
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
968
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
969
|
+
let kind = requestedKind;
|
|
970
|
+
if (kind === "auto") {
|
|
971
|
+
try {
|
|
972
|
+
await readFile(root, target);
|
|
973
|
+
kind = "file";
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
kind = /[*?]/.test(target) ? "code" : "symbol";
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (kind === "file") {
|
|
980
|
+
const content = await readFile(root, target);
|
|
981
|
+
const boundedContent = truncateText(content, Math.max(1, maxBytes - 128));
|
|
982
|
+
const result = {
|
|
983
|
+
target,
|
|
984
|
+
kind,
|
|
985
|
+
path: target,
|
|
986
|
+
content: boundedContent.text,
|
|
987
|
+
truncated: boundedContent.truncated,
|
|
988
|
+
};
|
|
989
|
+
structuredContent = result;
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
if (kind === "symbol") {
|
|
993
|
+
const definitions = await findDefinition(root, target, maxResults, maxBytes);
|
|
994
|
+
const definitionsWithContext = await Promise.all(definitions.map(async (definition) => {
|
|
995
|
+
const range = await readRange(root, definition.path, Math.max(1, definition.line - contextLines), definition.line + contextLines);
|
|
996
|
+
return { ...definition, context: range.content };
|
|
997
|
+
}));
|
|
998
|
+
const references = args.includeReferences === true
|
|
999
|
+
? await findReferences(root, target, maxResults, maxBytes)
|
|
1000
|
+
: undefined;
|
|
1001
|
+
const result = {
|
|
1002
|
+
target,
|
|
1003
|
+
kind,
|
|
1004
|
+
definitions: definitionsWithContext,
|
|
1005
|
+
...(references ? { references } : {}),
|
|
1006
|
+
};
|
|
1007
|
+
const bounded = truncateText(JSON.stringify(result), maxBytes);
|
|
1008
|
+
structuredContent = bounded.truncated
|
|
1009
|
+
? { target, kind, result: bounded.text, truncated: true }
|
|
1010
|
+
: result;
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
const matches = await searchCode(root, target, false, maxResults, maxBytes);
|
|
1014
|
+
const relevant = await Promise.all(matches.map(async (match) => {
|
|
1015
|
+
const range = await readRange(root, match.path, Math.max(1, match.line - contextLines), match.line + contextLines);
|
|
1016
|
+
return {
|
|
1017
|
+
...match,
|
|
1018
|
+
context: range.content,
|
|
1019
|
+
startLine: range.startLine,
|
|
1020
|
+
endLine: range.endLine,
|
|
1021
|
+
};
|
|
1022
|
+
}));
|
|
1023
|
+
const bounded = boundedItems(relevant, maxBytes);
|
|
1024
|
+
structuredContent = {
|
|
1025
|
+
target,
|
|
1026
|
+
kind: "code",
|
|
1027
|
+
matches: bounded.items,
|
|
1028
|
+
count: bounded.items.length,
|
|
1029
|
+
truncated: bounded.truncated || relevant.length >= maxResults,
|
|
1030
|
+
};
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
case "project_context": {
|
|
1034
|
+
const overview = await projectOverview(root);
|
|
1035
|
+
let pkg = { path: "package.json", exists: false };
|
|
1036
|
+
try {
|
|
1037
|
+
const packageJson = JSON.parse(await readFile(root, "package.json"));
|
|
1038
|
+
pkg = {
|
|
1039
|
+
path: "package.json",
|
|
1040
|
+
name: packageJson.name ?? null,
|
|
1041
|
+
version: packageJson.version ?? null,
|
|
1042
|
+
description: packageJson.description ?? null,
|
|
1043
|
+
packageManager: packageJson.packageManager ?? null,
|
|
1044
|
+
scripts: packageJson.scripts ?? {},
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
// Package metadata is optional; project context remains useful without it.
|
|
1049
|
+
}
|
|
1050
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1051
|
+
const bounded = truncateText(JSON.stringify({
|
|
1052
|
+
...overview,
|
|
1053
|
+
package: pkg,
|
|
1054
|
+
execution: { npmScripts: [...npmAllowed], commands: [...allowedCommands] },
|
|
1055
|
+
}), maxBytes);
|
|
1056
|
+
structuredContent = bounded.truncated
|
|
1057
|
+
? { result: bounded.text, truncated: true }
|
|
1058
|
+
: JSON.parse(bounded.text);
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
case "read_relevant": {
|
|
1062
|
+
const query = String(args.query);
|
|
1063
|
+
const contextLines = args.contextLines === undefined ? 3 : Number(args.contextLines);
|
|
1064
|
+
if (!Number.isInteger(contextLines) || contextLines < 0 || contextLines > 20)
|
|
1065
|
+
throw new Error("contextLines must be an integer between 0 and 20.");
|
|
1066
|
+
const maxResults = numberArg(args.maxResults, 5, 20);
|
|
1067
|
+
const matches = await searchCode(root, query, Boolean(args.regex), maxResults, outputBytesArg(args.maxBytes));
|
|
1068
|
+
const relevant = await Promise.all(matches.map(async (match) => {
|
|
1069
|
+
const range = await readRange(root, match.path, Math.max(1, match.line - contextLines), match.line + contextLines);
|
|
1070
|
+
return {
|
|
1071
|
+
...match,
|
|
1072
|
+
context: range.content,
|
|
1073
|
+
startLine: range.startLine,
|
|
1074
|
+
endLine: range.endLine,
|
|
1075
|
+
};
|
|
1076
|
+
}));
|
|
1077
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1078
|
+
const bounded = boundedItems(relevant, Math.max(1, maxBytes - 128));
|
|
1079
|
+
const result = {
|
|
1080
|
+
query,
|
|
1081
|
+
matches: bounded.items,
|
|
1082
|
+
count: relevant.length,
|
|
1083
|
+
truncated: bounded.truncated || relevant.length >= maxResults,
|
|
1084
|
+
};
|
|
1085
|
+
structuredContent = result;
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
case "validate": {
|
|
1089
|
+
const level = typeof args.level === "string" ? args.level : "diagnostics";
|
|
1090
|
+
const maxResults = numberArg(args.maxResults, 200, 1000);
|
|
1091
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1092
|
+
const diagnosticItems = await diagnostics(root, maxResults);
|
|
1093
|
+
const scripts = level === "diagnostics"
|
|
1094
|
+
? []
|
|
1095
|
+
: args.scripts !== undefined
|
|
1096
|
+
? stringArray(args.scripts, "scripts")
|
|
1097
|
+
: level === "full"
|
|
1098
|
+
? ["check", "test"].filter((script) => npmAllowed.includes(script))
|
|
1099
|
+
: [];
|
|
1100
|
+
const checks = [
|
|
1101
|
+
{
|
|
1102
|
+
kind: "diagnostics",
|
|
1103
|
+
ok: !diagnosticItems.some((item) => item.severity === "error"),
|
|
1104
|
+
count: diagnosticItems.length,
|
|
1105
|
+
issues: boundedItems(diagnosticItems, maxBytes).items,
|
|
1106
|
+
},
|
|
1107
|
+
];
|
|
1108
|
+
for (const script of scripts) {
|
|
1109
|
+
const result = await runNpm(root, script, 120000, npmAllowed, signal);
|
|
1110
|
+
checks.push({
|
|
1111
|
+
kind: "npm",
|
|
1112
|
+
script,
|
|
1113
|
+
ok: result.exitCode === 0,
|
|
1114
|
+
result,
|
|
1115
|
+
});
|
|
1116
|
+
if (result.exitCode !== 0)
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
structuredContent = {
|
|
1120
|
+
level,
|
|
1121
|
+
ok: checks.every((check) => check.ok === true),
|
|
1122
|
+
checks,
|
|
1123
|
+
scriptsRequested: scripts,
|
|
1124
|
+
};
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
case "git_changes": {
|
|
1128
|
+
const status = await gitStatus(root);
|
|
1129
|
+
const includeDiff = Boolean(args.includeDiff);
|
|
1130
|
+
const path = args.path === undefined ? undefined : String(args.path);
|
|
1131
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1132
|
+
const diff = includeDiff ? await gitDiff(root, false, path) : undefined;
|
|
1133
|
+
const statusBytes = Buffer.byteLength(JSON.stringify(status), "utf8");
|
|
1134
|
+
const diffBudget = Math.max(0, maxBytes - statusBytes - 64);
|
|
1135
|
+
const boundedDiff = diff ? truncateText(diff.stdout, diffBudget) : undefined;
|
|
1136
|
+
const result = {
|
|
1137
|
+
status,
|
|
1138
|
+
...(diff
|
|
1139
|
+
? {
|
|
1140
|
+
diff: {
|
|
1141
|
+
...diff,
|
|
1142
|
+
stdout: boundedDiff?.text ?? "",
|
|
1143
|
+
truncated: boundedDiff?.truncated ?? false,
|
|
1144
|
+
},
|
|
1145
|
+
}
|
|
1146
|
+
: {}),
|
|
1147
|
+
};
|
|
1148
|
+
structuredContent = result;
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
578
1151
|
case "project_overview":
|
|
579
1152
|
structuredContent = await projectOverview(root);
|
|
580
1153
|
break;
|
|
@@ -591,7 +1164,7 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, notify, s
|
|
|
591
1164
|
structuredContent = await fileInfo(root, String(args.path));
|
|
592
1165
|
break;
|
|
593
1166
|
case "diagnostics":
|
|
594
|
-
structuredContent = await diagnostics(root, numberArg(args.maxResults, 200, 1000));
|
|
1167
|
+
structuredContent = boundedItems(await diagnostics(root, numberArg(args.maxResults, 200, 1000)), outputBytesArg(args.maxBytes)).items;
|
|
595
1168
|
break;
|
|
596
1169
|
case "run_npm_batch": {
|
|
597
1170
|
const output = [];
|
|
@@ -601,7 +1174,24 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, notify, s
|
|
|
601
1174
|
if (value.exitCode !== 0)
|
|
602
1175
|
break;
|
|
603
1176
|
}
|
|
604
|
-
|
|
1177
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1178
|
+
const boundedOutput = [];
|
|
1179
|
+
let remaining = maxBytes - 2;
|
|
1180
|
+
for (const item of output) {
|
|
1181
|
+
const separatorBytes = boundedOutput.length ? 1 : 0;
|
|
1182
|
+
if (remaining <= separatorBytes)
|
|
1183
|
+
break;
|
|
1184
|
+
const budget = remaining - separatorBytes;
|
|
1185
|
+
const bounded = boundCommandResult(item, budget);
|
|
1186
|
+
const boundedBytes = Buffer.byteLength(JSON.stringify(bounded), "utf8");
|
|
1187
|
+
if (boundedBytes > budget)
|
|
1188
|
+
break;
|
|
1189
|
+
boundedOutput.push(bounded);
|
|
1190
|
+
remaining -= separatorBytes + boundedBytes;
|
|
1191
|
+
if (remaining <= 0)
|
|
1192
|
+
break;
|
|
1193
|
+
}
|
|
1194
|
+
structuredContent = boundedOutput;
|
|
605
1195
|
break;
|
|
606
1196
|
}
|
|
607
1197
|
case "run_command_batch": {
|
|
@@ -612,7 +1202,24 @@ async function callTool(name, args, root, npmAllowed, allowedCommands, notify, s
|
|
|
612
1202
|
if (value.exitCode !== 0)
|
|
613
1203
|
break;
|
|
614
1204
|
}
|
|
615
|
-
|
|
1205
|
+
const maxBytes = outputBytesArg(args.maxBytes);
|
|
1206
|
+
const boundedOutput = [];
|
|
1207
|
+
let remaining = maxBytes - 2;
|
|
1208
|
+
for (const item of output) {
|
|
1209
|
+
const separatorBytes = boundedOutput.length ? 1 : 0;
|
|
1210
|
+
if (remaining <= separatorBytes)
|
|
1211
|
+
break;
|
|
1212
|
+
const budget = remaining - separatorBytes;
|
|
1213
|
+
const bounded = boundCommandResult(item, budget);
|
|
1214
|
+
const boundedBytes = Buffer.byteLength(JSON.stringify(bounded), "utf8");
|
|
1215
|
+
if (boundedBytes > budget)
|
|
1216
|
+
break;
|
|
1217
|
+
boundedOutput.push(bounded);
|
|
1218
|
+
remaining -= separatorBytes + boundedBytes;
|
|
1219
|
+
if (remaining <= 0)
|
|
1220
|
+
break;
|
|
1221
|
+
}
|
|
1222
|
+
structuredContent = boundedOutput;
|
|
616
1223
|
break;
|
|
617
1224
|
}
|
|
618
1225
|
default:
|
|
@@ -745,6 +1352,69 @@ function objectArray(value, name) {
|
|
|
745
1352
|
throw new Error(`${name} must be an array of objects.`);
|
|
746
1353
|
return value;
|
|
747
1354
|
}
|
|
1355
|
+
function outputBytesArg(value) {
|
|
1356
|
+
const result = value === undefined ? DEFAULT_OUTPUT_BYTES : Number(value);
|
|
1357
|
+
if (!Number.isInteger(result) || result < 1024 || result > 1_000_000)
|
|
1358
|
+
throw new Error("maxBytes must be an integer between 1024 and 1000000.");
|
|
1359
|
+
return result;
|
|
1360
|
+
}
|
|
1361
|
+
function truncateText(value, maxBytes) {
|
|
1362
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
1363
|
+
return { text: value, truncated: false };
|
|
1364
|
+
if (maxBytes <= 32)
|
|
1365
|
+
return { text: "", truncated: true };
|
|
1366
|
+
const buffer = Buffer.from(value, "utf8");
|
|
1367
|
+
return {
|
|
1368
|
+
text: `${buffer.subarray(0, Math.max(0, maxBytes - 32)).toString("utf8")}\n…[truncated]`,
|
|
1369
|
+
truncated: true,
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function boundCommandResult(value, maxBytes) {
|
|
1373
|
+
if (!isRecord(value))
|
|
1374
|
+
return value;
|
|
1375
|
+
const command = typeof value.command === "string" ? value.command : "";
|
|
1376
|
+
const stdout = typeof value.stdout === "string" ? value.stdout : "";
|
|
1377
|
+
const stderr = typeof value.stderr === "string" ? value.stderr : "";
|
|
1378
|
+
let commandBudget = Buffer.byteLength(command, "utf8");
|
|
1379
|
+
let stdoutBudget = Buffer.byteLength(stdout, "utf8");
|
|
1380
|
+
let stderrBudget = Buffer.byteLength(stderr, "utf8");
|
|
1381
|
+
const candidate = () => ({
|
|
1382
|
+
...value,
|
|
1383
|
+
command: truncateText(command, commandBudget).text,
|
|
1384
|
+
stdout: truncateText(stdout, stdoutBudget).text,
|
|
1385
|
+
stderr: truncateText(stderr, stderrBudget).text,
|
|
1386
|
+
});
|
|
1387
|
+
let result = candidate();
|
|
1388
|
+
while (Buffer.byteLength(JSON.stringify(result), "utf8") > maxBytes &&
|
|
1389
|
+
(commandBudget > 0 || stdoutBudget > 0 || stderrBudget > 0)) {
|
|
1390
|
+
const excess = Buffer.byteLength(JSON.stringify(result), "utf8") - maxBytes;
|
|
1391
|
+
if (stdoutBudget >= stderrBudget && stdoutBudget > 0)
|
|
1392
|
+
stdoutBudget = Math.max(0, stdoutBudget - excess);
|
|
1393
|
+
else if (stderrBudget > 0)
|
|
1394
|
+
stderrBudget = Math.max(0, stderrBudget - excess);
|
|
1395
|
+
else
|
|
1396
|
+
commandBudget = Math.max(0, commandBudget - excess);
|
|
1397
|
+
result = candidate();
|
|
1398
|
+
}
|
|
1399
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") <= maxBytes)
|
|
1400
|
+
return result;
|
|
1401
|
+
return { ...value, command: truncateText(command, commandBudget).text, stdout: "", stderr: "" };
|
|
1402
|
+
}
|
|
1403
|
+
function boundedItems(items, maxBytes) {
|
|
1404
|
+
const selected = [];
|
|
1405
|
+
let bytes = 2;
|
|
1406
|
+
for (const item of items) {
|
|
1407
|
+
const itemBytes = Buffer.byteLength(JSON.stringify(item), "utf8");
|
|
1408
|
+
const separatorBytes = selected.length ? 1 : 0;
|
|
1409
|
+
if (selected.length && bytes + separatorBytes + itemBytes > maxBytes)
|
|
1410
|
+
break;
|
|
1411
|
+
if (!selected.length && bytes + itemBytes > maxBytes)
|
|
1412
|
+
break;
|
|
1413
|
+
selected.push(item);
|
|
1414
|
+
bytes += separatorBytes + itemBytes;
|
|
1415
|
+
}
|
|
1416
|
+
return { items: selected, truncated: selected.length < items.length };
|
|
1417
|
+
}
|
|
748
1418
|
function numberArg(value, fallback, max) {
|
|
749
1419
|
const result = value === undefined ? fallback : Number(value);
|
|
750
1420
|
if (!Number.isInteger(result) || result < 1 || result > max)
|
|
@@ -777,11 +1447,40 @@ function decodeCursor(value) {
|
|
|
777
1447
|
catch {
|
|
778
1448
|
throw new Error("Invalid pagination cursor.");
|
|
779
1449
|
}
|
|
1450
|
+
if (!/^(?:0|[1-9]\d*)$/.test(decoded))
|
|
1451
|
+
throw new Error("Invalid pagination cursor.");
|
|
780
1452
|
const offset = Number(decoded);
|
|
781
1453
|
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
782
1454
|
throw new Error("Invalid pagination cursor.");
|
|
783
1455
|
return offset;
|
|
784
1456
|
}
|
|
1457
|
+
async function patchFilesDryRun(root, files) {
|
|
1458
|
+
return Promise.all(files.map(async ({ path: relativePath, patches }) => {
|
|
1459
|
+
let content = await readFile(root, relativePath);
|
|
1460
|
+
for (const patch of patches) {
|
|
1461
|
+
if (!patch.search)
|
|
1462
|
+
throw new Error("Patch search text cannot be empty.");
|
|
1463
|
+
const occurrences = content.split(patch.search).length - 1;
|
|
1464
|
+
const count = patch.count ?? 1;
|
|
1465
|
+
if (occurrences === 0)
|
|
1466
|
+
throw new Error(`Patch text was not found in '${relativePath}'.`);
|
|
1467
|
+
if (!Number.isInteger(count) || count < 1)
|
|
1468
|
+
throw new Error("Patch count must be a positive integer.");
|
|
1469
|
+
if (occurrences < count)
|
|
1470
|
+
throw new Error(`Patch text occurs ${occurrences} time(s), but ${count} replacement(s) were requested.`);
|
|
1471
|
+
let offset = 0;
|
|
1472
|
+
for (let index = 0; index < count; index += 1) {
|
|
1473
|
+
const position = content.indexOf(patch.search, offset);
|
|
1474
|
+
content =
|
|
1475
|
+
content.slice(0, position) +
|
|
1476
|
+
patch.replace +
|
|
1477
|
+
content.slice(position + patch.search.length);
|
|
1478
|
+
offset = position + patch.replace.length;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return { path: relativePath, applied: patches.length, content, dryRun: true };
|
|
1482
|
+
}));
|
|
1483
|
+
}
|
|
785
1484
|
async function notifyResourceChange(notify, root, pathValue, before) {
|
|
786
1485
|
const after = await skillResourceUrisForPath(root, pathValue);
|
|
787
1486
|
if (before.length === 0 && after.length === 0)
|