Skip to content
Draft
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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ spawn claude gcp --beta tarball --beta parallel
| `parallel` | Parallelize server boot with setup prompts |
| `recursive` | Install spawn CLI on VM so it can spawn child VMs |
| `sandbox` | Run local agents in a Docker container (sandboxed) |
| `skills` | Pre-install MCP servers and tools on the VM |

`--fast` enables `tarball`, `images`, and `parallel` (not `recursive` or `sandbox`).

Expand Down Expand Up @@ -209,6 +210,42 @@ spawn --beta sandbox # Interactive picker shows both local options
spawn openclaw local --beta sandbox # Direct launch, sandboxed
```

#### Skills

Use `--beta skills` to pre-install MCP servers and instruction skills on the remote VM during setup:

```bash
spawn claude digitalocean --beta skills
```

A skills picker appears after setup options, letting you choose what to install:

**MCP Servers** (Claude Code, Cursor):

| Skill | Package | Default |
|-------|---------|---------|
| GitHub | `@modelcontextprotocol/server-github` | Yes |
| Playwright | `@anthropic-ai/mcp-server-playwright` | Yes |
| Fetch | `@anthropic-ai/mcp-server-fetch` | No |
| Context7 | `@upstash/context7-mcp` | No |
| PostgreSQL | `@modelcontextprotocol/server-postgres` | No |

**Instruction Skills** (Claude Code, OpenClaw, Codex):

| Skill | Description | Default |
|-------|-------------|---------|
| Git Workflow | Branching, conventional commits, PR workflow | Yes |
| Web Search | Search the web, fetch URLs, extract content | Yes |
| Docker | Container management, compose, best practices | No |
| Deploy | Deploy via SSH, Docker, pm2, systemd | No |

Skills requiring env vars (e.g. `GITHUB_TOKEN`) will prompt during setup.

For headless use:
```bash
SPAWN_SELECTED_SKILLS=github-mcp,git-workflow,web-search spawn claude hetzner
```

### Without the CLI

Every combination works as a one-liner — no install required:
Expand Down
198 changes: 198 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -492,5 +492,203 @@
"digitalocean/cursor": "implemented",
"gcp/cursor": "implemented",
"sprite/cursor": "implemented"
},
"skills": {
"github-mcp": {
"name": "GitHub",
"description": "PRs, issues, repos, search via GitHub API",
"type": "mcp",
"package": "@modelcontextprotocol/server-github",
"env_vars": ["GITHUB_TOKEN"],
"agents": {
"claude": {
"mcp_config": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
},
"default": true
},
"cursor": {
"mcp_config": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
},
"default": true
}
}
},
"playwright-mcp": {
"name": "Playwright",
"description": "Browser automation — navigate, click, screenshot, scrape",
"type": "mcp",
"package": "@anthropic-ai/mcp-server-playwright",
"agents": {
"claude": {
"mcp_config": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-server-playwright"]
},
"default": true
},
"cursor": {
"mcp_config": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-server-playwright"]
},
"default": true
}
}
},
"fetch-mcp": {
"name": "Fetch",
"description": "HTTP requests — fetch web pages, APIs, and files",
"type": "mcp",
"package": "@anthropic-ai/mcp-server-fetch",
"agents": {
"claude": {
"mcp_config": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-server-fetch"]
},
"default": false
},
"cursor": {
"mcp_config": {
"command": "npx",
"args": ["-y", "@anthropic-ai/mcp-server-fetch"]
},
"default": false
}
}
},
"context7": {
"name": "Context7",
"description": "Library docs lookup — up-to-date documentation in context",
"type": "mcp",
"package": "@upstash/context7-mcp",
"agents": {
"claude": {
"mcp_config": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"]
},
"default": false
},
"cursor": {
"mcp_config": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"]
},
"default": false
}
}
},
"postgres-mcp": {
"name": "PostgreSQL",
"description": "Database access — query, inspect schema, manage tables",
"type": "mcp",
"package": "@modelcontextprotocol/server-postgres",
"env_vars": ["DATABASE_URL"],
"agents": {
"claude": {
"mcp_config": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": { "DATABASE_URL": "${DATABASE_URL}" }
},
"default": false
},
"cursor": {
"mcp_config": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": { "DATABASE_URL": "${DATABASE_URL}" }
},
"default": false
}
}
},
"git-workflow": {
"name": "Git Workflow",
"description": "Branch management, PR workflow, conventional commits",
"type": "instruction",
"content": "---\nname: git-workflow\ndescription: Git branching strategy, conventional commits, and PR workflow best practices\n---\n\n# Git Workflow\n\nFollow these conventions for all git operations:\n\n## Branching\n- `feat/<name>` for new features\n- `fix/<name>` for bug fixes\n- `chore/<name>` for maintenance\n- Always branch from `main`\n\n## Commits\nUse conventional commits: `type(scope): description`\n- `feat:` new feature\n- `fix:` bug fix\n- `docs:` documentation\n- `refactor:` code restructuring\n- `test:` adding tests\n- `chore:` maintenance\n\n## Pull Requests\n1. Create a feature branch\n2. Make focused, atomic commits\n3. Open a PR with a clear title and description\n4. Request review, address feedback\n5. Squash merge when approved\n",
"agents": {
"claude": {
"instruction_path": "~/.claude/skills/git-workflow/SKILL.md",
"default": true
},
"openclaw": {
"instruction_path": "~/.openclaw/skills/git-workflow/SKILL.md",
"default": true
},
"codex": {
"instruction_path": "~/.agents/skills/git-workflow/SKILL.md",
"default": true
}
}
},
"docker": {
"name": "Docker",
"description": "Container management — build, run, compose, debug",
"type": "instruction",
"content": "---\nname: docker\ndescription: Docker container management, Dockerfile best practices, and docker compose workflows\n---\n\n# Docker\n\nYou can use Docker on this machine for containerized workflows.\n\n## Common Commands\n\n```bash\n# Build an image\ndocker build -t myapp .\n\n# Run a container\ndocker run -d -p 8080:8080 --name myapp myapp\n\n# View running containers\ndocker ps\n\n# View logs\ndocker logs -f myapp\n\n# Execute command in container\ndocker exec -it myapp bash\n\n# Stop and remove\ndocker stop myapp && docker rm myapp\n```\n\n## Docker Compose\n\n```bash\ndocker compose up -d # Start services\ndocker compose down # Stop services\ndocker compose logs -f # Follow logs\ndocker compose ps # List services\n```\n\n## Dockerfile Best Practices\n- Use multi-stage builds to minimize image size\n- Pin base image versions (e.g., `node:22-slim`)\n- Copy `package.json` before source for layer caching\n- Use `.dockerignore` to exclude `node_modules`, `.git`\n- Run as non-root user in production\n",
"agents": {
"claude": {
"instruction_path": "~/.claude/skills/docker/SKILL.md",
"default": false
},
"openclaw": {
"instruction_path": "~/.openclaw/skills/docker/SKILL.md",
"default": false
},
"codex": {
"instruction_path": "~/.agents/skills/docker/SKILL.md",
"default": false
}
}
},
"web-search": {
"name": "Web Search",
"description": "Search the web, fetch URLs, and extract content",
"type": "instruction",
"content": "---\nname: web-search\ndescription: Search the web and fetch URL content using command-line tools\nallowed-tools: Bash\n---\n\n# Web Search & Fetch\n\nYou can search the web and fetch content from URLs.\n\n## Fetch a URL\n\n```bash\ncurl -fsSL \"https://example.com\" | head -200\n```\n\n## Search with DuckDuckGo (no API key needed)\n\n```bash\n# HTML search results\ncurl -fsSL \"https://html.duckduckgo.com/html/?q=your+search+query\" | bun -e \"\nconst html = await Bun.stdin.text();\nconst results = [...html.matchAll(/class=\\\"result__a\\\"[^>]*href=\\\"([^\\\"]+)\\\"[^>]*>([^<]+)/g)];\nfor (const [, url, title] of results.slice(0, 5)) {\n const decoded = decodeURIComponent(url.replace(/.*uddg=/, '').replace(/&.*/, ''));\n console.log(title.trim(), '-', decoded);\n}\"\n```\n\n## Fetch and Extract Text\n\n```bash\n# Fetch URL and strip HTML tags\ncurl -fsSL \"URL\" | bun -e \"\nconst html = await Bun.stdin.text();\nconst text = html.replace(/<script[^>]*>[\\\\s\\\\S]*?<\\\\/script>/gi, '')\n .replace(/<style[^>]*>[\\\\s\\\\S]*?<\\\\/style>/gi, '')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\\\s+/g, ' ').trim();\nconsole.log(text.slice(0, 5000));\"\n```\n",
"agents": {
"claude": {
"instruction_path": "~/.claude/skills/web-search/SKILL.md",
"default": true
},
"openclaw": {
"instruction_path": "~/.openclaw/skills/web-search/SKILL.md",
"default": true
},
"codex": {
"instruction_path": "~/.agents/skills/web-search/SKILL.md",
"default": true
}
}
},
"deploy": {
"name": "Deploy",
"description": "Deploy apps via SSH, Docker, or cloud CLI tools",
"type": "instruction",
"content": "---\nname: deploy\ndescription: Deploy applications using common deployment strategies\nallowed-tools: Bash\n---\n\n# Deploy\n\nCommon deployment patterns for this VM.\n\n## SSH Deploy (to another server)\n\n```bash\n# Copy files and restart\nrsync -avz --exclude node_modules --exclude .git ./ user@host:/app/\nssh user@host 'cd /app && npm install --production && pm2 restart all'\n```\n\n## Docker Deploy\n\n```bash\n# Build, tag, push, deploy\ndocker build -t registry.example.com/app:latest .\ndocker push registry.example.com/app:latest\nssh user@host 'docker pull registry.example.com/app:latest && docker compose up -d'\n```\n\n## Process Management (pm2)\n\n```bash\nnpm install -g pm2\npm2 start app.js --name myapp\npm2 save\npm2 startup # Auto-start on reboot\npm2 logs myapp # View logs\npm2 restart myapp # Restart\n```\n\n## Systemd Service\n\n```bash\n# Create /etc/systemd/system/myapp.service\nsudo systemctl daemon-reload\nsudo systemctl enable myapp\nsudo systemctl start myapp\nsudo journalctl -u myapp -f\n```\n",
"agents": {
"claude": {
"instruction_path": "~/.claude/skills/deploy/SKILL.md",
"default": false
},
"openclaw": {
"instruction_path": "~/.openclaw/skills/deploy/SKILL.md",
"default": false
},
"codex": {
"instruction_path": "~/.agents/skills/deploy/SKILL.md",
"default": false
}
}
}
}
}
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.30.10",
"version": "0.31.0",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
28 changes: 28 additions & 0 deletions packages/cli/src/commands/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,28 @@ async function promptSetupOptions(agentName: string): Promise<Set<string> | unde
return stepSet;
}

