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

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 +60 -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 +54 -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 +63 -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 +73 -0
  62. package/src/core/tools/patch/parser.ts +528 -0
  63. package/src/core/tools/patch/shared.ts +257 -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 +239 -216
  67. package/src/core/tools/render-utils.ts +196 -77
  68. package/src/core/tools/renderers.ts +6 -2
  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 +108 -47
  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 +42 -0
  83. package/src/modes/interactive/components/tool-execution.ts +46 -8
  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
@@ -1,9 +1,9 @@
1
1
  import { relative, resolve, sep } from "node:path";
2
- import type { AgentTool, AgentToolContext } from "@oh-my-pi/pi-agent-core";
2
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
3
3
  import type { ImageContent } from "@oh-my-pi/pi-ai";
4
4
  import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text, truncateToWidth } from "@oh-my-pi/pi-tui";
6
- import { Type } from "@sinclair/typebox";
6
+ import { type Static, Type } from "@sinclair/typebox";
7
7
  import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate";
8
8
  import type { Theme } from "../../modes/interactive/theme/theme";
9
9
  import pythonDescription from "../../prompts/tools/python.md" with { type: "text" };
@@ -116,160 +116,163 @@ export function getPythonToolDescription(): string {
116
116
  return renderPromptTemplate(pythonDescription, { categories });
117
117
  }
118
118
 
