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

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 (58) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/docs/sdk.md +5 -5
  3. package/examples/sdk/10-settings.ts +2 -2
  4. package/package.json +5 -5
  5. package/src/capability/fs.ts +90 -0
  6. package/src/capability/index.ts +41 -227
  7. package/src/capability/types.ts +1 -11
  8. package/src/cli/args.ts +4 -0
  9. package/src/core/agent-session.ts +4 -4
  10. package/src/core/agent-storage.ts +50 -0
  11. package/src/core/auth-storage.ts +112 -4
  12. package/src/core/bash-executor.ts +1 -1
  13. package/src/core/custom-tools/loader.ts +2 -2
  14. package/src/core/extensions/loader.ts +2 -2
  15. package/src/core/extensions/types.ts +1 -1
  16. package/src/core/hooks/loader.ts +2 -2
  17. package/src/core/mcp/config.ts +2 -2
  18. package/src/core/model-registry.ts +46 -0
  19. package/src/core/sdk.ts +37 -29
  20. package/src/core/settings-manager.ts +152 -135
  21. package/src/core/skills.ts +72 -51
  22. package/src/core/slash-commands.ts +3 -3
  23. package/src/core/system-prompt.ts +10 -10
  24. package/src/core/tools/edit.ts +7 -4
  25. package/src/core/tools/find.ts +2 -2
  26. package/src/core/tools/index.test.ts +16 -0
  27. package/src/core/tools/index.ts +21 -8
  28. package/src/core/tools/lsp/index.ts +4 -1
  29. package/src/core/tools/ssh.ts +6 -6
  30. package/src/core/tools/task/commands.ts +3 -5
  31. package/src/core/tools/task/executor.ts +88 -3
  32. package/src/core/tools/task/index.ts +4 -0
  33. package/src/core/tools/task/model-resolver.ts +10 -7
  34. package/src/core/tools/task/worker-protocol.ts +48 -2
  35. package/src/core/tools/task/worker.ts +152 -7
  36. package/src/core/tools/write.ts +7 -4
  37. package/src/discovery/agents-md.ts +13 -19
  38. package/src/discovery/builtin.ts +367 -247
  39. package/src/discovery/claude.ts +181 -290
  40. package/src/discovery/cline.ts +30 -10
  41. package/src/discovery/codex.ts +185 -244
  42. package/src/discovery/cursor.ts +106 -121
  43. package/src/discovery/gemini.ts +72 -97
  44. package/src/discovery/github.ts +7 -10
  45. package/src/discovery/helpers.ts +94 -88
  46. package/src/discovery/index.ts +1 -2
  47. package/src/discovery/mcp-json.ts +15 -18
  48. package/src/discovery/ssh.ts +9 -17
  49. package/src/discovery/vscode.ts +10 -5
  50. package/src/discovery/windsurf.ts +52 -86
  51. package/src/main.ts +5 -1
  52. package/src/modes/interactive/components/extensions/extension-dashboard.ts +24 -11
  53. package/src/modes/interactive/components/extensions/state-manager.ts +19 -15
  54. package/src/modes/interactive/controllers/selector-controller.ts +6 -2
  55. package/src/modes/interactive/interactive-mode.ts +19 -15
  56. package/src/prompts/agents/plan.md +107 -30
  57. package/src/utils/shell.ts +2 -2
  58. package/src/prompts/agents/planner.md +0 -112
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { type Settings, settingsCapability } from "../../../capability/settings";
15
- import { loadSync } from "../../../discovery";
15
+ import { loadCapability } from "../../../discovery";
16
16
  import { resolveOmpCommand } from "./omp-command";
17
17
 
18
18
  /** Cache for available models (provider/modelId format) */
