From 3595b71f521b49312ed0e3ee045826de6ad75f1b Mon Sep 17 00:00:00 2001 From: chenchen Date: Wed, 4 Nov 2026 11:38:52 +0800 Subject: [PATCH 1/4] cli: Gemini CLI support, clear permission errors, Codex hint without the CLI - gemini-cli client on our JSON writer: ~/.gemini/settings.json, mcpServers.box = {type: "http", url}; connect, doctor and remove, with the same foreign-server guard, backups and box-control cleanup. The next hint is `/mcp auth box` in a trusted folder. - When a config can't be written (EACCES/EPERM), connect says so; for Codex it prints the steps to add box in the Codex app's Settings. - The Codex sign-in hint points at the app when `codex` is not on PATH. - gemini-cli joins the real-client install matrix on Linux, macOS and Windows (@google/gemini-cli@0.66.0). Closes #16 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish-cli.yml | 2 +- README.md | 2 +- cli/README.md | 5 +++-- cli/e2e/clients.json | 1 + cli/e2e/install-matrix.mjs | 9 +++++++++ cli/e2e/smoke.mjs | 2 +- cli/package.json | 3 ++- cli/src/cli.js | 10 ++++++++++ cli/src/clients/codex.js | 16 +++++++++++++++- cli/src/clients/gemini-cli.js | 21 +++++++++++++++++++++ cli/src/clients/index.js | 3 ++- cli/test/cli.test.js | 20 +++++++++++++++++++- cli/test/clients.test.js | 31 ++++++++++++++++++++++++++++++- 13 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 cli/src/clients/gemini-cli.js diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index cc67c2c..34dc8d3 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -148,7 +148,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - client: [claude-code, codex, cursor, opencode, hermes, openclaw] + client: [claude-code, codex, cursor, opencode, gemini-cli, hermes, openclaw] exclude: # cursor-agent ships for Linux and macOS only. - os: windows-latest diff --git a/README.md b/README.md index 241bb3a..ee75146 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Most agents need no plugin: add the MCP server `https://relay.simular.cloud/mcp` | --- | --- | --- | --- | | Claude Code and Cowork | [`claude/`](claude/) | OAuth, once in the browser | `/plugin marketplace add simular-ai/box-plugin`, then `/plugin install 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) | -| Any of Claude Code, Codex, Cursor, OpenCode, 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) | [`cli/`](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) | ## Adding an agent diff --git a/cli/README.md b/cli/README.md index 69fb8ee..bc67cbd 100644 --- a/cli/README.md +++ b/cli/README.md @@ -20,8 +20,9 @@ The command is `simular-box` (`box` belongs to box.com's CLI), so after `npm i - | App | `--client` | What connect does | Sign-in | | --- | --- | --- | --- | | Claude Code | `claude-code` | Installs the [`claude/`](../claude/) plugin (box server + box skill) with `claude plugin install box@simular-box`. With `BOX_MCP_URL` set, `claude mcp add --scope user --transport http box ` instead | OAuth: `/mcp` → box → Authenticate | -| Codex | `codex` | Writes `[mcp_servers.box]` to `$CODEX_HOME/config.toml` (default `~/.codex`) | OAuth: `codex mcp login box` | +| Codex | `codex` | Writes `[mcp_servers.box]` to `$CODEX_HOME/config.toml` (default `~/.codex`) | OAuth: `codex mcp login box`, or Settings → MCP servers → Authenticate in the Codex app | | Cursor | `cursor` | Merges `mcpServers.box` into `~/.cursor/mcp.json` | OAuth: Settings → MCP → Login | +| Gemini CLI | `gemini-cli` | Merges `mcpServers.box` (`type: http`) into `~/.gemini/settings.json` | OAuth: `/mcp auth box` (in a trusted folder) | | OpenCode | `opencode` | Merges `mcp.box` (`type: remote`) into `~/.config/opencode/opencode.json(c)` | OAuth: `opencode mcp auth box` | | Hermes Agent | `hermes` | Writes `mcp_servers.box` to `$HERMES_HOME/config.yaml` (default `~/.hermes`); restarts a running gateway | OAuth (`hermes mcp login box`) or `--auth key` | | OpenClaw | `openclaw` | `openclaw mcp set box …` with `transport: streamable-http`; restarts a running Gateway when the key changes | OAuth (`openclaw mcp login box`) or `--auth key` | @@ -30,7 +31,7 @@ The command is `simular-box` (`box` belongs to box.com's CLI), so after `npm i - Pick apps without prompts with `--client cursor,codex --yes`; `--yes` alone picks every app found. `--dry-run` shows what would change. -The CLI only changes or removes a server that points at box by Simular (`relay.simular.cloud`, its staging host, `mcp.box.simular.ai`, or your `BOX_MCP_URL`). If an app already has a different server named `box`, such as box.com's MCP server, `connect` stops without changing anything, and `remove` leaves it alone. To add box next to it, use `--name simular-box`. A symlinked config stays a symlink: the file it points to is updated. +The CLI only changes or removes a server that points at box by Simular (`relay.simular.cloud`, its staging host, `mcp.box.simular.ai`, or your `BOX_MCP_URL`). If an app already has a different server named `box`, such as box.com's MCP server, `connect` stops without changing anything, and `remove` leaves it alone. To add box next to it, use `--name simular-box`. A symlinked config stays a symlink: the file it points to is updated. If a config can't be written (owned by another user, or locked by the app), connect says so; for Codex it prints the steps to add box in the Codex app's Settings instead. ## Sign-in diff --git a/cli/e2e/clients.json b/cli/e2e/clients.json index ebc259c..6c9659e 100644 --- a/cli/e2e/clients.json +++ b/cli/e2e/clients.json @@ -3,6 +3,7 @@ "claude-code": { "npm": "@anthropic-ai/claude-code@2.1.316" }, "codex": { "npm": "@openai/codex@0.166.0" }, "opencode": { "npm": "opencode-ai@1.19.5" }, + "gemini-cli": { "npm": "@google/gemini-cli@0.66.0" }, "openclaw": { "npm": "openclaw@2026.10.9" }, "hermes": { "git": "https://github.com/NousResearch/hermes-agent", "tag": "v0.22.6" }, "cursor": { "cursorAgent": "2026.10.22-27dcf5f" } diff --git a/cli/e2e/install-matrix.mjs b/cli/e2e/install-matrix.mjs index 6caf83c..243eff0 100644 --- a/cli/e2e/install-matrix.mjs +++ b/cli/e2e/install-matrix.mjs @@ -97,6 +97,15 @@ const ADAPTERS = { present: (out) => /\bbox\s+https:\/\/relay\.simular/.test(out), otherKept: (out) => /\bother\b/.test(out), }, + 'gemini-cli': { + bin: 'gemini', + seed: () => writeJson(client.configLocation(ctx), { mcpServers: { other: { httpUrl: 'https://example.com/mcp' } } }), + // The isolated HOME is not a trusted folder, so Gemini lists the servers as Disabled and + // never connects; the listing still proves it reads our entry as an http server. + list: () => sh('gemini', ['mcp', 'list']), + present: (out) => /\bbox: https:\/\/relay\.simular\.cloud\/mcp \(http\)/.test(out), + otherKept: (out) => /\bother: https:\/\/example\.com\/mcp/.test(out), + }, cursor: { bin: 'cursor-agent', seed: () => writeJson(client.configLocation(ctx), { mcpServers: { other: { url: 'https://example.com/mcp' } } }), diff --git a/cli/e2e/smoke.mjs b/cli/e2e/smoke.mjs index 1f25a5a..1ba95ad 100644 --- a/cli/e2e/smoke.mjs +++ b/cli/e2e/smoke.mjs @@ -22,7 +22,7 @@ assert.equal(r.status, 0) assert.match(r.stdout, /npx @simular-ai\/box connect/) const before = fs.readdirSync(home).sort() -r = run('connect', '--client', 'cursor,codex,opencode,vscode,hermes', '--yes', '--dry-run') +r = run('connect', '--client', 'cursor,codex,opencode,vscode,gemini-cli,hermes', '--yes', '--dry-run') assert.equal(r.status, 0, r.stderr) assert.match(r.stdout, /would write/) assert.deepEqual(fs.readdirSync(home).sort(), before, 'dry run writes nothing') diff --git a/cli/package.json b/cli/package.json index 13d8f73..7399a88 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,7 +1,7 @@ { "name": "@simular-ai/box", "version": "0.1.2", - "description": "Connect box by Simular (a cloud computer your AI agent drives over MCP) to Claude Code, Codex, Cursor, OpenCode, Hermes, OpenClaw and more in one command.", + "description": "Connect box by Simular (a cloud computer your AI agent drives over MCP) to Claude Code, Codex, Cursor, OpenCode, Gemini CLI, Hermes, OpenClaw and more in one command.", "license": "MIT", "type": "module", "bin": { @@ -45,6 +45,7 @@ "codex", "cursor", "opencode", + "gemini-cli", "hermes", "openclaw" ], diff --git a/cli/src/cli.js b/cli/src/cli.js index fb23af7..c8812ad 100644 --- a/cli/src/cli.js +++ b/cli/src/cli.js @@ -192,6 +192,11 @@ async function connect(ctx, flags, { interactive, env, isTTY }) { failed++ spin.stop(`${client.name}: failed`, 1) p.log.error(` ${err.message}`) + if (isPermissionError(err)) { + const where = err.path ? ` ${err.path}` : ' its config' + p.log.warn(` box could not write${where}: permission denied. The file may belong to another user (for example after a run with sudo) or be locked by the app; check its owner, or quit the app and run again.`) + for (const n of client.permissionHelp?.(ctx) || []) next.push(`${client.name}: ${n}`) + } } } if (next.length) p.note(next.join('\n'), 'Next') @@ -202,6 +207,11 @@ async function connect(ctx, flags, { interactive, env, isTTY }) { return failed ? 1 : 0 } +/** EACCES/EPERM from Node, or the same reported as text (add-mcp, Windows "Access is denied"). */ +export function isPermissionError(err) { + return ['EACCES', 'EPERM'].includes(err?.code) || /\bEACCES\b|\bEPERM\b|permission denied|access is denied|operation not permitted/i.test(err?.message || '') +} + async function remove(ctx, flags, { interactive }) { p.intro(`Remove box ${ctx.dryRun ? '(dry run)' : ''}`) let clients = selectedFromFlags(flags) diff --git a/cli/src/clients/codex.js b/cli/src/clients/codex.js index dd4b6ad..6f4afe5 100644 --- a/cli/src/clients/codex.js +++ b/cli/src/clients/codex.js @@ -14,6 +14,12 @@ const file = (ctx) => path.join(home(ctx), 'config.toml') const servers = (ctx) => parseToml(readText(file(ctx)), file(ctx)).mcp_servers || {} const ourLegacy = (ctx, map) => LEGACY_SERVER_NAMES.filter((n) => map[n] && ctx.ours(map[n].url)) +// The Codex app (ChatGPT desktop) reads the same config.toml but ships no `codex` on PATH. +function signInHint(ctx) { + if (ctx.which('codex')) return `Run \`codex mcp login ${ctx.name}\`, sign in in the browser and click Approve.` + return `Restart the Codex app, open Settings -> MCP servers and click Authenticate next to ${ctx.name}; sign in in the browser and click Approve.` +} + export default { id: 'codex', name: 'Codex', @@ -56,10 +62,18 @@ export default { return { actions: res.changed ? [`${ctx.dryRun ? 'would write' : 'wrote'} [mcp_servers.${ctx.name}] to ${f}`] : [], backups: res.backup ? [res.backup] : [], - next: [`Run \`codex mcp login ${ctx.name}\`, sign in in the browser and click Approve.`], + next: [signInHint(ctx)], } }, + /** Shown when config.toml cannot be written (EACCES/EPERM, "Access is denied" on Windows). */ + permissionHelp(ctx) { + return [ + `Add box in the Codex app instead: Settings -> MCP servers -> Add server, name it ${ctx.name}, choose Streamable HTTP, paste ${ctx.url} as the URL and click Save.`, + `Restart Codex, then click Authenticate next to ${ctx.name}, sign in in the browser and click Approve.`, + ] + }, + remove(ctx) { const f = file(ctx) const map = servers(ctx) diff --git a/cli/src/clients/gemini-cli.js b/cli/src/clients/gemini-cli.js new file mode 100644 index 0000000..9200e2c --- /dev/null +++ b/cli/src/clients/gemini-cli.js @@ -0,0 +1,21 @@ +// Gemini CLI: ~/.gemini/settings.json, `mcpServers`, `{ type: "http", url }` (streamable HTTP). +// On a 401 Gemini CLI marks the server as needing OAuth; `/mcp auth box` discovers the sign-in +// from the WWW-Authenticate header and runs it. MCP servers load only in a trusted folder. +// Docs: https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md +import path from 'node:path' +import { jsonClient } from './json-client.js' + +export default jsonClient({ + id: 'gemini-cli', + name: 'Gemini CLI', + auth: 'oauth', + aliases: ['gemini'], + file: (ctx) => path.join(ctx.home, '.gemini', 'settings.json'), + container: ['mcpServers'], + entry: (ctx) => ({ type: 'http', url: ctx.url }), + // Gemini CLI also reads `httpUrl` (its older field for streamable HTTP). + entryUrl: (e) => e?.url ?? e?.httpUrl, + bins: ['gemini'], + dirs: (ctx) => [path.join(ctx.home, '.gemini')], + next: (ctx) => [`Start Gemini CLI in a trusted folder, run \`/mcp auth ${ctx.name}\`, sign in in the browser and click Approve.`], +}) diff --git a/cli/src/clients/index.js b/cli/src/clients/index.js index 0744fde..c5e5ad8 100644 --- a/cli/src/clients/index.js +++ b/cli/src/clients/index.js @@ -2,12 +2,13 @@ import claudeCode from './claude-code.js' import claudeDesktop from './claude-desktop.js' import codex from './codex.js' import cursor from './cursor.js' +import geminiCli from './gemini-cli.js' import hermes from './hermes.js' import openclaw from './openclaw.js' import opencode from './opencode.js' import vscode from './vscode.js' -export const CLIENTS = [claudeCode, codex, cursor, opencode, hermes, openclaw, vscode, claudeDesktop] +export const CLIENTS = [claudeCode, codex, cursor, opencode, geminiCli, hermes, openclaw, vscode, claudeDesktop] const ALIASES = { claude: 'claude-code', 'claude-code-cli': 'claude-code' } diff --git a/cli/test/cli.test.js b/cli/test/cli.test.js index ee8bbb4..c827ddd 100644 --- a/cli/test/cli.test.js +++ b/cli/test/cli.test.js @@ -79,7 +79,9 @@ test('real CLI: connect --client cursor,codex --yes in a temp HOME, then remove' const sb = sandbox() const res = run(['connect', '--client', 'cursor,codex', '--yes'], sb) assert.equal(res.status, 0, res.stderr + res.stdout) - assert.match(res.stdout, /codex mcp login box/) + // No `codex` on PATH (the Codex app alone): the hint points at the app's Settings. + assert.match(res.stdout, /Settings -> MCP servers and click Authenticate next to box/) + assert.ok(!res.stdout.includes('codex mcp login'), 'does not assume the codex CLI') assert.match(res.stdout, /start a new chat \(or restart the app\)/) for (const e of EXAMPLE_PROMPTS) assert.ok(res.stdout.includes(e), `shows the example prompt: ${e}`) assert.deepEqual(JSON.parse(sb.read('.cursor/mcp.json')), { mcpServers: { box: { url: DEFAULT_MCP_URL } } }) @@ -127,3 +129,19 @@ test('real CLI: example prompts only after something was set up', () => { const doctor = run(['doctor', '--client', 'cursor'], sb) assert.ok(!doctor.stdout.includes(EXAMPLE_PROMPTS[0]), 'doctor shows no example prompts') }) + +test('real CLI: a config it may not write gets a clear message (and the Codex app steps)', { skip: (process.platform === 'win32' || process.getuid?.() === 0) && 'needs POSIX permissions and a non-root user' }, () => { + const sb = sandbox() + sb.write('.codex/config.toml', 'model = "gpt-5"\n') + fs.chmodSync(path.join(sb.home, '.codex'), 0o500) + try { + const res = run(['connect', '--client', 'codex', '--yes'], sb) + assert.notEqual(res.status, 0) + const out = res.stdout + res.stderr + assert.match(out, /permission denied/) + assert.match(out, /Settings -> MCP servers -> Add server, name it box, choose Streamable HTTP, paste https:\/\/relay\.simular\.cloud\/mcp/) + assert.match(out, /click Authenticate next to box/) + } finally { + fs.chmodSync(path.join(sb.home, '.codex'), 0o700) + } +}) diff --git a/cli/test/clients.test.js b/cli/test/clients.test.js index 643a368..6d376b3 100644 --- a/cli/test/clients.test.js +++ b/cli/test/clients.test.js @@ -114,6 +114,34 @@ test('vscode: writes servers.box with type http next to other servers', () => { assert.deepEqual(doc.inputs, []) }) +test('gemini-cli: merges mcpServers.box as an http server, keeps comments and settings, idempotent, removes', () => { + const sb = sandbox() + const file = sb.write('.gemini/settings.json', '{\n // mine\n "theme": "Default",\n "mcpServers": { "other": { "httpUrl": "https://example.com/mcp" } }\n}\n') + const res = client('gemini').connect(sb.ctx) + assert.match(res.next[0], /\/mcp auth box/) + assert.equal(res.backups.length, 1) + const text = sb.read('.gemini/settings.json') + assert.match(text, /\/\/ mine/) + const doc = jsonc.parse(text) + assert.deepEqual(doc.mcpServers.box, { type: 'http', url: DEFAULT_MCP_URL }) + assert.equal(doc.theme, 'Default') + assert.deepEqual(client('gemini').connect(sb.ctx).actions, []) + assert.equal(backups(path.dirname(file)).length, 1, 'a no-op re-run makes no new backup') + assert.equal(client('gemini-cli').status(sb.ctx).entries[0].url, DEFAULT_MCP_URL) + client('gemini-cli').remove(sb.ctx) + assert.deepEqual(Object.keys(jsonc.parse(sb.read('.gemini/settings.json')).mcpServers), ['other']) +}) + +test('gemini-cli: a foreign box given as httpUrl is recognised and left alone', () => { + const sb = sandbox() + const original = JSON.stringify({ mcpServers: { box: { httpUrl: 'https://mcp.box.com' } } }, null, 2) + sb.write('.gemini/settings.json', original) + assert.throws(() => client('gemini-cli').connect(sb.ctx), (e) => e.foreign) + assert.equal(client('gemini-cli').status(sb.ctx).foreign.url, 'https://mcp.box.com') + client('gemini-cli').remove(sb.ctx) + assert.equal(sb.read('.gemini/settings.json'), original) +}) + test('hermes: OAuth entry keeps comments; key mode puts the key only in .env (0600)', () => { const sb = sandbox() sb.write('.hermes/config.yaml', fixture('hermes.before.yaml')) @@ -208,7 +236,7 @@ test('claude-desktop: writes nothing, prints connector steps', () => { test('dry run writes nothing for file-based clients', () => { const sb = sandbox() const ctx = { ...sb.ctx, dryRun: true } - for (const id of ['cursor', 'codex', 'opencode', 'vscode', 'hermes']) { + for (const id of ['cursor', 'codex', 'opencode', 'vscode', 'gemini-cli', 'hermes']) { const res = client(id).connect(ctx, { mode: 'oauth' }) assert.ok(res.actions[0].startsWith('would'), id) } @@ -224,6 +252,7 @@ test('json clients: a foreign box blocks connect, survives remove, and --name in ['cursor', '.cursor/mcp.json', ['mcpServers']], ['opencode', '.config/opencode/opencode.json', ['mcp']], ['vscode', '.config/Code/User/mcp.json', ['servers']], + ['gemini-cli', '.gemini/settings.json', ['mcpServers']], ] for (const [id, rel, container] of cases) { const sb = sandbox() From 7180cf3857b75f8dea2113b5247398f0f4cceac6 Mon Sep 17 00:00:00 2001 From: chenchen Date: Wed, 4 Nov 2026 11:42:33 +0800 Subject: [PATCH 2/4] cli: name the config (not a temp file) when it can't be changed; cover EBUSY From review of #17: the permission message printed err.path, which can be a temp file or a file whose folder is the real problem. It now names the client's config and says the file or its folder may belong to another user. EBUSY (Windows, app holds the file) gets the same help. The Codex app steps are offered for the case where the app holds the file. Refs #16 Co-Authored-By: Claude Opus 5.5 --- cli/src/cli.js | 8 ++++---- cli/src/clients/codex.js | 2 +- cli/test/cli.test.js | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/src/cli.js b/cli/src/cli.js index c8812ad..cb8f3c4 100644 --- a/cli/src/cli.js +++ b/cli/src/cli.js @@ -193,8 +193,8 @@ async function connect(ctx, flags, { interactive, env, isTTY }) { spin.stop(`${client.name}: failed`, 1) p.log.error(` ${err.message}`) if (isPermissionError(err)) { - const where = err.path ? ` ${err.path}` : ' its config' - p.log.warn(` box could not write${where}: permission denied. The file may belong to another user (for example after a run with sudo) or be locked by the app; check its owner, or quit the app and run again.`) + const where = safe(() => client.configLocation(ctx), 'its config') + p.log.warn(` box could not change ${where}: the file or its folder may belong to another user (for example after a run with sudo), or the app may hold it open. Check the owner of both, or quit the app, then run again.`) for (const n of client.permissionHelp?.(ctx) || []) next.push(`${client.name}: ${n}`) } } @@ -207,9 +207,9 @@ async function connect(ctx, flags, { interactive, env, isTTY }) { return failed ? 1 : 0 } -/** EACCES/EPERM from Node, or the same reported as text (add-mcp, Windows "Access is denied"). */ +/** EACCES/EPERM, or EBUSY (Windows: the app holds the file open), including as text ("Access is denied"). */ export function isPermissionError(err) { - return ['EACCES', 'EPERM'].includes(err?.code) || /\bEACCES\b|\bEPERM\b|permission denied|access is denied|operation not permitted/i.test(err?.message || '') + return ['EACCES', 'EPERM', 'EBUSY'].includes(err?.code) || /\bEACCES\b|\bEPERM\b|\bEBUSY\b|permission denied|access is denied|operation not permitted/i.test(err?.message || '') } async function remove(ctx, flags, { interactive }) { diff --git a/cli/src/clients/codex.js b/cli/src/clients/codex.js index 6f4afe5..4c42944 100644 --- a/cli/src/clients/codex.js +++ b/cli/src/clients/codex.js @@ -69,7 +69,7 @@ export default { /** Shown when config.toml cannot be written (EACCES/EPERM, "Access is denied" on Windows). */ permissionHelp(ctx) { return [ - `Add box in the Codex app instead: Settings -> MCP servers -> Add server, name it ${ctx.name}, choose Streamable HTTP, paste ${ctx.url} as the URL and click Save.`, + `If the Codex app holds the file, add box there instead: Settings -> MCP servers -> Add server, name it ${ctx.name}, choose Streamable HTTP, paste ${ctx.url} as the URL and click Save.`, `Restart Codex, then click Authenticate next to ${ctx.name}, sign in in the browser and click Approve.`, ] }, diff --git a/cli/test/cli.test.js b/cli/test/cli.test.js index c827ddd..c63b0e3 100644 --- a/cli/test/cli.test.js +++ b/cli/test/cli.test.js @@ -138,7 +138,7 @@ test('real CLI: a config it may not write gets a clear message (and the Codex ap const res = run(['connect', '--client', 'codex', '--yes'], sb) assert.notEqual(res.status, 0) const out = res.stdout + res.stderr - assert.match(out, /permission denied/) + assert.match(out, /could not change .*config\.toml: the file or its folder/) assert.match(out, /Settings -> MCP servers -> Add server, name it box, choose Streamable HTTP, paste https:\/\/relay\.simular\.cloud\/mcp/) assert.match(out, /click Authenticate next to box/) } finally { From b09edcd214f48f44325576ec81e44568697a76b5 Mon Sep 17 00:00:00 2001 From: chenchen Date: Wed, 4 Nov 2026 12:03:07 +0800 Subject: [PATCH 3/4] Release @simular-ai/box 0.1.3 (Gemini CLI) Co-Authored-By: Claude Opus 5.5 --- cli/package-lock.json | 4 ++-- cli/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 0850686..f54aecc 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@simular-ai/box", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@simular-ai/box", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "dependencies": { "@clack/prompts": "0.11.0", diff --git a/cli/package.json b/cli/package.json index 7399a88..9537df6 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@simular-ai/box", - "version": "0.1.2", + "version": "0.1.3", "description": "Connect box by Simular (a cloud computer your AI agent drives over MCP) to Claude Code, Codex, Cursor, OpenCode, Gemini CLI, Hermes, OpenClaw and more in one command.", "license": "MIT", "type": "module", From ab377f24ca7142db1b28cd04f6cb478b28504f9e Mon Sep 17 00:00:00 2001 From: Chenchen Date: Wed, 4 Nov 2026 12:53:35 +0800 Subject: [PATCH 4/4] Move box to https://mcp.box.simular.ai/mcp and migrate old relay entries (#19) * Move box to https://mcp.box.simular.ai/mcp and migrate old relay entries The box MCP server moved from relay.simular.cloud to mcp.box.simular.ai. The old address now returns protected-resource metadata naming the new URL, so MCP clients refuse to sign in through it. - CLI default URL, Claude Code / Hermes / dsh plugin configs and docs point at the new URL; plugin versions bumped (claude 0.1.1, hermes 0.1.1, dsh 0.2.1). - connect rewrites a box entry at the old address (it stays "ours"), and says the app may ask to sign in again. For Claude Code it runs `claude plugin marketplace update` + `claude plugin update` when the installed plugin still points at the old URL. - doctor flags an entry at the old address, and an endpoint whose metadata names a different resource. - Install matrix seeds a box at the old address for every client and, for Claude Code, installs the old plugin from a local git marketplace built from this checkout, so it tests this branch's plugin and the update path. Refs #18 Co-Authored-By: Claude Opus 5.5 * cli: check the Claude Code plugin's URL after updating it Review follow-up: if the marketplace still serves the old .mcp.json (a fork or pinned ref), connect now fails with the fix instead of reporting success and re-running the update on every connect. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- README.md | 8 +-- claude/.claude-plugin/plugin.json | 2 +- claude/.mcp.json | 2 +- cli/README.md | 10 ++-- cli/e2e/install-matrix.mjs | 81 ++++++++++++++++++++++++------ cli/src/cli.js | 17 ++++++- cli/src/clients/claude-code.js | 30 +++++++++-- cli/src/constants.js | 25 +++++++-- cli/src/probe.js | 11 ++++ cli/test/cli.test.js | 52 +++++++++++++++++-- cli/test/clients.test.js | 33 +++++++++++- cli/test/fixtures/codex.after.toml | 2 +- cli/test/helpers.js | 8 ++- dsh/README.md | 4 +- dsh/cordis.patch.yml | 2 +- dsh/package.json | 2 +- hermes/README.md | 4 +- hermes/mcp.json | 2 +- hermes/plugin.json | 2 +- 19 files changed, 245 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index ee75146..5a5df3e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [box](https://box.simular.ai) gives your AI agent a computer of its own: a cloud Linux desktop with Chrome and a shell that it drives over MCP, while your own computer stays free. You can watch it live and take over at https://box.simular.ai/box. -Most agents need no plugin: add the MCP server `https://relay.simular.cloud/mcp` and sign in once (see https://box.simular.ai/install). This repository holds a package for each agent that installs box its own way. Each folder stands alone and is released through that agent's channel. +Most agents need no plugin: add the MCP server `https://mcp.box.simular.ai/mcp` and sign in once (see https://box.simular.ai/install). This repository holds a package for each agent that installs box its own way. Each folder stands alone and is released through that agent's channel. | Agent | Folder | Sign-in | Install | | --- | --- | --- | --- | @@ -13,11 +13,11 @@ Most agents need no plugin: add the MCP server `https://relay.simular.cloud/mcp` ## Adding an agent -Add a folder named after the agent with its own manifest, MCP config and, where the agent supports skills, a copy of `claude/skills/box/SKILL.md`. Point it at `https://relay.simular.cloud/mcp`. Use OAuth sign-in where the agent supports it; otherwise read a personal key from an environment variable or the agent's own credentials store, never from a file in the package. Add a row above. +Add a folder named after the agent with its own manifest, MCP config and, where the agent supports skills, a copy of `claude/skills/box/SKILL.md`. Point it at `https://mcp.box.simular.ai/mcp`. Use OAuth sign-in where the agent supports it; otherwise read a personal key from an environment variable or the agent's own credentials store, never from a file in the package. Add a row above. ## Releasing -- `claude/`: bump `version` in `claude/.claude-plugin/plugin.json`; Claude Code picks it up from the marketplace. +- `claude/`: bump `version` in `claude/.claude-plugin/plugin.json`. Claude Code doesn't auto-update third-party marketplaces: users get it with `/plugin marketplace update simular-box` and `/plugin update box@simular-box`, or with `npx @simular-ai/box connect`. - `dsh/`: bump `version` in `dsh/package.json`, then push a tag `dsh-vX.Y.Z`. The release workflow attaches `dsh-box.tgz`. - `hermes/`: bump `version` in `hermes/plugin.json`. Users copy the folder by hand until box is in the Hermes plugin catalog. -- `cli/`: bump `version` in `cli/package.json`, then push a tag `cli-vX.Y.Z`. The `publish-cli.yml` workflow tests it and publishes `@simular-ai/box` to npm through trusted publishing (OIDC), with provenance; the one-time npm setup is in [cli/README.md](cli/README.md#releasing). When the box endpoint moves, change `DEFAULT_MCP_URL` in `cli/src/constants.js` and release. +- `cli/`: bump `version` in `cli/package.json`, then push a tag `cli-vX.Y.Z`. The `publish-cli.yml` workflow tests it and publishes `@simular-ai/box` to npm through trusted publishing (OIDC), with provenance; the one-time npm setup is in [cli/README.md](cli/README.md#releasing). When the box endpoint moves, change `DEFAULT_MCP_URL` in `cli/src/constants.js`, add the old host to `OLD_HOSTS` there, change the URL in every plugin folder, and release each one. diff --git a/claude/.claude-plugin/plugin.json b/claude/.claude-plugin/plugin.json index 1f6ac39..b512aab 100644 --- a/claude/.claude-plugin/plugin.json +++ b/claude/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "box", "displayName": "box by Simular", - "version": "0.1.0", + "version": "0.1.1", "description": "Give Claude a computer of its own: a cloud Linux desktop with Chrome and a shell that it drives over MCP, while your own computer stays free. You sign in once in the browser; no key to paste.", "author": { "name": "Simular", diff --git a/claude/.mcp.json b/claude/.mcp.json index 4453c09..25bd95c 100644 --- a/claude/.mcp.json +++ b/claude/.mcp.json @@ -2,7 +2,7 @@ "mcpServers": { "box": { "type": "http", - "url": "https://relay.simular.cloud/mcp" + "url": "https://mcp.box.simular.ai/mcp" } } } diff --git a/cli/README.md b/cli/README.md index bc67cbd..7604d7e 100644 --- a/cli/README.md +++ b/cli/README.md @@ -6,7 +6,7 @@ Connect [box](https://box.simular.ai) to your AI apps in one command. box gives npx @simular-ai/box connect ``` -The CLI finds the AI apps on this computer, asks which ones should use box, and adds the box MCP server (`https://relay.simular.cloud/mcp`) to each one in its own format. Other servers and settings in those files are left alone, each changed file gets a backup next to it (`*.box-backup-