@oh-my-pi/pi-coding-agent 4.2.2 → 4.3.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [4.3.0] - 2026-01-11
6
+
7
+ ### Added
8
+
9
+ - Added Cursor provider support with browser-based OAuth authentication
10
+ - Added default model configuration for Cursor provider (claude-sonnet-4-5)
11
+ - Added execution bridge for Cursor tool calls including read, ls, grep, write, delete, shell, diagnostics, and MCP operations
12
+
13
+ ### Fixed
14
+
15
+ - Improved fuzzy matching accuracy for edit operations when file and target have inconsistent indentation patterns
16
+
17
+ ## [4.2.3] - 2026-01-11
18
+
19
+ ### Changed
20
+
21
+ - Changed default for `hidden` option in find tool from `false` to `true`, now including hidden files by default
22
+
23
+ ### Fixed
24
+
25
+ - Fixed serialized auth storage initialization so OAuth refreshes in subagents don't crash
26
+
5
27
  ## [4.2.2] - 2026-01-11
6
28
  ### Added
7
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "4.2.2",
3
+ "version": "4.3.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -39,10 +39,10 @@
39
39
  "prepublishOnly": "bun run generate-template && bun run clean && bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "4.2.2",
43
- "@oh-my-pi/pi-agent-core": "4.2.2",
44
- "@oh-my-pi/pi-git-tool": "4.2.2",
45
- "@oh-my-pi/pi-tui": "4.2.2",
42
+ "@oh-my-pi/pi-ai": "4.3.0",
43
+ "@oh-my-pi/pi-agent-core": "4.3.0",
44
+ "@oh-my-pi/pi-git-tool": "4.3.0",
45
+ "@oh-my-pi/pi-tui": "4.3.0",
46
46
  "@openai/agents": "^0.3.7",
47
47
  "@sinclair/typebox": "^0.34.46",
48
48
  "ajv": "^8.17.1",
@@ -9,13 +9,14 @@ import {
9
9
  getOAuthApiKey,
10
10
  loginAnthropic,
11
11
  loginAntigravity,
12
+ loginCursor,
12
13
  loginGeminiCli,
13
14
  loginGitHubCopilot,
14
15
  loginOpenAICodex,
15
16
  type OAuthCredentials,
16
17
  type OAuthProvider,
17
18
  } from "@oh-my-pi/pi-ai";
18
- import { getAgentDbPath } from "../config";
19
+ import { getAgentDbPath, getAuthPath } from "../config";
19
20
  import { AgentStorage } from "./agent-storage";
20
21
  import { logger } from "./logger";
21
22
  import { migrateJsonStorage } from "./storage-migration";
@@ -49,6 +50,8 @@ export interface SerializedAuthStorage {
49
50
  }>
50
51
  >;
51
52
  runtimeOverrides?: Record<string, string>;
53
+ authPath?: string;
54
+ dbPath?: string;
52
55
  }
53
56
 
54
57
  /**
@@ -138,6 +141,11 @@ export class AuthStorage {
138
141
  */
139
142
  static fromSerialized(data: SerializedAuthStorage): AuthStorage {
140
143
  const instance = Object.create(AuthStorage.prototype) as AuthStorage;
144
+ const authPath = data.authPath ?? data.dbPath ?? getAuthPath();
145
+ instance.authPath = authPath;
146
+ instance.fallbackPaths = [];
147
+ instance.dbPath = data.dbPath ?? AuthStorage.resolveDbPath(authPath);
148
+ instance.storage = AgentStorage.open(instance.dbPath);
141
149
  instance.data = new Map();
142
150
  instance.runtimeOverrides = new Map();
143
151
  instance.providerRoundRobinIndex = new Map();
@@ -186,6 +194,8 @@ export class AuthStorage {
186
194
  return {
187
195
  credentials,
188
196
  runtimeOverrides: Object.keys(runtimeOverrides).length > 0 ? runtimeOverrides : undefined,
197
+ authPath: this.authPath,
198
+ dbPath: this.dbPath,
189
199
  };
190
200
  }
191
201
 
@@ -576,6 +586,12 @@ export class AuthStorage {
576
586
  onManualCodeInput: callbacks.onManualCodeInput,
577
587
  });
578
588
  break;
589
+ case "cursor":
590
+ credentials = await loginCursor(
591
+ (url) => callbacks.onAuth({ url }),
592
+ callbacks.onProgress ? () => callbacks.onProgress?.("Waiting for browser authentication...") : undefined,
593
+ );
594
+ break;
579
595
  default:
