@oh-my-pi/pi-coding-agent 4.2.3 → 4.3.1

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 (38) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +5 -5
  3. package/src/cli/update-cli.ts +2 -2
  4. package/src/config.ts +5 -5
  5. package/src/core/auth-storage.ts +13 -1
  6. package/src/core/cursor/exec-bridge.ts +234 -0
  7. package/src/core/custom-commands/loader.ts +3 -1
  8. package/src/core/custom-tools/loader.ts +1 -18
  9. package/src/core/extensions/loader.ts +5 -21
  10. package/src/core/hooks/loader.ts +1 -18
  11. package/src/core/keybindings.ts +3 -1
  12. package/src/core/logger.ts +1 -2
  13. package/src/core/model-resolver.ts +1 -0
  14. package/src/core/prompt-templates.ts +5 -4
  15. package/src/core/sdk.ts +17 -4
  16. package/src/core/skills.ts +5 -4
  17. package/src/core/tools/edit-diff.ts +44 -21
  18. package/src/core/tools/exa/mcp-client.ts +2 -2
  19. package/src/core/tools/task/agents.ts +5 -64
  20. package/src/core/tools/task/commands.ts +7 -33
  21. package/src/core/tools/task/discovery.ts +4 -66
  22. package/src/core/tools/task/executor.ts +32 -3
  23. package/src/core/tools/task/index.ts +11 -2
  24. package/src/core/tools/task/render.ts +25 -15
  25. package/src/core/tools/task/types.ts +3 -0
  26. package/src/core/tools/task/worker-protocol.ts +2 -1
  27. package/src/core/tools/task/worker.ts +2 -1
  28. package/src/core/tools/web-scrapers/huggingface.ts +1 -1
  29. package/src/core/tools/web-scrapers/readthedocs.ts +1 -1
  30. package/src/core/tools/web-scrapers/types.ts +1 -1
  31. package/src/core/tools/web-search/auth.ts +5 -3
  32. package/src/discovery/codex.ts +3 -1
  33. package/src/discovery/helpers.ts +124 -3
  34. package/src/migrations.ts +11 -9
  35. package/src/modes/interactive/components/extensions/state-manager.ts +19 -18
  36. package/src/prompts/agents/frontmatter.md +1 -0
  37. package/src/prompts/agents/reviewer.md +32 -4
  38. package/src/prompts/tools/task.md +3 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [4.3.1] - 2026-01-11
6
+
7
+ ### Changed
8
+
9
+ - Expanded system prompt with defensive reasoning guidance and assumption checks
10
+ - Allowed agent frontmatter to override subagent thinking level, clamped to model capabilities
11
+
12
+ ### Fixed
13
+
14
+ - Ensured reviewer agents use structured output schemas and include reported findings in task outputs
15
+
16
+ ## [4.3.0] - 2026-01-11
17
+
18
+ ### Added
19
+
20
+ - Added Cursor provider support with browser-based OAuth authentication
21
+ - Added default model configuration for Cursor provider (claude-sonnet-4-5)
22
+ - Added execution bridge for Cursor tool calls including read, ls, grep, write, delete, shell, diagnostics, and MCP operations
23
+
24
+ ### Fixed
25
+
26
+ - Improved fuzzy matching accuracy for edit operations when file and target have inconsistent indentation patterns
27
+
5
28
  ## [4.2.3] - 2026-01-11
6
29
 
7
30
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "4.2.3",
3
+ "version": "4.3.1",
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.3",
43
- "@oh-my-pi/pi-agent-core": "4.2.3",
44
- "@oh-my-pi/pi-git-tool": "4.2.3",
45
- "@oh-my-pi/pi-tui": "4.2.3",
42
+ "@oh-my-pi/pi-ai": "4.3.1",
43
+ "@oh-my-pi/pi-agent-core": "4.3.1",
44
+ "@oh-my-pi/pi-git-tool": "4.3.1",
45
+ "@oh-my-pi/pi-tui": "4.3.1",
46
46
  "@openai/agents": "^0.3.7",
47
47
  "@sinclair/typebox": "^0.34.46",
48
48
  "ajv": "^8.17.1",
@@ -144,8 +144,8 @@ async function updateViaBun(): Promise<void> {
144
144
  try {
145
145
  execSync(`bun update -g ${PACKAGE}`, { stdio: "inherit" });
146
146
  console.log(chalk.green(`\n${theme.status.success} Update complete`));
147
- } catch {
148
- throw new Error("bun update failed");
147
+ } catch (error) {
148
+ throw new Error("bun update failed", { cause: error });
149
149
  }
150
150
  }
151
151
 
package/src/config.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
-
5
4
  // Embed package.json at build time for config
6
5
  import packageJson from "../package.json" with { type: "json" };
