Add BlueBubbles, Discord, Slack, Signal, and Google Chat to the
multi-select setup options for OpenClaw. Selected channels get
`enabled: true` stubs written via `openclaw config set`, so the
dashboard renders channel cards properly instead of showing
"Unsupported type: . Use Raw mode."
Channels are gated by enabledSteps — only user-selected channels
get stubbed. WhatsApp and Telegram remain in the list as before.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add strict character validation for remotePath to prevent command injection
via crafted paths. Use shellQuote for tempRemote in the shell command. Add
a base64 output assertion to document and enforce the safety of single-quoted
interpolation for settingsB64.
Fixes#2668
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The test README was missing entries for 8 test files that were added
after the initial documentation was written:
- cmd-feedback.test.ts
- cmd-fix.test.ts
- config-priority.test.ts
- delete-spinner.test.ts
- gcp-shellquote.test.ts
- oauth-pkce.test.ts
- result-helpers.test.ts
- steps-flag.test.ts
- spawn-config.test.ts
Added descriptions under the appropriate section headers so the README
accurately reflects all test coverage.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Replace startup banner message from "Run spawn update to check for
updates." to "Run spawn feedback to tell us what to improve."
Bumps CLI patch version to 0.19.1.
Fixes#2664
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
deepMerge was exported from shared/parse.ts but never imported or called
from any other module. Biome confirms it as an unused variable. Removing
it eliminates dead code and the now-unused isPlainObject import.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
extractFlagValue() used `!args[idx + 1]` to detect a missing value,
which treated empty strings as missing. Change to `=== undefined` so
that `--steps ""` passes through correctly as documented.
Fixes#2661
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Move "Custom model" from OpenClaw-specific to common setup steps so
every agent shows it in the setup menu. Add modelEnvVar to agents that
support model override via environment variable:
- Kilo Code: KILOCODE_MODEL
- ZeroClaw: ZEROCLAW_MODEL
- Hermes: LLM_MODEL
- Junie: JUNIE_MODEL
When a custom model is selected, the env var is injected into .spawnrc
alongside the other agent env vars. OpenClaw continues to use its
existing configure() path. Claude and Codex don't have modelEnvVar
since they handle model routing differently.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Channel extensions only register their UI schemas when enabled. With
enabled=false the dashboard still shows "Unsupported type: . Use Raw
mode." Setting enabled=true lets the extensions load so users can
configure channels from the dashboard.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Write disabled telegram and whatsapp channel entries during setup so
the OpenClaw dashboard renders proper channel cards instead of showing
"Unsupported type: . Use Raw mode." Users can then configure channels
from the dashboard UI.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace the manual config JSON construction + download-merge-upload flow
with `openclaw onboard --non-interactive`, which creates a properly
structured config with auth profiles, provider setup, gateway config,
and workspace. Follow up with `openclaw config set` for browser and
Telegram settings.
This fixes the broken dashboard channel setup caused by bypassing
OpenClaw's credential/auth profile system. Removes the gateway auth
re-assertion hack that was needed due to field-dropping during
config set cycles on manually-written JSON.
Includes a fallback path that writes minimal JSON if onboard fails.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements RFC 7636 PKCE with S256 code challenge method for the
OpenRouter OAuth authorization flow. This prevents authorization code
interception attacks by binding the code to a cryptographic verifier.
Changes:
- Generate code_verifier (32 random bytes, base64url-encoded)
- Derive code_challenge via SHA-256 + base64url
- Send code_challenge + code_challenge_method=S256 in auth URL
- Send code_verifier + code_challenge_method in token exchange POST
- Add test suite with RFC 7636 Appendix B test vector validation
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
- security.test.ts: remove "comprehensively detect all command injection
patterns from issue #1400" test (14 lines). All 6 attack vectors
(&&, ||, >, <, &, ${}) are already tested individually in dedicated
tests above it, making this aggregate loop purely redundant.
- gcp-shellquote.test.ts: remove 2 redundant startsWith/endsWith
assertions from "should produce output that is safe for bash -c".
The toBe("'$(rm -rf /)'") assertion already proves the single-quote
wrapping; the follow-up checks add no signal.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
this function has no callers in production code but is intentionally
used in unit tests (custom-flag.test.ts) for state introspection.
adding documentation prevents it from being incorrectly identified
as dead code in future code quality scans.
code quality scan results:
- dead code: none found
- stale references: none found
- python usage: none found
- duplicate utilities: getCloudInitUserdata has per-cloud variants
with intentional differences (not mergeable)
- stale comments: none found
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Consolidate repetitive per-field test iterations in manifest-type-contracts.test.ts
into data-driven loops, eliminating ~15 near-identical it() blocks. Share a single
startGateway() invocation across all 3 gateway-resilience tests via beforeEach.
Remove redundant toBeDefined() check in junie-agent.test.ts that was immediately
superseded by a stronger assertion on the same value.
-- qa/dedup-scanner
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Each `openclaw config set` call does a read-modify-write that can drop
fields like channels and gateway auth. After all config set calls,
re-download the config, deep-merge our configObj on top, and re-upload
to restore any dropped fields.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- security.test.ts: remove "should handle prompt with only whitespace"
(line 614) — fully covered by "should reject empty prompts" (line 363)
which already tests validatePrompt(" ") and validatePrompt("\n\t")
- script-failure-guidance.test.ts: consolidate three separate "returns
simple command" tests (no-arg, undefined, empty string) into one.
All three called buildRetryCommand with absent/falsy prompt and
asserted identical output — the input variation is not a meaningful
behavioral distinction.
net: 3 tests removed. 1410 pass, 0 fail. biome lint clean.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
- commands-error-paths.test.ts: consolidate 4 groups of repetitive tests
into data-driven loops: 7 identifier validation tests, 6 prompt
validation tests, 5 cmdAgentInfo invalid-input tests, and 3 empty-input
tests — each group had identical structure (rejects.toThrow + exit(1))
with only the input varying. net: 21 separate tests → 4 compact loops
covering the same cases, reducing 41 lines of boilerplate.
- commands-cloud-info.test.ts: consolidate 8 separate "should reject cloud
with X" tests (invalid identifier describe block) into a single
data-driven loop, reducing 24 lines.
All 1413 tests still pass. biome lint clean.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- aws.test.ts: remove "all bundles have required fields" test that used
toBeTruthy() on id/label — fully redundant with the more specific
"bundle IDs follow naming convention" (/_3_0$/) and "labels include
pricing info" ($, /mo) tests below it.
- commands-cloud-info.test.ts: consolidate 3 separate tests for
"cloud with no implemented agents" that each fetched the same manifest,
called cmdCloudInfo("emptycloud"), and checked different assertions on
identical output into a single test.
- credential-hints.test.ts: merge "reports credentials appear set..."
and "lists the env var names when all are set" — identical setup (same
env vars, same function call) with overlapping assertions split across
two tests for no good reason.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
collectMissingCredentials() was incorrectly reporting saved credentials as
missing in two ways:
1. It only checked process.env.OPENROUTER_API_KEY, ignoring keys saved via
OAuth flow to ~/.config/spawn/openrouter.json
2. When hasCloudConfigCredentials() returned true, it filtered to keep
OPENROUTER_API_KEY in the missing list instead of returning []
Fix: also call hasSavedOpenRouterKey() before marking OPENROUTER_API_KEY as
missing, and return [] (not a filtered list) when cloud config exists.
Fixes#2639
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: offer delete or remap when server is gone from cloud provider
When a user tries to connect to a server that no longer exists, instead
of silently marking it as deleted, present an interactive picker that
lets them remap the history entry to an existing instance on the same
cloud or explicitly remove it from history.
- Add listServers() to Hetzner, DigitalOcean, AWS, and GCP providers
- Add updateRecordConnection() to history for remapping server details
- Add handleGoneServer() interactive flow in list.ts
- Fall back to silent deletion in non-interactive mode (SPAWN_NON_INTERACTIVE)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: move InstancesListSchema to module level
Declare valibot schema at module top level per project convention,
not inside the listServers() function body.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: extract shared CloudInstance type from duplicated inline types
The { id, name, ip, status } shape was declared inline 9 times across
5 files. Extract it as a shared CloudInstance interface in history.ts
and import it in all cloud providers and list.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add "Custom model" option to setup menu for OpenClaw
Adds a "Custom model" entry to the setup options multiselect. When
selected, prompts the user for an OpenRouter model ID (e.g.
anthropic/claude-sonnet-4) with validation. The model ID is passed
through via MODEL_ID env var to the orchestration pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: simplify custom model prompt text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
When DO API calls fail (billing issues, locked account, droplet creation
errors), users may be logged into the wrong account. Now shows email/team/
status and offers to re-authenticate before giving up.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
When AWS Lightsail's internal HTTP retry fires after a successful
create but dropped response, the NameExists error now checks if the
instance is in pending/running state and reuses it instead of failing.
Fixes#2630
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes Connection reset by peer failures on spotty networks by doubling
delay on each retry (10s→20s→40s→80s) and giving installAgent and
uploadConfigFile 4 attempts instead of 2.
Fixes#2631
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sort agent picker by github_stars descending so most popular agents
appear first. Add update-stars.sh script to QA quality sweep to keep
star counts fresh.
Security fixes from PR #2629 review:
- Validate repo format (owner/name pattern) before gh api calls
- Validate and canonicalize REPO_ROOT with realpath
Supersedes #2629.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
* feat: add downloadFile to CloudRunner + local OpenClaw config merge
Add `downloadFile(remotePath, localPath)` to the CloudRunner interface
and implement it across all 6 cloud providers (Hetzner, AWS, GCP,
DigitalOcean, Sprite, Local) — mirroring the existing `uploadFile` with
reversed SCP direction.
Replace the OpenClaw config write with a download → deep-merge → upload
flow so config merging happens in our own linted TypeScript instead of
a remote script.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: move isPlainObject and deepMerge to shared utils
Extract `isPlainObject` to `shared/type-guards.ts` and `deepMerge` to
`shared/parse.ts` so they're reusable across the codebase.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: promote isPlainObject to shared package, use across codebase
Move `isPlainObject` from cli/type-guards.ts into
@openrouter/spawn-shared so it can be used everywhere. Replace
inline `val !== null && typeof val === "object" && !Array.isArray(val)`
checks in:
- shared/type-guards.ts (toRecord, toObjectArray)
- shared/parse.ts (parseJsonObj)
- cli/manifest.ts (isValidManifest)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: remove type-guards re-export, import directly from spawn-shared
Delete `packages/cli/src/shared/type-guards.ts` (was just a re-export
barrel). All 35 consuming files now import `getErrorMessage`, `isString`,
`isNumber`, `isPlainObject`, `toRecord`, etc. directly from
`@openrouter/spawn-shared`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
doApi() throws on any non-2xx response before the isBillingError() check
at the call site could execute, making billing error detection dead code.
Wrap the POST /droplets call in asyncTryCatch so the thrown error message
(which includes the response body) is checked with isBillingError(). If it
matches a billing pattern, handleBillingError() is shown with the billing
page link and retry prompt — same UX as the proactive first-run warning.
Also adds a test asserting isBillingError() matches errors in the format
doApi throws (regression guard for #2395).
Fixes#2395
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
WhatsApp setup is too complex for normal users (QR scan + separate
device + pairing). Remove it from the setup options entirely.
Also change multiselect defaults to nothing pre-selected — let users
opt in to what they want instead of pre-selecting for them.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#2624
When reconnecting to an existing server via `spawn ls` or `spawn last`,
the CLI now queries the cloud provider API for the server's current IP
before attempting SSH. This prevents silent SSH timeouts when a server's
IP changes (e.g., after a restart or elastic IP reallocation).
Changes:
- Add `getServerIp()` to DigitalOcean, Hetzner, AWS, and GCP modules
- Add `updateRecordIp()` to history.ts to persist IP changes
- Add `refreshConnectionIp()` in list.ts that authenticates with the
cloud provider and refreshes the IP before enter/reconnect/fix actions
- If the server no longer exists, mark it deleted and inform the user
- If refresh fails (e.g., no credentials), fall back to cached IP
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add "Open Dashboard" as its own menu item for agents with tunnel
metadata (e.g., OpenClaw). Establishes an SSH tunnel, opens the
browser with the auth token, and waits for Enter to close.
The menu now shows both options for dashboard agents:
- Enter OpenClaw (launches TUI via SSH)
- Open Dashboard (opens web UI in browser)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When an agent has an SSH tunnel (e.g., OpenClaw dashboard), store the
tunnel remote port and browser URL template in connection.metadata at
spawn time. On reconnect via `spawn ls` → "Enter agent", re-establish
the SSH tunnel and open the dashboard automatically.
- Add saveMetadata() to history.ts for merging key-value pairs into records
- Store tunnel_remote_port and tunnel_browser_url_template in orchestrate.ts
- Re-establish tunnel in cmdEnterAgent (connect.ts) when metadata is present
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The OpenClaw dashboard (Control UI) is served by the Gateway on port
18789, which also handles WebSocket connections for agent communication.
Port 18791 is the internal Control Service — not the user-facing dashboard.
We were tunneling 18791, so the browser connected to the wrong service
and showed "Unauthorized" because the Control Service doesn't accept
token-based dashboard auth.
Fix: tunnel port 18789 (Gateway) and update all USER.md references.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OpenClaw 2026.3.7+ requires an explicit `gateway.auth.mode: "token"` field
when `gateway.auth.token` is set. Without it the gateway rejects auth and the
dashboard shows "Unauthorized".
Additionally, pass the token via URL fragment (`#token=`) instead of query
parameter (`?token=`) to match the updated auth flow and avoid leaking the
token in server logs / Referer headers (GHSA-rchv-x836-w7xp).
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ensureSshKeys tests had two identical tests covering the same code
path: "uses all keys in non-interactive mode when multiple exist" and
"uses all keys when multiselect is unavailable". Both created the same
two fake key pairs, used the same spawnSync mock, and made the identical
assertion (toHaveLength(2)).
The first test set SPAWN_NON_INTERACTIVE=1 which ensureSshKeys does not
check — stale logic from a removed interactive multiselect flow. The
second test referenced unavailable @clack/prompts multiselect which also
no longer exists in the implementation.
Consolidated into one deterministic test that also validates key ordering
(ed25519 sorts before rsa).
-- qa/dedup-scanner
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
The `sprite create` API call in `createSprite()` had no timeout, so when
the Sprite API blocked for certain agents (kilocode, opencode), the
process hung indefinitely. The bash-level timeout in provision.sh wraps
the outer subshell but the deeply-nested `sprite create` subprocess
could survive signal propagation.
Add a 300s (configurable via SPRITE_CREATE_TIMEOUT) timeout to the
`sprite create` subprocess using the existing killWithTimeout +
asyncTryCatch pattern already used by runSprite() and destroyServer().
Fixes#2612
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: messaging UX — silence doctor, fix groupPolicy, remove early WhatsApp pairing
- Set groupPolicy to "open" for both Telegram and WhatsApp (was
"allowlist" with empty allowFrom, causing doctor warnings)
- Suppress doctor warning spam by redirecting openclaw config set
stdout to /dev/null
- Remove WhatsApp pairing prompt (appeared immediately after QR scan
before user could message the bot — now just tells them the command)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: improve Telegram/WhatsApp pairing instructions
Add step-by-step instructions for Telegram pairing so users know to
search for their bot in Telegram and message it. Improve WhatsApp
post-link instructions to explain how contacts pair.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: pre-select Telegram in setup options as recommended channel
Telegram has the smoothest setup UX (bot token + pairing code) compared
to WhatsApp (QR scan + separate device). Pre-select it alongside Chrome
in the multiselect and label it as "recommended" in the hint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Telegram is a built-in channel, not a plugin. Replace broken
`openclaw plugins enable telegram` (OOM) and `openclaw channels add`
(doesn't exist) with proper setup:
- Write channel config (botToken, dmPolicy: pairing, groups) directly
into the atomic JSON config file during setup
- After gateway starts, prompt user to pair via
`openclaw pairing approve <channel> <CODE>`
- WhatsApp: QR scan via `openclaw channels login`, then pairing
- Bump version to 0.17.16
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- billing-guidance.test.ts: move stderrSpy.mockRestore() from each test
body to afterEach so restores run even when a test throws
- junie-agent.test.ts: add missing afterEach to restore stderrSpy that
was leaking across tests
- cloud-init.test.ts: consolidate repetitive needsNode/needsBun tests
into data-driven loops (8 individual its -> 2 parameterized loops)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: set telegram groupPolicy to open during channel setup
OpenClaw defaults groupPolicy to "allowlist" with an empty groupAllowFrom,
which silently drops all group messages. Set it to "open" after adding the
Telegram channel so group messages work out of the box.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use OpenClaw config file for Telegram setup instead of broken CLI commands
Telegram is a built-in channel in OpenClaw, not a plugin. The previous
approach used `openclaw plugins enable telegram` (caused OOM on 2GB) and
`openclaw channels add --channel telegram` (command doesn't exist).
Now writes Telegram config (botToken, enabled, groupPolicy) directly into
the atomic JSON config file during setup. Also sets groupPolicy to "open"
so group messages work out of the box instead of being silently dropped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use openclaw onboard for channel setup instead of manual config
OpenClaw has a built-in `openclaw onboard` command that interactively
guides users through Telegram/WhatsApp channel setup. Use that instead
of manually prompting for tokens and writing config ourselves.
- Remove custom Telegram token prompt from agent-setup.ts
- Remove broken `openclaw channels add` and `openclaw plugins enable`
- Run `openclaw onboard` after gateway starts for channel setup
- Base config (API key, gateway, model) still written atomically
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
openclaw-plugins OOMs on s-2vcpu-2gb (2GB) droplets during config
loading. Auto-upgrade to s-2vcpu-4gb when no custom size is set.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Consolidate 5 tests in sprite-keep-alive.test.ts that had identical
boilerplate (capturing session script or command list) into 2 tests:
- 2 installSpriteKeepAlive tests merged into 1 (both captured capturedCmds
to check different assertions about the same function call)
- 4 interactiveSession tests merged into 1 (all captured capturedSessionScript
to check different properties of the generated session script)
1391 → 1387 tests, zero regressions.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Adds `spawn fix [spawn-id]` command that SSHes into an existing VM and
re-applies agent setup without destroying or re-provisioning the server:
- Re-injects OpenRouter credentials and env vars into ~/.spawnrc
- Re-runs the agent's install command to get the latest version
- Also accessible via `spawn list` → "Fix this server" menu option
- Accepts optional spawn name/ID as positional argument
- Falls back to interactive picker for multiple active servers
- Single active server is fixed directly without prompting
Uses dependency injection (FixScriptRunner) for testability, following
the same pattern as confirmAndDelete's deleteHandler parameter.
Fixes#2589
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: move Telegram/WhatsApp channel setup to after gateway starts
OpenClaw's `channels add` and `channels login` commands require a running
gateway. Previously, Telegram token configuration ran in setupOpenclawConfig
(pre-gateway) using `openclaw config set`, causing the gateway to hang on
startup when a token was present for a disabled-by-default plugin.
Now:
- Plugin enables stay in setupOpenclawConfig (pre-gateway)
- Channel config (token add, QR login) runs in orchestrate.ts step 11c
after the gateway is up, using `openclaw channels add/login`
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* security: use shellQuote instead of jsonEscape for Telegram token
jsonEscape uses JSON.stringify which produces double-quoted strings that
the shell interprets, creating a command injection vector. shellQuote
wraps in single quotes, preventing shell interpretation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: fix biome export ordering in interactive.ts and manifest.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
* feat: add --beta images for DO marketplace images
Gate pre-built DigitalOcean marketplace images behind --beta images.
When active, uses hardcoded marketplace slugs (e.g. openrouter-spawnclaude)
instead of fresh Ubuntu + cloud-init, skipping agent install entirely.
All 8 images verified working via e2e smoke test (2026-03-13).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: sort exports to satisfy biome organizeImports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Added a note regarding the public anonymous survey and clarified that it is not a security vulnerability.
Signed-off-by: L <6723574+louisgv@users.noreply.github.com>
* feat: add `spawn feedback` subcommand
Sends anonymous feedback to the Spawn team via PostHog survey API.
Usage: spawn feedback "your message here"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update feedback survey ID and response key
Use the correct PostHog survey ID and $survey_response property.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use asyncTryCatch instead of try/catch in feedback command
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove 5 unused underscore-prefixed parameters that were accepted but
never read: extractFlagValue._flagLabel, performUpdate._remoteVersion,
reportDownloadFailure._primaryUrl/_fallbackUrl, buildRecordLabel._manifest,
and setupCodexConfig._apiKey. All callers updated accordingly.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>