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

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 (49) hide show
  1. package/CHANGELOG.md +58 -1
  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/commit/git/index.ts +3 -4
  8. package/src/commit/model-selection.ts +1 -19
  9. package/src/config/model-registry.ts +19 -3
  10. package/src/config/model-resolver.ts +21 -0
  11. package/src/config/settings-schema.ts +12 -13
  12. package/src/cursor.ts +66 -1
  13. package/src/discovery/claude-plugins.ts +95 -5
  14. package/src/discovery/helpers.ts +168 -41
  15. package/src/discovery/plugin-dir-roots.ts +28 -0
  16. package/src/discovery/substitute-plugin-root.ts +29 -0
  17. package/src/extensibility/plugins/index.ts +1 -0
  18. package/src/extensibility/plugins/marketplace/cache.ts +136 -0
  19. package/src/extensibility/plugins/marketplace/fetcher.ts +354 -0
  20. package/src/extensibility/plugins/marketplace/index.ts +6 -0
  21. package/src/extensibility/plugins/marketplace/manager.ts +528 -0
  22. package/src/extensibility/plugins/marketplace/registry.ts +181 -0
  23. package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
  24. package/src/extensibility/plugins/marketplace/types.ts +177 -0
  25. package/src/internal-urls/index.ts +1 -0
  26. package/src/internal-urls/local-protocol.ts +2 -19
  27. package/src/internal-urls/parse.ts +72 -0
  28. package/src/internal-urls/router.ts +2 -18
  29. package/src/lsp/config.ts +9 -0
  30. package/src/main.ts +51 -1
  31. package/src/modes/components/plugin-selector.ts +86 -0
  32. package/src/modes/components/settings-defs.ts +0 -4
  33. package/src/modes/controllers/mcp-command-controller.ts +14 -0
  34. package/src/modes/controllers/selector-controller.ts +104 -13
  35. package/src/modes/interactive-mode.ts +4 -0
  36. package/src/modes/types.ts +1 -0
  37. package/src/patch/shared.ts +28 -3
  38. package/src/prompts/agents/reviewer.md +3 -4
  39. package/src/sdk.ts +0 -7
  40. package/src/slash-commands/builtin-registry.ts +273 -0
  41. package/src/tools/auto-generated-guard.ts +1 -1
  42. package/src/tools/bash-skill-urls.ts +48 -5
  43. package/src/tools/read.ts +15 -9
  44. package/src/tools/render-utils.ts +2 -2
  45. package/src/utils/title-generator.ts +4 -8
  46. package/src/web/search/index.ts +2 -38
  47. package/src/web/search/types.ts +0 -6
  48. package/src/prompts/tools/code-search.md +0 -45
  49. package/src/web/search/code-search.ts +0 -385
