@cortexkit/aft-opencode 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.d.ts +8 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +45 -2
- package/dist/bridge.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/downloader.d.ts.map +1 -1
- package/dist/downloader.js +51 -15
- package/dist/downloader.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -16
- package/dist/index.js.map +1 -1
- package/dist/normalize-schemas.d.ts +16 -0
- package/dist/normalize-schemas.d.ts.map +1 -0
- package/dist/normalize-schemas.js +45 -0
- package/dist/normalize-schemas.js.map +1 -0
- package/dist/patch-parser.d.ts.map +1 -1
- package/dist/patch-parser.js +10 -0
- package/dist/patch-parser.js.map +1 -1
- package/dist/platform.d.ts +21 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +31 -0
- package/dist/platform.js.map +1 -0
- package/dist/pool.d.ts.map +1 -1
- package/dist/pool.js +14 -5
- package/dist/pool.js.map +1 -1
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +6 -9
- package/dist/resolver.js.map +1 -1
- package/dist/tools/ast.d.ts.map +1 -1
- package/dist/tools/ast.js +52 -59
- package/dist/tools/ast.js.map +1 -1
- package/dist/tools/hoisted.d.ts +1 -1
- package/dist/tools/hoisted.d.ts.map +1 -1
- package/dist/tools/hoisted.js +333 -230
- package/dist/tools/hoisted.js.map +1 -1
- package/dist/tools/imports.d.ts.map +1 -1
- package/dist/tools/imports.js +40 -30
- package/dist/tools/imports.js.map +1 -1
- package/dist/tools/lsp.d.ts +1 -2
- package/dist/tools/lsp.d.ts.map +1 -1
- package/dist/tools/lsp.js +31 -13
- package/dist/tools/lsp.js.map +1 -1
- package/dist/tools/navigation.d.ts.map +1 -1
- package/dist/tools/navigation.js +23 -14
- package/dist/tools/navigation.js.map +1 -1
- package/dist/tools/permissions.d.ts +8 -0
- package/dist/tools/permissions.d.ts.map +1 -0
- package/dist/tools/permissions.js +50 -0
- package/dist/tools/permissions.js.map +1 -0
- package/dist/tools/reading.d.ts +1 -2
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/reading.js +191 -12
- package/dist/tools/reading.js.map +1 -1
- package/dist/tools/refactoring.d.ts.map +1 -1
- package/dist/tools/refactoring.js +72 -34
- package/dist/tools/refactoring.js.map +1 -1
- package/dist/tools/safety.d.ts.map +1 -1
- package/dist/tools/safety.js +34 -12
- package/dist/tools/safety.js.map +1 -1
- package/dist/tools/structure.d.ts.map +1 -1
- package/dist/tools/structure.js +66 -31
- package/dist/tools/structure.js.map +1 -1
- package/package.json +7 -7
- package/dist/tools/editing.d.ts +0 -8
- package/dist/tools/editing.d.ts.map +0 -1
- package/dist/tools/editing.js +0 -8
- package/dist/tools/editing.js.map +0 -1
package/dist/tools/hoisted.js
CHANGED
|
@@ -59,63 +59,49 @@ function buildUnifiedDiff(fp, before, after) {
|
|
|
59
59
|
}
|
|
60
60
|
const z = tool.schema;
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
|
-
//
|
|
63
|
-
//
|
|
62
|
+
// Tool descriptions focus on behavior, modes, and return values.
|
|
63
|
+
// Parameter docs live in Zod .describe() and reach the LLM via JSON Schema.
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
|
-
const READ_DESCRIPTION = `Read
|
|
65
|
+
const READ_DESCRIPTION = `Read file contents or list directory entries.
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
1. **Read file** (default) — pass \`filePath\` only
|
|
70
|
-
Returns line-numbered content. Use \`start_line\`/\`end_line\` to read specific sections.
|
|
71
|
-
Example: \`{ "filePath": "src/app.ts" }\` or \`{ "filePath": "src/app.ts", "start_line": 50, "end_line": 100 }\`
|
|
72
|
-
|
|
73
|
-
2. **Inspect symbol** — pass \`filePath\` + \`symbol\`
|
|
74
|
-
Returns the full source of a named symbol (function, class, type) with call-graph
|
|
75
|
-
annotations showing what it calls and what calls it. Includes surrounding context lines.
|
|
76
|
-
Example: \`{ "filePath": "src/app.ts", "symbol": "handleRequest" }\`
|
|
77
|
-
|
|
78
|
-
3. **Inspect multiple symbols** — pass \`filePath\` + \`symbols\` array
|
|
79
|
-
Returns multiple symbols in one call. More efficient than separate calls.
|
|
80
|
-
Example: \`{ "filePath": "src/app.ts", "symbols": ["Config", "createApp"] }\`
|
|
67
|
+
Use either startLine/endLine OR offset/limit to read a section of a file.
|
|
81
68
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Example: \`{ "filePath": "src/" }\`
|
|
85
|
-
|
|
86
|
-
**Parameters:**
|
|
87
|
-
- \`filePath\` (string, required): Path to file or directory (absolute or relative to project root)
|
|
88
|
-
- \`symbol\` (string): Name of a single symbol to inspect — returns full source + call graph
|
|
89
|
-
- \`symbols\` (string[]): Array of symbol names to inspect in one call
|
|
90
|
-
- \`start_line\` (number): 1-based line to start reading from (default: 1)
|
|
91
|
-
- \`end_line\` (number): 1-based line to stop reading at, inclusive
|
|
92
|
-
- \`limit\` (number): Max lines to return (default: 2000). Ignored when end_line is set.
|
|
93
|
-
- \`context_lines\` (number): Lines of context around symbols (default: 3)
|
|
94
|
-
|
|
95
|
-
**Behavior:**
|
|
69
|
+
Behavior:
|
|
70
|
+
- Returns line-numbered content (e.g., "1: const x = 1")
|
|
96
71
|
- Lines longer than 2000 characters are truncated
|
|
97
|
-
- Output capped at 50KB
|
|
72
|
+
- Output capped at 50KB
|
|
98
73
|
- Binary files are auto-detected and return a size-only message
|
|
99
|
-
-
|
|
100
|
-
-
|
|
74
|
+
- Image files (.png, .jpg, .gif, .webp, etc.) and PDFs return a metadata string (format, size, path) — no file content is returned
|
|
75
|
+
- Directories return sorted entries with trailing / for subdirectories
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
Read full file: { "filePath": "src/app.ts" }
|
|
79
|
+
Read lines 50-100: { "filePath": "src/app.ts", "startLine": 50, "endLine": 100 }
|
|
80
|
+
Read 30 lines from line 200: { "filePath": "src/app.ts", "offset": 200, "limit": 30 }
|
|
81
|
+
List directory: { "filePath": "src/" }
|
|
82
|
+
|
|
83
|
+
Returns: Line-numbered file content string. For directories: newline-joined sorted entries. For binary files: size/message string.`;
|
|
101
84
|
/**
|
|
102
|
-
* Creates the
|
|
85
|
+
* Creates the simple read tool. Registers as "read" when hoisted, "aft_read" when not.
|
|
103
86
|
*/
|
|
104
87
|
export function createReadTool(ctx) {
|
|
105
88
|
return {
|
|
106
89
|
description: READ_DESCRIPTION,
|
|
107
90
|
args: {
|
|
108
|
-
filePath: z
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
limit: z.number().optional(),
|
|
114
|
-
|
|
91
|
+
filePath: z
|
|
92
|
+
.string()
|
|
93
|
+
.describe("Path to file or directory (absolute or relative to project root)"),
|
|
94
|
+
startLine: z.number().optional().describe("1-based line to start reading from"),
|
|
95
|
+
endLine: z.number().optional().describe("1-based line to stop reading at (inclusive)"),
|
|
96
|
+
limit: z.number().optional().describe("Max lines to return (default: 2000)"),
|
|
97
|
+
offset: z
|
|
98
|
+
.number()
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("1-based line number to start reading from (use with limit). Ignored if startLine is provided"),
|
|
115
101
|
},
|
|
116
102
|
execute: async (args, context) => {
|
|
117
103
|
const bridge = ctx.pool.getBridge(context.directory);
|
|
118
|
-
const file =
|
|
104
|
+
const file = args.filePath;
|
|
119
105
|
// Resolve relative paths
|
|
120
106
|
const filePath = path.isAbsolute(file) ? file : path.resolve(context.directory, file);
|
|
121
107
|
// Permission check
|
|
@@ -174,59 +160,23 @@ export function createReadTool(ctx) {
|
|
|
174
160
|
}
|
|
175
161
|
return `${msg} (${ext.slice(1).toUpperCase()}, ${sizeStr}). File: ${filePath}`;
|
|
176
162
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (hasSymbol)
|
|
185
|
-
params.symbol = args.symbol;
|
|
186
|
-
if (hasSymbols)
|
|
187
|
-
params.symbols = args.symbols;
|
|
188
|
-
if (args.start_line !== undefined)
|
|
189
|
-
params.start_line = args.start_line;
|
|
190
|
-
if (args.end_line !== undefined)
|
|
191
|
-
params.end_line = args.end_line;
|
|
192
|
-
if (args.context_lines !== undefined)
|
|
193
|
-
params.context_lines = args.context_lines;
|
|
194
|
-
const data = await bridge.send("zoom", params);
|
|
195
|
-
const callID = getCallID(context);
|
|
196
|
-
if (callID)
|
|
197
|
-
storeToolMetadata(context.sessionID, callID, {
|
|
198
|
-
title: relativeToWorktree(filePath, context.worktree),
|
|
199
|
-
metadata: { title: relativeToWorktree(filePath, context.worktree) },
|
|
200
|
-
});
|
|
201
|
-
return JSON.stringify(data);
|
|
202
|
-
}
|
|
203
|
-
// Line-range mode with start_line + end_line → also zoom (has context_before/after)
|
|
204
|
-
if (args.start_line !== undefined && args.end_line !== undefined) {
|
|
205
|
-
const params = {
|
|
206
|
-
file: filePath,
|
|
207
|
-
start_line: args.start_line,
|
|
208
|
-
end_line: args.end_line,
|
|
209
|
-
};
|
|
210
|
-
if (args.context_lines !== undefined)
|
|
211
|
-
params.context_lines = args.context_lines;
|
|
212
|
-
const data = await bridge.send("zoom", params);
|
|
213
|
-
const lineCallID = getCallID(context);
|
|
214
|
-
if (lineCallID) {
|
|
215
|
-
const dp = relativeToWorktree(filePath, context.worktree);
|
|
216
|
-
storeToolMetadata(context.sessionID, lineCallID, {
|
|
217
|
-
title: `${dp}:${args.start_line}-${args.end_line}`,
|
|
218
|
-
metadata: { title: `${dp}:${args.start_line}-${args.end_line}` },
|
|
219
|
-
});
|
|
163
|
+
// Normalize offset/limit to startLine/endLine (backward compat with opencode's read)
|
|
164
|
+
let startLine = args.startLine;
|
|
165
|
+
let endLine = args.endLine;
|
|
166
|
+
if (startLine === undefined && args.offset !== undefined) {
|
|
167
|
+
startLine = args.offset;
|
|
168
|
+
if (args.limit !== undefined) {
|
|
169
|
+
endLine = Number(args.offset) + Number(args.limit) - 1;
|
|
220
170
|
}
|
|
221
|
-
return JSON.stringify(data);
|
|
222
171
|
}
|
|
223
|
-
//
|
|
172
|
+
// Always use Rust read command — simple file reading only
|
|
224
173
|
const params = { file: filePath };
|
|
225
|
-
if (
|
|
226
|
-
params.start_line =
|
|
227
|
-
if (
|
|
228
|
-
params.end_line =
|
|
229
|
-
|
|
174
|
+
if (startLine !== undefined)
|
|
175
|
+
params.start_line = startLine;
|
|
176
|
+
if (endLine !== undefined)
|
|
177
|
+
params.end_line = endLine;
|
|
178
|
+
// Only send limit if we did NOT convert offset to startLine/endLine
|
|
179
|
+
if (args.limit !== undefined && args.offset === undefined)
|
|
230
180
|
params.limit = args.limit;
|
|
231
181
|
const data = await bridge.send("read", params);
|
|
232
182
|
const readCallID = getCallID(context);
|
|
@@ -254,7 +204,7 @@ export function createReadTool(ctx) {
|
|
|
254
204
|
let output = data.content;
|
|
255
205
|
// Add navigation hint if truncated
|
|
256
206
|
if (data.truncated) {
|
|
257
|
-
output += `\n(Showing lines ${data.start_line}-${data.end_line} of ${data.total_lines}. Use
|
|
207
|
+
output += `\n(Showing lines ${data.start_line}-${data.end_line} of ${data.total_lines}. Use startLine/endLine to read other sections.)`;
|
|
258
208
|
}
|
|
259
209
|
return output;
|
|
260
210
|
},
|
|
@@ -263,33 +213,35 @@ export function createReadTool(ctx) {
|
|
|
263
213
|
// ---------------------------------------------------------------------------
|
|
264
214
|
// WRITE tool
|
|
265
215
|
// ---------------------------------------------------------------------------
|
|
266
|
-
|
|
216
|
+
function getWriteDescription(editToolName) {
|
|
217
|
+
return `Write content to a file, creating it (and parent directories) if needed.
|
|
267
218
|
|
|
268
219
|
Automatically creates parent directories. Backs up existing files before overwriting.
|
|
269
220
|
If the project has a formatter configured (biome, prettier, rustfmt, etc.), the file
|
|
270
221
|
is auto-formatted after writing. Returns inline LSP diagnostics when available.
|
|
271
222
|
|
|
272
|
-
**Parameters:**
|
|
273
|
-
- \`filePath\` (string, required): Path to the file to write (absolute or relative to project root)
|
|
274
|
-
- \`content\` (string, required): The full content to write to the file
|
|
275
|
-
|
|
276
223
|
**Behavior:**
|
|
277
224
|
- Creates parent directories automatically (no need to mkdir first)
|
|
278
225
|
- Existing files are backed up before overwriting (recoverable via aft_safety undo)
|
|
279
226
|
- Auto-formats using project formatter if configured (biome.json, .prettierrc, etc.)
|
|
280
|
-
- Returns LSP diagnostics inline if type errors are introduced
|
|
227
|
+
- Returns LSP error-level diagnostics inline if type errors are introduced
|
|
281
228
|
- Use this for creating new files or completely replacing file contents
|
|
282
|
-
- For partial edits (find/replace), use the \`
|
|
283
|
-
|
|
229
|
+
- For partial edits (find/replace), use the \`${editToolName}\` tool instead
|
|
230
|
+
|
|
231
|
+
Returns: Status message string (for example: "Created new file. Auto-formatted.") with optional inline LSP error lines.`;
|
|
232
|
+
}
|
|
233
|
+
function createWriteTool(ctx, editToolName = "edit") {
|
|
284
234
|
return {
|
|
285
|
-
description:
|
|
235
|
+
description: getWriteDescription(editToolName),
|
|
286
236
|
args: {
|
|
287
|
-
filePath: z
|
|
288
|
-
|
|
237
|
+
filePath: z
|
|
238
|
+
.string()
|
|
239
|
+
.describe("Path to the file to write (absolute or relative to project root)"),
|
|
240
|
+
content: z.string().describe("The full content to write to the file"),
|
|
289
241
|
},
|
|
290
242
|
execute: async (args, context) => {
|
|
291
243
|
const bridge = ctx.pool.getBridge(context.directory);
|
|
292
|
-
const file =
|
|
244
|
+
const file = args.filePath;
|
|
293
245
|
const content = args.content;
|
|
294
246
|
const filePath = path.isAbsolute(file) ? file : path.resolve(context.directory, file);
|
|
295
247
|
const relPath = path.relative(context.worktree, filePath);
|
|
@@ -350,77 +302,93 @@ function createWriteTool(ctx) {
|
|
|
350
302
|
// ---------------------------------------------------------------------------
|
|
351
303
|
// EDIT tool
|
|
352
304
|
// ---------------------------------------------------------------------------
|
|
353
|
-
|
|
305
|
+
function getEditDescription(writeToolName) {
|
|
306
|
+
return `Edit a file by finding and replacing text, or by targeting named symbols.
|
|
354
307
|
|
|
355
308
|
**Modes** (determined by which parameters you provide):
|
|
356
309
|
|
|
357
|
-
|
|
358
|
-
Finds the exact text in \`match\` and replaces it with \`replacement\`.
|
|
359
|
-
Returns an error if multiple matches are found (use \`occurrence\` to select one,
|
|
360
|
-
or \`replace_all: true\` to replace all).
|
|
361
|
-
Example: \`{ "filePath": "src/app.ts", "match": "const x = 1", "replacement": "const x = 2" }\`
|
|
310
|
+
Mode priority: operations > edits > symbol (without oldString) > oldString (find/replace) > content-only (${writeToolName})
|
|
362
311
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
312
|
+
1. **Multi-file transaction** — pass \`operations\` array
|
|
313
|
+
Edits across multiple files with checkpoint-based rollback on failure.
|
|
314
|
+
Each operation: \`{ "file": "path", "command": "edit_match" | "write", ... }\`.
|
|
315
|
+
For \`edit_match\`: include \`match\`, \`replacement\`. For \`write\`: include \`content\`.
|
|
316
|
+
Example: \`{ "operations": [{ "file": "a.ts", "command": "edit_match", "match": "old", "replacement": "new" }, { "file": "b.ts", "command": "write", "content": "..." }] }\`
|
|
366
317
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
318
|
+
2. **Batch edits** — pass \`filePath\` + \`edits\` array
|
|
319
|
+
Multiple edits in one file atomically. Each edit is either:
|
|
320
|
+
- \`{ "oldString": "old", "newString": "new" }\` — find/replace
|
|
321
|
+
- \`{ "startLine": 5, "endLine": 7, "content": "new lines" }\` — replace line range (1-based, both inclusive)
|
|
322
|
+
Set content to empty string to delete lines.
|
|
370
323
|
|
|
371
|
-
|
|
324
|
+
3. **Symbol replace** — pass \`filePath\` + \`symbol\` + \`content\`
|
|
372
325
|
Replaces an entire named symbol (function, class, type) with new content.
|
|
373
326
|
Includes decorators, attributes, and doc comments in the replacement range.
|
|
327
|
+
**Important:** You must NOT provide \`oldString\` when using symbol mode — if present, the tool silently falls back to find/replace mode.
|
|
374
328
|
Example: \`{ "filePath": "src/app.ts", "symbol": "handleRequest", "content": "function handleRequest() { ... }" }\`
|
|
375
329
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
Example: \`{ "filePath": "src/app.ts", "edits": [{ "match": "foo", "replacement": "bar" }, { "line_start": 10, "line_end": 12, "content": "" }] }\`
|
|
330
|
+
4. **Find and replace** — pass \`filePath\` + \`oldString\` + \`newString\`
|
|
331
|
+
Finds the exact text in \`oldString\` and replaces it with \`newString\`.
|
|
332
|
+
Supports fuzzy matching (handles whitespace differences automatically).
|
|
333
|
+
If multiple matches exist, specify which one with \`occurrence\` or use \`replaceAll: true\`.
|
|
334
|
+
Example: \`{ "filePath": "src/app.ts", "oldString": "const x = 1", "newString": "const x = 2" }\`
|
|
382
335
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
Example: \`{ "
|
|
336
|
+
5. **Replace all occurrences** — add \`replaceAll: true\`
|
|
337
|
+
Replaces every occurrence of \`oldString\` in the file.
|
|
338
|
+
Example: \`{ "filePath": "src/app.ts", "oldString": "oldName", "newString": "newName", "replaceAll": true }\`
|
|
386
339
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
Example: \`{ "filePath": "src
|
|
340
|
+
6. **Select specific occurrence** — add \`occurrence: N\` (0-indexed)
|
|
341
|
+
When multiple matches exist, select the Nth one (0 = first, 1 = second, etc.).
|
|
342
|
+
Example: \`{ "filePath": "src/app.ts", "oldString": "TODO", "newString": "DONE", "occurrence": 0 }\`
|
|
390
343
|
|
|
391
|
-
|
|
392
|
-
- \`filePath\` (string): Path to file, or glob pattern for multi-file operations
|
|
393
|
-
- \`match\` (string): Text to find (exact match). For multi-line, use actual newlines.
|
|
394
|
-
- \`replacement\` (string): Text to replace with
|
|
395
|
-
- \`replace_all\` (boolean): Replace all occurrences instead of erroring on ambiguity
|
|
396
|
-
- \`occurrence\` (number): 0-indexed occurrence to replace when multiple matches exist
|
|
397
|
-
- \`symbol\` (string): Named symbol to replace (function, class, type)
|
|
398
|
-
- \`content\` (string): New content for symbol replace or file write
|
|
399
|
-
- \`edits\` (array): Batch edits — array of { match, replacement } or { line_start, line_end, content }
|
|
400
|
-
- \`operations\` (array): Transaction — array of { file, command, ... } for atomic multi-file edits
|
|
401
|
-
- \`dry_run\` (boolean): Preview changes without applying (returns diff)
|
|
402
|
-
- \`diagnostics\` (boolean): Return inline LSP diagnostics after the edit
|
|
344
|
+
Note: Modes 5 and 6 are options on mode 4 (find/replace) — they require \`oldString\`.
|
|
403
345
|
|
|
404
346
|
**Behavior:**
|
|
405
347
|
- Backs up files before editing (recoverable via aft_safety undo)
|
|
406
348
|
- Auto-formats using project formatter if configured
|
|
407
349
|
- Tree-sitter syntax validation on all edits
|
|
408
|
-
- Symbol replace includes decorators, attributes, and doc comments in range
|
|
409
|
-
|
|
350
|
+
- Symbol replace includes decorators, attributes, and doc comments in range
|
|
351
|
+
- LSP error-level diagnostics are returned automatically after non-dry-run edits
|
|
352
|
+
|
|
353
|
+
Returns: JSON string for the selected edit mode. Dry runs return diff data; non-dry-run edits may append inline LSP error lines.
|
|
354
|
+
|
|
355
|
+
Common response fields: success (boolean), diff (object with before/after), backup_id (string), syntax_valid (boolean). Exact fields vary by mode.`;
|
|
356
|
+
// Note: The Returns section intentionally stays high-level because per-mode JSON shapes
|
|
357
|
+
// vary by Rust command and documenting each would bloat the description for minimal gain.
|
|
358
|
+
// Agents can parse the JSON response generically — key fields include 'success' and 'diff'.
|
|
359
|
+
}
|
|
360
|
+
function createEditTool(ctx, writeToolName = "write") {
|
|
410
361
|
return {
|
|
411
|
-
description:
|
|
362
|
+
description: getEditDescription(writeToolName),
|
|
412
363
|
args: {
|
|
413
|
-
filePath: z
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
364
|
+
filePath: z
|
|
365
|
+
.string()
|
|
366
|
+
.optional()
|
|
367
|
+
.describe("Path to the file to edit (absolute or relative to project root). Required for all modes except 'operations' multi-file transactions"),
|
|
368
|
+
oldString: z.string().optional().describe("Text to find (exact match, with fuzzy fallback)"),
|
|
369
|
+
newString: z
|
|
370
|
+
.string()
|
|
371
|
+
.optional()
|
|
372
|
+
.describe("Text to replace with (omit or set to empty string to delete the matched text)"),
|
|
373
|
+
replaceAll: z.boolean().optional().describe("Replace all occurrences"),
|
|
374
|
+
occurrence: z
|
|
375
|
+
.number()
|
|
376
|
+
.optional()
|
|
377
|
+
.describe("0-indexed occurrence to replace when multiple matches exist"),
|
|
378
|
+
symbol: z.string().optional().describe("Named symbol to replace (function, class, type)"),
|
|
379
|
+
content: z.string().optional().describe("New content for symbol replace or file write"),
|
|
380
|
+
edits: z
|
|
381
|
+
.array(z.record(z.string(), z.unknown()))
|
|
382
|
+
.optional()
|
|
383
|
+
.describe("Batch edits — array of { oldString: string, newString: string } or { startLine: number (1-based), endLine: number (1-based, inclusive), content: string }"),
|
|
384
|
+
operations: z
|
|
385
|
+
.array(z.record(z.string(), z.unknown()))
|
|
386
|
+
.optional()
|
|
387
|
+
.describe("Transaction — array of { file: string, command: 'edit_match' | 'write', match?: string, replacement?: string, content?: string } for multi-file edits with rollback. Note: uses 'file'/'match'/'replacement' (not filePath/oldString/newString)"),
|
|
388
|
+
dryRun: z
|
|
389
|
+
.boolean()
|
|
390
|
+
.optional()
|
|
391
|
+
.describe("Preview changes without applying (returns diff, default: false)"),
|
|
424
392
|
},
|
|
425
393
|
execute: async (args, context) => {
|
|
426
394
|
const bridge = ctx.pool.getBridge(context.directory);
|
|
@@ -440,12 +408,14 @@ function createEditTool(ctx) {
|
|
|
440
408
|
? op.file
|
|
441
409
|
: path.resolve(context.directory, op.file),
|
|
442
410
|
}));
|
|
443
|
-
const
|
|
411
|
+
const params = { operations: resolvedOps };
|
|
412
|
+
params.dry_run = args.dryRun === true;
|
|
413
|
+
const data = await bridge.send("transaction", params);
|
|
444
414
|
return JSON.stringify(data);
|
|
445
415
|
}
|
|
446
|
-
const file =
|
|
416
|
+
const file = args.filePath;
|
|
447
417
|
if (!file)
|
|
448
|
-
throw new Error("'
|
|
418
|
+
throw new Error("'filePath' parameter is required");
|
|
449
419
|
const filePath = path.isAbsolute(file) ? file : path.resolve(context.directory, file);
|
|
450
420
|
const relPath = path.relative(context.worktree, filePath);
|
|
451
421
|
await context.ask({
|
|
@@ -458,48 +428,64 @@ function createEditTool(ctx) {
|
|
|
458
428
|
// Route to appropriate Rust command
|
|
459
429
|
let command;
|
|
460
430
|
if (Array.isArray(args.edits)) {
|
|
461
|
-
// Batch mode
|
|
431
|
+
// Batch mode — translate camelCase to snake_case for Rust
|
|
462
432
|
command = "batch";
|
|
463
|
-
params.edits = args.edits
|
|
433
|
+
params.edits = args.edits.map((edit) => {
|
|
434
|
+
const translated = {};
|
|
435
|
+
for (const [key, value] of Object.entries(edit)) {
|
|
436
|
+
if (key === "oldString")
|
|
437
|
+
translated.match = value;
|
|
438
|
+
else if (key === "newString")
|
|
439
|
+
translated.replacement = value;
|
|
440
|
+
else if (key === "startLine")
|
|
441
|
+
translated.line_start = value;
|
|
442
|
+
else if (key === "endLine")
|
|
443
|
+
translated.line_end = value;
|
|
444
|
+
else
|
|
445
|
+
translated[key] = value;
|
|
446
|
+
}
|
|
447
|
+
return translated;
|
|
448
|
+
});
|
|
464
449
|
}
|
|
465
|
-
else if (typeof args.symbol === "string"
|
|
466
|
-
|
|
450
|
+
else if (typeof args.symbol === "string" &&
|
|
451
|
+
typeof args.oldString !== "string" &&
|
|
452
|
+
args.content !== undefined) {
|
|
453
|
+
// Symbol replace — only when content is provided and oldString is NOT present
|
|
454
|
+
// (agents often pass symbol as "what to search for", not "replace whole symbol")
|
|
467
455
|
command = "edit_symbol";
|
|
468
456
|
params.symbol = args.symbol;
|
|
469
457
|
params.operation = "replace";
|
|
470
|
-
|
|
471
|
-
params.content = args.content;
|
|
458
|
+
params.content = args.content;
|
|
472
459
|
}
|
|
473
|
-
else if (typeof args.
|
|
474
|
-
// Find/replace mode (
|
|
460
|
+
else if (typeof args.oldString === "string") {
|
|
461
|
+
// Find/replace mode — default newString to "" (deletion) if not provided
|
|
475
462
|
command = "edit_match";
|
|
476
|
-
params.match = args.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
params.replace_all = args.replace_all;
|
|
463
|
+
params.match = args.oldString;
|
|
464
|
+
params.replacement = args.newString ?? "";
|
|
465
|
+
if (args.replaceAll !== undefined)
|
|
466
|
+
params.replace_all = args.replaceAll;
|
|
481
467
|
if (args.occurrence !== undefined)
|
|
482
468
|
params.occurrence = args.occurrence;
|
|
483
469
|
}
|
|
484
470
|
else if (typeof args.content === "string") {
|
|
485
|
-
// Write mode
|
|
471
|
+
// Write mode
|
|
486
472
|
command = "write";
|
|
487
473
|
params.content = args.content;
|
|
488
474
|
params.create_dirs = true;
|
|
489
475
|
}
|
|
490
476
|
else {
|
|
491
|
-
throw new Error("Provide '
|
|
477
|
+
throw new Error("Provide 'oldString' + 'newString', 'symbol' + 'content', 'edits' array, or 'content' for write");
|
|
492
478
|
}
|
|
493
|
-
if (args.
|
|
479
|
+
if (args.dryRun)
|
|
494
480
|
params.dry_run = true;
|
|
495
|
-
if (args.
|
|
481
|
+
if (!args.dryRun)
|
|
496
482
|
params.diagnostics = true;
|
|
497
483
|
// Request diff from Rust for UI metadata (avoids extra file reads in TS)
|
|
498
|
-
if (!args.
|
|
484
|
+
if (!args.dryRun)
|
|
499
485
|
params.include_diff = true;
|
|
500
486
|
const data = await bridge.send(command, params);
|
|
501
487
|
// Store metadata for tool.execute.after hook (fromPlugin overwrites context.metadata)
|
|
502
|
-
if (!args.
|
|
488
|
+
if (!args.dryRun && data.success && data.diff) {
|
|
503
489
|
const diff = data.diff;
|
|
504
490
|
const callID = getCallID(context);
|
|
505
491
|
if (callID) {
|
|
@@ -522,7 +508,19 @@ function createEditTool(ctx) {
|
|
|
522
508
|
});
|
|
523
509
|
}
|
|
524
510
|
}
|
|
525
|
-
|
|
511
|
+
let result = JSON.stringify(data);
|
|
512
|
+
// Append inline diagnostics to output (matching write tool pattern)
|
|
513
|
+
if (!args.dryRun) {
|
|
514
|
+
const diags = data.lsp_diagnostics;
|
|
515
|
+
if (diags && diags.length > 0) {
|
|
516
|
+
const errors = diags.filter((d) => d.severity === "error");
|
|
517
|
+
if (errors.length > 0) {
|
|
518
|
+
const diagLines = errors.map((d) => ` Line ${d.line}: ${d.message}`).join("\n");
|
|
519
|
+
result += `\n\nLSP errors detected, please fix:\n${diagLines}`;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return result;
|
|
526
524
|
},
|
|
527
525
|
};
|
|
528
526
|
}
|
|
@@ -544,6 +542,11 @@ Uses the opencode patch format with \`*** Begin Patch\` / \`*** End Patch\` mark
|
|
|
544
542
|
-old line to remove
|
|
545
543
|
+new line to add
|
|
546
544
|
context line (unchanged, prefixed with space)
|
|
545
|
+
*** Update File: path/to/old-name.ts
|
|
546
|
+
*** Move to: path/to/new-name.ts
|
|
547
|
+
@@ import { foo }
|
|
548
|
+
-import { foo } from './old'
|
|
549
|
+
+import { foo } from './new'
|
|
547
550
|
*** Delete File: path/to/obsolete-file.ts
|
|
548
551
|
*** End Patch
|
|
549
552
|
\`\`\`
|
|
@@ -560,23 +563,24 @@ Uses the opencode patch format with \`*** Begin Patch\` / \`*** End Patch\` mark
|
|
|
560
563
|
- \`+line\` — Add this line
|
|
561
564
|
- \` line\` — Context line (space prefix), appears in both old and new
|
|
562
565
|
|
|
563
|
-
**Parameters:**
|
|
564
|
-
- \`patch\` (string, required): The full patch text including Begin/End markers
|
|
565
|
-
|
|
566
566
|
**Behavior:**
|
|
567
|
-
- All file changes are applied
|
|
567
|
+
- All file changes are applied with checkpoint-based rollback — if any file fails, previous changes are rolled back (best-effort)
|
|
568
568
|
- Files are backed up before modification
|
|
569
569
|
- Parent directories are created automatically for new files
|
|
570
|
-
- Fuzzy matching for context anchors (handles whitespace and Unicode differences)
|
|
570
|
+
- Fuzzy matching for context anchors (handles whitespace and Unicode differences)
|
|
571
|
+
|
|
572
|
+
Returns: Status message string listing created, updated, moved, deleted, or failed file operations. May include inline LSP errors if type errors are introduced by the patch.`;
|
|
571
573
|
function createApplyPatchTool(ctx) {
|
|
572
574
|
return {
|
|
573
575
|
description: APPLY_PATCH_DESCRIPTION,
|
|
574
576
|
args: {
|
|
575
|
-
|
|
577
|
+
patchText: z.string().describe("The full patch text including Begin/End markers"),
|
|
576
578
|
},
|
|
577
579
|
execute: async (args, context) => {
|
|
578
580
|
const bridge = ctx.pool.getBridge(context.directory);
|
|
579
|
-
const patchText = args.
|
|
581
|
+
const patchText = args.patchText;
|
|
582
|
+
if (!patchText)
|
|
583
|
+
throw new Error("'patchText' is required");
|
|
580
584
|
// Parse the patch
|
|
581
585
|
let hunks;
|
|
582
586
|
try {
|
|
@@ -596,53 +600,138 @@ function createApplyPatchTool(ctx) {
|
|
|
596
600
|
always: ["*"],
|
|
597
601
|
metadata: {},
|
|
598
602
|
});
|
|
599
|
-
//
|
|
603
|
+
// Checkpoint all affected files for atomic rollback
|
|
604
|
+
const checkpointName = `apply_patch_${Date.now()}`;
|
|
605
|
+
try {
|
|
606
|
+
await bridge.send("checkpoint", {
|
|
607
|
+
name: checkpointName,
|
|
608
|
+
files: allPaths.map((p) => path.resolve(context.directory, p)),
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
// Checkpoint failure is non-fatal — proceed without rollback protection
|
|
613
|
+
}
|
|
614
|
+
// Process each hunk, track diffs for metadata
|
|
600
615
|
const results = [];
|
|
616
|
+
let combinedBefore = "";
|
|
617
|
+
let combinedAfter = "";
|
|
618
|
+
let patchFailed = false;
|
|
601
619
|
for (const hunk of hunks) {
|
|
602
620
|
const filePath = path.resolve(context.directory, hunk.path);
|
|
603
621
|
switch (hunk.type) {
|
|
604
622
|
case "add": {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
623
|
+
try {
|
|
624
|
+
await bridge.send("write", {
|
|
625
|
+
file: filePath,
|
|
626
|
+
content: hunk.contents.endsWith("\n") ? hunk.contents : `${hunk.contents}\n`,
|
|
627
|
+
create_dirs: true,
|
|
628
|
+
diagnostics: true,
|
|
629
|
+
});
|
|
630
|
+
combinedAfter += hunk.contents;
|
|
631
|
+
results.push(`Created ${hunk.path}`);
|
|
632
|
+
}
|
|
633
|
+
catch (e) {
|
|
634
|
+
patchFailed = true;
|
|
635
|
+
results.push(`Failed to create ${hunk.path}: ${e instanceof Error ? e.message : e}`);
|
|
636
|
+
}
|
|
611
637
|
break;
|
|
612
638
|
}
|
|
613
639
|
case "delete": {
|
|
614
640
|
try {
|
|
615
|
-
await fs.promises.
|
|
641
|
+
const before = await fs.promises.readFile(filePath, "utf-8").catch(() => "");
|
|
642
|
+
await bridge.send("delete_file", { file: filePath });
|
|
643
|
+
combinedBefore += before;
|
|
616
644
|
results.push(`Deleted ${hunk.path}`);
|
|
617
645
|
}
|
|
618
646
|
catch (e) {
|
|
647
|
+
patchFailed = true;
|
|
619
648
|
results.push(`Failed to delete ${hunk.path}: ${e instanceof Error ? e.message : e}`);
|
|
620
649
|
}
|
|
621
650
|
break;
|
|
622
651
|
}
|
|
623
652
|
case "update": {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
653
|
+
try {
|
|
654
|
+
// Read original, apply chunks, write back
|
|
655
|
+
const original = await fs.promises.readFile(filePath, "utf-8");
|
|
656
|
+
const newContent = applyUpdateChunks(original, filePath, hunk.chunks);
|
|
657
|
+
const targetPath = hunk.move_path
|
|
658
|
+
? path.resolve(context.directory, hunk.move_path)
|
|
659
|
+
: filePath;
|
|
660
|
+
const writeResult = await bridge.send("write", {
|
|
661
|
+
file: targetPath,
|
|
662
|
+
content: newContent,
|
|
663
|
+
create_dirs: true,
|
|
664
|
+
diagnostics: true,
|
|
665
|
+
});
|
|
666
|
+
// Collect diagnostics from this file
|
|
667
|
+
const diags = writeResult.lsp_diagnostics;
|
|
668
|
+
if (diags && diags.length > 0) {
|
|
669
|
+
const errors = diags.filter((d) => d.severity === "error");
|
|
670
|
+
if (errors.length > 0) {
|
|
671
|
+
const relPath = path.relative(context.worktree, targetPath);
|
|
672
|
+
const diagLines = errors.map((d) => ` Line ${d.line}: ${d.message}`).join("\n");
|
|
673
|
+
results.push(`\nLSP errors detected in ${relPath}, please fix:\n${diagLines}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// Track diff for metadata
|
|
677
|
+
combinedBefore += original;
|
|
678
|
+
combinedAfter += newContent;
|
|
679
|
+
if (hunk.move_path) {
|
|
680
|
+
await bridge.send("delete_file", { file: filePath });
|
|
681
|
+
results.push(`Updated and moved ${hunk.path} → ${hunk.move_path}`);
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
results.push(`Updated ${hunk.path}`);
|
|
685
|
+
}
|
|
638
686
|
}
|
|
639
|
-
|
|
640
|
-
|
|
687
|
+
catch (e) {
|
|
688
|
+
patchFailed = true;
|
|
689
|
+
results.push(`Failed to update ${hunk.path}: ${e instanceof Error ? e.message : e}`);
|
|
690
|
+
break;
|
|
641
691
|
}
|
|
642
692
|
break;
|
|
643
693
|
}
|
|
644
694
|
}
|
|
645
695
|
}
|
|
696
|
+
// On failure, restore checkpoint to undo partial changes
|
|
697
|
+
if (patchFailed) {
|
|
698
|
+
try {
|
|
699
|
+
await bridge.send("restore_checkpoint", { name: checkpointName });
|
|
700
|
+
results.push("Patch failed — restored files to pre-patch state.");
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
results.push("Patch failed — checkpoint restore also failed, files may be inconsistent.");
|
|
704
|
+
}
|
|
705
|
+
return results.join("\n");
|
|
706
|
+
}
|
|
707
|
+
// Store metadata for tool.execute.after hook (match opencode built-in format)
|
|
708
|
+
const callID = getCallID(context);
|
|
709
|
+
if (callID) {
|
|
710
|
+
// Build per-file metadata matching opencode's files array
|
|
711
|
+
const files = hunks.map((h) => {
|
|
712
|
+
const relPath = path.relative(context.worktree, path.resolve(context.directory, h.path));
|
|
713
|
+
return {
|
|
714
|
+
filePath: path.resolve(context.directory, h.path),
|
|
715
|
+
relativePath: relPath,
|
|
716
|
+
type: h.type,
|
|
717
|
+
};
|
|
718
|
+
});
|
|
719
|
+
// Build title matching built-in: "Success. Updated the following files:\nM path/to/file.ts"
|
|
720
|
+
const fileList = files
|
|
721
|
+
.map((f) => {
|
|
722
|
+
const prefix = f.type === "add" ? "A" : f.type === "delete" ? "D" : "M";
|
|
723
|
+
return `${prefix} ${f.relativePath}`;
|
|
724
|
+
})
|
|
725
|
+
.join("\n");
|
|
726
|
+
const title = `Success. Updated the following files:\n${fileList}`;
|
|
727
|
+
storeToolMetadata(context.sessionID, callID, {
|
|
728
|
+
title,
|
|
729
|
+
metadata: {
|
|
730
|
+
diff: buildUnifiedDiff(files.length === 1 ? files[0].filePath : "patch", combinedBefore, combinedAfter),
|
|
731
|
+
files,
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
}
|
|
646
735
|
return results.join("\n");
|
|
647
736
|
},
|
|
648
737
|
};
|
|
@@ -651,21 +740,21 @@ function createApplyPatchTool(ctx) {
|
|
|
651
740
|
// Delete
|
|
652
741
|
// ---------------------------------------------------------------------------
|
|
653
742
|
const DELETE_DESCRIPTION = "Delete a file with backup (recoverable via aft_safety undo).\n\n" +
|
|
654
|
-
"Parameters:\n" +
|
|
655
|
-
"- file (string, required): Path to file to delete. Relative paths resolved from project root.\n\n" +
|
|
656
743
|
"Returns: { file, deleted, backup_id } on success.\n" +
|
|
657
744
|
"The file content is backed up before deletion — use aft_safety undo to recover if needed.";
|
|
658
745
|
function createDeleteTool(ctx) {
|
|
659
746
|
return {
|
|
660
747
|
description: DELETE_DESCRIPTION,
|
|
661
748
|
args: {
|
|
662
|
-
|
|
749
|
+
filePath: z
|
|
750
|
+
.string()
|
|
751
|
+
.describe("Path to file to delete (absolute or relative to project root)"),
|
|
663
752
|
},
|
|
664
753
|
execute: async (args, context) => {
|
|
665
754
|
const bridge = ctx.pool.getBridge(context.directory);
|
|
666
|
-
const filePath = path.isAbsolute(args.
|
|
667
|
-
? args.
|
|
668
|
-
: path.resolve(context.directory, args.
|
|
755
|
+
const filePath = path.isAbsolute(args.filePath)
|
|
756
|
+
? args.filePath
|
|
757
|
+
: path.resolve(context.directory, args.filePath);
|
|
669
758
|
await context.ask({
|
|
670
759
|
permission: "edit",
|
|
671
760
|
patterns: [filePath],
|
|
@@ -681,24 +770,26 @@ function createDeleteTool(ctx) {
|
|
|
681
770
|
// Move / Rename
|
|
682
771
|
// ---------------------------------------------------------------------------
|
|
683
772
|
const MOVE_DESCRIPTION = "Move or rename a file with backup (recoverable via aft_safety undo).\n\n" +
|
|
684
|
-
"Parameters:\n" +
|
|
685
|
-
"- file (string, required): Source file path to move.\n" +
|
|
686
|
-
"- destination (string, required): Destination file path.\n\n" +
|
|
687
773
|
"Creates parent directories for destination automatically.\n" +
|
|
688
774
|
"Falls back to copy+delete for cross-filesystem moves.\n" +
|
|
689
|
-
"Returns: { file, destination, moved, backup_id } on success
|
|
775
|
+
"Returns: { file, destination, moved, backup_id } on success.\n\n" +
|
|
776
|
+
"Note: This moves/renames files at the OS level. To move a code symbol (function, class) to another file while updating all imports, use aft_refactor with op='move' instead.";
|
|
690
777
|
function createMoveTool(ctx) {
|
|
691
778
|
return {
|
|
692
779
|
description: MOVE_DESCRIPTION,
|
|
693
780
|
args: {
|
|
694
|
-
|
|
695
|
-
|
|
781
|
+
filePath: z
|
|
782
|
+
.string()
|
|
783
|
+
.describe("Source file path to move (absolute or relative to project root)"),
|
|
784
|
+
destination: z
|
|
785
|
+
.string()
|
|
786
|
+
.describe("Destination file path (absolute or relative to project root)"),
|
|
696
787
|
},
|
|
697
788
|
execute: async (args, context) => {
|
|
698
789
|
const bridge = ctx.pool.getBridge(context.directory);
|
|
699
|
-
const filePath = path.isAbsolute(args.
|
|
700
|
-
? args.
|
|
701
|
-
: path.resolve(context.directory, args.
|
|
790
|
+
const filePath = path.isAbsolute(args.filePath)
|
|
791
|
+
? args.filePath
|
|
792
|
+
: path.resolve(context.directory, args.filePath);
|
|
702
793
|
const destPath = path.isAbsolute(args.destination)
|
|
703
794
|
? args.destination
|
|
704
795
|
: path.resolve(context.directory, args.destination);
|
|
@@ -726,8 +817,8 @@ function createMoveTool(ctx) {
|
|
|
726
817
|
export function hoistedTools(ctx) {
|
|
727
818
|
return {
|
|
728
819
|
read: createReadTool(ctx),
|
|
729
|
-
write: createWriteTool(ctx),
|
|
730
|
-
edit: createEditTool(ctx),
|
|
820
|
+
write: createWriteTool(ctx, "edit"),
|
|
821
|
+
edit: createEditTool(ctx, "write"),
|
|
731
822
|
apply_patch: createApplyPatchTool(ctx),
|
|
732
823
|
aft_delete: createDeleteTool(ctx),
|
|
733
824
|
aft_move: createMoveTool(ctx),
|
|
@@ -737,10 +828,22 @@ export function hoistedTools(ctx) {
|
|
|
737
828
|
* Returns the same tools with aft_ prefix (for when hoisting is disabled).
|
|
738
829
|
*/
|
|
739
830
|
export function aftPrefixedTools(ctx) {
|
|
831
|
+
const aftEditTool = createEditTool(ctx, "aft_write");
|
|
740
832
|
return {
|
|
741
833
|
aft_read: createReadTool(ctx),
|
|
742
|
-
aft_write: createWriteTool(ctx),
|
|
743
|
-
aft_edit:
|
|
834
|
+
aft_write: createWriteTool(ctx, "aft_edit"),
|
|
835
|
+
aft_edit: {
|
|
836
|
+
...aftEditTool,
|
|
837
|
+
execute: async (args, context) => {
|
|
838
|
+
const argRecord = args;
|
|
839
|
+
const normalizedArgs = argRecord.mode !== undefined &&
|
|
840
|
+
argRecord.filePath === undefined &&
|
|
841
|
+
typeof argRecord.file === "string"
|
|
842
|
+
? { ...argRecord, filePath: argRecord.file }
|
|
843
|
+
: argRecord;
|
|
844
|
+
return aftEditTool.execute(normalizedArgs, context);
|
|
845
|
+
},
|
|
846
|
+
},
|
|
744
847
|
aft_apply_patch: createApplyPatchTool(ctx),
|
|
745
848
|
aft_delete: createDeleteTool(ctx),
|
|
746
849
|
aft_move: createMoveTool(ctx),
|