@typhons/sandbox-tools 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import{execSync as s}from"child_process";import a from"fs";import{setTimeout as x}from"timers/promises";var f=30,g=1,C=100,w=!1,v=!1,u=process.argv.slice(2);for(let e=0;e<u.length;e++)u[e]==="--json"?w=!0:u[e]==="--threshold"?f=parseInt(u[++e],10):u[e]==="--daemon"&&(v=!0);function N(){try{return a.readFileSync("/run/e2b/.E2B_SANDBOX_ID","utf8").trim()}catch{}try{return a.readFileSync("/opt/sandbox/sandbox-id","utf8").trim()}catch{}return process.env.E2B_SANDBOX_ID||""}function $(e){try{if(e==="claude")return s("pgrep -x claude 2>/dev/null | head -1",{encoding:"utf8"}).trim()||null;if(e==="codex"){let t=s("pgrep -f 'node.*codex' 2>/dev/null",{encoding:"utf8"}).trim().split(`
3
+ `);for(let o of t)if(o)try{let n=a.readFileSync(`/proc/${o}/cmdline`,"utf8").replace(/\0/g," ");if(/codex\.js|codex\/bin|@openai\/codex/.test(n))return o}catch{}}}catch{}return null}function E(){try{let e=s('find /home -name "*.jsonl" -path "*/.claude/projects/*" ! -path "*/subagents/*" -printf "%T@ %p\\n" 2>/dev/null | sort -rn | head -1',{encoding:"utf8"}).trim();if(e){let t=parseInt(e.split(" ")[0],10);return Math.floor(Date.now()/1e3)-t}}catch{}return-1}function T(){let e=-1,t=["/home/*/.codex/log/codex-tui.log","/root/.codex/log/codex-tui.log"];for(let o of t)try{let n=s(`ls ${o} 2>/dev/null`,{encoding:"utf8"}).trim().split(`
4
+ `);for(let c of n)if(c)try{let d=a.statSync(c),l=Math.floor(Date.now()/1e3)-Math.floor(d.mtimeMs/1e3);(e===-1||l<e)&&(e=l)}catch{}}catch{}return e}function I(e){if(!e)return 0;let t=0;try{let o=s(`pgrep -P ${e} 2>/dev/null`,{encoding:"utf8"}).trim().split(`
5
+ `);for(let n of o)if(n)try{if(a.readFileSync(`/proc/${n}/cmdline`,"utf8").replace(/\0/g," ").includes("check-claude-active"))continue;t++}catch{t++}}catch{}return t}function r(e){try{let o=a.readFileSync(`/proc/${e}/io`,"utf8").match(/^rchar:\s*(\d+)/m);return o?parseInt(o[1],10):0}catch{return 0}}async function _(e){if(!e)return 0;let t=r(e);return await x(g*1e3),r(e)-t}async function S(){let e=$("claude"),t=$("codex"),o=E(),n=T(),c=o>=0&&o<f||n>=0&&n<f,d=I(e),l=I(t),y=d>0||l>0,i=0;if(e&&t){let A=r(e),O=r(t);await x(g*1e3);let b=r(e),D=r(t);i=Math.max(b-A,D-O)}else e?i=await _(e):t&&(i=await _(t));let p=i>C,h=c||y||p,m=e&&t?"both":e?"claude":t?"codex":"none";return w||v?console.log(JSON.stringify({active:h,agent:m,claude_pid:e?parseInt(e):null,codex_pid:t?parseInt(t):null,claude_jsonl_age:o,codex_log_age:n,log_active:c,children:d+l,io_delta:i,io_active:p})):!e&&!t?console.log("Agents: not running"):h?(console.log(`Agent: ACTIVE (${m})`),c&&(o>=0&&o<f&&console.log(` Claude JSONL written ${o}s ago`),n>=0&&n<f&&console.log(` Codex log written ${n}s ago`)),y&&console.log(` ${d} claude + ${l} codex child process(es)`),p&&console.log(` I/O: ${i} bytes read in ${g}s`)):(console.log(`Agent: idle (${m} running, waiting for user)`),e&&console.log(` Claude JSONL last written ${o}s ago`),t&&console.log(` Codex log last written ${n}s ago`),console.log(" No child processes"),console.log(` I/O: ${i} bytes in ${g}s`)),h}if(v){let e=N();e||(console.error("[agent-keepalive] ERROR: cannot determine sandbox ID"),process.exit(1));let t=`https://49999-${e}.e2b.app/`;for(console.log(`[agent-keepalive] sandbox=${e} ping_url=${t} interval=30s`);;){let o=await S(),n=new Date().toISOString().slice(11,19);if(o){let c="err";try{c=s(`curl -sf -o /dev/null -w '%{http_code}' --max-time 5 "${t}" 2>/dev/null`,{encoding:"utf8"}).trim()}catch{}}await x(29e3)}}else{let e=await S();process.exit(e?0:1)}
package/clone.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import u from"fs";import E from"crypto";import{setTimeout as m}from"timers/promises";var l="",n="",i=process.env.TYPHONS_SERVER||"https://insta.d.typhons.dev:3000",r="",t=process.argv.slice(2).map(e=>e.replace(/[\x00-\x1f\x7f]/g,""));for(let e=0;e<t.length;e++)switch(t[e]){case"--name":case"-n":l=t[++e];break;case"--box":case"-b":n=t[++e];break;case"--server":case"-s":i=t[++e];break;case"--api-key":case"-k":r=t[++e];break;case"-h":case"--help":console.log("Usage: clone [--name NAME] [--box SOURCE_BOX] [--server URL] [--api-key KEY]"),console.log(""),console.log("Clone a sandbox. If --box is omitted, detects the current sandbox."),console.log("If --name is omitted, auto-generates a name."),process.exit(0);default:t[e].startsWith("-")||(l=t[e])}if(!r)for(let e of[`${process.env.HOME}/.config/typhons/api-key`,`${process.env.HOME}/.config/remote-claude/api-key`])try{r=u.readFileSync(e,"utf8").trim();break}catch{}!r&&process.env.TYPHONS_API_KEY&&(r=process.env.TYPHONS_API_KEY);r||(console.error("Error: No API key found."),console.error(" Set TYPHONS_API_KEY, pass --api-key, or save to ~/.config/typhons/api-key"),process.exit(1));if(!n){let e="";try{e=u.readFileSync("/run/e2b/.E2B_SANDBOX_ID","utf8").trim()}catch{}if(e)try{let a=(await(await fetch(`${i}/api/machines`,{headers:{Authorization:`Bearer ${r}`},signal:AbortSignal.timeout(1e4)})).json()).find(d=>d.machine?.sandboxId===e||d.sandboxId===e);a&&(n=a.app?.name||a.devboxName||"")}catch{}n||(console.error("Error: Could not detect current sandbox."),console.error(` sandbox_id=${e}, server=${i}`),console.error(" Use --box to specify."),process.exit(1))}var S=process.env.USER||"root",b=i.match(/https?:\/\/[^.]*\.(.+?)(?::\d+)?$/),g=b?b[1]:"d.typhons.dev",_=g.replace(/^d\./,"");function $(){try{return u.readFileSync("/run/e2b/.E2B_SANDBOX_ID","utf8").trim()}catch{}return""}async function f(e,s,p){try{let a={method:e,headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json"},signal:AbortSignal.timeout(5e3)};return p&&(a.body=JSON.stringify(p)),await(await fetch(`${i}${s}`,a)).json()}catch{return{}}}function h(e){console.log(`Clone ready: ${e}`),console.log(` ssh ${S}.${e}@ssh.${_}`),console.log(` https://${e}.${g}:3000`)}console.log(`Cloning ${n}...`);var I=E.randomBytes(24).toString("base64url").slice(0,32);function x(){let e={idempotencyKey:I},s=$();return s&&(e.sandboxId=s),l&&(e.name=l),e}var o=await f("POST",`/api/machines/${n}/clone`,x()),c=o.jobId||"";c||(o.error&&(console.error(`Error: ${o.error}`),process.exit(1)),o.name&&(h(o.name),process.exit(0)));var y="";for(let e=0;e<120;e++){if(!c&&(o=await f("POST",`/api/machines/${n}/clone`,x()),c=o.jobId||"",o.status==="ready"&&(h(o.name),process.exit(0)),o.status==="you_are_the_clone"&&(console.log(`You are the clone (${o.name}).`),process.exit(0)),!c)){await m(1e3);continue}o=await f("GET",`/api/clone-jobs/${c}`);let s=o.status||"";if(s!==y){switch(s){case"snapshotting":console.log("Snapshotting... (the current devbox will briefly freeze)");break;case"creating":console.log("Snapshot done, cloning to new machine...");break;case"registering":console.log("Waiting for clone to be ready...");break;case"auth":break;case"ready":h(o.name),process.exit(0);case"failed":console.error(`Error: ${o.error||"Clone failed"}`),process.exit(1)}y=s}await m(1e3)}console.error("Error: Clone did not complete within 2 minutes");process.exit(1);
package/ipc.js ADDED
@@ -0,0 +1 @@
1
+ import l from"net";import a from"fs";var S="/tmp/session-proxy.sock",y=2*1024*1024,h=class{constructor(t=y){this.capacity=t,this.chunks=[],this.totalBytes=0,this.trimmedTo=0}append(t){let e=this.totalBytes;return this.chunks.push({offset:e,data:Buffer.from(t)}),this.totalBytes+=t.length,this._trim(),e}sliceFrom(t){if(t>=this.totalBytes)return Buffer.alloc(0);if(t<this.trimmedTo)throw new Error(`RING_OVERFLOW: requested offset ${t} already trimmed (trimmedTo=${this.trimmedTo})`);let e=[];for(let n of this.chunks){if(n.offset+n.data.length<=t)continue;let i=Math.max(0,t-n.offset);e.push(n.data.slice(i))}return Buffer.concat(e)}_trim(){for(;this.chunks.length>1&&!(this.totalBytes-this.chunks[0].offset<=this.capacity);){let e=this.chunks.shift();this.trimmedTo=e.offset+e.data.length}}},d=1,m=2;function x(r,t){if(!r||r.destroyed)return!1;let e=Buffer.alloc(5);return e[0]=d,e.writeUInt32LE(t.length,1),r.write(e),r.write(t),!0}function w(r,t){if(!r||r.destroyed)return!1;let e=Buffer.from(JSON.stringify(t)),n=Buffer.alloc(5);return n[0]=m,n.writeUInt32LE(e.length,1),r.write(n),r.write(e),!0}var u=class{constructor(t,e){this.onBinary=t,this.onJSON=e,this.buf=Buffer.alloc(0)}feed(t){for(this.buf=this.buf.length?Buffer.concat([this.buf,t]):t;this.buf.length>=5;){let e=this.buf[0],n=this.buf.readUInt32LE(1);if(this.buf.length<5+n)break;let s=this.buf.slice(5,5+n);if(this.buf=this.buf.slice(5+n),e===d)this.onBinary(s);else if(e===m)try{this.onJSON(JSON.parse(s.toString()))}catch(i){console.error(`[ipc] bad JSON: ${i.message}`)}}}};function I(r,t){try{a.unlinkSync(r)}catch{}let e=l.createServer(t);return e.listen(r,()=>{try{a.chmodSync(r,511)}catch{}}),e}function N(r,{onConnect:t,onClose:e,onError:n,retryMs:s=500}={}){let i=null,o=!1,c=null;function f(){o||(i=l.createConnection(r),i.on("connect",()=>{t&&t(i)}),i.on("close",()=>{i=null,e&&e(),o||(c=setTimeout(f,s))}),i.on("error",p=>{n&&n(p)}))}return f(),{getSocket(){return i},close(){o=!0,c&&clearTimeout(c),i&&i.destroy()}}}function T(r,t,e,n){let s=Buffer.alloc(17);return s.write(r,0,8,"ascii"),s[8]=t,s.writeBigUInt64BE(BigInt(e),9),Buffer.concat([s,n])}function b(r){return r.length<17?null:{sessionId:r.slice(0,8).toString(),direction:r[8],offset:Number(r.readBigUInt64BE(9)),payload:r.slice(17)}}export{u as FrameParser,y as MAX_RING,h as RingBuffer,S as SOCK_PATH,N as connectIPC,I as createIPCServer,b as decodeDataFrame,T as encodeDataFrame,x as sendBinary,w as sendJSON};
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@typhons/sandbox-tools",
3
+ "version": "0.4.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "sandbox-relay-client": "./relay-client.js",
7
+ "sandbox-session-proxy": "./session-proxy.js",
8
+ "sandbox-watchdog": "./sandbox-watchdog.js",
9
+ "sandbox-start-all": "./sandbox-start-all.js",
10
+ "check-claude-active": "./check-claude-active.js",
11
+ "stop-agents": "./stop-agents.js",
12
+ "sandbox-clone": "./clone.js"
13
+ },
14
+ "dependencies": {
15
+ "ws": "^8.17.0",
16
+ "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1"
17
+ }
18
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import b from"ws";import m from"fs";import{execSync as x,execFileSync as U}from"child_process";import{dirname as L}from"path";import{fileURLToPath as D}from"url";import P from"net";var T="/tmp/session-proxy.sock",q=2*1024*1024;var B=1,I=2;function C(e,t){if(!e||e.destroyed)return!1;let n=Buffer.alloc(5);return n[0]=B,n.writeUInt32LE(t.length,1),e.write(n),e.write(t),!0}function R(e,t){if(!e||e.destroyed)return!1;let n=Buffer.from(JSON.stringify(t)),s=Buffer.alloc(5);return s[0]=I,s.writeUInt32LE(n.length,1),e.write(s),e.write(n),!0}var E=class{constructor(t,n){this.onBinary=t,this.onJSON=n,this.buf=Buffer.alloc(0)}feed(t){for(this.buf=this.buf.length?Buffer.concat([this.buf,t]):t;this.buf.length>=5;){let n=this.buf[0],s=this.buf.readUInt32LE(1);if(this.buf.length<5+s)break;let i=this.buf.slice(5,5+s);if(this.buf=this.buf.slice(5+s),n===B)this.onBinary(i);else if(n===I)try{this.onJSON(JSON.parse(i.toString()))}catch(r){console.error(`[ipc] bad JSON: ${r.message}`)}}}};function _(e,{onConnect:t,onClose:n,onError:s,retryMs:i=500}={}){let r=null,f=!1,l=null;function g(){f||(r=P.createConnection(e),r.on("connect",()=>{t&&t(r)}),r.on("close",()=>{r=null,n&&n(),f||(l=setTimeout(g,i))}),r.on("error",a=>{s&&s(a)}))}return g(),{getSocket(){return r},close(){f=!0,l&&clearTimeout(l),r&&r.destroy()}}}var ce=L(D(import.meta.url));process.on("uncaughtException",e=>{console.error(`[relay-client] UNCAUGHT: ${e.message}`),console.error(e.stack)});process.on("unhandledRejection",e=>{console.error(`[relay-client] UNHANDLED REJECTION: ${e.message||e}`)});var F=process.env.RELAY_HOST||"ssh.typhons.dev",v=process.env.SECRET||"",J="/run/e2b/.RELAY_TOKEN",Y=500,H=3e3,G=6e4,w="@typhons/sandbox-tools";function K(){try{return m.readFileSync(J,"utf8").trim()}catch{}return""}function A(){try{return m.readFileSync("/run/e2b/.E2B_SANDBOX_ID","utf8").trim()}catch{}return"unknown"}function o(e){console.log(`[relay-client ${new Date().toISOString().slice(11,19)}] ${e}`)}var p="unknown";function z(){if(process.env.RELAY_URL)return process.env.RELAY_URL;p=A();try{let e=m.readFileSync("/opt/sandbox/sandbox-id","utf8").trim();if(e&&e!==p){o(`Clone detected (${e} -> ${p}), interrupting agents...`);try{x("/opt/sandbox/bin/stop-agents",{timeout:1e4,stdio:"inherit"})}catch{}try{m.writeFileSync("/opt/sandbox/sandbox-id",p)}catch{}}}catch{}return`wss://${F}/tunnel/${p}`}var d=null,N=null;function W(){return _(T,{onConnect(t){o("Connected to session-proxy via IPC"),d=t,N=new E(n=>{c&&c.readyState===b.OPEN&&c.send(n)},n=>{c&&c.readyState===b.OPEN&&c.send(JSON.stringify(n))}),t.on("data",n=>N.feed(n))},onClose(){o("IPC connection to session-proxy lost, will retry..."),d=null,N=null},onError(t){t.code!=="ENOENT"&&t.code!=="ECONNREFUSED"&&o(`IPC error: ${t.message}`)}})}var c=null,$=null,u=0,y=null;function M(){if(c){try{c.terminate()}catch{}c=null}y&&(clearTimeout(y),y=null),y=setTimeout(()=>{y=null,O()},Y)}function O(){let e=++u,t=z();o(`Connecting to ${t}... (gen=${e}, sandbox=${p})`);let n={},s=K();if(s?n.Authorization=`Bearer ${s}`:v&&(n.Authorization=`Bearer ${v}`),c){try{c.terminate()}catch{}c=null}let i=new b(t,{headers:n});c=i;let r=null,f=!0;function l(){r&&(clearInterval(r),r=null),e===u&&M()}function g(){f=!0,r=setInterval(()=>{if(e!==u){clearInterval(r);return}if(!f){o("No pong received \u2014 connection dead, reconnecting"),l();return}f=!1;try{i.ping()}catch{l()}},H)}i.on("open",()=>{if(e!==u){i.terminate();return}o(`Connected${$?` (reconnect, port ${$})`:""} (gen=${e})`),g()}),i.on("pong",()=>{e===u&&(f=!0)}),i.on("message",(a,h)=>{if(e===u){if(!h){try{V(JSON.parse(a.toString()))}catch(S){o(`Bad control msg: ${S.message}`)}return}d&&!d.destroyed&&C(d,Buffer.from(a))}}),i.on("close",(a,h)=>{if(e!==u)return;let S=h?h.toString():"";a===4100&&(o(`Replaced by newer connection (code 4100, reason=${S}) \u2014 exiting`),process.exit(42)),o(`Data WS closed: code=${a}, reason=${S}`),l()}),i.on("error",a=>{e===u&&(o(`Data WS error: ${a.message} (code=${a.code||"none"})`),l())}),i.on("unexpected-response",(a,h)=>{e===u&&(o(`Data WS unexpected response: status=${h.statusCode}`),l())})}function V(e){if(e.type==="port"){$=e.port,o(`Assigned port ${e.port}`);return}if(e.type==="name"){if(o(`Name assigned: ${e.name}`),e.name&&/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(e.name))try{U("hostname",[e.name],{timeout:5e3,stdio:"ignore"}),m.writeFileSync("/etc/hostname",e.name+`
3
+ `)}catch{}return}d&&!d.destroyed&&R(d,e)}function k(){try{let e=x(`npm view ${w} version 2>/dev/null`,{timeout:15e3}).toString().trim();if(!e)return;let t="";try{t=JSON.parse(m.readFileSync("/opt/sandbox/lib/node_modules/@typhons/sandbox-tools/package.json","utf8")).version||""}catch{return}if(e!==t){o(`Update available: ${t} \u2192 ${e}, installing...`),x(`npm install -g --prefix /opt/sandbox ${w}@latest`,{timeout:12e4,stdio:"inherit"});try{x('pkill -f "check-claude-active.*--daemon" 2>/dev/null; nohup /opt/sandbox/bin/check-claude-active --daemon >> /tmp/claude-keepalive.log 2>&1 &',{shell:!0,timeout:5e3})}catch{}o("Update installed, exiting for restart..."),process.exit(0)}}catch{}}var ae=W();O();setTimeout(k,1e4);setInterval(k,G);o(`Relay client started (sandbox=${A()})`);
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{execSync as e,spawn as n}from"child_process";import{dirname as a,join as r}from"path";import{fileURLToPath as d}from"url";import c from"fs";var i=a(d(import.meta.url));function s(o){try{e(o,{stdio:"inherit"})}catch{}}try{e("pgrep -x sshd",{stdio:"ignore"})}catch{e("mkdir -p /run/sshd",{stdio:"ignore"}),s("/usr/sbin/sshd"),console.log("[start-all] sshd started on port 2222")}s("pkill -9 -f sandbox-session-proxy");s("pkill -9 -f sandbox-relay-client");s("pkill -9 -f sandbox-watchdog");e("sleep 1");var t=c.openSync("/tmp/ssh-relay.log","a");n(process.execPath,[r(i,"sandbox-watchdog.js")],{stdio:["ignore",t,t],detached:!0,env:process.env}).unref();try{e('pgrep -f "check-claude-active.*--daemon"',{stdio:"ignore"})}catch{let o=c.openSync("/tmp/claude-keepalive.log","a");n(process.execPath,[r(i,"check-claude-active.js"),"--daemon"],{stdio:["ignore",o,o],detached:!0,env:process.env}).unref()}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{spawn as l}from"child_process";import{dirname as y,join as p}from"path";import{fileURLToPath as f}from"url";import c from"fs";var a=y(f(import.meta.url));process.env.SSH_PORT=process.env.SSH_PORT||"2222";function s(e){console.log(`[watchdog ${new Date().toISOString().slice(11,19)}] ${e}`)}function d(e,o,i,t){let n=c.openSync(i,"a");l(process.execPath,[o],{stdio:["ignore",n,n],env:process.env}).on("exit",r=>{c.closeSync(n),!(t&&t(r))&&(s(`${e} exited (code ${r}), restarting in 2s...`),setTimeout(()=>d(e,o,i,t),2e3))})}d("session-proxy",p(a,"session-proxy.js"),"/tmp/session-proxy.log");s("session-proxy watchdog started");function m(){l(process.execPath,[p(a,"relay-client.js")],{stdio:"inherit",env:process.env}).on("exit",o=>{o===42&&(s("relay-client replaced by newer connection (exit 42), stopping all"),process.exit(0)),s(`relay-client exited (code ${o}), restarting in 2s...`),setTimeout(m,2e3)})}m();
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import q from"net";import j from"http";import{WebSocketServer as G}from"ws";import J from"fs";import{execFileSync as X}from"child_process";import z from"net";import C from"fs";var k="/tmp/session-proxy.sock",K=2*1024*1024,y=class{constructor(t=K){this.capacity=t,this.chunks=[],this.totalBytes=0,this.trimmedTo=0}append(t){let s=this.totalBytes;return this.chunks.push({offset:s,data:Buffer.from(t)}),this.totalBytes+=t.length,this._trim(),s}sliceFrom(t){if(t>=this.totalBytes)return Buffer.alloc(0);if(t<this.trimmedTo)throw new Error(`RING_OVERFLOW: requested offset ${t} already trimmed (trimmedTo=${this.trimmedTo})`);let s=[];for(let r of this.chunks){if(r.offset+r.data.length<=t)continue;let o=Math.max(0,t-r.offset);s.push(r.data.slice(o))}return Buffer.concat(s)}_trim(){for(;this.chunks.length>1&&!(this.totalBytes-this.chunks[0].offset<=this.capacity);){let s=this.chunks.shift();this.trimmedTo=s.offset+s.data.length}}},R=1,_=2;function D(e,t){if(!e||e.destroyed)return!1;let s=Buffer.alloc(5);return s[0]=R,s.writeUInt32LE(t.length,1),e.write(s),e.write(t),!0}function B(e,t){if(!e||e.destroyed)return!1;let s=Buffer.from(JSON.stringify(t)),r=Buffer.alloc(5);return r[0]=_,r.writeUInt32LE(s.length,1),e.write(r),e.write(s),!0}var w=class{constructor(t,s){this.onBinary=t,this.onJSON=s,this.buf=Buffer.alloc(0)}feed(t){for(this.buf=this.buf.length?Buffer.concat([this.buf,t]):t;this.buf.length>=5;){let s=this.buf[0],r=this.buf.readUInt32LE(1);if(this.buf.length<5+r)break;let n=this.buf.slice(5,5+r);if(this.buf=this.buf.slice(5+r),s===R)this.onBinary(n);else if(s===_)try{this.onJSON(JSON.parse(n.toString()))}catch(o){console.error(`[ipc] bad JSON: ${o.message}`)}}}};function H(e,t){try{C.unlinkSync(e)}catch{}let s=z.createServer(t);return s.listen(e,()=>{try{C.chmodSync(e,511)}catch{}}),s}function F(e,t,s,r){let n=Buffer.alloc(17);return n.write(e,0,8,"ascii"),n[8]=t,n.writeBigUInt64BE(BigInt(s),9),Buffer.concat([n,r])}function L(e){return e.length<17?null:{sessionId:e.slice(0,8).toString(),direction:e[8],offset:Number(e.readBigUInt64BE(9)),payload:e.slice(17)}}var v;try{v=(await import("@homebridge/node-pty-prebuilt-multiarch")).default}catch{try{v=(await import("node-pty")).default}catch{}}var A=parseInt(process.env.SSH_PORT||"22"),U=parseInt(process.env.WS_PORT||"49999");function i(e){console.log(`[session-proxy ${new Date().toISOString().slice(11,19)}] ${e}`)}process.on("uncaughtException",e=>{console.error(`[session-proxy] UNCAUGHT: ${e.message}`),console.error(e.stack)});process.on("unhandledRejection",e=>{console.error(`[session-proxy] UNHANDLED REJECTION: ${e.message||e}`)});var a=new Map,c=null;function P(e,t,s,r){if(!c||c.destroyed)return!1;let n=F(e,t,s,r);return D(c,n)}function d(e){!c||c.destroyed||B(c,e)}function h(e,t){let s=a.get(e);if(s){if(i(`session ${e}: closing \u2014 ${t}`),s.socket&&!s.socket.destroyed&&s.socket.destroy(),s.ptyProcess){s.closed=!0;try{s.ptyProcess.kill()}catch{}}d({type:"close",session:e}),a.delete(e)}}function I(e,t,s){let r=a.get(e);if(!r||!(r.ptyProcess||r.socket)||r.socket&&r.socket.destroyed||r.closed)return;let o=r.user2sshd.received;if(t+s.length<=o)return;let u=s;if(t<o&&(u=s.slice(o-t)),t>o){h(e,`user2sshd stream gap: expected offset ${o}, got ${t}`);return}r.ptyProcess?r.ptyProcess.write(u):r.socket.write(u),r.user2sshd.received=Math.max(o,t+s.length),d({type:"ack",session:e,direction:"user2sshd",offset:r.user2sshd.received})}function W(e,t){let s=a.get(e);if(!s)return;let r;try{r=s.sshd2user.ring.sliceFrom(t)}catch(n){h(e,`ring buffer overflow on resend: ${n.message}`);return}r.length>0?(i(`session ${e}: re-sending ${r.length}b sshd\u2192user from offset ${t}`),P(e,0,t,r)):i(`session ${e}: relay has all ${t}b sshd\u2192user, nothing to re-send`)}function V(e){if(e.type==="open"){e.mode==="pty"?v?Q(e.session,e.sshd2userOffset||0,e.user2sshdOffset||0,e):(i(`session ${e.session}: PTY mode requested but node-pty not available \u2014 rejecting`),d({type:"close",session:e.session})):Z(e.session,e.sshd2userOffset||0,e.user2sshdOffset||0);return}if(e.type==="resize"){let t=a.get(e.session);if(t?.ptyProcess)try{t.ptyProcess.resize(e.cols,e.rows)}catch{}return}if(e.type==="close"){h(e.session,"relay requested close");return}if(e.type==="ack"){let t=a.get(e.session);if(!t)return;e.direction==="sshd2user"&&typeof e.offset=="number"&&(t.sshd2user.acked=Math.max(t.sshd2user.acked,e.offset));return}if(e.type==="get-authorized-keys"){let t=e.user||"root",s=t==="root"?"/root":`/home/${t}`,r=null;try{r=J.readFileSync(`${s}/.ssh/authorized_keys`,"utf8")}catch{}d({type:"authorized-keys",requestId:e.requestId,keys:r});return}}function Q(e,t,s,r){let n=a.get(e);if(n&&n.ptyProcess&&!n.closed){i(`session ${e}: re-attaching PTY (relay sshd2user=${t}, user2sshd=${s}, our sshd2user=${n.sshd2user.ring.totalBytes}, user2sshd=${n.user2sshd.received})`),n.sshd2user.acked=t,d({type:"ready",session:e,sshd2userOffset:n.sshd2user.ring.totalBytes,user2sshdOffset:n.user2sshd.received}),W(e,t);return}let o=r.user||"root",u=r.shell||null,$=process.env.SHELL||"/bin/bash",T=r.cols||80,b=r.rows||24,O=r.term||"xterm-256color",m,x,S;try{let l=X("getent",["passwd",o],{timeout:2e3}).toString().trim().split(":");m=parseInt(l[2]),x=parseInt(l[3]),S=l[5]||(o==="root"?"/root":`/home/${o}`)}catch{m=o==="root"?0:void 0,x=void 0,S=o==="root"?"/root":`/home/${o}`}let E=$,N=u?["-c",u]:["-l"];i(`session ${e}: spawning PTY ${E} ${N.join(" ")} as ${o} (uid=${m}, ${T}x${b})`);let g={socket:null,ptyProcess:null,closed:!1,sshd2user:{ring:new y,acked:0},user2sshd:{received:0}};a.set(e,g);try{let f=v.spawn(E,N,{name:O,cols:T,rows:b,cwd:S,env:{...process.env,TERM:O,HOME:S,USER:o,LOGNAME:o,SHELL:$,PATH:process.env.PATH},uid:m,gid:x});g.ptyProcess=f,f.onData(l=>{let p=Buffer.from(l),Y=g.sshd2user.ring.append(p);P(e,0,Y,p)}),f.onExit(({exitCode:l,signal:p})=>{i(`session ${e}: PTY exited (code=${l}, signal=${p})`),g.closed=!0,d({type:"exit",session:e,code:l,signal:p||void 0}),h(e,"pty exited")}),d({type:"ready",session:e,sshd2userOffset:0,user2sshdOffset:0})}catch(f){i(`session ${e}: PTY spawn failed: ${f.message}`),h(e,`pty spawn error: ${f.message}`)}}function Z(e,t,s){let r=a.get(e);if(r&&r.socket&&!r.socket.destroyed){i(`session ${e}: re-attaching (relay sshd2user=${t}, user2sshd=${s}, our sshd2user=${r.sshd2user.ring.totalBytes}, user2sshd=${r.user2sshd.received})`),r.sshd2user.acked=t,d({type:"ready",session:e,sshd2userOffset:r.sshd2user.ring.totalBytes,user2sshdOffset:r.user2sshd.received}),W(e,t);return}i(`session ${e}: connecting to sshd :${A}`);let n={socket:null,sshd2user:{ring:new y,acked:0},user2sshd:{received:0}};a.set(e,n);let o=q.createConnection({host:"127.0.0.1",port:A},()=>{i(`session ${e}: sshd connected`),o.setNoDelay(!0),n.socket=o,d({type:"ready",session:e,sshd2userOffset:0,user2sshdOffset:0})});n.socket=o,o.on("data",u=>{let $=n.sshd2user.ring.append(u);P(e,0,$,u)}),o.on("close",()=>h(e,"sshd closed")),o.on("error",u=>h(e,`sshd error: ${u.message}`))}var le=H(k,e=>{if(i("Relay-client connected via IPC"),c&&!c.destroyed&&(i("Replacing previous relay-client IPC connection"),c.destroy()),c=e,a.size>0){let s=[];for(let[r,n]of a)s.push({id:r,sshd2userOffset:n.sshd2user.ring.totalBytes,user2sshdOffset:n.user2sshd.received,mode:n.ptyProcess?"pty":"sshd"});B(e,{type:"existing-sessions",sessions:s})}let t=new w(s=>{let r=L(s);r&&r.direction===1&&I(r.sessionId,r.offset,r.payload)},s=>{V(s)});e.on("data",s=>t.feed(s)),e.on("close",()=>{i("Relay-client IPC disconnected"),c===e&&(c=null)}),e.on("error",s=>{i(`IPC error: ${s.message}`)})});i(`IPC server listening on ${k}`);function ee(){try{return J.readFileSync("/run/e2b/.E2B_SANDBOX_ID","utf8").trim()}catch{}return"unknown"}var M=j.createServer((e,t)=>{t.setHeader("Content-Type","application/json"),e.url==="/health"?t.end(JSON.stringify({ok:!0,sandboxId:ee(),sessions:a.size,ipcConnected:!!(c&&!c.destroyed)})):(t.writeHead(404),t.end('{"error":"not found"}'))}),te=new G({server:M});te.on("connection",e=>{i("Keepalive WS connected from relay"),e.on("close",(t,s)=>i(`Keepalive WS closed: code=${t}, reason=${s?s.toString():""}`)),e.on("error",t=>i(`Keepalive WS error: ${t.message}`))});M.listen(U,"0.0.0.0",()=>{i(`Keepalive WS server on :${U}`)});
package/stop-agents.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import{execSync as i}from"child_process";import s from"fs";function n(t){console.log(`[stop-agents] ${t}`)}function e(t,d){let o;try{o=i(`pgrep -f "${d}" 2>/dev/null | head -1`,{encoding:"utf8"}).trim()}catch{}if(!o){n(`No ${t} process found`);return}let c;try{c=s.readlinkSync(`/proc/${o}/fd/0`)}catch{}if(!c||!c.startsWith("/dev/pts/")){n(`${t} (pid ${o}) has no PTY (fd/0 -> ${c}), skipping`);return}n(`Injecting Ctrl-C into ${t} (pid ${o}, tty ${c})`);try{i(`sudo python3 -c "
3
+ import fcntl, os, struct
4
+ fd = os.open('${c}', os.O_WRONLY)
5
+ fcntl.ioctl(fd, 0x5412, struct.pack('b', 0x03))
6
+ os.close(fd)
7
+ "`,{stdio:"ignore"}),n(`Ctrl-C injected for ${t}`)}catch{n(`TIOCSTI failed for ${t}, falling back to killing children`);try{let l=i(`pgrep -P ${o} 2>/dev/null`,{encoding:"utf8"}).trim().split(`
8
+ `);for(let r of l)if(r)try{process.kill(parseInt(r),"SIGINT")}catch{}}catch{}}}e("Claude Code","^claude$");e("Codex","@openai/codex");n("Done");