@@ -79,8 +79,8 @@ export function clearModelCache(): void {
79
79
  /**
80
80
  * Load model roles from settings files using capability API.
81
81
  */
82
- function loadModelRoles(): Record<string, string> {
83
- const result = loadSync<Settings>(settingsCapability.id, { cwd: process.cwd() });
82
+ async function loadModelRoles(): Promise<Record<string, string>> {
83
+ const result = await loadCapability<Settings>(settingsCapability.id, { cwd: process.cwd() });
84
84
 
85
85
  // Merge all settings, prioritizing first (highest priority)
86
86
  let modelRoles: Record<string, string> = {};
@@ -99,8 +99,8 @@ function loadModelRoles(): Record<string, string> {
99
99
  * Looks up the role in settings.modelRoles and returns the configured model.
100
100
  * Returns undefined if the role isn't configured.
101
101
  */
102
- function resolveOmpAlias(role: string, availableModels: string[]): string | undefined {
103
- const roles = loadModelRoles();
102
+ async function resolveOmpAlias(role: string, availableModels: string[]): Promise<string | undefined> {
103
+ const roles = await loadModelRoles();
104
104
 
105
105
  // Look up role in settings (case-insensitive)
106
106
  const configured = roles[role] || roles[role.toLowerCase()];
@@ -127,7 +127,10 @@ function getModelId(fullModel: string): string {
127
127
  * @param pattern - Model pattern to resolve
128
128
  * @param availableModels - Optional pre-fetched list of available models (in provider/modelId format)
129
129
  */
130
- export function resolveModelPattern(pattern: string | undefined, availableModels?: string[]): string | undefined {
130
+ export async function resolveModelPattern(
131
+ pattern: string | undefined,
132
+ availableModels?: string[],
133
+ ): Promise<string | undefined> {
131
134
  if (!pattern || pattern === "default") {
132
135
  return undefined;
133
136
  }
@@ -149,7 +152,7 @@ export function resolveModelPattern(pattern: string | undefined, availableModels
149
152
  const lower = p.toLowerCase();
150
153
  if (lower.startsWith("omp/") || lower.startsWith("pi/")) {
151
154
  const role = lower.startsWith("omp/") ? p.slice(4) : p.slice(3);
152
- const resolved = resolveOmpAlias(role, models);
155
+ const resolved = await resolveOmpAlias(role, models);
153
156
  if (resolved) return resolved;
154
157
  continue; // Role not configured, try next pattern
155
158
  }
@@ -1,4 +1,42 @@
1
1
  import type { AgentEvent } from "@oh-my-pi/pi-agent-core";
2
+ import type { SerializedAuthStorage } from "../../auth-storage";
3
+ import type { SerializedModelRegistry } from "../../model-registry";
4
+
5
+ /**
6
+ * MCP tool metadata passed from parent to worker for proxy tool creation.
7
+ */
8
+ export interface MCPToolMetadata {
9
+ name: string;
10
+ label: string;
11
+ description: string;
12
+ parameters: unknown;
13
+ serverName: string;
14
+ mcpToolName: string;
15
+ timeoutMs?: number;
16
+ }
17
+
18
+ /**
19
+ * Worker -> Parent: request to execute an MCP tool via parent's connection.
20
+ */
21
+ export interface MCPToolCallRequest {
22
+ type: "mcp_tool_call";
23
+ callId: string;
24
+ toolName: string;
25
+ params: Record<string, unknown>;
26
+ }
27
+
28
+ /**
29
+ * Parent -> Worker: result of an MCP tool call.
30
+ */
31
+ export interface MCPToolCallResponse {
32
+ type: "mcp_tool_result";
33
+ callId: string;
34
+ result?: {
35
+ content: Array<{ type: string; text?: string; [key: string]: unknown }>;
36
+ isError?: boolean;
37
+ };
38
+ error?: string;
39
+ }
2
40
 
3
41
  export interface SubagentWorkerStartPayload {
4
42
  cwd: string;
@@ -7,12 +45,20 @@ export interface SubagentWorkerStartPayload {
7
45
  model?: string;
8
46
  toolNames?: string[];
9
47
  outputSchema?: unknown;
48
+ enableLsp?: boolean;
10
49
  sessionFile?: string | null;
11
50
  spawnsEnv?: string;
51
+ serializedAuth?: SerializedAuthStorage;
52
+ serializedModels?: SerializedModelRegistry;
53
+ mcpTools?: MCPToolMetadata[];
12
54
  }
13
55
 
14
- export type SubagentWorkerRequest = { type: "start"; payload: SubagentWorkerStartPayload } | { type: "abort" };
56
+ export type SubagentWorkerRequest =
57
+ | { type: "start"; payload: SubagentWorkerStartPayload }
58
+ | { type: "abort" }
59
+ | MCPToolCallResponse;
15
60
 
16
61
  export type SubagentWorkerResponse =
17
62
  | { type: "event"; event: AgentEvent }
18
- | { type: "done"; exitCode: number; durationMs: number; error?: string; aborted?: boolean };
63
+ | { type: "done"; exitCode: number; durationMs: number; error?: string; aborted?: boolean }
64
+ | MCPToolCallRequest;
@@ -15,12 +15,22 @@
15
15
 
16
16
  import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
17
17
  import type { Api, Model } from "@oh-my-pi/pi-ai";
18
+ import type { TSchema } from "@sinclair/typebox";
18
19
  import type { AgentSessionEvent } from "../../agent-session";
20
+ import { AuthStorage } from "../../auth-storage";
21
+ import type { CustomTool } from "../../custom-tools/types";
22
+ import { ModelRegistry } from "../../model-registry";
19
23
  import { parseModelPattern, parseModelString } from "../../model-resolver";
20
24
  import { createAgentSession, discoverAuthStorage, discoverModels } from "../../sdk";
21
25
  import { SessionManager } from "../../session-manager";
22
26
  import { untilAborted } from "../../utils";
23
- import type { SubagentWorkerRequest, SubagentWorkerResponse, SubagentWorkerStartPayload } from "./worker-protocol";
27
+ import type {
28
+ MCPToolCallResponse,
29
+ MCPToolMetadata,
30
+ SubagentWorkerRequest,
31
+ SubagentWorkerResponse,
32
+ SubagentWorkerStartPayload,
33
+ } from "./worker-protocol";
24
34
 
25
35
  type PostMessageFn = (message: SubagentWorkerResponse) => void;
26
36
 
@@ -32,6 +42,120 @@ const postMessageSafe: PostMessageFn = (message) => {
32
42
  }
33
43
  };
34
44
 
45
+ interface PendingMCPCall {
46
+ resolve: (result: MCPToolCallResponse["result"]) => void;
47
+ reject: (error: Error) => void;
48
+ timeoutId: ReturnType<typeof setTimeout>;
49
+ }
50
+
51
+ const pendingMCPCalls = new Map<string, PendingMCPCall>();
52
+ const MCP_CALL_TIMEOUT_MS = 60_000;
53
+ let mcpCallIdCounter = 0;
54
+
55
+ function generateMCPCallId(): string {
56
+ return `mcp_${Date.now()}_${++mcpCallIdCounter}`;
57
+ }
58
+
59
+ function callMCPToolViaParent(
60
+ toolName: string,
61
+ params: Record<string, unknown>,
62
+ signal?: AbortSignal,
63
+ timeoutMs = MCP_CALL_TIMEOUT_MS,
64
+ ): Promise<{ content: Array<{ type: string; text?: string; [key: string]: unknown }>; isError?: boolean }> {
65
+ return new Promise((resolve, reject) => {
66
+ const callId = generateMCPCallId();
67
+ if (signal?.aborted) {
68
+ reject(new Error("Aborted"));
69
+ return;
70
+ }
71
+
72
+ const timeoutId = setTimeout(() => {
73
+ pendingMCPCalls.delete(callId);
74
+ reject(new Error(`MCP call timed out after ${timeoutMs}ms`));
75
+ }, timeoutMs);
76
+
77
+ const cleanup = () => {
78
+ clearTimeout(timeoutId);
79
+ pendingMCPCalls.delete(callId);
80
+ };
81
+
82
+ signal?.addEventListener(
83
+ "abort",
84
+ () => {
85
+ cleanup();
86
+ reject(new Error("Aborted"));
87
+ },
88
+ { once: true },
89
+ );
90
+
91
+ pendingMCPCalls.set(callId, {
92
+ resolve: (result) => {
93
+ cleanup();
94
+ resolve(result ?? { content: [] });
95
+ },
96
+ reject: (error) => {
97
+ cleanup();
98
+ reject(error);
99
+ },
100
+ timeoutId,
101
+ });
102
+
103
+ postMessageSafe({
104
+ type: "mcp_tool_call",
105
+ callId,
106
+ toolName,
107
+ params,
108
+ } as SubagentWorkerResponse);
109
+ });
110
+ }
111
+
112
+ function handleMCPToolResult(response: MCPToolCallResponse): void {
113
+ const pending = pendingMCPCalls.get(response.callId);
114
+ if (!pending) return;
115
+ if (response.error) {
116
+ pending.reject(new Error(response.error));
117
+ } else {
118
+ pending.resolve(response.result);
119
+ }
120
+ }
121
+
122
+ function createMCPProxyTool(metadata: MCPToolMetadata): CustomTool<TSchema> {
123
+ return {
124
+ name: metadata.name,
125
+ label: metadata.label,
126
+ description: metadata.description,
127
+ parameters: metadata.parameters as TSchema,
128
+ execute: async (_toolCallId, params, _onUpdate, _ctx, signal) => {
129
+ try {
130
+ const result = await callMCPToolViaParent(
131
+ metadata.name,
132
+ params as Record<string, unknown>,
133
+ signal,
134
+ metadata.timeoutMs,
135
+ );
136
+ return {
137
+ content: result.content.map((c) =>
138
+ c.type === "text"
139
+ ? { type: "text" as const, text: c.text ?? "" }
140
+ : { type: "text" as const, text: JSON.stringify(c) },
141
+ ),
142
+ details: { serverName: metadata.serverName, mcpToolName: metadata.mcpToolName, isError: result.isError },
143
+ };
144
+ } catch (error) {
145
+ return {
146
+ content: [
147
+ {
148
+ type: "text" as const,
149
+ text: `MCP error: ${error instanceof Error ? error.message : String(error)}`,
150
+ },
151
+ ],
152
+ details: { serverName: metadata.serverName, mcpToolName: metadata.mcpToolName, isError: true },
153
+ };
154
+ }
155
+ },
156
+ };
157
+ }
158
+
35
159
  interface WorkerMessageEvent<T> {
36
160
  data: T;
37
161
  }
@@ -145,11 +269,22 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
145
269
  // Set working directory (CLI does this implicitly)
146
270
  process.chdir(payload.cwd);
147
271
 
148
- // Discover auth and models (equivalent to CLI's discoverAuthStorage/discoverModels)
149
- const authStorage = await discoverAuthStorage();
150
- checkAbort();
151
- const modelRegistry = await discoverModels(authStorage);
152
- checkAbort();
272
+ // Use serialized auth/models if provided, otherwise discover from disk
273
+ let authStorage: AuthStorage;
274
+ let modelRegistry: ModelRegistry;
275
+
276
+ if (payload.serializedAuth && payload.serializedModels) {
277
+ authStorage = AuthStorage.fromSerialized(payload.serializedAuth);
278
+ modelRegistry = ModelRegistry.fromSerialized(payload.serializedModels, authStorage);
279
+ } else {
280
+ authStorage = await discoverAuthStorage();
281
+ checkAbort();
282
+ modelRegistry = await discoverModels(authStorage);
283
+ checkAbort();
284
+ }
285
+
286
+ // Create MCP proxy tools if provided
287
+ const mcpProxyTools = payload.mcpTools?.map(createMCPProxyTool) ?? [];
153
288
 
154
289
  // Resolve model override (equivalent to CLI's parseModelPattern with --model)
155
290
  const { model, thinkingLevel } = resolveModelOverride(payload.model, modelRegistry);
@@ -180,6 +315,11 @@ async function runTask(runState: RunState, payload: SubagentWorkerStartPayload):
180
315
  hasUI: false,
181
316
  // Pass spawn restrictions to nested tasks
182
317
  spawns: payload.spawnsEnv,
318
+ enableLsp: payload.enableLsp ?? true,
319
+ // Disable local MCP discovery if using proxy tools
320
+ enableMCP: !payload.mcpTools,
321
+ // Add MCP proxy tools
322
+ customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
183
323
  });
184
324
 
185
325
  runState.session = session;
@@ -394,7 +534,7 @@ self.addEventListener("messageerror", () => {
394
534
  reportFatal("Failed to deserialize parent message");
395
535
  });
396
536
 
397
- // Message handler - receives start/abort commands from parent
537
+ // Message handler - receives start/abort/mcp_tool_result commands from parent
398
538
  globalThis.addEventListener("message", (event: WorkerMessageEvent<SubagentWorkerRequest>) => {
399
539
  const message = event.data;
400
540
  if (!message) return;
@@ -404,6 +544,11 @@ globalThis.addEventListener("message", (event: WorkerMessageEvent<SubagentWorker
404
544
  return;
405
545
  }
406
546
 
547
+ if (message.type === "mcp_tool_result") {
548
+ handleMCPToolResult(message);
549
+ return;
550
+ }
551
+
407
552
  if (message.type === "start") {
408
553
  // Only allow one task per worker
409
554
  if (activeRun) return;
@@ -8,7 +8,7 @@ import type { RenderResultOptions } from "../custom-tools/types";
8
8
  import { renderPromptTemplate } from "../prompt-templates";
9
9
  import type { ToolSession } from "../sdk";
10
10
  import { untilAborted } from "../utils";
11
- import { createLspWritethrough, type FileDiagnosticsResult } from "./lsp/index";
11
+ import { createLspWritethrough, type FileDiagnosticsResult, writethroughNoop } from "./lsp/index";
12
12
  import { resolveToCwd } from "./path-utils";
13
13
  import { formatDiagnostics, replaceTabs, shortenPath } from "./render-utils";
14
14
 
@@ -23,9 +23,12 @@ export interface WriteToolDetails {
23
23
  }
24
24
 
25
25
  export function createWriteTool(session: ToolSession): AgentTool<typeof writeSchema, WriteToolDetails> {
26
- const enableFormat = session.settings?.getLspFormatOnWrite() ?? true;
27
- const enableDiagnostics = session.settings?.getLspDiagnosticsOnWrite() ?? true;
28
- const writethrough = createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics });
26
+ const enableLsp = session.enableLsp ?? true;
27
+ const enableFormat = enableLsp ? (session.settings?.getLspFormatOnWrite() ?? true) : false;
28
+ const enableDiagnostics = enableLsp ? (session.settings?.getLspDiagnosticsOnWrite() ?? true) : false;
29
+ const writethrough = enableLsp
30
+ ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
31
+ : writethroughNoop;
29
32
  return {
30
33
  name: "write",
31
34
  label: "Write",
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { dirname, join, sep } from "node:path";
10
10
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
11
+ import { readFile } from "../capability/fs";
11
12
  import { registerProvider } from "../capability/index";
12
13
  import type { LoadContext, LoadResult } from "../capability/types";
13
14
  import { calculateDepth, createSourceMeta } from "./helpers";
@@ -19,7 +20,7 @@ const MAX_DEPTH = 20; // Prevent walking up excessively far from cwd
19
20
  /**
20
21
  * Load standalone AGENTS.md files.
21
22
  */
22
- function loadAgentsMd(ctx: LoadContext): LoadResult<ContextFile> {
23
+ async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
23
24
  const items: ContextFile[] = [];
24
25
  const warnings: string[] = [];
25
26
 
@@ -29,30 +30,23 @@ function loadAgentsMd(ctx: LoadContext): LoadResult<ContextFile> {
29
30
 
30
31
  while (depth < MAX_DEPTH) {
31
32
  const candidate = join(current, "AGENTS.md");
33
+ const content = await readFile(candidate);
32
34
 
33
- if (ctx.fs.isFile(candidate)) {
34
- // Skip if it's inside a config directory (handled by other providers)
35
+ if (content !== null) {
35
36
  const parent = dirname(candidate);
36
37
  const baseName = parent.split(sep).pop() ?? "";
37
38
 
38
- // Skip if inside .codex, .gemini, or other config dirs
39
39
  if (!baseName.startsWith(".")) {
40
- const content = ctx.fs.readFile(candidate);
40
+ const fileDir = dirname(candidate);
41
+ const calculatedDepth = calculateDepth(ctx.cwd, fileDir, sep);
41
42
 
42
- if (content === null) {
43
- warnings.push(`Failed to read: ${candidate}`);
44
- } else {
45
- const fileDir = dirname(candidate);
46
- const calculatedDepth = calculateDepth(ctx.cwd, fileDir, sep);
47
-
48
- items.push({
49
- path: candidate,
50
- content,
51
- level: "project",
52
- depth: calculatedDepth,
53
- _source: createSourceMeta(PROVIDER_ID, candidate, "project"),
54
- });
55
- }
43
+ items.push({
44
+ path: candidate,
45
+ content,
46
+ level: "project",
47
+ depth: calculatedDepth,
48
+ _source: createSourceMeta(PROVIDER_ID, candidate, "project"),
49
+ });
56
50
  }
57
51
  }
58
52