@clinebot/core 0.0.6 → 0.0.10
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/agents/hooks-config-loader.d.ts +1 -0
- package/dist/auth/cline.d.ts +2 -0
- package/dist/auth/codex.d.ts +5 -1
- package/dist/auth/oca.d.ts +7 -1
- package/dist/auth/types.d.ts +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.node.d.ts +2 -0
- package/dist/index.node.js +164 -162
- package/dist/input/mention-enricher.d.ts +1 -0
- package/dist/providers/local-provider-service.d.ts +1 -1
- package/dist/runtime/session-runtime.d.ts +1 -1
- package/dist/session/default-session-manager.d.ts +13 -17
- package/dist/session/rpc-spawn-lease.d.ts +7 -0
- package/dist/session/runtime-oauth-token-manager.d.ts +4 -2
- package/dist/session/session-agent-events.d.ts +15 -0
- package/dist/session/session-config-builder.d.ts +13 -0
- package/dist/session/session-manager.d.ts +2 -2
- package/dist/session/session-team-coordination.d.ts +12 -0
- package/dist/session/session-telemetry.d.ts +9 -0
- package/dist/session/unified-session-persistence-service.d.ts +12 -16
- package/dist/session/utils/helpers.d.ts +1 -1
- package/dist/session/utils/types.d.ts +1 -1
- package/dist/storage/provider-settings-legacy-migration.d.ts +25 -0
- package/dist/telemetry/core-events.d.ts +122 -0
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/executors/file-read.d.ts +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/presets.d.ts +1 -1
- package/dist/tools/schemas.d.ts +48 -11
- package/dist/tools/types.d.ts +3 -3
- package/dist/types/config.d.ts +1 -1
- package/dist/types/events.d.ts +1 -1
- package/dist/types/provider-settings.d.ts +4 -4
- package/dist/types.d.ts +1 -1
- package/package.json +4 -3
- package/src/agents/hooks-config-loader.ts +2 -0
- package/src/auth/cline.ts +35 -1
- package/src/auth/codex.ts +27 -2
- package/src/auth/oca.ts +31 -4
- package/src/auth/types.ts +3 -0
- package/src/index.node.ts +4 -0
- package/src/index.ts +27 -0
- package/src/input/file-indexer.test.ts +40 -0
- package/src/input/file-indexer.ts +21 -0
- package/src/input/mention-enricher.test.ts +3 -0
- package/src/input/mention-enricher.ts +3 -0
- package/src/providers/local-provider-service.ts +6 -7
- package/src/runtime/hook-file-hooks.test.ts +51 -1
- package/src/runtime/hook-file-hooks.ts +91 -11
- package/src/runtime/session-runtime.ts +1 -1
- package/src/session/default-session-manager.e2e.test.ts +2 -1
- package/src/session/default-session-manager.ts +367 -601
- package/src/session/rpc-spawn-lease.test.ts +49 -0
- package/src/session/rpc-spawn-lease.ts +122 -0
- package/src/session/runtime-oauth-token-manager.ts +21 -14
- package/src/session/session-agent-events.ts +159 -0
- package/src/session/session-config-builder.ts +111 -0
- package/src/session/session-graph.ts +2 -0
- package/src/session/session-host.ts +21 -0
- package/src/session/session-manager.ts +2 -2
- package/src/session/session-team-coordination.ts +198 -0
- package/src/session/session-telemetry.ts +95 -0
- package/src/session/unified-session-persistence-service.test.ts +81 -0
- package/src/session/unified-session-persistence-service.ts +470 -469
- package/src/session/utils/helpers.ts +1 -1
- package/src/session/utils/types.ts +1 -1
- package/src/storage/provider-settings-legacy-migration.test.ts +133 -1
- package/src/storage/provider-settings-legacy-migration.ts +63 -11
- package/src/telemetry/core-events.ts +344 -0
- package/src/tools/definitions.test.ts +203 -36
- package/src/tools/definitions.ts +66 -28
- package/src/tools/executors/editor.test.ts +35 -0
- package/src/tools/executors/editor.ts +33 -46
- package/src/tools/executors/file-read.test.ts +29 -5
- package/src/tools/executors/file-read.ts +17 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/presets.ts +1 -1
- package/src/tools/schemas.ts +88 -38
- package/src/tools/types.ts +7 -3
- package/src/types/config.ts +1 -1
- package/src/types/events.ts +6 -1
- package/src/types/provider-settings.ts +6 -6
- package/src/types.ts +1 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createBashTool,
|
|
4
|
+
createDefaultTools,
|
|
5
|
+
createReadFilesTool,
|
|
6
|
+
} from "./definitions.js";
|
|
3
7
|
|
|
4
8
|
describe("default skills tool", () => {
|
|
5
9
|
it("is included only when enabled with a skills executor", () => {
|
|
@@ -213,23 +217,133 @@ describe("default apply_patch tool", () => {
|
|
|
213
217
|
});
|
|
214
218
|
});
|
|
215
219
|
|
|
220
|
+
describe("default run_commands tool", () => {
|
|
221
|
+
it("accepts object input with commands as a single string", async () => {
|
|
222
|
+
const execute = vi.fn(async (command: string) => `ran:${command}`);
|
|
223
|
+
const tool = createBashTool(execute);
|
|
224
|
+
|
|
225
|
+
const result = await tool.execute({ commands: "ls" } as never, {
|
|
226
|
+
agentId: "agent-1",
|
|
227
|
+
conversationId: "conv-1",
|
|
228
|
+
iteration: 1,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(result).toEqual([
|
|
232
|
+
{
|
|
233
|
+
query: "ls",
|
|
234
|
+
result: "ran:ls",
|
|
235
|
+
success: true,
|
|
236
|
+
},
|
|
237
|
+
]);
|
|
238
|
+
expect(execute).toHaveBeenCalledTimes(1);
|
|
239
|
+
expect(execute).toHaveBeenCalledWith(
|
|
240
|
+
"ls",
|
|
241
|
+
process.cwd(),
|
|
242
|
+
expect.objectContaining({
|
|
243
|
+
agentId: "agent-1",
|
|
244
|
+
conversationId: "conv-1",
|
|
245
|
+
iteration: 1,
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("default read_files tool", () => {
|
|
252
|
+
it("normalizes ranged file requests and passes them to the executor", async () => {
|
|
253
|
+
const execute = vi.fn(async () => "selected lines");
|
|
254
|
+
const tool = createReadFilesTool(execute);
|
|
255
|
+
|
|
256
|
+
const result = await tool.execute(
|
|
257
|
+
{
|
|
258
|
+
files: [
|
|
259
|
+
{
|
|
260
|
+
path: "/tmp/example.ts",
|
|
261
|
+
start_line: 3,
|
|
262
|
+
end_line: 5,
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
agentId: "agent-1",
|
|
268
|
+
conversationId: "conv-1",
|
|
269
|
+
iteration: 1,
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect(result).toEqual([
|
|
274
|
+
{
|
|
275
|
+
query: "/tmp/example.ts:3-5",
|
|
276
|
+
result: "selected lines",
|
|
277
|
+
success: true,
|
|
278
|
+
},
|
|
279
|
+
]);
|
|
280
|
+
expect(execute).toHaveBeenCalledWith(
|
|
281
|
+
{
|
|
282
|
+
path: "/tmp/example.ts",
|
|
283
|
+
start_line: 3,
|
|
284
|
+
end_line: 5,
|
|
285
|
+
},
|
|
286
|
+
expect.objectContaining({
|
|
287
|
+
agentId: "agent-1",
|
|
288
|
+
conversationId: "conv-1",
|
|
289
|
+
iteration: 1,
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("keeps legacy string inputs reading full file content", async () => {
|
|
295
|
+
const execute = vi.fn(async () => "full file");
|
|
296
|
+
const tool = createReadFilesTool(execute);
|
|
297
|
+
|
|
298
|
+
await tool.execute("/tmp/example.ts" as never, {
|
|
299
|
+
agentId: "agent-1",
|
|
300
|
+
conversationId: "conv-1",
|
|
301
|
+
iteration: 1,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(execute).toHaveBeenCalledWith(
|
|
305
|
+
{ path: "/tmp/example.ts" },
|
|
306
|
+
expect.objectContaining({
|
|
307
|
+
agentId: "agent-1",
|
|
308
|
+
conversationId: "conv-1",
|
|
309
|
+
iteration: 1,
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
216
315
|
describe("zod schema conversion", () => {
|
|
217
316
|
it("preserves read_files required properties in generated JSON schema", () => {
|
|
218
317
|
const tool = createReadFilesTool(async () => "ok");
|
|
219
318
|
const inputSchema = tool.inputSchema as Record<string, unknown>;
|
|
220
319
|
const properties = inputSchema.properties as Record<string, unknown>;
|
|
221
320
|
expect(inputSchema.type).toBe("object");
|
|
222
|
-
expect(properties.
|
|
321
|
+
expect(properties.files).toMatchObject({
|
|
223
322
|
type: "array",
|
|
224
323
|
items: {
|
|
225
|
-
type: "
|
|
226
|
-
|
|
227
|
-
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: {
|
|
326
|
+
path: {
|
|
327
|
+
type: "string",
|
|
328
|
+
description:
|
|
329
|
+
"The absolute file path of a text file to read content from",
|
|
330
|
+
},
|
|
331
|
+
start_line: {
|
|
332
|
+
type: "integer",
|
|
333
|
+
description: "Optional one-based starting line number to read from",
|
|
334
|
+
},
|
|
335
|
+
end_line: {
|
|
336
|
+
type: "integer",
|
|
337
|
+
description:
|
|
338
|
+
"Optional one-based ending line number to read through",
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
required: ["path"],
|
|
228
342
|
},
|
|
229
343
|
description:
|
|
230
|
-
"Array of
|
|
344
|
+
"Array of file read requests. Omit start_line and end_line to return the full file content; provide them to return only that inclusive one-based line range. Prefer this tool over running terminal command to get file content for better performance and reliability.",
|
|
231
345
|
});
|
|
232
|
-
expect(inputSchema.required).toEqual(["
|
|
346
|
+
expect(inputSchema.required).toEqual(["files"]);
|
|
233
347
|
});
|
|
234
348
|
|
|
235
349
|
it("exposes skills args as optional nullable in tool schemas", () => {
|
|
@@ -261,7 +375,7 @@ describe("zod schema conversion", () => {
|
|
|
261
375
|
});
|
|
262
376
|
|
|
263
377
|
describe("default editor tool", () => {
|
|
264
|
-
it("accepts
|
|
378
|
+
it("accepts replacement edits without insert fields", async () => {
|
|
265
379
|
const execute = vi.fn(async () => "patched");
|
|
266
380
|
const tools = createDefaultTools({
|
|
267
381
|
executors: {
|
|
@@ -284,12 +398,9 @@ describe("default editor tool", () => {
|
|
|
284
398
|
|
|
285
399
|
const result = await editorTool.execute(
|
|
286
400
|
{
|
|
287
|
-
command: "str_replace",
|
|
288
401
|
path: "/tmp/example.ts",
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
file_text: null,
|
|
292
|
-
insert_line: null,
|
|
402
|
+
old_text: "before",
|
|
403
|
+
new_text: "after",
|
|
293
404
|
},
|
|
294
405
|
{
|
|
295
406
|
agentId: "agent-1",
|
|
@@ -299,18 +410,15 @@ describe("default editor tool", () => {
|
|
|
299
410
|
);
|
|
300
411
|
|
|
301
412
|
expect(result).toEqual({
|
|
302
|
-
query: "
|
|
413
|
+
query: "edit:/tmp/example.ts",
|
|
303
414
|
result: "patched",
|
|
304
415
|
success: true,
|
|
305
416
|
});
|
|
306
417
|
expect(execute).toHaveBeenCalledWith(
|
|
307
418
|
expect.objectContaining({
|
|
308
|
-
command: "str_replace",
|
|
309
419
|
path: "/tmp/example.ts",
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
file_text: null,
|
|
313
|
-
insert_line: null,
|
|
420
|
+
old_text: "before",
|
|
421
|
+
new_text: "after",
|
|
314
422
|
}),
|
|
315
423
|
process.cwd(),
|
|
316
424
|
expect.objectContaining({
|
|
@@ -321,7 +429,7 @@ describe("default editor tool", () => {
|
|
|
321
429
|
);
|
|
322
430
|
});
|
|
323
431
|
|
|
324
|
-
it("
|
|
432
|
+
it("allows edit without old_text so missing files can be created", async () => {
|
|
325
433
|
const execute = vi.fn(async () => "patched");
|
|
326
434
|
const tools = createDefaultTools({
|
|
327
435
|
executors: {
|
|
@@ -342,21 +450,80 @@ describe("default editor tool", () => {
|
|
|
342
450
|
throw new Error("Expected editor tool to be defined.");
|
|
343
451
|
}
|
|
344
452
|
|
|
345
|
-
await
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
453
|
+
const result = await editorTool.execute(
|
|
454
|
+
{
|
|
455
|
+
path: "/tmp/example.ts",
|
|
456
|
+
new_text: "created",
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
agentId: "agent-1",
|
|
460
|
+
conversationId: "conv-1",
|
|
461
|
+
iteration: 1,
|
|
462
|
+
},
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
expect(result).toEqual({
|
|
466
|
+
query: "edit:/tmp/example.ts",
|
|
467
|
+
result: "patched",
|
|
468
|
+
success: true,
|
|
469
|
+
});
|
|
470
|
+
expect(execute).toHaveBeenCalledWith(
|
|
471
|
+
expect.objectContaining({
|
|
472
|
+
path: "/tmp/example.ts",
|
|
473
|
+
new_text: "created",
|
|
474
|
+
}),
|
|
475
|
+
process.cwd(),
|
|
476
|
+
expect.anything(),
|
|
477
|
+
);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("treats insert_line as an insert operation", async () => {
|
|
481
|
+
const execute = vi.fn(async () => "patched");
|
|
482
|
+
const tools = createDefaultTools({
|
|
483
|
+
executors: {
|
|
484
|
+
editor: execute,
|
|
485
|
+
},
|
|
486
|
+
enableReadFiles: false,
|
|
487
|
+
enableSearch: false,
|
|
488
|
+
enableBash: false,
|
|
489
|
+
enableWebFetch: false,
|
|
490
|
+
enableSkills: false,
|
|
491
|
+
enableAskQuestion: false,
|
|
492
|
+
enableApplyPatch: false,
|
|
493
|
+
enableEditor: true,
|
|
494
|
+
});
|
|
495
|
+
const editorTool = tools.find((tool) => tool.name === "editor");
|
|
496
|
+
expect(editorTool).toBeDefined();
|
|
497
|
+
if (!editorTool) {
|
|
498
|
+
throw new Error("Expected editor tool to be defined.");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const result = await editorTool.execute(
|
|
502
|
+
{
|
|
503
|
+
path: "/tmp/example.ts",
|
|
504
|
+
new_text: "after",
|
|
505
|
+
insert_line: 3,
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
agentId: "agent-1",
|
|
509
|
+
conversationId: "conv-1",
|
|
510
|
+
iteration: 1,
|
|
511
|
+
},
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
expect(result).toEqual({
|
|
515
|
+
query: "insert:/tmp/example.ts",
|
|
516
|
+
result: "patched",
|
|
517
|
+
success: true,
|
|
518
|
+
});
|
|
519
|
+
expect(execute).toHaveBeenCalledWith(
|
|
520
|
+
expect.objectContaining({
|
|
521
|
+
path: "/tmp/example.ts",
|
|
522
|
+
new_text: "after",
|
|
523
|
+
insert_line: 3,
|
|
524
|
+
}),
|
|
525
|
+
process.cwd(),
|
|
526
|
+
expect.anything(),
|
|
527
|
+
);
|
|
361
528
|
});
|
|
362
529
|
});
|
package/src/tools/definitions.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
EditFileInputSchema,
|
|
17
17
|
type FetchWebContentInput,
|
|
18
18
|
FetchWebContentInputSchema,
|
|
19
|
+
type ReadFileRequest,
|
|
19
20
|
type ReadFilesInput,
|
|
20
21
|
ReadFilesInputSchema,
|
|
21
22
|
ReadFilesInputUnionSchema,
|
|
@@ -72,6 +73,46 @@ function withTimeout<T>(
|
|
|
72
73
|
]);
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
function normalizeReadFileRequests(input: unknown): ReadFileRequest[] {
|
|
77
|
+
const validate = validateWithZod(ReadFilesInputUnionSchema, input);
|
|
78
|
+
|
|
79
|
+
if (typeof validate === "string") {
|
|
80
|
+
return [{ path: validate }];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(validate)) {
|
|
84
|
+
return validate.map((value) =>
|
|
85
|
+
typeof value === "string" ? { path: value } : value,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if ("files" in validate) {
|
|
90
|
+
const files = Array.isArray(validate.files)
|
|
91
|
+
? validate.files
|
|
92
|
+
: [validate.files];
|
|
93
|
+
return files;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if ("file_paths" in validate) {
|
|
97
|
+
const filePaths = Array.isArray(validate.file_paths)
|
|
98
|
+
? validate.file_paths
|
|
99
|
+
: [validate.file_paths];
|
|
100
|
+
return filePaths.map((filePath) => ({ path: filePath }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [validate];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatReadFileQuery(request: ReadFileRequest): string {
|
|
107
|
+
const { path, start_line, end_line } = request;
|
|
108
|
+
if (start_line === undefined && end_line === undefined) {
|
|
109
|
+
return path;
|
|
110
|
+
}
|
|
111
|
+
const start = start_line ?? 1;
|
|
112
|
+
const end = end_line ?? "EOF";
|
|
113
|
+
return `${path}:${start}-${end}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
75
116
|
const APPLY_PATCH_TOOL_DESC = `This is a custom utility that makes it more convenient to add, remove, move, or edit code in a single file. \`apply_patch\` effectively allows you to execute a diff/patch against a file, but the format of the diff specification is unique to this task, so pay careful attention to these instructions. To use the \`apply_patch\` command, you should pass a message of the following structure as "input":
|
|
76
117
|
|
|
77
118
|
%%bash
|
|
@@ -147,38 +188,32 @@ export function createReadFilesTool(
|
|
|
147
188
|
return createTool<ReadFilesInput, ToolOperationResult[]>({
|
|
148
189
|
name: "read_files",
|
|
149
190
|
description:
|
|
150
|
-
"Read the
|
|
151
|
-
"Returns file contents or error messages for each path.
|
|
191
|
+
"Read the full content of text files at the provided absolute paths, or return only an inclusive one-based line range when start_line/end_line are provided. " +
|
|
192
|
+
"Returns file contents or error messages for each path.",
|
|
152
193
|
inputSchema: zodToJsonSchema(ReadFilesInputSchema),
|
|
153
194
|
timeoutMs: timeoutMs * 2, // Account for multiple files
|
|
154
195
|
retryable: true,
|
|
155
196
|
maxRetries: 1,
|
|
156
197
|
execute: async (input, context) => {
|
|
157
|
-
|
|
158
|
-
const validate = validateWithZod(ReadFilesInputUnionSchema, input);
|
|
159
|
-
const filePaths = Array.isArray(validate)
|
|
160
|
-
? validate
|
|
161
|
-
: typeof validate === "object"
|
|
162
|
-
? validate.file_paths
|
|
163
|
-
: [validate];
|
|
198
|
+
const requests = normalizeReadFileRequests(input);
|
|
164
199
|
|
|
165
200
|
return Promise.all(
|
|
166
|
-
|
|
201
|
+
requests.map(async (request): Promise<ToolOperationResult> => {
|
|
167
202
|
try {
|
|
168
203
|
const content = await withTimeout(
|
|
169
|
-
executor(
|
|
204
|
+
executor(request, context),
|
|
170
205
|
timeoutMs,
|
|
171
206
|
`File read timed out after ${timeoutMs}ms`,
|
|
172
207
|
);
|
|
173
208
|
return {
|
|
174
|
-
query:
|
|
209
|
+
query: formatReadFileQuery(request),
|
|
175
210
|
result: content,
|
|
176
211
|
success: true,
|
|
177
212
|
};
|
|
178
213
|
} catch (error) {
|
|
179
214
|
const msg = formatError(error);
|
|
180
215
|
return {
|
|
181
|
-
query:
|
|
216
|
+
query: formatReadFileQuery(request),
|
|
182
217
|
result: "",
|
|
183
218
|
error: `Error reading file: ${msg}`,
|
|
184
219
|
success: false,
|
|
@@ -214,15 +249,14 @@ export function createSearchTool(
|
|
|
214
249
|
maxRetries: 1,
|
|
215
250
|
execute: async (input, context) => {
|
|
216
251
|
// Validate input with Zod schema
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
: [validatedInput];
|
|
252
|
+
const validate = validateWithZod(SearchCodebaseUnionInputSchema, input);
|
|
253
|
+
const queries = Array.isArray(validate)
|
|
254
|
+
? validate
|
|
255
|
+
: typeof validate === "object"
|
|
256
|
+
? Array.isArray(validate.queries)
|
|
257
|
+
? validate.queries
|
|
258
|
+
: [validate.queries]
|
|
259
|
+
: [validate];
|
|
226
260
|
|
|
227
261
|
return Promise.all(
|
|
228
262
|
queries.map(async (query): Promise<ToolOperationResult> => {
|
|
@@ -282,7 +316,9 @@ export function createBashTool(
|
|
|
282
316
|
const commands = Array.isArray(validate)
|
|
283
317
|
? validate
|
|
284
318
|
: typeof validate === "object"
|
|
285
|
-
? validate.commands
|
|
319
|
+
? Array.isArray(validate.commands)
|
|
320
|
+
? validate.commands
|
|
321
|
+
: [validate.commands]
|
|
286
322
|
: [validate];
|
|
287
323
|
|
|
288
324
|
return Promise.all(
|
|
@@ -432,14 +468,16 @@ export function createEditorTool(
|
|
|
432
468
|
return createTool<EditFileInput, ToolOperationResult>({
|
|
433
469
|
name: "editor",
|
|
434
470
|
description:
|
|
435
|
-
"
|
|
436
|
-
"
|
|
471
|
+
"An editor for controlled filesystem edits on the text file at the provided path. " +
|
|
472
|
+
"Provide `insert_line` to insert `new_text` at a specific line number. " +
|
|
473
|
+
"Otherwise, the tool replaces `old_text` with `new_text`, or creates the file with `new_text` if it does not exist.",
|
|
437
474
|
inputSchema: zodToJsonSchema(EditFileInputSchema),
|
|
438
475
|
timeoutMs,
|
|
439
476
|
retryable: false, // Editing operations are stateful and should not auto-retry
|
|
440
477
|
maxRetries: 0,
|
|
441
478
|
execute: async (input, context) => {
|
|
442
479
|
const validatedInput = validateWithZod(EditFileInputSchema, input);
|
|
480
|
+
const operation = validatedInput.insert_line == null ? "edit" : "insert";
|
|
443
481
|
|
|
444
482
|
try {
|
|
445
483
|
const result = await withTimeout(
|
|
@@ -449,14 +487,14 @@ export function createEditorTool(
|
|
|
449
487
|
);
|
|
450
488
|
|
|
451
489
|
return {
|
|
452
|
-
query: `${
|
|
490
|
+
query: `${operation}:${validatedInput.path}`,
|
|
453
491
|
result,
|
|
454
492
|
success: true,
|
|
455
493
|
};
|
|
456
494
|
} catch (error) {
|
|
457
495
|
const msg = formatError(error);
|
|
458
496
|
return {
|
|
459
|
-
query: `${
|
|
497
|
+
query: `${operation}:${validatedInput.path}`,
|
|
460
498
|
result: "",
|
|
461
499
|
error: `Editor operation failed: ${msg}`,
|
|
462
500
|
success: false,
|
|
@@ -560,7 +598,7 @@ export function createAskQuestionTool(
|
|
|
560
598
|
*
|
|
561
599
|
* const tools = createDefaultTools({
|
|
562
600
|
* executors: {
|
|
563
|
-
* readFile: async (path) => fs.readFile(path, "utf-8"),
|
|
601
|
+
* readFile: async ({ path }) => fs.readFile(path, "utf-8"),
|
|
564
602
|
* bash: async (cmd, cwd) => {
|
|
565
603
|
* return new Promise((resolve, reject) => {
|
|
566
604
|
* exec(cmd, { cwd }, (err, stdout, stderr) => {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { createEditorExecutor } from "./editor.js";
|
|
6
|
+
|
|
7
|
+
describe("createEditorExecutor", () => {
|
|
8
|
+
it("creates a missing file when edit is used", async () => {
|
|
9
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "agents-editor-"));
|
|
10
|
+
const filePath = path.join(dir, "example.txt");
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const editor = createEditorExecutor();
|
|
14
|
+
const result = await editor(
|
|
15
|
+
{
|
|
16
|
+
path: filePath,
|
|
17
|
+
new_text: "created with edit",
|
|
18
|
+
},
|
|
19
|
+
dir,
|
|
20
|
+
{
|
|
21
|
+
agentId: "agent-1",
|
|
22
|
+
conversationId: "conv-1",
|
|
23
|
+
iteration: 1,
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(result).toBe(`File created successfully at: ${filePath}`);
|
|
28
|
+
await expect(fs.readFile(filePath, "utf-8")).resolves.toBe(
|
|
29
|
+
"created with edit",
|
|
30
|
+
);
|
|
31
|
+
} finally {
|
|
32
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -113,6 +113,15 @@ async function createFile(
|
|
|
113
113
|
return `File created successfully at: ${filePath}`;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
117
|
+
try {
|
|
118
|
+
await fs.access(filePath);
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
116
125
|
async function replaceInFile(
|
|
117
126
|
filePath: string,
|
|
118
127
|
oldStr: string,
|
|
@@ -181,52 +190,30 @@ export function createEditorExecutor(
|
|
|
181
190
|
): Promise<string> => {
|
|
182
191
|
const filePath = resolveFilePath(cwd, input.path, restrictToCwd);
|
|
183
192
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
filePath,
|
|
201
|
-
input.old_str,
|
|
202
|
-
input.new_str,
|
|
203
|
-
encoding,
|
|
204
|
-
maxDiffLines,
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
case "insert":
|
|
208
|
-
if (input.insert_line == null) {
|
|
209
|
-
throw new Error(
|
|
210
|
-
"Parameter `insert_line` is required for insert command.",
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
if (input.new_str == null) {
|
|
214
|
-
throw new Error(
|
|
215
|
-
"Parameter `new_str` is required for insert command.",
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
return insertInFile(
|
|
219
|
-
filePath,
|
|
220
|
-
input.insert_line, // One-based index
|
|
221
|
-
input.new_str,
|
|
222
|
-
encoding,
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
default:
|
|
226
|
-
throw new Error(
|
|
227
|
-
`Unrecognized command ${(input as { command: string }).command}. ` +
|
|
228
|
-
"Allowed commands are: create, str_replace, insert",
|
|
229
|
-
);
|
|
193
|
+
if (input.insert_line != null) {
|
|
194
|
+
return insertInFile(
|
|
195
|
+
filePath,
|
|
196
|
+
input.insert_line, // One-based index
|
|
197
|
+
input.new_text,
|
|
198
|
+
encoding,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!(await fileExists(filePath))) {
|
|
203
|
+
return createFile(filePath, input.new_text, encoding);
|
|
204
|
+
}
|
|
205
|
+
if (input.old_text == null) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
"Parameter `old_text` is required when editing an existing file without `insert_line`",
|
|
208
|
+
);
|
|
230
209
|
}
|
|
210
|
+
|
|
211
|
+
return replaceInFile(
|
|
212
|
+
filePath,
|
|
213
|
+
input.old_text,
|
|
214
|
+
input.new_text,
|
|
215
|
+
encoding,
|
|
216
|
+
maxDiffLines,
|
|
217
|
+
);
|
|
231
218
|
};
|
|
232
219
|
}
|
|
@@ -12,14 +12,38 @@ describe("createFileReadExecutor", () => {
|
|
|
12
12
|
|
|
13
13
|
try {
|
|
14
14
|
const readFile = createFileReadExecutor();
|
|
15
|
-
const result = await readFile(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
const result = await readFile(
|
|
16
|
+
{ path: filePath },
|
|
17
|
+
{
|
|
18
|
+
agentId: "agent-1",
|
|
19
|
+
conversationId: "conv-1",
|
|
20
|
+
iteration: 1,
|
|
21
|
+
},
|
|
22
|
+
);
|
|
20
23
|
expect(result).toBe("1 | hello absolute path");
|
|
21
24
|
} finally {
|
|
22
25
|
await fs.rm(dir, { recursive: true, force: true });
|
|
23
26
|
}
|
|
24
27
|
});
|
|
28
|
+
|
|
29
|
+
it("returns only the requested inclusive line range", async () => {
|
|
30
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "agents-file-read-"));
|
|
31
|
+
const filePath = path.join(dir, "example.txt");
|
|
32
|
+
await fs.writeFile(filePath, "alpha\nbeta\ngamma\ndelta", "utf-8");
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const readFile = createFileReadExecutor();
|
|
36
|
+
const result = await readFile(
|
|
37
|
+
{ path: filePath, start_line: 2, end_line: 3 },
|
|
38
|
+
{
|
|
39
|
+
agentId: "agent-1",
|
|
40
|
+
conversationId: "conv-1",
|
|
41
|
+
iteration: 1,
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
expect(result).toBe("2 | beta\n3 | gamma");
|
|
45
|
+
} finally {
|
|
46
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
25
49
|
});
|