feat: added marketplace help command and improved catalog parsing resilience
- Added `/marketplace help` subcommand to display marketplace operations usage guide. - Enhanced marketplace catalog parsing to skip invalid entries with warnings instead of failing. - Improved `/marketplace discover` command to suggest official marketplace when no plugins available. - Fixed marketplace error messages to display error details instead of object stringification. - Added comprehensive marketplace plugin system documentation covering discovery, installation, and management.
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# Marketplace plugin system
|
||||
|
||||
The marketplace system lets you discover, install, and manage plugins from Git-hosted catalogs. It is compatible with the Claude Code plugin registry format.
|
||||
|
||||
## Quick start
|
||||
|
||||
```
|
||||
/marketplace add anthropics/claude-plugins-official
|
||||
/marketplace install wordpress.com@claude-plugins-official
|
||||
```
|
||||
|
||||
Or just type `/marketplace` with no arguments to open the interactive plugin browser.
|
||||
|
||||
## Concepts
|
||||
|
||||
A **marketplace** is a Git repository (or local directory) containing a catalog file at `.claude-plugin/marketplace.json`. The catalog lists available plugins with their sources, descriptions, and metadata.
|
||||
|
||||
A **plugin** is a directory containing skills, commands, hooks, MCP servers, or LSP servers. Plugins are identified by `name@marketplace` (e.g. `code-review@claude-plugins-official`).
|
||||
|
||||
**Scopes**: plugins can be installed at two scopes:
|
||||
|
||||
- **user** (default) -- available in all projects, stored in `~/.omp/plugins/installed_plugins.json`
|
||||
- **project** -- available only in the current project, stored in `.omp/installed_plugins.json`
|
||||
|
||||
Project-scoped installs shadow user-scoped installs of the same plugin.
|
||||
|
||||
## Commands
|
||||
|
||||
### Interactive mode
|
||||
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
| `/marketplace` | Open interactive plugin browser (install) |
|
||||
|
||||
### Marketplace management
|
||||
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
| `/marketplace add <source>` | Add a marketplace source |
|
||||
| `/marketplace remove <name>` | Remove a marketplace |
|
||||
| `/marketplace update [name]` | Re-fetch catalog(s); omit name to update all |
|
||||
| `/marketplace list` | List configured marketplaces |
|
||||
|
||||
### Plugin operations
|
||||
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
| `/marketplace discover [marketplace]` | Browse available plugins |
|
||||
| `/marketplace install [--force] [--scope user\|project] name@marketplace` | Install a plugin |
|
||||
| `/marketplace uninstall [--scope user\|project] name@marketplace` | Uninstall a plugin |
|
||||
| `/marketplace installed` | List installed marketplace plugins |
|
||||
| `/marketplace upgrade [--scope user\|project] [name@marketplace]` | Upgrade one or all plugins |
|
||||
|
||||
### CLI equivalents
|
||||
|
||||
The same operations are available from the command line:
|
||||
|
||||
```
|
||||
omp plugin marketplace add <source>
|
||||
omp plugin marketplace remove <name>
|
||||
omp plugin marketplace update [name]
|
||||
omp plugin marketplace list
|
||||
omp plugin discover [marketplace]
|
||||
omp plugin install --scope project name@marketplace
|
||||
```
|
||||
|
||||
## Marketplace sources
|
||||
|
||||
When you run `/marketplace add <source>`, the system classifies the source:
|
||||
|
||||
| Source format | Type | Example |
|
||||
|---|---|---|
|
||||
| `owner/repo` | GitHub shorthand | `anthropics/claude-plugins-official` |
|
||||
| `https://...*.json` | Direct catalog URL | `https://example.com/marketplace.json` |
|
||||
| `https://...*.git` or `git@...` | Git repository | `https://github.com/org/repo.git` |
|
||||
| `./path` or `~/path` or `/path` | Local directory | `./my-marketplace` |
|
||||
|
||||
The system clones the repository (or reads the local directory), locates `.claude-plugin/marketplace.json`, validates it, and caches the catalog locally.
|
||||
|
||||
## Catalog format (marketplace.json)
|
||||
|
||||
A marketplace catalog lives at `.claude-plugin/marketplace.json` in the repository root:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||
"name": "my-marketplace",
|
||||
"owner": {
|
||||
"name": "Your Name",
|
||||
"email": "you@example.com"
|
||||
},
|
||||
"description": "A collection of plugins",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "my-plugin",
|
||||
"description": "What this plugin does",
|
||||
"source": "./plugins/my-plugin",
|
||||
"category": "development",
|
||||
"homepage": "https://github.com/you/my-plugin"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Required fields
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `name` | Marketplace name. Lowercase alphanumeric, hyphens, and dots. Must start and end with alphanumeric. Max 64 chars. |
|
||||
| `owner.name` | Marketplace owner name |
|
||||
| `plugins` | Array of plugin entries |
|
||||
|
||||
### Plugin entry fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|---|---|---|
|
||||
| `name` | yes | Plugin name (same rules as marketplace name) |
|
||||
| `source` | yes | Where to find the plugin (see below) |
|
||||
| `description` | no | Short description |
|
||||
| `version` | no | Version string |
|
||||
| `author` | no | `{ name, email? }` |
|
||||
| `homepage` | no | URL |
|
||||
| `category` | no | Category string (e.g. `development`, `productivity`, `security`) |
|
||||
| `tags` | no | Array of string tags |
|
||||
| `strict` | no | Boolean |
|
||||
| `commands` | no | Slash commands provided |
|
||||
| `agents` | no | Agents provided |
|
||||
| `hooks` | no | Hook definitions |
|
||||
| `mcpServers` | no | MCP server definitions |
|
||||
| `lspServers` | no | LSP server definitions |
|
||||
|
||||
### Plugin source formats
|
||||
|
||||
The `source` field supports several formats:
|
||||
|
||||
**Relative path** (within the marketplace repo):
|
||||
```json
|
||||
"source": "./plugins/my-plugin"
|
||||
```
|
||||
|
||||
**Git repository URL**:
|
||||
```json
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/org/repo.git",
|
||||
"sha": "abc123..."
|
||||
}
|
||||
```
|
||||
|
||||
**GitHub shorthand**:
|
||||
```json
|
||||
"source": {
|
||||
"source": "github",
|
||||
"repo": "org/repo",
|
||||
"ref": "main",
|
||||
"sha": "abc123..."
|
||||
}
|
||||
```
|
||||
|
||||
**Git subdirectory** (monorepo):
|
||||
```json
|
||||
"source": {
|
||||
"source": "git-subdir",
|
||||
"url": "https://github.com/org/monorepo.git",
|
||||
"path": "plugins/my-plugin",
|
||||
"ref": "main",
|
||||
"sha": "abc123..."
|
||||
}
|
||||
```
|
||||
|
||||
**npm package**:
|
||||
```json
|
||||
"source": {
|
||||
"source": "npm",
|
||||
"package": "@scope/my-plugin",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
## On-disk layout
|
||||
|
||||
```
|
||||
~/.omp/
|
||||
config/
|
||||
marketplaces.json # Registry of added marketplaces
|
||||
plugins/
|
||||
installed_plugins.json # User-scoped installed plugins
|
||||
cache/
|
||||
marketplaces/ # Cached marketplace catalogs
|
||||
plugins/ # Cached plugin directories
|
||||
|
||||
<project>/.omp/
|
||||
installed_plugins.json # Project-scoped installed plugins
|
||||
```
|
||||
|
||||
## Naming rules
|
||||
|
||||
Marketplace and plugin names must:
|
||||
|
||||
- Start and end with a lowercase letter or digit
|
||||
- Contain only lowercase letters, digits, hyphens, and dots
|
||||
- Be at most 64 characters
|
||||
|
||||
Plugin IDs (`name@marketplace`) must be at most 128 characters total.
|
||||
|
||||
Valid examples: `my-plugin`, `code-review`, `wordpress.com`, `ai-firstify`
|
||||
Invalid examples: `-bad`, `bad-`, `.bad`, `Bad`, `under_score`
|
||||
@@ -1,9 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added `/marketplace help` command to display usage guide for all marketplace operations
|
||||
- Added dedicated `gh-renderer.ts` module for rich terminal rendering of GitHub Actions workflow runs with live status snapshots and job details
|
||||
- Added `gh_pr_checkout` tool to check out GitHub pull requests into dedicated git worktrees with contributor push metadata
|
||||
- Added `gh_pr_push` tool to push checked-out pull request branches back to their source branches
|
||||
@@ -34,12 +34,16 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved marketplace catalog parsing to skip invalid plugin entries with warnings instead of failing the entire catalog load
|
||||
- Enhanced `/marketplace discover` command to suggest adding the official marketplace when no plugins are available
|
||||
- Improved `/marketplace` command messaging with clearer guidance for first-time setup and available commands
|
||||
- Enhanced `gh_run_watch` tool call rendering to display animated spinner status and target description (run ID, branch, or current HEAD) with improved visual hierarchy
|
||||
- Enhanced `gh_pr_view` tool to include inline review comments alongside pull request reviews for improved discussion context
|
||||
- Improved `gh_run_watch` tool output rendering with dedicated visual component for streaming run snapshots and job status updates
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed marketplace error messages to display error details instead of object stringification
|
||||
- Fixed artifact storage for non-persistent sessions to use in-memory fallback instead of returning undefined, enabling proper spill truncation for all session types
|
||||
- Fixed prompt file formatting to include trailing newlines at EOF for consistency across all prompt markdown files
|
||||
- Fixed `gh_pr_diff` to preserve raw patch content instead of normalizing tabs and whitespace
|
||||
|
||||
@@ -125,48 +125,66 @@ export function parseMarketplaceCatalog(content: string, filePath: string): Mark
|
||||
assertField(Array.isArray(obj.plugins), "plugins", filePath);
|
||||
|
||||
const plugins = obj.plugins as unknown[];
|
||||
const validPlugins: unknown[] = [];
|
||||
for (let i = 0; i < plugins.length; i++) {
|
||||
const entry = plugins[i];
|
||||
assertField(typeof entry === "object" && entry !== null && !Array.isArray(entry), `plugins[${i}]`, filePath);
|
||||
const p = entry as Record<string, unknown>;
|
||||
assertField(typeof p.name === "string" && isValidNameSegment(p.name), `plugins[${i}].name`, filePath);
|
||||
// source can be a string path or a typed object (github/url/git-subdir/npm)
|
||||
// all typed objects carry a "source" discriminant string field
|
||||
assertField(
|
||||
typeof p.source === "string" ||
|
||||
(typeof p.source === "object" &&
|
||||
p.source !== null &&
|
||||
!Array.isArray(p.source) &&
|
||||
typeof (p.source as Record<string, unknown>).source === "string"),
|
||||
`plugins[${i}].source`,
|
||||
filePath,
|
||||
);
|
||||
// String sources must be relative paths starting with "./"
|
||||
if (typeof p.source === "string") {
|
||||
assertField((p.source as string).startsWith("./"), `plugins[${i}].source (must start with "./")`, filePath);
|
||||
}
|
||||
// Validate required fields for typed source variants
|
||||
if (typeof p.source === "object" && p.source !== null) {
|
||||
const src = p.source as Record<string, unknown>;
|
||||
const variant = src.source as string;
|
||||
if (variant === "github") {
|
||||
assertField(typeof src.repo === "string" && src.repo.length > 0, `plugins[${i}].source.repo`, filePath);
|
||||
} else if (variant === "url" || variant === "git-subdir") {
|
||||
assertField(typeof src.url === "string" && src.url.length > 0, `plugins[${i}].source.url`, filePath);
|
||||
if (variant === "git-subdir") {
|
||||
assertField(typeof src.path === "string" && src.path.length > 0, `plugins[${i}].source.path`, filePath);
|
||||
}
|
||||
} else if (variant === "npm") {
|
||||
assertField(
|
||||
typeof src.package === "string" && src.package.length > 0,
|
||||
`plugins[${i}].source.package`,
|
||||
filePath,
|
||||
);
|
||||
} else {
|
||||
assertField(false, `plugins[${i}].source.source (unknown variant: "${variant}")`, filePath);
|
||||
try {
|
||||
const entry = plugins[i];
|
||||
assertField(typeof entry === "object" && entry !== null && !Array.isArray(entry), `plugins[${i}]`, filePath);
|
||||
const p = entry as Record<string, unknown>;
|
||||
assertField(typeof p.name === "string" && isValidNameSegment(p.name), `plugins[${i}].name`, filePath);
|
||||
// source can be a string path or a typed object (github/url/git-subdir/npm)
|
||||
// all typed objects carry a "source" discriminant string field
|
||||
assertField(
|
||||
typeof p.source === "string" ||
|
||||
(typeof p.source === "object" &&
|
||||
p.source !== null &&
|
||||
!Array.isArray(p.source) &&
|
||||
typeof (p.source as Record<string, unknown>).source === "string"),
|
||||
`plugins[${i}].source`,
|
||||
filePath,
|
||||
);
|
||||
// String sources must be relative paths starting with "./"
|
||||
if (typeof p.source === "string") {
|
||||
assertField((p.source as string).startsWith("./"), `plugins[${i}].source (must start with "./")`, filePath);
|
||||
}
|
||||
// Validate required fields for typed source variants
|
||||
if (typeof p.source === "object" && p.source !== null) {
|
||||
const src = p.source as Record<string, unknown>;
|
||||
const variant = src.source as string;
|
||||
if (variant === "github") {
|
||||
assertField(typeof src.repo === "string" && src.repo.length > 0, `plugins[${i}].source.repo`, filePath);
|
||||
} else if (variant === "url" || variant === "git-subdir") {
|
||||
assertField(typeof src.url === "string" && src.url.length > 0, `plugins[${i}].source.url`, filePath);
|
||||
if (variant === "git-subdir") {
|
||||
assertField(
|
||||
typeof src.path === "string" && src.path.length > 0,
|
||||
`plugins[${i}].source.path`,
|
||||
filePath,
|
||||
);
|
||||
}
|
||||
} else if (variant === "npm") {
|
||||
assertField(
|
||||
typeof src.package === "string" && src.package.length > 0,
|
||||
`plugins[${i}].source.package`,
|
||||
filePath,
|
||||
);
|
||||
} else {
|
||||
assertField(false, `plugins[${i}].source.source (unknown variant: "${variant}")`, filePath);
|
||||
}
|
||||
}
|
||||
validPlugins.push(entry);
|
||||
} catch (err) {
|
||||
// Warn and skip invalid plugin entries instead of failing the entire catalog.
|
||||
// This lets the rest of the marketplace load even if one entry has a bad name/source.
|
||||
const name =
|
||||
typeof plugins[i] === "object" && plugins[i] !== null
|
||||
? ((plugins[i] as Record<string, unknown>).name ?? `[${i}]`)
|
||||
: `[${i}]`;
|
||||
logger.warn(`Skipping invalid plugin ${name}: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
// Replace the plugins array with only valid entries
|
||||
obj.plugins = validPlugins;
|
||||
|
||||
// Extra fields are preserved — cast through unknown for type safety
|
||||
return obj as unknown as MarketplaceCatalog;
|
||||
|
||||
@@ -579,6 +579,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
||||
{ name: "uninstall", description: "Uninstall a plugin (selector if no args)", usage: "[name@marketplace]" },
|
||||
{ name: "installed", description: "List installed marketplace plugins" },
|
||||
{ name: "upgrade", description: "Upgrade outdated plugins", usage: "[name@marketplace]" },
|
||||
{ name: "help", description: "Show usage guide" },
|
||||
],
|
||||
allowArgs: true,
|
||||
handle: async (command, runtime) => {
|
||||
@@ -648,7 +649,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
||||
case "discover": {
|
||||
const plugins = await mgr.listAvailablePlugins(rest || undefined);
|
||||
if (plugins.length === 0) {
|
||||
runtime.ctx.showStatus("No plugins available");
|
||||
const marketplaces = await mgr.listMarketplaces();
|
||||
if (marketplaces.length === 0) {
|
||||
runtime.ctx.showStatus(
|
||||
"No marketplaces configured. Try:\n /marketplace add anthropics/claude-plugins-official",
|
||||
);
|
||||
} else {
|
||||
runtime.ctx.showStatus("No plugins available in configured marketplaces");
|
||||
}
|
||||
} else {
|
||||
const lines = plugins.map(
|
||||
p =>
|
||||
@@ -725,20 +733,46 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "help": {
|
||||
runtime.ctx.showStatus(
|
||||
[
|
||||
"Marketplace commands:",
|
||||
" /marketplace Browse and install plugins",
|
||||
" /marketplace add <source> Add a marketplace (e.g. owner/repo)",
|
||||
" /marketplace remove <name> Remove a marketplace",
|
||||
" /marketplace update [name] Re-fetch catalog(s)",
|
||||
" /marketplace list List configured marketplaces",
|
||||
" /marketplace discover [marketplace] Browse available plugins",
|
||||
" /marketplace install <name@marketplace> Install a plugin",
|
||||
" /marketplace uninstall <name@marketplace> Uninstall a plugin",
|
||||
" /marketplace installed List installed plugins",
|
||||
" /marketplace upgrade [name@marketplace] Upgrade plugin(s)",
|
||||
"",
|
||||
"Quick start:",
|
||||
" /marketplace add anthropics/claude-plugins-official",
|
||||
" /marketplace (opens interactive browser)",
|
||||
].join("\n"),
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Default to list marketplaces
|
||||
const marketplaces = await mgr.listMarketplaces();
|
||||
if (marketplaces.length === 0) {
|
||||
runtime.ctx.showStatus("No marketplaces configured. Use /marketplace add <source>");
|
||||
runtime.ctx.showStatus(
|
||||
"No marketplaces configured.\n\nGet started:\n /marketplace add anthropics/claude-plugins-official\n\nThen browse plugins with /marketplace or /marketplace discover",
|
||||
);
|
||||
} else {
|
||||
const lines = marketplaces.map(m => ` ${m.name} ${m.sourceUri}`);
|
||||
runtime.ctx.showStatus(`Marketplaces:\n${lines.join("\n")}`);
|
||||
runtime.ctx.showStatus(
|
||||
`Marketplaces:\n${lines.join("\n")}\n\nUse /marketplace discover to browse plugins, or /marketplace help for all commands`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
runtime.ctx.showStatus(`Marketplace error: ${err}`);
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
runtime.ctx.showStatus(`Marketplace error: ${msg}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user