119
- interface CreatePythonToolOptions {
119
+ export interface PythonToolOptions {
120
120
  proxyExecutor?: PythonProxyExecutor;
121
121
  }
122
122
 
123
- export function createPythonTool(
124
- session: ToolSession | null,
125
- options?: CreatePythonToolOptions,
126
- ): AgentTool<typeof pythonSchema> {
127
- const { proxyExecutor } = options ?? {};
128
-
129
- return {
130
- name: "python",
131
- label: "Python",
132
- description: getPythonToolDescription(),
133
- parameters: pythonSchema,
134
- execute: async (
135
- _toolCallId: string,
136
- params: PythonToolParams,
137
- signal?: AbortSignal,
138
- onUpdate?,
139
- _ctx?: AgentToolContext,
140
- ) => {
141
- if (proxyExecutor) {
142
- return proxyExecutor(params, signal);
143
- }
123
+ export class PythonTool implements AgentTool<typeof pythonSchema> {
124
+ public readonly name = "python";
125
+ public readonly label = "Python";
126
+ public readonly description: string;
127
+ public readonly parameters = pythonSchema;
144
128
 
145
- if (!session) {
146
- throw new Error("Python tool requires a session when not using proxy executor");
147
- }
129
+ private readonly session: ToolSession | null;
130
+ private readonly proxyExecutor?: PythonProxyExecutor;
148
131
 
149
- const { code, timeout, workdir, reset } = params;
150
- const controller = new AbortController();
151
- const onAbort = () => controller.abort();
152
- signal?.addEventListener("abort", onAbort, { once: true });
132
+ constructor(session: ToolSession | null, options?: PythonToolOptions) {
133
+ this.session = session;
134
+ this.proxyExecutor = options?.proxyExecutor;
135
+ this.description = getPythonToolDescription();
136
+ }
153
137
 
154
- try {
155
- if (signal?.aborted) {
156
- throw new Error("Aborted");
157
- }
138
+ public async execute(
139
+ _toolCallId: string,
140
+ params: Static<typeof pythonSchema>,
141
+ signal?: AbortSignal,
142
+ onUpdate?: AgentToolUpdateCallback,
143
+ _ctx?: AgentToolContext,
144
+ ): Promise<AgentToolResult<PythonToolDetails | undefined>> {
145
+ if (this.proxyExecutor) {
146
+ return this.proxyExecutor(params, signal);
147
+ }
158
148
 
159
- const commandCwd = workdir ? resolveToCwd(workdir, session.cwd) : session.cwd;
160
- let cwdStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
161
- try {
162
- cwdStat = await Bun.file(commandCwd).stat();
163
- } catch {
164
- throw new Error(`Working directory does not exist: ${commandCwd}`);
165
- }
166
- if (!cwdStat.isDirectory()) {
167
- throw new Error(`Working directory is not a directory: ${commandCwd}`);
168
- }
149
+ if (!this.session) {
150
+ throw new Error("Python tool requires a session when not using proxy executor");
151
+ }
169
152
 
170
- const maxTailBytes = DEFAULT_MAX_BYTES * 2;
171
- const tailChunks: Array<{ text: string; bytes: number }> = [];
172
- let tailBytes = 0;
173
- const jsonOutputs: unknown[] = [];
174
- const images: ImageContent[] = [];
175
-
176
- const sessionFile = session.getSessionFile?.() ?? undefined;
177
- const sessionId = sessionFile ? `session:${sessionFile}:workdir:${commandCwd}` : `cwd:${commandCwd}`;
178
- const executorOptions: PythonExecutorOptions = {
179
- cwd: commandCwd,
180
- timeout: timeout ? timeout * 1000 : undefined,
181
- signal: controller.signal,
182
- sessionId,
183
- kernelMode: session.settings?.getPythonKernelMode?.() ?? "session",
184
- useSharedGateway: session.settings?.getPythonSharedGateway?.() ?? true,
185
- reset,
186
- onChunk: (chunk) => {
187
- const chunkBytes = Buffer.byteLength(chunk, "utf-8");
188
- tailChunks.push({ text: chunk, bytes: chunkBytes });
189
- tailBytes += chunkBytes;
190
- while (tailBytes > maxTailBytes && tailChunks.length > 1) {
191
- const removed = tailChunks.shift();
192
- if (removed) {
193
- tailBytes -= removed.bytes;
194
- }
195
- }
196
- if (onUpdate) {
197
- const tailText = tailChunks.map((entry) => entry.text).join("");
198
- const truncation = truncateTail(tailText);
199
- onUpdate({
200
- content: [{ type: "text", text: truncation.content || "" }],
201
- details: truncation.truncated ? { truncation } : undefined,
202
- });
203
- }
204
- },
205
- };
153
+ const { code, timeout, workdir, reset } = params;
154
+ const controller = new AbortController();
155
+ const onAbort = () => controller.abort();
156
+ signal?.addEventListener("abort", onAbort, { once: true });
206
157
 
207
- const result = await executePython(code, executorOptions);
158
+ try {
159
+ if (signal?.aborted) {
160
+ throw new Error("Aborted");
161
+ }
208
162
 
209
- const statusEvents: PythonStatusEvent[] = [];
210
- for (const output of result.displayOutputs) {
211
- if (output.type === "json") {
212
- jsonOutputs.push(output.data);
213
- }
214
- if (output.type === "image") {
215
- images.push({ type: "image", data: output.data, mimeType: output.mimeType });
163
+ const commandCwd = workdir ? resolveToCwd(workdir, this.session.cwd) : this.session.cwd;
164
+ let cwdStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
165
+ try {
166
+ cwdStat = await Bun.file(commandCwd).stat();
167
+ } catch {
168
+ throw new Error(`Working directory does not exist: ${commandCwd}`);
169
+ }
170
+ if (!cwdStat.isDirectory()) {
171
+ throw new Error(`Working directory is not a directory: ${commandCwd}`);
172
+ }
173
+
174
+ const maxTailBytes = DEFAULT_MAX_BYTES * 2;
175
+ const tailChunks: Array<{ text: string; bytes: number }> = [];
176
+ let tailBytes = 0;
177
+ const jsonOutputs: unknown[] = [];
178
+ const images: ImageContent[] = [];
179
+
180
+ const sessionFile = this.session.getSessionFile?.() ?? undefined;
181
+ const sessionId = sessionFile ? `session:${sessionFile}:workdir:${commandCwd}` : `cwd:${commandCwd}`;
182
+ const executorOptions: PythonExecutorOptions = {
183
+ cwd: commandCwd,
184
+ timeout: timeout ? timeout * 1000 : undefined,
185
+ signal: controller.signal,
186
+ sessionId,
187
+ kernelMode: this.session.settings?.getPythonKernelMode?.() ?? "session",
188
+ useSharedGateway: this.session.settings?.getPythonSharedGateway?.() ?? true,
189
+ reset,
190
+ onChunk: (chunk) => {
191
+ const chunkBytes = Buffer.byteLength(chunk, "utf-8");
192
+ tailChunks.push({ text: chunk, bytes: chunkBytes });
193
+ tailBytes += chunkBytes;
194
+ while (tailBytes > maxTailBytes && tailChunks.length > 1) {
195
+ const removed = tailChunks.shift();
196
+ if (removed) {
197
+ tailBytes -= removed.bytes;
198
+ }
216
199
  }
217
- if (output.type === "status") {
218
- statusEvents.push(output.event);
200
+ if (onUpdate) {
201
+ const tailText = tailChunks.map((entry) => entry.text).join("");
202
+ const truncation = truncateTail(tailText);
203
+ onUpdate({
204
+ content: [{ type: "text", text: truncation.content || "" }],
205
+ details: truncation.truncated ? { truncation } : undefined,
206
+ });
219
207
  }
220
- }
208
+ },
209
+ };
221
210
 
222
- if (result.cancelled) {
223
- throw new Error(result.output || "Command aborted");
224
- }
211
+ const result = await executePython(code, executorOptions);
225
212
 
226
- const truncation = truncateTail(result.output);
227
- let outputText =
228
- truncation.content || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)");
229
- let details: PythonToolDetails | undefined;
230
-
231
- if (truncation.truncated) {
232
- const fullOutputSuffix = result.fullOutputPath ? ` Full output: ${result.fullOutputPath}` : "";
233
- details = {
234
- truncation,
235
- fullOutputPath: result.fullOutputPath,
236
- jsonOutputs: jsonOutputs,
237
- images,
238
- statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
239
- };
240
-
241
- const startLine = truncation.totalLines - truncation.outputLines + 1;
242
- const endLine = truncation.totalLines;
243
-
244
- if (truncation.lastLinePartial) {
245
- const lastLineSize = formatSize(Buffer.byteLength(result.output.split("\n").pop() || "", "utf-8"));
246
- outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize})${fullOutputSuffix}]`;
247
- } else if (truncation.truncatedBy === "lines") {
248
- outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}${fullOutputSuffix}]`;
249
- } else {
250
- outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit)${fullOutputSuffix}]`;
251
- }
213
+ const statusEvents: PythonStatusEvent[] = [];
214
+ for (const output of result.displayOutputs) {
215
+ if (output.type === "json") {
216
+ jsonOutputs.push(output.data);
252
217
  }
253
-
254
- if (!details && (jsonOutputs.length > 0 || images.length > 0 || statusEvents.length > 0)) {
255
- details = {
256
- jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
257
- images: images.length > 0 ? images : undefined,
258
- statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
259
- };
218
+ if (output.type === "image") {
219
+ images.push({ type: "image", data: output.data, mimeType: output.mimeType });
260
220
  }
221
+ if (output.type === "status") {
222
+ statusEvents.push(output.event);
223
+ }
224
+ }
225
+
226
+ if (result.cancelled) {
227
+ throw new Error(result.output || "Command aborted");
228
+ }
229
+
230
+ const truncation = truncateTail(result.output);
231
+ let outputText =
232
+ truncation.content || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)");
233
+ let details: PythonToolDetails | undefined;
234
+
235
+ if (truncation.truncated) {
236
+ const fullOutputSuffix = result.fullOutputPath ? ` Full output: ${result.fullOutputPath}` : "";
237
+ details = {
238
+ truncation,
239
+ fullOutputPath: result.fullOutputPath,
240
+ jsonOutputs: jsonOutputs,
241
+ images,
242
+ statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
243
+ };
244
+
245
+ const startLine = truncation.totalLines - truncation.outputLines + 1;
246
+ const endLine = truncation.totalLines;
261
247
 
262
- if (result.exitCode !== 0 && result.exitCode !== undefined) {
263
- outputText += `\n\nCommand exited with code ${result.exitCode}`;
264
- throw new Error(outputText);
248
+ if (truncation.lastLinePartial) {
249
+ const lastLineSize = formatSize(Buffer.byteLength(result.output.split("\n").pop() || "", "utf-8"));
250
+ outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize})${fullOutputSuffix}]`;
251
+ } else if (truncation.truncatedBy === "lines") {
252
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}${fullOutputSuffix}]`;
253
+ } else {
254
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit)${fullOutputSuffix}]`;
265
255
  }
256
+ }
266
257
 
267
- return { content: [{ type: "text", text: outputText }], details };
268
- } finally {
269
- signal?.removeEventListener("abort", onAbort);
258
+ if (!details && (jsonOutputs.length > 0 || images.length > 0 || statusEvents.length > 0)) {
259
+ details = {
260
+ jsonOutputs: jsonOutputs.length > 0 ? jsonOutputs : undefined,
261
+ images: images.length > 0 ? images : undefined,
262
+ statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
263
+ };
270
264
  }
271
- },
272
- };
265
+
266
+ if (result.exitCode !== 0 && result.exitCode !== undefined) {
267
+ outputText += `\n\nCommand exited with code ${result.exitCode}`;
268
+ throw new Error(outputText);
269
+ }
270
+
271
+ return { content: [{ type: "text", text: outputText }], details };
272
+ } finally {
273
+ signal?.removeEventListener("abort", onAbort);
274
+ }
275
+ }
273
276
  }
274
277
 
275
278
  interface PythonRenderArgs {