6
+ import { logger } from "./core/logger";
7
7
 
8
8
  // =============================================================================
9
9
  // App Config (from embedded package.json)
@@ -244,8 +244,8 @@ export function readConfigFile<T = unknown>(
244
244
  content: JSON.parse(content) as T,
245
245
  };
246
246
  }
247
- } catch {
248
- // Continue to next file on parse error
247
+ } catch (error) {
248
+ logger.warn("Failed to parse config file", { path: filePath, error: String(error) });
249
249
  }
250
250
  }
251
251
 
@@ -275,8 +275,8 @@ export function readAllConfigFiles<T = unknown>(
275
275
  content: JSON.parse(content) as T,
276
276
  });
277
277
  }
278
- } catch {
279
- // Skip files that fail to parse
278
+ } catch (error) {
279
+ logger.warn("Failed to parse config file", { path: filePath, error: String(error) });
280
280
  }
281
281
  }
282
282
 
@@ -9,6 +9,7 @@ import {
9
9
  getOAuthApiKey,
10
10
  loginAnthropic,
11
11
  loginAntigravity,
12
+ loginCursor,
12
13
  loginGeminiCli,
13
14
  loginGitHubCopilot,
14
15
  loginOpenAICodex,
@@ -585,6 +586,12 @@ export class AuthStorage {
585
586
  onManualCodeInput: callbacks.onManualCodeInput,
586
587
  });
587
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;
588
595
  default:
589
596
  throw new Error(`Unknown OAuth provider: ${provider}`);
590
597
  }
@@ -932,7 +939,12 @@ export class AuthStorage {
932
939
 
933
940
  this.recordSessionCredential(provider, sessionId, "oauth", selection.index);
934
941
  return result.apiKey;
935
- } catch {
942
+ } catch (error) {
943
+ logger.warn("OAuth token refresh failed, removing credential", {
944
+ provider,
945
+ index: selection.index,
946
+ error: String(error),
947
+ });
936
948
  this.removeCredentialAt(provider, selection.index);
937
949
  if (this.getCredentialsForProvider(provider).some((credential) => credential.type === "oauth")) {
938
950
  return this.getApiKey(provider, sessionId, options);
@@ -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
+ }
@@ -11,6 +11,7 @@ import * as typebox from "@sinclair/typebox";
11
11
  import { getAgentDir, getConfigDirs } from "../../config";
12
12
  import * as piCodingAgent from "../../index";
13
13
  import { execCommand } from "../exec";
14
+ import { logger } from "../logger";
14
15
  import { createReviewCommand } from "./bundled/review";
15
16
  import { createWorktreeCommand } from "./bundled/wt";
16
17
  import type {
@@ -110,7 +111,8 @@ export function discoverCustomCommands(options: DiscoverCustomCommandsOptions =
110
111
  let entries: Dirent[];
111
112
  try {
112
113
  entries = readdirSync(commandsDir, { withFileTypes: true });
113
- } catch {
114
+ } catch (error) {
115
+ logger.warn("Failed to read custom commands directory", { path: commandsDir, error: String(error) });
114
116
  continue;
115
117
  }
116
118
  for (const entry of entries) {
@@ -5,11 +5,11 @@
5
5
  * to avoid import resolution issues with custom tools loaded from user directories.
6
6
  */
7
7
 
8
- import * as os from "node:os";
9
8
  import * as path from "node:path";
10
9
  import * as typebox from "@sinclair/typebox";
11
10
  import { toolCapability } from "../../capability/tool";
12
11
  import { type CustomTool, loadCapability } from "../../discovery";
12
+ import { expandPath } from "../../discovery/helpers";
13
13
  import * as piCodingAgent from "../../index";
14
14
  import { theme } from "../../modes/interactive/theme/theme";
15
15
  import type { ExecOptions } from "../exec";
@@ -19,23 +19,6 @@ import { logger } from "../logger";
19
19
  import { getAllPluginToolPaths } from "../plugins/loader";
20
20
  import type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from "./types";
21
21
 
22
- const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
23
-
24
- function normalizeUnicodeSpaces(str: string): string {
25
- return str.replace(UNICODE_SPACES, " ");
26
- }
27
-
28
- function expandPath(p: string): string {
29
- const normalized = normalizeUnicodeSpaces(p);
30
- if (normalized.startsWith("~/")) {
31
- return path.join(os.homedir(), normalized.slice(2));
32
- }
33
- if (normalized.startsWith("~")) {
34
- return path.join(os.homedir(), normalized.slice(1));
35
- }
36
- return normalized;
37
- }
38
-
39
22
  /**
40
23
  * Resolve tool path.
41
24
  * - Absolute paths used as-is
@@ -3,13 +3,12 @@
3
3
  */
4
4
 
5
5
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
6
- import { homedir } from "node:os";
7
6
  import * as path from "node:path";
8
7
  import type { KeyId } from "@oh-my-pi/pi-tui";
9
8
  import * as TypeBox from "@sinclair/typebox";
10
9
  import { type ExtensionModule, extensionModuleCapability } from "../../capability/extension-module";
11
10
  import { loadCapability } from "../../discovery";
12
- import { getExtensionNameFromPath } from "../../discovery/helpers";
11
+ import { expandPath, getExtensionNameFromPath } from "../../discovery/helpers";
13
12
  import * as piCodingAgent from "../../index";
14
13
  import { createEventBus, type EventBus } from "../event-bus";
15
14
  import type { ExecOptions } from "../exec";
@@ -27,23 +26,6 @@ import type {
27
26
  ToolDefinition,
28
27
  } from "./types";
29
28
 
30
- const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
31
-
32
- function normalizeUnicodeSpaces(str: string): string {
33
- return str.replace(UNICODE_SPACES, " ");
34
- }
35
-
36
- function expandPath(p: string): string {
37
- const normalized = normalizeUnicodeSpaces(p);
38
- if (normalized.startsWith("~/")) {
39
- return path.join(homedir(), normalized.slice(2));
40
- }
41
- if (normalized.startsWith("~")) {
42
- return path.join(homedir(), normalized.slice(1));
43
- }
44
- return normalized;
45
- }
46
-
47
29
  function resolvePath(extPath: string, cwd: string): string {
48
30
  const expanded = expandPath(extPath);
49
31
  if (path.isAbsolute(expanded)) {
@@ -291,7 +273,8 @@ function readExtensionManifest(packageJsonPath: string): ExtensionManifest | nul
291
273
  return manifest;
292
274
  }
293
275
  return null;
294
- } catch {
276
+ } catch (error) {
277
+ logger.warn("Failed to read extension manifest", { path: packageJsonPath, error: String(error) });
295
278
  return null;
296
279
  }
297
280
  }
@@ -370,7 +353,8 @@ function discoverExtensionsInDir(dir: string): string[] {
370
353
  }
371
354
  }
372
355
  }
373
- } catch {
356
+ } catch (error) {
357
+ logger.warn("Failed to discover extensions in directory", { path: dir, error: String(error) });
374
358
  return [];
375
359
  }
