diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index def71981..8f6a48c7 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -4,6 +4,90 @@ "displayName": "pleaseai" }, "plugins": [ + { + "name": "nanobanana", + "source": { + "source": "url", + "url": "https://github.com/pleaseai/nanobanana-plugin.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Ai" + }, + { + "name": "flutter", + "source": { + "source": "url", + "url": "https://github.com/pleaseai/flutter-plugin.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Mobile" + }, + { + "name": "spec-kit", + "source": { + "source": "url", + "url": "https://github.com/pleaseai/spec-kit-plugin.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" + }, + { + "name": "code-review", + "source": { + "source": "url", + "url": "https://github.com/pleaseai/code-review-plugin.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" + }, + { + "name": "gemini-cli-security", + "source": { + "source": "url", + "url": "https://github.com/pleaseai/security-plugin.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Security" + }, + { + "name": "firebase", + "source": { + "source": "url", + "url": "https://github.com/pleaseai/firebase-plugin.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Cloud" + }, + { + "name": "grafana", + "source": { + "source": "url", + "url": "https://github.com/grafana/mcp-grafana.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Monitoring" + }, { "name": "mcp-neo4j", "source": { @@ -28,6 +112,18 @@ }, "category": "Development" }, + { + "name": "chrome-devtools-mcp", + "source": { + "source": "url", + "url": "https://github.com/ChromeDevTools/chrome-devtools-mcp.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Browser" + }, { "name": "nuxt-ui", "source": { @@ -532,6 +628,92 @@ }, "category": "Development" }, + { + "name": "stripe", + "source": { + "source": "git-subdir", + "url": "https://github.com/stripe/ai.git", + "path": "./providers/claude/plugin", + "ref": "main" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" + }, + { + "name": "vercel", + "source": { + "source": "url", + "url": "https://github.com/vercel/vercel-plugin.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Deployment" + }, + { + "name": "sentry", + "source": { + "source": "url", + "url": "https://github.com/getsentry/sentry-for-claude.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Monitoring" + }, + { + "name": "posthog", + "source": { + "source": "url", + "url": "https://github.com/PostHog/ai-plugin.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Monitoring" + }, + { + "name": "revenuecat", + "source": { + "source": "url", + "url": "https://github.com/RevenueCat/rc-claude-code-plugin.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" + }, + { + "name": "notion", + "source": { + "source": "url", + "url": "https://github.com/makenotion/claude-code-notion-plugin.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + }, + { + "name": "asana", + "source": { + "source": "url", + "url": "https://github.com/pleaseai/asana.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + }, { "name": "please-plugins", "source": { @@ -628,6 +810,32 @@ }, "category": "Development" }, + { + "name": "ask", + "source": { + "source": "url", + "url": "https://github.com/pleaseai/ask.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" + }, + { + "name": "gemini", + "source": { + "source": "git-subdir", + "url": "https://github.com/pleaseai/gemini-plugin-cc.git", + "path": "./plugins/gemini", + "ref": "main" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Ai" + }, { "name": "edgeparse", "source": { @@ -712,6 +920,18 @@ }, "category": "Development" }, + { + "name": "modern-web-guidance", + "source": { + "source": "url", + "url": "https://github.com/GoogleChrome/modern-web-guidance.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" + }, { "name": "java-development", "source": { @@ -820,6 +1040,18 @@ }, "category": "Tooling" }, + { + "name": "cloudflare", + "source": { + "source": "url", + "url": "https://github.com/cloudflare/skills.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Cloud" + }, { "name": "axi", "source": { @@ -831,6 +1063,20 @@ "authentication": "ON_INSTALL" }, "category": "Productivity" + }, + { + "name": "csp", + "source": { + "source": "git-subdir", + "url": "https://github.com/pleaseai/code-search.git", + "path": "./plugins/csp", + "ref": "main" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Development" } ] } diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a237ee6b..0254c8a1 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -786,6 +786,20 @@ "repo": "pleaseai/asana" }, "homepage": "https://github.com/pleaseai/asana" + }, + { + "name": "csp", + "description": "Fast, local, token-efficient hybrid (dense + BM25) code search for agents. Bundles the csp MCP server (search, find_related) and a csp-search sub-agent.", + "category": "development", + "keywords": ["code-search", "hybrid-search", "semantic-search", "bm25", "mcp", "agent"], + "tags": ["mcp", "search"], + "source": { + "source": "git-subdir", + "url": "pleaseai/code-search", + "path": "plugins/csp", + "ref": "main" + }, + "homepage": "https://github.com/pleaseai/code-search" } ] } diff --git a/scripts/multi-format.test.ts b/scripts/multi-format.test.ts index d0ae7cb5..d9413309 100644 --- a/scripts/multi-format.test.ts +++ b/scripts/multi-format.test.ts @@ -10,6 +10,7 @@ import { toCodexManifest, toCodexMarketplace, toCodexMarketplaceEntry, + toCodexSource, type ClaudeMarketplace, type ClaudePluginManifest, type MarketplaceEntry, @@ -237,7 +238,7 @@ describe("readClaudeManifest", () => { }) describe("toCodexMarketplace", () => { - test("filters out non-local plugin entries (github/object sources)", () => { + test("includes both local and external plugin entries (in order)", () => { const input: ClaudeMarketplace = { name: "test-market", plugins: [ @@ -247,7 +248,28 @@ describe("toCodexMarketplace", () => { ], } const result = toCodexMarketplace(input) - expect(result.plugins.map(p => p.name)).toEqual(["local-one", "local-two"]) + expect(result.plugins.map(p => p.name)).toEqual(["local-one", "from-github", "local-two"]) + }) + + test("maps an external github source to a Codex url source", () => { + const input: ClaudeMarketplace = { + name: "m", + plugins: [{ name: "from-github", source: { source: "github", repo: "org/repo" } as unknown as object }], + } + const result = toCodexMarketplace(input) + expect(result.plugins[0]!.source).toEqual({ source: "url", url: "https://github.com/org/repo.git" }) + }) + + test("skips a plugin whose source Codex cannot express", () => { + const input: ClaudeMarketplace = { + name: "m", + plugins: [ + { name: "ok", source: "./plugins/ok" as unknown as object }, + { name: "weird", source: { source: "mystery" } as unknown as object }, + ], + } + const result = toCodexMarketplace(input) + expect(result.plugins.map(p => p.name)).toEqual(["ok"]) }) test("extracts directory name from local source path", () => { @@ -256,7 +278,8 @@ describe("toCodexMarketplace", () => { plugins: [{ name: "alias", source: "./plugins/real-dir" as unknown as object }], } const result = toCodexMarketplace(input) - expect(result.plugins[0]!.source.path).toBe("./plugins/real-dir") + const source = result.plugins[0]!.source + expect(source).toEqual({ source: "local", path: "./plugins/real-dir" }) }) test("inherits marketplace name and seeds interface.displayName", () => { @@ -281,7 +304,54 @@ describe("toCodexMarketplace", () => { } const result = toCodexMarketplace(input) expect(result.plugins).toHaveLength(1) - expect(result.plugins[0]!.source.path).toBe("./plugins/dir-a") + expect(result.plugins[0]!.source).toEqual({ source: "local", path: "./plugins/dir-a" }) + }) +}) + +describe("toCodexSource", () => { + test("maps a local ./plugins/ string to a local source", () => { + expect(toCodexSource("./plugins/foo")).toEqual({ source: "local", path: "./plugins/foo" }) + }) + + test("returns null for a non-local string source", () => { + expect(toCodexSource("./something-else")).toBeNull() + }) + + test("maps a github shorthand to a url source with a full git URL", () => { + expect(toCodexSource({ source: "github", repo: "owner/repo" })).toEqual({ + source: "url", + url: "https://github.com/owner/repo.git", + }) + }) + + test("maps a url source and preserves ref/sha", () => { + expect(toCodexSource({ source: "url", url: "https://github.com/org/repo.git", ref: "main" })).toEqual({ + source: "url", + url: "https://github.com/org/repo.git", + ref: "main", + }) + }) + + test("maps a git-subdir source, expanding the shorthand URL and ./-prefixing the path", () => { + expect(toCodexSource({ source: "git-subdir", url: "pleaseai/code-search", path: "plugins/csp", ref: "main" })).toEqual({ + source: "git-subdir", + url: "https://github.com/pleaseai/code-search.git", + path: "./plugins/csp", + ref: "main", + }) + }) + + test("passes through a full git URL untouched", () => { + expect(toCodexSource({ source: "git-subdir", url: "https://example.com/x.git", path: "./a" })).toEqual({ + source: "git-subdir", + url: "https://example.com/x.git", + path: "./a", + }) + }) + + test("returns null for an unknown source kind", () => { + expect(toCodexSource({ source: "mystery" })).toBeNull() + expect(toCodexSource(undefined)).toBeNull() }) }) diff --git a/scripts/multi-format.ts b/scripts/multi-format.ts index aeb2d09d..8a3ab916 100644 --- a/scripts/multi-format.ts +++ b/scripts/multi-format.ts @@ -295,13 +295,26 @@ export function extractMcpServersFile(claude: ClaudePluginManifest): { mcpServer return { mcpServers: servers } } +/** + * Codex marketplace source kinds. Codex supports the same external sources as + * Claude Code, but with its own JSON shape: + * - local: plugin vendored in this repo (`./plugins/`) + * - url: plugin IS an external git repo root + * - git-subdir: plugin lives in a subdirectory of an external git repo + * See https://developers.openai.com/codex/plugins/build for the schema. + */ +export type CodexSource = + | { source: "local"; path: string } + | { source: "url"; url: string; ref?: string; sha?: string } + | { source: "git-subdir"; url: string; path: string; ref?: string; sha?: string } + /** * Build a Codex marketplace entry for an existing local plugin. * Mirrors the schema documented in plugin-creator (policy + category required). */ export interface CodexMarketplaceEntry { name: string - source: { source: "local"; path: string } + source: CodexSource policy: { installation: "AVAILABLE" | "INSTALLED_BY_DEFAULT" | "NOT_AVAILABLE"; authentication: "ON_INSTALL" | "ON_USE" } category: string } @@ -315,6 +328,63 @@ export function toCodexMarketplaceEntry(entry: MarketplaceEntry, pluginDirName: } } +/** + * Normalise a repo reference to a full git URL. Claude marketplace entries use + * `owner/repo` shorthand (github/git-subdir sources); Codex marketplace JSON + * entries require a real git URL. Pass-through anything that already looks like + * a URL (https://, git@, ssh://). + */ +function normalizeGitUrl(urlOrRepo: string): string { + // Pass through anything that already carries an explicit scheme (https://, + // git://, ssh://, git+https://, git+ssh://, …) or scp-like `git@host:repo`. + if (/^([a-zA-Z+]+:\/\/|git@)/.test(urlOrRepo)) return urlOrRepo + return `https://github.com/${urlOrRepo}.git` +} + +/** Codex `git-subdir` paths are `./`-prefixed relative to the repo root. */ +function normalizeSubdirPath(p: string): string { + return p.startsWith("./") ? p : `./${p.replace(/^\//, "")}` +} + +/** + * Convert a Claude marketplace entry's `source` into a Codex marketplace + * `source`. Returns null for source shapes Codex cannot express (so the caller + * skips them rather than emitting an unresolvable entry). + */ +export function toCodexSource(source: unknown): CodexSource | null { + // Local plugins are referenced by a `./plugins/...` string in the Claude marketplace. + if (typeof source === "string") { + return source.startsWith("./plugins/") ? { source: "local", path: source } : null + } + if (!source || typeof source !== "object") return null + const s = source as Record + + // Whole-repo plugin: Claude `github` (owner/repo) or `url` (full git URL) → Codex `url`. + if (s.source === "github" && typeof s.repo === "string") { + return { source: "url", url: normalizeGitUrl(s.repo) } + } + if (s.source === "url" && typeof s.url === "string") { + const out: CodexSource = { source: "url", url: normalizeGitUrl(s.url) } + if (typeof s.ref === "string") out.ref = s.ref + if (typeof s.sha === "string") out.sha = s.sha + return out + } + + // Subdirectory plugin: Claude `git-subdir` → Codex `git-subdir`. + if (s.source === "git-subdir" && typeof s.url === "string" && typeof s.path === "string") { + const out: CodexSource = { + source: "git-subdir", + url: normalizeGitUrl(s.url), + path: normalizeSubdirPath(s.path), + } + if (typeof s.ref === "string") out.ref = s.ref + if (typeof s.sha === "string") out.sha = s.sha + return out + } + + return null +} + // --------------------------------------------------------------------------- // File I/O layer — keeps pure converters above; this section reads/writes disk. // --------------------------------------------------------------------------- @@ -450,8 +520,11 @@ export function generateForPlugin( } /** - * Build a Codex marketplace document from a Claude Code marketplace.json, - * filtering down to local plugins (those with `source: "./plugins/..."`). + * Build a Codex marketplace document from a Claude Code marketplace.json. + * Local plugins (`source: "./plugins/..."`) map to Codex `local` sources; + * external plugins (`github`/`git-subdir`/`url` objects) map to Codex + * `url`/`git-subdir` sources via {@link toCodexSource}. Entries whose source + * Codex cannot express are skipped. */ export interface ClaudeMarketplace { name?: string @@ -466,11 +539,16 @@ export function toCodexMarketplace(claudeMarketplace: ClaudeMarketplace): { const seen = new Set() const entries: CodexMarketplaceEntry[] = [] for (const p of claudeMarketplace.plugins) { - if (typeof p.source !== "string" || !p.source.startsWith("./plugins/")) continue if (seen.has(p.name)) continue + const source = toCodexSource(p.source) + if (!source) continue seen.add(p.name) - const dirName = p.source.replace(/^\.\/plugins\//, "") - entries.push(toCodexMarketplaceEntry(p, dirName)) + entries.push({ + name: p.name, + source, + policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" }, + category: pickCategory(p), + }) } return { name: claudeMarketplace.name ?? "personal",