From 2e7b083f8f49fd8b443d8aac815e0d9d35818d92 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Fri, 13 Feb 2026 07:16:14 -0800 Subject: [PATCH] fix: show cloud URL for missing credentials in dry-run and add spawn list --clear (#944) Two UX improvements: 1. Dry-run credential status now shows the cloud provider's URL next to missing cloud-specific auth vars (e.g., HCLOUD_TOKEN), helping users find where to create their credentials. Previously only OPENROUTER_API_KEY showed a URL hint. 2. Added `spawn list --clear` command to let users clear their spawn history. Previously there was no way to reset the 100-entry history file without manually deleting ~/.spawn/history.json. Agent: ux-engineer Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) --- cli/package.json | 2 +- cli/src/commands.ts | 28 ++++++++++++++++++++++------ cli/src/history.ts | 13 ++++++++++++- cli/src/index.ts | 6 ++++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/cli/package.json b/cli/package.json index 80d725dc..74b7dd65 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.2.72", + "version": "0.2.73", "type": "module", "bin": { "spawn": "cli.js" diff --git a/cli/src/commands.ts b/cli/src/commands.ts index ecb40759..b5691bb1 100644 --- a/cli/src/commands.ts +++ b/cli/src/commands.ts @@ -16,7 +16,7 @@ import { import pkg from "../package.json" with { type: "json" }; const VERSION = pkg.version; import { validateIdentifier, validateScriptContent, validatePrompt } from "./security.js"; -import { saveSpawnRecord, filterHistory, type SpawnRecord } from "./history.js"; +import { saveSpawnRecord, filterHistory, clearHistory, type SpawnRecord } from "./history.js"; // ── Helpers ──────────────────────────────────────────────────────────────────── @@ -418,6 +418,7 @@ function buildCredentialStatusLines(manifest: Manifest, cloud: string): string[] const lines: string[] = []; const cloudAuth = manifest.clouds[cloud].auth; const authVars = parseAuthEnvVars(cloudAuth); + const cloudUrl = manifest.clouds[cloud].url; // Always check OPENROUTER_API_KEY const orSet = !!process.env.OPENROUTER_API_KEY; @@ -425,12 +426,17 @@ function buildCredentialStatusLines(manifest: Manifest, cloud: string): string[] ? ` ${pc.green("OPENROUTER_API_KEY")} ${pc.dim("-- set")}` : ` ${pc.red("OPENROUTER_API_KEY")} ${pc.dim("-- not set")} ${pc.dim("https://openrouter.ai/settings/keys")}`); - // Check cloud-specific auth vars - for (const v of authVars) { + // Check cloud-specific auth vars (show provider URL hint for missing vars) + for (let i = 0; i < authVars.length; i++) { + const v = authVars[i]; const isSet = !!process.env[v]; - lines.push(isSet - ? ` ${pc.green(v)} ${pc.dim("-- set")}` - : ` ${pc.red(v)} ${pc.dim("-- not set")}`); + if (isSet) { + lines.push(` ${pc.green(v)} ${pc.dim("-- set")}`); + } else { + // Show the cloud provider URL on the first missing var to help users find their credentials + const urlHint = i === 0 && cloudUrl ? ` ${pc.dim(cloudUrl)}` : ""; + lines.push(` ${pc.red(v)} ${pc.dim("-- not set")}${urlHint}`); + } } return lines; @@ -1185,6 +1191,15 @@ async function interactiveListPicker(records: SpawnRecord[], manifest: Manifest await cmdRun(selected.agent, selected.cloud, selected.prompt); } +export function cmdListClear(): void { + const count = clearHistory(); + if (count === 0) { + p.log.info("No spawn history to clear."); + } else { + p.log.success(`Cleared ${count} spawn record${count !== 1 ? "s" : ""} from history.`); + } +} + export async function cmdList(agentFilter?: string, cloudFilter?: string): Promise { const resolved = await resolveListFilters(agentFilter, cloudFilter); const manifest = resolved.manifest; @@ -1551,6 +1566,7 @@ ${pc.bold("USAGE")} spawn list Filter history by agent or cloud name spawn list -a Filter spawn history by agent (or --agent) spawn list -c Filter spawn history by cloud (or --cloud) + spawn list --clear Clear all spawn history Aliases: ls, history spawn matrix Full availability matrix (alias: m) spawn agents List all agents with descriptions diff --git a/cli/src/history.ts b/cli/src/history.ts index 64b1a475..8d8c7e37 100644 --- a/cli/src/history.ts +++ b/cli/src/history.ts @@ -1,4 +1,4 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs"; import { join } from "path"; import { homedir } from "os"; @@ -45,6 +45,17 @@ export function saveSpawnRecord(record: SpawnRecord): void { writeFileSync(getHistoryPath(), JSON.stringify(history, null, 2) + "\n"); } +export function clearHistory(): number { + const path = getHistoryPath(); + if (!existsSync(path)) return 0; + const records = loadHistory(); + const count = records.length; + if (count > 0) { + unlinkSync(path); + } + return count; +} + export function filterHistory( agentFilter?: string, cloudFilter?: string diff --git a/cli/src/index.ts b/cli/src/index.ts index 138c4e6c..bf11c263 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -3,6 +3,7 @@ import { cmdInteractive, cmdRun, cmdList, + cmdListClear, cmdMatrix, cmdAgents, cmdClouds, @@ -64,6 +65,7 @@ const KNOWN_FLAGS = new Set([ "--prompt", "-p", "--prompt-file", "-f", "--dry-run", "-n", "-a", "-c", "--agent", "--cloud", + "--clear", ]); /** Expand --flag=value into --flag value so all flag parsing works uniformly */ @@ -368,6 +370,10 @@ async function dispatchCommand(cmd: string, filteredArgs: string[], prompt: stri if (LIST_COMMANDS.has(cmd)) { if (hasTrailingHelpFlag(filteredArgs)) { cmdHelp(); return; } + if (filteredArgs.slice(1).includes("--clear")) { + cmdListClear(); + return; + } const { agentFilter, cloudFilter } = parseListFilters(filteredArgs.slice(1)); await cmdList(agentFilter, cloudFilter); return;