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
170 changes: 27 additions & 143 deletions .github/workflows/publish-cli.yml → .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,51 +1,37 @@
name: Publish CLI
name: CI

# CI + npm publish for @simular-ai/box (cli/). Modeled on simular-ai/sai-sdk's publish-sai-mcp.yml.
# Checks this repo's plugins and the box CLI they use, against real clients. The CLI itself is the
# Rust simular-box, built and published to npm as @simular-ai/box from simular-ai/box-cli; this
# repo publishes nothing to npm (the old JavaScript CLI that lived in cli/ was retired).
#
# Every non-draft PR that touches the CLI or a plugin folder runs:
# check lint, unit tests, smoke test on Linux, macOS and Windows (plus Node 18 on Linux)
# plugins each agent's own validator over the plugin folders
# clients Layer 1: the real CLI against each real, installed client in an isolated HOME
# (versions pinned in cli/e2e/clients.json). No secrets.
# Every non-draft PR that touches a plugin folder or e2e/ runs:
# plugins each agent's own validator over the plugin folders, and the Claude plugin's CLI pin
# clients Layer 1: the pinned CLI against each real, installed client in an isolated HOME
# (versions pinned in e2e/clients.json). No secrets.
# live Layer 2: a real staging key (secret BOX_CI_KEY, variable BOX_CI_MCP_URL) proves the
# headless clients reach box and list its tools. Never on PRs from forks.
# Publishing happens only when a tag `cli-vX.Y.Z` (or `cli-vX.Y.Z-rc.N`, dist-tag "next") is pushed.
#
# Since @simular-ai/box 1.0.0 (the Rust CLI from simular-ai/box-cli) this repo no longer
# publishes the CLI: its npm trusted publisher was removed, so do not push cli-v* tags. The
# publish job below is kept only until cli/ is retired.
#
# Auth: npm trusted publishing (OIDC). The publish job's `id-token: write` lets the npm CLI swap
# a GitHub OIDC token for a short-lived publish token, so no long-lived npm token is stored.
# Requires the trusted-publisher config on npmjs.com (package @simular-ai/box -> Settings ->
# Trusted Publisher): organization "simular-ai", repository "box-plugin", workflow filename
# "publish-cli.yml", environment "npm". npm only lets you configure it on a package that already
# exists, so the very first version is published once by hand (see cli/README.md).
#
# Gating: the publish job runs in the GitHub environment "npm" (add required reviewers there)
# and refuses a tag whose commit is not on main. A tag ruleset on `cli-v*` limits who can push one.
# The repo is public, so npm attaches a provenance attestation.
# The CLI under test is `npx -y @simular-ai/box@X.Y.Z` at the version claude/ pins (e2e/cli.mjs).
#
# TODO(layer 3): nightly run against the latest client versions plus a real agent task on a box.

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
paths:
- 'cli/**'
- 'e2e/**'
- 'claude/**'
- 'dsh/**'
- 'hermes/**'
- 'openclaw/**'
- '.claude-plugin/**'
- '.github/workflows/publish-cli.yml'
- '.github/workflows/ci.yml'
push:
branches: [main]
paths:
- 'cli/**'
- '.github/workflows/publish-cli.yml'
tags:
- 'cli-v*'
- 'e2e/**'
- 'claude/**'
- '.claude-plugin/**'
- '.github/workflows/ci.yml'
workflow_dispatch:

concurrency:
Expand All @@ -60,43 +46,6 @@ defaults:
shell: bash

jobs:
check:
name: Check (${{ matrix.os }}, Node ${{ matrix.node }})
# Draft PRs run nothing; `ready_for_review` starts the checks. Non-PR events read `draft` as null.
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [24]
include:
- os: ubuntu-latest
node: 18
runs-on: ${{ matrix.os }}
timeout-minutes: 15
defaults:
run:
working-directory: cli
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ matrix.node }}
cache: npm
cache-dependency-path: cli/package-lock.json
- run: npm ci
- name: Lint
if: matrix.node == 24
run: npm run lint
- name: Unit tests
run: npm test
- name: Smoke test
run: npm run smoke
- name: Pack
run: npm pack --dry-run

