From 5749bf6958f1d3324dcc17cd71306327b76d240f Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Fri, 15 May 2026 15:14:29 -0400 Subject: [PATCH 1/5] feat: add Codex and Claude plugin bundles --- .agents/plugins/marketplace.json | 20 +++ .changeset/native-agent-plugin-bundles.md | 5 + .claude-plugin/marketplace.json | 14 ++ .claude-plugin/plugin.json | 15 +++ .codex-plugin/plugin.json | 29 +++++ README.md | 134 +++++++++++++------ claude.hooks.json | 14 ++ claude.mcp.json | 8 ++ codex.hooks.json | 16 +++ codex.mcp.json | 8 ++ packages/core/test/agent-plugins.test.ts | 152 ++++++++++++++++++++++ skills/caplets/SKILL.md | 39 ++++++ 12 files changed, 416 insertions(+), 38 deletions(-) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .changeset/native-agent-plugin-bundles.md create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 .codex-plugin/plugin.json create mode 100644 claude.hooks.json create mode 100644 claude.mcp.json create mode 100644 codex.hooks.json create mode 100644 codex.mcp.json create mode 100644 packages/core/test/agent-plugins.test.ts create mode 100644 skills/caplets/SKILL.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 00000000..3addd3ad --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "caplets", + "interface": { + "displayName": "Caplets" + }, + "plugins": [ + { + "name": "caplets", + "source": { + "source": "local", + "path": "./" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Developer Tools" + } + ] +} diff --git a/.changeset/native-agent-plugin-bundles.md b/.changeset/native-agent-plugin-bundles.md new file mode 100644 index 00000000..b3c49e3f --- /dev/null +++ b/.changeset/native-agent-plugin-bundles.md @@ -0,0 +1,5 @@ +--- +"caplets": patch +--- + +Add root-level Codex and Claude Code plugin artifacts that package Caplets MCP server configuration, shared agent skill guidance, and agent-specific plugin hooks. diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 00000000..c0fc241a --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,14 @@ +{ + "name": "caplets", + "owner": { + "name": "Spirit-Led Software LLC" + }, + "plugins": [ + { + "name": "caplets", + "source": "./", + "description": "Expose configured Caplets as progressive-disclosure tools in Claude Code.", + "category": "Developer Tools" + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 00000000..bb9d05fd --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "caplets", + "version": "0.12.1", + "description": "Expose configured Caplets as progressive-disclosure tools in Claude Code.", + "author": { + "name": "Spirit-Led Software LLC" + }, + "homepage": "https://github.com/spiritledsoftware/caplets#readme", + "repository": "https://github.com/spiritledsoftware/caplets", + "license": "MIT", + "keywords": ["caplets", "mcp", "claude-code", "tools"], + "skills": "./skills/", + "mcpServers": "./claude.mcp.json", + "hooks": "./claude.hooks.json" +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 00000000..e908ce71 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,29 @@ +{ + "name": "caplets", + "version": "0.12.1", + "description": "Expose configured Caplets as progressive-disclosure tools in Codex.", + "author": { + "name": "Spirit-Led Software LLC" + }, + "homepage": "https://github.com/spiritledsoftware/caplets#readme", + "repository": "https://github.com/spiritledsoftware/caplets", + "license": "MIT", + "keywords": ["caplets", "mcp", "codex", "tools"], + "skills": "./skills/", + "mcpServers": "./codex.mcp.json", + "hooks": "./codex.hooks.json", + "interface": { + "displayName": "Caplets", + "shortDescription": "Progressive disclosure for Caplets tools.", + "longDescription": "Use Caplets to discover and call configured MCP, OpenAPI, GraphQL, HTTP, and CLI tools without flattening every downstream tool into context.", + "developerName": "Spirit-Led Software LLC", + "category": "Developer Tools", + "capabilities": ["MCP", "Tools"], + "websiteURL": "https://github.com/spiritledsoftware/caplets", + "defaultPrompt": [ + "Use Caplets to discover configured capability domains before calling downstream tools.", + "Use Caplets progressive discovery before calling downstream tools." + ], + "brandColor": "#4F46E5" + } +} diff --git a/README.md b/README.md index b9a293f9..234e29ef 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,99 @@ # Caplets -Caplets is a progressive-disclosure gateway for Model Context Protocol (MCP) servers, -native OpenAPI endpoints, native GraphQL endpoints, and explicitly configured HTTP APIs. +Caplets turns sprawling tool setups into focused capability cards for coding agents. +Connect MCP servers, OpenAPI specs, GraphQL endpoints, HTTP actions, and curated CLI +commands without flooding the model with every downstream operation up front. -Instead of connecting an MCP client to many downstream servers or HTTP APIs and exposing -every operation up front, Caplets exposes one top-level tool per configured capability. -An agent first chooses a capability domain, then asks Caplets to list, search, inspect, -or call that backend's underlying tools or operations. +Instead of exposing a flat wall of tools, Caplets shows one top-level tool per capability. +The agent chooses a domain first, then uses scoped operations like `search_tools`, +`get_tool`, and `call_tool` only when it needs more detail. -This keeps the initial MCP tool list small, makes tool selection easier, and avoids -flattened tool-name collisions across servers. +## Quick Start + +Caplets requires Node.js 22 or newer. + +```sh +pnpm add -g caplets +caplets init +``` + +Add a capability from an existing system: + +```sh +# Wrap an MCP server +caplets add mcp docs --command npx --arg -y --arg @upstash/context7-mcp + +# Convert useful repository commands into curated tools +caplets add cli repo-tools --repo . --include git,gh,package + +# Install ready-made Caplets from a repository +caplets install spiritledsoftware/caplets github linear context7 +``` + +Connect Caplets to any MCP client: + +```json +{ + "mcpServers": { + "caplets": { + "command": "caplets", + "args": ["serve"] + } + } +} +``` + +Ask your agent to use Caplets. It will see a compact capability list first, then inspect +only the backend it needs. + +## Agent Plugins + +Use Caplets as a normal MCP server everywhere, or install a native agent integration when +your coding agent supports one. + +| Agent | Install | What It Provides | +| -------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| Any MCP client | Add `caplets serve` as a stdio MCP server | Universal progressive-disclosure gateway | +| Claude Code | `claude plugin marketplace add spiritledsoftware/caplets && claude plugin install caplets@caplets` | Claude Code plugin metadata, MCP config, hooks, and shared skill guidance | +| Codex | `codex plugin marketplace add spiritledsoftware/caplets`, then install `caplets` from Codex plugins | Codex plugin metadata, MCP config, hooks, and shared skill guidance | +| OpenCode | Install [`@caplets/opencode`](packages/opencode/README.md) | Native `caplets_` tools and prompt guidance hooks | +| Pi | Install [`@caplets/pi`](packages/pi/README.md) | Native `caplets_` tools with Pi prompt snippets/guidelines | + +Codex and Claude Code plugins are plugin-native but MCP-backed. Their manifests live in +`.codex-plugin/` and `.claude-plugin/`; component files live at the plugin root so +marketplace installs can copy and resolve them correctly. + +The Claude Code and Codex commands install from this GitHub repository through each agent's +plugin marketplace flow; users do not need to clone the repository manually. Plugin MCP +configs run `caplets serve` directly, so install the Caplets CLI globally first. + +## Convert Existing Tooling + +Caplets is designed to convert what you already use into agent-friendly capability domains. + +| Existing source | Command | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------- | +| Local MCP server | `caplets add mcp local-tools --command node --arg ./server.mjs` | +| Remote MCP server | `caplets add mcp remote-tools --url https://mcp.example.com/mcp --transport http --token-env MCP_TOKEN` | +| OpenAPI service | `caplets add openapi users --spec ./openapi.json --base-url https://api.example.com --token-env USERS_API_TOKEN` | +| GraphQL endpoint | `caplets add graphql catalog --endpoint-url https://api.example.com/graphql --schema ./schema.graphql` | +| Simple HTTP API | `caplets add http status-api --base-url https://api.example.com --action get_status:GET:/status/{service}` | +| Repository CLI workflows | `caplets add cli repo-tools --repo . --include git,gh,package` | +| Shared Caplet catalog | `caplets install spiritledsoftware/caplets github linear context7` | + +Generated Caplet files are written to `./.caplets` by default so teams can review, +version, and customize them with the project. ## Why It Matters -Large MCP setups make agents worse before they make them better. If every downstream -server exposes every tool up front, the model starts with a noisy flat list, duplicate -tool names, and a bigger context surface before it knows which capability matters. +Large MCP setups can make agents harder to steer. If every downstream server exposes +every tool up front, the model starts with a noisy flat list, duplicate tool names, and +a larger context surface before it knows which capability matters. Caplets turns that flat tool wall into progressive disclosure: one capability card first, then scoped discovery only after the agent chooses the relevant domain. -## Benchmark Results +## Benchmark In Caplets' reproducible coding-agent benchmark, the same three mock MCP servers are exposed two ways: direct flat MCP aggregation versus Caplets progressive disclosure. @@ -32,9 +105,9 @@ exposed two ways: direct flat MCP aggregation versus Caplets progressive disclos | Approx. context surface | 8,023 tokens | 2,100 tokens | 5,923 fewer | | Top-level name collisions | 3 duplicate names | 0 | eliminated | -The important part: Caplets does not remove access to the downstream tools. It hides -them behind scoped discovery operations like `search_tools`, `get_tool`, and `call_tool`, -so the agent sees less up front while still being able to reach the same capabilities. +Caplets does not remove access to downstream tools. It places them behind scoped +discovery operations, so the agent sees less up front while retaining access to the same +capabilities when needed. A local OpenCode live benchmark also completed the full benchmark matrix successfully: @@ -57,10 +130,10 @@ pnpm build CAPLETS_BENCH_LIVE=1 pnpm benchmark:live:opencode -- --model openai/gpt-5.5-fast ``` -## Inspiration +## Design Model -Caplets is a mashup of two ideas that work well separately but leave a gap together: -agent skills and MCP servers. +Caplets combines two ideas that work well separately but leave a gap together: agent +skills and MCP servers. Agent skills are great at progressive disclosure. They show an agent a compact capability card first, then let it read deeper instructions only when that skill is relevant. MCP @@ -72,7 +145,7 @@ Caplets borrows the skill-shaped discovery model and applies it to MCP. Each dow server becomes a skill-like capability card first; its actual MCP tools stay hidden until the agent chooses that server and asks to search, list, inspect, or call them. -## What It Does +## Capabilities - Reads downstream MCP server definitions, native OpenAPI endpoint definitions, native GraphQL endpoint definitions, explicit HTTP API action definitions, and curated CLI tool definitions from the user config file. - Registers one generated MCP tool for each enabled MCP server, OpenAPI endpoint, GraphQL endpoint, HTTP API, or CLI tools backend. @@ -89,21 +162,6 @@ the agent chooses that server and asks to search, list, inspect, or call them. - Redacts secrets from structured errors. - Supports static remote auth and OAuth token storage for remote servers. -## Install - -Caplets requires Node.js 22 or newer. - -```sh -pnpm add -g caplets -``` - -For local development from this repository: - -```sh -pnpm install -pnpm build -``` - ## Configure Create a starter user config at `${XDG_CONFIG_HOME:-~/.config}/caplets/config.json` on Unix-like platforms or `%APPDATA%\caplets\config.json` on Windows: @@ -706,11 +764,11 @@ top-level MCP tool list without restarting Caplets. When an MCP-backed Caplet ch removed, Caplets closes only that affected downstream connection; unrelated Caplets and their downstream connections keep running. -## Native Agent Integrations +## Additional Native Integrations -Caplets can also run as native agent extensions when the host supports native tools. Native -integrations expose one prefixed tool per configured Caplet, such as `caplets_github`, while -reusing the same Caplets config and backend runtime. +OpenCode and Pi support true native tool registration. Those integrations expose one +prefixed tool per configured Caplet, such as `caplets_github`, while reusing the same +Caplets config and backend runtime. - [`@caplets/opencode`](packages/opencode/README.md): OpenCode plugin that injects prompt guidance through plugin hooks instead of editing `opencode.json`. - [`@caplets/pi`](packages/pi/README.md): Pi extension installable with `pi install npm:@caplets/pi`, with guidance provided through Pi tool prompt snippets and guidelines. diff --git a/claude.hooks.json b/claude.hooks.json new file mode 100644 index 00000000..50ddcc6c --- /dev/null +++ b/claude.hooks.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "printf '%s\\n' 'Caplets guidance: use the bundled caplets skill for progressive discovery. Start with get_caplet or search_tools, inspect schemas with get_tool, and pass downstream inputs inside call_tool.arguments.'" + } + ] + } + ] + } +} diff --git a/claude.mcp.json b/claude.mcp.json new file mode 100644 index 00000000..399fe48a --- /dev/null +++ b/claude.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "caplets": { + "command": "caplets", + "args": ["serve"] + } + } +} diff --git a/codex.hooks.json b/codex.hooks.json new file mode 100644 index 00000000..5e8ac006 --- /dev/null +++ b/codex.hooks.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "SessionStart": [ + { + "matcher": "startup|resume|clear", + "hooks": [ + { + "type": "command", + "command": "printf '%s\\n' 'Caplets guidance: use the bundled caplets skill for progressive discovery. Start with get_caplet or search_tools, inspect schemas with get_tool, and pass downstream inputs inside call_tool.arguments.'", + "statusMessage": "Loading Caplets guidance" + } + ] + } + ] + } +} diff --git a/codex.mcp.json b/codex.mcp.json new file mode 100644 index 00000000..e0473a17 --- /dev/null +++ b/codex.mcp.json @@ -0,0 +1,8 @@ +{ + "caplets": { + "command": "caplets", + "args": ["serve"], + "startup_timeout_sec": 20, + "tool_timeout_sec": 120 + } +} diff --git a/packages/core/test/agent-plugins.test.ts b/packages/core/test/agent-plugins.test.ts new file mode 100644 index 00000000..50c7f0f9 --- /dev/null +++ b/packages/core/test/agent-plugins.test.ts @@ -0,0 +1,152 @@ +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); + +async function readJson(filePath: string): Promise { + return JSON.parse(await readFile(filePath, "utf8")) as T; +} + +describe("root agent plugin artifacts", () => { + it("declares Codex plugin components using Codex-specific files", async () => { + const manifest = await readJson>( + path.join(repoRoot, ".codex-plugin/plugin.json"), + ); + + expect(manifest.skills).toBe("./skills/"); + expect(manifest.mcpServers).toBe("./codex.mcp.json"); + expect(manifest.hooks).toBe("./codex.hooks.json"); + }); + + it("declares Claude Code plugin components using Claude-specific files", async () => { + const manifest = await readJson>( + path.join(repoRoot, ".claude-plugin/plugin.json"), + ); + + expect(manifest.skills).toBe("./skills/"); + expect(manifest.mcpServers).toBe("./claude.mcp.json"); + expect(manifest.hooks).toBe("./claude.hooks.json"); + }); + + it("keeps plugin manifest versions aligned with the CLI package", async () => { + const [cliPackage, codexManifest, claudeManifest] = await Promise.all([ + readJson<{ version: string }>(path.join(repoRoot, "packages/cli/package.json")), + readJson<{ version: string }>(path.join(repoRoot, ".codex-plugin/plugin.json")), + readJson<{ version: string }>(path.join(repoRoot, ".claude-plugin/plugin.json")), + ]); + + expect(codexManifest.version).toBe(cliPackage.version); + expect(claudeManifest.version).toBe(cliPackage.version); + }); + + it("declares a Claude Code marketplace entry using Claude's schema", async () => { + const marketplace = await readJson<{ + name: string; + owner: { name: string }; + plugins: Array<{ + name: string; + source: string; + }>; + }>(path.join(repoRoot, ".claude-plugin/marketplace.json")); + + expect(marketplace.name).toBe("caplets"); + expect(marketplace.owner.name).toBe("Spirit-Led Software LLC"); + expect(marketplace.plugins).toEqual([ + expect.objectContaining({ + name: "caplets", + source: "./", + }), + ]); + }); + + it("declares a Codex marketplace entry using Codex's schema", async () => { + const marketplace = await readJson<{ + name: string; + plugins: Array<{ + name: string; + source: { source: string; path: string }; + }>; + }>(path.join(repoRoot, ".agents/plugins/marketplace.json")); + + expect(marketplace.name).toBe("caplets"); + expect(marketplace.plugins).toEqual([ + expect.objectContaining({ + name: "caplets", + source: { source: "local", path: "./" }, + }), + ]); + }); + + it("runs the globally installed Caplets CLI in both MCP configs", async () => { + const [codexMcp, claudeMcp] = await Promise.all([ + readJson<{ caplets: { command: string; args: string[] } }>( + path.join(repoRoot, "codex.mcp.json"), + ), + readJson<{ mcpServers: { caplets: { command: string; args: string[] } } }>( + path.join(repoRoot, "claude.mcp.json"), + ), + ]); + + expect(codexMcp.caplets).toMatchObject({ + command: "caplets", + args: ["serve"], + }); + expect(claudeMcp.mcpServers.caplets).toEqual({ + command: "caplets", + args: ["serve"], + }); + expect(JSON.stringify(codexMcp)).not.toContain("caplets@"); + expect(JSON.stringify(claudeMcp)).not.toContain("caplets@"); + }); + + it("uses the shared root skill and keeps hooks agent-specific", async () => { + const [skill, codexHooks, claudeHooks] = await Promise.all([ + readFile(path.join(repoRoot, "skills/caplets/SKILL.md"), "utf8"), + readJson>(path.join(repoRoot, "codex.hooks.json")), + readJson>(path.join(repoRoot, "claude.hooks.json")), + ]); + + expect(skill).toContain("name: caplets"); + expect(skill).toContain("call_tool"); + expect(JSON.stringify(codexHooks)).toContain("statusMessage"); + expect(JSON.stringify(claudeHooks)).not.toContain("statusMessage"); + }); + + it("keeps plugin metadata and components in documented locations", () => { + for (const forbiddenPath of [ + "packages/codex", + "packages/claude-code", + "packages/agent-plugin-shared", + "plugins", + ".mcp.json", + "hooks", + ".codex-plugin/.mcp.json", + ".claude-plugin/.mcp.json", + ".codex-plugin/hooks.json", + ".claude-plugin/hooks.json", + ".codex-plugin/hooks", + ".claude-plugin/hooks", + ".codex-plugin/skills", + ".claude-plugin/skills", + ]) { + expect(existsSync(path.join(repoRoot, forbiddenPath)), forbiddenPath).toBe(false); + } + + for (const requiredPath of [ + ".codex-plugin/plugin.json", + ".claude-plugin/plugin.json", + ".claude-plugin/marketplace.json", + ".agents/plugins/marketplace.json", + "codex.mcp.json", + "claude.mcp.json", + "codex.hooks.json", + "claude.hooks.json", + "skills/caplets/SKILL.md", + ]) { + expect(existsSync(path.join(repoRoot, requiredPath)), requiredPath).toBe(true); + } + }); +}); diff --git a/skills/caplets/SKILL.md b/skills/caplets/SKILL.md new file mode 100644 index 00000000..47953a3a --- /dev/null +++ b/skills/caplets/SKILL.md @@ -0,0 +1,39 @@ +--- +name: caplets +description: Use Caplets when you need to inspect and call configured capability domains without loading every downstream MCP, OpenAPI, GraphQL, HTTP, or CLI tool up front. +--- + +# Caplets + +Use Caplets when a task benefits from tools exposed through configured Caplets backends, including MCP servers, OpenAPI services, GraphQL endpoints, HTTP tools, or CLI commands. + +Caplets exposes progressive discovery operations instead of flattening every downstream tool into the agent context. Start narrow, inspect only what you need, then call the specific downstream tool. + +## Workflow + +1. Read the caplet card with `get_caplet` when you need to understand what a configured Caplet provides. +2. Check backend availability with `check_mcp_server`, `check_backend`, or the equivalent operation before relying on a backend. +3. Discover tools with `list_tools` or `search_tools`. +4. Inspect a downstream tool schema with `get_tool` before calling it. +5. Call downstream tools with `call_tool`, putting downstream inputs inside the top-level `arguments` object. + +## Guidance + +- Prefer `search_tools` when you know the capability you need. +- Prefer `list_tools` when exploring a small or unfamiliar Caplet. +- Keep downstream arguments nested under `arguments`; do not put downstream fields at the top level. +- Request only the output fields needed when the Caplet supports field selection. +- Treat Caplet backends as live integrations: handle unavailable services, auth failures, validation errors, and partial responses explicitly. +- Avoid loading broad tool lists unless the user task requires exploration. + +## Example + +```json +{ + "operation": "call_tool", + "tool": "example_tool", + "arguments": { + "query": "what the user needs" + } +} +``` From cc0987e4b54428744ef1c3711662a72fcf944b97 Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Fri, 15 May 2026 15:23:58 -0400 Subject: [PATCH 2/5] fix: address plugin review feedback --- claude.hooks.json | 2 +- codex.hooks.json | 2 +- package.json | 2 +- packages/core/test/agent-plugins.test.ts | 9 ++++++++- scripts/sync-plugin-versions.mjs | 16 ++++++++++++++++ 5 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 scripts/sync-plugin-versions.mjs diff --git a/claude.hooks.json b/claude.hooks.json index 50ddcc6c..fe3542cb 100644 --- a/claude.hooks.json +++ b/claude.hooks.json @@ -5,7 +5,7 @@ "hooks": [ { "type": "command", - "command": "printf '%s\\n' 'Caplets guidance: use the bundled caplets skill for progressive discovery. Start with get_caplet or search_tools, inspect schemas with get_tool, and pass downstream inputs inside call_tool.arguments.'" + "command": "node -e \"process.stdout.write('Caplets guidance: use the bundled caplets skill for progressive discovery. Start with get_caplet or search_tools, inspect schemas with get_tool, and pass downstream inputs inside call_tool.arguments.\\n')\"" } ] } diff --git a/codex.hooks.json b/codex.hooks.json index 5e8ac006..d76ab9d3 100644 --- a/codex.hooks.json +++ b/codex.hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "printf '%s\\n' 'Caplets guidance: use the bundled caplets skill for progressive discovery. Start with get_caplet or search_tools, inspect schemas with get_tool, and pass downstream inputs inside call_tool.arguments.'", + "command": "node -e \"process.stdout.write('Caplets guidance: use the bundled caplets skill for progressive discovery. Start with get_caplet or search_tools, inspect schemas with get_tool, and pass downstream inputs inside call_tool.arguments.\\n')\"", "statusMessage": "Loading Caplets guidance" } ] diff --git a/package.json b/package.json index 02abc495..91dc371c 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "typecheck": "pnpm -r --filter './packages/*' typecheck", "test": "pnpm -r --filter './packages/*' test", "verify": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm schema:check && pnpm test && pnpm benchmark:check && pnpm build", - "version-packages": "changeset version" + "version-packages": "changeset version && node scripts/sync-plugin-versions.mjs" }, "devDependencies": { "@changesets/cli": "^2.31.0", diff --git a/packages/core/test/agent-plugins.test.ts b/packages/core/test/agent-plugins.test.ts index 50c7f0f9..09b895c6 100644 --- a/packages/core/test/agent-plugins.test.ts +++ b/packages/core/test/agent-plugins.test.ts @@ -32,7 +32,8 @@ describe("root agent plugin artifacts", () => { }); it("keeps plugin manifest versions aligned with the CLI package", async () => { - const [cliPackage, codexManifest, claudeManifest] = await Promise.all([ + const [rootPackage, cliPackage, codexManifest, claudeManifest] = await Promise.all([ + readJson<{ scripts: Record }>(path.join(repoRoot, "package.json")), readJson<{ version: string }>(path.join(repoRoot, "packages/cli/package.json")), readJson<{ version: string }>(path.join(repoRoot, ".codex-plugin/plugin.json")), readJson<{ version: string }>(path.join(repoRoot, ".claude-plugin/plugin.json")), @@ -40,6 +41,7 @@ describe("root agent plugin artifacts", () => { expect(codexManifest.version).toBe(cliPackage.version); expect(claudeManifest.version).toBe(cliPackage.version); + expect(rootPackage.scripts["version-packages"]).toContain("scripts/sync-plugin-versions.mjs"); }); it("declares a Claude Code marketplace entry using Claude's schema", async () => { @@ -111,6 +113,10 @@ describe("root agent plugin artifacts", () => { expect(skill).toContain("name: caplets"); expect(skill).toContain("call_tool"); + expect(JSON.stringify(codexHooks)).toContain("node -e"); + expect(JSON.stringify(claudeHooks)).toContain("node -e"); + expect(JSON.stringify(codexHooks)).not.toContain("printf"); + expect(JSON.stringify(claudeHooks)).not.toContain("printf"); expect(JSON.stringify(codexHooks)).toContain("statusMessage"); expect(JSON.stringify(claudeHooks)).not.toContain("statusMessage"); }); @@ -144,6 +150,7 @@ describe("root agent plugin artifacts", () => { "claude.mcp.json", "codex.hooks.json", "claude.hooks.json", + "scripts/sync-plugin-versions.mjs", "skills/caplets/SKILL.md", ]) { expect(existsSync(path.join(repoRoot, requiredPath)), requiredPath).toBe(true); diff --git a/scripts/sync-plugin-versions.mjs b/scripts/sync-plugin-versions.mjs new file mode 100644 index 00000000..0a26f65f --- /dev/null +++ b/scripts/sync-plugin-versions.mjs @@ -0,0 +1,16 @@ +import { readFile, writeFile } from "node:fs/promises"; + +const cliPackage = JSON.parse(await readFile("packages/cli/package.json", "utf8")); + +for (const manifestPath of [".codex-plugin/plugin.json", ".claude-plugin/plugin.json"]) { + const manifest = await readFile(manifestPath, "utf8"); + const versionFieldPattern = /"version":\s*"[^"]+"/; + + if (!versionFieldPattern.test(manifest)) { + throw new Error(`Could not find version field in ${manifestPath}`); + } + + const updated = manifest.replace(versionFieldPattern, `"version": "${cliPackage.version}"`); + + await writeFile(manifestPath, updated); +} From a2c24dcf89c28f56c164b930bc340959699ea811 Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Fri, 15 May 2026 15:32:15 -0400 Subject: [PATCH 3/5] fix: simplify agent plugin guidance --- .changeset/native-agent-plugin-bundles.md | 2 +- .claude-plugin/plugin.json | 3 +-- .codex-plugin/plugin.json | 1 - README.md | 14 ++++++------ claude.hooks.json | 14 ------------ codex.hooks.json | 16 -------------- packages/core/test/agent-plugins.test.ts | 27 +++++++++-------------- skills/caplets/SKILL.md | 14 +++++++++--- 8 files changed, 31 insertions(+), 60 deletions(-) delete mode 100644 claude.hooks.json delete mode 100644 codex.hooks.json diff --git a/.changeset/native-agent-plugin-bundles.md b/.changeset/native-agent-plugin-bundles.md index b3c49e3f..920cc4b0 100644 --- a/.changeset/native-agent-plugin-bundles.md +++ b/.changeset/native-agent-plugin-bundles.md @@ -2,4 +2,4 @@ "caplets": patch --- -Add root-level Codex and Claude Code plugin artifacts that package Caplets MCP server configuration, shared agent skill guidance, and agent-specific plugin hooks. +Add root-level Codex and Claude Code plugin artifacts that package Caplets MCP server configuration and shared agent skill guidance. diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index bb9d05fd..1fead988 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -10,6 +10,5 @@ "license": "MIT", "keywords": ["caplets", "mcp", "claude-code", "tools"], "skills": "./skills/", - "mcpServers": "./claude.mcp.json", - "hooks": "./claude.hooks.json" + "mcpServers": "./claude.mcp.json" } diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index e908ce71..e129a427 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -11,7 +11,6 @@ "keywords": ["caplets", "mcp", "codex", "tools"], "skills": "./skills/", "mcpServers": "./codex.mcp.json", - "hooks": "./codex.hooks.json", "interface": { "displayName": "Caplets", "shortDescription": "Progressive disclosure for Caplets tools.", diff --git a/README.md b/README.md index 234e29ef..3c2e9f96 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,13 @@ only the backend it needs. Use Caplets as a normal MCP server everywhere, or install a native agent integration when your coding agent supports one. -| Agent | Install | What It Provides | -| -------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -| Any MCP client | Add `caplets serve` as a stdio MCP server | Universal progressive-disclosure gateway | -| Claude Code | `claude plugin marketplace add spiritledsoftware/caplets && claude plugin install caplets@caplets` | Claude Code plugin metadata, MCP config, hooks, and shared skill guidance | -| Codex | `codex plugin marketplace add spiritledsoftware/caplets`, then install `caplets` from Codex plugins | Codex plugin metadata, MCP config, hooks, and shared skill guidance | -| OpenCode | Install [`@caplets/opencode`](packages/opencode/README.md) | Native `caplets_` tools and prompt guidance hooks | -| Pi | Install [`@caplets/pi`](packages/pi/README.md) | Native `caplets_` tools with Pi prompt snippets/guidelines | +| Agent | Install | What It Provides | +| -------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| Any MCP client | Add `caplets serve` as a stdio MCP server | Universal progressive-disclosure gateway | +| Claude Code | `claude plugin marketplace add spiritledsoftware/caplets && claude plugin install caplets@caplets` | Claude Code plugin metadata, MCP config, and shared skill guidance | +| Codex | `codex plugin marketplace add spiritledsoftware/caplets`, then install `caplets` from Codex plugins | Codex plugin metadata, MCP config, and shared skill guidance | +| OpenCode | Install [`@caplets/opencode`](packages/opencode/README.md) | Native `caplets_` tools and prompt guidance hooks | +| Pi | Install [`@caplets/pi`](packages/pi/README.md) | Native `caplets_` tools with Pi prompt snippets/guidelines | Codex and Claude Code plugins are plugin-native but MCP-backed. Their manifests live in `.codex-plugin/` and `.claude-plugin/`; component files live at the plugin root so diff --git a/claude.hooks.json b/claude.hooks.json deleted file mode 100644 index fe3542cb..00000000 --- a/claude.hooks.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "hooks": { - "SessionStart": [ - { - "hooks": [ - { - "type": "command", - "command": "node -e \"process.stdout.write('Caplets guidance: use the bundled caplets skill for progressive discovery. Start with get_caplet or search_tools, inspect schemas with get_tool, and pass downstream inputs inside call_tool.arguments.\\n')\"" - } - ] - } - ] - } -} diff --git a/codex.hooks.json b/codex.hooks.json deleted file mode 100644 index d76ab9d3..00000000 --- a/codex.hooks.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "hooks": { - "SessionStart": [ - { - "matcher": "startup|resume|clear", - "hooks": [ - { - "type": "command", - "command": "node -e \"process.stdout.write('Caplets guidance: use the bundled caplets skill for progressive discovery. Start with get_caplet or search_tools, inspect schemas with get_tool, and pass downstream inputs inside call_tool.arguments.\\n')\"", - "statusMessage": "Loading Caplets guidance" - } - ] - } - ] - } -} diff --git a/packages/core/test/agent-plugins.test.ts b/packages/core/test/agent-plugins.test.ts index 09b895c6..3f5f588e 100644 --- a/packages/core/test/agent-plugins.test.ts +++ b/packages/core/test/agent-plugins.test.ts @@ -18,7 +18,7 @@ describe("root agent plugin artifacts", () => { expect(manifest.skills).toBe("./skills/"); expect(manifest.mcpServers).toBe("./codex.mcp.json"); - expect(manifest.hooks).toBe("./codex.hooks.json"); + expect(manifest.hooks).toBeUndefined(); }); it("declares Claude Code plugin components using Claude-specific files", async () => { @@ -28,7 +28,7 @@ describe("root agent plugin artifacts", () => { expect(manifest.skills).toBe("./skills/"); expect(manifest.mcpServers).toBe("./claude.mcp.json"); - expect(manifest.hooks).toBe("./claude.hooks.json"); + expect(manifest.hooks).toBeUndefined(); }); it("keeps plugin manifest versions aligned with the CLI package", async () => { @@ -104,21 +104,18 @@ describe("root agent plugin artifacts", () => { expect(JSON.stringify(claudeMcp)).not.toContain("caplets@"); }); - it("uses the shared root skill and keeps hooks agent-specific", async () => { - const [skill, codexHooks, claudeHooks] = await Promise.all([ - readFile(path.join(repoRoot, "skills/caplets/SKILL.md"), "utf8"), - readJson>(path.join(repoRoot, "codex.hooks.json")), - readJson>(path.join(repoRoot, "claude.hooks.json")), - ]); + it("uses a strong shared root skill for automatic selection", async () => { + const skill = await readFile(path.join(repoRoot, "skills/caplets/SKILL.md"), "utf8"); expect(skill).toContain("name: caplets"); + expect(skill).toContain("when_to_use:"); + expect(skill).toContain("external tools"); + expect(skill).toContain("MCP servers"); + expect(skill).toContain("OpenAPI"); + expect(skill).toContain("GraphQL"); + expect(skill).toContain("HTTP endpoints"); expect(skill).toContain("call_tool"); - expect(JSON.stringify(codexHooks)).toContain("node -e"); - expect(JSON.stringify(claudeHooks)).toContain("node -e"); - expect(JSON.stringify(codexHooks)).not.toContain("printf"); - expect(JSON.stringify(claudeHooks)).not.toContain("printf"); - expect(JSON.stringify(codexHooks)).toContain("statusMessage"); - expect(JSON.stringify(claudeHooks)).not.toContain("statusMessage"); + expect(skill).toContain("Skip this skill for normal local code edits"); }); it("keeps plugin metadata and components in documented locations", () => { @@ -148,8 +145,6 @@ describe("root agent plugin artifacts", () => { ".agents/plugins/marketplace.json", "codex.mcp.json", "claude.mcp.json", - "codex.hooks.json", - "claude.hooks.json", "scripts/sync-plugin-versions.mjs", "skills/caplets/SKILL.md", ]) { diff --git a/skills/caplets/SKILL.md b/skills/caplets/SKILL.md index 47953a3a..c450807b 100644 --- a/skills/caplets/SKILL.md +++ b/skills/caplets/SKILL.md @@ -1,13 +1,21 @@ --- name: caplets -description: Use Caplets when you need to inspect and call configured capability domains without loading every downstream MCP, OpenAPI, GraphQL, HTTP, or CLI tool up front. +description: Use Caplets for any task that needs external tools, integrations, APIs, docs, repositories, project systems, MCP servers, OpenAPI, GraphQL, HTTP endpoints, or curated CLI commands exposed through Caplets. Invoke this before searching for or calling downstream tools so you can discover capability domains with progressive disclosure instead of loading a flat tool wall. +when_to_use: Trigger when the user asks to use an integration, inspect available tools, call a configured service, query docs, work with GitHub/Linear/Context7 or other installed Caplets, access an MCP/OpenAPI/GraphQL/HTTP backend, run curated repository commands, or when a task appears to need a tool that is not directly visible in the current tool list. Do not use for ordinary local code edits that only need built-in file, shell, or search tools. --- # Caplets -Use Caplets when a task benefits from tools exposed through configured Caplets backends, including MCP servers, OpenAPI services, GraphQL endpoints, HTTP tools, or CLI commands. +Use Caplets before searching for or calling downstream tools that may be exposed through configured Caplets backends, including MCP servers, OpenAPI services, GraphQL endpoints, HTTP tools, documentation services, project-management systems, source-control services, or curated CLI commands. -Caplets exposes progressive discovery operations instead of flattening every downstream tool into the agent context. Start narrow, inspect only what you need, then call the specific downstream tool. +Caplets exposes progressive discovery operations instead of flattening every downstream tool into the agent context. Start with the configured capability domain, inspect only what you need, then call the specific downstream tool. + +## Trigger Heuristics + +- Use this skill when the user mentions Caplets, configured tools, MCP, OpenAPI, GraphQL, HTTP tools, docs, GitHub, Linear, Context7, project systems, source-control systems, or other installed integration domains. +- Use this skill when the task needs a capability that may exist behind Caplets but is not directly available as a top-level tool. +- Use this skill before broad tool discovery so you can search Caplets capability domains first. +- Skip this skill for normal local code edits, file reads, shell commands, or repository searches that do not need an external or configured Caplets backend. ## Workflow From 6f9e86e9de2903f8c526b9523d10eb6b36f9d3bb Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Fri, 15 May 2026 15:37:01 -0400 Subject: [PATCH 4/5] fix: tighten Caplets skill trigger copy --- skills/caplets/SKILL.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skills/caplets/SKILL.md b/skills/caplets/SKILL.md index c450807b..1ddd1f94 100644 --- a/skills/caplets/SKILL.md +++ b/skills/caplets/SKILL.md @@ -1,18 +1,18 @@ --- name: caplets -description: Use Caplets for any task that needs external tools, integrations, APIs, docs, repositories, project systems, MCP servers, OpenAPI, GraphQL, HTTP endpoints, or curated CLI commands exposed through Caplets. Invoke this before searching for or calling downstream tools so you can discover capability domains with progressive disclosure instead of loading a flat tool wall. -when_to_use: Trigger when the user asks to use an integration, inspect available tools, call a configured service, query docs, work with GitHub/Linear/Context7 or other installed Caplets, access an MCP/OpenAPI/GraphQL/HTTP backend, run curated repository commands, or when a task appears to need a tool that is not directly visible in the current tool list. Do not use for ordinary local code edits that only need built-in file, shell, or search tools. +description: Use Caplets to discover and call configured external tools through progressive disclosure before using downstream MCP, API, or CLI capabilities. +when_to_use: Trigger when the user asks to use an integration, inspect available tools, call a configured service, query external information, access an MCP/OpenAPI/GraphQL/HTTP backend, run curated repository commands, or use a capability that may not appear as a direct top-level tool. Do not use for ordinary local code edits that only need built-in file, shell, or search tools. --- # Caplets -Use Caplets before searching for or calling downstream tools that may be exposed through configured Caplets backends, including MCP servers, OpenAPI services, GraphQL endpoints, HTTP tools, documentation services, project-management systems, source-control services, or curated CLI commands. +Use Caplets before searching for or calling downstream tools exposed through configured Caplets backends, including MCP servers, OpenAPI services, GraphQL endpoints, HTTP endpoints, external information services, project systems, source-control systems, or curated CLI commands. Caplets exposes progressive discovery operations instead of flattening every downstream tool into the agent context. Start with the configured capability domain, inspect only what you need, then call the specific downstream tool. ## Trigger Heuristics -- Use this skill when the user mentions Caplets, configured tools, MCP, OpenAPI, GraphQL, HTTP tools, docs, GitHub, Linear, Context7, project systems, source-control systems, or other installed integration domains. +- Use this skill when the user mentions Caplets, configured tools, MCP, OpenAPI, GraphQL, HTTP tools, external information, project systems, source-control systems, or other installed integration domains. - Use this skill when the task needs a capability that may exist behind Caplets but is not directly available as a top-level tool. - Use this skill before broad tool discovery so you can search Caplets capability domains first. - Skip this skill for normal local code edits, file reads, shell commands, or repository searches that do not need an external or configured Caplets backend. From df06c5b1e3570a58b058930396a1220dfb3f12c1 Mon Sep 17 00:00:00 2001 From: Ian Pascoe Date: Fri, 15 May 2026 15:42:46 -0400 Subject: [PATCH 5/5] fix: parse plugin manifests during version sync --- package.json | 2 +- packages/core/test/agent-plugins.test.ts | 1 + scripts/sync-plugin-versions.mjs | 19 +++++++++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 91dc371c..84666f19 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "typecheck": "pnpm -r --filter './packages/*' typecheck", "test": "pnpm -r --filter './packages/*' test", "verify": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm schema:check && pnpm test && pnpm benchmark:check && pnpm build", - "version-packages": "changeset version && node scripts/sync-plugin-versions.mjs" + "version-packages": "changeset version && node scripts/sync-plugin-versions.mjs && oxfmt .codex-plugin/plugin.json .claude-plugin/plugin.json" }, "devDependencies": { "@changesets/cli": "^2.31.0", diff --git a/packages/core/test/agent-plugins.test.ts b/packages/core/test/agent-plugins.test.ts index 3f5f588e..59ac9b09 100644 --- a/packages/core/test/agent-plugins.test.ts +++ b/packages/core/test/agent-plugins.test.ts @@ -42,6 +42,7 @@ describe("root agent plugin artifacts", () => { expect(codexManifest.version).toBe(cliPackage.version); expect(claudeManifest.version).toBe(cliPackage.version); expect(rootPackage.scripts["version-packages"]).toContain("scripts/sync-plugin-versions.mjs"); + expect(rootPackage.scripts["version-packages"]).toContain("oxfmt"); }); it("declares a Claude Code marketplace entry using Claude's schema", async () => { diff --git a/scripts/sync-plugin-versions.mjs b/scripts/sync-plugin-versions.mjs index 0a26f65f..b9fde03e 100644 --- a/scripts/sync-plugin-versions.mjs +++ b/scripts/sync-plugin-versions.mjs @@ -1,16 +1,23 @@ import { readFile, writeFile } from "node:fs/promises"; -const cliPackage = JSON.parse(await readFile("packages/cli/package.json", "utf8")); +async function readJson(filePath) { + try { + return JSON.parse(await readFile(filePath, "utf8")); + } catch (error) { + throw new Error(`Could not parse JSON at ${filePath}`, { cause: error }); + } +} + +const cliPackage = await readJson("packages/cli/package.json"); for (const manifestPath of [".codex-plugin/plugin.json", ".claude-plugin/plugin.json"]) { - const manifest = await readFile(manifestPath, "utf8"); - const versionFieldPattern = /"version":\s*"[^"]+"/; + const manifest = await readJson(manifestPath); - if (!versionFieldPattern.test(manifest)) { + if (typeof manifest.version !== "string") { throw new Error(`Could not find version field in ${manifestPath}`); } - const updated = manifest.replace(versionFieldPattern, `"version": "${cliPackage.version}"`); + manifest.version = cliPackage.version; - await writeFile(manifestPath, updated); + await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); }