diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json
index 3addd3ad..c0599d59 100644
--- a/.agents/plugins/marketplace.json
+++ b/.agents/plugins/marketplace.json
@@ -8,7 +8,7 @@
"name": "caplets",
"source": {
"source": "local",
- "path": "./"
+ "path": "./plugins/caplets"
},
"policy": {
"installation": "AVAILABLE",
diff --git a/.changeset/fix-codex-plugin-install.md b/.changeset/fix-codex-plugin-install.md
new file mode 100644
index 00000000..a718ffca
--- /dev/null
+++ b/.changeset/fix-codex-plugin-install.md
@@ -0,0 +1,5 @@
+---
+"caplets": patch
+---
+
+Fix Codex plugin marketplace installation by using the supported `plugins/caplets` local plugin source layout with bundled skills and shared MCP config.
diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index c0fc241a..b90d2ff4 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -1,14 +1,21 @@
{
"name": "caplets",
"owner": {
- "name": "Spirit-Led Software LLC"
+ "name": "Spirit-Led Software LLC",
+ "url": "https://github.com/spiritledsoftware"
},
"plugins": [
{
"name": "caplets",
- "source": "./",
+ "source": "./plugins/caplets",
"description": "Expose configured Caplets as progressive-disclosure tools in Claude Code.",
- "category": "Developer Tools"
+ "category": "Developer Tools",
+ "author": {
+ "name": "Spirit-Led Software LLC",
+ "url": "https://github.com/spiritledsoftware",
+ "email": "ianpascoe@spiritledsoftware.com"
+ },
+ "homepage": "https://github.com/spiritledsoftware/caplets"
}
]
}
diff --git a/README.md b/README.md
index ca9fcf69..ef034378 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-

+
Caplets
@@ -81,9 +81,11 @@ your coding agent supports one.
| 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.
+Codex and Claude Code plugins are plugin-native but MCP-backed. The installable plugin
+lives under `plugins/caplets/`, with agent-specific manifests in `.codex-plugin/` and
+`.claude-plugin/`, a shared `skills/` directory, and shared `mcp.json` config. The
+repo-level `.agents/plugins/marketplace.json` and `.claude-plugin/marketplace.json`
+files only advertise that installable plugin root.
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
diff --git a/codex.mcp.json b/codex.mcp.json
deleted file mode 100644
index e0473a17..00000000
--- a/codex.mcp.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "caplets": {
- "command": "caplets",
- "args": ["serve"],
- "startup_timeout_sec": 20,
- "tool_timeout_sec": 120
- }
-}
diff --git a/package.json b/package.json
index 84666f19..5dd8dd90 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 && oxfmt .codex-plugin/plugin.json .claude-plugin/plugin.json"
+ "version-packages": "changeset version && node scripts/sync-plugin-versions.mjs && oxfmt plugins/caplets/.codex-plugin/plugin.json plugins/caplets/.claude-plugin/plugin.json"
},
"devDependencies": {
"@changesets/cli": "^2.31.0",
diff --git a/packages/benchmarks/lib/config.mjs b/packages/benchmarks/lib/config.mjs
index 606001dc..e50485fc 100644
--- a/packages/benchmarks/lib/config.mjs
+++ b/packages/benchmarks/lib/config.mjs
@@ -12,7 +12,7 @@ export function getBenchmarkPaths({ repoRoot = REPO_ROOT } = {}) {
return {
repoRoot: absoluteRepoRoot,
fixtureServerPath: join(absoluteRepoRoot, "fixtures", "mcp-server.mjs"),
- capletsCliPath: resolve(absoluteRepoRoot, "../cli/dist/index.js"),
+ capletsCliPath: CAPLETS_CLI_PATH,
};
}
diff --git a/packages/core/test/agent-plugins.test.ts b/packages/core/test/agent-plugins.test.ts
index 59ac9b09..c0b3dabc 100644
--- a/packages/core/test/agent-plugins.test.ts
+++ b/packages/core/test/agent-plugins.test.ts
@@ -1,4 +1,4 @@
-import { existsSync } from "node:fs";
+import { existsSync, lstatSync } from "node:fs";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -13,21 +13,21 @@ async function readJson(filePath: string): Promise {
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"),
+ path.join(repoRoot, "plugins/caplets/.codex-plugin/plugin.json"),
);
expect(manifest.skills).toBe("./skills/");
- expect(manifest.mcpServers).toBe("./codex.mcp.json");
+ expect(manifest.mcpServers).toBe("./mcp.json");
expect(manifest.hooks).toBeUndefined();
});
it("declares Claude Code plugin components using Claude-specific files", async () => {
const manifest = await readJson>(
- path.join(repoRoot, ".claude-plugin/plugin.json"),
+ path.join(repoRoot, "plugins/caplets/.claude-plugin/plugin.json"),
);
expect(manifest.skills).toBe("./skills/");
- expect(manifest.mcpServers).toBe("./claude.mcp.json");
+ expect(manifest.mcpServers).toBe("./mcp.json");
expect(manifest.hooks).toBeUndefined();
});
@@ -35,8 +35,12 @@ describe("root agent plugin artifacts", () => {
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")),
+ readJson<{ version: string }>(
+ path.join(repoRoot, "plugins/caplets/.codex-plugin/plugin.json"),
+ ),
+ readJson<{ version: string }>(
+ path.join(repoRoot, "plugins/caplets/.claude-plugin/plugin.json"),
+ ),
]);
expect(codexManifest.version).toBe(cliPackage.version);
@@ -60,7 +64,7 @@ describe("root agent plugin artifacts", () => {
expect(marketplace.plugins).toEqual([
expect.objectContaining({
name: "caplets",
- source: "./",
+ source: "./plugins/caplets",
}),
]);
});
@@ -78,35 +82,28 @@ describe("root agent plugin artifacts", () => {
expect(marketplace.plugins).toEqual([
expect.objectContaining({
name: "caplets",
- source: { source: "local", path: "./" },
+ source: { source: "local", path: "./plugins/caplets" },
}),
]);
});
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"),
- ),
- ]);
+ const mcp = await readJson<{ mcpServers: { caplets: { command: string; args: string[] } } }>(
+ path.join(repoRoot, "plugins/caplets/mcp.json"),
+ );
- expect(codexMcp.caplets).toMatchObject({
+ expect(mcp.mcpServers.caplets).toEqual({
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@");
+ expect(JSON.stringify(mcp)).not.toContain("caplets@");
});
- it("uses a strong shared root skill for automatic selection", async () => {
- const skill = await readFile(path.join(repoRoot, "skills/caplets/SKILL.md"), "utf8");
+ it("uses a strong shared plugin skill for automatic selection", async () => {
+ const skill = await readFile(
+ path.join(repoRoot, "plugins/caplets/skills/caplets/SKILL.md"),
+ "utf8",
+ );
expect(skill).toContain("name: caplets");
expect(skill).toContain("when_to_use:");
@@ -119,35 +116,47 @@ describe("root agent plugin artifacts", () => {
expect(skill).toContain("Skip this skill for normal local code edits");
});
+ it("keeps the Codex marketplace source self-contained for plugin installs", () => {
+ const pluginRoot = path.join(repoRoot, "plugins/caplets");
+ const skillDir = path.join(pluginRoot, "skills");
+
+ expect(lstatSync(pluginRoot).isDirectory()).toBe(true);
+ expect(lstatSync(skillDir).isDirectory()).toBe(true);
+ expect(lstatSync(skillDir).isSymbolicLink()).toBe(false);
+ expect(existsSync(path.join(pluginRoot, ".codex-plugin/plugin.json"))).toBe(true);
+ expect(existsSync(path.join(pluginRoot, "skills/caplets/SKILL.md"))).toBe(true);
+ expect(existsSync(path.join(pluginRoot, "mcp.json"))).toBe(true);
+ });
+
it("keeps plugin metadata and components in documented locations", () => {
for (const forbiddenPath of [
"packages/codex",
"packages/claude-code",
"packages/agent-plugin-shared",
- "plugins",
+ ".codex-plugin",
+ ".codex-plugins",
".mcp.json",
"hooks",
- ".codex-plugin/.mcp.json",
+ ".claude-plugin/plugin.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",
+ "plugins/caplets/.codex-plugin/plugin.json",
+ "plugins/caplets/.claude-plugin/plugin.json",
+ "plugins/caplets/mcp.json",
+ "plugins/caplets/skills/caplets/SKILL.md",
+ "plugins/caplets/assets/icon.png",
".claude-plugin/marketplace.json",
".agents/plugins/marketplace.json",
- "codex.mcp.json",
- "claude.mcp.json",
"scripts/sync-plugin-versions.mjs",
- "skills/caplets/SKILL.md",
]) {
expect(existsSync(path.join(repoRoot, requiredPath)), requiredPath).toBe(true);
}
diff --git a/.claude-plugin/plugin.json b/plugins/caplets/.claude-plugin/plugin.json
similarity index 92%
rename from .claude-plugin/plugin.json
rename to plugins/caplets/.claude-plugin/plugin.json
index d1e171c5..0aa82ffc 100644
--- a/.claude-plugin/plugin.json
+++ b/plugins/caplets/.claude-plugin/plugin.json
@@ -10,5 +10,5 @@
"license": "MIT",
"keywords": ["caplets", "mcp", "claude-code", "tools"],
"skills": "./skills/",
- "mcpServers": "./claude.mcp.json"
+ "mcpServers": "./mcp.json"
}
diff --git a/.codex-plugin/plugin.json b/plugins/caplets/.codex-plugin/plugin.json
similarity index 96%
rename from .codex-plugin/plugin.json
rename to plugins/caplets/.codex-plugin/plugin.json
index 2f2bec33..6b78c951 100644
--- a/.codex-plugin/plugin.json
+++ b/plugins/caplets/.codex-plugin/plugin.json
@@ -10,7 +10,7 @@
"license": "MIT",
"keywords": ["caplets", "mcp", "codex", "tools"],
"skills": "./skills/",
- "mcpServers": "./codex.mcp.json",
+ "mcpServers": "./mcp.json",
"interface": {
"displayName": "Caplets",
"shortDescription": "Progressive disclosure for Caplets tools.",
diff --git a/assets/icon.png b/plugins/caplets/assets/icon.png
similarity index 100%
rename from assets/icon.png
rename to plugins/caplets/assets/icon.png
diff --git a/claude.mcp.json b/plugins/caplets/mcp.json
similarity index 100%
rename from claude.mcp.json
rename to plugins/caplets/mcp.json
diff --git a/skills/caplets/SKILL.md b/plugins/caplets/skills/caplets/SKILL.md
similarity index 100%
rename from skills/caplets/SKILL.md
rename to plugins/caplets/skills/caplets/SKILL.md
diff --git a/scripts/sync-plugin-versions.mjs b/scripts/sync-plugin-versions.mjs
index b9fde03e..d0ac157f 100644
--- a/scripts/sync-plugin-versions.mjs
+++ b/scripts/sync-plugin-versions.mjs
@@ -10,7 +10,10 @@ async function readJson(filePath) {
const cliPackage = await readJson("packages/cli/package.json");
-for (const manifestPath of [".codex-plugin/plugin.json", ".claude-plugin/plugin.json"]) {
+for (const manifestPath of [
+ "plugins/caplets/.codex-plugin/plugin.json",
+ "plugins/caplets/.claude-plugin/plugin.json",
+]) {
const manifest = await readJson(manifestPath);
if (typeof manifest.version !== "string") {