diff --git a/packages/cli/package.json b/packages/cli/package.json index 769a6ccae..563d703e2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.27.3", + "version": "0.27.4", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/__tests__/check-entity.test.ts b/packages/cli/src/__tests__/check-entity.test.ts index de817a158..1c6663912 100644 --- a/packages/cli/src/__tests__/check-entity.test.ts +++ b/packages/cli/src/__tests__/check-entity.test.ts @@ -1,7 +1,7 @@ import type { Manifest } from "../manifest"; import { beforeEach, describe, expect, it } from "bun:test"; -import { checkEntity } from "../commands/index.js"; +import { checkEntity, resolveAgentKey } from "../commands/index.js"; /** * Tests for checkEntity (commands/shared.ts). @@ -383,6 +383,76 @@ describe("checkEntity", () => { }); }); + // ── Disabled agents ───────────────────────────────────────────────────── + + describe("disabled agents", () => { + let disabledManifest: Manifest; + + beforeEach(() => { + disabledManifest = { + agents: { + claude: { + name: "Claude Code", + description: "AI coding assistant", + url: "https://claude.ai", + install: "npm install -g claude", + launch: "claude", + env: { + ANTHROPIC_API_KEY: "test", + }, + }, + cursor: { + name: "Cursor CLI", + description: "AI coding agent", + url: "https://cursor.com", + install: "curl https://cursor.com/install | bash", + launch: "agent", + env: {}, + disabled: true, + disabled_reason: "Cursor CLI uses a proprietary protocol.", + }, + }, + clouds: { + sprite: { + name: "Sprite", + description: "Lightweight VMs", + price: "test", + url: "https://sprite.sh", + type: "vm", + auth: "SPRITE_TOKEN", + provision_method: "api", + exec_method: "ssh", + interactive_method: "ssh", + }, + }, + matrix: { + "sprite/claude": "implemented", + "sprite/cursor": "implemented", + }, + }; + }); + + it("checkEntity returns false for a disabled agent", () => { + expect(checkEntity(disabledManifest, "cursor", "agent")).toBe(false); + }); + + it("checkEntity returns true for an enabled agent in the same manifest", () => { + expect(checkEntity(disabledManifest, "claude", "agent")).toBe(true); + }); + + it("resolveAgentKey returns null for a disabled agent", () => { + expect(resolveAgentKey(disabledManifest, "cursor")).toBeNull(); + }); + + it("resolveAgentKey resolves an enabled agent normally", () => { + expect(resolveAgentKey(disabledManifest, "claude")).toBe("claude"); + }); + + it("checkEntity still works for clouds even when agents are disabled", () => { + expect(checkEntity(disabledManifest, "sprite", "cloud")).toBe(true); + }); + }); + // ── Manifest with overlapping key names ──────────────────────────────── describe("manifest with overlapping patterns", () => { diff --git a/packages/cli/src/commands/shared.ts b/packages/cli/src/commands/shared.ts index f51783d22..94219a1d5 100644 --- a/packages/cli/src/commands/shared.ts +++ b/packages/cli/src/commands/shared.ts @@ -162,6 +162,9 @@ export function findClosestKeyByNameOrKey( function resolveEntityKey(manifest: Manifest, input: string, kind: "agent" | "cloud"): string | null { const collection = getEntityCollection(manifest, kind); if (collection[input]) { + if (kind === "agent" && manifest.agents[input].disabled) { + return null; + } return input; } const keys = getEntityKeys(manifest, kind); @@ -285,6 +288,13 @@ export function checkEntity(manifest: Manifest, value: string, kind: "agent" | " const def = ENTITY_DEFS[kind]; const collection = getEntityCollection(manifest, kind); if (collection[value]) { + if (kind === "agent" && manifest.agents[value].disabled) { + p.log.error(`${pc.bold(manifest.agents[value].name)} is temporarily disabled.`); + if (manifest.agents[value].disabled_reason) { + p.log.info(manifest.agents[value].disabled_reason); + } + return false; + } return true; } diff --git a/packages/cli/src/shared/agent-setup.ts b/packages/cli/src/shared/agent-setup.ts index 5a6bc9829..a7ed108b0 100644 --- a/packages/cli/src/shared/agent-setup.ts +++ b/packages/cli/src/shared/agent-setup.ts @@ -193,7 +193,7 @@ async function setupCursorConfig(runner: CloudRunner, _apiKey: string): Promise< 'spawn --headless --output json --prompt "task description"', "```", "", - "## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie, cursor", + "## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie", "## Clouds: hetzner, digitalocean, aws, gcp, sprite", "", "The command returns JSON with connection details. Use this to delegate subtasks", diff --git a/packages/cli/src/shared/spawn-skill.ts b/packages/cli/src/shared/spawn-skill.ts index 4340283ce..ce1c15340 100644 --- a/packages/cli/src/shared/spawn-skill.ts +++ b/packages/cli/src/shared/spawn-skill.ts @@ -30,7 +30,7 @@ You have the \`spawn\` CLI installed. Use it to provision cloud VMs with AI agen spawn --headless --output json --prompt "task description" \`\`\` -## Agents: claude, cursor, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie +## Agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie ## Clouds: hetzner, digitalocean, aws, gcp, sprite ## Example @@ -51,7 +51,7 @@ ssh -o StrictHostKeyChecking=accept-new root@ '~/.local/bin/claude - \`\`\` ### Known gotchas — do NOT waste time rediscovering these: -- Agent binaries are at \`~/.local/bin/\`, NOT on the default PATH. Always use the full path: \`~/.local/bin/claude\`, \`~/.local/bin/cursor\`, etc. +- Agent binaries are at \`~/.local/bin/\`, NOT on the default PATH. Always use the full path: \`~/.local/bin/claude\`, \`~/.local/bin/codex\`, etc. - Claude Code blocks \`--dangerously-skip-permissions\` as root. Use \`--print\` for one-shot prompts instead — it works as root and returns the response directly. - Do NOT try to create a non-root user to work around root restrictions — it requires copying config files (\`.claude.json\`, \`.claude/settings.json\`, env vars) and is fragile. Just use \`--print\`. - For long-running tasks, use \`spawn delete --name --yes\` to clean up when done. @@ -76,7 +76,7 @@ const HERMES_SNIPPET = ` You have the \`spawn\` CLI for creating child cloud VMs with AI agents. Use \`spawn --headless --output json --prompt "task"\` to delegate work. -Available agents: claude, cursor, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie. +Available agents: claude, codex, openclaw, zeroclaw, opencode, kilocode, hermes, junie. Cloud credentials are pre-configured. Run \`spawn list --json\` to see children. Note: --headless only provisions the VM. To run a prompt, SSH in: \`ssh root@ '~/.local/bin/ --print "prompt"'\`. `;