@clinebot/core 0.0.7 → 0.0.11

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 (67) hide show
  1. package/dist/auth/cline.d.ts +2 -0
  2. package/dist/auth/codex.d.ts +5 -1
  3. package/dist/auth/oca.d.ts +7 -1
  4. package/dist/auth/types.d.ts +2 -0
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.node.js +124 -122
  7. package/dist/input/mention-enricher.d.ts +1 -0
  8. package/dist/providers/local-provider-service.d.ts +1 -1
  9. package/dist/runtime/session-runtime.d.ts +1 -1
  10. package/dist/session/default-session-manager.d.ts +13 -17
  11. package/dist/session/runtime-oauth-token-manager.d.ts +4 -2
  12. package/dist/session/session-agent-events.d.ts +15 -0
  13. package/dist/session/session-config-builder.d.ts +13 -0
  14. package/dist/session/session-manager.d.ts +2 -2
  15. package/dist/session/session-team-coordination.d.ts +12 -0
  16. package/dist/session/session-telemetry.d.ts +9 -0
  17. package/dist/session/unified-session-persistence-service.d.ts +12 -16
  18. package/dist/session/utils/helpers.d.ts +2 -2
  19. package/dist/session/utils/types.d.ts +2 -1
  20. package/dist/telemetry/core-events.d.ts +122 -0
  21. package/dist/tools/definitions.d.ts +1 -1
  22. package/dist/tools/executors/file-read.d.ts +1 -1
  23. package/dist/tools/index.d.ts +1 -1
  24. package/dist/tools/presets.d.ts +1 -1
  25. package/dist/tools/schemas.d.ts +46 -3
  26. package/dist/tools/types.d.ts +3 -3
  27. package/dist/types/config.d.ts +1 -1
  28. package/dist/types/provider-settings.d.ts +4 -4
  29. package/dist/types.d.ts +1 -1
  30. package/package.json +4 -3
  31. package/src/auth/cline.ts +35 -1
  32. package/src/auth/codex.ts +27 -2
  33. package/src/auth/oca.ts +31 -4
  34. package/src/auth/types.ts +3 -0
  35. package/src/index.ts +27 -0
  36. package/src/input/mention-enricher.test.ts +3 -0
  37. package/src/input/mention-enricher.ts +3 -0
  38. package/src/providers/local-provider-service.ts +6 -7
  39. package/src/runtime/hook-file-hooks.ts +11 -10
  40. package/src/runtime/session-runtime.ts +1 -1
  41. package/src/session/default-session-manager.e2e.test.ts +2 -1
  42. package/src/session/default-session-manager.test.ts +131 -0
  43. package/src/session/default-session-manager.ts +372 -602
  44. package/src/session/runtime-oauth-token-manager.ts +21 -14
  45. package/src/session/session-agent-events.ts +159 -0
  46. package/src/session/session-config-builder.ts +111 -0
  47. package/src/session/session-host.ts +13 -0
  48. package/src/session/session-manager.ts +2 -2
  49. package/src/session/session-team-coordination.ts +198 -0
  50. package/src/session/session-telemetry.ts +95 -0
  51. package/src/session/unified-session-persistence-service.test.ts +81 -0
  52. package/src/session/unified-session-persistence-service.ts +470 -469
  53. package/src/session/utils/helpers.ts +14 -4
  54. package/src/session/utils/types.ts +2 -1
  55. package/src/storage/provider-settings-legacy-migration.ts +3 -3
  56. package/src/telemetry/core-events.ts +344 -0
  57. package/src/tools/definitions.test.ts +121 -7
  58. package/src/tools/definitions.ts +60 -24
  59. package/src/tools/executors/file-read.test.ts +29 -5
  60. package/src/tools/executors/file-read.ts +17 -6
  61. package/src/tools/index.ts +2 -0
  62. package/src/tools/presets.ts +1 -1
  63. package/src/tools/schemas.ts +65 -5
  64. package/src/tools/types.ts +7 -3
  65. package/src/types/config.ts +1 -1
  66. package/src/types/provider-settings.ts +6 -6
  67. package/src/types.ts +1 -1
@@ -1,5 +1,5 @@
1
1
  import type { AgentConfig, AgentEvent, AgentResult } from "@clinebot/agents";
