@clinebot/core 0.0.5 → 0.0.7

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 (57) hide show
  1. package/dist/agents/hooks-config-loader.d.ts +1 -0
  2. package/dist/index.d.ts +4 -1
  3. package/dist/index.node.d.ts +2 -0
  4. package/dist/index.node.js +134 -107
  5. package/dist/runtime/session-runtime.d.ts +3 -1
  6. package/dist/session/default-session-manager.d.ts +4 -0
  7. package/dist/session/rpc-spawn-lease.d.ts +7 -0
  8. package/dist/session/session-host.d.ts +2 -0
  9. package/dist/session/session-manager.d.ts +1 -0
  10. package/dist/storage/provider-settings-legacy-migration.d.ts +25 -0
  11. package/dist/telemetry/ITelemetryAdapter.d.ts +54 -0
  12. package/dist/telemetry/LoggerTelemetryAdapter.d.ts +21 -0
  13. package/dist/telemetry/OpenTelemetryAdapter.d.ts +43 -0
  14. package/dist/telemetry/OpenTelemetryProvider.d.ts +41 -0
  15. package/dist/telemetry/TelemetryService.d.ts +34 -0
  16. package/dist/telemetry/opentelemetry.d.ts +3 -0
  17. package/dist/telemetry/opentelemetry.js +27 -0
  18. package/dist/tools/schemas.d.ts +8 -8
  19. package/dist/types/config.d.ts +2 -1
  20. package/dist/types/events.d.ts +1 -1
  21. package/package.json +16 -3
  22. package/src/agents/hooks-config-loader.ts +21 -1
  23. package/src/index.node.ts +7 -0
  24. package/src/index.ts +16 -0
  25. package/src/input/file-indexer.test.ts +40 -0
  26. package/src/input/file-indexer.ts +21 -0
  27. package/src/runtime/hook-file-hooks.test.ts +98 -1
  28. package/src/runtime/hook-file-hooks.ts +93 -11
  29. package/src/runtime/runtime-builder.test.ts +20 -0
  30. package/src/runtime/runtime-builder.ts +1 -0
  31. package/src/runtime/session-runtime.ts +3 -1
  32. package/src/session/default-session-manager.test.ts +72 -0
  33. package/src/session/default-session-manager.ts +59 -1
  34. package/src/session/rpc-spawn-lease.test.ts +49 -0
  35. package/src/session/rpc-spawn-lease.ts +122 -0
  36. package/src/session/session-graph.ts +2 -0
  37. package/src/session/session-host.ts +14 -1
  38. package/src/session/session-manager.ts +1 -0
  39. package/src/storage/provider-settings-legacy-migration.test.ts +133 -1
  40. package/src/storage/provider-settings-legacy-migration.ts +60 -8
  41. package/src/telemetry/ITelemetryAdapter.ts +94 -0
  42. package/src/telemetry/LoggerTelemetryAdapter.test.ts +42 -0
  43. package/src/telemetry/LoggerTelemetryAdapter.ts +114 -0
  44. package/src/telemetry/OpenTelemetryAdapter.test.ts +157 -0
  45. package/src/telemetry/OpenTelemetryAdapter.ts +348 -0
  46. package/src/telemetry/OpenTelemetryProvider.test.ts +113 -0
  47. package/src/telemetry/OpenTelemetryProvider.ts +322 -0
  48. package/src/telemetry/TelemetryService.test.ts +134 -0
  49. package/src/telemetry/TelemetryService.ts +141 -0
  50. package/src/telemetry/opentelemetry.ts +20 -0
  51. package/src/tools/definitions.test.ts +82 -29
  52. package/src/tools/definitions.ts +41 -32
  53. package/src/tools/executors/editor.test.ts +35 -0
  54. package/src/tools/executors/editor.ts +33 -46
  55. package/src/tools/schemas.ts +34 -35
  56. package/src/types/config.ts +2 -0
  57. package/src/types/events.ts +6 -1
@@ -261,7 +261,7 @@ describe("zod schema conversion", () => {
261
261
  });
262
262
 
