@clinebot/core 0.0.0 → 0.0.2

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 (55) hide show
  1. package/README.md +1 -1
  2. package/dist/default-tools/index.d.ts +1 -0
  3. package/dist/default-tools/model-tool-routing.d.ts +33 -0
  4. package/dist/default-tools/schemas.d.ts +13 -7
  5. package/dist/index.browser.d.ts +1 -0
  6. package/dist/index.browser.js +220 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.node.d.ts +1 -0
  9. package/dist/index.node.js +220 -0
  10. package/dist/server/index.d.ts +5 -3
  11. package/dist/server/index.js +244 -192
  12. package/dist/session/default-session-manager.d.ts +3 -1
  13. package/dist/session/session-host.d.ts +2 -2
  14. package/dist/session/session-manager.d.ts +8 -0
  15. package/dist/session/unified-session-persistence-service.d.ts +1 -1
  16. package/dist/session/utils/helpers.d.ts +11 -0
  17. package/dist/session/utils/types.d.ts +42 -0
  18. package/dist/session/utils/usage.d.ts +9 -0
  19. package/dist/storage/provider-settings-manager.d.ts +2 -0
  20. package/dist/types/config.d.ts +8 -1
  21. package/dist/types.d.ts +1 -1
  22. package/package.json +19 -20
  23. package/src/default-tools/definitions.test.ts +130 -1
  24. package/src/default-tools/definitions.ts +6 -2
  25. package/src/default-tools/executors/editor.ts +10 -9
  26. package/src/default-tools/executors/file-read.test.ts +1 -1
  27. package/src/default-tools/executors/file-read.ts +11 -6
  28. package/src/default-tools/index.ts +5 -0
  29. package/src/default-tools/model-tool-routing.test.ts +86 -0
  30. package/src/default-tools/model-tool-routing.ts +132 -0
  31. package/src/default-tools/schemas.ts +49 -52
  32. package/src/index.browser.ts +1 -0
  33. package/src/index.node.ts +1 -0
  34. package/src/index.ts +41 -2
  35. package/src/input/file-indexer.ts +28 -2
  36. package/src/runtime/runtime-builder.test.ts +69 -0
  37. package/src/runtime/runtime-builder.ts +20 -0
  38. package/src/runtime/runtime-parity.test.ts +20 -9
  39. package/src/server/index.ts +40 -1
  40. package/src/session/default-session-manager.e2e.test.ts +11 -1
  41. package/src/session/default-session-manager.test.ts +270 -0
  42. package/src/session/default-session-manager.ts +109 -191
  43. package/src/session/index.ts +7 -2
  44. package/src/session/session-host.ts +30 -18
  45. package/src/session/session-manager.ts +11 -0
  46. package/src/session/unified-session-persistence-service.ts +11 -5
  47. package/src/session/utils/helpers.ts +148 -0
  48. package/src/session/utils/types.ts +46 -0
  49. package/src/session/utils/usage.ts +32 -0
  50. package/src/storage/provider-settings-legacy-migration.test.ts +3 -3
  51. package/src/storage/provider-settings-manager.test.ts +34 -0
  52. package/src/storage/provider-settings-manager.ts +22 -1
  53. package/src/types/config.ts +13 -0
  54. package/src/types.ts +1 -0
  55. package/dist/index.js +0 -220
@@ -7,7 +7,7 @@ import type { CoreSessionEvent } from "../types/events";
7
7
  import type { SessionRecord } from "../types/sessions";
8
8
  import type { RpcCoreSessionService } from "./rpc-session-service";
9
9
  import { RuntimeOAuthTokenManager } from "./runtime-oauth-token-manager";
10
- import type { SendSessionInput, SessionManager, StartSessionInput, StartSessionResult } from "./session-manager";
10
+ import type { SendSessionInput, SessionAccumulatedUsage, SessionManager, StartSessionInput, StartSessionResult } from "./session-manager";
11
11
  import type { CoreSessionService } from "./session-service";
12
12
  type SessionBackend = CoreSessionService | RpcCoreSessionService;
