Skip to content

cli: npx @simular-ai/box connect / doctor / remove - #4

Merged
chenchenSimular merged 5 commits into
mainfrom
cli-install
Nov 3, 2026
Merged

chenchenSimular merged 5 commits into
mainfrom
cli-install

Conversation

@chenchenSimular

@chenchenSimular chenchenSimular commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Closes #3

npx @simular-ai/box connect sets up box in every AI app on the machine. doctor checks the setup and remove undoes it. New folder cli/, published as @simular-ai/box with bin simular-box; box belongs to box.com's CLI.

Not published yet. Publishing uses npm trusted publishing and needs a one-time setup by repo and npm admins; see Before the first release.

What each app gets

App How Sign-in
Claude Code claude plugin marketplace add simular-ai/box-plugin + claude plugin install box@simular-box (reuses claude/: server and skill). With BOX_MCP_URL set: claude mcp add --scope user --transport http box <url>, and the plugin is disabled. Our own user-scope box from install.md is removed so box doesn't show twice OAuth (/mcp)
Codex [mcp_servers.box] url = … in $CODEX_HOME/config.toml, edited surgically: other tables, comments and our table's other settings and subtables (tool approvals, timeouts) are kept. Not codex mcp add: it starts the OAuth browser flow inline and would block OAuth (codex mcp login box)
Cursor mcpServers.box = {url} in ~/.cursor/mcp.json OAuth (Settings → MCP → Login)
OpenCode mcp.box = {type: remote, url, enabled} in opencode.jsonc if present, else opencode.json (XDG_CONFIG_HOME, OPENCODE_CONFIG) OAuth (opencode mcp auth box)
Hermes Agent mcp_servers.box in $HERMES_HOME/config.yaml, written directly because hermes mcp add is interactive and cancels without a TTY OAuth (auth: oauth); --auth key → header Bearer ${MCP_BOX_API_KEY}, key in $HERMES_HOME/.env
OpenClaw openclaw mcp set box '{url, transport: "streamable-http", …}': OpenClaw's own CLI, so its config journal stays consistent. Without transport, OpenClaw defaults to SSE OAuth (auth: oauth); --auth key → header Bearer ${BOX_TOKEN}, key in ~/.openclaw/.env
VS Code servers.box = {type: http, url} in the user mcp.json. Detected by its user-data folder, not code on PATH, because Cursor installs a code shim OAuth
Claude Desktop Nothing written: remote servers are connectors in the app. Prints the steps OAuth in the app

Windsurf was left out: it is now Devin Desktop, and its config moved.

Ownership

The CLI only changes or removes a server whose URL host is box by Simular: relay.simular.cloud, staging.relay.simular.cloud, mcp.box.simular.ai, or the host in BOX_MCP_URL.

  • Another server named box, such as box.com's https://mcp.box.com, stops connect with a message, and nothing is written.
  • remove leaves such a server alone and says so. doctor reports it.
  • --name <name> installs box under another name next to it.
  • The legacy box-control entry is removed only when it is ours.

Keys (--auth key)

  • The key comes only from BOX_TOKEN or a hidden prompt. It is rejected as an argument, so it never lands in shell history or ps.
  • It is written only to the app's .env (0600). The app config holds only a ${...} reference.
  • It is never printed; tests assert that.
  • No backup is kept of .env files, so rotating or removing a key leaves no old key on disk.

The two problems from #2

  • OpenClaw 401. OpenClaw substitutes ${VAR} from its own process environment and sends the placeholder literally when the variable is missing. A launchd or systemd Gateway never sees a shell export, but it loads <state dir>/.env at start. So the key goes there, and connect restarts a running Gateway when the key changes. doctor warns when the header references BOX_TOKEN but .env lacks it.
  • Hermes. No plugin folder is copied: the server goes into config.yaml, with the key name that Hermes's own mcp add uses. A running gateway is restarted (hermes gateway status / restart) after a change.

