Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"name": "caplets",
"source": {
"source": "local",
"path": "./"
"path": "./plugins/caplets"
},
"policy": {
"installation": "AVAILABLE",
Expand Down
5 changes: 5 additions & 0 deletions .changeset/fix-codex-plugin-install.md
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
ian-pascoe marked this conversation as resolved.
13 changes: 10 additions & 3 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
{
"name": "caplets",
"owner": {
"name": "Spirit-Led Software LLC"
"name": "Spirit-Led Software LLC",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"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"
}
]
}
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div align="center">
<img src="assets/icon.png" alt="Caplets logo" width="140" height="140" />
<img src="plugins/caplets/assets/icon.png" alt="Caplets logo" width="140" height="140" />

<h1>Caplets</h1>

Expand Down Expand Up @@ -81,9 +81,11 @@ your coding agent supports one.
| OpenCode | Install [`@caplets/opencode`](packages/opencode/README.md) | Native `caplets_<id>` tools and prompt guidance hooks |
| Pi | Install [`@caplets/pi`](packages/pi/README.md) | Native `caplets_<id>` 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.
Comment thread
ian-pascoe marked this conversation as resolved.

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
Expand Down
8 changes: 0 additions & 8 deletions codex.mcp.json

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/benchmarks/lib/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down
77 changes: 43 additions & 34 deletions packages/core/test/agent-plugins.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -13,30 +13,34 @@ async function readJson<T>(filePath: string): Promise<T> {
describe("root agent plugin artifacts", () => {
it("declares Codex plugin components using Codex-specific files", async () => {
const manifest = await readJson<Record<string, unknown>>(
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<Record<string, unknown>>(
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();
});

it("keeps plugin manifest versions aligned with the CLI package", async () => {
const [rootPackage, cliPackage, codexManifest, claudeManifest] = await Promise.all([
readJson<{ scripts: Record<string, string> }>(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);
Expand All @@ -60,7 +64,7 @@ describe("root agent plugin artifacts", () => {
expect(marketplace.plugins).toEqual([
expect.objectContaining({
name: "caplets",
source: "./",
source: "./plugins/caplets",
}),
]);
});
Expand All @@ -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:");
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"license": "MIT",
"keywords": ["caplets", "mcp", "claude-code", "tools"],
"skills": "./skills/",
"mcpServers": "./claude.mcp.json"
"mcpServers": "./mcp.json"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
File renamed without changes
File renamed without changes.
File renamed without changes.
5 changes: 4 additions & 1 deletion scripts/sync-plugin-versions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
Loading