@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
@@ -4,8 +4,9 @@
4
4
  * Subagents must call this tool to finish and return structured JSON output.
5
5
  */
6
6
 
7
- import type { AgentTool } from "@oh-my-pi/pi-agent-core";
7
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
8
8
  import { StringEnum } from "@oh-my-pi/pi-ai";
9
+ import type { Static, TObject } from "@sinclair/typebox";
9
10
  import { Type } from "@sinclair/typebox";
10
11
  import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
11
12
  import type { ToolSession } from "./index";
@@ -52,77 +53,86 @@ function formatAjvErrors(errors: ErrorObject[] | null | undefined): string {
52
53
  .join("; ");
53
54
  }
54
55
 
55
- export function createCompleteTool(session: ToolSession) {
56
- const schemaResult = normalizeSchema(session.outputSchema);
57
- // Convert JTD to JSON Schema if needed (auto-detected)
58
- const normalizedSchema =
59
- schemaResult.normalized !== undefined ? jtdToJsonSchema(schemaResult.normalized) : undefined;
60
- let validate: ValidateFunction | undefined;
61
- let schemaError = schemaResult.error;
62
-
63
- if (normalizedSchema !== undefined && !schemaError) {
64
- try {
65
- validate = ajv.compile(normalizedSchema as any);
66
- } catch (err) {
67
- schemaError = err instanceof Error ? err.message : String(err);
56
+ export class CompleteTool implements AgentTool<TObject, CompleteDetails> {
57
+ public readonly name = "complete";
58
+ public readonly label = "Complete";
59
+ public readonly description =
60
+ "Finish the task with structured JSON output. Call exactly once at the end of the task.\n\n" +
61
+ "If you cannot complete the task, call with status='aborted' and an error message.";
62
+ public readonly parameters: TObject;
63
+
64
+ private readonly validate?: ValidateFunction;
65
+ private readonly schemaError?: string;
66
+
67
+ constructor(session: ToolSession) {
68
+ const schemaResult = normalizeSchema(session.outputSchema);
69
+ // Convert JTD to JSON Schema if needed (auto-detected)
70
+ const normalizedSchema =
71
+ schemaResult.normalized !== undefined ? jtdToJsonSchema(schemaResult.normalized) : undefined;
72
+ let schemaError = schemaResult.error;
73
+
74
+ if (normalizedSchema !== undefined && !schemaError) {
75
+ try {
76
+ this.validate = ajv.compile(normalizedSchema as any);
77
+ } catch (err) {
78
+ schemaError = err instanceof Error ? err.message : String(err);
79
+ }
68
80
  }
81
+
82
+ this.schemaError = schemaError;
83
+
84
+ const schemaHint = formatSchema(normalizedSchema ?? session.outputSchema);
85
+
86
+ // Use actual schema if provided, otherwise fall back to Type.Any
87
+ // Merge description into the JSON schema for better tool documentation
88
+ const dataSchema = normalizedSchema
89
+ ? Type.Unsafe({
90
+ ...(normalizedSchema as object),
91
+ description: `Structured output matching the schema:\n${schemaHint}`,
92
+ })
93
+ : Type.Any({ description: "Structured JSON output (no schema specified)" });
94
+
95
+ this.parameters = Type.Object({
96
+ data: Type.Optional(dataSchema),
97
+ status: Type.Optional(
98
+ StringEnum(["success", "aborted"], {
99
+ description: "Use 'aborted' if the task cannot be completed, defaults to 'success'",
100
+ }),
101
+ ),
102
+ error: Type.Optional(Type.String({ description: "Error message when status is 'aborted'" })),
103
+ });
69
104
  }
70
105
 
71
- const schemaHint = formatSchema(normalizedSchema ?? session.outputSchema);
72
-
73
- // Use actual schema if provided, otherwise fall back to Type.Any
74
- // Merge description into the JSON schema for better tool documentation
75
- const dataSchema = normalizedSchema
76
- ? Type.Unsafe({
77
- ...(normalizedSchema as object),
78
- description: `Structured output matching the schema:\n${schemaHint}`,
79
- })
80
- : Type.Any({ description: "Structured JSON output (no schema specified)" });
81
-
82
- const completeParams = Type.Object({
83
- data: Type.Optional(dataSchema),
84
- status: Type.Optional(
85
- StringEnum(["success", "aborted"], {
86
- description: "Use 'aborted' if the task cannot be completed, defaults to 'success'",
87
- }),
88
- ),
89
- error: Type.Optional(Type.String({ description: "Error message when status is 'aborted'" })),
90
- });
91
-
92
- const tool: AgentTool<typeof completeParams, CompleteDetails> = {
93
- name: "complete",
94
- label: "Complete",
95
- description:
96
- "Finish the task with structured JSON output. Call exactly once at the end of the task.\n\n" +
97
- "If you cannot complete the task, call with status='aborted' and an error message.",
98
- parameters: completeParams,
99
- execute: async (_toolCallId, params) => {
100
- const status = params.status ?? "success";
101
-
102
- // Skip validation when aborting - data is optional for aborts
103
- if (status === "success") {
104
- if (params.data === undefined) {
105
- throw new Error("data is required when status is 'success'");
106
- }
107
- if (schemaError) {
108
- throw new Error(`Invalid output schema: ${schemaError}`);
109
- }
110
- if (validate && !validate(params.data)) {
111
- throw new Error(`Output does not match schema: ${formatAjvErrors(validate.errors)}`);
112
- }
106
+ public async execute(
107
+ _toolCallId: string,
108
+ params: Static<TObject>,
109
+ _signal?: AbortSignal,
110
+ _onUpdate?: AgentToolUpdateCallback<CompleteDetails>,
111
+ _context?: AgentToolContext,
112
+ ): Promise<AgentToolResult<CompleteDetails>> {
113
+ const status = (params.status ?? "success") as "success" | "aborted";
114
+
115
+ // Skip validation when aborting - data is optional for aborts
116
+ if (status === "success") {
117
+ if (params.data === undefined) {
118
+ throw new Error("data is required when status is 'success'");
113
119
  }
120
+ if (this.schemaError) {
121
+ throw new Error(`Invalid output schema: ${this.schemaError}`);
122
+ }
123
+ if (this.validate && !this.validate(params.data)) {
124
+ throw new Error(`Output does not match schema: ${formatAjvErrors(this.validate.errors)}`);
125
+ }
126
+ }
114
127
 
115
- const responseText =
116
- status === "aborted" ? `Task aborted: ${params.error || "No reason provided"}` : "Completion recorded.";
117
-
118
- return {
119
- content: [{ type: "text", text: responseText }],
120
- details: { data: params.data, status, error: params.error },
121
- };
122
- },
123
- };
128
+ const responseText =
129
+ status === "aborted" ? `Task aborted: ${params.error || "No reason provided"}` : "Completion recorded.";
124
130
 
125
- return tool;
131
+ return {
132
+ content: [{ type: "text", text: responseText }],
133
+ details: { data: params.data, status, error: params.error as string | undefined },
134
+ };
135
+ }
126
136
  }
127
137
 
128
138
  // Register subprocess tool handler for extraction + termination.
@@ -11,31 +11,29 @@ declare module "@oh-my-pi/pi-agent-core" {
11
11
  }
12
12
  }
13
13
 
14
- export interface ToolContextStore {
15
- getContext(toolCall?: ToolCallContext): AgentToolContext;
16
- setUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
17
- setToolNames(names: string[]): void;
18
- }
14
+ export class ToolContextStore {
15
+ private uiContext: ExtensionUIContext | undefined;
16
+ private hasUI = false;
17
+ private toolNames: string[] = [];
19
18
 
20
- export function createToolContextStore(getBaseContext: () => CustomToolContext): ToolContextStore {
21
- let uiContext: ExtensionUIContext | undefined;
22
- let hasUI = false;
23
- let toolNames: string[] = [];
19
+ constructor(private readonly getBaseContext: () => CustomToolContext) {}
24
20
 
25
- return {
26
- getContext: (toolCall) => ({
27
- ...getBaseContext(),
28
- ui: uiContext,
29
- hasUI,
30
- toolNames,
21
+ getContext(toolCall?: ToolCallContext): AgentToolContext {
22
+ return {
23
+ ...this.getBaseContext(),
24
+ ui: this.uiContext,
25
+ hasUI: this.hasUI,
26
+ toolNames: this.toolNames,
31
27
  toolCall,
32
- }),
33
- setUIContext: (context, uiAvailable) => {
34
- uiContext = context;
35
- hasUI = uiAvailable;
36
- },
37
- setToolNames: (names) => {
38
- toolNames = names;
39
- },
40
- };
28
+ };
29
+ }
30
+
31
+ setUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void {
32
+ this.uiContext = uiContext;
33
+ this.hasUI = hasUI;
34
+ }
35
+
36
+ setToolNames(names: string[]): void {
37
+ this.toolNames = names;
38
+ }
41
39
  }
@@ -48,11 +48,11 @@ export {
48
48
  callExaTool,
49
49
  callWebsetsTool,
50
50
  createMCPToolFromServer,
51
- createMCPWrappedTool,
52
51
  fetchMCPToolSchema,
53
52
  findApiKey,
54
53
  formatSearchResults,
55
54
  isSearchResponse,
55
+ MCPWrappedTool,
56
56
  } from "./mcp-client";
57
57
  export { renderExaCall, renderExaResult } from "./render";
58
58
  export { researcherTools } from "./researcher";
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Exa MCP Client
3
3
  *
4
- * Client for interacting with Exa MCP servers via JSON-RPC 2.0 over HTTPS.
4
+ * Client for interacting with Exa MCP servers.
5
5
  */
6
6
 
7
7
  import { existsSync, readFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import type { TSchema } from "@sinclair/typebox";
10
- import type { CustomTool } from "../../custom-tools/types";
10
+ import type { CustomTool, CustomToolResult } from "../../custom-tools/types";
11
11
  import { logger } from "../../logger";
12
+ import { callMCP } from "../../mcp/json-rpc";
12
13
  import type {
13
14
  ExaRenderDetails,
14
15
  ExaSearchResponse,
@@ -48,63 +49,6 @@ export async function findApiKey(): Promise<string | null> {
48
49
  return null;
49
50
  }
50
51
 
51
- /** Parse SSE response format (lines starting with "data: ") */
52
- function parseSSE(text: string): unknown {
53
- const lines = text.split("\n");
54
- for (const line of lines) {
55
- if (line.startsWith("data: ")) {
56
- const data = line.slice(6).trim();
57
- if (data === "[DONE]") continue;
58
- try {
59
- return JSON.parse(data);
60
- } catch {
61
- // Try next line
62
- }
63
- }
64
- }
65
- // Fallback: try parsing entire response as JSON
66
- try {
67
- return JSON.parse(text);
68
- } catch {
69
- return null;
70
- }
71
- }
72
-
73
- /** Call MCP server with JSON-RPC 2.0 */
74
- export async function callMCP(url: string, method: string, params?: Record<string, unknown>): Promise<unknown> {
75
- const body = {
76
- jsonrpc: "2.0",
77
- id: Math.random().toString(36).slice(2),
78
- method,
79
- params: params ?? {},
80
- };
81
-
82
- const response = await fetch(url, {
83
- method: "POST",
84
- headers: {
85
- "Content-Type": "application/json",
86
- Accept: "application/json, text/event-stream",
87
- },
88
- body: JSON.stringify(body),
89
- });
90
-
91
- if (!response.ok) {
92
- const errorMsg = `MCP request failed: ${response.status} ${response.statusText}`;
93
- logger.error(errorMsg, { url, method, params });
94
- throw new Error(errorMsg);
95
- }
96
-
97
- const text = await response.text();
98
- const result = parseSSE(text);
99
-
100
- if (!result) {
101
- logger.error("Failed to parse MCP response", { url, method, responseText: text.slice(0, 500) });
102
- throw new Error("Failed to parse MCP response");
103
- }
104
-
105
- return result;
106
- }
107
-
108
52
  /** Fetch available tools from Exa MCP */
109
53
  export async function fetchExaTools(apiKey: string, toolNames: string[]): Promise<MCPTool[]> {
110
54
  const url = `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(apiKey)}&toolNames=${encodeURIComponent(toolNames.join(","))}`;
@@ -299,56 +243,67 @@ export async function fetchMCPToolSchema(
299
243
  }
300
244
 
301
245
  /**
302
- * Create a CustomTool dynamically from MCP tool metadata.
246
+ * CustomTool dynamically created from MCP tool metadata.
303
247
  *
304
248
  * This allows tools to be generated from MCP server schemas without hardcoding,
305
249
  * reducing drift when MCP servers add new parameters.
306
250
  */
307
- export function createMCPWrappedTool(
308
- config: MCPToolWrapperConfig,
309
- schema: TSchema,
310
- description: string,
311
- ): CustomTool<TSchema, ExaRenderDetails> {
312
- return {
313
- name: config.name,
314
- label: config.label,
315
- description,
316
- parameters: schema,
317
- async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
318
- try {
319
- const apiKey = await findApiKey();
320
- if (!apiKey) {
321
- return {
322
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
323
- details: { error: "EXA_API_KEY not found", toolName: config.name },
324
- };
325
- }
326
-
327
- const response = config.isWebsetsTool
328
- ? await callWebsetsTool(apiKey, config.mcpToolName, params as Record<string, unknown>)
329
- : await callExaTool(config.mcpToolName, params as Record<string, unknown>, apiKey);
330
-
331
- if (isSearchResponse(response)) {
332
- const formatted = formatSearchResults(response);
333
- return {
334
- content: [{ type: "text" as const, text: formatted }],
335
- details: { response, toolName: config.name },
336
- };
337
- }
251
+ export class MCPWrappedTool implements CustomTool<TSchema, ExaRenderDetails> {
252
+ public readonly name: string;
253
+ public readonly label: string;
254
+ public readonly description: string;
255
+ public readonly parameters: TSchema;
256
+
257
+ private readonly config: MCPToolWrapperConfig;
258
+
259
+ constructor(config: MCPToolWrapperConfig, schema: TSchema, description: string) {
260
+ this.config = config;
261
+ this.name = config.name;
262
+ this.label = config.label;
263
+ this.description = description;
264
+ this.parameters = schema;
265
+ }
338
266
 
267
+ async execute(
268
+ _toolCallId: string,
269
+ params: unknown,
270
+ _onUpdate?: unknown,
271
+ _ctx?: unknown,
272
+ _signal?: AbortSignal,
273
+ ): Promise<CustomToolResult<ExaRenderDetails>> {
274
+ try {
275
+ const apiKey = await findApiKey();
276
+ if (!apiKey) {
339
277
  return {
340
- content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
341
- details: { raw: response, toolName: config.name },
278
+ content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
279
+ details: { error: "EXA_API_KEY not found", toolName: this.config.name },
342
280
  };
343
- } catch (error) {
344
- const message = error instanceof Error ? error.message : String(error);
281
+ }
282
+
283
+ const response = this.config.isWebsetsTool
284
+ ? await callWebsetsTool(apiKey, this.config.mcpToolName, params as Record<string, unknown>)
285
+ : await callExaTool(this.config.mcpToolName, params as Record<string, unknown>, apiKey);
286
+
287
+ if (isSearchResponse(response)) {
288
+ const formatted = formatSearchResults(response);
345
289
  return {
346
- content: [{ type: "text" as const, text: `Error: ${message}` }],
347
- details: { error: message, toolName: config.name },
290
+ content: [{ type: "text" as const, text: formatted }],
291
+ details: { response, toolName: this.config.name },
348
292
  };
349
293
  }
350
- },
351
- };
294
+
295
+ return {
296
+ content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
297
+ details: { raw: response, toolName: this.config.name },
298
+ };
299
+ } catch (error) {
300
+ const message = error instanceof Error ? error.message : String(error);
301
+ return {
302
+ content: [{ type: "text" as const, text: `Error: ${message}` }],
303
+ details: { error: message, toolName: this.config.name },
304
+ };
305
+ }
306
+ }
352
307
  }
353
308
 
354
309
  /**
@@ -361,9 +316,9 @@ export async function createMCPToolFromServer(
361
316
  config: MCPToolWrapperConfig,
362
317
  fallbackSchema: TSchema,
363
318
  fallbackDescription: string,
364
- ): Promise<CustomTool<TSchema, ExaRenderDetails>> {
319
+ ): Promise<MCPWrappedTool> {
365
320
  const mcpTool = await fetchMCPToolSchema(apiKey, config.mcpToolName, config.isWebsetsTool);
366
321
  const schema = mcpTool?.inputSchema ?? fallbackSchema;
367
322
  const description = mcpTool?.description ?? fallbackDescription;
368
- return createMCPWrappedTool(config, schema, description);
323
+ return new MCPWrappedTool(config, schema, description);
369
324
  }