the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 140 lines 3.9 kB view raw
1import WebSocket from "ws"; 2import chalk from "chalk"; 3import consola from "consola"; 4import getAccessToken from "../../lib/getAccessToken"; 5import { env } from "../../lib/env"; 6import type { Sandbox } from "../../types/sandbox"; 7 8// ── Protocol ────────────────────────────────────────────────────────────────── 9// 10// Server → Client (WS at /tty/:id/ws or /pty/:id/ws): 11// - Text frame raw PTY output, write directly to stdout 12// - Close shell exited or session error 13// 14// Client → Server: 15// - Text frame raw keystroke bytes (UTF-8) 16// - Text frame JSON { type: "resize", cols: number, rows: number } 17 18function toWsUrl(httpUrl: string, path: string, token: string): string { 19 const base = httpUrl.replace(/^http(s?)/, (_, s) => `ws${s}`); 20 const url = new URL(`${base}${path}`); 21 url.searchParams.set("token", token); 22 return url.toString(); 23} 24 25async function ssh(sandbox: Sandbox, tty: boolean = false): Promise<void> { 26 const token = await getAccessToken(); 27 const authToken = env.POCKETENV_TOKEN || token; 28 29 const baseUrl = tty ? env.POCKETENV_TTY_URL : env.POCKETENV_PTY_URL; 30 const wsUrl = toWsUrl(baseUrl, `/${sandbox.id}/ws`, authToken); 31 32 let cols = process.stdout.columns ?? 220; 33 let rows = process.stdout.rows ?? 50; 34 35 consola.info( 36 `Connecting to ${chalk.cyanBright(sandbox.name)} via ${tty ? "TTY" : "PTY"} WebSocket…`, 37 ); 38 39 const ws = new WebSocket(wsUrl, { 40 headers: { "User-Agent": "pocketenv-cli" }, 41 }); 42 43 let exiting = false; 44 let stdinAttached = false; 45 46 function sendResize(c: number, r: number): void { 47 if (ws.readyState === WebSocket.OPEN) { 48 ws.send(JSON.stringify({ type: "resize", cols: c, rows: r })); 49 } 50 } 51 52 function teardown(code = 0): void { 53 if (exiting) return; 54 exiting = true; 55 56 if (process.stdin.isTTY) { 57 try { 58 process.stdin.setRawMode(false); 59 } catch { 60 // already restored 61 } 62 } 63 process.stdin.pause(); 64 65 if ( 66 ws.readyState === WebSocket.OPEN || 67 ws.readyState === WebSocket.CONNECTING 68 ) { 69 ws.close(1000, "client disconnect"); 70 } 71 72 process.exit(code); 73 } 74 75 function attachStdin(): void { 76 if (stdinAttached) return; 77 stdinAttached = true; 78 79 if (process.stdin.isTTY) { 80 process.stdin.setRawMode(true); 81 } 82 process.stdin.resume(); 83 84 process.stdin.on("data", (chunk: Buffer) => { 85 if (chunk.includes(0x0b)) { 86 // Ctrl+K — local escape hatch 87 teardown(0); 88 return; 89 } 90 if (ws.readyState === WebSocket.OPEN) { 91 ws.send(chunk.toString("utf-8")); 92 } 93 }); 94 95 process.stdout.on("resize", () => { 96 cols = process.stdout.columns ?? cols; 97 rows = process.stdout.rows ?? rows; 98 sendResize(cols, rows); 99 }); 100 } 101 102 process.stdout.write(`\x1b[35mConnecting to terminal...\x1b[0m\r\n`); 103 104 ws.on("open", () => { 105 process.stdout.write("\r\x1b[K"); 106 sendResize(cols, rows); 107 attachStdin(); 108 }); 109 110 ws.on("message", (raw: WebSocket.RawData, isBinary: boolean) => { 111 process.stdout.write(isBinary ? (raw as Buffer) : raw.toString("utf-8")); 112 }); 113 114 ws.on("close", (code, reason) => { 115 if (!exiting) { 116 const msg = reason.length ? ` (${code}${reason})` : ""; 117 if (msg) { 118 process.stderr.write( 119 `\r\n${chalk.yellow("Connection closed")}${msg}\r\n`, 120 ); 121 } 122 teardown(0); 123 } 124 }); 125 126 ws.on("error", (err: Error) => { 127 consola.error("WebSocket error:", err.message); 128 teardown(1); 129 }); 130 131 process.on("SIGINT", () => teardown(0)); 132 process.on("SIGTERM", () => teardown(0)); 133 134 await new Promise<void>((resolve) => { 135 ws.on("close", resolve); 136 ws.on("error", () => resolve()); 137 }); 138} 139 140export default ssh;