Skip to content
Closed
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
4 changes: 2 additions & 2 deletions packages/cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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": "1.0.44",
"version": "1.0.46",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
15 changes: 10 additions & 5 deletions packages/cli/src/local/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
import { createCloudAgents } from "../shared/agent-setup.js";
import { downloadFile, runLocal, uploadFile } from "./local.js";

export const { agents, resolveAgent } = createCloudAgents({
runServer: runLocal,
uploadFile: async (l: string, r: string) => uploadFile(l, r),
downloadFile: async (r: string, l: string) => downloadFile(r, l),
});
export const { agents, resolveAgent } = createCloudAgents(
{
runServer: runLocal,
uploadFile: async (l: string, r: string) => uploadFile(l, r),
downloadFile: async (r: string, l: string) => downloadFile(r, l),
},
{
isLocal: true,
},
);
20 changes: 15 additions & 5 deletions packages/cli/src/shared/agent-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1195,7 +1195,12 @@ export async function setupSecurityScan(runner: CloudRunner): Promise<void> {

// ─── Default Agent Definitions ───────────────────────────────────────────────

function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
function createAgents(
runner: CloudRunner,
options?: {
isLocal?: boolean;
},
): Record<string, AgentConfig> {
return {
claude: {
name: "Claude Code",
Expand Down Expand Up @@ -1443,8 +1448,8 @@ function createAgents(runner: CloudRunner): Record<string, AgentConfig> {
`OPENROUTER_API_KEY=${apiKey}`,
`CURSOR_API_KEY=${apiKey}`,
],
configure: () => setupCursorProxy(runner),
preLaunch: () => startCursorProxy(runner),
configure: () => setupCursorProxy(runner, options),
preLaunch: () => startCursorProxy(runner, options),
launchCmd: () =>
'source ~/.spawnrc 2>/dev/null; export PATH="$HOME/.local/bin:$PATH"; agent --endpoint https://api2.cursor.sh',
promptCmd: (prompt) =>
Expand All @@ -1468,11 +1473,16 @@ function resolveAgent(agents: Record<string, AgentConfig>, name: string): AgentC
* Factory that creates agents + resolveAgent for a given CloudRunner.
* Replaces the identical 16-line boilerplate in each cloud's agents.ts.
*/
export function createCloudAgents(runner: CloudRunner): {
export function createCloudAgents(
runner: CloudRunner,
options?: {
isLocal?: boolean;
},
): {
agents: Record<string, AgentConfig>;
resolveAgent: (name: string) => AgentConfig;
} {
const agentMap = createAgents(runner);
const agentMap = createAgents(runner, options);
return {
agents: agentMap,
resolveAgent: (name: string) => resolveAgent(agentMap, name),
Expand Down
65 changes: 52 additions & 13 deletions packages/cli/src/shared/cursor-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,18 +272,41 @@ const CURSOR_DOMAINS = [
// ── Deployment ──────────────────────────────────────────────────────────────

/**
* Deploy the Cursor proxy infrastructure onto the remote VM.
* Deploy the Cursor proxy infrastructure onto a remote VM.
* Installs Caddy, uploads proxy scripts, writes Caddyfile, configures /etc/hosts.
*
* SAFETY: This modifies /etc/hosts and installs system-level services.
* Must NOT run on the user's local machine — only on remote VMs/containers.
*/
export async function setupCursorProxy(runner: CloudRunner): Promise<void> {
export async function setupCursorProxy(
runner: CloudRunner,
options?: {
isLocal?: boolean;
},
): Promise<void> {
if (options?.isLocal) {
logWarn("Cursor proxy setup skipped (not supported on local cloud — would modify host system)");
return;
}
logStep("Deploying Cursor→OpenRouter proxy...");

// 1. Install Caddy if not present
// Detect OS and arch at runtime so this works on macOS (darwin/arm64|amd64)
// and Linux (linux/amd64|arm64). Write to ~/.local/bin which is user-writable
// on every platform -- /usr/local/bin is SIP-protected on macOS and requires
// root elsewhere.
const installCaddy = [
"set -e",
'if command -v caddy >/dev/null 2>&1; then echo "caddy already installed"; exit 0; fi',
'echo "Installing Caddy..."',
'curl -sf "https://caddyserver.com/api/download?os=linux&arch=amd64" -o /usr/local/bin/caddy',
"chmod +x /usr/local/bin/caddy",
'OS=$(uname -s | tr "[:upper:]" "[:lower:]")',
"ARCH=$(uname -m)",
'[ "$ARCH" = "x86_64" ] && ARCH="amd64"',
'[ "$ARCH" = "aarch64" ] && ARCH="arm64"',
'mkdir -p "$HOME/.local/bin"',
'curl -fsSL "https://caddyserver.com/api/download?os=${OS}&arch=${ARCH}" -o "$HOME/.local/bin/caddy"',
'chmod +x "$HOME/.local/bin/caddy"',
'export PATH="$HOME/.local/bin:$PATH"',
"caddy version",
].join("\n");

Expand Down Expand Up @@ -322,18 +345,23 @@ export async function setupCursorProxy(runner: CloudRunner): Promise<void> {
logInfo("Proxy scripts deployed");

// 3. Configure /etc/hosts for domain spoofing
// Requires elevated privileges on all platforms (/etc/hosts is root-owned).
// Use a temp-file swap via sudo cp to avoid sed -i portability issues
// (macOS sed requires a backup suffix with -i; Linux does not).
const hostsScript = [
// Remove any existing cursor entries
'sed -i "/cursor\\.sh/d" /etc/hosts 2>/dev/null || true',
// Add our entries
`echo "127.0.0.1 ${CURSOR_DOMAINS.join(" ")}" >> /etc/hosts`,
// Build a clean copy without existing cursor entries then append ours
'grep -v "cursor.sh" /etc/hosts > /tmp/hosts_cursor_new 2>/dev/null || cp /etc/hosts /tmp/hosts_cursor_new',
`echo "127.0.0.1 ${CURSOR_DOMAINS.join(" ")}" >> /tmp/hosts_cursor_new`,
"sudo cp /tmp/hosts_cursor_new /etc/hosts",
"rm -f /tmp/hosts_cursor_new",
].join(" && ");

await wrapSshCall(runner.runServer(hostsScript));
logInfo("Hosts spoofing configured");

// 4. Install Caddy's internal CA cert
const trustScript = "caddy trust 2>/dev/null || true";
// Include ~/.local/bin in PATH since Caddy may have been installed there above.
const trustScript = 'export PATH="$HOME/.local/bin:$PATH" && caddy trust 2>/dev/null || true';
await wrapSshCall(runner.runServer(trustScript, 30));
logInfo("Caddy CA trusted");

Expand All @@ -353,9 +381,17 @@ CONF`,

/**
* Start the Cursor proxy services (Caddy + two Node.js backends).
* Uses systemd if available, falls back to setsid/nohup.
* Uses systemd if available, falls back to nohup (POSIX -- works on macOS and Linux).
*/
export async function startCursorProxy(runner: CloudRunner): Promise<void> {
export async function startCursorProxy(
runner: CloudRunner,
options?: {
isLocal?: boolean;
},
): Promise<void> {
if (options?.isLocal) {
return;
}
logStep("Starting Cursor proxy services...");

// Find Node.js binary (cursor bundles its own)
Expand All @@ -370,6 +406,8 @@ export async function startCursorProxy(runner: CloudRunner): Promise<void> {

const script = [
"source ~/.spawnrc 2>/dev/null",
// Ensure ~/.local/bin (where Caddy may be installed) is in PATH for this script.
'export PATH="$HOME/.local/bin:$PATH"',
nodeFind,

// Start unary backend
Expand All @@ -395,7 +433,8 @@ export async function startCursorProxy(runner: CloudRunner): Promise<void> {
" $_sudo systemctl daemon-reload",
" $_sudo systemctl restart cursor-proxy-unary",
" else",
" setsid $NODE ~/.cursor/proxy/unary.mjs < /dev/null &",
// setsid is Linux-only; nohup is POSIX and works on macOS and Linux.
" nohup $NODE ~/.cursor/proxy/unary.mjs < /dev/null > /tmp/cursor-proxy-unary.log 2>&1 &",
" fi",
"fi",

Expand Down Expand Up @@ -423,7 +462,7 @@ export async function startCursorProxy(runner: CloudRunner): Promise<void> {
" $_sudo systemctl daemon-reload",
" $_sudo systemctl restart cursor-proxy-bidi",
" else",
" setsid $NODE ~/.cursor/proxy/bidi.mjs < /dev/null &",
" nohup $NODE ~/.cursor/proxy/bidi.mjs < /dev/null > /tmp/cursor-proxy-bidi.log 2>&1 &",
" fi",
"fi",

Expand Down
Loading