13
13
  export interface DefaultSessionManagerOptions {
@@ -32,11 +32,13 @@ export declare class DefaultSessionManager implements SessionManager {
32
32
  private readonly defaultRequestToolApproval?;
33
33
  private readonly listeners;
34
34
  private readonly sessions;
35
+ private readonly usageBySession;
35
36
  constructor(options: DefaultSessionManagerOptions);
36
37
  private resolveStoredProviderSettings;
37
38
  private buildResolvedProviderConfig;
38
39
  start(input: StartSessionInput): Promise<StartSessionResult>;
39
40
  send(input: SendSessionInput): Promise<AgentResult | undefined>;
41
+ getAccumulatedUsage(sessionId: string): Promise<SessionAccumulatedUsage | undefined>;
40
42
  abort(sessionId: string): Promise<void>;
41
43
  stop(sessionId: string): Promise<void>;
42
44
  dispose(reason?: string): Promise<void>;
@@ -3,7 +3,7 @@ import type { ToolExecutors } from "../default-tools";
3
3
  import { RpcCoreSessionService } from "./rpc-session-service";
4
4
  import type { SessionManager } from "./session-manager";
5
5
  import { CoreSessionService } from "./session-service";
6
- type SessionBackend = RpcCoreSessionService | CoreSessionService;
6
+ export type SessionBackend = RpcCoreSessionService | CoreSessionService;
7
7
  export interface CreateSessionHostOptions {
8
8
  distinctId?: string;
9
9
  sessionService?: SessionBackend;
@@ -17,5 +17,5 @@ export interface CreateSessionHostOptions {
17
17
  requestToolApproval?: (request: ToolApprovalRequest) => Promise<ToolApprovalResult>;
18
18
  }
19
19
  export type SessionHost = SessionManager;
20
+ export declare function resolveSessionBackend(options: CreateSessionHostOptions): Promise<SessionBackend>;
20
21
  export declare function createSessionHost(options: CreateSessionHostOptions): Promise<SessionHost>;
21
- export {};
@@ -34,9 +34,17 @@ export interface SendSessionInput {
34
34
  userImages?: string[];
35
35
  userFiles?: string[];
36
36
  }
37
+ export interface SessionAccumulatedUsage {
38
+ inputTokens: number;
39
+ outputTokens: number;
40
+ cacheReadTokens: number;
41
+ cacheWriteTokens: number;
42
+ totalCost: number;
43
+ }
37
44
  export interface SessionManager {
38
45
  start(input: StartSessionInput): Promise<StartSessionResult>;
39
46
  send(input: SendSessionInput): Promise<AgentResult | undefined>;
47
+ getAccumulatedUsage(sessionId: string): Promise<SessionAccumulatedUsage | undefined>;
40
48
  abort(sessionId: string): Promise<void>;
41
49
  stop(sessionId: string): Promise<void>;
42
50
  dispose(reason?: string): Promise<void>;
@@ -76,7 +76,7 @@ export declare class UnifiedSessionPersistenceService {
76
76
  upsertSubagentSessionFromHook(event: HookEventPayload): Promise<string | undefined>;
77
77
  appendSubagentHookAudit(subSessionId: string, event: HookEventPayload): Promise<void>;
78
78
  appendSubagentTranscriptLine(subSessionId: string, line: string): Promise<void>;
79
- persistSessionMessages(sessionId: string, messages: LlmsProviders.Message[]): Promise<void>;
79
+ persistSessionMessages(sessionId: string, messages: LlmsProviders.Message[], systemPrompt?: string): Promise<void>;
80
80
  applySubagentStatus(subSessionId: string, event: HookEventPayload): Promise<void>;
81
81
  applySubagentStatusBySessionId(subSessionId: string, status: SessionStatus): Promise<void>;
82
82
  applyStatusToRunningChildSessions(parentSessionId: string, status: Exclude<SessionStatus, "running">): Promise<void>;
@@ -0,0 +1,11 @@
1
+ import type { AgentConfig, AgentEvent, AgentResult } from "@clinebot/agents";
2
+ import type { providers as LlmsProviders } from "@clinebot/llms";
3
+ import type { SessionRecord } from "../../types/sessions";
4
+ import type { SessionRowShape } from "../session-service";
5
+ import type { StoredMessageWithMetadata } from "./types";
6
+ export declare function extractWorkspaceMetadataFromSystemPrompt(systemPrompt: string): string | undefined;
7
+ export declare function hasRuntimeHooks(hooks: AgentConfig["hooks"]): boolean;
8
+ export declare function mergeAgentExtensions(explicitExtensions: AgentConfig["extensions"] | undefined, loadedExtensions: AgentConfig["extensions"] | undefined): AgentConfig["extensions"];
9
+ export declare function serializeAgentEvent(event: AgentEvent): string;
10
+ export declare function withLatestAssistantTurnMetadata(messages: LlmsProviders.Message[], result: AgentResult): StoredMessageWithMetadata[];
11
+ export declare function toSessionRecord(row: SessionRowShape): SessionRecord;
@@ -0,0 +1,42 @@
1
+ import type { Agent } from "@clinebot/agents";
2
+ import type { providers as LlmsProviders } from "@clinebot/llms";
3
+ import type { BuiltRuntime } from "../../runtime/session-runtime";
4
+ import type { SessionSource } from "../../types/common";
5
+ import type { CoreSessionConfig } from "../../types/config";
6
+ import type { SessionAccumulatedUsage } from "../session-manager";
7
+ import type { RootSessionArtifacts } from "../session-service";
8
+ export type ActiveSession = {
9
+ sessionId: string;
10
+ config: CoreSessionConfig;
11
+ artifacts?: RootSessionArtifacts;
12
+ source: SessionSource;
13
+ startedAt: string;
14
+ pendingPrompt?: string;
15
+ runtime: BuiltRuntime;
16
+ agent: Agent;
17
+ started: boolean;
18
+ aborting: boolean;
19
+ interactive: boolean;
20
+ activeTeamRunIds: Set<string>;
21
+ pendingTeamRunUpdates: TeamRunUpdate[];
22
+ teamRunWaiters: Array<() => void>;
23
+ pluginSandboxShutdown?: () => Promise<void>;
24
+ turnUsageBaseline?: SessionAccumulatedUsage;
25
+ };
26
+ export type TeamRunUpdate = {
27
+ runId: string;
28
+ agentId: string;
29
+ taskId?: string;
30
+ status: "completed" | "failed" | "cancelled" | "interrupted";
31
+ error?: string;
32
+ iterations?: number;
33
+ };
34
+ export type StoredMessageWithMetadata = LlmsProviders.MessageWithMetadata & {
35
+ providerId?: string;
36
+ modelId?: string;
37
+ };
38
+ export type PreparedTurnInput = {
39
+ prompt: string;
40
+ userImages?: string[];
41
+ userFiles?: string[];
42
+ };
@@ -0,0 +1,9 @@
1
+ import type { SessionAccumulatedUsage } from "../session-manager";
2
+ export declare function createInitialAccumulatedUsage(): SessionAccumulatedUsage;
3
+ export declare function accumulateUsageTotals(baseline: SessionAccumulatedUsage, usage: {
4
+ inputTokens?: number;
5
+ outputTokens?: number;
6
+ cacheReadTokens?: number;
7
+ cacheWriteTokens?: number;
8
+ totalCost?: number;
9
+ }): SessionAccumulatedUsage;
@@ -1,6 +1,7 @@
1
1
  import { type ProviderConfig, type ProviderSettings, type ProviderTokenSource, type StoredProviderSettings } from "../types/provider-settings";
2
2
  export interface ProviderSettingsManagerOptions {
3
3
  filePath?: string;
4
+ dataDir?: string;
4
5
  }
5
6
  export interface SaveProviderSettingsOptions {
6
7
  setLastUsed?: boolean;
@@ -8,6 +9,7 @@ export interface SaveProviderSettingsOptions {
8
9
  }
9
10
  export declare class ProviderSettingsManager {
10
11
  private readonly filePath;
12
+ private readonly dataDir?;
11
13
  constructor(options?: ProviderSettingsManagerOptions);
12
14
  getFilePath(): string;
13
15
  read(): StoredProviderSettings;
@@ -1,6 +1,7 @@
1
- import type { AgentConfig, AgentHooks, HookErrorMode, TeamEvent, Tool } from "@clinebot/agents";
1
+ import type { AgentConfig, AgentHooks, ConsecutiveMistakeLimitContext, ConsecutiveMistakeLimitDecision, HookErrorMode, TeamEvent, Tool } from "@clinebot/agents";
2
2
  import type { providers as LlmsProviders } from "@clinebot/llms";
3
3
  import type { AgentMode, BasicLogger, SessionExecutionConfig, SessionPromptConfig, SessionWorkspaceConfig } from "@clinebot/shared";
4
+ import type { ToolRoutingRule } from "../default-tools/model-tool-routing.js";
4
5
  export type CoreAgentMode = AgentMode;
5
6
  export interface CoreModelConfig {
6
7
  providerId: string;
@@ -14,6 +15,10 @@ export interface CoreModelConfig {
14
15
  * Request model-side thinking/reasoning when supported.
15
16
  */
16
17
  thinking?: boolean;
18
+ /**
19
+ * Explicit reasoning effort override for capable models.
20
+ */
21
+ reasoningEffort?: LlmsProviders.ProviderConfig["reasoningEffort"];
17
22
  }
18
23
  export interface CoreRuntimeFeatures {
19
24
  enableTools: boolean;
@@ -34,4 +39,6 @@ export interface CoreSessionConfig extends CoreModelConfig, CoreRuntimeFeatures,
34
39
  pluginPaths?: string[];
35
40
  extensions?: AgentConfig["extensions"];
36
41
  onTeamEvent?: (event: TeamEvent) => void;
42
+ onConsecutiveMistakeLimitReached?: (context: ConsecutiveMistakeLimitContext) => Promise<ConsecutiveMistakeLimitDecision> | ConsecutiveMistakeLimitDecision;
43
+ toolRoutingRules?: ToolRoutingRule[];
37
44
  }
package/dist/types.d.ts CHANGED
@@ -7,7 +7,7 @@ export type { SandboxCallOptions, SubprocessSandboxOptions, } from "./runtime/sa
7
7
  export { SubprocessSandbox } from "./runtime/sandbox/subprocess-sandbox";
8
8
  export type { BuiltRuntime as RuntimeEnvironment, RuntimeBuilder, RuntimeBuilderInput, SessionRuntime, } from "./runtime/session-runtime";
9
9
  export type { CreateSessionHostOptions, SessionHost, } from "./session/session-host";
10
- export type { SendSessionInput, SessionManager, StartSessionInput, StartSessionResult, } from "./session/session-manager";
10
+ export type { SendSessionInput, SessionAccumulatedUsage, SessionManager, StartSessionInput, StartSessionResult, } from "./session/session-manager";
11
11
  export type { SessionManifest } from "./session/session-manifest";
12
12
  export type { CreateRootSessionWithArtifactsInput, RootSessionArtifacts, } from "./session/session-service";
13
13
  export type { WorkspaceManager, WorkspaceManagerEvent, } from "./session/workspace-manager";
package/package.json CHANGED
@@ -1,45 +1,44 @@
1
1
  {
2
2
  "name": "@clinebot/core",
3
- "version": "0.0.0",
4
- "main": "dist/index.js",
3
+ "version": "0.0.2",
4
+ "main": "dist/index.node.js",
5
5
  "dependencies": {
6
- "@clinebot/agents": "workspace:*",
7
- "@clinebot/llms": "workspace:*",
8
- "@clinebot/rpc": "workspace:*",
9
- "@clinebot/shared": "workspace:*",
10
- "nanoid": "^5.1.6",
6
+ "@clinebot/agents": "0.0.2",
7
+ "@clinebot/llms": "0.0.2",
8
+ "nanoid": "^5.1.7",
11
9
  "simple-git": "^3.32.3",
12
10
  "yaml": "^2.8.2",
13
11
  "zod": "^4.3.6"
14
12
  },
15
13
  "exports": {
16
14
  ".": {
17
- "development": "./src/index.ts",
18
- "types": "./dist/index.d.ts",
19
- "import": "./dist/index.js"
15
+ "browser": "./dist/index.browser.js",
16
+ "development": "./dist/index.node.js",
17
+ "types": "./dist/index.node.d.ts",
18
+ "import": "./dist/index.node.js"
20
19
  },
21
20
  "./node": {
22
- "development": "./src/index.ts",
23
- "types": "./dist/index.d.ts",
24
- "import": "./dist/index.js"
21
+ "development": "./src/index.node.ts",
22
+ "types": "./dist/index.node.d.ts",
23
+ "import": "./dist/index.node.js"
25
24
  },
26
25
  "./browser": {
27
- "development": "./src/index.ts",
26
+ "development": "./dist/index.browser.js",
28
27
  "types": "./dist/index.browser.d.ts",
29
28
  "import": "./dist/index.browser.js"
30
29
  },
31
30
  "./server": {
32
- "development": "./src/server/index.ts",
31
+ "development": "./dist/server/index.js",
33
32
  "types": "./dist/server/index.d.ts",
34
33
  "import": "./dist/server/index.js"
35
34
  },
36
35
  "./server/node": {
37
- "development": "./src/server/index.ts",
36
+ "development": "./dist/server/index.js",
38
37
  "types": "./dist/server/index.d.ts",
39
38
  "import": "./dist/server/index.js"
40
39
  },
41
40
  "./server/browser": {
42
- "development": "./src/server/index.ts",
41
+ "development": "./dist/server/index.js",
43
42
  "types": "./dist/server.browser.d.ts",
44
43
  "import": "./dist/server.browser.js"
45
44
  }
@@ -50,7 +49,7 @@
50
49
  "src"
51
50
  ],
52
51
  "scripts": {
53
- "build": "bun run ./build.ts && bun tsc -p tsconfig.build.json",
52
+ "build": "bun run ./bun.mts && bun tsc -p tsconfig.build.json",
54
53
  "clean": "rm -rf dist node_modules",
55
54
  "typecheck": "bun tsc -p tsconfig.dev.json --noEmit",
56
55
  "test": "bun run test:unit && bun run test:e2e",
@@ -59,5 +58,5 @@
59
58
  "test:watch": "vitest --config vitest.config.ts"
60
59
  },
61
60
  "type": "module",
62
- "types": "dist/index.d.ts"
63
- }
61
+ "types": "dist/index.node.d.ts"
62
+ }
@@ -226,8 +226,137 @@ describe("zod schema conversion", () => {
226
226
  description:
227
227
  "The absolute file path of a text file to read content from",
228
228
  },
229
- description: "Array of absolute file paths",
229
+ 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.",
230
231
  });
231
232
  expect(inputSchema.required).toEqual(["file_paths"]);
232
233
  });
234
+
235
+ it("exposes skills args as optional nullable in tool schemas", () => {
236
+ const tools = createDefaultTools({
237
+ executors: {
238
+ skills: async () => "ok",
239
+ },
240
+ enableReadFiles: false,
241
+ enableSearch: false,
242
+ enableBash: false,
243
+ enableWebFetch: false,
244
+ enableEditor: false,
245
+ enableApplyPatch: false,
246
+ enableAskQuestion: false,
247
+ enableSkills: true,
248
+ });
249
+ const skills = tools.find((tool) => tool.name === "skills");
250
+ expect(skills).toBeDefined();
251
+ if (!skills) {
252
+ throw new Error("Expected skills tool.");
253
+ }
254
+ const schema = skills.inputSchema as {
255
+ required?: string[];
256
+ properties?: Record<string, unknown>;
257
+ };
258
+ expect(schema.required).toEqual(["skill"]);
259
+ expect(schema.properties).toHaveProperty("args");
260
+ });
261
+ });
262
+
263
+ describe("default editor tool", () => {
264
+ it("accepts null for unused optional fields on str_replace", async () => {
265
+ const execute = vi.fn(async () => "patched");
266
+ const tools = createDefaultTools({
267
+ executors: {
268
+ editor: execute,
269
+ },
270
+ enableReadFiles: false,
271
+ enableSearch: false,
272
+ enableBash: false,
273
+ enableWebFetch: false,
274
+ enableSkills: false,
275
+ enableAskQuestion: false,
276
+ enableApplyPatch: false,
277
+ enableEditor: true,
278
+ });
279
+ const editorTool = tools.find((tool) => tool.name === "editor");
280
+ expect(editorTool).toBeDefined();
281
+ if (!editorTool) {
282
+ throw new Error("Expected editor tool to be defined.");
283
+ }
284
+
285
+ const result = await editorTool.execute(
286
+ {
287
+ command: "str_replace",
288
+ path: "/tmp/example.ts",
289
+ old_str: "before",
290
+ new_str: "after",
291
+ file_text: null,
292
+ insert_line: null,
293
+ },
294
+ {
295
+ agentId: "agent-1",
296
+ conversationId: "conv-1",
297
+ iteration: 1,
298
+ },
299
+ );
300
+
301
+ expect(result).toEqual({
302
+ query: "str_replace:/tmp/example.ts",
303
+ result: "patched",
304
+ success: true,
305
+ });
306
+ expect(execute).toHaveBeenCalledWith(
307
+ expect.objectContaining({
308
+ command: "str_replace",
309
+ path: "/tmp/example.ts",
310
+ old_str: "before",
311
+ new_str: "after",
312
+ file_text: null,
313
+ insert_line: null,
314
+ }),
315
+ process.cwd(),
316
+ expect.objectContaining({
317
+ agentId: "agent-1",
318
+ conversationId: "conv-1",
319
+ iteration: 1,
320
+ }),
321
+ );
322
+ });
323
+
324
+ it("still rejects null for required insert fields", async () => {
325
+ const execute = vi.fn(async () => "patched");
326
+ const tools = createDefaultTools({
327
+ executors: {
328
+ editor: execute,
329
+ },
330
+ enableReadFiles: false,
331
+ enableSearch: false,
332
+ enableBash: false,
333
+ enableWebFetch: false,
334
+ enableSkills: false,
335
+ enableAskQuestion: false,
336
+ enableApplyPatch: false,
337
+ enableEditor: true,
338
+ });
339
+ const editorTool = tools.find((tool) => tool.name === "editor");
340
+ expect(editorTool).toBeDefined();
341
+ if (!editorTool) {
342
+ throw new Error("Expected editor tool to be defined.");
343
+ }
344
+
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();
361
+ });
233
362
  });
@@ -426,7 +426,7 @@ export function createEditorTool(
426
426
  name: "editor",
427
427
  description:
428
428
  "Edit file using absolute path with create, string replacement, and line insert operations. " +
429
- "Supported commands: create, str_replace, insert, undo_edit.",
429
+ "Supported commands: create, str_replace, insert.",
430
430
  inputSchema: zodToJsonSchema(EditFileInputSchema),
431
431
  timeoutMs,
432
432
  retryable: false, // Editing operations are stateful and should not auto-retry
@@ -487,7 +487,11 @@ export function createSkillsTool(
487
487
  execute: async (input, context) => {
488
488
  const validatedInput = validateWithZod(SkillsInputSchema, input);
489
489
  return withTimeout(
490
- executor(validatedInput.skill, validatedInput.args, context),
490
+ executor(
491
+ validatedInput.skill,
492
+ validatedInput.args || undefined,
493
+ context,
494
+ ),
491
495
  timeoutMs,
492
496
  `Skills operation timed out after ${timeoutMs}ms`,
493
497
  );
@@ -116,7 +116,7 @@ async function createFile(
116
116
  async function replaceInFile(
117
117
  filePath: string,
118
118
  oldStr: string,
119
- newStr: string | undefined,
119
+ newStr: string | null | undefined,
120
120
  encoding: BufferEncoding,
121
121
  maxDiffLines: number,
122
122
  ): Promise<string> {
@@ -142,23 +142,24 @@ async function replaceInFile(
142
142
 
143
143
  async function insertInFile(
144
144
  filePath: string,
145
- insertLine: number,
145
+ insertLineOneBased: number,
146
146
  newStr: string,
147
147
  encoding: BufferEncoding,
148
148
  ): Promise<string> {
149
149
  const content = await fs.readFile(filePath, encoding);
150
150
  const lines = content.split("\n");
151
+ const insertLine = insertLineOneBased - 1; // Convert to zero-based index
151
152
 
152
153
  if (insertLine < 0 || insertLine > lines.length) {
153
154
  throw new Error(
154
- `Invalid line number: ${insertLine}. Valid range: 0-${lines.length}`,
155
+ `Invalid line number: ${insertLineOneBased}. Valid range: 1-${lines.length}`,
155
156
  );
156
157
  }
157
158
 
158
159
  lines.splice(insertLine, 0, ...newStr.split("\n"));
159
160
  await fs.writeFile(filePath, lines.join("\n"), { encoding });
160
161
 
161
- return `Inserted content at line ${insertLine} in ${filePath}.`;
162
+ return `Inserted content at line ${insertLineOneBased} in ${filePath}.`;
162
163
  }
163
164
 
164
165
  /**
@@ -182,7 +183,7 @@ export function createEditorExecutor(
182
183
 
183
184
  switch (input.command) {
184
185
  case "create":
185
- if (input.file_text === undefined) {
186
+ if (input.file_text == null) {
186
187
  throw new Error(
187
188
  "Parameter `file_text` is required for command: create",
188
189
  );
@@ -190,7 +191,7 @@ export function createEditorExecutor(
190
191
  return createFile(filePath, input.file_text, encoding);
191
192
 
192
193
  case "str_replace":
193
- if (input.old_str === undefined) {
194
+ if (input.old_str == null) {
194
195
  throw new Error(
195
196
  "Parameter `old_str` is required for command: str_replace",
196
197
  );
@@ -204,19 +205,19 @@ export function createEditorExecutor(
204
205
  );
205
206
 
206
207
  case "insert":
207
- if (input.insert_line === undefined) {
208
+ if (input.insert_line == null) {
208
209
  throw new Error(
209
210
  "Parameter `insert_line` is required for insert command.",
210
211
  );
211
212
  }
212
- if (input.new_str === undefined) {
213
+ if (input.new_str == null) {
213
214
  throw new Error(
214
215
  "Parameter `new_str` is required for insert command.",
215
216
  );
216
217
  }
217
218
  return insertInFile(
218
219
  filePath,
219
- input.insert_line,
220
+ input.insert_line, // One-based index
220
221
  input.new_str,
221
222
  encoding,
222
223
  );
@@ -17,7 +17,7 @@ describe("createFileReadExecutor", () => {
17
17
  conversationId: "conv-1",
18
18
  iteration: 1,
19
19
  });
20
- expect(result).toBe("hello absolute path");
20
+ expect(result).toBe("1 | hello absolute path");
21
21
  } finally {
22
22
  await fs.rm(dir, { recursive: true, force: true });
23
23
  }
@@ -32,6 +32,12 @@ export interface FileReadExecutorOptions {
32
32
  includeLineNumbers?: boolean;
33
33
  }
34
34
 
35
+ const DEFAULT_FILE_READ_OPTIONS: Required<FileReadExecutorOptions> = {
36
+ maxFileSizeBytes: 10_000_000, // 10MB default limit
37
+ encoding: "utf-8", // Default to UTF-8 encoding
38
+ includeLineNumbers: true, // Include line numbers by default
39
+ };
40
+
35
41
  /**
36
42
  * Create a file read executor using Node.js fs module
37
43
  *
@@ -48,11 +54,10 @@ export interface FileReadExecutorOptions {
48
54
  export function createFileReadExecutor(
49
55
  options: FileReadExecutorOptions = {},
50
56
  ): FileReadExecutor {
51
- const {
52
- maxFileSizeBytes = 10_000_000,
53
- encoding = "utf-8",
54
- includeLineNumbers = false,
55
- } = options;
57
+ const { maxFileSizeBytes, encoding, includeLineNumbers } = {
58
+ ...DEFAULT_FILE_READ_OPTIONS,
59
+ ...options,
60
+ };
56
61
 
57
62
  return async (filePath: string, _context: ToolContext): Promise<string> => {
58
63
  const resolvedPath = path.isAbsolute(filePath)
@@ -77,7 +82,7 @@ export function createFileReadExecutor(
77
82
  // Read file content
78
83
  const content = await fs.readFile(resolvedPath, encoding);
79
84
 
80
- // Optionally add line numbers
85
+ // Optionally add line numbers - one-based indexing for better readability
81
86
  if (includeLineNumbers) {
82
87
  const lines = content.split("\n");
83
88
  const maxLineNumWidth = String(lines.length).length;
@@ -37,6 +37,11 @@ export {
37
37
  type SearchExecutorOptions,
38
38
  type WebFetchExecutorOptions,
39
39
  } from "./executors/index.js";
40
+ export {
41
+ DEFAULT_MODEL_TOOL_ROUTING_RULES,
42
+ resolveToolRoutingConfig,
43
+ type ToolRoutingRule,
44
+ } from "./model-tool-routing.js";
40
45
  // Presets
41
46
  export {
42
47
  createDefaultToolsWithPreset,
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ DEFAULT_MODEL_TOOL_ROUTING_RULES,
4
+ resolveToolRoutingConfig,
5
+ } from "./model-tool-routing.js";
6
+
7
+ describe("model tool routing", () => {
8
+ it("applies default codex/gpt routing in act mode", () => {
9
+ const config = resolveToolRoutingConfig(
10
+ "openai",
11
+ "openai/gpt-5.4",
12
+ "act",
13
+ DEFAULT_MODEL_TOOL_ROUTING_RULES,
14
+ );
15
+
16
+ expect(config.enableApplyPatch).toBe(true);
17
+ expect(config.enableEditor).toBe(false);
18
+ });
19
+
20
+ it("does not apply default codex/gpt routing in plan mode", () => {
21
+ const config = resolveToolRoutingConfig(
22
+ "openai",
23
+ "openai/gpt-5.4",
24
+ "plan",
25
+ DEFAULT_MODEL_TOOL_ROUTING_RULES,
26
+ );
27
+
28
+ expect(config).toEqual({});
29
+ });
30
+
31
+ it("applies matching custom rules in order", () => {
32
+ const config = resolveToolRoutingConfig(
33
+ "anthropic",
34
+ "claude-sonnet-4-6",
35
+ "act",
36
+ [
37
+ {
38
+ name: "claude-editor-off",
39
+ mode: "act",
40
+ modelIdIncludes: ["claude"],
41
+ disableTools: ["editor"],
42
+ },
43
+ {
44
+ name: "claude-apply-patch-on",
45
+ mode: "act",
46
+ modelIdIncludes: ["claude"],
47
+ enableTools: ["apply_patch"],
48
+ },
49
+ ],
50
+ );
51
+
52
+ expect(config.enableEditor).toBe(false);
53
+ expect(config.enableApplyPatch).toBe(true);
54
+ });
55
+
56
+ it("returns empty config when no rules match", () => {
57
+ const config = resolveToolRoutingConfig(
58
+ "anthropic",
59
+ "claude-sonnet-4-6",
60
+ "act",
61
+ [
62
+ {
63
+ mode: "act",
64
+ modelIdIncludes: ["gpt"],
65
+ enableTools: ["apply_patch"],
66
+ },
67
+ ],
68
+ );
69
+
70
+ expect(config).toEqual({});
71
+ });
72
+
73
+ it("can match provider-only rules", () => {
74
+ const config = resolveToolRoutingConfig("openai", "o4-mini", "act", [
75
+ {
76
+ mode: "act",
77
+ providerIdIncludes: ["openai"],
78
+ enableTools: ["apply_patch"],
79
+ disableTools: ["editor"],
80
+ },
81
+ ]);
82
+
83
+ expect(config.enableApplyPatch).toBe(true);
84
+ expect(config.enableEditor).toBe(false);
85
+ });
86
+ });