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
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.27.3",
"version": "0.27.4",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
72 changes: 71 additions & 1 deletion packages/cli/src/__tests__/check-entity.test.ts
Original file line number Diff line number Diff line change
@@ -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).
Expand Down Expand Up @@ -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", () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/commands/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/shared/agent-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ async function setupCursorConfig(runner: CloudRunner, _apiKey: string): Promise<
'spawn <agent> <cloud> --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",
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/shared/spawn-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ You have the \`spawn\` CLI installed. Use it to provision cloud VMs with AI agen
spawn <agent> <cloud> --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
Expand All @@ -51,7 +51,7 @@ ssh -o StrictHostKeyChecking=accept-new root@<ip_address> '~/.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 <name> --yes\` to clean up when done.
Expand All @@ -76,7 +76,7 @@ const HERMES_SNIPPET = `

You have the \`spawn\` CLI for creating child cloud VMs with AI agents.
Use \`spawn <agent> <cloud> --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@<ip> '~/.local/bin/<agent> --print "prompt"'\`.
`;
Expand Down
Loading