@oh-my-pi/pi-coding-agent 4.2.0 → 4.2.2

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 (64) hide show
  1. package/CHANGELOG.md +46 -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 +7 -7
  10. package/src/core/agent-storage.ts +50 -0
  11. package/src/core/auth-storage.ts +102 -3
  12. package/src/core/bash-executor.ts +1 -1
  13. package/src/core/custom-tools/loader.ts +2 -2
  14. package/src/core/export-html/index.ts +1 -33
  15. package/src/core/extensions/loader.ts +2 -2
  16. package/src/core/extensions/types.ts +1 -1
  17. package/src/core/hooks/loader.ts +2 -2
  18. package/src/core/mcp/config.ts +2 -2
  19. package/src/core/model-registry.ts +46 -0
  20. package/src/core/sdk.ts +37 -29
  21. package/src/core/settings-manager.ts +152 -135
  22. package/src/core/skills.ts +72 -51
  23. package/src/core/slash-commands.ts +3 -3
  24. package/src/core/system-prompt.ts +52 -10
  25. package/src/core/tools/complete.ts +5 -2
  26. package/src/core/tools/edit.ts +7 -4
  27. package/src/core/tools/index.test.ts +16 -0
  28. package/src/core/tools/index.ts +21 -8
  29. package/src/core/tools/lsp/index.ts +4 -1
  30. package/src/core/tools/ssh.ts +6 -6
  31. package/src/core/tools/task/commands.ts +3 -9
  32. package/src/core/tools/task/executor.ts +88 -3
  33. package/src/core/tools/task/index.ts +4 -0
  34. package/src/core/tools/task/model-resolver.ts +10 -7
  35. package/src/core/tools/task/worker-protocol.ts +48 -2
  36. package/src/core/tools/task/worker.ts +152 -7
  37. package/src/core/tools/write.ts +7 -4
  38. package/src/discovery/agents-md.ts +13 -19
  39. package/src/discovery/builtin.ts +368 -293
  40. package/src/discovery/claude.ts +183 -345
  41. package/src/discovery/cline.ts +30 -10
  42. package/src/discovery/codex.ts +188 -272
  43. package/src/discovery/cursor.ts +106 -121
  44. package/src/discovery/gemini.ts +72 -97
  45. package/src/discovery/github.ts +7 -10
  46. package/src/discovery/helpers.ts +114 -57
  47. package/src/discovery/index.ts +1 -2
  48. package/src/discovery/mcp-json.ts +15 -18
  49. package/src/discovery/ssh.ts +9 -17
  50. package/src/discovery/vscode.ts +10 -5
  51. package/src/discovery/windsurf.ts +52 -86
  52. package/src/main.ts +5 -1
  53. package/src/modes/interactive/components/extensions/extension-dashboard.ts +24 -11
  54. package/src/modes/interactive/components/extensions/state-manager.ts +19 -15
  55. package/src/modes/interactive/controllers/selector-controller.ts +9 -5
  56. package/src/modes/interactive/interactive-mode.ts +22 -15
  57. package/src/prompts/agents/plan.md +107 -30
  58. package/src/prompts/agents/task.md +5 -4
  59. package/src/prompts/system/system-prompt.md +5 -0
  60. package/src/prompts/tools/task.md +25 -19
  61. package/src/utils/shell.ts +2 -2
  62. package/src/prompts/agents/architect-plan.md +0 -10
  63. package/src/prompts/agents/implement-with-critic.md +0 -11
  64. package/src/prompts/agents/implement.md +0 -11
@@ -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";
@@ -148,6 +148,45 @@ function stripQuotes(value: string): string {
148
148
  return value.replace(/^"|"$/g, "");
149
149
  }
150
150
 
