diff --git a/docs/marketplace.md b/docs/marketplace.md index 70dc2df17..a803620f3 100644 --- a/docs/marketplace.md +++ b/docs/marketplace.md @@ -13,7 +13,7 @@ Or just type `/marketplace` with no arguments to open the interactive plugin bro ## 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 **marketplace** is a Git repository (or local directory) containing a catalog file at `.omp-plugin/marketplace.json` (preferred) or `.claude-plugin/marketplace.json` (Claude Code-compatible fallback). 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`). @@ -77,11 +77,11 @@ When you run `/marketplace add `, the system classifies the source: | `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. +The system clones the repository (or reads the local directory), locates the catalog (`.omp-plugin/marketplace.json` if present, otherwise `.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: +A marketplace catalog lives at `.omp-plugin/marketplace.json` in the repository root. When omp is the only intended consumer, prefer this path. To remain Claude Code-compatible (omp loads the same shape from either path), publish at `.claude-plugin/marketplace.json` instead — omp uses it as a fallback when `.omp-plugin/marketplace.json` is absent. A repository may ship both: omp reads the `.omp-plugin/` copy, Claude Code reads the `.claude-plugin/` copy. Same catalog format either way: ```json { diff --git a/docs/skills/authoring-marketplaces.md b/docs/skills/authoring-marketplaces.md index 16d452169..5cbb845d4 100644 --- a/docs/skills/authoring-marketplaces.md +++ b/docs/skills/authoring-marketplaces.md @@ -5,7 +5,7 @@ description: Use when creating a new omp marketplace. Covers marketplace.json sc # Authoring Marketplaces -A marketplace is a Git repository (or local directory) that contains a catalog file at `.claude-plugin/marketplace.json`. Anyone can author one. Users add it with `/marketplace add owner/repo` and then install individual plugins from it. +A marketplace is a Git repository (or local directory) that contains a catalog file at either `.omp-plugin/marketplace.json` (preferred for omp-specific catalogs) or `.claude-plugin/marketplace.json` (Claude Code-compatible; used as the fallback). Anyone can author one. Users add it with `/marketplace add owner/repo` and then install individual plugins from it. ## Minimum viable marketplace @@ -42,7 +42,7 @@ Push to GitHub. Users install with: ## marketplace.json schema -The catalog file must live at `.claude-plugin/marketplace.json` in the repository root. +The catalog file lives at either `.omp-plugin/marketplace.json` or `.claude-plugin/marketplace.json` in the repository root. omp prefers the `.omp-plugin/` path and falls back to the Claude path; a repository may publish both to expose tool-specific catalogs from a single source tree. ### Top-level fields @@ -241,7 +241,7 @@ Invalid: `-bad-start`, `bad-end-`, `.dot-start`, `Under_score`, `HAS_CAPS` ## Publishing workflow -1. Create `marketplace.json` at `.claude-plugin/marketplace.json` in a new Git repo. +1. Create `marketplace.json` at `.omp-plugin/marketplace.json` (omp-only) or `.claude-plugin/marketplace.json` (shared with Claude Code) in a new Git repo. 2. Add plugin entries pointing to subdirectories (or external sources). 3. Push to GitHub. 4. Share the `owner/repo` string. Users add it with `/marketplace add owner/repo`. diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a8912a45a..1a1d32713 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -439,7 +439,11 @@ - Fixed `RULES.md` not being injected. The documented sticky-rules file at `~/.omp/agent/RULES.md` and `/.omp/RULES.md` was never read by any discovery provider; only `.omp/rules/*.md` was scanned. The native provider now loads both as always-apply rules so they re-attach every turn as documented ([#1266](https://github.com/can1357/oh-my-pi/issues/1266)). ## [15.2.1] - 2026-05-21 +### Added +- Added `.omp-plugin/marketplace.json` as a preferred marketplace catalog path. `fetchMarketplace` now searches `.omp-plugin/marketplace.json` before `.claude-plugin/marketplace.json` for every local and cloned source. Lets a single marketplace repository publish a tool-specific catalog (e.g. an omp-only superset of a shared Claude Code marketplace) without forcing the omp/Claude distinction into per-plugin tagging. Mirrors the `package.json#omp.extensions` precedence pattern; the `.claude-plugin/marketplace.json` fallback keeps every existing marketplace loading unchanged. + +## [15.2.1] - 2026-05-21 ### Fixed - Fixed compaction routing to the wrong provider when `modelRoles.default` is set to a different model than the active chat. Auto- and manual compaction now prefer the active session's model and only fall back to role-based candidates when the current model has no usable credentials. Previously, an Anthropic chat with `modelRoles.default = openai/gpt-5` would compact through OpenAI (including the remote-compaction endpoint), even though the live conversation never used OpenAI. diff --git a/packages/coding-agent/src/extensibility/plugins/marketplace/fetcher.ts b/packages/coding-agent/src/extensibility/plugins/marketplace/fetcher.ts index 5046acd08..28d342fce 100644 --- a/packages/coding-agent/src/extensibility/plugins/marketplace/fetcher.ts +++ b/packages/coding-agent/src/extensibility/plugins/marketplace/fetcher.ts @@ -192,8 +192,33 @@ export function parseMarketplaceCatalog(content: string, filePath: string): Mark // ── fetchMarketplace ────────────────────────────────────────────────── -/** Relative path from a marketplace root to its catalog file. */ -const CATALOG_RELATIVE_PATH = path.join(".claude-plugin", "marketplace.json"); +/** + * Catalog paths tried in priority order: omp-namespaced override first, then + * the Claude Code-compatible fallback so existing marketplaces keep loading. + */ +const CATALOG_RELATIVE_PATHS: readonly string[] = [ + path.join(".omp-plugin", "marketplace.json"), + path.join(".claude-plugin", "marketplace.json"), +]; + +async function readMarketplaceCatalog(root: string): Promise<{ catalogPath: string; content: string }> { + const tried: string[] = []; + for (const rel of CATALOG_RELATIVE_PATHS) { + const catalogPath = path.join(root, rel); + tried.push(catalogPath); + try { + const content = await Bun.file(catalogPath).text(); + return { catalogPath, content }; + } catch (err) { + if (isEnoent(err)) continue; + throw err; + } + } + throw new Error( + `Marketplace catalog not found at ${tried.map(p => `"${p}"`).join(" or ")}. ` + + `Ensure the directory exists and contains one of: ${CATALOG_RELATIVE_PATHS.join(", ")}.`, + ); +} /** * Expand a `~/...` path to an absolute path using os.homedir(). @@ -220,21 +245,7 @@ export async function fetchMarketplace(source: string, cacheDir: string): Promis if (type === "local") { const resolved = path.resolve(expandHome(source)); - const catalogPath = path.join(resolved, CATALOG_RELATIVE_PATH); - - let content: string; - try { - content = await Bun.file(catalogPath).text(); - } catch (err) { - if (isEnoent(err)) { - throw new Error( - `Marketplace catalog not found at "${catalogPath}". ` + - `Ensure the directory exists and contains a .claude-plugin/marketplace.json file.`, - ); - } - throw err; - } - + const { catalogPath, content } = await readMarketplaceCatalog(resolved); const catalog = parseMarketplaceCatalog(content, catalogPath); return { catalog }; } @@ -280,27 +291,14 @@ async function cloneAndReadCatalog(url: string, cacheDir: string): Promise {}); - throw err; + throw new Error(`Cloned repository ${url}: ${(err as Error).message}`, { cause: err }); } - - return { catalog, clonePath: tmpDir }; } /** diff --git a/packages/coding-agent/test/marketplace/fetcher.test.ts b/packages/coding-agent/test/marketplace/fetcher.test.ts index 2977195df..b4a1dc6fd 100644 --- a/packages/coding-agent/test/marketplace/fetcher.test.ts +++ b/packages/coding-agent/test/marketplace/fetcher.test.ts @@ -180,6 +180,57 @@ describe("fetchMarketplace", () => { await expect(fetchMarketplace(fakeSrc, tmpDir)).rejects.toThrow(/Marketplace catalog not found/); }); + it("loads catalog from .omp-plugin/marketplace.json when present", async () => { + const root = path.join(tmpDir, "omp-only"); + fs.mkdirSync(path.join(root, ".omp-plugin"), { recursive: true }); + const catalog = { + name: "omp-only-marketplace", + owner: { name: "Test" }, + plugins: [{ name: "omp-plugin", source: "./plugins/omp-plugin", description: "x" }], + }; + fs.writeFileSync(path.join(root, ".omp-plugin", "marketplace.json"), JSON.stringify(catalog)); + + const result = await fetchMarketplace(root, tmpDir); + expect(result.catalog.name).toBe("omp-only-marketplace"); + expect(result.catalog.plugins[0].name).toBe("omp-plugin"); + }); + + it("prefers .omp-plugin/marketplace.json over .claude-plugin/marketplace.json when both exist", async () => { + const root = path.join(tmpDir, "both-catalogs"); + fs.mkdirSync(path.join(root, ".omp-plugin"), { recursive: true }); + fs.mkdirSync(path.join(root, ".claude-plugin"), { recursive: true }); + const ompCatalog = { + name: "from-omp-plugin", + owner: { name: "Test" }, + plugins: [{ name: "p", source: "./p", description: "x" }], + }; + const claudeCatalog = { + name: "from-claude-plugin", + owner: { name: "Test" }, + plugins: [{ name: "p", source: "./p", description: "x" }], + }; + fs.writeFileSync(path.join(root, ".omp-plugin", "marketplace.json"), JSON.stringify(ompCatalog)); + fs.writeFileSync(path.join(root, ".claude-plugin", "marketplace.json"), JSON.stringify(claudeCatalog)); + + const result = await fetchMarketplace(root, tmpDir); + expect(result.catalog.name).toBe("from-omp-plugin"); + }); + + it("falls back to .claude-plugin/marketplace.json when .omp-plugin is absent", async () => { + // The shared fixture only ships .claude-plugin/marketplace.json — confirms + // the legacy path still loads unchanged. + const result = await fetchMarketplace(FIXTURE_DIR, tmpDir); + expect(result.catalog.name).toBe("test-marketplace"); + }); + + it("error message names both candidate paths when neither exists", async () => { + const empty = path.join(tmpDir, "empty-dir"); + fs.mkdirSync(empty, { recursive: true }); + await expect(fetchMarketplace(empty, tmpDir)).rejects.toThrow( + /\.omp-plugin[\\/]marketplace\.json.*\.claude-plugin[\\/]marketplace\.json/, + ); + }); + // Network-dependent tests — skip in CI / offline environments. // These verify real git clone and HTTP fetch error handling. it.skip("github source throws on nonexistent repo", async () => {