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
22 changes: 22 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "cardstack-boxel",
"interface": {
"displayName": "Cardstack Boxel"
},
"plugins": [
{
"name": "boxel-cli",
"source": {
"source": "local",
"path": "./packages/boxel-cli/plugin"
},
"description": "Skills for working with Boxel realms via @cardstack/boxel-cli.",
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL",
"products": ["CODEX"]
},
"category": "Developer Tools"
}
]
}
22 changes: 14 additions & 8 deletions .github/workflows/boxel-cli-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,19 @@ jobs:
echo "package.json → $NEXT_NPM"
fi
if [ -n "$NEXT_PLUGIN" ]; then
node -e '
const fs = require("fs");
const p = "packages/boxel-cli/plugin/.claude-plugin/plugin.json";
const j = JSON.parse(fs.readFileSync(p, "utf8"));
j.version = process.env.NEXT_PLUGIN;
fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\n");
'
echo "plugin.json → $NEXT_PLUGIN"
# The Claude and Codex manifests version together; build:plugin
# syncs the rest of the Codex manifest from the Claude one.
for p in packages/boxel-cli/plugin/.claude-plugin/plugin.json \
packages/boxel-cli/plugin/.codex-plugin/plugin.json; do
PLUGIN_MANIFEST="$p" node -e '
const fs = require("fs");
const p = process.env.PLUGIN_MANIFEST;
const j = JSON.parse(fs.readFileSync(p, "utf8"));
j.version = process.env.NEXT_PLUGIN;
fs.writeFileSync(p, JSON.stringify(j, null, 2) + "\n");
'
echo "$p → $NEXT_PLUGIN"
done
fi

- name: Generate release notes
Expand Down Expand Up @@ -231,6 +236,7 @@ jobs:
set -euo pipefail
git add packages/boxel-cli/package.json \
packages/boxel-cli/plugin/.claude-plugin/plugin.json \
packages/boxel-cli/plugin/.codex-plugin/plugin.json \
packages/boxel-cli/plugin/skills \
packages/boxel-cli/plugin/commands \
packages/boxel-cli/plugin/README.md \
Expand Down
26 changes: 26 additions & 0 deletions packages/boxel-cli/plugin/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "boxel-cli",
"version": "0.10.1",
"description": "Skills for working with Boxel realms via @cardstack/boxel-cli. Requires @cardstack/boxel-cli >= 0.0.1 installed on PATH (npm install -g @cardstack/boxel-cli).",
"skills": "./skills/",
"author": {
"name": "Cardstack",
"url": "https://boxel.ai"
},
"homepage": "https://github.com/cardstack/boxel/tree/main/packages/boxel-cli/plugin",
"repository": "https://github.com/cardstack/boxel.git",
"license": "MIT",
"interface": {
"displayName": "Boxel CLI",
"shortDescription": "Author and sync Boxel cards, realms, and workspaces",
"longDescription": "Create and edit Boxel cards, fields, and templates, then sync them between local disk and a Boxel realm — with skills covering card authoring, theming, catalog listings, federated search, and realm indexing diagnostics.",
"developerName": "Cardstack",
"category": "Developer Tools",
"capabilities": ["Interactive", "Read", "Write"],
"defaultPrompt": [
"Create a Boxel card for the data I describe",
"Pull my Boxel workspace down so I can edit it locally"
],
"websiteURL": "https://boxel.ai"
Comment thread
jurgenwerk marked this conversation as resolved.
}
}
37 changes: 30 additions & 7 deletions packages/boxel-cli/plugin/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `boxel-cli` Claude Code plugin
# `boxel-cli` agent plugin

