@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
@@ -11,6 +11,6 @@ export {
11
11
  type SendMessageHandler,
12
12
  } from "./loader";
13
13
  export { execCommand, HookRunner, type HookErrorListener } from "./runner";
14
- export { wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper";
14
+ export { HookToolWrapper, wrapToolsWithHooks, wrapToolWithHooks } from "./tool-wrapper";
15
15
  export * from "./types";
16
16
  export type { UsageStatistics, ReadonlySessionManager } from "../session-manager";
@@ -7,93 +7,119 @@ import type { HookRunner } from "./runner";
7
7
  import type { ToolCallEventResult, ToolResultEventResult } from "./types";
8
8
 
9
9
  /**
10
- * Wrap a tool with hook callbacks.
10
+ * Wraps an AgentTool with hook callbacks for interception.
11
+ *
12
+ * Features:
11
13
  * - Emits tool_call event before execution (can block)
12
14
  * - Emits tool_result event after execution (can modify result)
13
15
  * - Forwards onUpdate callback to wrapped tool for progress streaming
14
16
  */
15
- export function wrapToolWithHooks<T>(tool: AgentTool<any, T>, hookRunner: HookRunner): AgentTool<any, T> {
16
- return {
17
- ...tool,
18
- execute: async (
19
- toolCallId: string,
20
- params: Record<string, unknown>,
21
- signal?: AbortSignal,
22
- onUpdate?: AgentToolUpdateCallback<T>,
23
- context?: AgentToolContext,
24
- ) => {
25
- // Emit tool_call event - hooks can block execution
26
- // If hook errors/times out, block by default (fail-safe)
27
- if (hookRunner.hasHandlers("tool_call")) {
28
- try {
29
- const callResult = (await hookRunner.emitToolCall({
30
- type: "tool_call",
31
- toolName: tool.name,
32
- toolCallId,
33
- input: params,
34
- })) as ToolCallEventResult | undefined;
17
+ export class HookToolWrapper<T> implements AgentTool<any, T> {
18
+ name: string;
19
+ label: string;
20
+ description: string;
21
+ parameters: unknown;
22
+ renderCall?: AgentTool["renderCall"];
23
+ renderResult?: AgentTool["renderResult"];
24
+
25
+ constructor(
26
+ private tool: AgentTool<any, T>,
27
+ private hookRunner: HookRunner,
28
+ ) {
29
+ this.name = tool.name;
30
+ this.label = tool.label ?? "";
31
+ this.description = tool.description;
32
+ this.parameters = tool.parameters;
33
+ this.renderCall = tool.renderCall;
34
+ this.renderResult = tool.renderResult;
35
+ }
36
+
37
+ async execute(
38
+ toolCallId: string,
39
+ params: Record<string, unknown>,
40
+ signal?: AbortSignal,
41
+ onUpdate?: AgentToolUpdateCallback<T>,
42
+ context?: AgentToolContext,
43
+ ) {
44
+ // Emit tool_call event - hooks can block execution
45
+ // If hook errors/times out, block by default (fail-safe)
46
+ if (this.hookRunner.hasHandlers("tool_call")) {
47
+ try {
48
+ const callResult = (await this.hookRunner.emitToolCall({
49
+ type: "tool_call",
50
+ toolName: this.tool.name,
51
+ toolCallId,
52
+ input: params,
53
+ })) as ToolCallEventResult | undefined;
35
54
 
36
- if (callResult?.block) {
37
- const reason = callResult.reason || "Tool execution was blocked by a hook";
38
- throw new Error(reason);
39
- }
40
- } catch (err) {
41
- // Hook error or block - throw to mark as error
42
- if (err instanceof Error) {
43
- throw err;
44
- }
45
- throw new Error(`Hook failed, blocking execution: ${String(err)}`);
55
+ if (callResult?.block) {
56
+ const reason = callResult.reason || "Tool execution was blocked by a hook";
57
+ throw new Error(reason);
46
58
  }
59
+ } catch (err) {
60
+ // Hook error or block - throw to mark as error
61
+ if (err instanceof Error) {
62
+ throw err;
63
+ }
64
+ throw new Error(`Hook failed, blocking execution: ${String(err)}`);
47
65
  }
66
+ }
48
67
 
49
- // Execute the actual tool, forwarding onUpdate for progress streaming
50
- try {
51
- const result = await tool.execute(toolCallId, params, signal, onUpdate, context);
68
+ // Execute the actual tool, forwarding onUpdate for progress streaming
69
+ try {
70
+ const result = await this.tool.execute(toolCallId, params, signal, onUpdate, context);
52
71
 
53
- // Emit tool_result event - hooks can modify the result
54
- if (hookRunner.hasHandlers("tool_result")) {
55
- const resultResult = (await hookRunner.emit({
56
- type: "tool_result",
57
- toolName: tool.name,
58
- toolCallId,
59
- input: params,
60
- content: result.content,
61
- details: result.details,
62
- isError: false,
63
- })) as ToolResultEventResult | undefined;
72
+ // Emit tool_result event - hooks can modify the result
73
+ if (this.hookRunner.hasHandlers("tool_result")) {
74
+ const resultResult = (await this.hookRunner.emit({
75
+ type: "tool_result",
76
+ toolName: this.tool.name,
77
+ toolCallId,
78
+ input: params,
79
+ content: result.content,
80
+ details: result.details,
81
+ isError: false,
82
+ })) as ToolResultEventResult | undefined;
64
83
 
65
- // Apply modifications if any
66
- if (resultResult) {
67
- return {
68
- content: resultResult.content ?? result.content,
69
- details: (resultResult.details ?? result.details) as T,
70
- };
71
- }
84
+ // Apply modifications if any
85
+ if (resultResult) {
86
+ return {
87
+ content: resultResult.content ?? result.content,
88
+ details: (resultResult.details ?? result.details) as T,
89
+ };
72
90
  }
91
+ }
73
92
 
74
- return result;
75
- } catch (err) {
76
- // Emit tool_result event for errors so hooks can observe failures
77
- if (hookRunner.hasHandlers("tool_result")) {
78
- await hookRunner.emit({
79
- type: "tool_result",
80
- toolName: tool.name,
81
- toolCallId,
82
- input: params,
83
- content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
84
- details: undefined,
85
- isError: true,
86
- });
87
- }
88
- throw err; // Re-throw original error for agent-loop
93
+ return result;
94
+ } catch (err) {
95
+ // Emit tool_result event for errors so hooks can observe failures
96
+ if (this.hookRunner.hasHandlers("tool_result")) {
97
+ await this.hookRunner.emit({
98
+ type: "tool_result",
99
+ toolName: this.tool.name,
100
+ toolCallId,
101
+ input: params,
102
+ content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
103
+ details: undefined,
104
+ isError: true,
105
+ });
89
106
  }
90
- },
91
- };
107
+ throw err; // Re-throw original error for agent-loop
108
+ }
109
+ }
92
110
  }
93
111
 
94
112
  /**
95
113
  * Wrap all tools with hook callbacks.
96
114
  */
97
115
  export function wrapToolsWithHooks<T>(tools: AgentTool<any, T>[], hookRunner: HookRunner): AgentTool<any, T>[] {
98
- return tools.map((tool) => wrapToolWithHooks(tool, hookRunner));
116
+ return tools.map((tool) => new HookToolWrapper(tool, hookRunner));
117
+ }
118
+
119
+ /**
120
+ * Backward compatibility alias - use HookToolWrapper directly.
121
+ * @deprecated Use HookToolWrapper class instead
122
+ */
123
+ export function wrapToolWithHooks<T>(tool: AgentTool<any, T>, hookRunner: HookRunner): AgentTool<any, T> {
124
+ return new HookToolWrapper(tool, hookRunner);
99
125
  }
@@ -21,9 +21,8 @@ import type {
21
21
  SessionEntry,
22
22
  SessionManager,
23
23
  } from "../session-manager";
24
-
25
- import type { EditToolDetails } from "../tools/edit";
26
24
  import type { BashToolDetails, FindToolDetails, GrepToolDetails, LsToolDetails, ReadToolDetails } from "../tools/index";
25
+ import type { EditToolDetails } from "../tools/patch";
27
26
 
28
27
  // Re-export for backward compatibility
29
28
  export type { ExecOptions, ExecResult } from "../exec";
package/src/core/index.ts CHANGED
@@ -35,6 +35,7 @@ export {
35
35
  MCPManager,
36
36
  type MCPServerConfig,
37
37
  type MCPServerConnection,
38
+ MCPToolCache,
38
39
  type MCPToolDefinition,
39
40
  type MCPToolDetails,
40
41
  type MCPToolsLoadResult,
@@ -7,7 +7,6 @@
7
7
 
8
8
  // Client
9
9
  export { callTool, connectToServer, disconnectServer, listTools, serverSupportsTools } from "./client";
10
-
11
10
  // Config
12
11
  export type { ExaFilterResult, LoadMCPConfigsOptions, LoadMCPConfigsResult } from "./config";
13
12
  export {
@@ -17,6 +16,9 @@ export {
17
16
  loadAllMCPConfigs,
18
17
  validateServerConfig,
19
18
  } from "./config";
19
+ // JSON-RPC (lightweight HTTP-based MCP calls)
20
+ export type { JsonRpcResponse } from "./json-rpc";
21
+ export { callMCP, parseSSE } from "./json-rpc";
20
22
  // Loader (for SDK integration)
21
23
  export type { MCPToolsLoadOptions, MCPToolsLoadResult } from "./loader";
22
24
  export { discoverAndLoadMCPTools } from "./loader";
@@ -25,7 +27,9 @@ export type { MCPDiscoverOptions, MCPLoadResult } from "./manager";
25
27
  export { createMCPManager, MCPManager } from "./manager";
26
28
  // Tool bridge
27
29
  export type { MCPToolDetails } from "./tool-bridge";
28
- export { createMCPTool, createMCPToolName, createMCPTools, parseMCPToolName } from "./tool-bridge";
30
+ export { createMCPToolName, DeferredMCPTool, MCPTool, parseMCPToolName } from "./tool-bridge";
31
+ // Tool cache
32
+ export { MCPToolCache } from "./tool-cache";
29
33
  // Transports
30
34
  export { createHttpTransport, HttpTransport } from "./transports/http";
31
35
  export { createStdioTransport, StdioTransport } from "./transports/stdio";
@@ -0,0 +1,88 @@
1
+ /**
2
+ * MCP JSON-RPC 2.0 over HTTPS.
3
+ *
4
+ * Lightweight utilities for calling MCP servers directly via HTTP
5
+ * without maintaining persistent connections.
6
+ */
7
+
8
+ import { logger } from "../logger";
9
+
10
+ /** Parse SSE response format (lines starting with "data: ") */
11
+ export function parseSSE(text: string): unknown {
12
+ const lines = text.split("\n");
13
+ for (const line of lines) {
14
+ if (line.startsWith("data: ")) {
15
+ const data = line.slice(6).trim();
16
+ if (data === "[DONE]") continue;
17
+ try {
18
+ return JSON.parse(data);
19
+ } catch {
20
+ // Try next line
21
+ }
22
+ }
23
+ }
24
+ // Fallback: try parsing entire response as JSON
25
+ try {
26
+ return JSON.parse(text);
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ /** JSON-RPC 2.0 response structure */
33
+ export interface JsonRpcResponse<T = unknown> {
34
+ jsonrpc: "2.0";
35
+ id: string | number;
36
+ result?: T;
37
+ error?: {
38
+ code: number;
39
+ message: string;
40
+ data?: unknown;
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Call an MCP server with JSON-RPC 2.0 over HTTPS.
46
+ *
47
+ * @param url - Full MCP server URL (including any query parameters)
48
+ * @param method - JSON-RPC method name (e.g., "tools/list", "tools/call")
49
+ * @param params - Method parameters
50
+ * @returns Parsed JSON-RPC response
51
+ */
52
+ export async function callMCP<T = unknown>(
53
+ url: string,
54
+ method: string,
55
+ params?: Record<string, unknown>,
56
+ ): Promise<JsonRpcResponse<T>> {
57
+ const body = {
58
+ jsonrpc: "2.0",
59
+ id: Math.random().toString(36).slice(2),
60
+ method,
61
+ params: params ?? {},
62
+ };
63
+
64
+ const response = await fetch(url, {
65
+ method: "POST",
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ Accept: "application/json, text/event-stream",
69
+ },
70
+ body: JSON.stringify(body),
71
+ });
72
+
73
+ if (!response.ok) {
74
+ const errorMsg = `MCP request failed: ${response.status} ${response.statusText}`;
75
+ logger.error(errorMsg, { url, method, params });
76
+ throw new Error(errorMsg);
77
+ }
78
+
79
+ const text = await response.text();
80
+ const result = parseSSE(text) as JsonRpcResponse<T> | null;
81
+
82
+ if (!result) {
83
+ logger.error("Failed to parse MCP response", { url, method, responseText: text.slice(0, 500) });
84
+ throw new Error("Failed to parse MCP response");
85
+ }
86
+
87
+ return result;
88
+ }
@@ -4,9 +4,12 @@
4
4
  * Integrates MCP tool discovery with the custom tools system.
5
5
  */
6
6
 
7
+ import { AgentStorage } from "../agent-storage";
7
8
  import type { LoadedCustomTool } from "../custom-tools/types";
9
+ import { logger } from "../logger";
8
10
  import { type MCPLoadResult, MCPManager } from "./manager";
9
11
  import { parseMCPToolName } from "./tool-bridge";
12
+ import { MCPToolCache } from "./tool-cache";
10
13
 
11
14
  /** Result from loading MCP tools */
12
15
  export interface MCPToolsLoadResult {
@@ -30,6 +33,19 @@ export interface MCPToolsLoadOptions {
30
33
  enableProjectConfig?: boolean;
31
34
  /** Whether to filter out Exa MCP servers (default: true) */
32
35
  filterExa?: boolean;
36
+ /** SQLite storage for MCP tool cache (null disables cache) */
37
+ cacheStorage?: AgentStorage | null;
38
+ }
39
+
40
+ function resolveToolCache(storage: AgentStorage | null | undefined): MCPToolCache | null {
41
+ if (storage === null) return null;
42
+ try {
43
+ const resolved = storage ?? AgentStorage.open();
44
+ return new MCPToolCache(resolved);
45
+ } catch (error) {
46
+ logger.warn("MCP tool cache unavailable", { error: String(error) });
47
+ return null;
48
+ }
33
49
  }
34
50
 
35
51
  /**
@@ -40,7 +56,8 @@ export interface MCPToolsLoadOptions {
40
56
  * @returns MCP tools in LoadedCustomTool format for integration
41
57
  */
42
58
  export async function discoverAndLoadMCPTools(cwd: string, options?: MCPToolsLoadOptions): Promise<MCPToolsLoadResult> {
43
- const manager = new MCPManager(cwd);
59
+ const toolCache = resolveToolCache(options?.cacheStorage);
60
+ const manager = new MCPManager(cwd, toolCache);
44
61
 
45
62
  let result: MCPLoadResult;
46
63
  try {
@@ -69,12 +86,13 @@ export async function discoverAndLoadMCPTools(cwd: string, options?: MCPToolsLoa
69
86
 
70
87
  // Get provider info from manager's connection if available
71
88
  const connection = serverName ? manager.getConnection(serverName) : undefined;
72
- const provider = connection?._source?.provider;
89
+ const source = serverName ? manager.getSource(serverName) : undefined;
90
+ const providerName =
91
+ connection?._source?.providerName ?? source?.providerName ?? connection?._source?.provider ?? source?.provider;
73
92
 
74
93
  // Format path with provider info if available
75
94
  // Format: "mcp:serverName via providerName" (e.g., "mcp:agentx via Claude Code")
76
- const path =
77
- provider && serverName ? `mcp:${serverName} via ${connection._source!.providerName}` : `mcp:${tool.name}`;
95
+ const path = serverName && providerName ? `mcp:${serverName} via ${providerName}` : `mcp:${tool.name}`;
78
96
 
79
97
  return {
80
98
  path,