From 1b40197b8c52c876f56d563a57daaea580e312e0 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 15 May 2026 01:06:59 +0000 Subject: [PATCH 1/3] fix(discovery): respect disabledProviders in discoverAgents for claude-plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit discoverAgents() called listClaudePluginRoots() unconditionally, so agents from Claude Code marketplace plugins appeared in /agents and the Agent Control Center even when claude-plugins was listed in disabledProviders. Guard the listClaudePluginRoots() call with isProviderEnabled("claude-plugins"), returning an empty roots array when the provider is disabled — matching how filterProviders() handles every other capability's provider set. Added regression test that verifies both the enabled path (agents visible) and the disabled path (agents absent) using a real temp-directory plugin registry fixture. Fixes #1075 --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/task/discovery.ts | 7 +- ...agent-discovery-disabled-providers.test.ts | 80 +++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/test/discovery/agent-discovery-disabled-providers.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3aa4adaa6..401b05c7f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -19,6 +19,7 @@ - Updated the `read` tool prompt to describe the new elision footer and instruct the model to follow `:raw` (or an explicit line range) when the elided body is actually needed, rather than guessing. - Fixed plugin extensions failing to load when their `peerDependencies` reference internal `pi-*` packages under any scope other than `@mariozechner` (e.g. `Cannot find module '@earendil-works/pi-tui'` from `@juicesharp/rpiv-ask-user-question`, or `Cannot find module '@oh-my-pi/pi-utils'` from `@oh-my-pi/swarm-extension`). The legacy-pi specifier shim now treats `@mariozechner`, `@earendil-works`, **and** the canonical `@oh-my-pi` itself as aliases for the same set of bundled in-process packages (`pi-agent-core`, `pi-ai`, `pi-coding-agent`, `pi-natives`, `pi-tui`, `pi-utils`), and additionally rewrites the upstream-only `pi-ai/oauth` subpath onto our `pi-ai/utils/oauth` layout. Restored the `Key` runtime helper export on `@oh-my-pi/pi-tui` to match upstream — plugins using `Key.enter` / `Key.ctrl("c")` (e.g. `@plannotator/pi-extension`, `@juicesharp/rpiv-ask-user-question`) no longer fail with `Export named 'Key' not found`. End-to-end verified against `@juicesharp/rpiv-ask-user-question`, `@oh-my-pi/swarm-extension`, and `@plannotator/pi-extension` — each now loads cleanly with all of its tools/commands/handlers registered. Plugins importing any of those scopes are remapped to the omp binary's own copy at load time, so peer deps are no longer dragged in from npm and there is exactly one module instance per package regardless of which scope name the plugin's manifest happened to declare. +- Fixed `discoverAgents()` ignoring `disabledProviders` for the `claude-plugins` provider. Plugin roots from `~/.claude/plugins/` were scanned unconditionally, so agents from Claude Code marketplace plugins continued to appear in `/agents` and the Agent Control Center even when `disabledProviders: [claude-plugins]` was set. The discovery path now checks `isProviderEnabled("claude-plugins")` before calling `listClaudePluginRoots()`, matching how every other capability respects the disabled-providers set. ([#1075](https://github.com/can1357/oh-my-pi/issues/1075)) ## [15.0.1] - 2026-05-14 ### Breaking Changes diff --git a/packages/coding-agent/src/task/discovery.ts b/packages/coding-agent/src/task/discovery.ts index adb2bf1d9..c1ce37136 100644 --- a/packages/coding-agent/src/task/discovery.ts +++ b/packages/coding-agent/src/task/discovery.ts @@ -17,6 +17,7 @@ import * as path from "node:path"; import { logger } from "@oh-my-pi/pi-utils"; import { findAllNearestProjectConfigDirs, getConfigDirs } from "../config"; import { listClaudePluginRoots } from "../discovery/helpers"; +import { isProviderEnabled } from "../capability"; import { loadBundledAgents, parseAgent } from "./agents"; import type { AgentDefinition, AgentSource } from "./types"; @@ -87,8 +88,10 @@ export async function discoverAgents(cwd: string, home: string = os.homedir()): if (user) orderedDirs.push({ dir: user.path, source: "user" }); } - // Load agents from Claude Code marketplace plugins - const { roots: pluginRoots } = await listClaudePluginRoots(home, resolvedCwd); + // Load agents from Claude Code marketplace plugins (respects disabledProviders) + const { roots: pluginRoots } = isProviderEnabled("claude-plugins") + ? await listClaudePluginRoots(home, resolvedCwd) + : { roots: [] }; const sortedPluginRoots = [...pluginRoots].sort((a, b) => { if (a.scope === b.scope) return 0; return a.scope === "project" ? -1 : 1; diff --git a/packages/coding-agent/test/discovery/agent-discovery-disabled-providers.test.ts b/packages/coding-agent/test/discovery/agent-discovery-disabled-providers.test.ts new file mode 100644 index 000000000..cdca0d86a --- /dev/null +++ b/packages/coding-agent/test/discovery/agent-discovery-disabled-providers.test.ts @@ -0,0 +1,80 @@ +/** + * Regression test for #1075: + * discoverAgents() must skip Claude plugin roots when claude-plugins is disabled. + */ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { disableProvider, enableProvider } from "../../src/capability"; +import { clearCache as clearFsCache } from "../../src/capability/fs"; +import { clearClaudePluginRootsCache } from "../../src/discovery/helpers"; +import { discoverAgents } from "../../src/task/discovery"; + +const PLUGIN_AGENT_MD = [ + "---", + "name: simplifier", + "description: A code simplifier agent from a Claude plugin", + "---", + "Simplify code.", +].join("\n"); + +describe("discoverAgents — claude-plugins disabled provider", () => { + let tempHome: string; + + beforeEach(() => { + tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-agent-disco-home-")); + + // Build a fake Claude plugin install with an agents/ subdirectory. + const pluginInstallPath = path.join(tempHome, "plugin-cache", "code-simplifier"); + const agentsDir = path.join(pluginInstallPath, "agents"); + fs.mkdirSync(agentsDir, { recursive: true }); + fs.writeFileSync(path.join(agentsDir, "simplifier.md"), PLUGIN_AGENT_MD); + + // Register the plugin in the Claude registry so listClaudePluginRoots picks it up. + const claudePluginsDir = path.join(tempHome, ".claude", "plugins"); + fs.mkdirSync(claudePluginsDir, { recursive: true }); + fs.writeFileSync( + path.join(claudePluginsDir, "installed_plugins.json"), + JSON.stringify({ + version: 2, + plugins: { + "code-simplifier@claude-plugins-official": [ + { + installPath: pluginInstallPath, + version: "1.0.0", + scope: "user", + installedAt: "2025-01-01T00:00:00Z", + lastUpdated: "2025-01-01T00:00:00Z", + }, + ], + }, + }), + ); + + // Start each test with a clean provider + cache state. + enableProvider("claude-plugins"); + clearFsCache(); + clearClaudePluginRootsCache(); + }); + + afterEach(() => { + fs.rmSync(tempHome, { recursive: true, force: true }); + // Restore global state so other tests in the suite are not affected. + enableProvider("claude-plugins"); + clearFsCache(); + clearClaudePluginRootsCache(); + }); + + test("includes plugin agents when claude-plugins is enabled", async () => { + const { agents } = await discoverAgents(tempHome, tempHome); + expect(agents.map(a => a.name)).toContain("simplifier"); + }); + + test("excludes plugin agents when claude-plugins is disabled", async () => { + disableProvider("claude-plugins"); + clearClaudePluginRootsCache(); + const { agents } = await discoverAgents(tempHome, tempHome); + expect(agents.map(a => a.name)).not.toContain("simplifier"); + }); +}); From 8b22d6de6530cfdcd1706c7adb10bc353f846b30 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 15 May 2026 01:07:19 +0000 Subject: [PATCH 2/3] style: bun run fix --- packages/coding-agent/src/task/discovery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/coding-agent/src/task/discovery.ts b/packages/coding-agent/src/task/discovery.ts index c1ce37136..b78d04bea 100644 --- a/packages/coding-agent/src/task/discovery.ts +++ b/packages/coding-agent/src/task/discovery.ts @@ -15,9 +15,9 @@ import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import { logger } from "@oh-my-pi/pi-utils"; +import { isProviderEnabled } from "../capability"; import { findAllNearestProjectConfigDirs, getConfigDirs } from "../config"; import { listClaudePluginRoots } from "../discovery/helpers"; -import { isProviderEnabled } from "../capability"; import { loadBundledAgents, parseAgent } from "./agents"; import type { AgentDefinition, AgentSource } from "./types"; From 1519224949b1a8c46b609aa0105eaaed6b208b62 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 15 May 2026 01:08:09 +0000 Subject: [PATCH 3/3] fix(bash): accept readonly string[] for notices in buildCompletedResult MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TypeScript 6 tightens mutability checks; the notices parameter was typed as string[] but the caller held a readonly string[] (from the options object). Widening both private method overloads to readonly string[] satisfies the type checker without any runtime change — filter(Boolean) works on readonly arrays. --- packages/coding-agent/src/tools/bash.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 1340589b5..3139224c3 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -292,7 +292,7 @@ export class BashTool implements AgentTool { #buildCompletedResult( result: BashResult | BashInteractiveResult, timeoutSec: number, - options: { requestedTimeoutSec?: number; notices?: string[]; terminalId?: string } = {}, + options: { requestedTimeoutSec?: number; notices?: readonly string[]; terminalId?: string } = {}, ): AgentToolResult { const outputLines = [this.#formatResultOutput(result)]; const notices = options.notices?.filter(Boolean) ?? []; @@ -315,7 +315,7 @@ export class BashTool implements AgentTool { label: string, previewText: string, timeoutSec: number, - options: { requestedTimeoutSec?: number; notices?: string[] } = {}, + options: { requestedTimeoutSec?: number; notices?: readonly string[] } = {}, ): AgentToolResult { const details: BashToolDetails = { timeoutSeconds: timeoutSec,