General behaviour

  • Every write is atomic: a temp file plus a rename, through symlinks to the real file. The file is backed up next to itself (*.box-backup-<time>).
  • A re-run that changes nothing writes nothing.
  • A file that doesn't parse is never touched.
  • JSONC, YAML and TOML comments survive, and so do CRLF line endings.
  • --dry-run previews changes.
  • Client CLIs run through cross-spawn, which quotes arguments for Windows .cmd shims.
  • BOX_MCP_URL overrides the single constant in src/constants.js. It must be https, except http for localhost.

doctor

doctor checks, for each app:

  • box is configured, points at the current URL, isn't duplicated or shadowed by a foreign box, and no legacy entry remains
  • the endpoint answers an unauthenticated POST with 401 and a WWW-Authenticate resource_metadata link whose document names an authorization server
  • a configured key is accepted (200) or rejected (invalid_token)

It prints a fix for each problem, supports --json, and exits 1 when something is wrong.

Dependencies: @clack/prompts 0.11 (1.x needs Node 20), cross-spawn, jsonc-parser, smol-toml (parse only; edits are surgical), yaml (its Document API keeps comments). Node >= 18. Plain ESM, no build step. Dev dependency: eslint.

CI: .github/workflows/publish-cli.yml

One workflow for checks and publishing, modeled on sai-sdk's publish-sai-mcp.yml.

  • Triggers: PRs touching cli/ or the plugin folders (paths filter; draft PRs run nothing), pushes to main, cli-v* tags, and manual dispatch.
  • A concurrency group cancels superseded runs. Actions are pinned by SHA, checkout uses persist-credentials: false, and fail-fast is false.