376
360
 
@@ -2,12 +2,12 @@
2
2
  * Hook loader - loads TypeScript hook modules using native Bun import.
3
3
  */
4
4
 
5
- import * as os from "node:os";
6
5
  import * as path from "node:path";
7
6
  import * as typebox from "@sinclair/typebox";
8
7
  import { hookCapability } from "../../capability/hook";
9
8
  import type { Hook } from "../../discovery";
10
9
  import { loadCapability } from "../../discovery";
10
+ import { expandPath } from "../../discovery/helpers";
11
11
  import * as piCodingAgent from "../../index";
12
12
  import { logger } from "../logger";
13
13
  import type { HookMessage } from "../messages";
@@ -84,23 +84,6 @@ export interface LoadHooksResult {
84
84
  errors: Array<{ path: string; error: string }>;
85
85
  }
86
86
 
87
- const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
88
-
89
- function normalizeUnicodeSpaces(str: string): string {
90
- return str.replace(UNICODE_SPACES, " ");
91
- }
92
-
93
- function expandPath(p: string): string {
94
- const normalized = normalizeUnicodeSpaces(p);
95
- if (normalized.startsWith("~/")) {
96
- return path.join(os.homedir(), normalized.slice(2));
97
- }
98
- if (normalized.startsWith("~")) {
99
- return path.join(os.homedir(), normalized.slice(1));
100
- }
101
- return normalized;
102
- }
103
-
104
87
  /**
105
88
  * Resolve hook path.
106
89
  * - Absolute paths used as-is
@@ -10,6 +10,7 @@ import {
10
10
  setEditorKeybindings,
11
11
  } from "@oh-my-pi/pi-tui";
12
12
  import { getAgentDir } from "../config";
13
+ import { logger } from "./logger";
13
14
 
14
15
  /**
15
16
  * Application-level actions (coding agent specific).
@@ -136,7 +137,8 @@ export class KeybindingsManager {
136
137
  if (!existsSync(path)) return {};
137
138
  try {
138
139
  return JSON.parse(readFileSync(path, "utf-8"));
139
- } catch {
140
+ } catch (error) {
141
+ logger.warn("Failed to parse keybindings config", { path, error: String(error) });
140
142
  return {};
141
143
  }
142
144
  }
@@ -10,11 +10,10 @@ import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import winston from "winston";
12
12
  import DailyRotateFile from "winston-daily-rotate-file";
13
- import { CONFIG_DIR_NAME } from "../config";
14
13
 
15
14
  /** Get the logs directory (~/.omp/logs/) */
