@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/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.17.0] - 2026-03-30
6
+
7
+ ### Added
8
+
9
+ - Added `marketplace.autoUpdate` setting (`off`/`notify`/`auto`, default `notify`) for automatic plugin update checking on startup
10
+ - Added background marketplace catalog refresh on startup when catalogs are stale (>24h)
11
+ - Added `/marketplace upgrade [name@marketplace]` slash command to upgrade outdated plugins
12
+ - Added `omp plugin upgrade [name@marketplace]` CLI command for plugin upgrades
13
+ - Added `checkForUpdates()`, `upgradePlugin()`, `upgradeAllPlugins()`, and `refreshStaleMarketplaces()` to MarketplaceManager
14
+ - Added marketplace plugin system: registry types, ID helpers, atomic read/write for `marketplaces.json` and `installed_plugins.json` (Claude Code-compatible format)
15
+ - Added `MarketplaceManager` orchestrator for marketplace and plugin lifecycle (add/remove/update marketplaces, install/uninstall/enable plugins)
16
+ - Added marketplace fetcher with source classification (GitHub, git, URL, local) and catalog validation
17
+ - Added plugin source resolver with `pathIsWithin` containment checks and versioned cache manager
18
+ - Added CLI commands: `omp plugin marketplace add|remove|update|list`, `omp plugin discover [marketplace]`
19
+ - Added `classifyInstallTarget()` to distinguish `name@marketplace` from npm install targets
20
+ - Extended `listClaudePluginRoots()` to read OMP's installed plugins registry alongside Claude Code's, with OMP as authoritative for duplicate plugin IDs
21
+ - Added `--plugin-dir <path>` repeatable CLI flag for loading plugins from local directories
22
+ - Added `/reload-plugins` slash command that invalidates fs content cache and plugin roots cache
23
+ - Added `printPluginHelp()` entries for marketplace and discover commands
24
+ - Added MCP server loading from marketplace plugin `.mcp.json` files with `${CLAUDE_PLUGIN_ROOT}` variable substitution
25
+ - Added skill and command namespacing for marketplace plugins (`plugin-name:skill-name`)
26
+ - Added LSP config loading from marketplace plugin roots via `getPreloadedPluginRoots()`
27
+ - Wired `--plugin-dir` runtime injection into plugin roots at session startup with highest precedence
28
+ - Added git (GitHub, SSH, HTTPS) and HTTP URL marketplace source fetching
29
+ - Added `/marketplace` TUI slash command with subcommands: add, remove, update, list, discover, install, uninstall, installed
30
+ - Added `/plugins` TUI slash command to view all installed plugins (npm + marketplace) and enable/disable marketplace plugins
31
+
32
+ ### Changed
33
+
34
+ - Changed marketplace clone promotion to occur after duplicate and drift checks, improving safety of concurrent marketplace operations
35
+
36
+ ### Removed
37
+
38
+ - Removed grep.app code search provider support; code search now uses Exa exclusively
39
+ - Removed `providers.codeSearch` setting and related configuration options
40
+
41
+ ### Fixed
42
+
43
+ - Fixed git-subdir plugin source resolution to properly clean up temporary clone directories on path validation errors
44
+ - Fixed LSP config loading to use correct filenames variable when scanning plugin roots
45
+ - Fixed plugin selector UI to request render on cancel, preventing stale display state
46
+ - Fixed marketplace install command error handling to display user-friendly error messages instead of crashing
47
+ - 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
48
+ - Fixed `skill://` URI resolver to handle namespaced skills via longest-prefix matching against registered skill names
49
+
5
50
  ## [13.16.5] - 2026-03-29
6
51
 
