@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.0

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 (93) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/docs/sdk.md +1 -1
  3. package/package.json +5 -5
  4. package/scripts/generate-template.ts +6 -6
  5. package/src/cli/args.ts +3 -0
  6. package/src/core/agent-session.ts +39 -0
  7. package/src/core/bash-executor.ts +3 -3
  8. package/src/core/cursor/exec-bridge.ts +95 -88
  9. package/src/core/custom-commands/bundled/review/index.ts +142 -145
  10. package/src/core/custom-commands/bundled/wt/index.ts +68 -66
  11. package/src/core/custom-commands/loader.ts +4 -6
  12. package/src/core/custom-tools/index.ts +2 -2
  13. package/src/core/custom-tools/loader.ts +66 -61
  14. package/src/core/custom-tools/types.ts +4 -4
  15. package/src/core/custom-tools/wrapper.ts +61 -25
  16. package/src/core/event-bus.ts +19 -47
  17. package/src/core/extensions/index.ts +8 -4
  18. package/src/core/extensions/loader.ts +160 -120
  19. package/src/core/extensions/types.ts +4 -4
  20. package/src/core/extensions/wrapper.ts +149 -100
  21. package/src/core/hooks/index.ts +1 -1
  22. package/src/core/hooks/tool-wrapper.ts +96 -70
  23. package/src/core/hooks/types.ts +1 -2
  24. package/src/core/index.ts +1 -0
  25. package/src/core/mcp/index.ts +6 -2
  26. package/src/core/mcp/json-rpc.ts +88 -0
  27. package/src/core/mcp/loader.ts +22 -4
  28. package/src/core/mcp/manager.ts +202 -48
  29. package/src/core/mcp/tool-bridge.ts +143 -55
  30. package/src/core/mcp/tool-cache.ts +122 -0
  31. package/src/core/python-executor.ts +3 -9
  32. package/src/core/sdk.ts +33 -32
  33. package/src/core/session-manager.ts +30 -0
  34. package/src/core/settings-manager.ts +34 -1
  35. package/src/core/ssh/ssh-executor.ts +6 -84
  36. package/src/core/streaming-output.ts +107 -53
  37. package/src/core/tools/ask.ts +92 -93
  38. package/src/core/tools/bash.ts +103 -94
  39. package/src/core/tools/calculator.ts +41 -26
  40. package/src/core/tools/complete.ts +76 -66
  41. package/src/core/tools/context.ts +22 -24
  42. package/src/core/tools/exa/index.ts +1 -1
  43. package/src/core/tools/exa/mcp-client.ts +56 -101
  44. package/src/core/tools/find.ts +250 -253
  45. package/src/core/tools/git.ts +39 -33
  46. package/src/core/tools/grep.ts +440 -427
  47. package/src/core/tools/index.ts +62 -61
  48. package/src/core/tools/ls.ts +119 -114
  49. package/src/core/tools/lsp/clients/biome-client.ts +5 -7
  50. package/src/core/tools/lsp/clients/index.ts +4 -4
  51. package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
  52. package/src/core/tools/lsp/config.ts +2 -2
  53. package/src/core/tools/lsp/index.ts +604 -578
  54. package/src/core/tools/notebook.ts +121 -119
  55. package/src/core/tools/output.ts +163 -147
  56. package/src/core/tools/patch/applicator.ts +1100 -0
  57. package/src/core/tools/patch/diff.ts +362 -0
  58. package/src/core/tools/patch/fuzzy.ts +647 -0
  59. package/src/core/tools/patch/index.ts +430 -0
  60. package/src/core/tools/patch/normalize.ts +220 -0
  61. package/src/core/tools/patch/normative.ts +49 -0
  62. package/src/core/tools/patch/parser.ts +528 -0
  63. package/src/core/tools/patch/shared.ts +228 -0
  64. package/src/core/tools/patch/types.ts +244 -0
  65. package/src/core/tools/python.ts +139 -136
  66. package/src/core/tools/read.ts +237 -216
  67. package/src/core/tools/render-utils.ts +196 -77
  68. package/src/core/tools/renderers.ts +1 -1
  69. package/src/core/tools/ssh.ts +99 -80
  70. package/src/core/tools/task/executor.ts +11 -7
  71. package/src/core/tools/task/index.ts +352 -343
  72. package/src/core/tools/task/worker.ts +13 -23
  73. package/src/core/tools/todo-write.ts +74 -59
  74. package/src/core/tools/web-fetch.ts +54 -47
  75. package/src/core/tools/web-search/index.ts +27 -16
  76. package/src/core/tools/write.ts +73 -44
  77. package/src/core/ttsr.ts +106 -152
  78. package/src/core/voice.ts +49 -39
  79. package/src/index.ts +16 -12
  80. package/src/lib/worktree/index.ts +1 -9
  81. package/src/modes/interactive/components/diff.ts +15 -8
  82. package/src/modes/interactive/components/settings-defs.ts +24 -0
  83. package/src/modes/interactive/components/tool-execution.ts +34 -6
  84. package/src/modes/interactive/controllers/event-controller.ts +6 -19
  85. package/src/modes/interactive/controllers/input-controller.ts +1 -1
  86. package/src/modes/interactive/utils/ui-helpers.ts +5 -1
  87. package/src/modes/rpc/rpc-mode.ts +99 -81
  88. package/src/prompts/tools/patch.md +76 -0
  89. package/src/prompts/tools/read.md +1 -1
  90. package/src/prompts/tools/{edit.md → replace.md} +1 -0
  91. package/src/utils/shell.ts +0 -40
  92. package/src/core/tools/edit-diff.ts +0 -574
  93. package/src/core/tools/edit.ts +0 -345
