@oh-my-pi/pi-coding-agent 13.16.5 → 13.17.0

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 (40) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/package.json +7 -7
  3. package/src/cli/args.ts +7 -0
  4. package/src/cli/classify-install-target.ts +50 -0
  5. package/src/cli/plugin-cli.ts +245 -31
  6. package/src/commands/plugin.ts +3 -0
  7. package/src/config/settings-schema.ts +12 -13
  8. package/src/cursor.ts +66 -1
  9. package/src/discovery/claude-plugins.ts +95 -5
  10. package/src/discovery/helpers.ts +168 -41
  11. package/src/discovery/plugin-dir-roots.ts +28 -0
  12. package/src/discovery/substitute-plugin-root.ts +29 -0
  13. package/src/extensibility/plugins/index.ts +1 -0
  14. package/src/extensibility/plugins/marketplace/cache.ts +136 -0
  15. package/src/extensibility/plugins/marketplace/fetcher.ts +354 -0
  16. package/src/extensibility/plugins/marketplace/index.ts +6 -0
  17. package/src/extensibility/plugins/marketplace/manager.ts +528 -0
  18. package/src/extensibility/plugins/marketplace/registry.ts +181 -0
  19. package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
  20. package/src/extensibility/plugins/marketplace/types.ts +177 -0
  21. package/src/internal-urls/index.ts +1 -0
  22. package/src/internal-urls/local-protocol.ts +2 -19
  23. package/src/internal-urls/parse.ts +72 -0
  24. package/src/internal-urls/router.ts +2 -18
  25. package/src/lsp/config.ts +9 -0
  26. package/src/main.ts +50 -1
  27. package/src/modes/components/plugin-selector.ts +86 -0
  28. package/src/modes/components/settings-defs.ts +0 -4
  29. package/src/modes/controllers/mcp-command-controller.ts +14 -0
  30. package/src/modes/controllers/selector-controller.ts +104 -13
  31. package/src/modes/interactive-mode.ts +4 -0
  32. package/src/modes/types.ts +1 -0
  33. package/src/prompts/agents/reviewer.md +3 -4
  34. package/src/sdk.ts +0 -7
  35. package/src/slash-commands/builtin-registry.ts +273 -0
  36. package/src/tools/bash-skill-urls.ts +48 -5
  37. package/src/tools/read.ts +15 -9
  38. package/src/web/search/code-search.ts +2 -179
  39. package/src/web/search/index.ts +2 -3
  40. package/src/web/search/types.ts +1 -5
package/src/main.ts CHANGED
@@ -11,8 +11,9 @@ import * as os from "node:os";
11
11
  import * as path from "node:path";
12
12
  import { createInterface } from "node:readline/promises";
13
13
  import type { ImageContent } from "@oh-my-pi/pi-ai";
14
- import { $env, getProjectDir, logger, postmortem, setProjectDir, VERSION } from "@oh-my-pi/pi-utils";
14
+ import { $env, getConfigDirName, getProjectDir, logger, postmortem, setProjectDir, VERSION } from "@oh-my-pi/pi-utils";
15
15
  import chalk from "chalk";
16
+ import { invalidate as invalidateFsCache } from "./capability/fs";
16
17
  import type { Args } from "./cli/args";
17
18
  import { processFileArguments } from "./cli/file-processor";
18
19
  import { buildInitialMessage } from "./cli/initial-message";
@@ -23,8 +24,16 @@ import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
23
24
  import { resolveCliModel, resolveModelRoleValue, resolveModelScope, type ScopedModel } from "./config/model-resolver";
24
25
  import { Settings, settings } from "./config/settings";
25
26
  import { initializeWithSettings } from "./discovery";
27
+ import { clearClaudePluginRootsCache, injectPluginDirRoots, preloadPluginRoots } from "./discovery/helpers";
26
28
  import { exportFromFile } from "./export/html";
27
29
  import type { ExtensionUIContext } from "./extensibility/extensions/types";
30
+ import {
31
+ getInstalledPluginsRegistryPath,
32
+ getMarketplacesCacheDir,
33
+ getMarketplacesRegistryPath,
34
+ getPluginsCacheDir,
35
+ MarketplaceManager,
36
+ } from "./extensibility/plugins/marketplace";
28
37
  import type { MCPManager } from "./mcp";
29
38
  import { InteractiveMode, runAcpMode, runPrintMode, runRpcMode } from "./modes";
30
39
  import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