7
52
  ### Fixed
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.0",
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.0",
46
+ "@oh-my-pi/pi-agent-core": "13.17.0",
47
+ "@oh-my-pi/pi-ai": "13.17.0",
48
+ "@oh-my-pi/pi-natives": "13.17.0",
49
+ "@oh-my-pi/pi-tui": "13.17.0",
50
+ "@oh-my-pi/pi-utils": "13.17.0",
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 {
@@ -198,6 +198,18 @@ export const SETTINGS_SCHEMA = {
198
198
 
199
199
  extensions: { type: "array", default: EMPTY_STRING_ARRAY },
200
200
 
201
+ "marketplace.autoUpdate": {
202
+ type: "enum",
203
+ values: ["off", "notify", "auto"] as const,
204
+ default: "notify",
205
+ ui: {
206
+ tab: "tools",
207
+ label: "Marketplace Auto-Update",
208
+ description: "Check for plugin updates on startup (off/notify/auto)",
209
+ submenu: true,
210
+ },
211
+ },
212
+
201
213
  enabledModels: { type: "array", default: EMPTY_STRING_ARRAY },
202
214
 
203
215
  disabledProviders: { type: "array", default: EMPTY_STRING_ARRAY },
@@ -1490,19 +1502,6 @@ export const SETTINGS_SCHEMA = {
1490
1502
  submenu: true,
1491
1503
  },
1492
1504
  },
1493
-
1494
- "providers.codeSearch": {
1495
- type: "enum",
1496
- values: ["grep", "exa"] as const,
1497
- default: "grep",
1498
- ui: {
1499
- tab: "providers",
1500
- label: "Code Search Provider",
1501
- description: "Provider for code search tool",
1502
- submenu: true,
1503
- },
1504
- },
1505
-
1506
1505
  "providers.image": {
1507
1506
  type: "enum",
1508
1507
  values: ["auto", "gemini", "openrouter"] as const,
package/src/cursor.ts CHANGED
@@ -7,7 +7,12 @@ import type {
7
7
  AgentToolResult,
8
8
  AgentToolUpdateCallback,
9
9
  } from "@oh-my-pi/pi-agent-core";
10
- import type { CursorMcpCall, CursorExecHandlers as ICursorExecHandlers, ToolResultMessage } from "@oh-my-pi/pi-ai";
10
+ import type {
11
+ CursorMcpCall,
12
+ CursorShellStreamCallbacks,
13
+ CursorExecHandlers as ICursorExecHandlers,
14
+ ToolResultMessage,
15
+ } from "@oh-my-pi/pi-ai";
11
16
  import { resolveToCwd } from "./tools/path-utils";
12
17
 
13
18
  interface CursorExecBridgeOptions {
@@ -204,6 +209,66 @@ export class CursorExecHandlers implements ICursorExecHandlers {
204
209
  return toolResultMessage;
205
210
  }
206
211
 
212
+ async shellStream(
213
+ args: Parameters<NonNullable<ICursorExecHandlers["shellStream"]>>[0],
214
+ callbacks: CursorShellStreamCallbacks,
215
+ ) {
216
+ const toolCallId = decodeToolCallId(args.toolCallId);
217
+ const toolName = "bash";
218
+ const tool = this.options.tools.get(toolName);
219
+ if (!tool) {
220
+ const result = buildToolErrorResult(`Tool "${toolName}" not available`);
221
+ return createToolResultMessage(toolCallId, toolName, result, true);
222
+ }
223
+
224
+ const timeoutSeconds = args.timeout && args.timeout > 0 ? args.timeout : undefined;
225
+ const toolArgs: Record<string, unknown> = {
226
+ command: args.command,
227
+ cwd: args.workingDirectory || undefined,
228
+ timeout: timeoutSeconds,
229
+ };
230
+
231
+ this.options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: toolArgs });
232
+
233
+ let result: AgentToolResult<unknown>;
234
+ let isError = false;
235
+
236
+ // Track previously streamed text so we only forward deltas.
237
+ let streamedLen = 0;
238
+ const onUpdate: AgentToolUpdateCallback<unknown> = partialResult => {
239
+ this.options.emitEvent?.({
240
+ type: "tool_execution_update",
241
+ toolCallId,
242
+ toolName,
243
+ args: toolArgs,
244
+ partialResult,
245
+ });
246
+ const text = partialResult.content.map(c => (c.type === "text" ? c.text : "")).join("");
247
+ if (text.length > streamedLen) {
248
+ callbacks.onStdout(text.slice(streamedLen));
249
+ streamedLen = text.length;
250
+ }
251
+ };
252
+
253
+ try {
254
+ result = await tool.execute(toolCallId, toolArgs, undefined, onUpdate, this.options.getToolContext?.());
255
+ } catch (error) {
256
+ const message = error instanceof Error ? error.message : String(error);
257
+ result = buildToolErrorResult(message);
258
+ isError = true;
259
+ }
260
+
261
+ // onUpdate may not fire for every chunk — flush any remaining output
262
+ // from the final result that wasn't already streamed.
263
+ const finalText = result.content.map(c => (c.type === "text" ? c.text : "")).join("");
264
+ if (finalText.length > streamedLen) {
265
+ callbacks.onStdout(finalText.slice(streamedLen));
266
+ }
267
+
268
+ this.options.emitEvent?.({ type: "tool_execution_end", toolCallId, toolName, result, isError });
269
+ return createToolResultMessage(toolCallId, toolName, result, isError);
270
+ }
271
+
207
272
  async diagnostics(args: Parameters<NonNullable<ICursorExecHandlers["diagnostics"]>>[0]) {
208
273
  const toolCallId = decodeToolCallId(args.toolCallId);
209
274
  const toolResultMessage = await executeTool(this.options, "lsp", toolCallId, {