@@ -443,30 +443,20 @@ const isAgentEvent = (event: AgentSessionEvent): event is AgentEvent => {
443
443
  return agentEventTypes.has(event.type as AgentEvent["type"]);
444
444
  };
445
445
 
446
- interface RunState {
447
- abortController: AbortController;
448
- startTime: number;
449
- session: { abort: () => Promise<void>; dispose: () => Promise<void> } | null;
450
- unsubscribe: (() => void) | null;
451
- sendDoneOnce: (message: Extract<SubagentWorkerResponse, { type: "done" }>) => void;
452
- }
446
+ class RunState {
447
+ abortController = new AbortController();
448
+ startTime = Date.now();
449
+ session: { abort: () => Promise<void>; dispose: () => Promise<void> } | null = null;
450
+ unsubscribe: (() => void) | null = null;
453
451
 
454
- const createSendDoneOnce = (): RunState["sendDoneOnce"] => {
455
- let sent = false;
456
- return (message) => {
457
- if (sent) return;
458
- sent = true;
459
- postMessageSafe(message);
460
- };
461
- };
452
+ private doneSent = false;
462
453
 
463
- const createRunState = (): RunState => ({
464
- abortController: new AbortController(),
465
- startTime: Date.now(),
466
- session: null,
467
- unsubscribe: null,
468
- sendDoneOnce: createSendDoneOnce(),
469
- });
454
+ sendDoneOnce(message: Extract<SubagentWorkerResponse, { type: "done" }>): void {
455
+ if (this.doneSent) return;
456
+ this.doneSent = true;
457
+ postMessageSafe(message);
458
+ }
459
+ }
470
460
 
471
461
  let activeRun: RunState | null = null;
472
462
  let pendingAbort = false;