151
+ const AGENTS_MD_PATTERN = "**/AGENTS.md";
152
+ const AGENTS_MD_LIMIT = 200;
153
+
154
+ interface AgentsMdSearch {
155
+ scopePath: string;
156
+ limit: number;
157
+ pattern: string;
158
+ files: string[];
159
+ }
160
+
161
+ function normalizePath(value: string): string {
162
+ return value.replace(/\\/g, "/");
163
+ }
164
+
165
+ function listAgentsMdFiles(root: string, limit: number): string[] {
166
+ try {
167
+ const entries = Array.from(
168
+ new Bun.Glob(AGENTS_MD_PATTERN).scanSync({ cwd: root, onlyFiles: true, dot: false, absolute: false }),
169
+ );
170
+ const normalized = entries
171
+ .map((entry) => normalizePath(entry))
172
+ .filter((entry) => entry.length > 0 && !entry.includes("node_modules"))
173
+ .sort();
174
+ return normalized.length > limit ? normalized.slice(0, limit) : normalized;
175
+ } catch {
176
+ return [];
177
+ }
178
+ }
179
+
180
+ function buildAgentsMdSearch(cwd: string): AgentsMdSearch {
181
+ const files = listAgentsMdFiles(cwd, AGENTS_MD_LIMIT);
182
+ return {
183
+ scopePath: ".",
184
+ limit: AGENTS_MD_LIMIT,
185
+ pattern: AGENTS_MD_PATTERN,
186
+ files,
187
+ };
188
+ }
189
+
151
190
  function getOsName(): string {
152
191
  switch (process.platform) {
153
192
  case "win32":
@@ -519,12 +558,12 @@ export interface LoadContextFilesOptions {
519
558
  * Returns {path, content, depth} entries for all discovered context files.
520
559
  * Files are sorted by depth (descending) so files closer to cwd appear last/more prominent.
521
560
  */
522
- export function loadProjectContextFiles(
561
+ export async function loadProjectContextFiles(
523
562
  options: LoadContextFilesOptions = {},
524
- ): Array<{ path: string; content: string; depth?: number }> {
563
+ ): Promise<Array<{ path: string; content: string; depth?: number }>> {
525
564
  const resolvedCwd = options.cwd ?? process.cwd();
526
565
 
527
- const result = loadSync(contextFileCapability.id, { cwd: resolvedCwd });
566
+ const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
528
567
 
529
568
  // Convert ContextFile items and preserve depth info
530
569
  const files = result.items.map((item) => {
@@ -551,10 +590,10 @@ export function loadProjectContextFiles(
551
590
  * Load system prompt customization files (SYSTEM.md).
552
591
  * Returns combined content from all discovered SYSTEM.md files.
553
592
  */
554
- export function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): string | null {
593
+ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): Promise<string | null> {
555
594
  const resolvedCwd = options.cwd ?? process.cwd();
556
595
 
557
- const result = loadSync<SystemPromptFile>(systemPromptCapability.id, { cwd: resolvedCwd });
596
+ const result = await loadCapability<SystemPromptFile>(systemPromptCapability.id, { cwd: resolvedCwd });
558
597
 
559
598
  if (result.items.length === 0) return null;
560
599
 
@@ -592,7 +631,7 @@ export interface BuildSystemPromptOptions {
592
631
  }
593
632
 
594
633
  /** Build the system prompt with tools, guidelines, and context */
595
- export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
634
+ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<string> {
596
635
  const {
597
636
  customPrompt,
598
637
  tools,
@@ -609,7 +648,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
609
648
  const resolvedAppendPrompt = resolvePromptInput(appendSystemPrompt, "append system prompt");
610
649
 
611
650
  // Load SYSTEM.md customization (prepended to prompt)
612
- const systemPromptCustomization = loadSystemPromptFiles({ cwd: resolvedCwd });
651
+ const systemPromptCustomization = await loadSystemPromptFiles({ cwd: resolvedCwd });
613
652
 
614
653
  const now = new Date();
615
654
  const dateTime = now.toLocaleString("en-US", {
@@ -624,7 +663,8 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
624
663
  });
625
664
 
626
665
  // Resolve context files: use provided or discover
627
- const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd });
666
+ const contextFiles = providedContextFiles ?? (await loadProjectContextFiles({ cwd: resolvedCwd }));
667
+ const agentsMdSearch = buildAgentsMdSearch(resolvedCwd);
628
668
 
629
669
  // Build tool descriptions array
630
670
  // Priority: toolNames (explicit list) > tools (Map) > defaults
@@ -648,7 +688,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
648
688
  // Resolve skills: use provided or discover
649
689
  const skills =
650
690
  providedSkills ??
651
- (skillsSettings?.enabled !== false ? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).skills : []);
691
+ (skillsSettings?.enabled !== false ? (await loadSkills({ ...skillsSettings, cwd: resolvedCwd })).skills : []);
652
692
 
653
693
  // Get git context
654
694
  const git = loadGitContext(resolvedCwd);
@@ -663,6 +703,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
663
703
  customPrompt: resolvedCustomPrompt,
664
704
  appendPrompt: resolvedAppendPrompt ?? "",
665
705
  contextFiles,
706
+ agentsMdSearch,
666
707
  toolDescriptions: toolDescriptionsArray,
667
708
  git,
668
709
  skills: filteredSkills,
@@ -678,6 +719,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
678
719
  environment: getEnvironmentInfo(),
679
720
  systemPromptCustomization: systemPromptCustomization ?? "",
680
721
  contextFiles,
722
+ agentsMdSearch,
681
723
  git,
682
724
  skills: filteredSkills,
683
725
  rules: rules ?? [],
@@ -79,7 +79,7 @@ export function createCompleteTool(session: ToolSession) {
79
79
  : Type.Any({ description: "Structured JSON output (no schema specified)" });
80
80
 
81
81
  const completeParams = Type.Object({
82
- data: dataSchema,
82
+ data: Type.Optional(dataSchema),
83
83
  status: Type.Optional(
84
84
  Type.Union([Type.Literal("success"), Type.Literal("aborted")], {
85
85
  default: "success",
@@ -99,8 +99,11 @@ export function createCompleteTool(session: ToolSession) {
99
99
  execute: async (_toolCallId, params) => {
100
100
  const status = params.status ?? "success";
101
101
 
102
- // Skip schema validation when aborting - the agent is giving up
102
+ // Skip validation when aborting - data is optional for aborts
103
103
  if (status === "success") {
104
+ if (params.data === undefined) {
105
+ throw new Error("data is required when status is 'success'");
106
+ }
104
107
  if (schemaError) {
105
108
  throw new Error(`Invalid output schema: ${schemaError}`);
106
109
  }
@@ -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",
@@ -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,19 +6,13 @@
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
- import architectPlanMd from "../../../prompts/agents/architect-plan.md" with { type: "text" };
13
- import implementMd from "../../../prompts/agents/implement.md" with { type: "text" };
14
- import implementWithCriticMd from "../../../prompts/agents/implement-with-critic.md" with { type: "text" };
15
12
  import initMd from "../../../prompts/agents/init.md" with { type: "text" };
16
13
  import { renderPromptTemplate } from "../../prompt-templates";
17
14
 
18
15
  const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
19
- { name: "architect-plan.md", content: renderPromptTemplate(architectPlanMd) },
20
- { name: "implement-with-critic.md", content: renderPromptTemplate(implementWithCriticMd) },
21
- { name: "implement.md", content: renderPromptTemplate(implementMd) },
22
16
  { name: "init.md", content: renderPromptTemplate(initMd) },
23
17
  ];
24
18
 
@@ -101,11 +95,11 @@ export function loadBundledCommands(): WorkflowCommand[] {
101
95
  *
102
96
  * Precedence (highest wins): .omp > .pi > .claude (project before user), then bundled
103
97
  */
104
- export function discoverCommands(cwd: string): WorkflowCommand[] {
98
+ export async function discoverCommands(cwd: string): Promise<WorkflowCommand[]> {
105
99
  const resolvedCwd = path.resolve(cwd);
106
100
 
107
101
  // Load slash commands from capability API
108
- const result = loadSync<SlashCommand>(slashCommandCapability.id, { cwd: resolvedCwd });
102
+ const result = await loadCapability<SlashCommand>(slashCommandCapability.id, { cwd: resolvedCwd });
109
103
 
110
104
  const commands: WorkflowCommand[] = [];
111
105
  const seen = new Set<string>();