plugins:
name: Validate plugin folders
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
Expand All @@ -110,12 +59,12 @@ jobs:
with:
node-version: 24
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
- name: Install validators (versions from cli/e2e/clients.json)
- name: Install validators (versions from e2e/clients.json)
run: |
set -euo pipefail
spec() { node -p "require('./cli/e2e/clients.json')['$1'].npm"; }
spec() { node -p "require('./e2e/clients.json')['$1'].npm"; }
npm install -g "$(spec claude-code)" "$(spec openclaw)"
tag=$(node -p "require('./cli/e2e/clients.json').hermes.tag")
tag=$(node -p "require('./e2e/clients.json').hermes.tag")
git clone --quiet --depth 1 --branch "$tag" https://github.com/NousResearch/hermes-agent "$RUNNER_TEMP/hermes"
(cd "$RUNNER_TEMP/hermes" && uv sync --frozen --extra mcp --quiet)
echo "$RUNNER_TEMP/hermes/.venv/bin" >> "$GITHUB_PATH"
Expand All @@ -127,8 +76,8 @@ jobs:
claude plugin validate ./claude
- name: Plugin pins the CLI to one exact version that is on npm
# A range (^1.0.0) lets npx run a version published after review; the Claude directory
# review asks for an exact pin. Since 1.0.0 the CLI is the Rust simular-box, released from
# simular-ai/box-cli, so the pin follows its npm releases, not cli/package.json.
# review asks for an exact pin. The CLI is the Rust simular-box, released from
# simular-ai/box-cli, so the pin follows its npm releases. e2e/ tests this same version.
run: |
set -euo pipefail
pins=$(grep -rhoE '@simular-ai/box@[^ `"]*' claude | sort -u)
Expand Down Expand Up @@ -177,15 +126,15 @@ jobs:
with:
node-version: 24
cache: npm
cache-dependency-path: cli/package-lock.json
cache-dependency-path: e2e/package-lock.json
- run: npm ci
working-directory: cli
working-directory: e2e
- name: Install ${{ matrix.client }}
env:
CLIENT: ${{ matrix.client }}
run: |
set -euo pipefail
get() { node -p "(require('./cli/e2e/clients.json')['$CLIENT'] || {})['$1'] || ''"; }
get() { node -p "(require('./e2e/clients.json')['$CLIENT'] || {})['$1'] || ''"; }
if [ -n "$(get npm)" ]; then
npm install -g "$(get npm)"
elif [ "$CLIENT" = hermes ]; then
Expand All @@ -208,7 +157,6 @@ jobs:
uv sync --frozen --extra mcp --quiet
if [ -d .venv/Scripts ]; then echo "$(cygpath -w "$PWD/.venv/Scripts")" >> "$GITHUB_PATH"; else echo "$PWD/.venv/bin" >> "$GITHUB_PATH"; fi
- name: connect, list, re-run, doctor, remove
working-directory: cli
run: node e2e/install-matrix.mjs ${{ matrix.client }}

live:
Expand All @@ -227,85 +175,21 @@ jobs:
with:
node-version: 24
cache: npm
cache-dependency-path: cli/package-lock.json
cache-dependency-path: e2e/package-lock.json
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
- run: npm ci
working-directory: cli
working-directory: e2e
- name: Install OpenClaw and Hermes
run: |
set -euo pipefail
npm install -g "$(node -p "require('./cli/e2e/clients.json').openclaw.npm")"
git clone --quiet --depth 1 --branch "$(node -p "require('./cli/e2e/clients.json').hermes.tag")" https://github.com/NousResearch/hermes-agent "$RUNNER_TEMP/hermes"
npm install -g "$(node -p "require('./e2e/clients.json').openclaw.npm")"
git clone --quiet --depth 1 --branch "$(node -p "require('./e2e/clients.json').hermes.tag")" https://github.com/NousResearch/hermes-agent "$RUNNER_TEMP/hermes"
(cd "$RUNNER_TEMP/hermes" && uv sync --frozen --extra mcp --quiet)
echo "$RUNNER_TEMP/hermes/.venv/bin" >> "$GITHUB_PATH"
- name: Key valid, clients connect and list box's tools
working-directory: cli
env:
BOX_MCP_URL: ${{ vars.BOX_CI_MCP_URL }}
BOX_TOKEN: ${{ secrets.BOX_CI_KEY }}
run: |
echo "::add-mask::$BOX_TOKEN"
node e2e/live.mjs hermes openclaw