/** Show the skills picker if --beta skills is active and the agent has skills available. */
async function maybePromptSkills(manifest: Manifest, agentName: string): Promise<void> {
if (process.env.SPAWN_SELECTED_SKILLS) {
return;
}
const betaFeatures = (process.env.SPAWN_BETA ?? "").split(",").filter(Boolean);
if (!betaFeatures.includes("skills")) {
return;
}
const { promptSkillSelection, collectSkillEnvVars } = await import("../shared/skills.js");
const selectedSkills = await promptSkillSelection(manifest, agentName);
if (selectedSkills && selectedSkills.length > 0) {
process.env.SPAWN_SELECTED_SKILLS = selectedSkills.join(",");
// Prompt for any missing env vars required by selected skills
const envPairs = await collectSkillEnvVars(manifest, selectedSkills);
if (envPairs.length > 0) {
const existing = process.env.SPAWN_SKILL_ENV_PAIRS ?? "";
process.env.SPAWN_SKILL_ENV_PAIRS = existing ? `${existing},${envPairs.join(",")}` : envPairs.join(",");
}
}
}

export { getAndValidateCloudChoices, promptSetupOptions, promptSpawnName, selectCloud };

export async function cmdInteractive(): Promise<void> {
Expand Down Expand Up @@ -295,6 +317,9 @@ export async function cmdInteractive(): Promise<void> {
}
}

