@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
@@ -1,10 +1,11 @@
1
- import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { realpath } from "node:fs/promises";
2
3
  import { basename, join } from "node:path";
3
4
  import { minimatch } from "minimatch";
4
5
  import { skillCapability } from "../capability/skill";
5
6
  import type { SourceMeta } from "../capability/types";
6
7
  import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
7
- import { loadSync } from "../discovery";
8
+ import { loadCapability } from "../discovery";
8
9
  import { parseFrontmatter } from "../discovery/helpers";
9
10
  import type { SkillsSettings } from "./settings-manager";
10
11
 
@@ -215,7 +216,7 @@ export interface LoadSkillsOptions extends SkillsSettings {
215
216
  * Load skills from all configured locations.
216
217
  * Returns skills and any validation warnings.
217
218
  */
218
- export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
219
+ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadSkillsResult> {
219
220
  const {
220
221
  cwd = process.cwd(),
221
222
  enabled = true,
@@ -247,7 +248,7 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
247
248
  }
248
249
 
249
250
  // Use capability API to load all skills
250
- const result = loadSync<CapabilitySkill>(skillCapability.id, { cwd });
251
+ const result = await loadCapability<CapabilitySkill>(skillCapability.id, { cwd });
251
252
 
252
253
  const skillMap = new Map<string, Skill>();
253
254
  const realPathSet = new Set<string>();
@@ -265,28 +266,33 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
265
266
  return ignoredSkills.some((pattern) => minimatch(name, pattern));
266
267
  }
267
268
 
268
- // Helper to add a skill to the map
269
- function addSkill(capSkill: CapabilitySkill, sourceProvider: string) {
270
- // Apply ignore filter (glob patterns) - takes precedence over include
271
- if (matchesIgnorePatterns(capSkill.name)) {
272
- return;
273
- }
274
- // Apply include filter (glob patterns)
275
- if (!matchesIncludePatterns(capSkill.name)) {
276
- return;
277
- }
269
+ // Filter skills by source and patterns first
270
+ const filteredSkills = result.items.filter((capSkill) => {
271
+ if (!isSourceEnabled(capSkill._source)) return false;
272
+ if (matchesIgnorePatterns(capSkill.name)) return false;
273
+ if (!matchesIncludePatterns(capSkill.name)) return false;
274
+ return true;
275
+ });
276
+
277
+ // Batch resolve all real paths in parallel
278
+ const realPaths = await Promise.all(
279
+ filteredSkills.map(async (capSkill) => {
280
+ try {
281
+ return await realpath(capSkill.path);
282
+ } catch {
283
+ return capSkill.path;
284
+ }
285
+ }),
286
+ );
278
287
 
279
- // Resolve symlinks to detect duplicate files
280
- let realPath: string;
281
- try {
282
- realPath = realpathSync(capSkill.path);
283
- } catch {
284
- realPath = capSkill.path;
285
- }
288
+ // Process skills with resolved paths
289
+ for (let i = 0; i < filteredSkills.length; i++) {
290
+ const capSkill = filteredSkills[i];
291
+ const resolvedPath = realPaths[i];
286
292
 
287
293
  // Skip silently if we've already loaded this exact file (via symlink)
288
- if (realPathSet.has(realPath)) {
289
- return;
294
+ if (realPathSet.has(resolvedPath)) {
295
+ continue;
290
296
  }
291
297
 
292
298
  const existing = skillMap.get(capSkill.name);
@@ -302,46 +308,61 @@ export function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {
302
308
  description: capSkill.frontmatter?.description || "",
303
309
  filePath: capSkill.path,
304
310
  baseDir: capSkill.path.replace(/\/SKILL\.md$/, ""),
305
- source: `${sourceProvider}:${capSkill.level}`,
311
+ source: `${capSkill._source.provider}:${capSkill.level}`,
306
312
  _source: capSkill._source,
307
313
  };
308
314
  skillMap.set(capSkill.name, skill);
309
- realPathSet.add(realPath);
310
- }
311
- }
312
-
313
- // Process skills from capability API
314
- for (const capSkill of result.items) {
315
- // Check if this source is enabled
316
- if (!isSourceEnabled(capSkill._source)) {
317
- continue;
315
+ realPathSet.add(resolvedPath);
318
316
  }
319
-
320
- addSkill(capSkill, capSkill._source.provider);
321
317
  }
322
318
 
323
319
  // Process custom directories - scan directly without using full provider system
320
+ const allCustomSkills: Array<{ skill: Skill; path: string }> = [];
324
321
  for (const dir of customDirectories) {
325
322
  const customSkills = scanDirectoryForSkills(dir);
326
323
  for (const s of customSkills.skills) {
327
- // Convert to capability format for addSkill processing
328
- const capSkill: CapabilitySkill = {
329
- name: s.name,
330
- path: s.filePath,
331
- content: "",
332
- frontmatter: { description: s.description },
333
- level: "user",
334
- _source: {
335
- provider: "custom",
336
- providerName: "Custom",
337
- path: s.filePath,
338
- level: "user",
324
+ if (matchesIgnorePatterns(s.name)) continue;
325
+ if (!matchesIncludePatterns(s.name)) continue;
326
+ allCustomSkills.push({
327
+ skill: {
328
+ name: s.name,
329
+ description: s.description,
330
+ filePath: s.filePath,
331
+ baseDir: s.filePath.replace(/\/SKILL\.md$/, ""),
332
+ source: "custom:user",
333
+ _source: { provider: "custom", providerName: "Custom", path: s.filePath, level: "user" },
339
334
  },
340
- };
341
- addSkill(capSkill, "custom");
335
+ path: s.filePath,
336
+ });
342
337
  }
343
- for (const warning of customSkills.warnings) {
344
- collisionWarnings.push(warning);
338
+ collisionWarnings.push(...customSkills.warnings);
339
+ }
340
+
341
+ // Batch resolve custom skill paths
342
+ const customRealPaths = await Promise.all(
343
+ allCustomSkills.map(async ({ path }) => {
344
+ try {
345
+ return await realpath(path);
346
+ } catch {
347
+ return path;
348
+ }
349
+ }),
350
+ );
351
+
352
+ for (let i = 0; i < allCustomSkills.length; i++) {
353
+ const { skill } = allCustomSkills[i];
354
+ const resolvedPath = customRealPaths[i];
355
+ if (realPathSet.has(resolvedPath)) continue;
356
+
357
+ const existing = skillMap.get(skill.name);
358
+ if (existing) {
359
+ collisionWarnings.push({
360
+ skillPath: skill.filePath,
361
+ message: `name collision: "${skill.name}" already loaded from ${existing.filePath}, skipping this one`,
362
+ });
363
+ } else {
364
+ skillMap.set(skill.name, skill);
365
+ realPathSet.add(resolvedPath);
345
366
  }
346
367
  }
347
368
 
@@ -1,6 +1,6 @@
1
1
  import { slashCommandCapability } from "../capability/slash-command";
2
2
  import type { SlashCommand } from "../discovery";
3
- import { loadSync } from "../discovery";
3
+ import { loadCapability } from "../discovery";
4
4
  import { parseFrontmatter } from "../discovery/helpers";
5
5
  import { renderPromptTemplate } from "./prompt-templates";
6
6
  import { EMBEDDED_COMMAND_TEMPLATES } from "./tools/task/commands";
@@ -108,8 +108,8 @@ export interface LoadSlashCommandsOptions {
108
108
  * Load all custom slash commands using the capability API.
109
109
  * Loads from all registered providers (builtin, user, project).
110
110
  */
111
- export function loadSlashCommands(options: LoadSlashCommandsOptions = {}): FileSlashCommand[] {
112
- const result = loadSync<SlashCommand>(slashCommandCapability.id, { cwd: options.cwd });
111
+ export async function loadSlashCommands(options: LoadSlashCommandsOptions = {}): Promise<FileSlashCommand[]> {
112
+ const result = await loadCapability<SlashCommand>(slashCommandCapability.id, { cwd: options.cwd });
113
113
 
114
114
  const fileCommands: FileSlashCommand[] = result.items.map((cmd) => {
115
115
  const { description, body } = parseCommandTemplate(cmd.content);
@@ -8,7 +8,7 @@ import { join } from "node:path";
8
8
  import chalk from "chalk";
9
9
  import { contextFileCapability } from "../capability/context-file";
10
10
  import { systemPromptCapability } from "../capability/system-prompt";
11
- import { type ContextFile, loadSync, type SystemPrompt as SystemPromptFile } from "../discovery/index";
11
+ import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "../discovery/index";
12
12
  import customSystemPromptTemplate from "../prompts/system/custom-system-prompt.md" with { type: "text" };
13
13
  import systemPromptTemplate from "../prompts/system/system-prompt.md" with { type: "text" };
14
14
  import { renderPromptTemplate } from "./prompt-templates";
@@ -558,12 +558,12 @@ export interface LoadContextFilesOptions {
558
558
  * Returns {path, content, depth} entries for all discovered context files.
559
559
  * Files are sorted by depth (descending) so files closer to cwd appear last/more prominent.
560
560
  */
561
- export function loadProjectContextFiles(
561
+ export async function loadProjectContextFiles(
562
562
  options: LoadContextFilesOptions = {},
563
- ): Array<{ path: string; content: string; depth?: number }> {
563
+ ): Promise<Array<{ path: string; content: string; depth?: number }>> {
564
564
  const resolvedCwd = options.cwd ?? process.cwd();
565
565
 
566
- const result = loadSync(contextFileCapability.id, { cwd: resolvedCwd });
566
+ const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
567
567
 
568
568
  // Convert ContextFile items and preserve depth info
569
569
  const files = result.items.map((item) => {
@@ -590,10 +590,10 @@ export function loadProjectContextFiles(
590
590
  * Load system prompt customization files (SYSTEM.md).
591
591
  * Returns combined content from all discovered SYSTEM.md files.
592
592
  */
593
- export function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): string | null {
593
+ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): Promise<string | null> {
594
594
  const resolvedCwd = options.cwd ?? process.cwd();
595
595
 
596
- const result = loadSync<SystemPromptFile>(systemPromptCapability.id, { cwd: resolvedCwd });
596
+ const result = await loadCapability<SystemPromptFile>(systemPromptCapability.id, { cwd: resolvedCwd });
597
597
 
598
598
  if (result.items.length === 0) return null;
599
599
 
@@ -631,7 +631,7 @@ export interface BuildSystemPromptOptions {
631
631
  }
632
632
 
633
633
  /** Build the system prompt with tools, guidelines, and context */
634
- export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
634
+ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<string> {
635
635
  const {
636
636
  customPrompt,
637
637
  tools,
@@ -648,7 +648,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
648
648
  const resolvedAppendPrompt = resolvePromptInput(appendSystemPrompt, "append system prompt");
649
649
 
650
650
  // Load SYSTEM.md customization (prepended to prompt)
651
- const systemPromptCustomization = loadSystemPromptFiles({ cwd: resolvedCwd });
651
+ const systemPromptCustomization = await loadSystemPromptFiles({ cwd: resolvedCwd });
652
652
 
653
653
  const now = new Date();
654
654
  const dateTime = now.toLocaleString("en-US", {
@@ -663,7 +663,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
663
663
  });
664
664
 
665
665
  // Resolve context files: use provided or discover
666
- const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd });
666
+ const contextFiles = providedContextFiles ?? (await loadProjectContextFiles({ cwd: resolvedCwd }));
667
667
  const agentsMdSearch = buildAgentsMdSearch(resolvedCwd);
668
668
 
669
669
  // Build tool descriptions array
@@ -688,7 +688,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
688
688
  // Resolve skills: use provided or discover
689
689
  const skills =
690
690
  providedSkills ??
691
- (skillsSettings?.enabled !== false ? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).skills : []);
691
+ (skillsSettings?.enabled !== false ? (await loadSkills({ ...skillsSettings, cwd: resolvedCwd })).skills : []);
692
692
 
693
693
  // Get git context
694
694
  const git = loadGitContext(resolvedCwd);
@@ -19,7 +19,7 @@ import {
19
19
  stripBom,
20
20
  } from "./edit-diff";
21
21
  import type { ToolSession } from "./index";
22
- import { createLspWritethrough, type FileDiagnosticsResult } from "./lsp/index";
22
+ import { createLspWritethrough, type FileDiagnosticsResult, writethroughNoop } from "./lsp/index";
23
23
  import { resolveToCwd } from "./path-utils";
24
24
  import { createToolUIKit, getDiffStats, shortenPath, truncateDiffByHunk } from "./render-utils";
25
25
 
@@ -43,9 +43,12 @@ export interface EditToolDetails {
43
43
 
44
44
  export function createEditTool(session: ToolSession): AgentTool<typeof editSchema> {
45
45
  const allowFuzzy = session.settings?.getEditFuzzyMatch() ?? true;
46
- const enableDiagnostics = session.settings?.getLspDiagnosticsOnEdit() ?? false;
47
- const enableFormat = session.settings?.getLspFormatOnWrite() ?? true;
48
- const writethrough = createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics });
46
+ const enableLsp = session.enableLsp ?? true;
47
+ const enableDiagnostics = enableLsp ? (session.settings?.getLspDiagnosticsOnEdit() ?? false) : false;
48
+ const enableFormat = enableLsp ? (session.settings?.getLspFormatOnWrite() ?? true) : false;
49
+ const writethrough = enableLsp
50
+ ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
51
+ : writethroughNoop;
49
52
  return {
50
53
  name: "edit",
51
54
  label: "Edit",
@@ -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
@@ -34,6 +34,22 @@ describe("createTools", () => {
34
34
  expect(names).toContain("web_search");
35
35
  });
36
36
 
37
+ it("excludes lsp tool when session disables LSP", async () => {
38
+ const session = createTestSession({ enableLsp: false });
39
+ const tools = await createTools(session, ["read", "lsp", "write"]);
40
+ const names = tools.map((t) => t.name);
41
+
42
+ expect(names).toEqual(["read", "write"]);
43
+ });
44
+
45
+ it("excludes lsp tool when disabled", async () => {
46
+ const session = createTestSession({ enableLsp: false });
47
+ const tools = await createTools(session);
48
+ const names = tools.map((t) => t.name);
49
+
50
+ expect(names).not.toContain("lsp");
51
+ });
52
+
37
53
  it("respects requested tool subset", async () => {
38
54
  const session = createTestSession();
39
55
  const tools = await createTools(session, ["read", "write"]);
@@ -92,6 +92,8 @@ export interface ToolSession {
92
92
  cwd: string;
93
93
  /** Whether UI is available */
94
94
  hasUI: boolean;
95
+ /** Whether LSP integrations are enabled */
96
+ enableLsp?: boolean;
95
97
  /** Event bus for tool/extension communication */
96
98
  eventBus?: EventBus;
97
99
  /** Output schema for structured completion (subagents) */
@@ -106,6 +108,12 @@ export interface ToolSession {
106
108
  getModelString?: () => string | undefined;
107
109
  /** Get the current session model string, regardless of how it was chosen */
108
110
  getActiveModelString?: () => string | undefined;
111
+ /** Auth storage for passing to subagents (avoids re-discovery) */
112
+ authStorage?: import("../auth-storage").AuthStorage;
113
+ /** Model registry for passing to subagents (avoids re-discovery) */
114
+ modelRegistry?: import("../model-registry").ModelRegistry;
115
+ /** MCP manager for proxying MCP calls through parent */
116
+ mcpManager?: import("../mcp/manager").MCPManager;
109
117
  /** Settings manager (optional) */
110
118
  settings?: {
111
119
  getImageAutoResize(): boolean;
@@ -154,23 +162,28 @@ export type ToolName = keyof typeof BUILTIN_TOOLS;
154
162
  */
155
163
  export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
156
164
  const includeComplete = session.requireCompleteTool === true;
165
+ const enableLsp = session.enableLsp ?? true;
157
166
  const requestedTools = toolNames && toolNames.length > 0 ? [...new Set(toolNames)] : undefined;
158
167
  const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
168
+ const isToolAllowed = (name: string) => (name === "lsp" ? enableLsp : true);
159
169
  if (includeComplete && requestedTools && !requestedTools.includes("complete")) {
160
170
  requestedTools.push("complete");
161
171
  }
162
172
 
163
- const entries = requestedTools
164
- ? requestedTools.filter((name) => name in allTools).map((name) => [name, allTools[name]] as const)
165
- : [
166
- ...Object.entries(BUILTIN_TOOLS),
167
- ...(includeComplete ? ([["complete", HIDDEN_TOOLS.complete]] as const) : []),
168
- ];
173
+ const filteredRequestedTools = requestedTools?.filter((name) => name in allTools && isToolAllowed(name));
174
+
175
+ const entries =
176
+ filteredRequestedTools !== undefined
177
+ ? filteredRequestedTools.map((name) => [name, allTools[name]] as const)
178
+ : [
179
+ ...Object.entries(BUILTIN_TOOLS).filter(([name]) => isToolAllowed(name)),
180
+ ...(includeComplete ? ([["complete", HIDDEN_TOOLS.complete]] as const) : []),
181
+ ];
169
182
  const results = await Promise.all(entries.map(([, factory]) => factory(session)));
170
183
  const tools = results.filter((t): t is Tool => t !== null);
171
184
 
172
- if (requestedTools) {
173
- const allowed = new Set(requestedTools);
185
+ if (filteredRequestedTools !== undefined) {
186
+ const allowed = new Set(filteredRequestedTools);
174
187
  return tools.filter((tool) => allowed.has(tool.name));
175
188
  }
176
189
 
@@ -734,7 +734,10 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
734
734
  }
735
735
 
736
736
  /** Create an LSP tool */
737
- export function createLspTool(session: ToolSession): AgentTool<typeof lspSchema, LspToolDetails, Theme> {
737
+ export function createLspTool(session: ToolSession): AgentTool<typeof lspSchema, LspToolDetails, Theme> | null {
738
+ if (session.enableLsp === false) {
739
+ return null;
740
+ }
738
741
  return {
739
742
  name: "lsp",
740
743
  label: "LSP",
@@ -4,7 +4,7 @@ import { Text } from "@oh-my-pi/pi-tui";
4
4
  import { Type } from "@sinclair/typebox";
5
5
  import type { SSHHost } from "../../capability/ssh";
6
6
  import { sshCapability } from "../../capability/ssh";
7
- import { loadSync } from "../../discovery/index";
7
+ import { loadCapability } from "../../discovery/index";
8
8
  import type { Theme } from "../../modes/interactive/theme/theme";
9
9
  import sshDescriptionBase from "../../prompts/tools/ssh.md" with { type: "text" };
10
10
  import type { RenderResultOptions } from "../custom-tools/types";
@@ -97,11 +97,11 @@ function buildRemoteCommand(command: string, cwd: string | undefined, info: SSHH
97
97
  return `cd -- ${quoteRemotePath(cwd)} && ${command}`;
98
98
  }
99
99
 
100
- function loadHosts(session: ToolSession): {
100
+ async function loadHosts(session: ToolSession): Promise<{
101
101
  hostNames: string[];
102
102
  hostsByName: Map<string, SSHHost>;
103
- } {
104
- const result = loadSync<SSHHost>(sshCapability.id, { cwd: session.cwd });
103
+ }> {
104
+ const result = await loadCapability<SSHHost>(sshCapability.id, { cwd: session.cwd });
105
105
  const hostsByName = new Map<string, SSHHost>();
106
106
  for (const host of result.items) {
107
107
  if (!hostsByName.has(host.name)) {
@@ -112,8 +112,8 @@ function loadHosts(session: ToolSession): {
112
112
  return { hostNames, hostsByName };
113
113
  }
114
114
 
115
- export function createSshTool(session: ToolSession): AgentTool<typeof sshSchema> | null {
116
- const { hostNames, hostsByName } = loadHosts(session);
115
+ export async function createSshTool(session: ToolSession): Promise<AgentTool<typeof sshSchema> | null> {
116
+ const { hostNames, hostsByName } = await loadHosts(session);
117
117
  if (hostNames.length === 0) {
118
118
  return null;
119
119
  }
@@ -6,16 +6,14 @@
6
6
 
7
7
  import * as path from "node:path";
8
8
  import { type SlashCommand, slashCommandCapability } from "../../../capability/slash-command";
9
- import { loadSync } from "../../../discovery";
9
+ import { loadCapability } from "../../../discovery";
10
10
 
11
11
  // Embed command markdown files at build time
12
12
  import initMd from "../../../prompts/agents/init.md" with { type: "text" };
13
- import plannerMd from "../../../prompts/agents/planner.md" with { type: "text" };
14
13
  import { renderPromptTemplate } from "../../prompt-templates";
15
14
 
16
15
  const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
17
16
  { name: "init.md", content: renderPromptTemplate(initMd) },
18
- { name: "planner.md", content: renderPromptTemplate(plannerMd) },
19
17
  ];
20
18
 
21
19
  export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;
@@ -97,11 +95,11 @@ export function loadBundledCommands(): WorkflowCommand[] {
97
95
  *
98
96
  * Precedence (highest wins): .omp > .pi > .claude (project before user), then bundled
99
97
  */
100
- export function discoverCommands(cwd: string): WorkflowCommand[] {
98
+ export async function discoverCommands(cwd: string): Promise<WorkflowCommand[]> {
101
99
  const resolvedCwd = path.resolve(cwd);
102
100
 
103
101
  // Load slash commands from capability API
104
- const result = loadSync<SlashCommand>(slashCommandCapability.id, { cwd: resolvedCwd });
102
+ const result = await loadCapability<SlashCommand>(slashCommandCapability.id, { cwd: resolvedCwd });
105
103
 
106
104
  const commands: WorkflowCommand[] = [];
107
105
  const seen = new Set<string>();
@@ -5,7 +5,11 @@
5
5
  */
6
6
 
7
7
  import type { AgentEvent } from "@oh-my-pi/pi-agent-core";
8
+ import type { AuthStorage } from "../../auth-storage";
8
9
  import type { EventBus } from "../../event-bus";
10
+ import { callTool } from "../../mcp/client";
11
+ import type { MCPManager } from "../../mcp/manager";
12
+ import type { ModelRegistry } from "../../model-registry";
9
13
  import { ensureArtifactsDir, getArtifactPaths } from "./artifacts";
10
14
  import { resolveModelPattern } from "./model-resolver";
11
15
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
@@ -18,7 +22,12 @@ import {
18
22
  TASK_SUBAGENT_EVENT_CHANNEL,
19
23
  TASK_SUBAGENT_PROGRESS_CHANNEL,
20
24
  } from "./types";
21
- import type { SubagentWorkerRequest, SubagentWorkerResponse } from "./worker-protocol";
25
+ import type {
26
+ MCPToolCallRequest,
27
+ MCPToolMetadata,
28
+ SubagentWorkerRequest,
29
+ SubagentWorkerResponse,
30
+ } from "./worker-protocol";
22
31
 
23
32
  /** Options for worker execution */
24
33
  export interface ExecutorOptions {
@@ -31,12 +40,16 @@ export interface ExecutorOptions {
31
40
  context?: string;
32
41
  modelOverride?: string;
33
42
  outputSchema?: unknown;
43
+ enableLsp?: boolean;
34
44
  signal?: AbortSignal;
35
45
  onProgress?: (progress: AgentProgress) => void;
36
46
  sessionFile?: string | null;
37
47
  persistArtifacts?: boolean;
38
48
  artifactsDir?: string;
39
49
  eventBus?: EventBus;
50
+ mcpManager?: MCPManager;
51
+ authStorage?: AuthStorage;
52
+ modelRegistry?: ModelRegistry;
40
53
  }
41
54
 
42
55
  /**
@@ -133,11 +146,45 @@ function getUsageTokens(usage: unknown): number {
133
146
  return input + output + cacheRead + cacheWrite;
134
147
  }
135
148
 
149
+ /**
150
+ * Parse MCP tool name to extract server and tool names.
151
+ * Format: mcp_<serverName>_<toolName>
152
+ * Note: Uses lastIndexOf to handle server names with underscores.
153
+ */
154
+ function parseMCPToolName(fullName: string): { serverName: string; toolName: string } | undefined {
155
+ if (!fullName.startsWith("mcp_")) return undefined;
156
+ const rest = fullName.slice(4);
157
+ const underscoreIndex = rest.lastIndexOf("_");
158
+ if (underscoreIndex === -1) return undefined;
159
+ return {
160
+ serverName: rest.slice(0, underscoreIndex),
161
+ toolName: rest.slice(underscoreIndex + 1),
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Extract MCP tool metadata from MCPManager for passing to worker.
167
+ */
168
+ function extractMCPToolMetadata(mcpManager: MCPManager): MCPToolMetadata[] {
169
+ return mcpManager.getTools().map((tool) => {
170
+ const parsed = parseMCPToolName(tool.name);
171
+ return {
172
+ name: tool.name,
173
+ label: tool.label ?? tool.name,
174
+ description: tool.description ?? "",
175
+ parameters: tool.parameters,
176
+ serverName: parsed?.serverName ?? "",
177
+ mcpToolName: parsed?.toolName ?? "",
178
+ };
179
+ });
180
+ }
181
+
136
182
  /**
137
183
  * Run a single agent in a worker.
138
184
  */
139
185
  export async function runSubprocess(options: ExecutorOptions): Promise<SingleResult> {
140
- const { cwd, agent, task, index, taskId, context, modelOverride, outputSchema, signal, onProgress } = options;
186
+ const { cwd, agent, task, index, taskId, context, modelOverride, outputSchema, enableLsp, signal, onProgress } =
187
+ options;
141
188
  const startTime = Date.now();
142
189
 
143
190
  // Initialize progress
@@ -208,7 +255,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
208
255
  }
209
256
 
210
257
  // Resolve and add model
211
- const resolvedModel = resolveModelPattern(modelOverride || agent.model);
258
+ const resolvedModel = await resolveModelPattern(modelOverride || agent.model);
212
259
  const sessionFile = subtaskSessionFile ?? options.sessionFile ?? null;
213
260
  const spawnsEnv = agent.spawns === undefined ? "" : agent.spawns === "*" ? "*" : agent.spawns.join(",");
214
261
 
@@ -535,6 +582,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
535
582
  outputSchema,
536
583
  sessionFile,
537
584
  spawnsEnv,
585
+ enableLsp,
586
+ serializedAuth: options.authStorage?.serialize(),
587
+ serializedModels: options.modelRegistry?.serialize(),
588
+ mcpTools: options.mcpManager ? extractMCPToolMetadata(options.mcpManager) : undefined,
538
589
  },
539
590
  };
540
591
 
@@ -556,9 +607,43 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
556
607
  cleanup();
557
608
  resolve(message);
558
609
  };
610
+ const handleMCPCall = async (request: MCPToolCallRequest) => {
611
+ const mcpManager = options.mcpManager;
612
+ if (!mcpManager) {
613
+ worker.postMessage({
614
+ type: "mcp_tool_result",
615
+ callId: request.callId,
616
+ error: "MCP not available",
617
+ });
618
+ return;
619
+ }
620
+ try {
621
+ const parsed = parseMCPToolName(request.toolName);
622
+ if (!parsed) throw new Error(`Invalid MCP tool name: ${request.toolName}`);
623
+ const connection = mcpManager.getConnection(parsed.serverName);
624
+ if (!connection) throw new Error(`MCP server not connected: ${parsed.serverName}`);
625
+ const result = await callTool(connection, parsed.toolName, request.params);
626
+ worker.postMessage({
627
+ type: "mcp_tool_result",
628
+ callId: request.callId,
629
+ result: { content: result.content ?? [], isError: result.isError },
630
+ });
631
+ } catch (error) {
632
+ worker.postMessage({
633
+ type: "mcp_tool_result",
634
+ callId: request.callId,
635
+ error: error instanceof Error ? error.message : String(error),
636
+ });
637
+ }
638
+ };
639
+
559
640
  const onMessage = (event: WorkerMessageEvent<SubagentWorkerResponse>) => {
560
641
  const message = event.data;
561
642
  if (!message || resolved) return;
643
+ if (message.type === "mcp_tool_call") {
644
+ handleMCPCall(message as MCPToolCallRequest);
645
+ return;
646
+ }
562
647
  if (message.type === "event") {
563
648
  try {
564
649
  processEvent(message.event);
@@ -349,12 +349,16 @@ export async function createTaskTool(
349
349
  sessionFile,
350
350
  persistArtifacts: !!artifactsDir,
351
351
  artifactsDir: effectiveArtifactsDir,
352
+ enableLsp: false,
352
353
  signal,
353
354
  eventBus: undefined,
354
355
  onProgress: (progress) => {
355
356
  progressMap.set(index, structuredClone(progress));
356
357
  emitProgress();
357
358
  },
359
+ authStorage: session.authStorage,
360
+ modelRegistry: session.modelRegistry,
361
+ mcpManager: session.mcpManager,
358
362
  });
359
363
  },
360
364
  signal,