2
- import type { providers as LlmsProviders } from "@clinebot/llms";
2
+ import type { LlmsProviders } from "@clinebot/llms";
3
3
  import type { SessionSource } from "../../types/common";
4
4
  import type { SessionRecord } from "../../types/sessions";
5
5
  import { nowIso } from "../session-artifacts";
@@ -62,10 +62,20 @@ export function serializeAgentEvent(event: AgentEvent): string {
62
62
  export function withLatestAssistantTurnMetadata(
63
63
  messages: LlmsProviders.Message[],
64
64
  result: AgentResult,
65
+ previousMessages: LlmsProviders.MessageWithMetadata[] = [],
65
66
  ): StoredMessageWithMetadata[] {
66
- const next = messages.map((message) => ({
67
- ...message,
68
- })) as StoredMessageWithMetadata[];
67
+ const next = messages.map((message, index) => {
68
+ const previous = previousMessages[index];
69
+ const sameMessage =
70
+ previous?.role === message.role &&
71
+ JSON.stringify(previous.content) === JSON.stringify(message.content);
72
+ return sameMessage
73
+ ? ({
74
+ ...previous,
75
+ ...message,
76
+ } as StoredMessageWithMetadata)
77
+ : ({ ...message } as StoredMessageWithMetadata);
78
+ });
69
79
  const assistantIndex = [...next]
70
80
  .reverse()
71
81
  .findIndex((message) => message.role === "assistant");
@@ -1,5 +1,5 @@
1
1
  import type { Agent } from "@clinebot/agents";
2
- import type { providers as LlmsProviders } from "@clinebot/llms";
2
+ import type { LlmsProviders } from "@clinebot/llms";
3
3
  import type { BuiltRuntime } from "../../runtime/session-runtime";
4
4
  import type { SessionSource } from "../../types/common";
5
5
  import type { CoreSessionConfig } from "../../types/config";
@@ -18,6 +18,7 @@ export type ActiveSession = {
18
18
  started: boolean;
19
19
  aborting: boolean;
20
20
  interactive: boolean;
21
+ persistedMessages?: LlmsProviders.MessageWithMetadata[];
21
22
  activeTeamRunIds: Set<string>;
22
23
  pendingTeamRunUpdates: TeamRunUpdate[];
23
24
  teamRunWaiters: Array<() => void>;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { models, providers } from "@clinebot/llms";
3
+ import { LlmsModels, LlmsProviders } from "@clinebot/llms";
4
4
  import { resolveClineDataDir } from "@clinebot/shared/storage";
5
5
  import type { ProviderSettings } from "../types/provider-settings";
6
6
  import { emptyStoredProviderSettings } from "../types/provider-settings";
@@ -373,7 +373,7 @@ function resolveLegacyCodexAuth(
373
373
  }
374
374
 
375
375
  function getDefaultModelForProvider(providerId: string): string | undefined {
376
- const builtInModels = models.getGeneratedModelsForProvider(providerId);
376
+ const builtInModels = LlmsModels.getGeneratedModelsForProvider(providerId);
377
377
  const firstModelId = Object.keys(builtInModels)[0];
378
378
  return firstModelId ?? undefined;
379
379
  }
@@ -568,7 +568,7 @@ function buildLegacyProviderSettings(
568
568
  ...(timeout ? { timeout } : {}),
569
569
  ...providerSpecific,
570
570
  };
571
- const parsed = providers.ProviderSettingsSchema.safeParse(settings);
571
+ const parsed = LlmsProviders.ProviderSettingsSchema.safeParse(settings);
572
572
  if (!parsed.success) {
573
573
  return undefined;
574
574
  }
@@ -0,0 +1,344 @@
1
+ import type { ITelemetryService, TelemetryProperties } from "@clinebot/shared";
2
+
3
+ const MAX_ERROR_MESSAGE_LENGTH = 500;
4
+
5
+ export const LegacyTelemetryEvents = {
6
+ USER: {
7
+ AUTH_STARTED: "user.auth_started",
8
+ AUTH_SUCCEEDED: "user.auth_succeeded",
9
+ AUTH_FAILED: "user.auth_failed",
10
+ AUTH_LOGGED_OUT: "user.auth_logged_out",
11
+ },
12
+ TASK: {
13
+ CREATED: "task.created",
14
+ RESTARTED: "task.restarted",
15
+ COMPLETED: "task.completed",
16
+ CONVERSATION_TURN: "task.conversation_turn",
17
+ TOKEN_USAGE: "task.tokens",
18
+ MODE_SWITCH: "task.mode",
19
+ TOOL_USED: "task.tool_used",
20
+ SKILL_USED: "task.skill_used",
21
+ DIFF_EDIT_FAILED: "task.diff_edit_failed",
22
+ PROVIDER_API_ERROR: "task.provider_api_error",
23
+ MENTION_USED: "task.mention_used",
24
+ MENTION_FAILED: "task.mention_failed",
25
+ MENTION_SEARCH_RESULTS: "task.mention_search_results",
26
+ SUBAGENT_STARTED: "task.subagent_started",
27
+ SUBAGENT_COMPLETED: "task.subagent_completed",
28
+ },
29
+ HOOKS: {
30
+ DISCOVERY_COMPLETED: "hooks.discovery_completed",
31
+ },
32
+ } as const;
33
+
34
+ function emit(
35
+ telemetry: ITelemetryService | undefined,
36
+ event: string,
37
+ properties?: TelemetryProperties,
38
+ ): void {
39
+ telemetry?.capture({ event, properties });
40
+ }
41
+
42
+ function truncateErrorMessage(errorMessage?: string): string | undefined {
43
+ if (!errorMessage) {
44
+ return undefined;
45
+ }
46
+ return errorMessage.substring(0, MAX_ERROR_MESSAGE_LENGTH);
47
+ }
48
+
49
+ export function captureAuthStarted(
50
+ telemetry: ITelemetryService | undefined,
51
+ provider?: string,
52
+ ): void {
53
+ emit(telemetry, LegacyTelemetryEvents.USER.AUTH_STARTED, { provider });
54
+ }
55
+
56
+ export function captureAuthSucceeded(
57
+ telemetry: ITelemetryService | undefined,
58
+ provider?: string,
59
+ ): void {
60
+ emit(telemetry, LegacyTelemetryEvents.USER.AUTH_SUCCEEDED, { provider });
61
+ }
62
+
63
+ export function captureAuthFailed(
64
+ telemetry: ITelemetryService | undefined,
65
+ provider?: string,
66
+ errorMessage?: string,
67
+ ): void {
68
+ emit(telemetry, LegacyTelemetryEvents.USER.AUTH_FAILED, {
69
+ provider,
70
+ errorMessage: truncateErrorMessage(errorMessage),
71
+ });
72
+ }
73
+
74
+ export function captureAuthLoggedOut(
75
+ telemetry: ITelemetryService | undefined,
76
+ provider?: string,
77
+ reason?: string,
78
+ ): void {
79
+ emit(telemetry, LegacyTelemetryEvents.USER.AUTH_LOGGED_OUT, {
80
+ provider,
81
+ reason,
82
+ });
83
+ }
84
+
85
+ export function identifyAccount(
86
+ telemetry: ITelemetryService | undefined,
87
+ account: {
88
+ id?: string;
89
+ email?: string;
90
+ provider?: string;
91
+ organizationId?: string;
92
+ organizationName?: string;
93
+ memberId?: string;
94
+ },
95
+ ): void {
96
+ const distinctId = account.id?.trim();
97
+ if (distinctId) {
98
+ telemetry?.setDistinctId(distinctId);
99
+ }
100
+ telemetry?.updateCommonProperties({
101
+ account_id: account.id,
102
+ account_email: account.email,
103
+ provider: account.provider,
104
+ organization_id: account.organizationId,
105
+ organization_name: account.organizationName,
106
+ member_id: account.memberId,
107
+ });
108
+ }
109
+
110
+ export function captureTaskCreated(
111
+ telemetry: ITelemetryService | undefined,
112
+ properties: {
113
+ ulid: string;
114
+ apiProvider?: string;
115
+ openAiCompatibleDomain?: string;
116
+ },
117
+ ): void {
118
+ emit(telemetry, LegacyTelemetryEvents.TASK.CREATED, properties);
119
+ }
120
+
121
+ export function captureTaskRestarted(
122
+ telemetry: ITelemetryService | undefined,
123
+ properties: {
124
+ ulid: string;
125
+ apiProvider?: string;
126
+ openAiCompatibleDomain?: string;
127
+ },
128
+ ): void {
129
+ emit(telemetry, LegacyTelemetryEvents.TASK.RESTARTED, properties);
130
+ }
131
+
132
+ export function captureTaskCompleted(
133
+ telemetry: ITelemetryService | undefined,
134
+ properties: {
135
+ ulid: string;
136
+ provider?: string;
137
+ modelId?: string;
138
+ mode?: string;
139
+ durationMs?: number;
140
+ },
141
+ ): void {
142
+ emit(telemetry, LegacyTelemetryEvents.TASK.COMPLETED, properties);
143
+ }
144
+
145
+ export function captureConversationTurnEvent(
146
+ telemetry: ITelemetryService | undefined,
147
+ properties: {
148
+ ulid: string;
149
+ provider?: string;
150
+ model?: string;
151
+ source: "user" | "assistant";
152
+ mode?: string;
153
+ tokensIn?: number;
154
+ tokensOut?: number;
155
+ cacheWriteTokens?: number;
156
+ cacheReadTokens?: number;
157
+ totalCost?: number;
158
+ isNativeToolCall?: boolean;
159
+ },
160
+ ): void {
161
+ emit(telemetry, LegacyTelemetryEvents.TASK.CONVERSATION_TURN, {
162
+ ...properties,
163
+ timestamp: new Date().toISOString(),
164
+ });
165
+ }
166
+
167
+ export function captureTokenUsage(
168
+ telemetry: ITelemetryService | undefined,
169
+ properties: {
170
+ ulid: string;
171
+ tokensIn: number;
172
+ tokensOut: number;
173
+ model: string;
174
+ },
175
+ ): void {
176
+ emit(telemetry, LegacyTelemetryEvents.TASK.TOKEN_USAGE, properties);
177
+ }
178
+
179
+ export function captureModeSwitch(
180
+ telemetry: ITelemetryService | undefined,
181
+ ulid: string,
182
+ mode?: string,
183
+ ): void {
184
+ emit(telemetry, LegacyTelemetryEvents.TASK.MODE_SWITCH, { ulid, mode });
185
+ }
186
+
187
+ export function captureToolUsage(
188
+ telemetry: ITelemetryService | undefined,
189
+ properties: {
190
+ ulid: string;
191
+ tool: string;
192
+ modelId?: string;
193
+ provider?: string;
194
+ autoApproved?: boolean;
195
+ success: boolean;
196
+ isNativeToolCall?: boolean;
197
+ },
198
+ ): void {
199
+ emit(telemetry, LegacyTelemetryEvents.TASK.TOOL_USED, properties);
200
+ }
201
+
202
+ export function captureSkillUsed(
203
+ telemetry: ITelemetryService | undefined,
204
+ properties: {
205
+ ulid: string;
206
+ skillName: string;
207
+ skillSource: "global" | "project";
208
+ skillsAvailableGlobal: number;
209
+ skillsAvailableProject: number;
210
+ provider?: string;
211
+ modelId?: string;
212
+ },
213
+ ): void {
214
+ emit(telemetry, LegacyTelemetryEvents.TASK.SKILL_USED, properties);
215
+ }
216
+
217
+ export function captureDiffEditFailure(
218
+ telemetry: ITelemetryService | undefined,
219
+ properties: {
220
+ ulid: string;
221
+ modelId?: string;
222
+ provider?: string;
223
+ errorType?: string;
224
+ isNativeToolCall?: boolean;
225
+ },
226
+ ): void {
227
+ emit(telemetry, LegacyTelemetryEvents.TASK.DIFF_EDIT_FAILED, properties);
228
+ }
229
+
230
+ export function captureProviderApiError(
231
+ telemetry: ITelemetryService | undefined,
232
+ properties: {
233
+ ulid: string;
234
+ model: string;
235
+ errorMessage: string;
236
+ provider?: string;
237
+ errorStatus?: number;
238
+ requestId?: string;
239
+ isNativeToolCall?: boolean;
240
+ },
241
+ ): void {
242
+ emit(telemetry, LegacyTelemetryEvents.TASK.PROVIDER_API_ERROR, {
243
+ ...properties,
244
+ errorMessage: truncateErrorMessage(properties.errorMessage) ?? "unknown",
245
+ timestamp: new Date().toISOString(),
246
+ });
247
+ }
248
+
249
+ export function captureMentionUsed(
250
+ telemetry: ITelemetryService | undefined,
251
+ mentionType:
252
+ | "file"
253
+ | "folder"
254
+ | "url"
255
+ | "problems"
256
+ | "terminal"
257
+ | "git-changes"
258
+ | "commit",
259
+ contentLength?: number,
260
+ ): void {
261
+ emit(telemetry, LegacyTelemetryEvents.TASK.MENTION_USED, {
262
+ mentionType,
263
+ contentLength,
264
+ timestamp: new Date().toISOString(),
265
+ });
266
+ }
267
+
268
+ export function captureMentionFailed(
269
+ telemetry: ITelemetryService | undefined,
270
+ mentionType:
271
+ | "file"
272
+ | "folder"
273
+ | "url"
274
+ | "problems"
275
+ | "terminal"
276
+ | "git-changes"
277
+ | "commit",
278
+ errorType:
279
+ | "not_found"
280
+ | "permission_denied"
281
+ | "network_error"
282
+ | "parse_error"
283
+ | "unknown",
284
+ errorMessage?: string,
285
+ ): void {
286
+ emit(telemetry, LegacyTelemetryEvents.TASK.MENTION_FAILED, {
287
+ mentionType,
288
+ errorType,
289
+ errorMessage: truncateErrorMessage(errorMessage),
290
+ timestamp: new Date().toISOString(),
291
+ });
292
+ }
293
+
294
+ export function captureMentionSearchResults(
295
+ telemetry: ITelemetryService | undefined,
296
+ query: string,
297
+ resultCount: number,
298
+ searchType: "file" | "folder" | "all",
299
+ isEmpty: boolean,
300
+ ): void {
301
+ emit(telemetry, LegacyTelemetryEvents.TASK.MENTION_SEARCH_RESULTS, {
302
+ queryLength: query.length,
303
+ resultCount,
304
+ searchType,
305
+ isEmpty,
306
+ timestamp: new Date().toISOString(),
307
+ });
308
+ }
309
+
310
+ export function captureSubagentExecution(
311
+ telemetry: ITelemetryService | undefined,
312
+ properties: {
313
+ ulid: string;
314
+ durationMs: number;
315
+ outputLines: number;
316
+ success: boolean;
317
+ },
318
+ ): void {
319
+ emit(
320
+ telemetry,
321
+ properties.success
322
+ ? LegacyTelemetryEvents.TASK.SUBAGENT_COMPLETED
323
+ : LegacyTelemetryEvents.TASK.SUBAGENT_STARTED,
324
+ {
325
+ ...properties,
326
+ timestamp: new Date().toISOString(),
327
+ },
328
+ );
329
+ }
330
+
331
+ export function captureHookDiscovery(
332
+ telemetry: ITelemetryService | undefined,
333
+ hookName: string,
334
+ globalCount: number,
335
+ workspaceCount: number,
336
+ ): void {
337
+ emit(telemetry, LegacyTelemetryEvents.HOOKS.DISCOVERY_COMPLETED, {
338
+ hookName,
339
+ globalCount,
340
+ workspaceCount,
341
+ totalCount: globalCount + workspaceCount,
342
+ timestamp: new Date().toISOString(),
343
+ });
344
+ }
@@ -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", () => {
@@ -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(
@@ -562,7 +598,7 @@ export function createAskQuestionTool(
562
598
  *
563
599
  * const tools = createDefaultTools({
564
600
  * executors: {
565
- * readFile: async (path) => fs.readFile(path, "utf-8"),
601
+ * readFile: async ({ path }) => fs.readFile(path, "utf-8"),
566
602
  * bash: async (cmd, cwd) => {
567
603
  * return new Promise((resolve, reject) => {
568
604
  * exec(cmd, { cwd }, (err, stdout, stderr) => {