580
596
  throw new Error(`Unknown OAuth provider: ${provider}`);
581
597
  }
@@ -0,0 +1,234 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { rmSync, statSync } from "node:fs";
3
+ import type {
4
+ AgentEvent,
5
+ AgentTool,
6
+ AgentToolContext,
7
+ AgentToolResult,
8
+ AgentToolUpdateCallback,
9
+ } from "@oh-my-pi/pi-agent-core";
10
+ import type { CursorExecHandlers, CursorMcpCall, ToolResultMessage } from "@oh-my-pi/pi-ai";
11
+ import { resolveToCwd } from "../tools/path-utils";
12
+
13
+ interface CursorExecBridgeOptions {
14
+ cwd: string;
15
+ tools: Map<string, AgentTool>;
16
+ getToolContext?: () => AgentToolContext | undefined;
17
+ emitEvent?: (event: AgentEvent) => void;
18
+ }
19
+
20
+ function createToolResultMessage(
21
+ toolCallId: string,
22
+ toolName: string,
23
+ result: AgentToolResult<unknown>,
24
+ isError: boolean,
25
+ ): ToolResultMessage {
26
+ return {
27
+ role: "toolResult",
28
+ toolCallId,
29
+ toolName,
30
+ content: result.content,
31
+ details: result.details,
32
+ isError,
33
+ timestamp: Date.now(),
34
+ };
35
+ }
36
+
37
+ function buildToolErrorResult(message: string): AgentToolResult<unknown> {
38
+ return {
39
+ content: [{ type: "text", text: message }],
40
+ details: {},
41
+ };
42
+ }
43
+
44
+ async function executeTool(
45
+ options: CursorExecBridgeOptions,
46
+ toolName: string,
47
+ toolCallId: string,
48
+ args: Record<string, unknown>,
49
+ ): Promise<ToolResultMessage> {
50
+ const tool = options.tools.get(toolName);
51
+ if (!tool) {
52
+ const result = buildToolErrorResult(`Tool "${toolName}" not available`);
53
+ return createToolResultMessage(toolCallId, toolName, result, true);
54
+ }
55
+
56
+ options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args });
57
+
58
+ let result: AgentToolResult<unknown>;
59
+ let isError = false;
60
+
61
+ const onUpdate: AgentToolUpdateCallback<unknown> | undefined = options.emitEvent
62
+ ? (partialResult) => {
63
+ options.emitEvent?.({
64
+ type: "tool_execution_update",
65
+ toolCallId,
66
+ toolName,
67
+ args,
68
+ partialResult,
69
+ });
70
+ }
71
+ : undefined;
72
+
73
+ try {
74
+ result = await tool.execute(
75
+ toolCallId,
76
+ args as Record<string, unknown>,
77
+ undefined,
78
+ onUpdate,
79
+ options.getToolContext?.(),
80
+ );
81
+ } catch (error) {
82
+ const message = error instanceof Error ? error.message : String(error);
83
+ result = buildToolErrorResult(message);
84
+ isError = true;
85
+ }
86
+
87
+ options.emitEvent?.({ type: "tool_execution_end", toolCallId, toolName, result, isError });
88
+
89
+ return createToolResultMessage(toolCallId, toolName, result, isError);
90
+ }
91
+
92
+ async function executeDelete(options: CursorExecBridgeOptions, pathArg: string, toolCallId: string) {
93
+ const toolName = "delete";
94
+ options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: { path: pathArg } });
95
+
96
+ const absolutePath = resolveToCwd(pathArg, options.cwd);
97
+ let isError = false;
98
+ let result: AgentToolResult<unknown>;
99
+
100
+ try {
101
+ const stat = statSync(absolutePath, { throwIfNoEntry: false });
102
+ if (!stat) {
103
+ throw new Error(`File not found: ${pathArg}`);
104
+ }
105
+ if (!stat.isFile()) {
106
+ throw new Error(`Path is not a file: ${pathArg}`);
107
+ }
108
+
109
+ rmSync(absolutePath);
110
+
111
+ const sizeText = stat.size ? ` (${stat.size} bytes)` : "";
112
+ const message = `Deleted ${pathArg}${sizeText}`;
113
+ result = { content: [{ type: "text", text: message }], details: {} };
114
+ } catch (error) {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ result = buildToolErrorResult(message);
117
+ isError = true;
118
+ }
119
+
120
+ options.emitEvent?.({ type: "tool_execution_end", toolCallId, toolName, result, isError });
121
+ return createToolResultMessage(toolCallId, toolName, result, isError);
122
+ }
123
+
124
+ function decodeToolCallId(toolCallId?: string): string {
125
+ return toolCallId && toolCallId.length > 0 ? toolCallId : randomUUID();
126
+ }
127
+
128
+ function decodeMcpArgs(rawArgs: Record<string, Uint8Array>): Record<string, unknown> {
129
+ const decoded: Record<string, unknown> = {};
130
+ for (const [key, value] of Object.entries(rawArgs)) {
131
+ const text = new TextDecoder().decode(value);
132
+ try {
133
+ decoded[key] = JSON.parse(text);
134
+ } catch {
135
+ decoded[key] = text;
136
+ }
137
+ }
138
+ return decoded;
139
+ }
140
+
141
+ function formatMcpToolErrorMessage(toolName: string, availableTools: string[]): string {
142
+ const list = availableTools.length > 0 ? availableTools.join(", ") : "none";
143
+ return `MCP tool "${toolName}" not found. Available tools: ${list}`;
144
+ }
145
+
146
+ export function createCursorExecHandlers(options: CursorExecBridgeOptions): CursorExecHandlers {
147
+ return {
148
+ read: async (args) => {
149
+ const toolCallId = decodeToolCallId(args.toolCallId);
150
+ const toolResultMessage = await executeTool(options, "read", toolCallId, { path: args.path });
151
+ return toolResultMessage;
152
+ },
153
+ ls: async (args) => {
154
+ const toolCallId = decodeToolCallId(args.toolCallId);
155
+ const toolResultMessage = await executeTool(options, "ls", toolCallId, { path: args.path });
156
+ return toolResultMessage;
157
+ },
158
+ grep: async (args) => {
159
+ const toolCallId = decodeToolCallId(args.toolCallId);
160
+ const toolResultMessage = await executeTool(options, "grep", toolCallId, {
161
+ pattern: args.pattern,
162
+ path: args.path || undefined,
163
+ glob: args.glob || undefined,
164
+ outputMode: args.outputMode || undefined,
165
+ context: args.context ?? args.contextBefore ?? args.contextAfter ?? undefined,
166
+ ignoreCase: args.caseInsensitive || undefined,
167
+ type: args.type || undefined,
168
+ headLimit: args.headLimit ?? undefined,
169
+ multiline: args.multiline || undefined,
170
+ });
171
+ return toolResultMessage;
172
+ },
173
+ write: async (args) => {
174
+ const toolCallId = decodeToolCallId(args.toolCallId);
175
+ const content = args.fileText ?? new TextDecoder().decode(args.fileBytes ?? new Uint8Array());
176
+ const toolResultMessage = await executeTool(options, "write", toolCallId, {
177
+ path: args.path,
178
+ content,
179
+ });
180
+ return toolResultMessage;
181
+ },
182
+ delete: async (args) => {
183
+ const toolCallId = decodeToolCallId(args.toolCallId);
184
+ const toolResultMessage = await executeDelete(options, args.path, toolCallId);
185
+ return toolResultMessage;
186
+ },
187
+ shell: async (args) => {
188
+ const toolCallId = decodeToolCallId(args.toolCallId);
189
+ const timeoutSeconds =
190
+ args.timeout && args.timeout > 0
191
+ ? args.timeout > 1000
192
+ ? Math.ceil(args.timeout / 1000)
193
+ : args.timeout
194
+ : undefined;
195
+ const toolResultMessage = await executeTool(options, "bash", toolCallId, {
196
+ command: args.command,
197
+ workdir: args.workingDirectory || undefined,
198
+ timeout: timeoutSeconds,
199
+ });
200
+ return toolResultMessage;
201
+ },
202
+ diagnostics: async (args) => {
203
+ const toolCallId = decodeToolCallId(args.toolCallId);
204
+ const toolResultMessage = await executeTool(options, "lsp", toolCallId, {
205
+ action: "diagnostics",
206
+ file: args.path,
207
+ });
208
+ return toolResultMessage;
209
+ },
210
+ mcp: async (call: CursorMcpCall) => {
211
+ const toolName = call.toolName || call.name;
212
+ const toolCallId = decodeToolCallId(call.toolCallId);
213
+ const tool = options.tools.get(toolName);
214
+ if (!tool) {
215
+ const availableTools = Array.from(options.tools.keys()).filter((name) => name.startsWith("mcp_"));
216
+ const message = formatMcpToolErrorMessage(toolName, availableTools);
217
+ const toolResult: ToolResultMessage = {
218
+ role: "toolResult",
219
+ toolCallId,
220
+ toolName,
221
+ content: [{ type: "text", text: message }],
222
+ details: {},
223
+ isError: true,
224
+ timestamp: Date.now(),
225
+ };
226
+ return toolResult;
227
+ }
228
+
229
+ const args = Object.keys(call.args ?? {}).length > 0 ? call.args : decodeMcpArgs(call.rawArgs ?? {});
230
+ const toolResultMessage = await executeTool(options, toolName, toolCallId, args);
231
+ return toolResultMessage;
232
+ },
233
+ };
234
+ }
@@ -19,6 +19,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
19
19
  "google-antigravity": "gemini-3-pro-high",
