Commit graph

220 commits

Author SHA1 Message Date
A
c55b78235c
test: fix inline source copies — import real isValidManifest instead of stale replica (#1900)
The isValidManifest copy in manifest-cache-lifecycle.test.ts was already
out of sync with the real implementation (missing typeof check,
!Array.isArray guard, and "in" operator checks added since the copy was
made). Export the real function and import it so tests break when the
source changes.

Also remove the CSRF state generation describe block from do-oauth.test.ts —
it tested an inline copy of a private function, not the real source.

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 22:04:03 -08:00
A
588cecc435
fix: resolve biome nested root configuration conflict (#1896)
Move shared biome.json from lint/ to repo root so it's the single root
config. Nested configs (packages/cli, .claude/skills/setup-spa) get
`"root": false` via `biome migrate`. This fixes lint failing when run
from the repo root.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 21:57:50 -08:00
A
9e54f0cf57
ci: add Mock Tests job to satisfy required status check (#1904)
* ci: add Mock Tests job to satisfy required status check

Split the unit-tests job into mock-tests (runs bun test) and unit-tests
(verifies cloud bundles build). The repo ruleset requires "Mock Tests",
"Unit Tests", and "Biome Lint" checks — the missing "Mock Tests" job was
blocking all PR merges.

Fixes #1901

Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* style: fix pre-existing Biome format issues in 9 files

Auto-applied Biome formatter to src/ to resolve failing "Biome Lint"
required status check. No logic changes — formatting only.

Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-25 00:54:33 -05:00
A
40417d845d
fix: use single-quote escaping for restart loop in interactiveSession (#1893)
JSON.stringify double-quoting caused two bugs in the restart wrapper:
1. Literal \n instead of newlines (bash doesn't interpret \n in "...")
2. Shell variables ($vars) expanded to empty strings before script ran

Affected clouds: fly, gcp, hetzner, digitalocean, aws.
Daytona already had the correct single-quote escaping.

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 18:47:23 -05:00
A
d423ea57d5
refactor: deduplicate pickToTTY as wrapper around pickToTTYWithActions (#1891)
Fixes #1890

Agent: complexity-hunter

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 17:48:57 -05:00
A
5b9ceef060
perf: replace child_process.spawn with Bun.spawn for interactive sessions (#1888)
Using Node's child_process.spawn() to launch interactive SSH/shell sessions
from inside a Bun process adds unnecessary overhead: an extra process fork,
PTY negotiation indirection, and a forced Bun→Node stdio context switch.

Switch all interactiveSession() functions to Bun.spawn() with
stdio: ["inherit","inherit","inherit"], which hands off file descriptors
directly without forking a Node wrapper process.

Also removes the 500ms hardcoded sleep in orchestrate.ts that was a
band-aid for the old child_process handoff latency. The synchronous
prepareStdinForHandoff() is sufficient on its own.

Affected clouds: hetzner, aws, gcp, digitalocean, fly, daytona, sprite, local
Also fixes runInteractiveCommand() in commands.ts (spawn connect).

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 15:17:53 -05:00
A
80c044dbb3
fix: correct CLI entry point path in E2E provision script (#1887)
* fix: correct CLI entry point path in E2E provision script

The path resolution went up 2 levels (../../) from sh/e2e/lib/ which
landed in sh/ instead of the repo root. After the monorepo restructure,
packages/cli/src/index.ts is at the repo root — need 3 levels (../../..).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: format p.text({ message }) to multi-line for biome

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:16:07 -05:00
A
43627ca793
fix(digitalocean): fix silent exit after droplet name prompt (#1885)
- Set SPAWN_NAME_KEBAB alongside SPAWN_NAME in runBash/runBashHeadless
  so cloud scripts (DO, Fly, AWS, etc.) skip their redundant name prompts
- Call prepareStdinForHandoff() before spawnBash() to clean up stdin
  state left by @clack/prompts, preventing child process hangs
- Race prompt() against stdin close event so it rejects with an error
  instead of hanging forever when stdin dies unexpectedly
- Bump CLI version to 0.10.5

Fixes #1884

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 14:27:50 -05:00
A
2a1ed4164e
fix(gcp): add network/subnet flags to fix custom VPC subnet mode (#1883)
GCP instance creation was failing with 'Invalid value for field
resource.networkInterfaces[0].subnetwork' when the project VPC uses
custom subnet mode. Add --network and --subnet flags defaulting to
'default', with GCP_NETWORK and GCP_SUBNET env var overrides for
custom VPC setups.

Fixes #1882

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 14:27:46 -05:00
Ahmed Abushagur
5ffa0f663b
fix: use @clack/prompts for all text input to prevent stdin death after multiselect (#1881)
The SSH key multiselect (@clack/prompts) creates and destroys its own
readline interface on stdin. Due to Bun #1707, subsequent Node readline
interfaces silently receive an immediate "close" event — causing the
model selection prompt to exit the process without reading input.

Fix: replace the readline-based prompt() with p.text() from @clack/prompts,
so all stdin interactions use the same library and never trigger the bug.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 13:52:40 -05:00
A
0edc588629
fix(daytona): use single-quote escaping in interactiveSession to prevent shell injection (#1880)
Replace JSON.stringify double-quoting with single-quote escaping for the
cmd argument in interactiveSession(). Double-quoted strings in bash allow
$() and ${} expansion, making the previous pattern vulnerable to injection
if cmd ever contained shell metacharacters. Single-quoted strings prevent
ALL shell expansion, matching the defense-in-depth approach Fly already uses.

Fixes #1879

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 12:28:26 -05:00
A
7419ff8290
fix: use npm install for OpenClaw to fix Node module resolution (#1878)
OpenClaw runs under Node, but bun's flat node_modules layout breaks
Node's require() resolver when dynamically loading channel plugins.
Switching to npm install ensures standard node_modules layout.

Fixes #1875

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 04:51:09 -05:00
A
4e1467a521
fix: remove broken clone_cli() — go straight to pre-built binary (#1873)
* fix: remove broken clone_cli() — go straight to pre-built binary

The clone_cli() function (added before the monorepo migration, PR #1853)
only fetches top-level .ts files via the GitHub Contents API. Since the
monorepo reorganised source code into subdirectories (aws/, fly/, hetzner/,
shared/, etc.), clone_cli() silently downloads an incomplete source tree.
bun run build then always fails because cross-directory imports cannot
resolve, and the installer falls through to the pre-built binary anyway.

Every install was burning ~12 unnecessary GitHub API requests (rate-limited
at 60/hr for unauthenticated clients) and several seconds of wasted bun
install + failed build time.

Fix: remove clone_cli() entirely, replace build_and_install() with a
direct binary download. Behaviour is identical for all users (binary path
was already the universal outcome); installs are now faster and cheaper on
the API rate limit.

Agent: code-health
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: update install.sh tests for simplified binary-only installer

Remove tests for clone_cli() and source builds which were removed in
the parent commit. Add tests verifying the direct binary download
approach and asserting that the old clone/build code is gone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix biome format errors in commands.ts and duplicate-detection test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 01:19:30 -08:00
A
038c94c72a
feat(cli): detect duplicate --name and route to existing-instance picker (#1866)
When `spawn <agent> <cloud> --name "foo"` is run and an active instance
named "foo" already exists for that agent + cloud, cmdRun now checks
getActiveServers() after resolving the spawn name.  On a match it shows
a warning and presents the same action picker the user gets from
`spawn ls` (Enter agent / SSH / Spawn a new VM / Delete), instead of
silently provisioning a duplicate VM.

When "Spawn a new VM" is chosen from the picker, SPAWN_NAME is cleared
so the user is prompted for a fresh name, preventing an infinite
duplicate-detection loop.

Bumps CLI to 0.10.3.

Fixes #1864

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 03:50:22 -05:00
A
7c53b4e4f6
fix: replace 9 CommonJS require() calls with ESM import in history test (#1871)
The project is ESM-only ("type": "module") and CLAUDE.md bans
require(). All 9 `const { homedir } = require("node:os")` calls
replaced with a single top-level `import { homedir } from "node:os"`.

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 00:16:22 -08:00
A
01b23042b8
style: enforce separated type imports via biome (#1870)
* refactor: clean up SPA code — extract helpers, use isString, DRY Slack calls

helpers.ts:
- Extract parseAssistantEvent/parseUserEvent/formatToolHint from parseStreamEvent
- Eliminate redundant second scans for toolName and isError (captured during loop)
- Use isString() from @openrouter/spawn-shared instead of typeof checks
- Split long node:fs import across multiple lines

main.ts:
- Extract postOrUpdate() to DRY the post-vs-update pattern (was 3 copy-paste blocks)
- Add SlackClient type alias (replaces 4x InstanceType<typeof App>["client"])
- Remove unused Mapping import
- Inline REQUIRED_VARS into for loop
- Rename currentMsgTs → msgTs
- Tighten formatting throughout

spa.test.ts:
- Remove unused beforeEach import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add biome config to SPA, fix all lint/format, add PostToolUse hook

- Add biome.json extending lint/biome.json (2-space indent, 120 line width,
  useBlockStatements, expand:always, etc.)
- Fix all 36 useBlockStatements violations (braceless if/continue/return)
- Fix all format issues (line width, expand, trailing commas)
- Add biome lint+format to PostToolUse hook — runs automatically on any
  .ts file edit when a biome.json is found in the file's directory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: enforce separated type imports via biome useImportType

Add `style: "separatedType"` to the base biome config's useImportType
rule. This enforces `import type { T }` on its own line instead of
mixing `type` into value imports (`import { type T, foo }`).

Auto-fixed 21 violations across CLI (18) and SPA (3).

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>
2026-02-24 00:16:03 -08:00
A
c701b745ab
fix: add process supervision restart loop for agents on cloud VMs (#1862)
When an agent process dies on a cloud VM (SIGTERM, OOM, crash), it now
automatically restarts after 5 seconds, up to 10 times. Clean exits
(code 0) break out immediately. Local execution is unaffected.

Fixes #1860

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 02:51:31 -05:00
A
9f6bc423ac
fix: add swap space before ZeroClaw install to prevent OOM on nano instances (#1851)
* fix: add swap space before ZeroClaw install to prevent OOM on nano instances

ZeroClaw's Rust compilation gets OOM-killed on nano_3_0 (512 MB) — build
fails at a random dependency each run. Add ensureSwapSpace() that creates
a 1 GB swap file before running the installer:

- Idempotent: skips silently if swap already exists
- Non-fatal: logs a warning if sudo fails (larger instances won't need it)
- Timeout bumped from 5 min to 10 min (swap-backed builds are slower)
- Defense-in-depth: --prefer-prebuilt avoids compilation in the common
  case, but fallback source builds still need memory

Fixes #1840

Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add input validation to ensureSwapSpace() to prevent command injection

Validate sizeMb is a positive integer before interpolating into shell
commands, as requested in security review.

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-23 23:12:13 -08:00
A
f5f020d250
feat: Result monad in shared + Claude Code fixtures + SPA Result adoption (#1858)
* refactor: split SPA into helpers + main, add build script and tests

Split slack-bot.ts into helpers.ts (pure functions) and main.ts (entry
point) for testability. Add build.ts to bundle SPA into spa.js. Add
spa.test.ts with 19 tests covering stream parsing and text helpers.

Improved streaming: tool_use and tool_result events get their own Slack
messages instead of concatenating everything into one. Prompt is passed
via stdin to avoid CLI flag parsing issues with user content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: drop build.ts — run main.ts directly via bun

Bun runs TypeScript natively, no bundling step needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: move Result monad to shared, add Claude Code fixtures, use Result in SPA

- Move Result type/Ok/Err from packages/cli/src/shared/result.ts to
  packages/shared/src/result.ts and re-export from @openrouter/spawn-shared
- Update CLI imports (ui.ts) to use the shared package
- Add fixtures/claude-code/ with realistic stream-json events covering
  all event types (assistant text, tool_use, user tool_result, result)
- Refactor SPA helpers to return Result<T> instead of throwing/returning null:
  loadState() → Result<State>, saveState() → Result<void>,
  downloadSlackFile() → Result<string>, addMapping() → Result<void>
- Update main.ts call sites to handle Result returns
- Update SPA tests to import events from fixtures and test Result returns
- Bump CLI version 0.10.0 → 0.10.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: biome format issues in aws.test.ts, aws.ts, daytona.ts

Expand inline objects/arrays to multi-line format to satisfy biome
formatter rules. No logic changes.

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>
2026-02-23 22:48:51 -08:00
A
65f6f1be32
feat: Bun workspace monorepo — packages/cli + packages/shared (#1853)
Restructure the repo as a Bun workspace monorepo:

- Move cli/ → packages/cli/
- Create packages/shared/ (@openrouter/spawn-shared) with type-guards and parse utilities
- Add root package.json with workspace configuration
- Update all CLI imports to use @openrouter/spawn-shared
- Deduplicate toRecord/toObjectArray helpers from 4 cloud modules
- Update SPA (slack-bot) to use shared package instead of local toObj()
- Update 48 agent shell scripts for new packages/cli/ path
- Update install.sh, install.ps1, e2e, and test scripts
- Update all GitHub workflows, .gitignore, pre-commit hooks
- Update CLAUDE.md, README.md, and skill prompt references
- Pin all dependency versions (no ^ ranges)
- Bump CLI version 0.9.1 → 0.10.0

All 1908 tests pass. Lint clean. All 8 cloud bundles build.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 22:07:05 -08:00