@@ -861,7 +851,7 @@ globalThis.addEventListener("message", (event: WorkerMessageEvent<SubagentWorker
861
851
  if (message.type === "start") {
862
852
  // Only allow one task per worker
863
853
  if (activeRun) return;
864
- const runState = createRunState();
854
+ const runState = new RunState();
865
855
  if (pendingAbort) {
866
856
  pendingAbort = false;
867
857
  runState.abortController.abort();
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import path from "node:path";
4
- import type { AgentTool } from "@oh-my-pi/pi-agent-core";
4
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
5
  import { StringEnum } from "@oh-my-pi/pi-ai";
6
6
  import type { Component } from "@oh-my-pi/pi-tui";
7
7
  import { Text } from "@oh-my-pi/pi-tui";
@@ -49,6 +49,8 @@ export interface TodoWriteToolDetails {
49
49
 
50
50
  const TODO_FILE_NAME = "todos.json";
51
51
 
52
+ type TodoWriteParams = { todos: Array<{ id?: string; content?: string; activeForm?: string; status?: string }> };
53
+
52
54
  function normalizeTodoStatus(status?: string): TodoStatus {
53
55
  switch (status) {
54
56
  case "in_progress":
@@ -137,6 +139,14 @@ async function saveTodoFile(filePath: string, data: TodoFile): Promise<void> {
137
139
  await Bun.write(filePath, JSON.stringify(data, null, 2));
138
140
  }
139
141
 
142
+ function formatTodoSummary(todos: TodoItem[]): string {
143
+ if (todos.length === 0) return "Todo list cleared.";
144
+ const completed = todos.filter((t) => t.status === "completed").length;
145
+ const inProgress = todos.filter((t) => t.status === "in_progress").length;
146
+ const pending = todos.filter((t) => t.status === "pending").length;
147
+ return `Saved ${todos.length} todos (${pending} pending, ${inProgress} in progress, ${completed} completed).`;
148
+ }
149
+
140
150
  function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string {
141
151
  const checkbox = uiTheme.checkbox;
142
152
  const displayText =
@@ -151,71 +161,76 @@ function formatTodoLine(item: TodoItem, uiTheme: Theme, prefix: string): string
151
161
  }
152
162
  }
153
163
 
154
- function formatTodoSummary(todos: TodoItem[]): string {
155
- if (todos.length === 0) return "Todo list cleared.";
156
- const completed = todos.filter((t) => t.status === "completed").length;
157
- const inProgress = todos.filter((t) => t.status === "in_progress").length;
158
- const pending = todos.filter((t) => t.status === "pending").length;
159
- return `Saved ${todos.length} todos (${pending} pending, ${inProgress} in progress, ${completed} completed).`;
160
- }
164
+ // =============================================================================
165
+ // Tool Class
166
+ // =============================================================================
161
167
 
162
- export function createTodoWriteTool(session: ToolSession): AgentTool<typeof todoWriteSchema, TodoWriteToolDetails> {
163
- return {
164
- name: "todo_write",
165
- label: "Todo Write",
166
- description: renderPromptTemplate(todoWriteDescription),
167
- parameters: todoWriteSchema,
168
- execute: async (
169
- _toolCallId: string,
170
- params: { todos: Array<{ id?: string; content?: string; activeForm?: string; status?: string }> },
171
- ) => {
172
- const todos = normalizeTodos(params.todos ?? []);
173
- const validation = validateSequentialTodos(todos);
174
- if (!validation.valid) {
175
- throw new Error(validation.error ?? "Todos must be completed sequentially.");
176
- }
177
- const updatedAt = Date.now();
168
+ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWriteToolDetails> {
169
+ public readonly name = "todo_write";
170
+ public readonly label = "Todo Write";
171
+ public readonly description: string;
172
+ public readonly parameters = todoWriteSchema;
178
173
 
179
- const sessionFile = session.getSessionFile();
180
- if (!sessionFile) {
181
- return {
182
- content: [{ type: "text", text: formatTodoSummary(todos) }],
183
- details: { todos, updatedAt, storage: "memory" },
184
- };
185
- }
174
+ private readonly session: ToolSession;
186
175
 
187
- const artifactsDir = getArtifactsDir(sessionFile);
188
- if (!artifactsDir) {
189
- return {
190
- content: [{ type: "text", text: formatTodoSummary(todos) }],
191
- details: { todos, updatedAt, storage: "memory" },
192
- };
193
- }
176
+ constructor(session: ToolSession) {
177
+ this.session = session;
178
+ this.description = renderPromptTemplate(todoWriteDescription);
179
+ }
194
180
 
195
- ensureArtifactsDir(artifactsDir);
196
- const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
197
- const existing = await loadTodoFile(todoPath);
198
- const storedTodos = existing?.todos ?? [];
199
- const merged = todos.length > 0 ? todos : [];
200
- const fileData: TodoFile = { updatedAt, todos: merged };
201
-
202
- try {
203
- mkdirSync(artifactsDir, { recursive: true });
204
- await saveTodoFile(todoPath, fileData);
205
- } catch (error) {
206
- logger.error("Failed to write todo file", { path: todoPath, error: String(error) });
207
- return {
208
- content: [{ type: "text", text: "Failed to save todos." }],
209
- details: { todos: storedTodos, updatedAt, storage: "session" },
210
- };
211
- }
181
+ public async execute(
182
+ _toolCallId: string,
183
+ params: TodoWriteParams,
184
+ _signal?: AbortSignal,
185
+ _onUpdate?: AgentToolUpdateCallback<TodoWriteToolDetails>,
186
+ _context?: AgentToolContext,
187
+ ): Promise<AgentToolResult<TodoWriteToolDetails>> {
188
+ const todos = normalizeTodos(params.todos ?? []);
189
+ const validation = validateSequentialTodos(todos);
190
+ if (!validation.valid) {
191
+ throw new Error(validation.error ?? "Todos must be completed sequentially.");
192
+ }
193
+ const updatedAt = Date.now();
194
+
195
+ const sessionFile = this.session.getSessionFile();
196
+ if (!sessionFile) {
197
+ return {
198
+ content: [{ type: "text", text: formatTodoSummary(todos) }],
199
+ details: { todos, updatedAt, storage: "memory" },
200
+ };
201
+ }
202
+
203
+ const artifactsDir = getArtifactsDir(sessionFile);
204
+ if (!artifactsDir) {
205
+ return {
206
+ content: [{ type: "text", text: formatTodoSummary(todos) }],
207
+ details: { todos, updatedAt, storage: "memory" },
208
+ };
209
+ }
210
+
211
+ ensureArtifactsDir(artifactsDir);
212
+ const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
213
+ const existing = await loadTodoFile(todoPath);
214
+ const storedTodos = existing?.todos ?? [];
215
+ const merged = todos.length > 0 ? todos : [];
216
+ const fileData: TodoFile = { updatedAt, todos: merged };
212
217
 
218
+ try {
219
+ mkdirSync(artifactsDir, { recursive: true });
220
+ await saveTodoFile(todoPath, fileData);
221
+ } catch (error) {
222
+ logger.error("Failed to write todo file", { path: todoPath, error: String(error) });
213
223
  return {
214
- content: [{ type: "text", text: formatTodoSummary(merged) }],
215
- details: { todos: merged, updatedAt, storage: "session" },
224
+ content: [{ type: "text", text: "Failed to save todos." }],
225
+ details: { todos: storedTodos, updatedAt, storage: "session" },
216
226
  };
217
- },
218
- };
227
+ }
228
+
229
+ return {
230
+ content: [{ type: "text", text: formatTodoSummary(merged) }],
231
+ details: { todos: merged, updatedAt, storage: "session" },
232
+ };
233
+ }
219
234
  }
220
235
 
221
236
  // =============================================================================
@@ -1,9 +1,9 @@
1
1
  import { tmpdir } from "node:os";
2
2
  import * as path from "node:path";
3
- import type { AgentTool } from "@oh-my-pi/pi-agent-core";
3
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
- import { Type } from "@sinclair/typebox";
6
+ import { type Static, Type } from "@sinclair/typebox";
7
7
  import { nanoid } from "nanoid";
8
8
  import { parse as parseHtml } from "node-html-parser";
9
9
  import { type Theme, theme } from "../../modes/interactive/theme/theme";
@@ -848,55 +848,62 @@ export interface WebFetchToolDetails {
848
848
  notes: string[];
849
849
  }
850
850
 
851
- export function createWebFetchTool(_session: ToolSession): AgentTool<typeof webFetchSchema> {
852
- return {
853
- name: "web_fetch",
854
- label: "Web Fetch",
855
- description: renderPromptTemplate(webFetchDescription),
856
- parameters: webFetchSchema,
857
- execute: async (
858
- _toolCallId: string,
859
- { url, timeout = DEFAULT_TIMEOUT, raw = false }: { url: string; timeout?: number; raw?: boolean },
860
- signal?: AbortSignal,
861
- ) => {
862
- if (signal?.aborted) {
863
- throw new Error("Operation aborted");
864
- }
851
+ export class WebFetchTool implements AgentTool<typeof webFetchSchema, WebFetchToolDetails> {
852
+ public readonly name = "web_fetch";
853
+ public readonly label = "Web Fetch";
854
+ public readonly description: string;
855
+ public readonly parameters = webFetchSchema;
865
856
 
866
- // Clamp timeout
867
- const effectiveTimeout = Math.min(Math.max(timeout, 1), 120);
857
+ constructor(_session: ToolSession) {
858
+ this.description = renderPromptTemplate(webFetchDescription);
859
+ }
868
860
 
869
- const result = await renderUrl(url, effectiveTimeout, raw, signal);
861
+ public async execute(
862
+ _toolCallId: string,
863
+ params: Static<typeof webFetchSchema>,
864
+ signal?: AbortSignal,
865
+ _onUpdate?: AgentToolUpdateCallback<WebFetchToolDetails>,
866
+ _context?: AgentToolContext,
867
+ ): Promise<AgentToolResult<WebFetchToolDetails>> {
868
+ const { url, timeout = DEFAULT_TIMEOUT, raw = false } = params;
870
869
 
871
- // Format output
872
- let output = "";
873
- output += `URL: ${result.finalUrl}\n`;
874
- output += `Content-Type: ${result.contentType}\n`;
875
- output += `Method: ${result.method}\n`;
876
- if (result.truncated) {
877
- output += `Warning: Output was truncated\n`;
878
- }
879
- if (result.notes.length > 0) {
880
- output += `Notes: ${result.notes.join("; ")}\n`;
881
- }
882
- output += `\n---\n\n`;
883
- output += result.content;
884
-
885
- const details: WebFetchToolDetails = {
886
- url: result.url,
887
- finalUrl: result.finalUrl,
888
- contentType: result.contentType,
889
- method: result.method,
890
- truncated: result.truncated,
891
- notes: result.notes,
892
- };
870
+ if (signal?.aborted) {
871
+ throw new Error("Operation aborted");
872
+ }
893
873
 
894
- return {
895
- content: [{ type: "text", text: output }],
896
- details,
897
- };
898
- },
899
- };
874
+ // Clamp timeout
875
+ const effectiveTimeout = Math.min(Math.max(timeout, 1), 120);
876
+
877
+ const result = await renderUrl(url, effectiveTimeout, raw, signal);
878
+
879
+ // Format output
880
+ let output = "";
881
+ output += `URL: ${result.finalUrl}\n`;
882
+ output += `Content-Type: ${result.contentType}\n`;
883
+ output += `Method: ${result.method}\n`;
884
+ if (result.truncated) {
885
+ output += `Warning: Output was truncated\n`;
886
+ }
887
+ if (result.notes.length > 0) {
888
+ output += `Notes: ${result.notes.join("; ")}\n`;
889
+ }
890
+ output += `\n---\n\n`;
891
+ output += result.content;
892
+
893
+ const details: WebFetchToolDetails = {
894
+ url: result.url,
895
+ finalUrl: result.finalUrl,
896
+ contentType: result.contentType,
897
+ method: result.method,
898
+ truncated: result.truncated,
899
+ notes: result.notes,
900
+ };
901
+
902
+ return {
903
+ content: [{ type: "text", text: output }],
904
+ details,
905
+ };
906
+ }
900
907
  }
901
908
 
902
909
  // =============================================================================
@@ -12,7 +12,7 @@
12
12
  * - web_search_company: Comprehensive company research
13
13
  */
14
14
 
15
- import type { AgentTool } from "@oh-my-pi/pi-agent-core";
15
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
16
16
  import { StringEnum } from "@oh-my-pi/pi-ai";
17
17
  import { Type } from "@sinclair/typebox";
18
18
  import type { Theme } from "../../../modes/interactive/theme/theme";
@@ -330,16 +330,32 @@ async function executeWebSearch(
330
330
  };
331
331
  }
332
332
 
333
- /** Web search tool as AgentTool (for allTools export) */
334
- export const webSearchTool: AgentTool<typeof webSearchSchema> = {
335
- name: "web_search",
336
- label: "Web Search",
337
- description: renderPromptTemplate(webSearchDescription),
338
- parameters: webSearchSchema,
339
- execute: async (toolCallId, params) => {
340
- return executeWebSearch(toolCallId, params as WebSearchParams);
341
- },
342
- };
333
+ /**
334
+ * Web search tool implementation.
335
+ *
336
+ * Supports Anthropic, Perplexity, and Exa providers with automatic fallback.
337
+ * Session is accepted for interface consistency but not used.
338
+ */
339
+ export class WebSearchTool implements AgentTool<typeof webSearchSchema, WebSearchRenderDetails> {
340
+ public readonly name = "web_search";
341
+ public readonly label = "Web Search";
342
+ public readonly description: string;
343
+ public readonly parameters = webSearchSchema;
344
+
345
+ constructor(_session: ToolSession) {
346
+ this.description = renderPromptTemplate(webSearchDescription);
347
+ }
348
+
349
+ public async execute(
350
+ _toolCallId: string,
351
+ params: WebSearchParams,
352
+ _signal?: AbortSignal,
353
+ _onUpdate?: AgentToolUpdateCallback<WebSearchRenderDetails>,
354
+ _context?: AgentToolContext,
355
+ ): Promise<AgentToolResult<WebSearchRenderDetails>> {
356
+ return executeWebSearch(_toolCallId, params);
357
+ }
358
+ }
343
359
 
344
360
  /** Web search tool as CustomTool (for TUI rendering support) */
345
361
  export const webSearchCustomTool: CustomTool<typeof webSearchSchema, WebSearchRenderDetails> = {
@@ -367,11 +383,6 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, WebSearchRe
367
383
  },
368
384
  };
369
385
 
370
- /** Factory function for backward compatibility */
371
- export function createWebSearchTool(_session: ToolSession): AgentTool<typeof webSearchSchema> {
372
- return webSearchTool;
373
- }
374
-
375
386
  // ============================================================================
376
387
  // Exa-specific tools (available when EXA_API_KEY is present)
377
388
  // ============================================================================
@@ -1,4 +1,10 @@
1
- import type { AgentTool, AgentToolContext, ToolCallContext } from "@oh-my-pi/pi-agent-core";
1
+ import type {
2
+ AgentTool,
3
+ AgentToolContext,
4
+ AgentToolResult,
5
+ AgentToolUpdateCallback,
6
+ ToolCallContext,
7
+ } from "@oh-my-pi/pi-agent-core";
2
8
  import type { Component } from "@oh-my-pi/pi-tui";
3
9
  import { Text } from "@oh-my-pi/pi-tui";
4
10
  import { Type } from "@sinclair/typebox";
@@ -8,7 +14,12 @@ import type { RenderResultOptions } from "../custom-tools/types";
8
14
  import { renderPromptTemplate } from "../prompt-templates";
9
15
  import type { ToolSession } from "../sdk";
10
16
  import { untilAborted } from "../utils";
11
- import { createLspWritethrough, type FileDiagnosticsResult, writethroughNoop } from "./lsp/index";
17
+ import {
18
+ createLspWritethrough,
19
+ type FileDiagnosticsResult,
20
+ type WritethroughCallback,
21
+ writethroughNoop,
22
+ } from "./lsp/index";
12
23
  import { resolveToCwd } from "./path-utils";
13
24
  import { formatDiagnostics, formatExpandHint, replaceTabs, shortenPath } from "./render-utils";
14
25
 
@@ -38,51 +49,69 @@ function getLspBatchRequest(toolCall: ToolCallContext | undefined): { id: string
38
49
  return { id: toolCall.batchId, flush: !hasLaterWrites };
39
50
  }
40
51
 
41
- export function createWriteTool(session: ToolSession): AgentTool<typeof writeSchema, WriteToolDetails> {
42
- const enableLsp = session.enableLsp ?? true;
43
- const enableFormat = enableLsp ? (session.settings?.getLspFormatOnWrite() ?? true) : false;
44
- const enableDiagnostics = enableLsp ? (session.settings?.getLspDiagnosticsOnWrite() ?? true) : false;
45
- const writethrough = enableLsp
46
- ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
47
- : writethroughNoop;
48
- return {
49
- name: "write",
50
- label: "Write",
51
- description: renderPromptTemplate(writeDescription),
52
- parameters: writeSchema,
53
- execute: async (
54
- _toolCallId: string,
55
- { path, content }: { path: string; content: string },
56
- signal?: AbortSignal,
57
- _onUpdate?: unknown,
58
- context?: AgentToolContext,
59
- ) => {
60
- return untilAborted(signal, async () => {
61
- const absolutePath = resolveToCwd(path, session.cwd);
62
- const batchRequest = getLspBatchRequest(context?.toolCall);
63
-
64
- const diagnostics = await writethrough(absolutePath, content, signal, undefined, batchRequest);
65
-
66
- let resultText = `Successfully wrote ${content.length} bytes to ${path}`;
67
- if (!diagnostics) {
68
- return {
69
- content: [{ type: "text", text: resultText }],
70
- details: {},
71
- };
72
- }
73
-
74
- const messages = diagnostics?.messages;
75
- if (messages && messages.length > 0) {
76
- resultText += `\n\nLSP Diagnostics (${diagnostics.summary}):\n`;
77
- resultText += messages.map((d) => ` ${d}`).join("\n");
78
- }
52
+ // ═══════════════════════════════════════════════════════════════════════════
53
+ // Tool Class
54
+ // ═══════════════════════════════════════════════════════════════════════════
55
+
56
+ type WriteParams = { path: string; content: string };
57
+
58
+ /**
59
+ * Write tool implementation.
60
+ *
61
+ * Creates or overwrites files with optional LSP formatting and diagnostics.
62
+ */
63
+ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails> {
64
+ public readonly name = "write";
65
+ public readonly label = "Write";
66
+ public readonly description: string;
67
+ public readonly parameters = writeSchema;
68
+
69
+ private readonly session: ToolSession;
70
+ private readonly writethrough: WritethroughCallback;
71
+
72
+ constructor(session: ToolSession) {
73
+ this.session = session;
74
+ const enableLsp = session.enableLsp ?? true;
75
+ const enableFormat = enableLsp ? (session.settings?.getLspFormatOnWrite() ?? true) : false;
76
+ const enableDiagnostics = enableLsp ? (session.settings?.getLspDiagnosticsOnWrite() ?? true) : false;
77
+ this.writethrough = enableLsp
78
+ ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
79
+ : writethroughNoop;
80
+ this.description = renderPromptTemplate(writeDescription);
81
+ }
82
+
83
+ public async execute(
84
+ _toolCallId: string,
85
+ { path, content }: WriteParams,
86
+ signal?: AbortSignal,
87
+ _onUpdate?: AgentToolUpdateCallback<WriteToolDetails>,
88
+ context?: AgentToolContext,
89
+ ): Promise<AgentToolResult<WriteToolDetails>> {
90
+ return untilAborted(signal, async () => {
91
+ const absolutePath = resolveToCwd(path, this.session.cwd);
92
+ const batchRequest = getLspBatchRequest(context?.toolCall);
93
+
94
+ const diagnostics = await this.writethrough(absolutePath, content, signal, undefined, batchRequest);
95
+
96
+ let resultText = `Successfully wrote ${content.length} bytes to ${path}`;
97
+ if (!diagnostics) {
79
98
  return {
80
99
  content: [{ type: "text", text: resultText }],
81
- details: { diagnostics },
100
+ details: {},
82
101
  };
83
- });
84
- },
85
- };
102
+ }
103
+
104
+ const messages = diagnostics?.messages;
105
+ if (messages && messages.length > 0) {
106
+ resultText += `\n\nLSP Diagnostics (${diagnostics.summary}):\n`;
107
+ resultText += messages.map((d) => ` ${d}`).join("\n");
108
+ }
109
+ return {
110
+ content: [{ type: "text", text: resultText }],
111
+ details: { diagnostics },
112
+ };
113
+ });
114
+ }
86
115
  }
87
116
 
88
117
  // =============================================================================