20
20
  "google-vertex": "gemini-2.5-pro",
21
21
  "github-copilot": "gpt-4o",
22
+ cursor: "claude-sonnet-4-5",
22
23
  openrouter: "openai/gpt-5.1-codex",
23
24
  xai: "grok-4-fast-non-reasoning",
24
25
  groq: "openai/gpt-oss-120b",
package/src/core/sdk.ts CHANGED
@@ -27,7 +27,7 @@
27
27
  */
28
28
 
29
29
  import { join } from "node:path";
30
- import { Agent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
30
+ import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
31
31
  import type { Message, Model } from "@oh-my-pi/pi-ai";
32
32
  import type { Component } from "@oh-my-pi/pi-tui";
33
33
  import chalk from "chalk";
@@ -40,6 +40,7 @@ import { initializeWithSettings } from "../discovery";
40
40
  import { registerAsyncCleanup } from "../modes/cleanup";
41
41
  import { AgentSession } from "./agent-session";
42
42
  import { AuthStorage } from "./auth-storage";
43
+ import { createCursorExecHandlers } from "./cursor/exec-bridge";
43
44
  import {
44
45
  type CustomCommandsLoadResult,
45
46
  loadCustomCommands as loadCustomCommandsInternal,
@@ -854,6 +855,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
854
855
  }
855
856
  time("combineTools");
856
857
 
858
+ let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
859
+ const cursorExecHandlers = createCursorExecHandlers({
860
+ cwd,
861
+ tools: toolRegistry,
862
+ getToolContext: toolContextStore.getContext,
863
+ emitEvent: (event) => cursorEventEmitter?.(event),
864
+ });
865
+
857
866
  const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
858
867
  toolContextStore.setToolNames(toolNames);
859
868
  const defaultPrompt = await buildSystemPromptInternal({
@@ -964,7 +973,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
964
973
  }
965
974
  return key;
966
975
  },
976
+ cursorExecHandlers,
967
977
  });