Claude Code skills for working with Boxel realms via [`@cardstack/boxel-cli`](https://www.npmjs.com/package/@cardstack/boxel-cli).
Agent skills for working with Boxel realms via [`@cardstack/boxel-cli`](https://www.npmjs.com/package/@cardstack/boxel-cli). Packaged for both Claude Code (`.claude-plugin/`) and OpenAI Codex (`.codex-plugin/`); the two manifests share the same `skills/` directory.

## Prerequisites

Expand All @@ -20,26 +20,49 @@ The plugin documents commands in `@cardstack/boxel-cli >= 0.0.1`. Newer plugin v

## Install

### External users (marketplace)
### Claude Code

```text
/plugin marketplace add cardstack/boxel
/plugin install boxel-cli
```

### Internal / development (`--plugin-dir`)

From a checkout of `cardstack/boxel`:
For internal development, from a checkout of `cardstack/boxel`:

```bash
claude --plugin-dir packages/boxel-cli/plugin
```

`/reload-plugins` picks up local edits without restarting Claude Code.

### OpenAI Codex

Codex discovers the plugin through the marketplace manifest at
`.agents/plugins/marketplace.json` in the repo root:

```text
/plugin marketplace add cardstack/boxel
/plugin install boxel-cli@cardstack-boxel
```

In Codex the skills are namespaced `boxel-cli:<name>` — invoke one with the `$`
prefix (`$boxel`, `$realm-sync`, …), or let Codex pick it up by description
match. The `/boxel-cli:<name>` form in the tables below is Claude Code's.

Without installing the plugin, a checkout also works directly: Codex reads
skills from `~/.agents/skills` (or a project's `.agents/skills`), expecting
`<name>/SKILL.md` one level down. Copy each skill in — Codex does not follow
symlinked skill directories:

```bash
mkdir -p ~/.agents/skills
cp -R /path/to/boxel/packages/boxel-cli/plugin/skills/*/ ~/.agents/skills/
```

## What you get

Skills appear under the `/boxel-cli:` namespace. Two surfaces:
Skills appear under the `boxel-cli` namespace — written `/boxel-cli:<name>`
below, which is how Claude Code invokes them. Two surfaces:

### CLI command skills

Expand Down
78 changes: 74 additions & 4 deletions packages/boxel-cli/scripts/build-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
/**
* Generate the `<!-- generated:commands -->` synopsis blocks in the plugin's
* SKILL.md files from the Commander program. Run via `pnpm build:plugin`.
* SKILL.md files from the Commander program, and sync the Codex plugin
* manifest (`.codex-plugin/plugin.json`) from the Claude Code one. Run via
* `pnpm build:plugin`.
*
* CI runs this and `git diff --exit-code` to fail PRs whose CLI changes
* weren't reflected in the plugin. The "synopsis-bump coupling" check (see
* .github/workflows/ci-lint.yaml) further ensures `plugin.json` is bumped
* whenever a generated block changes.
*/
import type { Command, Option } from 'commander';
import { readFileSync, writeFileSync } from 'fs';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { resolve } from 'path';
import { buildBoxelProgram } from '../src/build-program.ts';

const PLUGIN_DIR = resolve(import.meta.dirname, '..', 'plugin');
const CLAUDE_MANIFEST_PATH = resolve(
PLUGIN_DIR,
'.claude-plugin',
'plugin.json',
);
const CODEX_MANIFEST_PATH = resolve(PLUGIN_DIR, '.codex-plugin', 'plugin.json');

interface SkillSpec {
/** Skill folder name under plugin/skills/ */
Expand Down Expand Up @@ -174,6 +182,64 @@ function rewriteSkillFile(skill: string, body: string): boolean {
return true;
}

/**
* Derive `.codex-plugin/plugin.json` from `.claude-plugin/plugin.json` so the
* two manifests can't drift: name/version/metadata come from the Claude
* manifest (which the publish workflow bumps in both files), while the
* description, `skills` pointer, and `interface` block are Codex-specific.
*/
const CLAUDE_DESCRIPTION_PREFIX = 'Claude Code skills';

function syncCodexManifest(): boolean {
const claude = JSON.parse(readFileSync(CLAUDE_MANIFEST_PATH, 'utf8'));
// Fail rather than let a reworded Claude description carry "Claude Code"
// into the Codex manifest, which a silent no-op replace would do.
if (!claude.description.startsWith(CLAUDE_DESCRIPTION_PREFIX)) {
throw new Error(
`Expected .claude-plugin/plugin.json description to start with ` +
`"${CLAUDE_DESCRIPTION_PREFIX}" so the Codex description can be derived ` +
`from it. Got: ${JSON.stringify(claude.description.slice(0, 60))}. ` +
`Update CLAUDE_DESCRIPTION_PREFIX in scripts/build-plugin.ts.`,
);
}
const codex = {
name: claude.name,
version: claude.version,
description:
'Skills' + claude.description.slice(CLAUDE_DESCRIPTION_PREFIX.length),
skills: './skills/',
author: claude.author,
homepage: claude.homepage,
repository: claude.repository,
license: claude.license,
interface: {
displayName: 'Boxel CLI',
shortDescription: 'Author and sync Boxel cards, realms, and workspaces',
longDescription:
'Create and edit Boxel cards, fields, and templates, then sync them ' +
'between local disk and a Boxel realm — with skills covering card ' +
'authoring, theming, catalog listings, federated search, and realm ' +
'indexing diagnostics.',
developerName: 'Cardstack',
category: 'Developer Tools',
capabilities: ['Interactive', 'Read', 'Write'],
defaultPrompt: [
'Create a Boxel card for the data I describe',
'Pull my Boxel workspace down so I can edit it locally',
],
websiteURL: 'https://boxel.ai',
},
};
const next = JSON.stringify(codex, null, 2) + '\n';
const prior = existsSync(CODEX_MANIFEST_PATH)
? readFileSync(CODEX_MANIFEST_PATH, 'utf8')
: null;
if (next === prior) return false;
mkdirSync(resolve(PLUGIN_DIR, '.codex-plugin'), { recursive: true });
writeFileSync(CODEX_MANIFEST_PATH, next);
return true;
}

function main(): void {
const program = buildBoxelProgram('0.0.0');
let changed = 0;
Expand All @@ -184,10 +250,14 @@ function main(): void {
console.log(`updated plugin/skills/${spec.skill}/SKILL.md`);
}
}
if (syncCodexManifest()) {
changed++;
console.log('updated plugin/.codex-plugin/plugin.json');
}
console.log(
changed === 0
? 'Plugin synopsis already up to date.'
: `Plugin synopsis regenerated (${changed} file${changed === 1 ? '' : 's'} changed).`,
? 'Plugin content already up to date.'
: `Plugin content regenerated (${changed} file${changed === 1 ? '' : 's'} changed).`,
);
}

Expand Down
Loading