From 0f48e4dae54f7d7101707f1a817c56ddb94b8aef Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Thu, 26 Mar 2026 12:30:15 -0700 Subject: [PATCH] feat: headless delete via `spawn delete --name --yes` (#3015) Agents running on spawned VMs couldn't delete child spawns because `spawn delete` requires an interactive terminal for the picker UI. Added --name and --yes flags: when both are provided in non-interactive mode, the server matching the name is deleted without prompts. This enables agents to manage their own child VMs programmatically. Updated all skill files to teach agents the headless delete syntax. Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: A <258483684+la14-1@users.noreply.github.com> --- packages/cli/package.json | 2 +- packages/cli/src/commands/delete.ts | 34 +++++++++++++++++++++++--- packages/cli/src/index.ts | 12 +++++++-- packages/cli/src/shared/spawn-skill.ts | 2 +- skills/claude/SKILL.md | 2 +- skills/codex/SKILL.md | 2 +- skills/junie/AGENTS.md | 2 +- skills/kilocode/spawn.md | 2 +- skills/openclaw/SKILL.md | 2 +- skills/opencode/AGENTS.md | 2 +- skills/zeroclaw/AGENTS.md | 2 +- 11 files changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 0352f96c..fefd9283 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.26.11", + "version": "0.26.12", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/commands/delete.ts b/packages/cli/src/commands/delete.ts index 3146158e..c3db4a38 100644 --- a/packages/cli/src/commands/delete.ts +++ b/packages/cli/src/commands/delete.ts @@ -352,7 +352,12 @@ export async function cascadeDelete(record: SpawnRecord, manifest: Manifest | nu return confirmAndDelete(record, manifest); } -export async function cmdDelete(agentFilter?: string, cloudFilter?: string): Promise { +export async function cmdDelete( + agentFilter?: string, + cloudFilter?: string, + nameFilter?: string, + forceYes?: boolean, +): Promise { const resolved = await resolveListFilters(agentFilter, cloudFilter); agentFilter = resolved.agentFilter; cloudFilter = resolved.cloudFilter; @@ -368,6 +373,15 @@ export async function cmdDelete(agentFilter?: string, cloudFilter?: string): Pro const lower = cloudFilter.toLowerCase(); filtered = filtered.filter((r) => r.cloud.toLowerCase() === lower); } + if (nameFilter) { + const lower = nameFilter.toLowerCase(); + filtered = filtered.filter( + (r) => + (r.name ?? "").toLowerCase() === lower || + (r.connection?.server_name ?? "").toLowerCase() === lower || + r.id === nameFilter, + ); + } if (filtered.length === 0) { p.log.info("No active servers to delete."); @@ -387,10 +401,22 @@ export async function cmdDelete(agentFilter?: string, cloudFilter?: string): Pro const manifestResult = await asyncTryCatchIf(isNetworkError, loadManifest); const manifest: Manifest | null = manifestResult.ok ? manifestResult.data : null; + // Non-interactive headless delete: --name + --yes skips the picker if (!isInteractiveTTY()) { - p.log.error("spawn delete requires an interactive terminal."); - p.log.info(`Use ${pc.cyan("spawn list")} to see your servers.`); - process.exit(1); + if (!forceYes) { + p.log.error("spawn delete requires --yes in non-interactive mode."); + p.log.info(`Usage: ${pc.cyan("spawn delete --name --yes")}`); + process.exit(1); + } + for (const record of filtered) { + const label = record.connection?.server_name || record.name || record.id; + await ensureDeleteCredentials(record); + const ok = await execDeleteServer(record); + if (ok) { + p.log.success(`Server "${label}" deleted`); + } + } + return; } await activeServerPicker(filtered, manifest); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 77990eba..049e77a9 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -613,8 +613,16 @@ async function dispatchDeleteCommand(filteredArgs: string[]): Promise { cmdHelp(); return; } - const { agentFilter, cloudFilter } = parseListFilters(filteredArgs.slice(1)); - await cmdDelete(agentFilter, cloudFilter); + const args = filteredArgs.slice(1); + const forceYes = args.includes("--yes") || args.includes("-y"); + let nameFilter: string | undefined; + const nameIdx = args.indexOf("--name"); + if (nameIdx !== -1 && args[nameIdx + 1]) { + nameFilter = args[nameIdx + 1]; + } + const cleanArgs = args.filter((a) => a !== "--yes" && a !== "-y" && a !== "--name" && a !== nameFilter); + const { agentFilter, cloudFilter } = parseListFilters(cleanArgs); + await cmdDelete(agentFilter, cloudFilter, nameFilter, forceYes); } /** Handle status/ps commands with --prune and --json flags */ diff --git a/packages/cli/src/shared/spawn-skill.ts b/packages/cli/src/shared/spawn-skill.ts index 0cb58a95..c04a09f1 100644 --- a/packages/cli/src/shared/spawn-skill.ts +++ b/packages/cli/src/shared/spawn-skill.ts @@ -44,7 +44,7 @@ Returns JSON: \`{"status":"success","ip_address":"...","ssh_user":"root","server ## Managing Children - \`spawn list --json\` — see running children -- \`spawn delete\` — tear down a child VM +- \`spawn delete --name --yes\` — tear down a child VM (headless) - \`spawn tree\` — see the full spawn tree ## Context diff --git a/skills/claude/SKILL.md b/skills/claude/SKILL.md index 5918846d..92e4ee65 100644 --- a/skills/claude/SKILL.md +++ b/skills/claude/SKILL.md @@ -28,7 +28,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_ ## Managing Children - `spawn list --json` — see running children -- `spawn delete` — tear down a child VM +- `spawn delete --name --yes` — tear down a child VM - `spawn tree` — see the full spawn tree ## Context diff --git a/skills/codex/SKILL.md b/skills/codex/SKILL.md index 5918846d..92e4ee65 100644 --- a/skills/codex/SKILL.md +++ b/skills/codex/SKILL.md @@ -28,7 +28,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_ ## Managing Children - `spawn list --json` — see running children -- `spawn delete` — tear down a child VM +- `spawn delete --name --yes` — tear down a child VM - `spawn tree` — see the full spawn tree ## Context diff --git a/skills/junie/AGENTS.md b/skills/junie/AGENTS.md index c0f13a22..cf8db3a6 100644 --- a/skills/junie/AGENTS.md +++ b/skills/junie/AGENTS.md @@ -22,7 +22,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_ ## Managing Children - `spawn list --json` — see running children -- `spawn delete` — tear down a child VM +- `spawn delete --name --yes` — tear down a child VM - `spawn tree` — see the full spawn tree ## Context diff --git a/skills/kilocode/spawn.md b/skills/kilocode/spawn.md index c0f13a22..cf8db3a6 100644 --- a/skills/kilocode/spawn.md +++ b/skills/kilocode/spawn.md @@ -22,7 +22,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_ ## Managing Children - `spawn list --json` — see running children -- `spawn delete` — tear down a child VM +- `spawn delete --name --yes` — tear down a child VM - `spawn tree` — see the full spawn tree ## Context diff --git a/skills/openclaw/SKILL.md b/skills/openclaw/SKILL.md index 5918846d..92e4ee65 100644 --- a/skills/openclaw/SKILL.md +++ b/skills/openclaw/SKILL.md @@ -28,7 +28,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_ ## Managing Children - `spawn list --json` — see running children -- `spawn delete` — tear down a child VM +- `spawn delete --name --yes` — tear down a child VM - `spawn tree` — see the full spawn tree ## Context diff --git a/skills/opencode/AGENTS.md b/skills/opencode/AGENTS.md index c0f13a22..cf8db3a6 100644 --- a/skills/opencode/AGENTS.md +++ b/skills/opencode/AGENTS.md @@ -22,7 +22,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_ ## Managing Children - `spawn list --json` — see running children -- `spawn delete` — tear down a child VM +- `spawn delete --name --yes` — tear down a child VM - `spawn tree` — see the full spawn tree ## Context diff --git a/skills/zeroclaw/AGENTS.md b/skills/zeroclaw/AGENTS.md index c0f13a22..cf8db3a6 100644 --- a/skills/zeroclaw/AGENTS.md +++ b/skills/zeroclaw/AGENTS.md @@ -22,7 +22,7 @@ Returns JSON: `{"status":"success","ip_address":"...","ssh_user":"root","server_ ## Managing Children - `spawn list --json` — see running children -- `spawn delete` — tear down a child VM +- `spawn delete --name --yes` — tear down a child VM - `spawn tree` — see the full spawn tree ## Context