263
263
  describe("default editor tool", () => {
264
- it("accepts null for unused optional fields on str_replace", async () => {
264
+ it("accepts replacement edits without insert fields", async () => {
265
265
  const execute = vi.fn(async () => "patched");
266
266
  const tools = createDefaultTools({
267
267
  executors: {
@@ -284,12 +284,9 @@ describe("default editor tool", () => {
284
284
 
285
285
  const result = await editorTool.execute(
286
286
  {
287
- command: "str_replace",
288
287
  path: "/tmp/example.ts",
289
- old_str: "before",
290
- new_str: "after",
291
- file_text: null,
292
- insert_line: null,
288
+ old_text: "before",
289
+ new_text: "after",
293
290
  },
294
291
  {
295
292
  agentId: "agent-1",
@@ -299,18 +296,15 @@ describe("default editor tool", () => {
299
296
  );
300
297
 
301
298
  expect(result).toEqual({
302
- query: "str_replace:/tmp/example.ts",
299
+ query: "edit:/tmp/example.ts",
303
300
  result: "patched",
304
301
  success: true,
305
302
  });
306
303
  expect(execute).toHaveBeenCalledWith(
307
304
  expect.objectContaining({
308
- command: "str_replace",
309
305
  path: "/tmp/example.ts",
310
- old_str: "before",
311
- new_str: "after",
312
- file_text: null,
313
- insert_line: null,
306
+ old_text: "before",
307
+ new_text: "after",
314
308
  }),
315
309
  process.cwd(),
316
310
  expect.objectContaining({
@@ -321,7 +315,7 @@ describe("default editor tool", () => {
321
315
  );
322
316
  });
323
317
 
324
- it("still rejects null for required insert fields", async () => {
318
+ it("allows edit without old_text so missing files can be created", async () => {
325
319
  const execute = vi.fn(async () => "patched");
326
320
  const tools = createDefaultTools({
327
321
  executors: {
@@ -342,21 +336,80 @@ describe("default editor tool", () => {
342
336
  throw new Error("Expected editor tool to be defined.");
343
337
  }
344
338
 
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();
339
+ const result = await editorTool.execute(
340
+ {
341
+ path: "/tmp/example.ts",
342
+ new_text: "created",
343
+ },
344
+ {
345
+ agentId: "agent-1",
346
+ conversationId: "conv-1",
347
+ iteration: 1,
348
+ },
349
+ );
350
+
351
+ expect(result).toEqual({
352
+ query: "edit:/tmp/example.ts",
353
+ result: "patched",
354
+ success: true,
355
+ });
356
+ expect(execute).toHaveBeenCalledWith(
357
+ expect.objectContaining({
358
+ path: "/tmp/example.ts",
359
+ new_text: "created",
360
+ }),
361
+ process.cwd(),
362
+ expect.anything(),
363
+ );
364
+ });
365
+
366
+ it("treats insert_line as an insert operation", async () => {
367
+ const execute = vi.fn(async () => "patched");
368
+ const tools = createDefaultTools({
369
+ executors: {
370
+ editor: execute,
371
+ },
372
+ enableReadFiles: false,
373
+ enableSearch: false,
374
+ enableBash: false,
375
+ enableWebFetch: false,
376
+ enableSkills: false,
377
+ enableAskQuestion: false,
378
+ enableApplyPatch: false,
379
+ enableEditor: true,
380
+ });
381
+ const editorTool = tools.find((tool) => tool.name === "editor");
382
+ expect(editorTool).toBeDefined();
383
+ if (!editorTool) {
384
+ throw new Error("Expected editor tool to be defined.");
385
+ }
386
+
387
+ const result = await editorTool.execute(
388
+ {
389
+ path: "/tmp/example.ts",
390
+ new_text: "after",
391
+ insert_line: 3,
392
+ },
393
+ {
394
+ agentId: "agent-1",
395
+ conversationId: "conv-1",
396
+ iteration: 1,
397
+ },
398
+ );
399
+
400
+ expect(result).toEqual({
401
+ query: "insert:/tmp/example.ts",
402
+ result: "patched",
403
+ success: true,
404
+ });
405
+ expect(execute).toHaveBeenCalledWith(
406
+ expect.objectContaining({
407
+ path: "/tmp/example.ts",
408
+ new_text: "after",
409
+ insert_line: 3,
410
+ }),
411
+ process.cwd(),
412
+ expect.anything(),
413
+ );
361
414
  });
362
415
  });
@@ -24,6 +24,7 @@ import {
24
24
  RunCommandsInputUnionSchema,
25
25
  type SearchCodebaseInput,
26
26
  SearchCodebaseInputSchema,
27
+ SearchCodebaseUnionInputSchema,
27
28
  type SkillsInput,
28
29
  SkillsInputSchema,
29
30
  } from "./schemas.js";
@@ -213,36 +214,42 @@ export function createSearchTool(
213
214
  maxRetries: 1,
214
215
  execute: async (input, context) => {
215
216
  // Validate input with Zod schema
216
- const validatedInput = validateWithZod(SearchCodebaseInputSchema, input);
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];
217
226
 
218
227
  return Promise.all(
219
- validatedInput.queries.map(
220
- async (query): Promise<ToolOperationResult> => {
221
- try {
222
- const results = await withTimeout(
223
- executor(query, cwd, context),
224
- timeoutMs,
225
- `Search timed out after ${timeoutMs}ms`,
226
- );
227
- // Check if results contain matches
228
- const hasResults =
229
- results.length > 0 && !results.includes("No results found");
230
- return {
231
- query,
232
- result: results,
233
- success: hasResults,
234
- };
235
- } catch (error) {
236
- const msg = formatError(error);
237
- return {
238
- query,
239
- result: "",
240
- error: `Search failed: ${msg}`,
241
- success: false,
242
- };
243
- }
244
- },
245
- ),
228
+ queries.map(async (query): Promise<ToolOperationResult> => {
229
+ try {
230
+ const results = await withTimeout(
231
+ executor(query, cwd, context),
232
+ timeoutMs,
233
+ `Search timed out after ${timeoutMs}ms`,
234
+ );
235
+ // Check if results contain matches
236
+ const hasResults =
237
+ results.length > 0 && !results.includes("No results found");
238
+ return {
239
+ query,
240
+ result: results,
241
+ success: hasResults,
242
+ };
243
+ } catch (error) {
244
+ const msg = formatError(error);
245
+ return {
246
+ query,
247
+ result: "",
248
+ error: `Search failed: ${msg}`,
249
+ success: false,
250
+ };
251
+ }
252
+ }),
246
253
  );
247
254
  },
248
255
  });
@@ -425,14 +432,16 @@ export function createEditorTool(
425
432
  return createTool<EditFileInput, ToolOperationResult>({
426
433
  name: "editor",
427
434
  description:
428
- "Edit file using absolute path with create, string replacement, and line insert operations. " +
429
- "Supported commands: create, str_replace, insert.",
435
+ "An editor for controlled filesystem edits on the text file at the provided path. " +
436
+ "Provide `insert_line` to insert `new_text` at a specific line number. " +
437
+ "Otherwise, the tool replaces `old_text` with `new_text`, or creates the file with `new_text` if it does not exist.",
430
438
  inputSchema: zodToJsonSchema(EditFileInputSchema),
431
439
  timeoutMs,
432
440
  retryable: false, // Editing operations are stateful and should not auto-retry
433
441
  maxRetries: 0,
434
442
  execute: async (input, context) => {
435
443
  const validatedInput = validateWithZod(EditFileInputSchema, input);
444
+ const operation = validatedInput.insert_line == null ? "edit" : "insert";
436
445
 
437
446
  try {
438
447
  const result = await withTimeout(
@@ -442,14 +451,14 @@ export function createEditorTool(
442
451
  );
443
452
 
444
453
  return {
445
- query: `${validatedInput.command}:${validatedInput.path}`,
454
+ query: `${operation}:${validatedInput.path}`,
446
455
  result,
447
456
  success: true,
448
457
  };
449
458
  } catch (error) {
450
459
  const msg = formatError(error);
451
460
  return {
452
- query: `${validatedInput.command}:${validatedInput.path}`,
461
+ query: `${operation}:${validatedInput.path}`,
453
462
  result: "",
454
463
  error: `Editor operation failed: ${msg}`,
455
464
  success: false,
@@ -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
  }
@@ -43,14 +43,25 @@ export const SearchCodebaseInputSchema = z.object({
43
43
  .describe("Array of regex search queries to execute"),
44
44
  });
45
45
 
46
- const CommandInputSchema = z.string();
46
+ /**
47
+ * Union schema for search_codebase tool input, allowing either a single string, an array of strings, or the full object schema
48
+ */
49
+ export const SearchCodebaseUnionInputSchema = z.union([
50
+ SearchCodebaseInputSchema,
51
+ z.array(z.string()),
52
+ z.string(),
53
+ ]);
54
+
55
+ const CommandInputSchema = z
56
+ .string()
57
+ .describe("The non-interactive shell command to execute");
47
58
  /**
48
59
  * Schema for run_commands tool input
49
60
  */
50
61
  export const RunCommandsInputSchema = z.object({
51
62
  commands: z
52
63
  .array(CommandInputSchema)
53
- .describe("Array of shell commands to execute."),
64
+ .describe("Array of shell commands to execute"),
54
65
  });
55
66
 
56
67
  /**
@@ -58,8 +69,8 @@ export const RunCommandsInputSchema = z.object({
58
69
  */
59
70
  export const RunCommandsInputUnionSchema = z.union([
60
71
  RunCommandsInputSchema,
61
- z.array(CommandInputSchema),
62
- CommandInputSchema,
72
+ z.array(z.string()),
73
+ z.string(),
63
74
  ]);
64
75
 
65
76
  /**
@@ -84,46 +95,34 @@ export const FetchWebContentInputSchema = z.object({
84
95
  */
85
96
  export const EditFileInputSchema = z
86
97
  .object({
87
- command: z
88
- .enum(["create", "str_replace", "insert"])
89
- .describe("Editor command to execute: create, str_replace, insert"),
90
- path: z.string().min(1).describe("Absolute file path"),
91
- file_text: z
98
+ path: z
92
99
  .string()
93
- .nullish()
94
- .describe("Full file content required for 'create' command"),
95
- old_str: z
100
+ .min(1)
101
+ .describe("The absolute file path for the action to be performed on"),
102
+ old_text: z
96
103
  .string()
97
- .nullish()
104
+ .nullable()
105
+ .optional()
98
106
  .describe(
99
- "Exact text to replace (must match exactly once) for 'str_replace' command",
107
+ "Exact text to replace (must match exactly once). Omit this when creating a missing file or inserting via insert_line.",
100
108
  ),
101
- new_str: z
109
+ new_text: z
102
110
  .string()
103
- .nullish()
104
- .describe("Replacement text for 'str_replace' or 'insert' commands"),
111
+ .describe(
112
+ "The new content to write when creating a missing file, the replacement text for edits, or the inserted text when insert_line is provided",
113
+ ),
105
114
  insert_line: z
106
115
  .number()
107
116
  .int()
108
- .nullish()
109
- .describe("Optional one-based line index for 'insert' command"),
110
- })
111
- .refine((v) => v.command !== "create" || v.file_text != null, {
112
- path: ["file_text"],
113
- message: "file_text is required for command=create",
114
- })
115
- .refine((v) => v.command !== "str_replace" || v.old_str != null, {
116
- path: ["old_str"],
117
- message: "old_str is required for command=str_replace",
118
- })
119
- .refine((v) => v.command !== "insert" || v.insert_line != null, {
120
- path: ["insert_line"],
121
- message: "insert_line is required for command=insert",
117
+ .nullable()
118
+ .optional()
119
+ .describe(
120
+ "Optional one-based line index. When provided, the tool inserts new_text at that line instead of performing a replacement edit.",
121
+ ),
122
122
  })
123
- .refine((v) => v.command !== "insert" || v.new_str != null, {
124
- path: ["new_str"],
125
- message: "new_str is required for command=insert",
126
- });
123
+ .describe(
124
+ "Edit a text file by replacing old_text with new_text, create the file with new_text if it does not exist, or insert new_text at insert_line when insert_line is provided. IMPORTANT: large edits can time out, so use small chunks and multiple calls when possible.",
125
+ );
127
126
 
128
127
  /**
129
128
  * Schema for apply_patch tool input
@@ -11,6 +11,7 @@ import type { providers as LlmsProviders } from "@clinebot/llms";
11
11
  import type {
12
12
  AgentMode,
13
13
  BasicLogger,
14
+ ITelemetryService,
14
15
  SessionExecutionConfig,
15
16
  SessionPromptConfig,
16
17
  SessionWorkspaceConfig,
@@ -64,6 +65,7 @@ export interface CoreSessionConfig
64
65
  hooks?: AgentHooks;
65
66
  hookErrorMode?: HookErrorMode;
66
67
  logger?: BasicLogger;
68
+ telemetry?: ITelemetryService;
67
69
  extraTools?: Tool[];
68
70
  pluginPaths?: string[];
69
71
  extensions?: AgentConfig["extensions"];
@@ -13,7 +13,12 @@ export interface SessionEndedEvent {
13
13
 
14
14
  export interface SessionToolEvent {
15
15
  sessionId: string;
16
- hookEventName: "tool_call" | "tool_result" | "agent_end" | "session_shutdown";
16
+ hookEventName:
17
+ | "tool_call"
18
+ | "tool_result"
19
+ | "agent_end"
20
+ | "agent_error"
21
+ | "session_shutdown";
17
22
  agentId?: string;
18
23
  conversationId?: string;
19
24
  parentAgentId?: string;