16
15
  function getLogsDir(): string {
17
- return join(homedir(), CONFIG_DIR_NAME, "logs");
16
+ return join(homedir(), ".omp", "logs");
18
17
  }
19
18
 
20
19
  /** Ensure logs directory exists */
@@ -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",
@@ -1,6 +1,7 @@
1
1
  import { join, resolve } from "node:path";
2
2
  import Handlebars from "handlebars";
3
3
  import { CONFIG_DIR_NAME, getPromptsDir } from "../config";
4
+ import { logger } from "./logger";
4
5
 
5
6
  /**
6
7
  * Represents a prompt template loaded from a markdown file
@@ -448,12 +449,12 @@ async function loadTemplatesFromDir(
448
449
  source: sourceStr,
449
450
  });
450
451
  }
451
- } catch (_error) {
452
- // Silently skip files that can't be read
452
+ } catch (error) {
453
+ logger.warn("Failed to load prompt template", { path: fullPath, error: String(error) });
453
454
  }
454
455
  }
455
- } catch (_error) {
456
- // Silently skip directories that can't be read
456
+ } catch (error) {
457
+ logger.warn("Failed to scan prompt templates directory", { dir, error: String(error) });
457
458
  }
458
459
 
459
460
  return templates;
package/src/core/sdk.ts CHANGED
@@ -27,8 +27,8 @@
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";
31
- import type { Message, Model } from "@oh-my-pi/pi-ai";
30
+ import { Agent, type AgentEvent, type AgentMessage, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
31
+ import { type Message, type Model, supportsXhigh } from "@oh-my-pi/pi-ai";
32
32
  import type { Component } from "@oh-my-pi/pi-tui";
33
33
  import chalk from "chalk";
34
34
  // Import discovery to register all providers on startup
@@ -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,
@@ -630,6 +631,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
630
631
  // Clamp to model capabilities
631
632
  if (!model || !model.reasoning) {
632
633
  thinkingLevel = "off";
634
+ } else if (thinkingLevel === "xhigh" && !supportsXhigh(model)) {
635
+ thinkingLevel = "high";
633
636
  }
634
637
 
635
638
  let skills: Skill[];
@@ -854,6 +857,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
854
857
  }
855
858
  time("combineTools");
856
859
 
860
+ let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
861
+ const cursorExecHandlers = createCursorExecHandlers({
862
+ cwd,
863
+ tools: toolRegistry,
864
+ getToolContext: toolContextStore.getContext,
865
+ emitEvent: (event) => cursorEventEmitter?.(event),
866
+ });
867
+
857
868
  const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
858
869
  toolContextStore.setToolNames(toolNames);
859
870
  const defaultPrompt = await buildSystemPromptInternal({
@@ -964,7 +975,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
964
975
  }
965
976
  return key;
966
977
  },
978
+ cursorExecHandlers,
967
979
  });
980
+ cursorEventEmitter = (event) => agent.emitExternalEvent(event);
968
981
  time("createAgent");
969
982
 
970
983
  // Restore messages if session has existing data
@@ -1010,8 +1023,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1010
1023
  });
1011
1024
  lspServers = result.servers;
1012
1025
  time("warmupLspServers");
1013
- } catch {
1014
- // Ignore warmup errors
1026
+ } catch (error) {
1027
+ logger.warn("LSP server warmup failed", { cwd, error: String(error) });
1015
1028
  }
1016
1029
  }
1017
1030
 
@@ -7,6 +7,7 @@ import type { SourceMeta } from "../capability/types";
7
7
  import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
8
8
  import { loadCapability } from "../discovery";
9
9
  import { parseFrontmatter } from "../discovery/helpers";
10
+ import { logger } from "./logger";
10
11
  import type { SkillsSettings } from "./settings-manager";
11
12
 
12
13
  // Re-export SkillFrontmatter for backward compatibility
@@ -67,8 +68,8 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills
67
68
  source: options.source,
68
69
  });
69
70
  }
70
- } catch {
71
- // Skip invalid skills
71
+ } catch (error) {
72
+ logger.warn("Failed to load skill", { path: skillFile, error: String(error) });
72
73
  }
73
74
  }
74
75
 
@@ -131,8 +132,8 @@ function scanDirectoryForSkills(dir: string): LoadSkillsResult {
131
132
  source: "custom",
132
133
  });
133
134
  }
134
- } catch {
135
- // Skip invalid skills
135
+ } catch (error) {
136
+ logger.warn("Failed to load skill", { path: skillFile, error: String(error) });
136
137
  }
137
138
  }
138
139