// Skills picker (--beta skills)
await maybePromptSkills(manifest, agentChoice);

const spawnName = await promptSpawnName();

const agentName = manifest.agents[agentChoice].name;
Expand Down Expand Up @@ -354,6 +379,9 @@ export async function cmdAgentInteractive(agent: string, prompt?: string, dryRun
}
}

// Skills picker (--beta skills)
await maybePromptSkills(manifest, resolvedAgent);

const spawnName = await promptSpawnName();

const agentName = manifest.agents[resolvedAgent].name;
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,7 @@ async function main(): Promise<void> {
"docker",
"recursive",
"sandbox",
"skills",
]);
const betaFeatures = extractAllFlagValues(filteredArgs, "--beta", "spawn <agent> <cloud> --beta parallel");
for (const flag of betaFeatures) {
Expand All @@ -911,6 +912,7 @@ async function main(): Promise<void> {
console.error(` ${pc.cyan("parallel")} Parallelize server boot with setup prompts`);
console.error(` ${pc.cyan("docker")} Use Docker CE app image on Hetzner/GCP (faster boot)`);
console.error(` ${pc.cyan("sandbox")} Run local agents in a Docker container (sandboxed)`);
console.error(` ${pc.cyan("skills")} Pre-install MCP servers and tools on the VM`);
console.error(` ${pc.cyan("recursive")} Install spawn CLI on VM for recursive spawning`);
process.exit(1);
}
Expand Down
33 changes: 33 additions & 0 deletions packages/cli/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,43 @@ export interface CloudDef {
icon?: string;
}

/** MCP server configuration (matches Claude Code settings.json mcpServers format). */
export interface McpServerConfig {
command: string;
args: string[];
env?: Record<string, string>;
}

/** Per-agent skill configuration. */
export interface SkillAgentConfig {
mcp_config?: McpServerConfig;
/** Remote path for instruction-type skills (e.g. ~/.openclaw/skills/git-workflow/SKILL.md). */
instruction_path?: string;
/** Whether this skill is pre-selected in the picker for this agent. */
default: boolean;
}

/** A skill that can be pre-installed on a remote VM. */
export interface SkillDef {
name: string;
description: string;
type: "mcp" | "instruction";
/** npm package name (for MCP-type skills). */
package?: string;
/** YAML frontmatter + markdown content (for instruction-type skills). */
content?: string;
/** Env vars required by this skill (shown as hints in picker). */
env_vars?: string[];
/** Per-agent installation config. Only agents listed here support this skill. */
agents: Record<string, SkillAgentConfig>;
}

export interface Manifest {
agents: Record<string, AgentDef>;
clouds: Record<string, CloudDef>;
matrix: Record<string, string>;
/** Skill catalog — MCP servers and tools that can be pre-installed on VMs. */
skills?: Record<string, SkillDef>;
}

// ── Constants ──────────────────────────────────────────────────────────────────
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 @@ -85,7 +85,7 @@ async function installAgent(
/**
* Upload a config file to the remote machine via a temp file and mv.
*/
async function uploadConfigFile(runner: CloudRunner, content: string, remotePath: string): Promise<void> {
export async function uploadConfigFile(runner: CloudRunner, content: string, remotePath: string): Promise<void> {
const safePath = validateRemotePath(remotePath);

const tmpFile = join(getTmpDir(), `spawn_config_${Date.now()}_${Math.random().toString(36).slice(2)}`);
Expand Down
Loading
Loading