@@ -639,6 +648,46 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
639
648
  sessionManager = await SessionManager.open(selectedPath);
640
649
  }
641
650
 
651
+ // Wire --plugin-dir and preload plugin roots for sync consumers (LSP config)
652
+ const home = os.homedir();
653
+ if (parsedArgs.pluginDirs && parsedArgs.pluginDirs.length > 0) {
654
+ await logger.timeAsync("injectPluginDirRoots", () => injectPluginDirRoots(home, parsedArgs.pluginDirs!));
655
+ } else {
656
+ await logger.timeAsync("preloadPluginRoots", () => preloadPluginRoots(home));
657
+ }
658
+
659
+ // Background marketplace auto-update — never blocks startup.
660
+ const autoUpdate = settings.get("marketplace.autoUpdate");
661
+ if (autoUpdate !== "off") {
662
+ void (async () => {
663
+ try {
664
+ const mgr = new MarketplaceManager({
665
+ marketplacesRegistryPath: getMarketplacesRegistryPath(),
666
+ installedRegistryPath: getInstalledPluginsRegistryPath(),
667
+ marketplacesCacheDir: getMarketplacesCacheDir(),
668
+ pluginsCacheDir: getPluginsCacheDir(),
669
+ clearPluginRootsCache: () => {
670
+ const h = os.homedir();
671
+ invalidateFsCache(path.join(h, ".claude", "plugins", "installed_plugins.json"));
672
+ invalidateFsCache(path.join(h, getConfigDirName(), "plugins", "installed_plugins.json"));
673
+ clearClaudePluginRootsCache();
674
+ },
675
+ });
676
+ await mgr.refreshStaleMarketplaces();
677
+ const updates = await mgr.checkForUpdates();
678
+ if (updates.length === 0) return;
679
+ if (autoUpdate === "auto") {
680
+ await mgr.upgradeAllPlugins();
681
+ logger.debug(`Auto-upgraded ${updates.length} marketplace plugin(s)`);
682
+ } else {
683
+ logger.debug(`${updates.length} marketplace plugin update(s) available \u2014 /marketplace upgrade`);
684
+ }
685
+ } catch {
686
+ // Silently ignore — network failure, corrupt data, offline.
687
+ }
688
+ })();
689
+ }
690
+
642
691
  const { options: sessionOptions } = await logger.timeAsync("buildSessionOptions", () =>
643
692
  buildSessionOptions(parsedArgs, scopedModels, sessionManager, modelRegistry),
644
693
  );
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Interactive marketplace plugin selector.
3
+ *
4
+ * Shows available plugins from all configured marketplaces in a SelectList.
5
+ * Selecting a plugin triggers installation. Esc cancels.
6
+ */
7
+ import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
8
+ import { getSelectListTheme } from "../theme/theme";
9
+ import { DynamicBorder } from "./dynamic-border";
10
+
11
+ export interface PluginSelectorCallbacks {
12
+ onSelect: (pluginName: string, marketplace: string) => void;
13
+ onCancel: () => void;
14
+ }
15
+
16
+ export interface PluginItem {
17
+ plugin: { name: string; version?: string; description?: string };
18
+ marketplace: string;
19
+ }
20
+
21
+ export class PluginSelectorComponent extends Container {
22
+ #selectList: SelectList;
23
+
24
+ constructor(
25
+ marketplaceCount: number,
26
+ plugins: PluginItem[],
27
+ installedIds: Set<string>,
28
+ callbacks: PluginSelectorCallbacks,
29
+ ) {
30
+ super();
31
+
32
+ const items: SelectItem[] = plugins.map(({ plugin, marketplace }) => {
33
+ const id = `${plugin.name}@${marketplace}`;
34
+ const installed = installedIds.has(id);
35
+ const version = plugin.version ? `@${plugin.version}` : "";
36
+ const status = installed ? " [installed]" : "";
37
+
38
+ return {
39
+ value: id,
40
+ label: `${plugin.name}${version}${status}`,
41
+ description: plugin.description,
42
+ hint: marketplace,
43
+ };
44
+ });
45
+
46
+ if (items.length === 0) {
47
+ items.push({
48
+ value: "__empty__",
49
+ label: "No plugins available",
50
+ description:
51
+ marketplaceCount === 0
52
+ ? "Add a marketplace first: /marketplace add <source>"
53
+ : "Configured marketplaces have no plugins",
54
+ });
55
+ }
56
+
57
+ this.addChild(new DynamicBorder());
58
+
59
+ this.#selectList = new SelectList(items, Math.min(items.length, 20), getSelectListTheme());
60
+
61
+ this.#selectList.onSelect = item => {
62
+ if (item.value === "__empty__") return;
63
+ const [name, marketplace] = splitPluginId(item.value);
64
+ if (name && marketplace) {
65
+ callbacks.onSelect(name, marketplace);
66
+ }
67
+ };
68
+
69
+ this.#selectList.onCancel = () => {
70
+ callbacks.onCancel();
71
+ };
72
+
73
+ this.addChild(this.#selectList);
74
+ this.addChild(new DynamicBorder());
75
+ }
76
+
77
+ getSelectList(): SelectList {
78
+ return this.#selectList;
79
+ }
80
+ }
81
+
82
+ function splitPluginId(id: string): [string, string] | [null, null] {
83
+ const atIdx = id.lastIndexOf("@");
84
+ if (atIdx <= 0) return [null, null];
85
+ return [id.slice(0, atIdx), id.slice(atIdx + 1)];
86
+ }
@@ -312,10 +312,6 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
312
312
  { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
313
313
  { value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
314
314
  ],
315
- "providers.codeSearch": [
316
- { value: "exa", label: "Exa", description: "Uses Exa public MCP code search" },
317
- { value: "grep", label: "grep.app", description: "Uses Vercel grep.app public code search" },
318
- ],
319
315
  "providers.image": [
320
316
  { value: "auto", label: "Auto", description: "Priority: OpenRouter > Gemini" },
321
317
  { value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
@@ -793,6 +793,20 @@ export class MCPCommandController {
793
793
  }
794
794
  }
795
795
 
796
+ // refreshMCPTools preserves the prior MCP tool selection, so tools from
797
+ // brand-new servers are registered in the registry but never activated.
798
+ // Explicitly activate the newly added server's tools now.
799
+ if (isConnected && this.ctx.mcpManager) {
800
+ const serverTools = this.ctx.mcpManager.getTools().filter(t => t.mcpServerName === name);
801
+ if (serverTools.length > 0) {
802
+ const currentActive = this.ctx.session.getActiveToolNames();
803
+ const toActivate = serverTools.map(t => t.name).filter(n => this.ctx.session.getToolByName(n));
804
+ if (toActivate.length > 0) {
805
+ await this.ctx.session.setActiveToolsByName([...new Set([...currentActive, ...toActivate])]);
806
+ }
807
+ }
808
+ }
809
+
796
810
  // Show success message
797
811
  const scopeLabel = scope === "user" ? "user" : "project";
798
812
  const lines = ["", theme.fg("success", `✓ Added server "${name}" to ${scopeLabel} config`), ""];
@@ -1,12 +1,23 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
1
3
  import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
4
  import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
3
5
  import type { Component } from "@oh-my-pi/pi-tui";
4
6
  import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
5
- import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
7
+ import { getAgentDbPath, getConfigDirName, getProjectDir } from "@oh-my-pi/pi-utils";
8
+ import { invalidate as invalidateFsCache } from "../../capability/fs";
6
9
  import { getRoleInfo } from "../../config/model-registry";
7
10
  import { settings } from "../../config/settings";
8
11
  import { DebugSelectorComponent } from "../../debug";
9
12
  import { disableProvider, enableProvider } from "../../discovery";
13
+ import { clearClaudePluginRootsCache } from "../../discovery/helpers";
14
+ import {
15
+ getInstalledPluginsRegistryPath,
16
+ getMarketplacesCacheDir,
17
+ getMarketplacesRegistryPath,
18
+ getPluginsCacheDir,
19
+ MarketplaceManager,
20
+ } from "../../extensibility/plugins/marketplace";
10
21
  import {
11
22
  getAvailableThemes,
12
23
  getSymbolTheme,
@@ -19,13 +30,7 @@ import {
19
30
  import type { InteractiveModeContext } from "../../modes/types";
20
31
  import { type SessionInfo, SessionManager } from "../../session/session-manager";
21
32
  import { FileSessionStorage } from "../../session/session-storage";
22
- import {
23
- isCodeSearchProviderId,
24
- isSearchProviderPreference,
25
- setPreferredCodeSearchProvider,
26
- setPreferredImageProvider,
27
- setPreferredSearchProvider,
28
- } from "../../tools";
33
+ import { isSearchProviderPreference, setPreferredImageProvider, setPreferredSearchProvider } from "../../tools";
29
34
  import { setSessionTerminalTitle } from "../../utils/title-generator";
30
35
  import { AgentDashboard } from "../components/agent-dashboard";
31
36
  import { AssistantMessageComponent } from "../components/assistant-message";
@@ -33,6 +38,7 @@ import { ExtensionDashboard } from "../components/extensions";
33
38
  import { HistorySearchComponent } from "../components/history-search";
34
39
  import { ModelSelectorComponent } from "../components/model-selector";
35
40
  import { OAuthSelectorComponent } from "../components/oauth-selector";
41
+ import { PluginSelectorComponent } from "../components/plugin-selector";
36
42
  import { SessionSelectorComponent } from "../components/session-selector";
37
43
  import { SettingsSelectorComponent } from "../components/settings-selector";
38
44
  import { ToolExecutionComponent } from "../components/tool-execution";
@@ -355,11 +361,6 @@ export class SelectorController {
355
361
  setPreferredSearchProvider(value);
356
362
  }
357
363
  break;
358
- case "providers.codeSearch":
359
- if (typeof value === "string" && isCodeSearchProviderId(value)) {
360
- setPreferredCodeSearchProvider(value);
361
- }
362
- break;
363
364
  case "providers.image":
364
365
  if (value === "auto" || value === "gemini" || value === "openrouter") {
365
366
  setPreferredImageProvider(value);
@@ -425,6 +426,96 @@ export class SelectorController {
425
426
  });
426
427
  }
427
428
 
429
+ async showPluginSelector(mode: "install" | "uninstall" = "install"): Promise<void> {
430
+ const mgr = new MarketplaceManager({
431
+ marketplacesRegistryPath: getMarketplacesRegistryPath(),
432
+ installedRegistryPath: getInstalledPluginsRegistryPath(),
433
+ marketplacesCacheDir: getMarketplacesCacheDir(),
434
+ pluginsCacheDir: getPluginsCacheDir(),
435
+ clearPluginRootsCache: () => {
436
+ const home = os.homedir();
437
+ invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
438
+ invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
439
+ clearClaudePluginRootsCache();
440
+ },
441
+ });
442
+
443
+ const [marketplaces, installed] = await Promise.all([mgr.listMarketplaces(), mgr.listInstalledPlugins()]);
444
+ const installedIds = new Set(installed.map(p => p.id));
445
+
446
+ if (mode === "uninstall") {
447
+ // Show only installed plugins for uninstall
448
+ const items = installed.map(p => {
449
+ const entry = p.entries[0];
450
+ const atIdx = p.id.lastIndexOf("@");
451
+ const pluginName = atIdx > 0 ? p.id.slice(0, atIdx) : p.id;
452
+ const mkt = atIdx > 0 ? p.id.slice(atIdx + 1) : "unknown";
453
+ return {
454
+ plugin: { name: pluginName, version: entry?.version, description: undefined as string | undefined },
455
+ marketplace: mkt,
456
+ };
457
+ });
458
+ this.showSelector(done => {
459
+ const selector = new PluginSelectorComponent(marketplaces.length, items, new Set(), {
460
+ onSelect: async (name, marketplace) => {
461
+ done();
462
+ const pluginId = `${name}@${marketplace}`;
463
+ this.ctx.showStatus(`Uninstalling ${pluginId}...`);
464
+ this.ctx.ui.requestRender();
465
+ try {
466
+ await mgr.uninstallPlugin(pluginId);
467
+ this.ctx.showStatus(`Uninstalled ${pluginId}`);
468
+ } catch (err) {
469
+ this.ctx.showStatus(`Uninstall failed: ${err}`);
470
+ }
471
+ this.ctx.ui.requestRender();
472
+ },
473
+ onCancel: () => {
474
+ done();
475
+ this.ctx.ui.requestRender();
476
+ },
477
+ });
478
+ return { component: selector, focus: selector.getSelectList() };
479
+ });
480
+ return;
481
+ }
482
+
483
+ // Install mode: show all available plugins from all marketplaces
484
+ const allPlugins: Array<{
485
+ plugin: { name: string; version?: string; description?: string };
486
+ marketplace: string;
487
+ }> = [];
488
+ for (const mkt of marketplaces) {
489
+ const plugins = await mgr.listAvailablePlugins(mkt.name);
490
+ for (const plugin of plugins) {
491
+ allPlugins.push({ plugin, marketplace: mkt.name });
492
+ }
493
+ }
494
+
495
+ this.showSelector(done => {
496
+ const selector = new PluginSelectorComponent(marketplaces.length, allPlugins, installedIds, {
497
+ onSelect: async (name, marketplace) => {
498
+ done();
499
+ this.ctx.showStatus(`Installing ${name} from ${marketplace}...`);
500
+ this.ctx.ui.requestRender();
501
+ try {
502
+ const force = installedIds.has(`${name}@${marketplace}`);
503
+ await mgr.installPlugin(name, marketplace, { force });
504
+ this.ctx.showStatus(`Installed ${name} from ${marketplace}`);
505
+ } catch (err) {
506
+ this.ctx.showStatus(`Install failed: ${err}`);
507
+ }
508
+ this.ctx.ui.requestRender();
509
+ },
510
+ onCancel: () => {
511
+ done();
512
+ this.ctx.ui.requestRender();
513
+ },
514
+ });
515
+ return { component: selector, focus: selector.getSelectList() };
516
+ });
517
+ }
518
+
428
519
  showUserMessageSelector(): void {
429
520
  const userMessages = this.ctx.session.getUserMessagesForBranching();
430
521
 
@@ -1243,6 +1243,10 @@ export class InteractiveMode implements InteractiveModeContext {
1243
1243
  this.#selectorController.showModelSelector(options);
1244
1244
  }
1245
1245
 
1246
+ showPluginSelector(mode?: "install" | "uninstall"): void {
1247
+ void this.#selectorController.showPluginSelector(mode);
1248
+ }
1249
+
1246
1250
  showUserMessageSelector(): void {
1247
1251
  this.#selectorController.showUserMessageSelector();
1248
1252
  }
@@ -198,6 +198,7 @@ export interface InteractiveModeContext {
198
198
  showExtensionsDashboard(): void;
199
199
  showAgentsDashboard(): void;
200
200
  showModelSelector(options?: { temporaryOnly?: boolean }): void;
201
+ showPluginSelector(mode?: "install" | "uninstall"): void;
201
202
  showUserMessageSelector(): void;
202
203
  showTreeSelector(): void;
203
204
  showSessionSelector(): void;
@@ -2,7 +2,7 @@
2
2
  name: reviewer
3
3
  description: "Code review specialist for quality/security analysis"
4
4
  tools: read, grep, find, bash, lsp, fetch, web_search, ast_grep, report_finding
5
- spawns: explore, task
5
+ spawns: explore
6
6
  model: pi/slow
7
7
  thinking-level: high
8
8
  blocking: true
@@ -62,9 +62,8 @@ Your goal is to identify bugs the author would want fixed before merge.
62
62
  <procedure>
63
63
  1. Run `git diff` (or `gh pr diff <number>`) to view patch
64
64
  2. Read modified files for full context
65
- 3. For large changes, spawn parallel `task` agents (per module/concern)
66
- 4. Call `report_finding` per issue
67
- 5. Call `submit_result` with verdict
65
+ 3. Call `report_finding` per issue
66
+ 4. Call `submit_result` with verdict
68
67
 
69
68
  Bash is read-only: `git diff`, `git log`, `git show`, `gh pr diff`. You **MUST NOT** make file edits or trigger builds.
70
69
  </procedure>
package/src/sdk.ts CHANGED
@@ -106,14 +106,12 @@ import {
106
106
  GrepTool,
107
107
  getSearchTools,
108
108
  HIDDEN_TOOLS,
109
- isCodeSearchProviderId,
110
109
  isSearchProviderPreference,
111
110
  loadSshTool,
112
111
  PythonTool,
113
112
  ReadTool,
114
113
  ResolveTool,
115
114
  renderSearchToolBm25Description,
116
- setPreferredCodeSearchProvider,
117
115
  setPreferredImageProvider,
118
116
  setPreferredSearchProvider,
119
117
  type Tool,
@@ -667,11 +665,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
667
665
  setPreferredSearchProvider(webSearchProvider);
668
666
  }
669
667
 
670
- const codeSearchProvider = settings.get("providers.codeSearch");
671
- if (typeof codeSearchProvider === "string" && isCodeSearchProviderId(codeSearchProvider)) {
672
- setPreferredCodeSearchProvider(codeSearchProvider);
673
- }
674
-
675
668
  const imageProvider = settings.get("providers.image");
676
669
  if (imageProvider === "auto" || imageProvider === "gemini" || imageProvider === "openrouter") {
677
670
  setPreferredImageProvider(imageProvider);