@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.
Files changed (83) hide show
  1. package/dist/agents/hooks-config-loader.d.ts +1 -0
  2. package/dist/auth/cline.d.ts +2 -0
  3. package/dist/auth/codex.d.ts +5 -1
  4. package/dist/auth/oca.d.ts +7 -1
  5. package/dist/auth/types.d.ts +2 -0
  6. package/dist/index.d.ts +3 -1
  7. package/dist/index.node.d.ts +2 -0
  8. package/dist/index.node.js +164 -162
  9. package/dist/input/mention-enricher.d.ts +1 -0
  10. package/dist/providers/local-provider-service.d.ts +1 -1
  11. package/dist/runtime/session-runtime.d.ts +1 -1
  12. package/dist/session/default-session-manager.d.ts +13 -17
  13. package/dist/session/rpc-spawn-lease.d.ts +7 -0
  14. package/dist/session/runtime-oauth-token-manager.d.ts +4 -2
  15. package/dist/session/session-agent-events.d.ts +15 -0
  16. package/dist/session/session-config-builder.d.ts +13 -0
  17. package/dist/session/session-manager.d.ts +2 -2
  18. package/dist/session/session-team-coordination.d.ts +12 -0
  19. package/dist/session/session-telemetry.d.ts +9 -0
  20. package/dist/session/unified-session-persistence-service.d.ts +12 -16
  21. package/dist/session/utils/helpers.d.ts +1 -1
  22. package/dist/session/utils/types.d.ts +1 -1
  23. package/dist/storage/provider-settings-legacy-migration.d.ts +25 -0
  24. package/dist/telemetry/core-events.d.ts +122 -0
  25. package/dist/tools/definitions.d.ts +1 -1
  26. package/dist/tools/executors/file-read.d.ts +1 -1
  27. package/dist/tools/index.d.ts +1 -1
  28. package/dist/tools/presets.d.ts +1 -1
  29. package/dist/tools/schemas.d.ts +48 -11
  30. package/dist/tools/types.d.ts +3 -3
  31. package/dist/types/config.d.ts +1 -1
  32. package/dist/types/events.d.ts +1 -1
  33. package/dist/types/provider-settings.d.ts +4 -4
  34. package/dist/types.d.ts +1 -1
  35. package/package.json +4 -3
  36. package/src/agents/hooks-config-loader.ts +2 -0
  37. package/src/auth/cline.ts +35 -1
  38. package/src/auth/codex.ts +27 -2
  39. package/src/auth/oca.ts +31 -4
  40. package/src/auth/types.ts +3 -0
  41. package/src/index.node.ts +4 -0
  42. package/src/index.ts +27 -0
  43. package/src/input/file-indexer.test.ts +40 -0
  44. package/src/input/file-indexer.ts +21 -0
  45. package/src/input/mention-enricher.test.ts +3 -0
  46. package/src/input/mention-enricher.ts +3 -0
  47. package/src/providers/local-provider-service.ts +6 -7
  48. package/src/runtime/hook-file-hooks.test.ts +51 -1
  49. package/src/runtime/hook-file-hooks.ts +91 -11
  50. package/src/runtime/session-runtime.ts +1 -1
  51. package/src/session/default-session-manager.e2e.test.ts +2 -1
  52. package/src/session/default-session-manager.ts +367 -601
  53. package/src/session/rpc-spawn-lease.test.ts +49 -0
  54. package/src/session/rpc-spawn-lease.ts +122 -0
  55. package/src/session/runtime-oauth-token-manager.ts +21 -14
  56. package/src/session/session-agent-events.ts +159 -0
  57. package/src/session/session-config-builder.ts +111 -0
  58. package/src/session/session-graph.ts +2 -0
  59. package/src/session/session-host.ts +21 -0
  60. package/src/session/session-manager.ts +2 -2
  61. package/src/session/session-team-coordination.ts +198 -0
  62. package/src/session/session-telemetry.ts +95 -0
  63. package/src/session/unified-session-persistence-service.test.ts +81 -0
  64. package/src/session/unified-session-persistence-service.ts +470 -469
  65. package/src/session/utils/helpers.ts +1 -1
  66. package/src/session/utils/types.ts +1 -1
  67. package/src/storage/provider-settings-legacy-migration.test.ts +133 -1
  68. package/src/storage/provider-settings-legacy-migration.ts +63 -11
  69. package/src/telemetry/core-events.ts +344 -0
  70. package/src/tools/definitions.test.ts +203 -36
  71. package/src/tools/definitions.ts +66 -28
  72. package/src/tools/executors/editor.test.ts +35 -0
  73. package/src/tools/executors/editor.ts +33 -46
  74. package/src/tools/executors/file-read.test.ts +29 -5
  75. package/src/tools/executors/file-read.ts +17 -6
  76. package/src/tools/index.ts +2 -0
  77. package/src/tools/presets.ts +1 -1
  78. package/src/tools/schemas.ts +88 -38
  79. package/src/tools/types.ts +7 -3
  80. package/src/types/config.ts +1 -1
  81. package/src/types/events.ts +6 -1
  82. package/src/types/provider-settings.ts +6 -6
  83. package/src/types.ts +1 -1