Job What it runs Secrets
check Lint (eslint --max-warnings 0), unit tests, smoke test and npm pack --dry-run, on ubuntu, macOS and windows (Node 24), plus ubuntu on Node 18 none
plugins claude plugin validate . and ./claude, npm pack ./dsh. hermes plugins validate ./hermes and openclaw plugins install/inspect/doctor ./openclaw run once those folders exist (PR #2); until then they skip with a message none
clients (Layer 1) Each real client, at the version pinned in cli/e2e/clients.json, on ubuntu, macOS and windows, in an isolated HOME. e2e/install-matrix.mjs seeds an unrelated server, runs connect --client X --yes, and asserts through the client's own listing that box is there and needs sign-in. It then re-runs (must be a no-op with no new backup), runs doctor --json (must be clean), runs remove, and asserts that box is gone and the seeded server survived none
live (Layer 2) e2e/live.mjs with BOX_MCP_URL=${{ vars.BOX_CI_MCP_URL }} and BOX_TOKEN=${{ secrets.BOX_CI_KEY }} (masked). Steps: a minimal MCP initialize + tools/list; connect --auth key for Hermes and OpenClaw; doctor (key valid); openclaw mcp probe box; hermes mcp test box. Tools are never called. Runs on pushes to main, tags, manual runs and same-repo PRs; never on fork PRs BOX_CI_KEY
publish Only on cli-v* tags, after check, plugins and clients. Runs in environment npm. Checks the tagged commit is on origin/main and the tag equals the cli/package.json version. A plain version gets dist-tag latest, -rc.N gets next. Then npm publish --provenance --access public through trusted publishing (OIDC, id-token: write, no registry-url, no npm token), and gh release create --generate-notes none

Clients that don't run everywhere:

  • Cursor is tested through cursor-agent, which ships for Linux and macOS only, so Windows is excluded.
  • On Windows, Codex finds its home through the OS profile API rather than USERPROFILE, so the matrix sets CODEX_HOME there. The CLI honours CODEX_HOME too.

Layer 2 can't reach staging from GitHub-hosted runners. staging.relay.simular.cloud resolves only to 10.16.0.11, an internal address. The job checks this first and skips with a notice; it doesn't fail or work around it. Choose one:

  • (a) Run live against prod https://relay.simular.cloud/mcp with a dedicated prod free-tier test key: set BOX_CI_MCP_URL and BOX_CI_KEY accordingly.
  • (b) A self-hosted runner inside the VPN.
  • (c) Expose a public staging MCP host.

The live path was exercised locally against a header-logging stand-in. openclaw mcp probe box listed the tool with the key read from .env, and hermes mcp test box showed "Connected" and "Tools discovered: 1".

TODO, Layer 3 (not in this PR): a nightly run against the latest client versions, plus a real agent task on a box.

Before the first release

Repo admin settings, which I did not change:

  1. Environment npm. Settings → Environments → New environment npm. Add required reviewers and restrict deployment to tags matching cli-v*.
  2. Tag ruleset. Settings → Rules → New tag ruleset targeting cli-v*. Restrict creation, update and deletion to maintainers, and block force pushes.

npm, by an owner of the @simular-ai org:
3. Publish 0.1.0 once by hand. npm can only attach a trusted publisher to a package that already exists. npm help trust: "Package must exist: The package you're configuring must already exist on the npm registry". From cli/ on main, with 2FA: npm publish --access public. This version has no provenance; later CI releases do.
4. Add the trusted publisher. npmjs.com → @simular-ai/box → Settings → Trusted Publisher, or with npm >= 11.10.0: npm trust github @simular-ai/box --repo simular-ai/box-plugin --file publish-cli.yml --env npm. Values: organization simular-ai, repository box-plugin, workflow publish-cli.yml, environment npm.
5. Push the next tag within 2 days. The docs say: "A newly created trusted publisher configuration must complete its first successful publish within 2 days", otherwise it expires.

Study (8–12 comparable CLIs)

Studied: add-mcp, Neon neonctl init, install-mcp, Smithery, Context7 ctx7 setup, mcpm.sh and Composio; also what Stripe, Sentry and Supabase ship. Details are in #3.

Adopted:

  • the app's own CLI where it is non-interactive
  • comment-preserving edits, and refusing to write files that don't parse
  • each app's override env var
  • OAuth by default, left to the app
  • key references in config, with the secret in the app's secrets file
  • --client, -y, and a TTY-only multiselect
  • removal of our entry only
  • a doctor that checks real state

Avoided:

  • treating an unparseable file as empty (Composio, mcpm)
  • writes with no backup or atomicity
  • parse-and-stringify TOML (add-mcp #122)
  • mcp-remote wrappers for apps that speak HTTP natively
  • keys in argv (neon-init)
  • single-app runs with no uninstall

Sources for each app's config

Tested

No real config was touched, no OAuth login was performed, and no real key was used; the fake key was cb_test_fake_key_123.

  • Unit tests: 34, passing locally on Node 24 and 18. They cover:
    • fixture-based merges for each client, with fake claude and openclaw CLIs
    • a foreign box for every client (connect refuses, remove leaves it, --name installs alongside)
    • Codex keeping comments above the next table, other settings and subtables, and CRLF
    • symlinked configs, and no .env backups after rotation or remove
    • refusal on broken or inline config; CODEX_HOME, BOX_MCP_URL and dry run
    • doctor never containing the key, and the real CLI spawned for connect, remove and key errors
    • on Windows, the tests that need shebang fake CLIs or POSIX modes are skipped; the real clients cover them there
  • Layer 1, locally on macOS: all six clients passed e2e/install-matrix.mjs with Claude Code 2.1.316, Codex 0.166.0, cursor-agent 2026.10.22, OpenCode 1.19.5, OpenClaw 2026.10.5 and Hermes 0.22.6. CI runs the pinned versions on all three OSes.
  • Interactive: driven in a pty: multiselect, the OAuth-or-key choice and the hidden key prompt. The key was not echoed.

Open questions

  • Layer 2 target: (a), (b) or (c) above.
  • Admin setup: the npm environment, the tag ruleset and the first manual publish.
  • Not verified:
    • an OpenClaw or Hermes gateway restart against a running gateway
    • whether newer OpenClaw refuses mcp set while the Gateway runs (connect then prints the stop/start steps)
    • the VS Code mcp.json path on a real VS Code install; the docs also describe ~/.copilot/mcp-config.json
  • Hermes and OpenClaw skill: once Add Hermes Agent plugin #2 merges, the CLI could also install the skills/box folder for both.
  • dsh is not in the CLI yet. It is key-only and has its own plugin (dsh v0.2.0: paste the box key on the Plugins page (Desktop) #1).
  • Default for Hermes and OpenClaw: should --yes default them to the personal key (better for 24/7 servers) instead of OAuth?

🤖 Generated with Claude Code

chenchenSimular and others added 5 commits November 3, 2026 07:47
Adds @simular-ai/box (bin simular-box), which detects Claude Code, Codex,
Cursor, OpenCode, Hermes, OpenClaw and VS Code, and writes the box MCP server
into each app's native config. It merges without clobbering other entries,
backs up each file and is idempotent. Claude Code reuses the claude/ plugin.
Every app signs in with OAuth; Hermes and OpenClaw can use a personal key
(--auth key) kept in the app's .env, with the gateway restarted when it
changes. doctor checks config, endpoint and key; remove undoes connect.
BOX_MCP_URL overrides the single URL constant. A cli-v* tag publishes to npm
with provenance.

Closes #3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…blish

- One workflow, publish-cli.yml, modeled on sai-sdk's publish-sai-mcp.yml. It
  replaces release-cli.yml and test-cli.yml:
  - check: lint, test and smoke on Linux, macOS, Windows, plus Node 18
  - plugins: each agent's validator over the plugin folders
  - clients (Layer 1): the real CLI against real Claude Code, Codex, Cursor
    (cursor-agent), OpenCode, Hermes and OpenClaw in an isolated HOME
  - live (Layer 2): a staging key proves Hermes and OpenClaw list box's tools;
    never runs on fork PRs
  - publish: only on cli-v* tags, through npm trusted publishing (OIDC, no
    token) with provenance, then a GitHub release
- Actions pinned by SHA, persist-credentials false, a draft guard and a
  concurrency group.
- Client versions are pinned in cli/e2e/clients.json.
- cross-spawn runs the client CLIs, so Windows .cmd shims get proper quoting.
  The home dir follows os.homedir() on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Unit-test sandbox sets USERPROFILE, APPDATA and LOCALAPPDATA. On Windows,
  libuv copies USERPROFILE from the parent into a child env that lacks it, so
  the spawned CLI was writing to the runner's real profile.
- The install matrix sets CODEX_HOME for Codex on Windows. Codex finds its home
  through the OS profile API and ignores USERPROFILE.
- TOML and JSONC edits keep the file's line endings. Removing the last TOML
  table keeps the final newline.
- Test fixtures are checked out with LF line endings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… gating

- connect and remove act only on servers whose URL host is box by Simular:
  relay.simular.cloud, the staging relay, mcp.box.simular.ai, or the host in
  BOX_MCP_URL.
  - Any other server named box (e.g. box.com's MCP server) stops connect with
    a clear message and is left alone by remove.
  - --name installs box under another name.
  - Legacy box-control entries are removed only if they are ours.
  - Applies to every client, including Claude Code's user scope.
- Codex keeps other settings and subtables of our own table, such as tool
  approvals and timeouts. When a table is removed, the comment and blank lines
  above the next table stay with it.
- Writes go through symlinks to the real file, so the link survives.
- .env files that hold keys get no backup, so rotation and remove leave no old
  key on disk.
- The publish job runs in the GitHub environment "npm" and refuses tags that
  are not on main.
- The live job skips with a notice when the endpoint is unreachable from the
  runner (staging resolves to a private address).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ALAPPDATA on Windows)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chenchenSimular
chenchenSimular merged commit 45018b8 into main Nov 3, 2026
48 checks passed
@chenchenSimular
chenchenSimular deleted the cli-install branch November 3, 2026 01:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

One-command install CLI: npx @simular-ai/box connect / doctor / remove

1 participant