publish:
name: Publish to npm
needs: [check, plugins, clients]
if: startsWith(github.ref, 'refs/tags/cli-v')
runs-on: ubuntu-latest
timeout-minutes: 15
environment: npm
permissions:
contents: write # create the GitHub Release
id-token: write # npm trusted publishing (OIDC) and provenance
defaults:
run:
working-directory: cli
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: Tagged commit is on main
run: |
set -euo pipefail
git fetch --quiet origin main
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "::error::$GITHUB_REF_NAME points at $GITHUB_SHA, which is not on main. Tag a commit on main." >&2
exit 1
fi
# No `registry-url`: setup-node would write an .npmrc with `_authToken=${NODE_AUTH_TOKEN}`,
# which shadows the OIDC handshake. Node 24 ships npm 11 (trusted publishing needs >= 11.5.1).
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 24
cache: npm
cache-dependency-path: cli/package-lock.json
- run: npm ci
- name: Verify tag matches package version and pick the dist-tag
id: release
run: |
set -euo pipefail
TAG_NAME="${GITHUB_REF#refs/tags/}"
TAG_VERSION="${TAG_NAME#cli-v}"
PKG_VERSION="$(node -p "require('./package.json').version")"
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "::error::Tag version ($TAG_VERSION) does not match cli/package.json ($PKG_VERSION)." >&2
exit 1
fi
if echo "$TAG_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "dist-tag=latest" >> "$GITHUB_OUTPUT"
elif echo "$TAG_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$'; then
echo "dist-tag=next" >> "$GITHUB_OUTPUT"
else
echo "::error::Tag '$TAG_NAME' is not a release tag (expected cli-vX.Y.Z or cli-vX.Y.Z-rc.N)." >&2
exit 1
fi
- name: Publish to npmjs.org
run: npm publish --provenance --access public --tag "${{ steps.release.outputs.dist-tag }}"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
TAG_NAME: ${{ github.ref_name }}
PRERELEASE_FLAG: ${{ steps.release.outputs.dist-tag == 'next' && '--prerelease' || '' }}
run: |
gh release create "$TAG_NAME" --title "$TAG_NAME" --generate-notes --latest=false $PRERELEASE_FLAG
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ Most agents need no plugin: add the MCP server `https://mcp.box.simular.ai/mcp`
| Claude Code and Cowork | [`claude/`](claude/) | OAuth, once in the browser | `/plugin marketplace add simular-ai/box-plugin`, then `/plugin install simular-box@simular-box` |
| DeepSeek Harness (dsh) | [`dsh/`](dsh/) | Personal key (`cb_...`), pasted on the plugin's page in Plugins | [dsh/README.md](dsh/README.md) |
| OpenClaw | [`openclaw/`](openclaw/) | OAuth, once from the plugin's page in Settings | `openclaw plugins install @simular-ai/openclaw-box` |
| Any of Claude Code, Codex, Cursor, OpenCode, Gemini CLI, Hermes, OpenClaw, VS Code (and steps for Claude Desktop) | [`cli/`](cli/) | OAuth; personal key with `--auth key` for Hermes and OpenClaw | `npx @simular-ai/box connect` (check with `npx @simular-ai/box doctor`) |
| Any of Claude Code, Codex, Cursor, OpenCode, Gemini CLI, Hermes, OpenClaw, VS Code (and steps for Claude Desktop) | [`@simular-ai/box`](https://www.npmjs.com/package/@simular-ai/box) (the box CLI, from [simular-ai/box-cli](https://github.com/simular-ai/box-cli)) | OAuth; personal key with `--auth key` for Hermes and OpenClaw | `npx @simular-ai/box connect` (check with `npx @simular-ai/box doctor`) |
| Hermes Agent | [`hermes/`](hermes/) | Personal key (`cb_...`) in `MCP_BOX_API_KEY`; OAuth optional | [hermes/README.md](hermes/README.md) |
| ChatGPT and Codex (OpenAI plugin directory) | [`openai/`](openai/) | OAuth, once in the browser | Listed by OpenAI after review; build the upload with `openai/build-zip.sh` ([openai/README.md](openai/README.md)) |

## Offload: move a coding task into your box and back

Going offline in the middle of a task? In Claude Code with the plugin, `/simular-box:offload` hands the task to a fresh Claude Code (or Codex, Cursor) session in your box, which keeps working while you are away; `/simular-box:back` brings its commits and its written hand-over home and merges them. Any other agent or a plain terminal uses `npx @simular-ai/box offload` and `npx @simular-ai/box back`. Setup and details: [cli/README.md](cli/README.md#offload).
Going offline in the middle of a task? In Claude Code with the plugin, `/simular-box:offload` hands the task to a fresh Claude Code (or Codex, Cursor) session in your box, which keeps working while you are away; `/simular-box:back` brings its commits and its written hand-over home and merges them. Any other agent or a plain terminal uses `npx @simular-ai/box offload` and `npx @simular-ai/box back`. Setup and details: [the CLI's README](https://www.npmjs.com/package/@simular-ai/box).

## Adding an agent

Expand All @@ -29,4 +29,4 @@ Add a folder named after the agent with its own manifest, MCP config and, where
- `openai/`: bump `version` in `openai/plugin.json` and `publication.release_notes`, run `openai/build-zip.sh`, upload the ZIP at platform.openai.com/plugins.
- `hermes/`: bump `version` in `hermes/plugin.json`. Users copy the folder by hand until box is in the Hermes plugin catalog.
- The Claude plugin's CLI pin: since 1.0.0, `@simular-ai/box` on npm is the Rust CLI released from [simular-ai/box-cli](https://github.com/simular-ai/box-cli). To move the plugin to a new CLI release, change the exact pin `npx -y @simular-ai/box@X.Y.Z` in `claude/skills/box-offload/SKILL.md` and bump `version` in `claude/.claude-plugin/plugin.json` in one PR (CI fails unless every pin under `claude/` names the same exact version and that version is on npm; never a range, because the Claude directory review requires that what runs is what it read).
- `cli/`: the JavaScript CLI (0.2.x). It no longer publishes to npm: its trusted publisher on `@simular-ai/box` was removed when the Rust 1.0.0 shipped, so do not push `cli-v*` tags. The endpoint and CLI changes now go to simular-ai/box-cli.
- The box CLI (`@simular-ai/box` on npm) is released from simular-ai/box-cli, not from here. `e2e/` tests the pinned version against each real client (versions in `e2e/clients.json`); see `.github/workflows/ci.yml`.
4 changes: 2 additions & 2 deletions claude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Most agents need no plugin: add the MCP server `https://mcp.box.simular.ai/mcp`
| Claude Code and Cowork | [`claude/`](https://github.com/simular-ai/box-plugin/tree/main/claude) | OAuth, once in the browser | `/plugin marketplace add simular-ai/box-plugin`, then `/plugin install simular-box@simular-box` |
| DeepSeek Harness (dsh) | [`dsh/`](https://github.com/simular-ai/box-plugin/tree/main/dsh) | Personal key (`cb_...`), pasted on the plugin's page in Plugins | [dsh/README.md](https://github.com/simular-ai/box-plugin/blob/main/dsh/README.md) |
| OpenClaw | [`openclaw/`](https://github.com/simular-ai/box-plugin/tree/main/openclaw) | OAuth, once from the plugin's page in Settings | `openclaw plugins install @simular-ai/openclaw-box` |
| Any of Claude Code, Codex, Cursor, OpenCode, Gemini CLI, Hermes, OpenClaw, VS Code (and steps for Claude Desktop) | [`cli/`](https://github.com/simular-ai/box-plugin/tree/main/cli) | OAuth; personal key with `--auth key` for Hermes and OpenClaw | `npx @simular-ai/box connect` (check with `npx @simular-ai/box doctor`) |
| Any of Claude Code, Codex, Cursor, OpenCode, Gemini CLI, Hermes, OpenClaw, VS Code (and steps for Claude Desktop) | [`@simular-ai/box`](https://www.npmjs.com/package/@simular-ai/box) (the box CLI, from [simular-ai/box-cli](https://github.com/simular-ai/box-cli)) | OAuth; personal key with `--auth key` for Hermes and OpenClaw | `npx @simular-ai/box connect` (check with `npx @simular-ai/box doctor`) |
| Hermes Agent | [`hermes/`](https://github.com/simular-ai/box-plugin/tree/main/hermes) | Personal key (`cb_...`) in `MCP_BOX_API_KEY`; OAuth optional | [hermes/README.md](https://github.com/simular-ai/box-plugin/blob/main/hermes/README.md) |
| ChatGPT and Codex (OpenAI plugin directory) | [`openai/`](https://github.com/simular-ai/box-plugin/tree/main/openai) | OAuth, once in the browser | Listed by OpenAI after review; build the upload with `openai/build-zip.sh` ([openai/README.md](https://github.com/simular-ai/box-plugin/blob/main/openai/README.md)) |

Expand Down Expand Up @@ -79,4 +79,4 @@ Add a folder named after the agent with its own manifest, MCP config and, where
- `openai/`: bump `version` in `openai/plugin.json` and `publication.release_notes`, run `openai/build-zip.sh`, upload the ZIP at platform.openai.com/plugins.
- `hermes/`: bump `version` in `hermes/plugin.json`. Users copy the folder by hand until box is in the Hermes plugin catalog.
- The Claude plugin's CLI pin: since 1.0.0, `@simular-ai/box` on npm is the Rust CLI released from [simular-ai/box-cli](https://github.com/simular-ai/box-cli). To move the plugin to a new CLI release, change the exact CLI version in every `npx -y` command in `claude/skills/box-offload/SKILL.md` and `claude/README.md`, and bump `version` in `claude/.claude-plugin/plugin.json`, in one PR (CI fails unless every pin under `claude/` names the same exact version and that version is on npm; never a range, because the Claude directory review requires that what runs is what it read).
- `cli/`: the JavaScript CLI (0.2.x). It no longer publishes to npm: its trusted publisher on `@simular-ai/box` was removed when the Rust 1.0.0 shipped, so do not push `cli-v*` tags. The endpoint and CLI changes now go to simular-ai/box-cli.
- The box CLI (`@simular-ai/box` on npm) is released from simular-ai/box-cli, not from here. `e2e/` tests the pinned version against each real client (versions in `e2e/clients.json`); see `.github/workflows/ci.yml`.
1 change: 0 additions & 1 deletion cli/.gitattributes

This file was deleted.

1 change: 0 additions & 1 deletion cli/.gitignore

This file was deleted.

Loading
Loading