978
+ cursorEventEmitter = (event) => agent.emitExternalEvent(event);
968
979
  time("createAgent");
969
980
 
970
981
  // Restore messages if session has existing data
@@ -49,9 +49,9 @@ function countLeadingWhitespace(line: string): number {
49
49
  const char = line[i];
50
50
  if (char === " " || char === "\t") {
51
51
  count++;
52
- continue;
52
+ } else {
53
+ break;
53
54
  }
54
- break;
55
55
  }
56
56
  return count;
57
57
  }
@@ -80,15 +80,16 @@ function computeRelativeIndentDepths(lines: string[]): number[] {
80
80
  });
81
81
  }
82
82
 
83
- function normalizeLinesForMatch(lines: string[]): string[] {
84
- const indentDepths = computeRelativeIndentDepths(lines);
83
+ function normalizeLinesForMatch(lines: string[], includeDepth = true): string[] {
84
+ const indentDepths = includeDepth ? computeRelativeIndentDepths(lines) : null;
85
85
  return lines.map((line, index) => {
86
86
  const trimmed = line.trim();
87
+ const prefix = indentDepths ? `${indentDepths[index]}|` : "|";
87
88
  if (trimmed.length === 0) {
88
- return `${indentDepths[index]}|`;
89
+ return prefix;
89
90
  }
90
91
  const collapsed = trimmed.replace(/[ \t]+/g, " ");
91
- return `${indentDepths[index]}|${collapsed}`;
92
+ return `${prefix}${collapsed}`;
92
93
  });
93
94
  }
94
95
 
@@ -148,22 +149,14 @@ function computeLineOffsets(lines: string[]): number[] {
148
149
  return offsets;
149
150
  }
150
151
 
151
- function findBestFuzzyMatch(
152
- content: string,
153
- target: string,
152
+ function findBestFuzzyMatchCore(
153
+ contentLines: string[],
154
+ targetLines: string[],
155
+ offsets: number[],
154
156
  threshold: number,
157
+ includeDepth: boolean,
155
158
  ): { best?: EditMatch; aboveThresholdCount: number } {
156
- const contentLines = content.split("\n");
157
- const targetLines = target.split("\n");
158
- if (targetLines.length === 0 || target.length === 0) {
159
- return { aboveThresholdCount: 0 };
160
- }
161
- if (targetLines.length > contentLines.length) {
162
- return { aboveThresholdCount: 0 };
163
- }
164
-
165
- const targetNormalized = normalizeLinesForMatch(targetLines);
166
- const offsets = computeLineOffsets(contentLines);
159
+ const targetNormalized = normalizeLinesForMatch(targetLines, includeDepth);
167
160
 
168
161
  let best: EditMatch | undefined;
169
162
  let bestScore = -1;
@@ -171,7 +164,7 @@ function findBestFuzzyMatch(
171
164
 
172
165
  for (let start = 0; start <= contentLines.length - targetLines.length; start++) {
173
166
  const windowLines = contentLines.slice(start, start + targetLines.length);
174
- const windowNormalized = normalizeLinesForMatch(windowLines);
167
+ const windowNormalized = normalizeLinesForMatch(windowLines, includeDepth);
175
168
  let score = 0;
176
169
  for (let i = 0; i < targetLines.length; i++) {
177
170
  score += similarityScore(targetNormalized[i], windowNormalized[i]);
@@ -196,6 +189,36 @@ function findBestFuzzyMatch(
196
189
  return { best, aboveThresholdCount };
197
190
  }
198
191
 
192
+ const FALLBACK_THRESHOLD = 0.8;
193
+
194
+ function findBestFuzzyMatch(
195
+ content: string,
196
+ target: string,
197
+ threshold: number,
198
+ ): { best?: EditMatch; aboveThresholdCount: number } {
199
+ const contentLines = content.split("\n");
200
+ const targetLines = target.split("\n");
201
+ if (targetLines.length === 0 || target.length === 0) {
202
+ return { aboveThresholdCount: 0 };
203
+ }
204
+ if (targetLines.length > contentLines.length) {
205
+ return { aboveThresholdCount: 0 };
206
+ }
207
+
208
+ const offsets = computeLineOffsets(contentLines);
209
+
210
+ let result = findBestFuzzyMatchCore(contentLines, targetLines, offsets, threshold, true);
211
+
212
+ if (result.best && result.best.confidence < threshold && result.best.confidence >= FALLBACK_THRESHOLD) {
213
+ const noDepthResult = findBestFuzzyMatchCore(contentLines, targetLines, offsets, threshold, false);
214
+ if (noDepthResult.best && noDepthResult.best.confidence > result.best.confidence) {
215
+ result = noDepthResult;
216
+ }
217
+ }
218
+
219
+ return result;
220
+ }
221
+
199
222
  export function findEditMatch(
200
223
  content: string,
201
224
  target: string,
@@ -20,7 +20,7 @@ const findSchema = Type.Object({
20
20
  }),
21
21
  path: Type.Optional(Type.String({ description: "Directory to search in (default: current directory)" })),
22
22
  limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 1000)" })),
23
- hidden: Type.Optional(Type.Boolean({ description: "Include hidden files (default: false)" })),
23
+ hidden: Type.Optional(Type.Boolean({ description: "Include hidden files (default: true)" })),
24
24
  sortByMtime: Type.Optional(
25
25
  Type.Boolean({ description: "Sort results by modification time, most recent first (default: false)" }),
26
26
  ),
@@ -143,7 +143,7 @@ export function createFindTool(session: ToolSession, options?: FindToolOptions):
143
143
  })();
144
144
  const effectiveLimit = limit ?? DEFAULT_LIMIT;
145
145
  const effectiveType = type ?? "all";
146
- const includeHidden = hidden ?? false;
146
+ const includeHidden = hidden ?? true;
147
147
  const shouldSortByMtime = sortByMtime ?? false;
148
148
 
149
149
  // If custom operations provided with glob, use that instead of fd