@@ -1,5 +1,9 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
- import { createDefaultTools, createReadFilesTool } from "./definitions.js";
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.file_paths).toEqual({
321
+ expect(properties.files).toMatchObject({
223
322
  type: "array",
224
323
  items: {
225
- type: "string",
226
- description:
227
- "The absolute file path of a text file to read content from",
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 absolute file paths to get full content from. Prefer this tool over running terminal command to get file content for better performance and reliability.",
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(["file_paths"]);
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 null for unused optional fields on str_replace", async () => {
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
- old_str: "before",
290
- new_str: "after",
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: "str_replace:/tmp/example.ts",
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
- old_str: "before",
311
- new_str: "after",
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("still rejects null for required insert fields", async () => {
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 expect(
346
- editorTool.execute(
347
- {
348
- command: "insert",
349
- path: "/tmp/example.ts",
350
- new_str: "after",
351
- insert_line: null,
352
- },
353
- {
354
- agentId: "agent-1",
355
- conversationId: "conv-1",
356
- iteration: 1,
357
- },
358
- ),
359
- ).rejects.toThrow(/insert_line is required for command=insert/);
360
- expect(execute).not.toHaveBeenCalled();
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
  });
@@ -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 FULL content of text file at the provided absolute paths. " +
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
- // Validate input with Zod schema
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
- filePaths.map(async (filePath): Promise<ToolOperationResult> => {
201
+ requests.map(async (request): Promise<ToolOperationResult> => {
167
202
  try {
168
203
  const content = await withTimeout(
169
- executor(filePath, context),
204
+ executor(request, context),
170
205
  timeoutMs,
171
206
  `File read timed out after ${timeoutMs}ms`,
172
207
  );
173
208
  return {
174
- query: filePath,
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: filePath,
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 validatedInput = validateWithZod(
218
- SearchCodebaseUnionInputSchema,
219
- input,
220
- );
221
- const queries = Array.isArray(validatedInput)
222
- ? validatedInput
223
- : typeof validatedInput === "object"
224
- ? validatedInput.queries
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
- "Edit file using absolute path with create, string replacement, and line insert operations. " +
436
- "Supported commands: create, str_replace, insert.",
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: `${validatedInput.command}:${validatedInput.path}`,
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: `${validatedInput.command}:${validatedInput.path}`,
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
- switch (input.command) {
185
- case "create":
186
- if (input.file_text == null) {
187
- throw new Error(
188
- "Parameter `file_text` is required for command: create",
189
- );
190
- }
191
- return createFile(filePath, input.file_text, encoding);
192
-
193
- case "str_replace":
194
- if (input.old_str == null) {
195
- throw new Error(
196
- "Parameter `old_str` is required for command: str_replace",
197
- );
198
- }
199
- return replaceInFile(
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(filePath, {
16
- agentId: "agent-1",
17
- conversationId: "conv-1",
18
- iteration: 1,
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
  });