package/CHANGELOG.md CHANGED
@@ -2,6 +2,63 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.17.1] - 2026-04-01
6
+ ### Removed
7
+
8
+ - Removed `code_search` tool for code snippet and documentation search
9
+
10
+ ### Fixed
11
+
12
+ - Fixed edit tool diff rendering to wrap long diff lines with continuation gutters instead of truncating them at terminal width ([#578](https://github.com/can1357/oh-my-pi/issues/578))
13
+ - Fixed `--list-models` and `/model` provider filtering to hide models from disabled providers ([#588](https://github.com/can1357/oh-my-pi/issues/588))
14
+ - Fixed edit tool diffstats to use diff-specific add/remove theme colors instead of success/error status colors ([#589](https://github.com/can1357/oh-my-pi/issues/589))
15
+
16
+
17
+ ## [13.17.0] - 2026-03-30
18
+
19
+ ### Added
20
+
21
+ - Added `marketplace.autoUpdate` setting (`off`/`notify`/`auto`, default `notify`) for automatic plugin update checking on startup
22
+ - Added background marketplace catalog refresh on startup when catalogs are stale (>24h)
23
+ - Added `/marketplace upgrade [name@marketplace]` slash command to upgrade outdated plugins
24
+ - Added `omp plugin upgrade [name@marketplace]` CLI command for plugin upgrades
25
+ - Added `checkForUpdates()`, `upgradePlugin()`, `upgradeAllPlugins()`, and `refreshStaleMarketplaces()` to MarketplaceManager
26
+ - Added marketplace plugin system: registry types, ID helpers, atomic read/write for `marketplaces.json` and `installed_plugins.json` (Claude Code-compatible format)
27
+ - Added `MarketplaceManager` orchestrator for marketplace and plugin lifecycle (add/remove/update marketplaces, install/uninstall/enable plugins)
28
+ - Added marketplace fetcher with source classification (GitHub, git, URL, local) and catalog validation
29
+ - Added plugin source resolver with `pathIsWithin` containment checks and versioned cache manager
30
+ - Added CLI commands: `omp plugin marketplace add|remove|update|list`, `omp plugin discover [marketplace]`
31
+ - Added `classifyInstallTarget()` to distinguish `name@marketplace` from npm install targets
32
+ - Extended `listClaudePluginRoots()` to read OMP's installed plugins registry alongside Claude Code's, with OMP as authoritative for duplicate plugin IDs
33
+ - Added `--plugin-dir <path>` repeatable CLI flag for loading plugins from local directories
34
+ - Added `/reload-plugins` slash command that invalidates fs content cache and plugin roots cache
35
+ - Added `printPluginHelp()` entries for marketplace and discover commands
36
+ - Added MCP server loading from marketplace plugin `.mcp.json` files with `${CLAUDE_PLUGIN_ROOT}` variable substitution
37
+ - Added skill and command namespacing for marketplace plugins (`plugin-name:skill-name`)
38
+ - Added LSP config loading from marketplace plugin roots via `getPreloadedPluginRoots()`
39
+ - Wired `--plugin-dir` runtime injection into plugin roots at session startup with highest precedence
40
+ - Added git (GitHub, SSH, HTTPS) and HTTP URL marketplace source fetching
41
+ - Added `/marketplace` TUI slash command with subcommands: add, remove, update, list, discover, install, uninstall, installed
42
+ - Added `/plugins` TUI slash command to view all installed plugins (npm + marketplace) and enable/disable marketplace plugins
43
+
44
+ ### Changed
45
+
46
+ - Changed marketplace clone promotion to occur after duplicate and drift checks, improving safety of concurrent marketplace operations
47
+
48
+ ### Removed
49
+
50
+ - Removed grep.app code search provider support; code search now uses Exa exclusively
51
+ - Removed `providers.codeSearch` setting and related configuration options
52
+
53
+ ### Fixed
54
+
55
+ - Fixed git-subdir plugin source resolution to properly clean up temporary clone directories on path validation errors
56
+ - Fixed LSP config loading to use correct filenames variable when scanning plugin roots
57
+ - Fixed plugin selector UI to request render on cancel, preventing stale display state
58
+ - Fixed marketplace install command error handling to display user-friendly error messages instead of crashing
59
+ - Fixed MCP tools from newly added servers not being activated after `/mcp add` — `refreshMCPTools` preserves prior MCP tool selections, so brand-new servers had their tools registered in the registry but never passed to the agent; tools are now explicitly activated on successful connection
60
+ - Fixed `skill://` URI resolver to handle namespaced skills via longest-prefix matching against registered skill names
61
+
5
62
  ## [13.16.5] - 2026-03-29
6
63
 
7
64
  ### Fixed
@@ -6406,4 +6463,4 @@ Initial public release.
6406
6463
  - Git branch display in footer
6407
6464
  - Message queueing during streaming responses
6408
6465
  - OAuth integration for Gmail and Google Calendar access
6409
- - HTML export with syntax highlighting and collapsible sections
6466
+ - HTML export with syntax highlighting and collapsible sections
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.16.5",
4
+ "version": "13.17.1",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -42,12 +42,12 @@
42
42
  "dependencies": {
43
43
  "@agentclientprotocol/sdk": "0.16.1",
44
44
  "@mozilla/readability": "^0.6",
45
- "@oh-my-pi/omp-stats": "13.16.5",
46
- "@oh-my-pi/pi-agent-core": "13.16.5",
47
- "@oh-my-pi/pi-ai": "13.16.5",
48
- "@oh-my-pi/pi-natives": "13.16.5",
49
- "@oh-my-pi/pi-tui": "13.16.5",
50
- "@oh-my-pi/pi-utils": "13.16.5",
45
+ "@oh-my-pi/omp-stats": "13.17.1",
46
+ "@oh-my-pi/pi-agent-core": "13.17.1",
47
+ "@oh-my-pi/pi-ai": "13.17.1",
48
+ "@oh-my-pi/pi-natives": "13.17.1",
49
+ "@oh-my-pi/pi-tui": "13.17.1",
50
+ "@oh-my-pi/pi-utils": "13.17.1",
51
51
  "@sinclair/typebox": "^0.34",
52
52
  "@xterm/headless": "^6.0",
53
53
  "ajv": "^8.18",
package/src/cli/args.ts CHANGED
@@ -38,6 +38,7 @@ export interface Args {
38
38
  hooks?: string[];
39
39
  extensions?: string[];
40
40
  noExtensions?: boolean;
41
+ pluginDirs?: string[];
41
42
  print?: boolean;
42
43
  export?: string;
43
44
  noSkills?: boolean;
@@ -151,6 +152,9 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
151
152
  } else if ((arg === "--extension" || arg === "-e") && i + 1 < args.length) {
152
153
  result.extensions = result.extensions ?? [];
153
154
  result.extensions.push(args[++i]);
155
+ } else if (arg === "--plugin-dir" && i + 1 < args.length) {
156
+ result.pluginDirs = result.pluginDirs ?? [];
157
+ result.pluginDirs.push(args[++i]);
154
158
  } else if (arg === "--no-extensions") {
155
159
  result.noExtensions = true;
156
160
  } else if (arg === "--no-skills") {
@@ -262,6 +266,9 @@ ${chalk.bold("Available Tools (default-enabled unless noted):")}
262
266
  web_search - Search the web
263
267
  ask - Ask user questions (interactive mode only)
264
268
 
269
+ ${chalk.bold("Plugin Options:")}
270
+ --plugin-dir <path> Load plugin from directory (repeatable)
271
+
265
272
  ${chalk.bold("Useful Commands:")}
266
273
  omp agents unpack - Export bundled subagents to ~/.omp/agent/agents (default)
267
274
  omp agents unpack --project - Export bundled subagents to ./.omp/agents`;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Classify an install spec as a marketplace plugin reference or a plain npm package.
3
+ *
4
+ * Rules (applied in order):
5
+ * 1. Starts with `@` (scoped npm) -> always npm.
6
+ * 2. Contains `@` after the first character -> split on the LAST `@`.
7
+ * If the right-hand side is a known marketplace name, it's a marketplace ref.
8
+ * Otherwise it's an npm spec (e.g. `pkg@1.2.3`).
9
+ * 3. No `@` -> npm.
10
+ */
11
+ // Common npm dist-tags that should never be interpreted as marketplace names
12
+ const NPM_DIST_TAGS = new Set([
13
+ "latest",
14
+ "next",
15
+ "beta",
16
+ "alpha",
17
+ "canary",
18
+ "rc",
19
+ "dev",
20
+ "stable",
21
+ "nightly",
22
+ "experimental",
23
+ ]);
24
+
25
+ // Semver-like: starts with digit, or contains version range prefixes
26
+ const LOOKS_LIKE_VERSION = /^[\d~^>=<]/;
27
+
28
+ export function classifyInstallTarget(
29
+ spec: string,
30
+ knownMarketplaces: Set<string>,
31
+ ): { type: "marketplace"; name: string; marketplace: string } | { type: "npm"; spec: string } {
32
+ // Rule 1: scoped npm package — @ at position 0 is never a marketplace separator.
33
+ if (spec.startsWith("@")) return { type: "npm", spec };
34
+ // Rule 2: @ somewhere after the first character.
35
+ const atIdx = spec.lastIndexOf("@");
36
+ if (atIdx > 0) {
37
+ const rhs = spec.slice(atIdx + 1);
38
+ // Dist-tags and version specifiers are never marketplace names.
39
+ if (NPM_DIST_TAGS.has(rhs) || LOOKS_LIKE_VERSION.test(rhs)) {
40
+ return { type: "npm", spec };
41
+ }
42
+ if (knownMarketplaces.has(rhs)) {
43
+ return { type: "marketplace", name: spec.slice(0, atIdx), marketplace: rhs };
44
+ }
45
+ // Not a known marketplace — treat as npm version specifier.
46
+ return { type: "npm", spec };
47
+ }
48
+ // Rule 3: no @ at all.
49
+ return { type: "npm", spec };
50
+ }
@@ -7,6 +7,13 @@
7
7
  import { APP_NAME } from "@oh-my-pi/pi-utils";
8
8
  import chalk from "chalk";
9
9
  import { PluginManager, parseSettingValue, validateSetting } from "../extensibility/plugins";
10
+ import {
11
+ getInstalledPluginsRegistryPath,
12
+ getMarketplacesCacheDir,
13
+ getMarketplacesRegistryPath,
14
+ getPluginsCacheDir,
15
+ MarketplaceManager,
16
+ } from "../extensibility/plugins/marketplace/index.js";
10
17
  import { theme } from "../modes/theme/theme";
11
18
 
12
19
  // =============================================================================
@@ -22,7 +29,10 @@ export type PluginAction =
22
29
  | "features"
23
30
  | "config"
24
31
  | "enable"
25
- | "disable";
32
+ | "disable"
33
+ | "marketplace"
34
+ | "discover"
35
+ | "upgrade";
26
36
 
27
37
  export interface PluginCommandArgs {
28
38
  action: PluginAction;
@@ -53,6 +63,9 @@ const VALID_ACTIONS: PluginAction[] = [
53
63
  "config",
54
64
  "enable",
55
65
  "disable",
66
+ "marketplace",
67
+ "discover",
68
+ "upgrade",
56
69
  ];
57
70
 
58
71
  /**
@@ -108,6 +121,10 @@ export function parsePluginArgs(args: string[]): PluginCommandArgs | undefined {
108
121
  return result;
109
122
  }
110
123
 
124
+ import { classifyInstallTarget } from "./classify-install-target";
125
+
126
+ export { classifyInstallTarget } from "./classify-install-target";
127
+
111
128
  // =============================================================================
112
129
  // Command Handlers
113
130
  // =============================================================================
@@ -146,6 +163,153 @@ export async function runPluginCommand(cmd: PluginCommandArgs): Promise<void> {
146
163
  case "disable":
147
164
  await handleDisable(manager, cmd.args, cmd.flags);
148
165
  break;
166
+ case "marketplace":
167
+ await handleMarketplace(cmd.args, cmd.flags);
168
+ break;
169
+ case "discover":
170
+ await handleDiscover(cmd.args, cmd.flags);
171
+ break;
172
+ case "upgrade":
173
+ await handleUpgrade(cmd.args, cmd.flags);
174
+ break;
175
+ }
176
+ }
177
+
178
+ // =============================================================================
179
+ // Marketplace Handlers
180
+ // =============================================================================
181
+
182
+ function makeMarketplaceManager(): MarketplaceManager {
183
+ return new MarketplaceManager({
184
+ marketplacesRegistryPath: getMarketplacesRegistryPath(),
185
+ installedRegistryPath: getInstalledPluginsRegistryPath(),
186
+ marketplacesCacheDir: getMarketplacesCacheDir(),
187
+ pluginsCacheDir: getPluginsCacheDir(),
188
+ });
189
+ }
190
+
191
+ async function handleMarketplace(args: string[], _flags: PluginCommandArgs["flags"]): Promise<void> {
192
+ const subcommand = args[0] ?? "list";
193
+ const manager = makeMarketplaceManager();
194
+
195
+ switch (subcommand) {
196
+ case "add": {
197
+ const source = args[1];
198
+ if (!source) {
199
+ console.error(chalk.red(`Usage: ${APP_NAME} plugin marketplace add <source>`));
200
+ process.exit(1);
201
+ }
202
+ try {
203
+ await manager.addMarketplace(source);
204
+ console.log(chalk.green(`${theme.status.success} Added marketplace: ${source}`));
205
+ } catch (err) {
206
+ console.error(chalk.red(`${theme.status.error} Failed to add marketplace: ${err}`));
207
+ process.exit(1);
208
+ }
209
+ break;
210
+ }
211
+ case "remove":
212
+ case "rm": {
213
+ const name = args[1];
214
+ if (!name) {
215
+ console.error(chalk.red(`Usage: ${APP_NAME} plugin marketplace remove <name>`));
216
+ process.exit(1);
217
+ }
218
+ try {
219
+ await manager.removeMarketplace(name);
220
+ console.log(chalk.green(`${theme.status.success} Removed marketplace: ${name}`));
221
+ } catch (err) {
222
+ console.error(chalk.red(`${theme.status.error} Failed to remove marketplace: ${err}`));
223
+ process.exit(1);
224
+ }
225
+ break;
226
+ }
227
+ case "update": {
228
+ try {
229
+ const name = args[1];
230
+ if (name) {
231
+ await manager.updateMarketplace(name);
232
+ console.log(chalk.green(`${theme.status.success} Updated marketplace: ${name}`));
233
+ } else {
234
+ const results = await manager.updateAllMarketplaces();
235
+ console.log(chalk.green(`${theme.status.success} Updated ${results.length} marketplace(s)`));
236
+ }
237
+ } catch (err) {
238
+ console.error(chalk.red(`${theme.status.error} Failed to update marketplace: ${err}`));
239
+ process.exit(1);
240
+ }
241
+ break;
242
+ }
243
+ default: {
244
+ if (subcommand !== "list") {
245
+ console.error(chalk.red(`Unknown marketplace subcommand: ${subcommand}`));
246
+ console.error(chalk.dim("Valid subcommands: add, remove, update, list"));
247
+ process.exit(1);
248
+ }
249
+ try {
250
+ const marketplaces = await manager.listMarketplaces();
251
+ if (marketplaces.length === 0) {
252
+ console.log(chalk.dim("No marketplaces configured"));
253
+ console.log(chalk.dim(`\nAdd one with: ${APP_NAME} plugin marketplace add <source>`));
254
+ return;
255
+ }
256
+ console.log(chalk.bold("Configured Marketplaces:\n"));
257
+ for (const mp of marketplaces) {
258
+ console.log(` ${chalk.cyan(mp.name)} ${chalk.dim(mp.sourceUri)}`);
259
+ }
260
+ } catch (err) {
261
+ console.error(chalk.red(`${theme.status.error} Failed to list marketplaces: ${err}`));
262
+ process.exit(1);
263
+ }
264
+ break;
265
+ }
266
+ }
267
+ }
268
+
269
+ async function handleDiscover(args: string[], _flags: PluginCommandArgs["flags"]): Promise<void> {
270
+ const marketplace = args[0];
271
+ const manager = makeMarketplaceManager();
272
+ try {
273
+ const plugins = await manager.listAvailablePlugins(marketplace);
274
+
275
+ if (plugins.length === 0) {
276
+ console.log(chalk.dim(marketplace ? `No plugins found in ${marketplace}` : "No plugins available"));
277
+ return;
278
+ }
279
+
280
+ console.log(chalk.bold(`Available Plugins${marketplace ? ` (${marketplace})` : ""}:\n`));
281
+ for (const plugin of plugins) {
282
+ console.log(` ${chalk.cyan(plugin.name)}${plugin.version ? `@${plugin.version}` : ""}`);
283
+ if (plugin.description) {
284
+ console.log(chalk.dim(` ${plugin.description}`));
285
+ }
286
+ }
287
+ } catch (err) {
288
+ console.error(chalk.red(`${theme.status.error} Failed to discover plugins: ${err}`));
289
+ process.exit(1);
290
+ }
291
+ }
292
+
293
+ async function handleUpgrade(args: string[], _flags: PluginCommandArgs["flags"]): Promise<void> {
294
+ const manager = makeMarketplaceManager();
295
+ const pluginId = args[0];
296
+ try {
297
+ if (pluginId) {
298
+ const result = await manager.upgradePlugin(pluginId);
299
+ console.log(chalk.green(`Upgraded ${pluginId} to ${result.version}`));
300
+ } else {
301
+ const results = await manager.upgradeAllPlugins();
302
+ if (results.length === 0) {
303
+ console.log("All marketplace plugins are up to date.");
304
+ } else {
305
+ for (const r of results) {
306
+ console.log(chalk.green(` ${r.pluginId}: ${r.from} -> ${r.to}`));
307
+ }
308
+ }
309
+ }
310
+ } catch (err) {
311
+ console.error(chalk.red(`Failed to upgrade: ${err}`));
312
+ process.exit(1);
149
313
  }
150
314
  }
151
315
 
@@ -158,13 +322,35 @@ async function handleInstall(
158
322
  console.error(chalk.red(`Usage: ${APP_NAME} plugin install <package[@version]>[features] ...`));
159
323
  console.error(chalk.dim("Examples:"));
160
324
  console.error(chalk.dim(` ${APP_NAME} plugin install @oh-my-pi/exa`));
161
- console.error(chalk.dim(` ${APP_NAME} plugin install @oh-my-pi/exa[search,websets]`));
162
- console.error(chalk.dim(` ${APP_NAME} plugin install @oh-my-pi/exa[*] # all features`));
163
- console.error(chalk.dim(` ${APP_NAME} plugin install @oh-my-pi/exa[] # no optional features`));
325
+ console.error(chalk.dim(` ${APP_NAME} plugin install name@marketplace`));
164
326
  process.exit(1);
165
327
  }
166
328
 
329
+ // Build known marketplace set for classification
330
+ const mktMgr = makeMarketplaceManager();
331
+ const knownMarketplaces = new Set((await mktMgr.listMarketplaces()).map(m => m.name));
332
+
167
333
  for (const spec of packages) {
334
+ const target = classifyInstallTarget(spec, knownMarketplaces);
335
+
336
+ if (target.type === "marketplace") {
337
+ try {
338
+ const entry = await mktMgr.installPlugin(target.name, target.marketplace, {
339
+ force: flags.force,
340
+ });
341
+ console.log(
342
+ chalk.green(
343
+ `${theme.status.success} Installed ${target.name} from ${target.marketplace} (${entry.version})`,
344
+ ),
345
+ );
346
+ } catch (err) {
347
+ console.error(chalk.red(`${theme.status.error} Failed to install ${spec}: ${err}`));
348
+ process.exit(1);
349
+ }
350
+ continue;
351
+ }
352
+
353
+ // npm path
168
354
  try {
169
355
  const result = await manager.install(spec, { force: flags.force, dryRun: flags.dryRun });
170
356
 
@@ -196,10 +382,27 @@ async function handleUninstall(manager: PluginManager, packages: string[], flags
196
382
  process.exit(1);
197
383
  }
198
384
 
385
+ // For uninstall, check the installed plugins registry directly.
386
+ // This works even if the marketplace entry was later removed from marketplaces.json.
387
+ const mktMgr = makeMarketplaceManager();
388
+ const installedPlugins = new Set((await mktMgr.listInstalledPlugins()).map(p => p.id));
389
+
199
390
  for (const name of packages) {
391
+ if (installedPlugins.has(name)) {
392
+ // Exact match against installed marketplace plugin IDs (name@marketplace)
393
+ try {
394
+ await mktMgr.uninstallPlugin(name);
395
+ console.log(chalk.green(`${theme.status.success} Uninstalled ${name}`));
396
+ } catch (err) {
397
+ console.error(chalk.red(`${theme.status.error} Failed to uninstall ${name}: ${err}`));
398
+ process.exit(1);
399
+ }
400
+ continue;
401
+ }
402
+
403
+ // npm path
200
404
  try {
201
405
  await manager.uninstall(name);
202
-
203
406
  if (flags.json) {
204
407
  console.log(JSON.stringify({ uninstalled: name }));
205
408
  } else {
@@ -213,44 +416,53 @@ async function handleUninstall(manager: PluginManager, packages: string[], flags
213
416
  }
214
417
 
215
418
  async function handleList(manager: PluginManager, flags: { json?: boolean }): Promise<void> {
216
- const plugins = await manager.list();
419
+ const npmPlugins = await manager.list();
420
+ const mktMgr = makeMarketplaceManager();
421
+ const mktPlugins = await mktMgr.listInstalledPlugins();
217
422
 
218
423
  if (flags.json) {
219
- console.log(JSON.stringify(plugins, null, 2));
424
+ console.log(JSON.stringify({ npm: npmPlugins, marketplace: mktPlugins }, null, 2));
220
425
  return;
221
426
  }
222
427
 
223
- if (plugins.length === 0) {
428
+ if (npmPlugins.length === 0 && mktPlugins.length === 0) {
224
429
  console.log(chalk.dim("No plugins installed"));
225
430
  console.log(chalk.dim(`\nInstall plugins with: ${APP_NAME} plugin install <package>`));
226
431
  return;
227
432
  }
228
433
 
229
- console.log(chalk.bold("Installed Plugins:\n"));
230
-
231
- for (const plugin of plugins) {
232
- const status = plugin.enabled ? chalk.green(theme.status.enabled) : chalk.dim(theme.status.disabled);
233
- const nameVersion = `${plugin.name}@${plugin.version}`;
234
- console.log(`${status} ${nameVersion}`);
235
-
236
- if (plugin.manifest.description) {
237
- console.log(chalk.dim(` ${plugin.manifest.description}`));
238
- }
239
-
240
- if (plugin.enabledFeatures && plugin.enabledFeatures.length > 0) {
241
- console.log(chalk.dim(` Features: ${plugin.enabledFeatures.join(", ")}`));
434
+ if (npmPlugins.length > 0) {
435
+ console.log(chalk.bold("npm Plugins:\n"));
436
+ for (const plugin of npmPlugins) {
437
+ const status = plugin.enabled ? chalk.green(theme.status.enabled) : chalk.dim(theme.status.disabled);
438
+ const nameVersion = `${plugin.name}@${plugin.version}`;
439
+ console.log(`${status} ${nameVersion}`);
440
+ if (plugin.manifest.description) {
441
+ console.log(chalk.dim(` ${plugin.manifest.description}`));
442
+ }
443
+ if (plugin.enabledFeatures && plugin.enabledFeatures.length > 0) {
444
+ console.log(chalk.dim(` Features: ${plugin.enabledFeatures.join(", ")}`));
445
+ }
446
+ if (plugin.manifest.features) {
447
+ const availableFeatures = Object.keys(plugin.manifest.features);
448
+ if (availableFeatures.length > 0) {
449
+ const enabledSet = new Set(plugin.enabledFeatures ?? []);
450
+ const featureDisplay = availableFeatures
451
+ .map(f => (enabledSet.has(f) ? chalk.green(f) : chalk.dim(f)))
452
+ .join(", ");
453
+ console.log(chalk.dim(` Available: [${featureDisplay}]`));
454
+ }
455
+ }
242
456
  }
457
+ }
243
458
 
244
- // Show available features if manifest has them
245
- if (plugin.manifest.features) {
246
- const availableFeatures = Object.keys(plugin.manifest.features);
247
- if (availableFeatures.length > 0) {
248
- const enabledSet = new Set(plugin.enabledFeatures ?? []);
249
- const featureDisplay = availableFeatures
250
- .map(f => (enabledSet.has(f) ? chalk.green(f) : chalk.dim(f)))
251
- .join(", ");
252
- console.log(chalk.dim(` Available: [${featureDisplay}]`));
253
- }
459
+ if (mktPlugins.length > 0) {
460
+ if (npmPlugins.length > 0) console.log();
461
+ console.log(chalk.bold("Marketplace Plugins:\n"));
462
+ for (const plugin of mktPlugins) {
463
+ const entry = plugin.entries[0];
464
+ const version = entry?.version ?? "unknown";
465
+ console.log(` ${plugin.id} (${version})`);
254
466
  }
255
467
  }
256
468
  }
@@ -630,6 +842,8 @@ ${chalk.bold("Commands:")}
630
842
  config <cmd> <pkg> [key] [val] Manage plugin settings
631
843
  enable <pkg> Enable a disabled plugin
632
844
  disable <pkg> Disable plugin without uninstalling
845
+ marketplace <cmd> Manage marketplace sources (add, remove, update, list)
846
+ discover [marketplace] Browse available marketplace plugins
633
847
 
634
848
  ${chalk.bold("Feature Syntax:")}
635
849
  pkg Install with default features
@@ -15,6 +15,9 @@ const ACTIONS: PluginAction[] = [
15
15
  "config",
16
16
  "enable",
17
17
  "disable",
18
+ "marketplace",
19
+ "discover",
20
+ "upgrade",
18
21
  ];
19
22
 
20
23
  export default class Plugin extends Command {
@@ -189,12 +189,11 @@ function extractFileHeader(diff: string): string {
189
189
  return headerLines.join("\n");
190
190
  }
191
191
 
192
- function joinPatch(parts: string[]): string {
193
- return parts
192
+ export function joinPatch(parts: string[]): string {
193
+ return `${parts
194
194
  .map(part => (part.endsWith("\n") ? part : `${part}\n`))
195
195
  .join("\n")
196
- .trimEnd()
197
- .concat("\n");
196
+ .replace(/\n+$/, "")}\n`;
198
197
  }
199
198
 
200
199
  function selectHunks(file: FileHunks, selector: HunkSelection["hunks"]): FileHunks["hunks"] {
@@ -1,7 +1,7 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
3
  import { MODEL_ROLE_IDS } from "../config/model-registry";
4
- import { parseModelPattern, resolveModelRoleValue } from "../config/model-resolver";
4
+ import { parseModelPattern, resolveModelRoleValue, resolveRoleSelection } from "../config/model-resolver";
5
5
  import type { Settings } from "../config/settings";
6
6
  import MODEL_PRIO from "../priority.json" with { type: "json" };
7
7
 
@@ -11,24 +11,6 @@ export interface ResolvedCommitModel {
11
11
  thinkingLevel?: ThinkingLevel;
12
12
  }
13
13
 
14
- function resolveRoleSelection(
15
- roles: readonly string[],
16
- settings: Settings,
17
- availableModels: Model<Api>[],
18
- ): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
19
- const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
20
- for (const role of roles) {
21
- const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
22
- settings,
23
- matchPreferences,
24
- });
25
- if (resolved.model) {
26
- return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
27
- }
28
- }
29
- return undefined;
30
- }
31
-
32
14
  export async function resolvePrimaryModel(
33
15
  override: string | undefined,
34
16
  settings: Settings,
@@ -31,7 +31,7 @@ import { type ConfigError, ConfigFile } from "../config";
31
31
  import { parseModelString } from "../config/model-resolver";
32
32
  import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
33
33
  import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
34
- import type { Settings } from "./settings";
34
+ import { type Settings, settings } from "./settings";
35
35
 
36
36
  export const kNoAuth = "N/A";
37
37
 
@@ -730,6 +730,14 @@ function normalizeSuppressedSelector(selector: string): string {
730
730
  return `${parsed.provider}/${parsed.id}`;
731
731
  }
732
732
 
733
+ function getDisabledProviderIdsFromSettings(): Set<string> {
734
+ try {
735
+ return new Set(settings.get("disabledProviders"));
736
+ } catch {
737
+ return new Set();
738
+ }
739
+ }
740
+
733
741
  /**
734
742
  * Model registry - loads and manages models, resolves API keys via AuthStorage.
735
743
  */
@@ -1670,11 +1678,19 @@ export class ModelRegistry {
1670
1678
  * This is a fast check that doesn't refresh OAuth tokens.
1671
1679
  */
1672
1680
  getAvailable(): Model<Api>[] {
1673
- return this.#models.filter(m => this.#keylessProviders.has(m.provider) || this.authStorage.hasAuth(m.provider));
1681
+ const disabledProviders = getDisabledProviderIdsFromSettings();
1682
+ return this.#models.filter(
1683
+ m =>
1684
+ !disabledProviders.has(m.provider) &&
1685
+ (this.#keylessProviders.has(m.provider) || this.authStorage.hasAuth(m.provider)),
1686
+ );
1674
1687
  }
1675
1688
 
1676
1689
  getDiscoverableProviders(): string[] {
1677
- return this.#discoverableProviders.map(provider => provider.provider);
1690
+ const disabledProviders = getDisabledProviderIdsFromSettings();
1691
+ return this.#discoverableProviders
1692
+ .filter(provider => !disabledProviders.has(provider.provider))
1693
+ .map(provider => provider.provider);
1678
1694
  }
1679
1695
 
1680
1696
  getProviderDiscoveryState(provider: string): ProviderDiscoveryState | undefined {
@@ -587,6 +587,27 @@ export function resolveModelOverride(
587
587
  return { explicitThinkingLevel: false };
588
588
  }
589
589
 
590
+ /**
591
+ * Resolve a list of role patterns to the first matching model.
592
+ */
593
+ export function resolveRoleSelection(
594
+ roles: readonly string[],
595
+ settings: Settings,
596
+ availableModels: Model<Api>[],
597
+ ): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
598
+ const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() };
599
+ for (const role of roles) {
600
+ const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, {
601
+ settings,
602
+ matchPreferences,
603
+ });
604
+ if (resolved.model) {
605
+ return { model: resolved.model, thinkingLevel: resolved.thinkingLevel };
606
+ }
607
+ }
608
+ return undefined;
609
+ }
610
+
590
611
  /**
591
612
  * Resolve model patterns to actual Model objects with optional thinking levels
592
613
  * Format: "pattern:level" where :level is optional