@shogo-ai/worker 2.3.13 → 2.4.1

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.
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import{createRequire as Br}from"node:module";var dt=(e,t,r)=>()=>{if(e)try{t=e(e=0)}catch(o){r=[o]}if(r)throw r[0];return t};var Oe=Br(import.meta.url);import*as P from"node:child_process";import{promisify as wt}from"node:util";function yt(e){return!!e&&typeof e==="object"&&!Array.isArray(e)}function vt(e){if(e.windowsHide!==void 0)return e;return{...e,windowsHide:!0}}function St(e){let t=e.slice(),r=t[1];if(t.length<2||typeof r==="function")return t.splice(1,0,{windowsHide:!0}),t;if(r==null)return t[1]={windowsHide:!0},t;if(yt(r))return t[1]=vt(r),t;return t}function ie(e){let t=e.slice(),r=-1;for(let i=1;i<t.length;i++)if(typeof t[i]==="function"){r=i;break}let o=r===-1?t.length:r;for(let i=1;i<o;i++)if(yt(t[i]))return t[i]=vt(t[i]),t;let n=o>1&&(Array.isArray(t[1])||t[1]==null)?2:1;if(n>=o)t.splice(o,0,{windowsHide:!0});else if(t[n]==null)t[n]={windowsHide:!0};return t}function J(e,t){let r=function(...o){let n=t();return Reflect.apply(n,this,e(o))};return Object.defineProperty(r,ft,{enumerable:!1,configurable:!0,writable:!1,value(...o){let n=t(),i=n[ft];if(typeof i==="function")return Reflect.apply(i,n,e(o));let s=wt(n);return Reflect.apply(s,this,e(o))}}),r}var ft,T,fe,Ei,Ii,we,We,xi;var W=dt(()=>{ft=wt.custom;T=J(ie,()=>P.spawn),fe=J(ie,()=>P.spawnSync),Ei=J(St,()=>P.exec),Ii=J(St,()=>P.execSync),we=J(ie,()=>P.execFile),We=J(ie,()=>P.execFileSync),xi=J(ie,()=>P.fork)});import{existsSync as Be}from"node:fs";import{join as Ge}from"node:path";import{promisify as Ro}from"node:util";async function jt(e=!1){if(se!==null&&!e)return se;try{await Po("git",["--version"],{timeout:3000}),se=!0}catch{se=!1}return se}function be(e,t){return`${e.replace(/\/+$/,"")}/api/projects/${t}/git`}function E(e,t={}){let{cwd:r,env:o,timeoutMs:n=300000,logger:i}=t;return new Promise((s,a)=>{let l=T("git",e,{cwd:r,env:{...process.env,...o},stdio:["ignore","pipe","pipe"]}),c="",u="",p=[],d=[];l.stdout.setEncoding("utf-8"),l.stderr.setEncoding("utf-8"),l.stdout.on("data",(m)=>{if(p.push(m),i)i.log(m.trimEnd())}),l.stderr.on("data",(m)=>{if(d.push(m),i)i.warn(m.trimEnd())});let h=setTimeout(()=>{try{l.kill("SIGKILL")}catch{}a(Error(`git ${e[0]} timed out after ${n}ms`))},n);l.on("error",(m)=>{clearTimeout(h),a(m)}),l.on("close",(m)=>{clearTimeout(h),c=p.join(""),u=d.join("");let w=m??-1;if(w===0)s({stdout:c,stderr:u,exitCode:w});else{let g=Error(`git ${e[0]} exited with code ${w}: ${u.slice(0,500)}`);g.exitCode=w,g.stdout=c,g.stderr=u,a(g)}})})}async function Ft(e){let{apiUrl:t,apiKey:r,projectId:o,localDir:n,shallow:i=!0,logger:s,timeoutMs:a}=e;if(Be(Ge(n,".git")))throw Error(`cloneProject: ${n}/.git already exists; use gitFetchAndReset instead`);let l=be(t,o),c=["-c",`http.extraHeader=Authorization: Bearer ${r}`,"clone"];if(i)c.push("--depth=1");return c.push(l,n),await E(c,{logger:s,timeoutMs:a}),{commitSha:(await E(["rev-parse","HEAD"],{cwd:n,logger:s,timeoutMs:1e4})).stdout.trim()}}async function ke(e){let{apiUrl:t,apiKey:r,projectId:o,localDir:n,branch:i="HEAD",logger:s,timeoutMs:a}=e,l=be(t,o),c=["-c",`http.extraHeader=Authorization: Bearer ${r}`];return await E([...c,"fetch",l,i],{cwd:n,logger:s,timeoutMs:a}),await E(["reset","--hard","FETCH_HEAD"],{cwd:n,logger:s,timeoutMs:a}),{commitSha:(await E(["rev-parse","HEAD"],{cwd:n,logger:s,timeoutMs:1e4})).stdout.trim()}}async function Ke(e){let{apiUrl:t,apiKey:r,projectId:o,localDir:n,logger:i,timeoutMs:s}=e,a=be(t,o),l=["-c",`http.extraHeader=Authorization: Bearer ${r}`];if(Be(Ge(n,".git","shallow")))await E([...l,"fetch","--unshallow",a],{cwd:n,logger:i,timeoutMs:s})}async function $o(e){let{apiUrl:t,apiKey:r,projectId:o,localDir:n,message:i,branch:s="HEAD",logger:a,timeoutMs:l}=e,c=be(t,o),u={GIT_AUTHOR_NAME:Le,GIT_AUTHOR_EMAIL:He,GIT_COMMITTER_NAME:Le,GIT_COMMITTER_EMAIL:He};await E(["add","-A"],{cwd:n,env:u,logger:a,timeoutMs:l});let p=!1;try{await E(["diff","--cached","--quiet"],{cwd:n,env:u,logger:a,timeoutMs:l})}catch{p=!0}if(!p)return{committed:!1};await E(["commit","-m",i,"--no-verify","--trailer",To],{cwd:n,env:u,logger:a,timeoutMs:l});let h=(await E(["rev-parse","HEAD"],{cwd:n,env:u,logger:a,timeoutMs:1e4})).stdout.trim(),m=["-c",`http.extraHeader=Authorization: Bearer ${r}`];return await E([...m,"push",c,s],{cwd:n,env:u,logger:a,timeoutMs:l}),{committed:!0,commitSha:h}}function Re(e){return Be(Ge(e,".git"))}var Po,Le="Shogo Agent",He="agent@shogo.ai",To,se=null;var Pe=dt(()=>{W();Po=Ro(we),To=`Co-authored-by: ${Le} <${He}>`});import{Command as mi}from"commander";import Lr from"picocolors";import f from"picocolors";import{existsSync as ln}from"node:fs";import{readFileSync as Kr,writeFileSync as Jr,existsSync as qr,chmodSync as Vr}from"node:fs";import{hostname as Yr}from"node:os";import{homedir as Gr}from"node:os";import{join as x}from"node:path";import{mkdirSync as de}from"node:fs";var F=x(Gr(),".shogo"),L=x(F,"config.json"),Si=x(F,"credentials.json"),ee=x(F,"device-id"),te=x(F,"worker.pid"),Me=x(F,"logs"),me=x(Me,"worker.log"),ge=x(Me,"worker.err.log"),re=x(F,"runtime"),M=x(re,process.platform==="win32"?"agent-runtime.exe":"agent-runtime"),oe=x(re,"version.json"),he=x(F,"projects");function X(e,t=he){return x(t,e)}function H(){de(F,{recursive:!0,mode:448}),de(Me,{recursive:!0,mode:448})}function mt(){H(),de(re,{recursive:!0,mode:448})}function gt(e=he){H(),de(e,{recursive:!0,mode:448})}var De={cloudUrl:"https://studio.shogo.ai",port:8002,projectsDir:he};function B(){if(!qr(L))return{};try{return JSON.parse(Kr(L,"utf-8"))}catch(e){throw Error(`Corrupt config at ${L}: ${e.message}`)}}function ne(e){H(),Jr(L,JSON.stringify(e,null,2),{mode:384}),Vr(L,384)}function ht(e,t){return{...e,...Object.fromEntries(Object.entries(t).filter(([,r])=>r!==void 0))}}function D(e={}){let t=B(),r={apiKey:process.env.SHOGO_API_KEY,cloudUrl:process.env.SHOGO_CLOUD_URL,name:process.env.SHOGO_INSTANCE_NAME,workerDir:process.env.SHOGO_WORKER_DIR,port:process.env.PORT?parseInt(process.env.PORT,10):void 0,projectsDir:process.env.SHOGO_PROJECTS_DIR},o=ht(ht(t,r),e);if(!o.apiKey)throw Error("No API key. Run `shogo login` or pass --api-key / set SHOGO_API_KEY.");return{apiKey:o.apiKey,cloudUrl:o.cloudUrl??De.cloudUrl,name:o.name??Yr(),workerDir:o.workerDir??process.cwd(),port:o.port??De.port,projectsDir:o.projectsDir??De.projectsDir}}W();import{existsSync as Xr,openSync as bt,readFileSync as zr,unlinkSync as Zr,writeFileSync as Qr}from"node:fs";function ye(){if(!Xr(te))return null;let e=zr(te,"utf-8").trim(),t=parseInt(e,10);return Number.isFinite(t)?t:null}function ve(e){try{return process.kill(e,0),!0}catch{return!1}}function Ue(){try{Zr(te)}catch{}}function kt(e){H();let t=ye();if(t&&ve(t))throw Error(`Worker already running (pid=${t}). Run \`shogo worker stop\` first.`);if(t)Ue();let r=e.inheritStdio?["ignore","inherit","inherit"]:["ignore",bt(me,"a"),bt(ge,"a")],o=T(e.runner,[e.entry],{cwd:e.cwd,env:e.env,detached:!!e.detach,stdio:r});if(!o.pid)throw Error("Failed to spawn worker process.");if(Qr(te,String(o.pid),{mode:384}),e.detach)o.unref();return{pid:o.pid,child:o}}function Rt(e="SIGTERM"){let t=ye();if(!t)return{killedPid:null};if(!ve(t))return Ue(),{killedPid:null};try{process.kill(t,e)}catch{}return Ue(),{killedPid:t}}import A from"picocolors";import{existsSync as to}from"node:fs";import{request as eo}from"node:http";function Tt(e,t=process.env){let r=(i)=>i&&i.trim().length>0?i.trim():void 0,o=r(e);if(o)return{url:Pt(o),source:"flag"};let n=[["HTTPS_PROXY",t.HTTPS_PROXY],["https_proxy",t.https_proxy],["HTTP_PROXY",t.HTTP_PROXY],["http_proxy",t.http_proxy]];for(let[i,s]of n){let a=r(s);if(a)return{url:Pt(a),source:i}}return null}function Pt(e){if(/^https?:\/\//i.test(e))return e;return`http://${e}`}function Ne(e,t){if(!t)return e;return{...e,HTTPS_PROXY:e.HTTPS_PROXY??t.url,https_proxy:e.https_proxy??t.url,HTTP_PROXY:e.HTTP_PROXY??t.url,http_proxy:e.http_proxy??t.url}}async function $t(e,t="api.shogo.ai",r=5000){return new Promise((o)=>{let n=!1,i=(s)=>{if(n)return;n=!0,o(s)};try{let s=new URL(e.url),a=s.hostname,l=s.port?parseInt(s.port,10):s.protocol==="https:"?443:80,c=eo({method:"CONNECT",host:a,port:l,path:`${t}:443`,timeout:r});c.on("connect",(u)=>{c.destroy();let p=u.statusCode??0;if(p===200)i({ok:!0,detail:`CONNECT ${t}:443 → 200 (via ${a}:${l})`});else if(p===407)i({ok:!1,detail:`407 Proxy Authentication Required — check credentials in ${e.source}`});else i({ok:!1,detail:`CONNECT → HTTP ${p}`})}),c.on("timeout",()=>{c.destroy(),i({ok:!1,detail:`timeout after ${r}ms`})}),c.on("error",(u)=>{let p=u.code?` (${u.code})`:"";i({ok:!1,detail:`${u.message}${p}`})}),c.end()}catch(s){i({ok:!1,detail:s?.message??"unknown error"})}})}function Et(e){let t;try{t=new URL(e)}catch{return[]}let r=t.host,o=t.protocol.replace(":","")||"https",n=t.hostname.split("."),i=n.length>=2?n.slice(-2).join("."):t.hostname;return[{url:`${o}://${r}`,host:r,purpose:"control",criticality:"fatal"},{url:`${o}://api-direct.${i}`,host:`api-direct.${i}`,purpose:"tunnel-direct",criticality:"graceful"},{url:`${o}://artifacts.${i}`,host:`artifacts.${i}`,purpose:"artifacts",criticality:"graceful"}]}var ro=5000;async function oo(e,t){try{let r=new AbortController,o=setTimeout(()=>r.abort(),t),n=await fetch(`${e.replace(/\/$/,"")}/health`,{signal:r.signal}).catch(()=>null);if(clearTimeout(o),!n)return{ok:!1,detail:"no response (firewall? DNS?)"};return{ok:!0,detail:`HTTP ${n.status}`}}catch(r){return{ok:!1,detail:r?.message??"unknown error"}}}var It=(e)=>{let t=[{name:"Runtime (node >= 20)",criticality:"fatal",async run(){let[o]=process.versions.node.split(".").map(Number);return(o??0)>=20?{ok:!0,detail:`node v${process.versions.node}`}:{ok:!1,detail:`node v${process.versions.node} — need >=20`}}},{name:"Worker directory exists",criticality:"fatal",async run(){return to(e.workerDir)?{ok:!0,detail:e.workerDir}:{ok:!1,detail:`${e.workerDir} does not exist`}}}];if(e.proxy)t.push({name:`Proxy reachable (${no(e.proxy.url)})`,criticality:"fatal",async run(){return $t(e.proxy)}});let r=Et(e.cloudUrl);for(let o of r)t.push({name:`Reach ${o.host}${o.purpose!=="control"?A.dim(` (${o.purpose})`):""}`,criticality:o.criticality,run:()=>oo(o.url,ro)});return t.push({name:"API key valid",criticality:"fatal",async run(){try{let o=await fetch(`${e.cloudUrl}/api/instances/heartbeat`,{method:"POST",headers:{"content-type":"application/json","x-api-key":e.apiKey},body:JSON.stringify({hostname:"preflight",name:"preflight",os:"preflight",arch:"preflight",metadata:{preflight:!0}})});if(o.status===401||o.status===403)return{ok:!1,detail:`HTTP ${o.status} — key rejected`};return o.ok?{ok:!0,detail:`HTTP ${o.status}`}:{ok:!1,detail:`HTTP ${o.status}`}}catch(o){return{ok:!1,detail:o?.message??"unknown error"}}}}),t};function no(e){try{return new URL(e).host}catch{return e}}async function xt(e){console.log(A.bold(`
1
+ import{createRequire as Vr}from"node:module";var gt=(e,t,r)=>()=>{if(e)try{t=e(e=0)}catch(o){r=[o]}if(r)throw r[0];return t};var Ce=Vr(import.meta.url);import*as P from"node:child_process";import{promisify as vt}from"node:util";function St(e){return!!e&&typeof e==="object"&&!Array.isArray(e)}function bt(e){if(e.windowsHide!==void 0)return e;return{...e,windowsHide:!0}}function kt(e){let t=e.slice(),r=t[1];if(t.length<2||typeof r==="function")return t.splice(1,0,{windowsHide:!0}),t;if(r==null)return t[1]={windowsHide:!0},t;if(St(r))return t[1]=bt(r),t;return t}function ie(e){let t=e.slice(),r=-1;for(let i=1;i<t.length;i++)if(typeof t[i]==="function"){r=i;break}let o=r===-1?t.length:r;for(let i=1;i<o;i++)if(St(t[i]))return t[i]=bt(t[i]),t;let n=o>1&&(Array.isArray(t[1])||t[1]==null)?2:1;if(n>=o)t.splice(o,0,{windowsHide:!0});else if(t[n]==null)t[n]={windowsHide:!0};return t}function q(e,t){let r=function(...o){let n=t();return Reflect.apply(n,this,e(o))};return Object.defineProperty(r,wt,{enumerable:!1,configurable:!0,writable:!1,value(...o){let n=t(),i=n[wt];if(typeof i==="function")return Reflect.apply(i,n,e(o));let s=vt(n);return Reflect.apply(s,this,e(o))}}),r}var wt,T,we,Oi,Di,se,We,Wi;var W=gt(()=>{wt=vt.custom;T=q(ie,()=>P.spawn),we=q(ie,()=>P.spawnSync),Oi=q(kt,()=>P.exec),Di=q(kt,()=>P.execSync),se=q(ie,()=>P.execFile),We=q(ie,()=>P.execFileSync),Wi=q(ie,()=>P.fork)});import{existsSync as Ke}from"node:fs";import{join as Je}from"node:path";import{promisify as Io}from"node:util";async function Gt(e=!1){if(le!==null&&!e)return le;try{await Eo("git",["--version"],{timeout:3000}),le=!0}catch{le=!1}return le}function be(e,t){return`${e.replace(/\/+$/,"")}/api/projects/${t}/git`}function x(e,t={}){let{cwd:r,env:o,timeoutMs:n=300000,logger:i}=t;return new Promise((s,a)=>{let l=T("git",e,{cwd:r,env:{...process.env,...o},stdio:["ignore","pipe","pipe"]}),c="",u="",p=[],m=[];l.stdout.setEncoding("utf-8"),l.stderr.setEncoding("utf-8"),l.stdout.on("data",(y)=>{if(p.push(y),i)i.log(y.trimEnd())}),l.stderr.on("data",(y)=>{if(m.push(y),i)i.warn(y.trimEnd())});let f=setTimeout(()=>{try{l.kill("SIGKILL")}catch{}a(Error(`git ${e[0]} timed out after ${n}ms`))},n);l.on("error",(y)=>{clearTimeout(f),a(y)}),l.on("close",(y)=>{clearTimeout(f),c=p.join(""),u=m.join("");let g=y??-1;if(g===0)s({stdout:c,stderr:u,exitCode:g});else{let d=Error(`git ${e[0]} exited with code ${g}: ${u.slice(0,500)}`);d.exitCode=g,d.stdout=c,d.stderr=u,a(d)}})})}async function Kt(e){let{apiUrl:t,apiKey:r,projectId:o,localDir:n,shallow:i=!0,logger:s,timeoutMs:a}=e;if(Ke(Je(n,".git")))throw Error(`cloneProject: ${n}/.git already exists; use gitFetchAndReset instead`);let l=be(t,o),c=["-c",`http.extraHeader=Authorization: Bearer ${r}`,"clone"];if(i)c.push("--depth=1");return c.push(l,n),await x(c,{logger:s,timeoutMs:a}),{commitSha:(await x(["rev-parse","HEAD"],{cwd:n,logger:s,timeoutMs:1e4})).stdout.trim()}}async function ke(e){let{apiUrl:t,apiKey:r,projectId:o,localDir:n,branch:i="HEAD",logger:s,timeoutMs:a}=e,l=be(t,o),c=["-c",`http.extraHeader=Authorization: Bearer ${r}`];return await x([...c,"fetch",l,i],{cwd:n,logger:s,timeoutMs:a}),await x(["reset","--hard","FETCH_HEAD"],{cwd:n,logger:s,timeoutMs:a}),{commitSha:(await x(["rev-parse","HEAD"],{cwd:n,logger:s,timeoutMs:1e4})).stdout.trim()}}async function qe(e){let{apiUrl:t,apiKey:r,projectId:o,localDir:n,logger:i,timeoutMs:s}=e,a=be(t,o),l=["-c",`http.extraHeader=Authorization: Bearer ${r}`];if(Ke(Je(n,".git","shallow")))await x([...l,"fetch","--unshallow",a],{cwd:n,logger:i,timeoutMs:s})}async function _o(e){let{apiUrl:t,apiKey:r,projectId:o,localDir:n,message:i,branch:s="HEAD",logger:a,timeoutMs:l}=e,c=be(t,o),u={GIT_AUTHOR_NAME:Be,GIT_AUTHOR_EMAIL:Ge,GIT_COMMITTER_NAME:Be,GIT_COMMITTER_EMAIL:Ge};await x(["add","-A"],{cwd:n,env:u,logger:a,timeoutMs:l});let p=!1;try{await x(["diff","--cached","--quiet"],{cwd:n,env:u,logger:a,timeoutMs:l})}catch{p=!0}if(!p)return{committed:!1};await x(["commit","-m",i,"--no-verify","--trailer",Ao],{cwd:n,env:u,logger:a,timeoutMs:l});let f=(await x(["rev-parse","HEAD"],{cwd:n,env:u,logger:a,timeoutMs:1e4})).stdout.trim(),y=["-c",`http.extraHeader=Authorization: Bearer ${r}`];return await x([...y,"push",c,s],{cwd:n,env:u,logger:a,timeoutMs:l}),{committed:!0,commitSha:f}}function Re(e){return Ke(Je(e,".git"))}var Eo,Be="Shogo Agent",Ge="agent@shogo.ai",Ao,le=null;var Pe=gt(()=>{W();Eo=Io(se),Ao=`Co-authored-by: ${Be} <${Ge}>`});import{Command as Si}from"commander";import Jr from"picocolors";import h from"picocolors";import{existsSync as hn}from"node:fs";import{readFileSync as Xr,writeFileSync as zr,existsSync as Zr,chmodSync as Qr}from"node:fs";import{hostname as eo}from"node:os";import{homedir as Yr}from"node:os";import{join as E}from"node:path";import{mkdirSync as ge}from"node:fs";var j=E(Yr(),".shogo"),H=E(j,"config.json"),xi=E(j,"credentials.json"),ee=E(j,"device-id"),te=E(j,"worker.pid"),Oe=E(j,"logs"),he=E(Oe,"worker.log"),fe=E(Oe,"worker.err.log"),re=E(j,"runtime"),O=E(re,process.platform==="win32"?"agent-runtime.exe":"agent-runtime"),oe=E(re,"version.json"),ye=E(j,"projects");function z(e,t=ye){return E(t,e)}function B(){ge(j,{recursive:!0,mode:448}),ge(Oe,{recursive:!0,mode:448})}function ht(){B(),ge(re,{recursive:!0,mode:448})}function ft(e=ye){B(),ge(e,{recursive:!0,mode:448})}var De={cloudUrl:"https://studio.shogo.ai",port:8002,projectsDir:ye};function G(){if(!Zr(H))return{};try{return JSON.parse(Xr(H,"utf-8"))}catch(e){throw Error(`Corrupt config at ${H}: ${e.message}`)}}function ne(e){B(),zr(H,JSON.stringify(e,null,2),{mode:384}),Qr(H,384)}function yt(e,t){return{...e,...Object.fromEntries(Object.entries(t).filter(([,r])=>r!==void 0))}}function D(e={}){let t=G(),r={apiKey:process.env.SHOGO_API_KEY,cloudUrl:process.env.SHOGO_CLOUD_URL,name:process.env.SHOGO_INSTANCE_NAME,workerDir:process.env.SHOGO_WORKER_DIR,port:process.env.PORT?parseInt(process.env.PORT,10):void 0,projectsDir:process.env.SHOGO_PROJECTS_DIR},o=yt(yt(t,r),e);if(!o.apiKey)throw Error("No API key. Run `shogo login` or pass --api-key / set SHOGO_API_KEY.");return{apiKey:o.apiKey,cloudUrl:o.cloudUrl??De.cloudUrl,name:o.name??eo(),workerDir:o.workerDir??process.cwd(),port:o.port??De.port,projectsDir:o.projectsDir??De.projectsDir}}W();import{existsSync as to,openSync as Rt,readFileSync as ro,unlinkSync as oo,writeFileSync as no}from"node:fs";function ve(){if(!to(te))return null;let e=ro(te,"utf-8").trim(),t=parseInt(e,10);return Number.isFinite(t)?t:null}function Se(e){try{return process.kill(e,0),!0}catch{return!1}}function Ue(){try{oo(te)}catch{}}function Pt(e){B();let t=ve();if(t&&Se(t))throw Error(`Worker already running (pid=${t}). Run \`shogo worker stop\` first.`);if(t)Ue();let r=e.inheritStdio?["ignore","inherit","inherit"]:["ignore",Rt(he,"a"),Rt(fe,"a")],o=T(e.runner,[e.entry],{cwd:e.cwd,env:e.env,detached:!!e.detach,stdio:r});if(!o.pid)throw Error("Failed to spawn worker process.");if(no(te,String(o.pid),{mode:384}),e.detach)o.unref();return{pid:o.pid,child:o}}function Tt(e="SIGTERM"){let t=ve();if(!t)return{killedPid:null};if(!Se(t))return Ue(),{killedPid:null};try{process.kill(t,e)}catch{}return Ue(),{killedPid:t}}import A from"picocolors";import{existsSync as so}from"node:fs";import{request as io}from"node:http";function xt(e,t=process.env){let r=(i)=>i&&i.trim().length>0?i.trim():void 0,o=r(e);if(o)return{url:$t(o),source:"flag"};let n=[["HTTPS_PROXY",t.HTTPS_PROXY],["https_proxy",t.https_proxy],["HTTP_PROXY",t.HTTP_PROXY],["http_proxy",t.http_proxy]];for(let[i,s]of n){let a=r(s);if(a)return{url:$t(a),source:i}}return null}function $t(e){if(/^https?:\/\//i.test(e))return e;return`http://${e}`}function Fe(e,t){if(!t)return e;return{...e,HTTPS_PROXY:e.HTTPS_PROXY??t.url,https_proxy:e.https_proxy??t.url,HTTP_PROXY:e.HTTP_PROXY??t.url,http_proxy:e.http_proxy??t.url}}async function It(e,t="api.shogo.ai",r=5000){return new Promise((o)=>{let n=!1,i=(s)=>{if(n)return;n=!0,o(s)};try{let s=new URL(e.url),a=s.hostname,l=s.port?parseInt(s.port,10):s.protocol==="https:"?443:80,c=io({method:"CONNECT",host:a,port:l,path:`${t}:443`,timeout:r});c.on("connect",(u)=>{c.destroy();let p=u.statusCode??0;if(p===200)i({ok:!0,detail:`CONNECT ${t}:443 → 200 (via ${a}:${l})`});else if(p===407)i({ok:!1,detail:`407 Proxy Authentication Required — check credentials in ${e.source}`});else i({ok:!1,detail:`CONNECT → HTTP ${p}`})}),c.on("timeout",()=>{c.destroy(),i({ok:!1,detail:`timeout after ${r}ms`})}),c.on("error",(u)=>{let p=u.code?` (${u.code})`:"";i({ok:!1,detail:`${u.message}${p}`})}),c.end()}catch(s){i({ok:!1,detail:s?.message??"unknown error"})}})}function Et(e){let t;try{t=new URL(e)}catch{return[]}let r=t.host,o=t.protocol.replace(":","")||"https",n=t.hostname.split("."),i=n.length>=2?n.slice(-2).join("."):t.hostname;return[{url:`${o}://${r}`,host:r,purpose:"control",criticality:"fatal"},{url:`${o}://api-direct.${i}`,host:`api-direct.${i}`,purpose:"tunnel-direct",criticality:"graceful"},{url:`${o}://artifacts.${i}`,host:`artifacts.${i}`,purpose:"artifacts",criticality:"graceful"}]}var ao=5000;async function lo(e,t){try{let r=new AbortController,o=setTimeout(()=>r.abort(),t),n=await fetch(`${e.replace(/\/$/,"")}/health`,{signal:r.signal}).catch(()=>null);if(clearTimeout(o),!n)return{ok:!1,detail:"no response (firewall? DNS?)"};return{ok:!0,detail:`HTTP ${n.status}`}}catch(r){return{ok:!1,detail:r?.message??"unknown error"}}}var At=(e)=>{let t=[{name:"Runtime (node >= 20)",criticality:"fatal",async run(){let[o]=process.versions.node.split(".").map(Number);return(o??0)>=20?{ok:!0,detail:`node v${process.versions.node}`}:{ok:!1,detail:`node v${process.versions.node} — need >=20`}}},{name:"Worker directory exists",criticality:"fatal",async run(){return so(e.workerDir)?{ok:!0,detail:e.workerDir}:{ok:!1,detail:`${e.workerDir} does not exist`}}}];if(e.proxy)t.push({name:`Proxy reachable (${co(e.proxy.url)})`,criticality:"fatal",async run(){return It(e.proxy)}});let r=Et(e.cloudUrl);for(let o of r)t.push({name:`Reach ${o.host}${o.purpose!=="control"?A.dim(` (${o.purpose})`):""}`,criticality:o.criticality,run:()=>lo(o.url,ao)});return t.push({name:"API key valid",criticality:"fatal",async run(){try{let o=await fetch(`${e.cloudUrl}/api/instances/heartbeat`,{method:"POST",headers:{"content-type":"application/json","x-api-key":e.apiKey},body:JSON.stringify({hostname:"preflight",name:"preflight",os:"preflight",arch:"preflight",metadata:{preflight:!0}})});if(o.status===401||o.status===403)return{ok:!1,detail:`HTTP ${o.status} — key rejected`};return o.ok?{ok:!0,detail:`HTTP ${o.status}`}:{ok:!1,detail:`HTTP ${o.status}`}}catch(o){return{ok:!1,detail:o?.message??"unknown error"}}}}),t};function co(e){try{return new URL(e).host}catch{return e}}async function _t(e){console.log(A.bold(`
2
2
  Shogo Worker — Preflight
3
3
  `));let t=!1,r=0;for(let o of e){process.stdout.write(` ${A.dim("...")} ${o.name}`);let n=await o.run();if(process.stdout.write("\r"),n.ok)console.log(` ${A.green("✓")} ${o.name}${n.detail?A.dim(` — ${n.detail}`):""}`);else if(o.criticality==="graceful")console.log(` ${A.yellow("◦")} ${o.name}${n.detail?A.dim(` — ${n.detail}`):""}${A.yellow(" (optional — worker will still start)")}`),r++;else console.log(` ${A.red("✗")} ${o.name}${n.detail?A.dim(` — ${n.detail}`):""}`),t=!0}if(!t&&r===0)console.log(A.green(`
4
4
  All checks passed.
@@ -6,26 +6,26 @@ All checks passed.
6
6
  Starting with ${r} optional host(s) blocked. See docs/my-machines-networking.md.
7
7
  `));else console.log(A.red(`
8
8
  Preflight failed — fix blocking issues before starting.
9
- `));return!t}import{accessSync as io,constants as so,existsSync as ao}from"node:fs";import{delimiter as lo,join as co}from"node:path";function G(e={}){let t=e.env??process.env,r=uo(e);for(let i of r)if(Ct(i.path))return i;let o=e.systemBinName??At(),n=po(o,t.PATH);if(n)return{path:n,source:"path"};return null}function uo(e){let t=e.env??process.env,r=[];if(e.flag&&e.flag.trim())r.push({path:e.flag.trim(),source:"flag"});let o=t.SHOGO_AGENT_RUNTIME_BIN?.trim();if(o)r.push({path:o,source:"env"});return r.push({path:M,source:"home"}),r}function At(){return process.platform==="win32"?"shogo-agent-runtime.exe":"shogo-agent-runtime"}function Ct(e){if(!ao(e))return!1;try{if(process.platform==="win32")return!0;return io(e,so.X_OK),!0}catch{return!1}}function po(e,t){if(!t)return null;for(let r of t.split(lo)){if(!r)continue;let o=co(r,e);if(Ct(o))return o}return null}function q(e={}){let t=e.env??process.env,r=[];if(r.push("Error: agent-runtime binary not found."),r.push(""),r.push("Looked in (priority order):"),e.flag)r.push(` --runtime-bin ${e.flag}`);if(t.SHOGO_AGENT_RUNTIME_BIN)r.push(` $SHOGO_AGENT_RUNTIME_BIN = ${t.SHOGO_AGENT_RUNTIME_BIN}`);return r.push(` ${M} (default install location)`),r.push(` ${At()} on $PATH`),r.push(""),r.push("Fix:"),r.push(" - Run `shogo runtime install` to download the latest binary, or"),r.push(" - Pass `--runtime-bin <path>` to point at an existing build."),r.join(`
10
- `)}W();import{createHmac as tr,randomBytes as Wo}from"node:crypto";import{existsSync as Ye,mkdirSync as Bt,readdirSync as rr,rmSync as Uo}from"node:fs";import{createConnection as No}from"node:net";import{tmpdir as Gt}from"node:os";import{dirname as jo,join as V}from"node:path";W();import{existsSync as mo,mkdirSync as go,readFileSync as ho,writeFileSync as Se}from"node:fs";import{join as z}from"node:path";var _t=1e5;function Ot(e){let t=parseInt(e.RUNTIME_MEMORY_MB||"",10);if(!Number.isFinite(t)||t<=0)return null;let r=parseInt(e.RUNTIME_CPU_PERCENT||"0",10),o=Number.isFinite(r)&&r>0?r:0,n=parseInt(e.RUNTIME_TASKS_MAX||"",10),i=Number.isFinite(n)&&n>0?n:4096;return{memoryMB:t,cpuPercent:o,tasksMax:i}}function Mt(e,t){let o=`--max-old-space-size=${Math.max(256,Math.floor(t*0.75))}`,n=e.NODE_OPTIONS?.trim();if(n){if(/--max-old-space-size=/.test(n))return;e.NODE_OPTIONS=`${n} ${o}`}else e.NODE_OPTIONS=o}function fo(){if(process.platform!=="linux")return!1;try{return fe("systemd-run",["--version"],{stdio:"ignore",timeout:3000}).status===0}catch{return!1}}function Dt(e){let{command:t,args:r,limits:o,scopeName:n,logger:i}=e;if(process.platform!=="linux"||!fo())return{command:t,args:r,wrapped:!1};let s=["-p",`MemoryMax=${o.memoryMB}M`,"-p",`MemoryHigh=${Math.max(128,Math.floor(o.memoryMB*0.9))}M`,"-p",`TasksMax=${o.tasksMax}`];if(o.cpuPercent>0)s.push("-p",`CPUQuota=${o.cpuPercent}%`);let a=["--user","--scope","--quiet","--collect",`--unit=${n}`,...s,"--",t,...r];return i?.log(`[resource-limits] wrapping spawn in systemd scope ${n} (MemoryMax=${o.memoryMB}M)`),{command:"systemd-run",args:a,wrapped:!0}}function Wt(e){let{pid:t,limits:r,logger:o}=e;if(process.platform!=="linux")return!1;let n="/sys/fs/cgroup";if(!mo(z(n,"cgroup.controllers")))return!1;let i=z(n,"shogo",`rt-${t}`);try{if(go(i,{recursive:!0}),Se(z(i,"memory.max"),String(r.memoryMB*1024*1024)),r.cpuPercent>0){let s=Math.floor(r.cpuPercent/100*_t);Se(z(i,"cpu.max"),`${s} ${_t}`)}try{Se(z(i,"pids.max"),String(r.tasksMax))}catch{}return Se(z(i,"cgroup.procs"),String(t)),o?.log(`[resource-limits] attached pid ${t} to cgroup ${i} (memory.max=${r.memoryMB}M)`),!0}catch(s){return o?.warn(`[resource-limits] direct cgroup attach failed for pid ${t}: ${s?.message??s}`),!1}}function Ut(e){let{pid:t,limits:r,logger:o}=e;if(process.platform!=="win32")return!1;let n=process.env.SHOGO_WINJOB_HELPER||"@shogo/winjob";try{let i=Oe(n);if(typeof i.limitProcess!=="function")return!1;let s=i.limitProcess(t,{memoryBytes:r.memoryMB*1024*1024,killOnClose:!0});if(s)o?.log(`[resource-limits] attached pid ${t} to Job Object (${r.memoryMB}M)`);return s}catch{return!1}}function Nt(e){try{let t=JSON.parse(ho(e,"utf-8"));if(!Array.isArray(t.pids))return[];return t.pids.filter((r)=>Number.isInteger(r)&&r>0)}catch{return[]}}var wo=20000,yo=`$ErrorActionPreference='SilentlyContinue';foreach($p in Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,WorkingSetSize){"$($p.ProcessId) $($p.ParentProcessId) $([math]::Floor($p.WorkingSetSize/1024))"}`;function vo(e=process.platform){let[t,r]=e==="win32"?["powershell.exe",["-NoProfile","-NonInteractive","-Command",yo]]:["ps",["-A","-o","pid=,ppid=,pgid=,rss="]];return new Promise((o)=>{we(t,r,{encoding:"utf-8",timeout:wo,maxBuffer:8388608},(n,i)=>{if(n||!i){o(null);return}let s=[];for(let a of String(i).split(/\r?\n/)){let l=a.trim().split(/\s+/).map((c)=>parseInt(c,10));if(l.some((c)=>!Number.isFinite(c)))continue;if(e==="win32"&&l.length===3)s.push({pid:l[0],ppid:l[1],pgid:0,kb:l[2]});else if(e!=="win32"&&l.length===4)s.push({pid:l[0],ppid:l[1],pgid:l[2],kb:l[3]})}o(s.length?s:null)})})}function So(e,t,r=process.platform,o=[]){if(r!=="win32"){let u=e.filter((p)=>p.pgid===t);return u.length?u.reduce((p,d)=>p+d.kb,0):null}if(!e.some((u)=>u.pid===t))return null;let n=new Set(o.filter((u)=>u!==t)),i=new Map;for(let u of e){if(u.pid===u.ppid)continue;let p=i.get(u.ppid);if(p)p.push(u);else i.set(u.ppid,[u])}let s=new Map(e.map((u)=>[u.pid,u])),a=new Set,l=[t],c=0;while(l.length){let u=l.pop();if(a.has(u))continue;a.add(u),c+=s.get(u)?.kb??0;for(let p of i.get(u)??[])if(!n.has(p.pid))l.push(p.pid)}return c}var je=new Map;function bo(e,t,r){let o=`${t}:${r}`,n=je.get(o);if(!n){let i={key:o,watchdogs:new Set,sampling:!1,timer:setInterval(()=>{ko(i,t)},r)};try{i.timer.unref?.()}catch{}je.set(o,i),n=i}return n.watchdogs.add(e),n}async function ko(e,t){if(e.sampling)return;e.sampling=!0;try{let r=await vo(t);for(let o of[...e.watchdogs])o.observe(r)}finally{e.sampling=!1}}class Fe{sampler=null;consecutive=0;fired=!1;pid;ceilingMB;platform;intervalMs;strikes;excludePids;logger;onBreach;constructor(e){this.pid=e.pid,this.ceilingMB=e.ceilingMB,this.platform=e.platform??process.platform,this.intervalMs=e.intervalMs??parseInt(process.env.RUNTIME_RSS_WATCHDOG_MS||"15000",10),this.strikes=e.strikes??parseInt(process.env.RUNTIME_RSS_WATCHDOG_STRIKES||"2",10),this.excludePids=e.excludePids,this.logger=e.logger,this.onBreach=e.onBreach}start(){if(this.sampler)return;this.sampler=bo(this,this.platform,Math.max(2000,this.intervalMs))}stop(){if(this.sampler?.watchdogs.delete(this),this.sampler?.watchdogs.size===0)clearInterval(this.sampler.timer),je.delete(this.sampler.key);this.sampler=null}observe(e){if(this.fired)return;let t=e?So(e,this.pid,this.platform,this.excludePids?.()??[]):null;if(t==null){this.consecutive=0;return}let r=Math.floor(t/1024);if(r<=this.ceilingMB){this.consecutive=0;return}if(this.consecutive+=1,this.logger?.warn(`[resource-limits] pid ${this.pid} RSS ${r}MB over ceiling ${this.ceilingMB}MB (strike ${this.consecutive}/${this.strikes})`),this.consecutive>=this.strikes)this.fired=!0,this.stop(),this.onBreach(r)}}import{CloudFileTransport as Xe}from"@shogo-ai/sdk/cloud-file-transport";import{killViteWatchFromPidfile as Kt}from"@shogo-ai/sdk/vite-watch";import{reapChildLedger as Fo}from"@shogo-ai/sdk/child-ledger";import{watch as Lt,statSync as Eo}from"node:fs";import{relative as os,sep as Io,posix as xo}from"node:path";var Ao=1500,Co=new Set(["node_modules",".git","dist","build",".vite",".cache"]),_o=[".shogo/"];function Oo(e){if(!e)return!0;return e.split("/").some((t)=>Co.has(t))}class ae{rootDir;transport;debounceMs;logger;onFlush;mode;git;commitAndPush;watcher=null;pending=new Set;timer=null;flushing=!1;stopped=!1;constructor(e){if(this.rootDir=e.rootDir,this.transport=e.transport,this.debounceMs=e.debounceMs??Ao,this.logger=e.logger??console,this.onFlush=e.onFlush,this.mode=e.mode??"files",this.mode==="git"&&!e.git)throw Error('CloudSyncWatcher: mode: "git" requires the `git` option block');this.git=e.git,this.commitAndPush=e.commitAndPush??(async(t)=>{let r=await Promise.resolve().then(() => (Pe(),{}));return $o(t)})}start(){if(this.watcher)return;if(this.stopped)throw Error("CloudSyncWatcher: already stopped, build a fresh instance");try{this.watcher=Lt(this.rootDir,{recursive:!0},(e,t)=>{if(!t)return;this.handleEvent(String(t))})}catch(e){this.logger.warn(`[CloudSyncWatcher] recursive watch failed (${e?.message??e}); falling back to root-only watch.`),this.watcher=Lt(this.rootDir,(t,r)=>{if(!r)return;this.handleEvent(String(r))})}}async stop(){if(this.stopped)return;if(this.stopped=!0,this.timer)clearTimeout(this.timer),this.timer=null;if(this.watcher){try{this.watcher.close()}catch{}this.watcher=null}if(this.pending.size>0)await this.flush()}handleEvent(e){if(this.stopped)return;let t=e.split(Io).join(xo.sep);if(Oo(t))return;try{let r=`${this.rootDir}/${t}`;if(Eo(r).isDirectory())return}catch{return}this.pending.add(t),this.scheduleFlush()}scheduleFlush(){if(this.timer)return;this.timer=setTimeout(()=>{this.timer=null,this.flush()},this.debounceMs)}async flush(){if(this.flushing){this.scheduleFlush();return}if(this.pending.size===0)return;this.flushing=!0;let e=Array.from(this.pending);this.pending.clear();try{if(this.mode==="git")await this.flushGit(e);else await this.flushFiles(e)}catch(t){this.logger.error(`[CloudSyncWatcher] flush failed: ${t?.message??t}`);for(let r of e)this.pending.add(r);this.scheduleFlush()}finally{this.flushing=!1}}async flushFiles(e){if(e.length===0)return;let t=await this.transport.uploadFiles(e);if(this.onFlush?.({uploaded:e,errors:t.errors.length}),t.errors.length>0)for(let r of t.errors)this.logger.warn(`[CloudSyncWatcher] upload ${r.path}: ${r.message}`)}async flushGit(e){if(!this.git)throw Error("CloudSyncWatcher: missing git options");let t=[],r=[];for(let i of e)if(_o.some((s)=>i===s.slice(0,-1)||i.startsWith(s)))t.push(i);else r.push(i);if(t.length>0){let i=await this.transport.uploadFiles(t);if(i.errors.length>0)for(let s of i.errors)this.logger.warn(`[CloudSyncWatcher] upload ${s.path}: ${s.message}`)}let o=!1,n;if(r.length>0){let i=`auto: ${new Date().toISOString()}`;try{let s=await this.commitAndPush({apiUrl:this.git.apiUrl,apiKey:this.git.apiKey,projectId:this.git.projectId,localDir:this.rootDir,message:i,branch:this.git.branch,logger:this.logger});o=s.committed,n=s.commitSha}catch(s){throw s}}this.onFlush?.({uploaded:e,errors:0,committed:o,commitSha:n})}}Pe();var Ht=1e4,Je=2000;var qe={turns:0,tasks:0,streams:0,children:0};function Mo(e){return`turns=${e.turns} tasks=${e.tasks} streams=${e.streams} children=${e.children}`}function Do(e){return e.turns+e.tasks+e.streams+e.children}function C(e,t,r,o,n){return{busy:e,dead:t,reason:r,counts:o,countsText:Mo(o),...n!=null?{sampleAgeMs:n}:{}}}function Ve(e){let{now:t,startedAt:r,status:o,sample:n,processExited:i=!1,startupGraceMs:s=120000,staleMs:a=30000,deadAfterMs:l=60000}=e,c=e.terminalSessions??0,u=e.inflightCalls??0,p=n?.counts??qe,d=n?Math.max(0,t-n.at):void 0;if(c>0)return C(!0,!1,"terminal",p,d);if(u>0)return C(!0,!1,"inflight-call",p,d);if(i)return C(!1,!0,"dead",p,d);if(o==="starting"||o==="restarting")return C(!0,!1,"grace",p,d);let h=t-(r||t);if(h>=0&&h<s)return C(!0,!1,"grace",p,d);if(o!=="running")return C(!0,!1,"unknown",p,d);if(!n)return C(!0,!1,"unknown",p,d);if(!n.ok){if((n.failingSince!=null?t-n.failingSince:0)>=l)return C(!1,!0,"dead",p,d);return C(!0,!1,"unknown",p,d)}if((d??0)>a)return C(!0,!1,"stale",p,d);if(Do(p)>0)return C(!0,!1,"work",p,d);return C(!1,!1,"idle",p,d)}function le(e,t){switch(e.reason){case"terminal":return"open terminal";case"inflight-call":return"in-flight project call";case"grace":return"starting up";case"unknown":return`unknown activity${e.sampleAgeMs!=null?` (last sample ${Math.round(e.sampleAgeMs/1000)}s old)`:" (no sample yet)"}`;case"stale":return`stale activity (last sample ${Math.round((e.sampleAgeMs??0)/1000)}s old)`;case"work":return e.countsText;case"dead":return"process gone";case"idle":return t!=null?`idle ${Math.round(t/60000)}m, ${e.countsText}`:`idle, ${e.countsText}`}}var ze=37100,Ze=37900,Jt=1,Lo=2,Te=16,Ho=900000,Bo=30000,Go=3000,qt=8,Ko=1000,Jo=60000,Vt=8,Yt=300000,Xt=3600000,qo=8,Vo=120000,Yo=900000,Xo=60000,zo=500,Zo=30000,zt=25000,Qo=5000,en=500,tn=process.env.SHOGO_PERF_LOG==="1";function $e(e,t,r,o){if(!tn)return;console.log(`[shogo-perf] ${JSON.stringify({perf:"open",source:"worker",phase:e,projectId:t,openAttemptId:r,atMs:Date.now(),...o})}`)}var rn=(e)=>({command:e,args:[]}),Qe=null;function or(){if(!Qe)Qe=Wo(32).toString("hex");return Qe}function Zt(e){return tr("sha256",or()).update(`runtime:${e}`).digest("hex")}function on(e){return tr("sha256",or()).update(`webhook:${e}`).digest("hex")}function Qt(e){let t=e.indexOf("?");if(t===-1)return{pathname:e,search:""};return{pathname:e.slice(0,t),search:e.slice(t)}}function er(e){try{return rr(e).length===0}catch{return!0}}function et(e,t,r){let o=[];switch(o.push(`Cannot spawn agent-runtime for project ${e}: no workspace directory available.`),o.push(""),t){case"no-auto-pull-config":o.push(" Reason: WorkerRuntimeManager was constructed without an `autoPull` config and no caller-provided `projectDir` was found on disk."),o.push(" This usually means a programmatic embedder forgot to wire up enrichSpawnConfig "+"or autoPull. CLI users should not see this — please file a bug.");break;case"no-projects-dir":o.push(" Reason: auto-pull is configured but `projectsDir` is empty. The worker needs a persistent root directory under which it can store cloned project workspaces.");break;case"auto-pull-disabled":o.push(` Reason: auto-pull was disabled (--no-auto-pull) and the expected pre-pulled workspace at ${r} is missing or empty.`);break}return o.push(""),o.push(" How to fix (pick one):"),o.push(" 1. Re-enable auto-pull (default). Drop the --no-auto-pull flag and restart"),o.push(" the worker. The first inbound request for this project will clone its"),o.push(" workspace from Shogo Cloud into <projectsDir>/<projectId>/."),o.push(""),o.push(" 2. Pre-pull manually with `shogo project pull <projectId>` before starting"),o.push(" the worker. Use this when you want full control over when the clone runs"),o.push(" (slow links, scheduled maintenance windows, etc.)."),o.push(""),o.push(" 3. Point the worker at an existing workspace by setting either:"),o.push(" --projects-dir <path> (per-invocation flag)"),o.push(" SHOGO_PROJECTS_DIR=<path> (env var, persists across restarts)"),o.push(" shogo config set projectsDir <path>"),o.push(" Whichever path you pick must contain a subdirectory named after the "),o.push(` project id (e.g. <path>/${e}/) populated with the project's source.`),o.push(""),o.push(" Docs: https://shogo.ai/docs/self-hosted-worker#workspace-seeding"),o.join(`
11
- `)}class tt{opts;log;runtimes=new Map;usedPorts=new Set;spawnCommand;resolved=null;stopped=!1;resolvedLimits=void 0;watchers=new Map;pulledProjects=new Set;syncModes=new Map;activityTimer=null;constructor(e={}){this.opts=e,this.log=e.logger??console,this.spawnCommand=e.spawnCommand??rn,this.sweepOrphanedViteWatches(),this.ensureActivitySampler()}resolveBinary(){if(!this.resolved)this.resolved=this.opts.resolveBin?this.opts.resolveBin():G({flag:this.opts.runtimeBin,env:this.opts.env});return this.resolved}async resolveLocalUrl(e,t){let{pathname:r,search:o}=Qt(e);if(!(r.startsWith("/agent/")||r==="/agent"))return null;if(!t){let s=this.getActiveProjects();if(s.length!==1)return null;t=s[0]}let n=await this.spawnConfigFor(t);if(!n)return this.log.warn(`[WorkerRuntimeManager] No spawn config for ${t} — set defaultSpawnConfig or enrichSpawnConfig`),null;let i=await this.ensureRunning(t,n);if(!i.agentPort)return null;return this.touch(t),`http://127.0.0.1:${i.agentPort}${r}${o}`}deriveRuntimeToken(e){return Zt(e)}describeRejection(e,t){let{pathname:r}=Qt(e);if(!(r.startsWith("/agent/")||r==="/agent"))return{code:"CLI_WORKER_HAS_NO_DATA_API",message:`cli-worker only serves /agent/* paths; tried: ${r}`};return{code:"CLI_WORKER_NO_PROJECT_FOR_PATH",message:`cli-worker received an /agent path without a single active project; projectId=${t??"none"}, path=${r}`}}async spawnConfigFor(e){let t=this.opts.defaultSpawnConfig;if(!t)return null;if(this.opts.enrichSpawnConfig)try{return await this.opts.enrichSpawnConfig(e,t)}catch(r){this.log.warn(`[WorkerRuntimeManager] enrichSpawnConfig failed for ${e}: ${r?.message??r}`)}return t}async ensureRunning(e,t){if(this.stopped)throw Error("WorkerRuntimeManager is stopped");let r=this.runtimes.get(e);if(r?.status==="failed"){let i=this.breakerCooldownMs(r.breakerTrips),s=Date.now()-r.lastFailureAt;if(s>=i)this.log.log(`[WorkerRuntimeManager] auto-heal: ${e} cooldown elapsed (${Math.round(s/1000)}s since trip #${r.breakerTrips}) — `+"clearing circuit breaker and attempting a fresh spawn"),this.healBreaker(r);else{let a=i-s;throw Error(`[WorkerRuntimeManager] cannot ensureRunning(${e}): ${r.lastError??"runtime is in failed state"} Will auto-retry in ~${Math.ceil(a/1000)}s; call resetFailure(${e}) to retry immediately, or stop(${e}) to give up.`)}}t=await this.maybeAutoPull(e,t);let o=this.runtimes.get(e);if(o?.status==="running")return this.touch(e),this.snapshot(o);if(o?.startPromise){let i=await o.startPromise;return this.snapshot(i)}let n=o??this.makeSlot(e,t);if(!o)this.runtimes.set(e,n);n.spawnConfig=t,n.startPromise=this.doStart(n);try{let i=await n.startPromise;return this.enforceMaxRuntimes(e),this.snapshot(i)}finally{n.startPromise=null}}enforceMaxRuntimes(e){let t=this.opts.maxRuntimes;if(t==null||!Number.isFinite(t)||t<=0)return;let r=Date.now(),o=Array.from(this.runtimes.values()).filter((i)=>i.status==="running");if(o.length<=t)return;let n=o.filter((i)=>i.projectId!==e&&r-i.lastUsedAt>=Bo).sort((i,s)=>i.lastUsedAt-s.lastUsedAt);this.evictLru(t,o.length,n,r)}async evictLru(e,t,r,o){let n=t-e,i=new Map;await Promise.all(r.map(async(s)=>{i.set(s.projectId,await this.freshActivityDecision(s.projectId))}));for(let s of r){if(n<=0)break;if(s.status!=="running"||this.runtimes.get(s.projectId)!==s)continue;let a=i.get(s.projectId);if(!a)continue;if(a.dead){this.log.log(`[WorkerRuntimeManager] maxRuntimes=${e}: reaping dead ${s.projectId} (${le(a)})`),this.stop(s.projectId,"SIGTERM","dead").catch((c)=>{this.log.warn(`[WorkerRuntimeManager] reap of ${s.projectId} failed: ${c?.message??c}`)}),n--;continue}if(a.busy){this.log.log(`[WorkerRuntimeManager] maxRuntimes=${e}: keeping ${s.projectId} over cap=${e}: ${a.reason} (${le(a)})`);continue}let l=o-s.lastUsedAt;this.log.log(`[WorkerRuntimeManager] maxRuntimes=${e} exceeded (${t} running) — `+`LRU-evicting ${s.projectId} (idle ${Math.round(l/1000)}s, ${a.countsText})`),this.stop(s.projectId,"SIGTERM","lru-evict").catch((c)=>{this.log.warn(`[WorkerRuntimeManager] maxRuntimes eviction of ${s.projectId} failed: ${c?.message??c}`)}),n--}if(n>0)this.log.log(`[WorkerRuntimeManager] maxRuntimes=${e} still exceeded by ${n} after eviction pass — `+"remaining over-cap slots are active or unverifiable; will retry on next spawn / idle reap")}async ensurePulled(e,t){return this.maybeAutoPull(e,t)}async maybeAutoPull(e,t){let r=this.opts.autoPull;if(t.projectDir&&Ye(t.projectDir))return t;if(!r)throw Error(et(e,"no-auto-pull-config",null));if(!r.projectsDir)throw Error(et(e,"no-projects-dir",null));if(!r.enabled){let s=V(r.projectsDir,e);if(Ye(s)&&!er(s))return{...t,projectDir:s};throw Error(et(e,"auto-pull-disabled",s))}if(this.pulledProjects.has(e))return{...t,projectDir:V(r.projectsDir,e)};let o=V(r.projectsDir,e),n=r.logger??this.log,i={cloneProject:r.gitOps?.cloneProject??Ft,gitIsAvailable:r.gitOps?.gitIsAvailable??jt,isGitRepo:r.gitOps?.isGitRepo??Re};this.pulledProjects.add(e);try{Bt(o,{recursive:!0});let s=er(o),a=i.isGitRepo(o),u=(r.useGit!==!1?await i.gitIsAvailable():!1)&&(s||a)?"git":"files";if(this.syncModes.set(e,u),u==="git"){if(s){n.log(`[WorkerRuntimeManager] auto-pull: git clone project ${e} into ${o}`);try{let p=await i.cloneProject({apiUrl:t.cloudUrl,apiKey:t.apiKey,projectId:e,localDir:o,shallow:!0,logger:n});n.log(`[WorkerRuntimeManager] auto-pull: ${e} cloned at ${p.commitSha.slice(0,8)}`)}catch(p){n.warn(`[WorkerRuntimeManager] auto-pull: git clone failed for ${e} (${p?.message??p}); falling back to CloudFileTransport.downloadAll`),this.syncModes.set(e,"files"),await this.fileTransportClone(e,o,t,n)}}else if(a)n.log(`[WorkerRuntimeManager] auto-pull: ${e} already has .git/; skipping clone`);if(this.syncModes.get(e)==="git")await this.topUpShogoState(e,o,t,n)}else if(s)await this.fileTransportClone(e,o,t,n);else n.log(`[WorkerRuntimeManager] auto-pull: ${e} workspace already populated; skipping clone`);if(r.watch!==!1&&!this.watchers.has(e))try{let p=new Xe({apiUrl:t.cloudUrl,apiKey:t.apiKey,projectId:e,localDir:o}),d=this.syncModes.get(e)??"files",h=new ae({rootDir:o,transport:p,logger:n,mode:d,git:d==="git"?{apiUrl:t.cloudUrl,apiKey:t.apiKey,projectId:e}:void 0});h.start(),this.watchers.set(e,h)}catch(p){n.warn(`[WorkerRuntimeManager] auto-pull: watcher start failed for ${e}: ${p?.message??p}`)}}catch(s){n.warn(`[WorkerRuntimeManager] auto-pull: failed for ${e} — runtime will fall back to template defaults. `+`(${s?.message??s})`)}return{...t,projectDir:o}}async fileTransportClone(e,t,r,o){o.log(`[WorkerRuntimeManager] auto-pull: file-transport clone of ${e} into ${t}`);let i=await new Xe({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:t}).downloadAll();o.log(`[WorkerRuntimeManager] auto-pull: ${e} downloaded ${i.downloaded} files (${i.errors.length} errors)`)}async topUpShogoState(e,t,r,o){try{let n=new Xe({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:t}),s=(await n.listManifest()).filter((l)=>l.path===".shogo"||l.path.startsWith(".shogo/"));if(s.length===0)return;let a=await n.downloadFiles(s);o.log(`[WorkerRuntimeManager] auto-pull: ${e} .shogo/ top-up downloaded ${a.downloaded} files (${a.errors.length} errors)`)}catch(n){o.warn(`[WorkerRuntimeManager] auto-pull: .shogo top-up failed for ${e}: ${n?.message??n}`)}}shouldDeferToCloudSync(e){return!!this.opts.autoPull?.enabled&&this.pulledProjects.has(e)}status(e){let t=this.runtimes.get(e);return t?this.snapshot(t):null}getActiveProjects(){return Array.from(this.runtimes.keys()).filter((e)=>{let t=this.runtimes.get(e);return t&&(t.status==="running"||t.status==="starting"||t.status==="restarting")})}touch(e){let t=this.runtimes.get(e);if(!t)return;t.lastUsedAt=Date.now(),this.armIdleTimer(t)}async stop(e,t="SIGTERM",r="stop"){let o=this.runtimes.get(e);if(!o)return;if(o.status="stopping",o.restartTimer)clearTimeout(o.restartTimer),o.restartTimer=null;if(o.idleTimer)clearTimeout(o.idleTimer),o.idleTimer=null;if(o.graceTimer)clearTimeout(o.graceTimer),o.graceTimer=null;if(o.limitWatchdog)o.limitWatchdog.stop(),o.limitWatchdog=null;if(o.proc){this.killProcessGroup(o,t),this.killViteWatch(o);try{o.proc.kill(t)}catch{}await this.waitForExit(o.proc,5000),this.killProcessGroup(o,"SIGKILL"),this.killViteWatch(o),this.reapChildLedgerFor(e,o.spawnConfig.projectDir)}o.pid=null,this.releasePort(o.agentPort),this.runtimes.delete(e),this.notifyGone(e,{reason:r})}notifyGone(e,t){let r=this.opts.onRuntimeGone;if(!r)return;try{r(e,t)}catch(o){this.log.warn(`[WorkerRuntimeManager] onRuntimeGone(${e}) threw: ${o?.message??o}`)}}resetFailure(e){let t=this.runtimes.get(e);if(!t||t.status!=="failed")return!1;if(t.restartTimer)clearTimeout(t.restartTimer),t.restartTimer=null;if(t.idleTimer)clearTimeout(t.idleTimer),t.idleTimer=null;if(t.graceTimer)clearTimeout(t.graceTimer),t.graceTimer=null;if(t.limitWatchdog)t.limitWatchdog.stop(),t.limitWatchdog=null;return this.runtimes.delete(e),this.log.log(`[WorkerRuntimeManager] resetFailure: ${e} cleared, next ensureRunning will respawn`),!0}breakerCooldownMs(e){let t=Math.max(0,e-1),r=Vo*Math.pow(2,t);return Math.min(r,Yo)}healBreaker(e){e.status="stopped",e.consecutiveFailures=0}async stopAll(e="SIGTERM"){if(this.stopped=!0,this.activityTimer)clearInterval(this.activityTimer),this.activityTimer=null;let t=Array.from(this.watchers.keys());await Promise.all(t.map(async(o)=>{let n=this.watchers.get(o);if(this.watchers.delete(o),n)try{await n.stop()}catch(i){this.log.warn(`[WorkerRuntimeManager] watcher stop ${o}: ${i?.message??i}`)}}));let r=Array.from(this.runtimes.keys());await Promise.all(r.map((o)=>this.stop(o,e).catch((n)=>{this.log.error(`[WorkerRuntimeManager] Failed to stop ${o}: ${n?.message??n}`)})))}makeSlot(e,t){return{projectId:e,agentPort:0,apiServerPort:0,status:"starting",proc:null,pid:null,startedAt:0,lastStdoutAt:0,lastUsedAt:Date.now(),restarts:0,consecutiveFailures:0,lastFailureAt:0,breakerTrips:0,restartTimestamps:[],graceTimer:null,restartTimer:null,idleTimer:null,limitWatchdog:null,spawnConfig:t,startPromise:null}}async doStart(e){let t=this.resolveBinary();if(!t)throw e.status="error",e.lastError="agent-runtime binary not found (run `shogo runtime install`)",Error(e.lastError);if(!e.agentPort)e.agentPort=await this.allocatePort(),e.apiServerPort=e.agentPort+Jt;let r=this.buildEnv(e,t.path);if(e.runtimeToken=r.RUNTIME_AUTH_SECRET,r.SHOGO_EXEC_PIDS_FILE)try{Uo(r.SHOGO_EXEC_PIDS_FILE,{force:!0})}catch{}let o=this.resolveCwd(e),n=this.spawnCommand(t.path),i=this.resolveLimits(),{command:s,args:a}=n,l=!1;if(i){let m=Dt({command:n.command,args:n.args,limits:i,scopeName:`shogo-rt-${e.projectId.slice(0,8)}-${Date.now().toString(36)}`,logger:this.log});s=m.command,a=m.args,l=m.wrapped}this.log.log(`[WorkerRuntimeManager] Spawning agent-runtime for ${e.projectId} via ${s} ${a.join(" ")} (port=${e.agentPort}, source=${t.source}${i?`, memCeil=${i.memoryMB}M`:""})`);let c=process.platform!=="win32",u=T(s,a,{cwd:o,env:r,detached:c,stdio:["ignore","pipe","pipe"]});if(c)try{u.unref()}catch{}e.proc=u,e.pid=u.pid??null,e.status="starting",e.startedAt=Date.now(),e.activity=null,e.stderrTail=[];let p=e.spawnConfig.extraEnv?.SHOGO_OPEN_ID;if($e("agent-spawned",e.projectId,p,{pid:e.pid,command:s,args:a,source:t.source}),i)try{this.applyPostSpawnLimits(e,i,l)}catch(m){this.log.warn(`[WorkerRuntimeManager] failed to apply resource limits for ${e.projectId}: ${m?.message??m}`)}e.lastStdoutAt=e.startedAt,u.on("error",(m)=>{e.lastError=m?.message??String(m),this.log.error(`[WorkerRuntimeManager] spawn error for ${e.projectId}: ${e.lastError}`)}),u.on("exit",(m,w)=>{this.handleExit(e,m,w)});let d=`[runtime:${e.projectId.slice(0,8)}]`,h=!0;u.stdout?.on("data",(m)=>{if(e.lastStdoutAt=Date.now(),h)h=!1,$e("agent-first-output",e.projectId,p,{elapsedMs:Date.now()-(e.startedAt??Date.now())});for(let w of m.toString().trimEnd().split(`
12
- `))if(w)this.log.log(`${d} ${w}`)}),u.stderr?.on("data",(m)=>{if(e.lastStdoutAt=Date.now(),h)h=!1,$e("agent-first-output",e.projectId,p,{elapsedMs:Date.now()-(e.startedAt??Date.now()),stream:"stderr"});let w=e.stderrTail??=[];for(let g of m.toString().trimEnd().split(`
13
- `)){if(!g)continue;if(this.log.error(`${d} ${g}`),w.push(g),w.length>qt)w.splice(0,w.length-qt)}});try{return await this.waitForHealth(e,Zo),$e("agent-health-ready",e.projectId,p,{elapsedMs:Date.now()-(e.startedAt??Date.now()),agentPort:e.agentPort}),e.status="running",e.lastUsedAt=Date.now(),this.armIdleTimer(e),this.armGraceTimer(e),this.sampleActivity(e).catch(()=>null),e}catch(m){e.status="error",e.lastError=m?.message??String(m),this.killProcessGroup(e,"SIGTERM"),this.killViteWatch(e);try{u.kill("SIGTERM")}catch{}throw this.releasePort(e.agentPort),e.agentPort=0,e.apiServerPort=0,m}}resolveLimits(){if(this.resolvedLimits===void 0)this.resolvedLimits=Ot(this.opts.env??process.env);return this.resolvedLimits}applyPostSpawnLimits(e,t,r){let o=e.pid;if(!o)return;if(process.platform==="linux"){if(r)return;if(Wt({pid:o,limits:t,logger:this.log}))return}else if(process.platform==="win32"){if(Ut({pid:o,limits:t,logger:this.log}))return}let n=this.execPidsFile(e);e.limitWatchdog=new Fe({pid:o,ceilingMB:t.memoryMB,excludePids:()=>Nt(n),logger:this.log,onBreach:(i)=>this.handleLimitBreach(e,i,t.memoryMB)}),e.limitWatchdog.start()}handleLimitBreach(e,t,r){if(this.log.warn(`[WorkerRuntimeManager] runtime ${e.projectId} exceeded memory ceiling (${t}MB > ${r}MB) — restarting`),e.lastError=`memory ceiling exceeded (${t}MB > ${r}MB)`,e.limitWatchdog)e.limitWatchdog.stop(),e.limitWatchdog=null;this.killProcessGroup(e,"SIGTERM"),this.killViteWatch(e);try{e.proc?.kill("SIGTERM")}catch{}let o=setTimeout(()=>{this.killProcessGroup(e,"SIGKILL"),this.killViteWatch(e)},5000);try{o.unref?.()}catch{}}killProcessGroup(e,t){if(!e.pid)return;if(process.platform==="win32"){try{fe("taskkill",["/F","/T","/PID",String(e.pid)],{stdio:"ignore",windowsHide:!0})}catch{}return}try{process.kill(-e.pid,t)}catch{}}armGraceTimer(e){if(e.graceTimer)clearTimeout(e.graceTimer),e.graceTimer=null;e.graceTimer=setTimeout(()=>{if(e.graceTimer=null,e.consecutiveFailures>0)e.consecutiveFailures=0;e.breakerTrips=0,e.restartTimestamps=[]},Xo);try{e.graceTimer.unref?.()}catch{}}buildEnv(e,t){let r=e.spawnConfig,o={...this.opts.env??process.env,PROJECT_ID:e.projectId,PORT:String(e.agentPort),API_SERVER_PORT:String(e.apiServerPort),SKILL_SERVER_PORT:String(e.apiServerPort),WORKSPACE_API_PORT_BASE:String(e.agentPort+Lo),NODE_ENV:"production",STARTUP_TIME:String(Date.now()),SHOGO_CLOUD_URL:r.cloudUrl,SHOGO_API_URL:r.cloudUrl,SHOGO_API_KEY:r.apiKey,RUNTIME_AUTH_SECRET:Zt(e.projectId),WEBHOOK_TOKEN:on(e.projectId)};if(r.projectDir)o.PROJECT_DIR=r.projectDir,o.WORKSPACE_DIR=r.projectDir;if(this.shouldDeferToCloudSync(e.projectId))o.SHOGO_CLOUD_SYNC="1";if(r.aiProxyUrl)o.AI_PROXY_URL=r.aiProxyUrl;if(r.aiProxyToken)o.AI_PROXY_TOKEN=r.aiProxyToken;if(r.techStackId)o.TECH_STACK_ID=r.techStackId;if(r.name)o.AGENT_NAME=r.name;if(r.workspaceId)o.WORKSPACE_ID=r.workspaceId;if(!o.TREE_SITTER_WASM_DIR)o.TREE_SITTER_WASM_DIR=V(jo(t),"tree-sitter-wasm");if(r.extraEnv)Object.assign(o,r.extraEnv);let n=this.resolveLimits();if(n)Mt(o,n.memoryMB),o.SHOGO_EXEC_PIDS_FILE=this.execPidsFile(e),o.SHOGO_EXEC_MEMORY_MB??=String(n.memoryMB);return o}execPidsFile(e){return V(Gt(),"shogo-exec-pids",`${e.projectId.replace(/[^\w.-]/g,"_")}.json`)}killViteWatch(e){let t=e.spawnConfig.projectDir;if(!t)return;try{Kt(t,{logger:this.log})}catch(r){this.log.warn(`[WorkerRuntimeManager] Vite watch cleanup for ${e.projectId} failed: ${r?.message??r}`)}}reapChildLedgerFor(e,t){if(!t||process.platform==="win32")return;try{let{reaped:r,dropped:o}=Fo(t,{logger:this.log});if(r.length>0)this.log.log(`[WorkerRuntimeManager] Reaped ${r.length} leftover child group(s) for ${e}: `+r.map((n)=>`${n.label}(pgid=${n.pgid})`).join(", "));if(o.length>0)this.log.log(`[WorkerRuntimeManager] Dropped ${o.length} stale ledger entr(ies) for ${e} (process gone or pid reused)`)}catch(r){this.log.warn(`[WorkerRuntimeManager] Child ledger reap for ${e} failed: ${r?.message??r}`)}}sweepOrphanedViteWatches(){let e=this.opts.autoPull?.projectsDir;if(!e)return;let t;try{t=rr(e,{withFileTypes:!0})}catch{return}for(let r of t){if(!r.isDirectory()||this.runtimes.has(r.name))continue;let o=V(e,r.name);try{let n=Kt(o,{logger:this.log});if(n)this.log.log(`[WorkerRuntimeManager] Reaped orphaned Vite watch for ${r.name} (pid=${n.pid})`);this.reapChildLedgerFor(r.name,o)}catch(n){this.log.warn(`[WorkerRuntimeManager] Vite watch startup sweep failed for ${r.name}: ${n?.message??n}`)}}}resolveCwd(e){let t=e.spawnConfig;if(t.projectDir&&Ye(t.projectDir))return t.projectDir;let r=this.opts.runtimeWorkDir??V(Gt(),"shogo-runtime",e.projectId);return Bt(r,{recursive:!0}),r}handleExit(e,t,r){let o=r===null&&t===0,n=e.status==="stopping"||this.stopped;if(this.log.log(`[WorkerRuntimeManager] runtime ${e.projectId} exited (code=${t}, signal=${r})`),!n){let u=e.stderrTail?.length?`
9
+ `));return!t}import{accessSync as uo,constants as po,existsSync as mo}from"node:fs";import{delimiter as go,join as ho}from"node:path";function K(e={}){let t=e.env??process.env,r=fo(e);for(let i of r)if(Ct(i.path))return i;let o=e.systemBinName??Mt(),n=yo(o,t.PATH);if(n)return{path:n,source:"path"};return null}function fo(e){let t=e.env??process.env,r=[];if(e.flag&&e.flag.trim())r.push({path:e.flag.trim(),source:"flag"});let o=t.SHOGO_AGENT_RUNTIME_BIN?.trim();if(o)r.push({path:o,source:"env"});return r.push({path:O,source:"home"}),r}function Mt(){return process.platform==="win32"?"shogo-agent-runtime.exe":"shogo-agent-runtime"}function Ct(e){if(!mo(e))return!1;try{if(process.platform==="win32")return!0;return uo(e,po.X_OK),!0}catch{return!1}}function yo(e,t){if(!t)return null;for(let r of t.split(go)){if(!r)continue;let o=ho(r,e);if(Ct(o))return o}return null}function V(e={}){let t=e.env??process.env,r=[];if(r.push("Error: agent-runtime binary not found."),r.push(""),r.push("Looked in (priority order):"),e.flag)r.push(` --runtime-bin ${e.flag}`);if(t.SHOGO_AGENT_RUNTIME_BIN)r.push(` $SHOGO_AGENT_RUNTIME_BIN = ${t.SHOGO_AGENT_RUNTIME_BIN}`);return r.push(` ${O} (default install location)`),r.push(` ${Mt()} on $PATH`),r.push(""),r.push("Fix:"),r.push(" - Run `shogo runtime install` to download the latest binary, or"),r.push(" - Pass `--runtime-bin <path>` to point at an existing build."),r.join(`
10
+ `)}W();import{createHmac as sr,randomBytes as jo}from"node:crypto";import{existsSync as ze,mkdirSync as Vt,readdirSync as ar,rmSync as Ho,unwatchFile as Bo,watchFile as Go}from"node:fs";import{createConnection as Ko}from"node:net";import{tmpdir as Yt}from"node:os";import{dirname as Jo,join as Y}from"node:path";W();import{existsSync as Dt,mkdirSync as wo,readFileSync as Wt,writeFileSync as ae}from"node:fs";import{join as U}from"node:path";var Ot=1e5;function vo(e){try{let t=JSON.parse(Wt(e,"utf-8"));return t&&typeof t==="object"?t:null}catch{return null}}function So(e){let t=parseInt(e.SHOGO_EXEC_MEMORY_MB||"",10);if(Number.isFinite(t)&&t>0)return t;if(!e.SHOGO_HOST_LIMITS_FILE)return 0;let r=vo(e.SHOGO_HOST_LIMITS_FILE)?.execMemoryMB;return typeof r==="number"&&Number.isFinite(r)&&r>0?Math.floor(r):0}function je(e){let t=parseInt(e.RUNTIME_MEMORY_MB||"",10);if(!Number.isFinite(t)||t<=0)return null;let r=parseInt(e.RUNTIME_CPU_PERCENT||"0",10),o=Number.isFinite(r)&&r>0?r:0,n=parseInt(e.RUNTIME_TASKS_MAX||"",10),i=Number.isFinite(n)&&n>0?n:4096;return{memoryMB:t,osMemoryMB:t+So(e),cpuPercent:o,tasksMax:i}}function Ut(e,t){let o=`--max-old-space-size=${Math.max(256,Math.floor(t*0.75))}`,n=e.NODE_OPTIONS?.trim();if(n){if(/--max-old-space-size=/.test(n))return;e.NODE_OPTIONS=`${n} ${o}`}else e.NODE_OPTIONS=o}function bo(){if(process.platform!=="linux")return!1;try{return we("systemd-run",["--version"],{stdio:"ignore",timeout:3000}).status===0}catch{return!1}}function Ft(e){let{command:t,args:r,limits:o,scopeName:n,logger:i}=e;if(process.platform!=="linux"||!bo())return{command:t,args:r,wrapped:!1};let s=["-p",`MemoryMax=${o.osMemoryMB}M`,"-p",`MemoryHigh=${Ne(o.osMemoryMB)}M`,"-p",`TasksMax=${o.tasksMax}`];if(o.cpuPercent>0)s.push("-p",`CPUQuota=${o.cpuPercent}%`);let a=["--user","--scope","--quiet","--collect",`--unit=${n}`,...s,"--",t,...r];return i?.log(`[resource-limits] wrapping spawn in systemd scope ${n} (MemoryMax=${o.osMemoryMB}M)`),{command:"systemd-run",args:a,wrapped:!0}}function Ne(e){return Math.max(128,Math.floor(e*0.9))}function Nt(e){return U("/sys/fs/cgroup","shogo",`rt-${e}`)}function Lt(e){let{pid:t,limits:r,logger:o}=e;if(process.platform!=="linux")return!1;if(!Dt(U("/sys/fs/cgroup","cgroup.controllers")))return!1;let i=Nt(t);try{if(wo(i,{recursive:!0}),ae(U(i,"memory.max"),String(r.osMemoryMB*1024*1024)),r.cpuPercent>0){let s=Math.floor(r.cpuPercent/100*Ot);ae(U(i,"cpu.max"),`${s} ${Ot}`)}try{ae(U(i,"pids.max"),String(r.tasksMax))}catch{}return ae(U(i,"cgroup.procs"),String(t)),o?.log(`[resource-limits] attached pid ${t} to cgroup ${i} (memory.max=${r.osMemoryMB}M)`),!0}catch(s){return o?.warn(`[resource-limits] direct cgroup attach failed for pid ${t}: ${s?.message??s}`),!1}}function jt(e,t={}){let r=t.platform??process.platform,{limits:o,logger:n}=e;if(r==="win32")return n?.log("[resource-limits] Job Object memory limit applies on the next runtime spawn"),!1;if(r!=="linux")return!1;if(e.scopeName){let l=["--user","set-property","--runtime",e.scopeName,`MemoryMax=${o.osMemoryMB}M`,`MemoryHigh=${Ne(o.osMemoryMB)}M`];if(t.run)return t.run("systemctl",l),!0;return se("systemctl",l,{timeout:5000},(c)=>{if(c)n?.warn(`[resource-limits] set-property ${e.scopeName} failed: ${c.message}`);else n?.log(`[resource-limits] ${e.scopeName} MemoryMax=${o.osMemoryMB}M`)}),!0}if(!e.pid)return!1;let i=Nt(e.pid),s=t.exists??Dt,a=t.writeFile??ae;try{if(!s(U(i,"memory.max")))return!1;if(a(U(i,"memory.max"),String(o.osMemoryMB*1024*1024)),s(U(i,"memory.high")))a(U(i,"memory.high"),String(Ne(o.osMemoryMB)*1024*1024));return n?.log(`[resource-limits] raised cgroup ${i} memory.max to ${o.osMemoryMB}M`),!0}catch(l){return n?.warn(`[resource-limits] live cgroup update failed for ${i}: ${l?.message??l}`),!1}}function Ht(e){let{pid:t,limits:r,logger:o}=e;if(process.platform!=="win32")return!1;let n=process.env.SHOGO_WINJOB_HELPER||"@shogo/winjob";try{let i=Ce(n);if(typeof i.limitProcess!=="function")return!1;let s=i.limitProcess(t,{memoryBytes:r.osMemoryMB*1024*1024,killOnClose:!0});if(s)o?.log(`[resource-limits] attached pid ${t} to Job Object (${r.osMemoryMB}M)`);return s}catch{return!1}}function Bt(e){try{let t=JSON.parse(Wt(e,"utf-8"));if(!Array.isArray(t.pids))return[];return t.pids.filter((r)=>Number.isInteger(r)&&r>0)}catch{return[]}}var ko=20000,Ro=`$ErrorActionPreference='SilentlyContinue';foreach($p in Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,WorkingSetSize){"$($p.ProcessId) $($p.ParentProcessId) $([math]::Floor($p.WorkingSetSize/1024))"}`;function Po(e=process.platform){let[t,r]=e==="win32"?["powershell.exe",["-NoProfile","-NonInteractive","-Command",Ro]]:["ps",["-A","-o","pid=,ppid=,pgid=,rss="]];return new Promise((o)=>{se(t,r,{encoding:"utf-8",timeout:ko,maxBuffer:8388608},(n,i)=>{if(n||!i){o(null);return}let s=[];for(let a of String(i).split(/\r?\n/)){let l=a.trim().split(/\s+/).map((c)=>parseInt(c,10));if(l.some((c)=>!Number.isFinite(c)))continue;if(e==="win32"&&l.length===3)s.push({pid:l[0],ppid:l[1],pgid:0,kb:l[2]});else if(e!=="win32"&&l.length===4)s.push({pid:l[0],ppid:l[1],pgid:l[2],kb:l[3]})}o(s.length?s:null)})})}function To(e,t,r=process.platform,o=[]){if(r!=="win32"){let u=e.filter((p)=>p.pgid===t);return u.length?u.reduce((p,m)=>p+m.kb,0):null}if(!e.some((u)=>u.pid===t))return null;let n=new Set(o.filter((u)=>u!==t)),i=new Map;for(let u of e){if(u.pid===u.ppid)continue;let p=i.get(u.ppid);if(p)p.push(u);else i.set(u.ppid,[u])}let s=new Map(e.map((u)=>[u.pid,u])),a=new Set,l=[t],c=0;while(l.length){let u=l.pop();if(a.has(u))continue;a.add(u),c+=s.get(u)?.kb??0;for(let p of i.get(u)??[])if(!n.has(p.pid))l.push(p.pid)}return c}var Le=new Map;function $o(e,t,r){let o=`${t}:${r}`,n=Le.get(o);if(!n){let i={key:o,watchdogs:new Set,sampling:!1,timer:setInterval(()=>{xo(i,t)},r)};try{i.timer.unref?.()}catch{}Le.set(o,i),n=i}return n.watchdogs.add(e),n}async function xo(e,t){if(e.sampling)return;e.sampling=!0;try{let r=await Po(t);for(let o of[...e.watchdogs])o.observe(r)}finally{e.sampling=!1}}class He{sampler=null;consecutive=0;fired=!1;pid;ceilingMB;platform;intervalMs;strikes;excludePids;logger;onBreach;constructor(e){this.pid=e.pid,this.ceilingMB=e.ceilingMB,this.platform=e.platform??process.platform,this.intervalMs=e.intervalMs??parseInt(process.env.RUNTIME_RSS_WATCHDOG_MS||"15000",10),this.strikes=e.strikes??parseInt(process.env.RUNTIME_RSS_WATCHDOG_STRIKES||"2",10),this.excludePids=e.excludePids,this.logger=e.logger,this.onBreach=e.onBreach}start(){if(this.sampler)return;this.sampler=$o(this,this.platform,Math.max(2000,this.intervalMs))}stop(){if(this.sampler?.watchdogs.delete(this),this.sampler?.watchdogs.size===0)clearInterval(this.sampler.timer),Le.delete(this.sampler.key);this.sampler=null}observe(e){if(this.fired)return;let t=e?To(e,this.pid,this.platform,this.excludePids?.()??[]):null;if(t==null){this.consecutive=0;return}let r=Math.floor(t/1024);if(r<=this.ceilingMB){this.consecutive=0;return}if(this.consecutive+=1,this.logger?.warn(`[resource-limits] pid ${this.pid} RSS ${r}MB over ceiling ${this.ceilingMB}MB (strike ${this.consecutive}/${this.strikes})`),this.consecutive>=this.strikes)this.fired=!0,this.stop(),this.onBreach(r)}}import{CloudFileTransport as Ze}from"@shogo-ai/sdk/cloud-file-transport";import{killViteWatchFromPidfile as Xt}from"@shogo-ai/sdk/vite-watch";import{reapChildLedger as qo}from"@shogo-ai/sdk/child-ledger";import{watch as Jt,statSync as Mo}from"node:fs";import{relative as us,sep as Co,posix as Oo}from"node:path";var Do=1500,Wo=new Set(["node_modules",".git","dist","build",".vite",".cache"]),Uo=[".shogo/"];function Fo(e){if(!e)return!0;return e.split("/").some((t)=>Wo.has(t))}class ce{rootDir;transport;debounceMs;logger;onFlush;mode;git;commitAndPush;watcher=null;pending=new Set;timer=null;flushing=!1;stopped=!1;constructor(e){if(this.rootDir=e.rootDir,this.transport=e.transport,this.debounceMs=e.debounceMs??Do,this.logger=e.logger??console,this.onFlush=e.onFlush,this.mode=e.mode??"files",this.mode==="git"&&!e.git)throw Error('CloudSyncWatcher: mode: "git" requires the `git` option block');this.git=e.git,this.commitAndPush=e.commitAndPush??(async(t)=>{let r=await Promise.resolve().then(() => (Pe(),{}));return _o(t)})}start(){if(this.watcher)return;if(this.stopped)throw Error("CloudSyncWatcher: already stopped, build a fresh instance");try{this.watcher=Jt(this.rootDir,{recursive:!0},(e,t)=>{if(!t)return;this.handleEvent(String(t))})}catch(e){this.logger.warn(`[CloudSyncWatcher] recursive watch failed (${e?.message??e}); falling back to root-only watch.`),this.watcher=Jt(this.rootDir,(t,r)=>{if(!r)return;this.handleEvent(String(r))})}}async stop(){if(this.stopped)return;if(this.stopped=!0,this.timer)clearTimeout(this.timer),this.timer=null;if(this.watcher){try{this.watcher.close()}catch{}this.watcher=null}if(this.pending.size>0)await this.flush()}handleEvent(e){if(this.stopped)return;let t=e.split(Co).join(Oo.sep);if(Fo(t))return;try{let r=`${this.rootDir}/${t}`;if(Mo(r).isDirectory())return}catch{return}this.pending.add(t),this.scheduleFlush()}scheduleFlush(){if(this.timer)return;this.timer=setTimeout(()=>{this.timer=null,this.flush()},this.debounceMs)}async flush(){if(this.flushing){this.scheduleFlush();return}if(this.pending.size===0)return;this.flushing=!0;let e=Array.from(this.pending);this.pending.clear();try{if(this.mode==="git")await this.flushGit(e);else await this.flushFiles(e)}catch(t){this.logger.error(`[CloudSyncWatcher] flush failed: ${t?.message??t}`);for(let r of e)this.pending.add(r);this.scheduleFlush()}finally{this.flushing=!1}}async flushFiles(e){if(e.length===0)return;let t=await this.transport.uploadFiles(e);if(this.onFlush?.({uploaded:e,errors:t.errors.length}),t.errors.length>0)for(let r of t.errors)this.logger.warn(`[CloudSyncWatcher] upload ${r.path}: ${r.message}`)}async flushGit(e){if(!this.git)throw Error("CloudSyncWatcher: missing git options");let t=[],r=[];for(let i of e)if(Uo.some((s)=>i===s.slice(0,-1)||i.startsWith(s)))t.push(i);else r.push(i);if(t.length>0){let i=await this.transport.uploadFiles(t);if(i.errors.length>0)for(let s of i.errors)this.logger.warn(`[CloudSyncWatcher] upload ${s.path}: ${s.message}`)}let o=!1,n;if(r.length>0){let i=`auto: ${new Date().toISOString()}`;try{let s=await this.commitAndPush({apiUrl:this.git.apiUrl,apiKey:this.git.apiKey,projectId:this.git.projectId,localDir:this.rootDir,message:i,branch:this.git.branch,logger:this.logger});o=s.committed,n=s.commitSha}catch(s){throw s}}this.onFlush?.({uploaded:e,errors:0,committed:o,commitSha:n})}}Pe();var qt=1e4,Ve=2000;var Ye={turns:0,tasks:0,streams:0,children:0};function No(e){return`turns=${e.turns} tasks=${e.tasks} streams=${e.streams} children=${e.children}`}function Lo(e){return e.turns+e.tasks+e.streams+e.children}function _(e,t,r,o,n){return{busy:e,dead:t,reason:r,counts:o,countsText:No(o),...n!=null?{sampleAgeMs:n}:{}}}function Xe(e){let{now:t,startedAt:r,status:o,sample:n,processExited:i=!1,startupGraceMs:s=120000,staleMs:a=30000,deadAfterMs:l=60000}=e,c=e.terminalSessions??0,u=e.inflightCalls??0,p=n?.counts??Ye,m=n?Math.max(0,t-n.at):void 0;if(c>0)return _(!0,!1,"terminal",p,m);if(u>0)return _(!0,!1,"inflight-call",p,m);if(i)return _(!1,!0,"dead",p,m);if(o==="starting"||o==="restarting")return _(!0,!1,"grace",p,m);let f=t-(r||t);if(f>=0&&f<s)return _(!0,!1,"grace",p,m);if(o!=="running")return _(!0,!1,"unknown",p,m);if(!n)return _(!0,!1,"unknown",p,m);if(!n.ok){if((n.failingSince!=null?t-n.failingSince:0)>=l)return _(!1,!0,"dead",p,m);return _(!0,!1,"unknown",p,m)}if((m??0)>a)return _(!0,!1,"stale",p,m);if(Lo(p)>0)return _(!0,!1,"work",p,m);return _(!1,!1,"idle",p,m)}function ue(e,t){switch(e.reason){case"terminal":return"open terminal";case"inflight-call":return"in-flight project call";case"grace":return"starting up";case"unknown":return`unknown activity${e.sampleAgeMs!=null?` (last sample ${Math.round(e.sampleAgeMs/1000)}s old)`:" (no sample yet)"}`;case"stale":return`stale activity (last sample ${Math.round((e.sampleAgeMs??0)/1000)}s old)`;case"work":return e.countsText;case"dead":return"process gone";case"idle":return t!=null?`idle ${Math.round(t/60000)}m, ${e.countsText}`:`idle, ${e.countsText}`}}var Qe=37100,et=37900,zt=1,Vo=2,Te=16,Yo=900000,Xo=30000,zo=3000,Zt=8,Zo=1000,Qo=60000,Qt=8,er=300000,tr=3600000,en=8,tn=120000,rn=900000,on=60000,nn=500,sn=30000,rr=25000,an=5000,ln=500,cn=process.env.SHOGO_PERF_LOG==="1";function $e(e,t,r,o){if(!cn)return;console.log(`[shogo-perf] ${JSON.stringify({perf:"open",source:"worker",phase:e,projectId:t,openAttemptId:r,atMs:Date.now(),...o})}`)}var un=(e)=>({command:e,args:[]}),tt=null;function lr(){if(!tt)tt=jo(32).toString("hex");return tt}function or(e){return sr("sha256",lr()).update(`runtime:${e}`).digest("hex")}function pn(e){return sr("sha256",lr()).update(`webhook:${e}`).digest("hex")}function nr(e){let t=e.indexOf("?");if(t===-1)return{pathname:e,search:""};return{pathname:e.slice(0,t),search:e.slice(t)}}function ir(e){try{return ar(e).length===0}catch{return!0}}function rt(e,t,r){let o=[];switch(o.push(`Cannot spawn agent-runtime for project ${e}: no workspace directory available.`),o.push(""),t){case"no-auto-pull-config":o.push(" Reason: WorkerRuntimeManager was constructed without an `autoPull` config and no caller-provided `projectDir` was found on disk."),o.push(" This usually means a programmatic embedder forgot to wire up enrichSpawnConfig "+"or autoPull. CLI users should not see this — please file a bug.");break;case"no-projects-dir":o.push(" Reason: auto-pull is configured but `projectsDir` is empty. The worker needs a persistent root directory under which it can store cloned project workspaces.");break;case"auto-pull-disabled":o.push(` Reason: auto-pull was disabled (--no-auto-pull) and the expected pre-pulled workspace at ${r} is missing or empty.`);break}return o.push(""),o.push(" How to fix (pick one):"),o.push(" 1. Re-enable auto-pull (default). Drop the --no-auto-pull flag and restart"),o.push(" the worker. The first inbound request for this project will clone its"),o.push(" workspace from Shogo Cloud into <projectsDir>/<projectId>/."),o.push(""),o.push(" 2. Pre-pull manually with `shogo project pull <projectId>` before starting"),o.push(" the worker. Use this when you want full control over when the clone runs"),o.push(" (slow links, scheduled maintenance windows, etc.)."),o.push(""),o.push(" 3. Point the worker at an existing workspace by setting either:"),o.push(" --projects-dir <path> (per-invocation flag)"),o.push(" SHOGO_PROJECTS_DIR=<path> (env var, persists across restarts)"),o.push(" shogo config set projectsDir <path>"),o.push(" Whichever path you pick must contain a subdirectory named after the "),o.push(` project id (e.g. <path>/${e}/) populated with the project's source.`),o.push(""),o.push(" Docs: https://shogo.ai/docs/self-hosted-worker#workspace-seeding"),o.join(`
11
+ `)}class ot{opts;log;runtimes=new Map;usedPorts=new Set;spawnCommand;resolved=null;stopped=!1;resolvedLimits=void 0;watchers=new Map;pulledProjects=new Set;syncModes=new Map;activityTimer=null;hostLimitsFile=null;constructor(e={}){this.opts=e,this.log=e.logger??console,this.spawnCommand=e.spawnCommand??un,this.sweepOrphanedViteWatches(),this.ensureActivitySampler(),this.watchHostLimits()}resolveBinary(){if(!this.resolved)this.resolved=this.opts.resolveBin?this.opts.resolveBin():K({flag:this.opts.runtimeBin,env:this.opts.env});return this.resolved}async resolveLocalUrl(e,t){let{pathname:r,search:o}=nr(e);if(!(r.startsWith("/agent/")||r==="/agent"))return null;if(!t){let s=this.getActiveProjects();if(s.length!==1)return null;t=s[0]}let n=await this.spawnConfigFor(t);if(!n)return this.log.warn(`[WorkerRuntimeManager] No spawn config for ${t} — set defaultSpawnConfig or enrichSpawnConfig`),null;let i=await this.ensureRunning(t,n);if(!i.agentPort)return null;return this.touch(t),`http://127.0.0.1:${i.agentPort}${r}${o}`}deriveRuntimeToken(e){return or(e)}describeRejection(e,t){let{pathname:r}=nr(e);if(!(r.startsWith("/agent/")||r==="/agent"))return{code:"CLI_WORKER_HAS_NO_DATA_API",message:`cli-worker only serves /agent/* paths; tried: ${r}`};return{code:"CLI_WORKER_NO_PROJECT_FOR_PATH",message:`cli-worker received an /agent path without a single active project; projectId=${t??"none"}, path=${r}`}}async spawnConfigFor(e){let t=this.opts.defaultSpawnConfig;if(!t)return null;if(this.opts.enrichSpawnConfig)try{return await this.opts.enrichSpawnConfig(e,t)}catch(r){this.log.warn(`[WorkerRuntimeManager] enrichSpawnConfig failed for ${e}: ${r?.message??r}`)}return t}async ensureRunning(e,t){if(this.stopped)throw Error("WorkerRuntimeManager is stopped");let r=this.runtimes.get(e);if(r?.status==="failed"){let i=this.breakerCooldownMs(r.breakerTrips),s=Date.now()-r.lastFailureAt;if(s>=i)this.log.log(`[WorkerRuntimeManager] auto-heal: ${e} cooldown elapsed (${Math.round(s/1000)}s since trip #${r.breakerTrips}) — `+"clearing circuit breaker and attempting a fresh spawn"),this.healBreaker(r);else{let a=i-s;throw Error(`[WorkerRuntimeManager] cannot ensureRunning(${e}): ${r.lastError??"runtime is in failed state"} Will auto-retry in ~${Math.ceil(a/1000)}s; call resetFailure(${e}) to retry immediately, or stop(${e}) to give up.`)}}t=await this.maybeAutoPull(e,t);let o=this.runtimes.get(e);if(o?.status==="running")return this.touch(e),this.snapshot(o);if(o?.startPromise){let i=await o.startPromise;return this.snapshot(i)}let n=o??this.makeSlot(e,t);if(!o)this.runtimes.set(e,n);n.spawnConfig=t,n.startPromise=this.doStart(n);try{let i=await n.startPromise;return this.enforceMaxRuntimes(e),this.snapshot(i)}finally{n.startPromise=null}}enforceMaxRuntimes(e){let t=this.opts.maxRuntimes;if(t==null||!Number.isFinite(t)||t<=0)return;let r=Date.now(),o=Array.from(this.runtimes.values()).filter((i)=>i.status==="running");if(o.length<=t)return;let n=o.filter((i)=>i.projectId!==e&&r-i.lastUsedAt>=Xo).sort((i,s)=>i.lastUsedAt-s.lastUsedAt);this.evictLru(t,o.length,n,r)}async evictLru(e,t,r,o){let n=t-e,i=new Map;await Promise.all(r.map(async(s)=>{i.set(s.projectId,await this.freshActivityDecision(s.projectId))}));for(let s of r){if(n<=0)break;if(s.status!=="running"||this.runtimes.get(s.projectId)!==s)continue;let a=i.get(s.projectId);if(!a)continue;if(a.dead){this.log.log(`[WorkerRuntimeManager] maxRuntimes=${e}: reaping dead ${s.projectId} (${ue(a)})`),this.stop(s.projectId,"SIGTERM","dead").catch((c)=>{this.log.warn(`[WorkerRuntimeManager] reap of ${s.projectId} failed: ${c?.message??c}`)}),n--;continue}if(a.busy){this.log.log(`[WorkerRuntimeManager] maxRuntimes=${e}: keeping ${s.projectId} over cap=${e}: ${a.reason} (${ue(a)})`);continue}let l=o-s.lastUsedAt;this.log.log(`[WorkerRuntimeManager] maxRuntimes=${e} exceeded (${t} running) — `+`LRU-evicting ${s.projectId} (idle ${Math.round(l/1000)}s, ${a.countsText})`),this.stop(s.projectId,"SIGTERM","lru-evict").catch((c)=>{this.log.warn(`[WorkerRuntimeManager] maxRuntimes eviction of ${s.projectId} failed: ${c?.message??c}`)}),n--}if(n>0)this.log.log(`[WorkerRuntimeManager] maxRuntimes=${e} still exceeded by ${n} after eviction pass — `+"remaining over-cap slots are active or unverifiable; will retry on next spawn / idle reap")}async ensurePulled(e,t){return this.maybeAutoPull(e,t)}async maybeAutoPull(e,t){let r=this.opts.autoPull;if(t.projectDir&&ze(t.projectDir))return t;if(!r)throw Error(rt(e,"no-auto-pull-config",null));if(!r.projectsDir)throw Error(rt(e,"no-projects-dir",null));if(!r.enabled){let s=Y(r.projectsDir,e);if(ze(s)&&!ir(s))return{...t,projectDir:s};throw Error(rt(e,"auto-pull-disabled",s))}if(this.pulledProjects.has(e))return{...t,projectDir:Y(r.projectsDir,e)};let o=Y(r.projectsDir,e),n=r.logger??this.log,i={cloneProject:r.gitOps?.cloneProject??Kt,gitIsAvailable:r.gitOps?.gitIsAvailable??Gt,isGitRepo:r.gitOps?.isGitRepo??Re};this.pulledProjects.add(e);try{Vt(o,{recursive:!0});let s=ir(o),a=i.isGitRepo(o),u=(r.useGit!==!1?await i.gitIsAvailable():!1)&&(s||a)?"git":"files";if(this.syncModes.set(e,u),u==="git"){if(s){n.log(`[WorkerRuntimeManager] auto-pull: git clone project ${e} into ${o}`);try{let p=await i.cloneProject({apiUrl:t.cloudUrl,apiKey:t.apiKey,projectId:e,localDir:o,shallow:!0,logger:n});n.log(`[WorkerRuntimeManager] auto-pull: ${e} cloned at ${p.commitSha.slice(0,8)}`)}catch(p){n.warn(`[WorkerRuntimeManager] auto-pull: git clone failed for ${e} (${p?.message??p}); falling back to CloudFileTransport.downloadAll`),this.syncModes.set(e,"files"),await this.fileTransportClone(e,o,t,n)}}else if(a)n.log(`[WorkerRuntimeManager] auto-pull: ${e} already has .git/; skipping clone`);if(this.syncModes.get(e)==="git")await this.topUpShogoState(e,o,t,n)}else if(s)await this.fileTransportClone(e,o,t,n);else n.log(`[WorkerRuntimeManager] auto-pull: ${e} workspace already populated; skipping clone`);if(r.watch!==!1&&!this.watchers.has(e))try{let p=new Ze({apiUrl:t.cloudUrl,apiKey:t.apiKey,projectId:e,localDir:o}),m=this.syncModes.get(e)??"files",f=new ce({rootDir:o,transport:p,logger:n,mode:m,git:m==="git"?{apiUrl:t.cloudUrl,apiKey:t.apiKey,projectId:e}:void 0});f.start(),this.watchers.set(e,f)}catch(p){n.warn(`[WorkerRuntimeManager] auto-pull: watcher start failed for ${e}: ${p?.message??p}`)}}catch(s){n.warn(`[WorkerRuntimeManager] auto-pull: failed for ${e} — runtime will fall back to template defaults. `+`(${s?.message??s})`)}return{...t,projectDir:o}}async fileTransportClone(e,t,r,o){o.log(`[WorkerRuntimeManager] auto-pull: file-transport clone of ${e} into ${t}`);let i=await new Ze({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:t}).downloadAll();o.log(`[WorkerRuntimeManager] auto-pull: ${e} downloaded ${i.downloaded} files (${i.errors.length} errors)`)}async topUpShogoState(e,t,r,o){try{let n=new Ze({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:t}),s=(await n.listManifest()).filter((l)=>l.path===".shogo"||l.path.startsWith(".shogo/"));if(s.length===0)return;let a=await n.downloadFiles(s);o.log(`[WorkerRuntimeManager] auto-pull: ${e} .shogo/ top-up downloaded ${a.downloaded} files (${a.errors.length} errors)`)}catch(n){o.warn(`[WorkerRuntimeManager] auto-pull: .shogo top-up failed for ${e}: ${n?.message??n}`)}}shouldDeferToCloudSync(e){return!!this.opts.autoPull?.enabled&&this.pulledProjects.has(e)}status(e){let t=this.runtimes.get(e);return t?this.snapshot(t):null}getActiveProjects(){return Array.from(this.runtimes.keys()).filter((e)=>{let t=this.runtimes.get(e);return t&&(t.status==="running"||t.status==="starting"||t.status==="restarting")})}touch(e){let t=this.runtimes.get(e);if(!t)return;t.lastUsedAt=Date.now(),this.armIdleTimer(t)}async stop(e,t="SIGTERM",r="stop"){let o=this.runtimes.get(e);if(!o)return;if(o.status="stopping",o.restartTimer)clearTimeout(o.restartTimer),o.restartTimer=null;if(o.idleTimer)clearTimeout(o.idleTimer),o.idleTimer=null;if(o.graceTimer)clearTimeout(o.graceTimer),o.graceTimer=null;if(o.limitWatchdog)o.limitWatchdog.stop(),o.limitWatchdog=null;if(o.proc){this.killProcessGroup(o,t),this.killViteWatch(o);try{o.proc.kill(t)}catch{}await this.waitForExit(o.proc,5000),this.killProcessGroup(o,"SIGKILL"),this.killViteWatch(o),this.reapChildLedgerFor(e,o.spawnConfig.projectDir)}o.pid=null,this.releasePort(o.agentPort),this.runtimes.delete(e),this.notifyGone(e,{reason:r})}notifyGone(e,t){let r=this.opts.onRuntimeGone;if(!r)return;try{r(e,t)}catch(o){this.log.warn(`[WorkerRuntimeManager] onRuntimeGone(${e}) threw: ${o?.message??o}`)}}resetFailure(e){let t=this.runtimes.get(e);if(!t||t.status!=="failed")return!1;if(t.restartTimer)clearTimeout(t.restartTimer),t.restartTimer=null;if(t.idleTimer)clearTimeout(t.idleTimer),t.idleTimer=null;if(t.graceTimer)clearTimeout(t.graceTimer),t.graceTimer=null;if(t.limitWatchdog)t.limitWatchdog.stop(),t.limitWatchdog=null;return this.runtimes.delete(e),this.log.log(`[WorkerRuntimeManager] resetFailure: ${e} cleared, next ensureRunning will respawn`),!0}breakerCooldownMs(e){let t=Math.max(0,e-1),r=tn*Math.pow(2,t);return Math.min(r,rn)}healBreaker(e){e.status="stopped",e.consecutiveFailures=0}async stopAll(e="SIGTERM"){if(this.stopped=!0,this.unwatchHostLimits(),this.activityTimer)clearInterval(this.activityTimer),this.activityTimer=null;let t=Array.from(this.watchers.keys());await Promise.all(t.map(async(o)=>{let n=this.watchers.get(o);if(this.watchers.delete(o),n)try{await n.stop()}catch(i){this.log.warn(`[WorkerRuntimeManager] watcher stop ${o}: ${i?.message??i}`)}}));let r=Array.from(this.runtimes.keys());await Promise.all(r.map((o)=>this.stop(o,e).catch((n)=>{this.log.error(`[WorkerRuntimeManager] Failed to stop ${o}: ${n?.message??n}`)})))}makeSlot(e,t){return{projectId:e,agentPort:0,apiServerPort:0,status:"starting",proc:null,pid:null,startedAt:0,lastStdoutAt:0,lastUsedAt:Date.now(),restarts:0,consecutiveFailures:0,lastFailureAt:0,breakerTrips:0,restartTimestamps:[],graceTimer:null,restartTimer:null,idleTimer:null,limitWatchdog:null,spawnConfig:t,startPromise:null}}async doStart(e){let t=this.resolveBinary();if(!t)throw e.status="error",e.lastError="agent-runtime binary not found (run `shogo runtime install`)",Error(e.lastError);if(!e.agentPort)e.agentPort=await this.allocatePort(),e.apiServerPort=e.agentPort+zt;let r=this.buildEnv(e,t.path);if(e.runtimeToken=r.RUNTIME_AUTH_SECRET,r.SHOGO_EXEC_PIDS_FILE)try{Ho(r.SHOGO_EXEC_PIDS_FILE,{force:!0})}catch{}let o=this.resolveCwd(e),n=this.spawnCommand(t.path),i=this.resolveLimits(),{command:s,args:a}=n,l=!1,c=null;if(i){let g=`shogo-rt-${e.projectId.slice(0,8)}-${Date.now().toString(36)}`,d=Ft({command:n.command,args:n.args,limits:i,scopeName:g,logger:this.log});s=d.command,a=d.args,l=d.wrapped,c=d.wrapped?g:null}e.cgroupScope=c,this.log.log(`[WorkerRuntimeManager] Spawning agent-runtime for ${e.projectId} via ${s} ${a.join(" ")} (port=${e.agentPort}, source=${t.source}${i?`, memCeil=${i.memoryMB}M`:""})`);let u=process.platform!=="win32",p=T(s,a,{cwd:o,env:r,detached:u,stdio:["ignore","pipe","pipe"]});if(u)try{p.unref()}catch{}e.proc=p,e.pid=p.pid??null,e.status="starting",e.startedAt=Date.now(),e.activity=null,e.stderrTail=[];let m=e.spawnConfig.extraEnv?.SHOGO_OPEN_ID;if($e("agent-spawned",e.projectId,m,{pid:e.pid,command:s,args:a,source:t.source}),i)try{this.applyPostSpawnLimits(e,i,l)}catch(g){this.log.warn(`[WorkerRuntimeManager] failed to apply resource limits for ${e.projectId}: ${g?.message??g}`)}e.lastStdoutAt=e.startedAt,p.on("error",(g)=>{e.lastError=g?.message??String(g),this.log.error(`[WorkerRuntimeManager] spawn error for ${e.projectId}: ${e.lastError}`)}),p.on("exit",(g,d)=>{this.handleExit(e,g,d)});let f=`[runtime:${e.projectId.slice(0,8)}]`,y=!0;p.stdout?.on("data",(g)=>{if(e.lastStdoutAt=Date.now(),y)y=!1,$e("agent-first-output",e.projectId,m,{elapsedMs:Date.now()-(e.startedAt??Date.now())});for(let d of g.toString().trimEnd().split(`
12
+ `))if(d)this.log.log(`${f} ${d}`)}),p.stderr?.on("data",(g)=>{if(e.lastStdoutAt=Date.now(),y)y=!1,$e("agent-first-output",e.projectId,m,{elapsedMs:Date.now()-(e.startedAt??Date.now()),stream:"stderr"});let d=e.stderrTail??=[];for(let v of g.toString().trimEnd().split(`
13
+ `)){if(!v)continue;if(this.log.error(`${f} ${v}`),d.push(v),d.length>Zt)d.splice(0,d.length-Zt)}});try{return await this.waitForHealth(e,sn),$e("agent-health-ready",e.projectId,m,{elapsedMs:Date.now()-(e.startedAt??Date.now()),agentPort:e.agentPort}),e.status="running",e.lastUsedAt=Date.now(),this.armIdleTimer(e),this.armGraceTimer(e),this.sampleActivity(e).catch(()=>null),e}catch(g){e.status="error",e.lastError=g?.message??String(g),this.killProcessGroup(e,"SIGTERM"),this.killViteWatch(e);try{p.kill("SIGTERM")}catch{}throw this.releasePort(e.agentPort),e.agentPort=0,e.apiServerPort=0,g}}resolveLimits(){if(this.resolvedLimits===void 0)this.resolvedLimits=je(this.opts.env??process.env);return this.resolvedLimits}watchHostLimits(){let e=(this.opts.env??process.env).SHOGO_HOST_LIMITS_FILE;if(!e||this.hostLimitsFile)return;this.hostLimitsFile=e,Go(e,{interval:2000},()=>this.applyHostLimitsChange())}applyHostLimitsChange(){let e=this.opts.env??process.env,t;try{t=je(e)}catch(o){this.log.warn(`[WorkerRuntimeManager] host limits re-read failed: ${o?.message??o}`);return}if(this.resolvedLimits=t,!t)return;let r=[...this.runtimes.values()].filter((o)=>o.pid&&o.status!=="stopped");if(r.length===0)return;this.log.log(`[WorkerRuntimeManager] agent-command budget changed — memory cap now ${t.osMemoryMB}M (${t.memoryMB}M runtime + ${t.osMemoryMB-t.memoryMB}M commands)`);for(let o of r)try{jt({pid:o.pid,scopeName:o.cgroupScope??null,limits:t,logger:this.log})}catch(n){this.log.warn(`[WorkerRuntimeManager] live memory cap for ${o.projectId} failed: ${n?.message??n}`)}}unwatchHostLimits(){if(!this.hostLimitsFile)return;Bo(this.hostLimitsFile),this.hostLimitsFile=null}applyPostSpawnLimits(e,t,r){let o=e.pid;if(!o)return;if(process.platform==="linux"){if(r)return;if(Lt({pid:o,limits:t,logger:this.log}))return}else if(process.platform==="win32"){if(Ht({pid:o,limits:t,logger:this.log}))return}let n=this.execPidsFile(e);e.limitWatchdog=new He({pid:o,ceilingMB:t.memoryMB,excludePids:()=>Bt(n),logger:this.log,onBreach:(i)=>this.handleLimitBreach(e,i,t.memoryMB)}),e.limitWatchdog.start()}handleLimitBreach(e,t,r){if(this.log.warn(`[WorkerRuntimeManager] runtime ${e.projectId} exceeded memory ceiling (${t}MB > ${r}MB) — restarting`),e.lastError=`memory ceiling exceeded (${t}MB > ${r}MB)`,e.limitWatchdog)e.limitWatchdog.stop(),e.limitWatchdog=null;this.killProcessGroup(e,"SIGTERM"),this.killViteWatch(e);try{e.proc?.kill("SIGTERM")}catch{}let o=setTimeout(()=>{this.killProcessGroup(e,"SIGKILL"),this.killViteWatch(e)},5000);try{o.unref?.()}catch{}}killProcessGroup(e,t){if(!e.pid)return;if(process.platform==="win32"){try{we("taskkill",["/F","/T","/PID",String(e.pid)],{stdio:"ignore",windowsHide:!0})}catch{}return}try{process.kill(-e.pid,t)}catch{}}armGraceTimer(e){if(e.graceTimer)clearTimeout(e.graceTimer),e.graceTimer=null;e.graceTimer=setTimeout(()=>{if(e.graceTimer=null,e.consecutiveFailures>0)e.consecutiveFailures=0;e.breakerTrips=0,e.restartTimestamps=[]},on);try{e.graceTimer.unref?.()}catch{}}buildEnv(e,t){let r=e.spawnConfig,o={...this.opts.env??process.env,PROJECT_ID:e.projectId,PORT:String(e.agentPort),API_SERVER_PORT:String(e.apiServerPort),SKILL_SERVER_PORT:String(e.apiServerPort),WORKSPACE_API_PORT_BASE:String(e.agentPort+Vo),NODE_ENV:"production",STARTUP_TIME:String(Date.now()),SHOGO_CLOUD_URL:r.cloudUrl,SHOGO_API_URL:r.cloudUrl,SHOGO_API_KEY:r.apiKey,RUNTIME_AUTH_SECRET:or(e.projectId),WEBHOOK_TOKEN:pn(e.projectId)};if(r.projectDir)o.PROJECT_DIR=r.projectDir,o.WORKSPACE_DIR=r.projectDir;if(this.shouldDeferToCloudSync(e.projectId))o.SHOGO_CLOUD_SYNC="1";if(r.aiProxyUrl)o.AI_PROXY_URL=r.aiProxyUrl;if(r.aiProxyToken)o.AI_PROXY_TOKEN=r.aiProxyToken;if(r.techStackId)o.TECH_STACK_ID=r.techStackId;if(r.name)o.AGENT_NAME=r.name;if(r.workspaceId)o.WORKSPACE_ID=r.workspaceId;if(!o.TREE_SITTER_WASM_DIR)o.TREE_SITTER_WASM_DIR=Y(Jo(t),"tree-sitter-wasm");if(r.extraEnv)Object.assign(o,r.extraEnv);let n=this.resolveLimits();if(n){Ut(o,n.memoryMB),o.SHOGO_EXEC_PIDS_FILE=this.execPidsFile(e);let i=this.opts.env??process.env;if(i.SHOGO_HOST_LIMITS_FILE)o.SHOGO_HOST_LIMITS_FILE=i.SHOGO_HOST_LIMITS_FILE;else o.SHOGO_EXEC_MEMORY_MB??=String(n.memoryMB)}return o}execPidsFile(e){return Y(Yt(),"shogo-exec-pids",`${e.projectId.replace(/[^\w.-]/g,"_")}.json`)}killViteWatch(e){let t=e.spawnConfig.projectDir;if(!t)return;try{Xt(t,{logger:this.log})}catch(r){this.log.warn(`[WorkerRuntimeManager] Vite watch cleanup for ${e.projectId} failed: ${r?.message??r}`)}}reapChildLedgerFor(e,t){if(!t||process.platform==="win32")return;try{let{reaped:r,dropped:o}=qo(t,{logger:this.log});if(r.length>0)this.log.log(`[WorkerRuntimeManager] Reaped ${r.length} leftover child group(s) for ${e}: `+r.map((n)=>`${n.label}(pgid=${n.pgid})`).join(", "));if(o.length>0)this.log.log(`[WorkerRuntimeManager] Dropped ${o.length} stale ledger entr(ies) for ${e} (process gone or pid reused)`)}catch(r){this.log.warn(`[WorkerRuntimeManager] Child ledger reap for ${e} failed: ${r?.message??r}`)}}sweepOrphanedViteWatches(){let e=this.opts.autoPull?.projectsDir;if(!e)return;let t;try{t=ar(e,{withFileTypes:!0})}catch{return}for(let r of t){if(!r.isDirectory()||this.runtimes.has(r.name))continue;let o=Y(e,r.name);try{let n=Xt(o,{logger:this.log});if(n)this.log.log(`[WorkerRuntimeManager] Reaped orphaned Vite watch for ${r.name} (pid=${n.pid})`);this.reapChildLedgerFor(r.name,o)}catch(n){this.log.warn(`[WorkerRuntimeManager] Vite watch startup sweep failed for ${r.name}: ${n?.message??n}`)}}}resolveCwd(e){let t=e.spawnConfig;if(t.projectDir&&ze(t.projectDir))return t.projectDir;let r=this.opts.runtimeWorkDir??Y(Yt(),"shogo-runtime",e.projectId);return Vt(r,{recursive:!0}),r}handleExit(e,t,r){let o=r===null&&t===0,n=e.status==="stopping"||this.stopped;if(this.log.log(`[WorkerRuntimeManager] runtime ${e.projectId} exited (code=${t}, signal=${r})`),!n){let u=e.stderrTail?.length?`
14
14
  ${e.stderrTail.join(`
15
- `)}`:" (nothing on stderr)";this.log.warn(`[WorkerRuntimeManager] runtime ${e.projectId} exited WITHOUT a stop request (code=${t}, signal=${r}, uptime ${Math.round((Date.now()-e.startedAt)/1000)}s, last activity ${this.activityDecision(e.projectId).countsText}) — last stderr:${u}`)}if(e.proc=null,e.graceTimer)clearTimeout(e.graceTimer),e.graceTimer=null;if(e.limitWatchdog)e.limitWatchdog.stop(),e.limitWatchdog=null;if(this.killViteWatch(e),e.status==="stopping"||this.stopped){e.status="stopped",e.pid=null,this.releasePort(e.agentPort),e.agentPort=0,e.apiServerPort=0;return}if(o){e.status="stopped",e.pid=null,this.releasePort(e.agentPort),e.agentPort=0,e.apiServerPort=0,this.runtimes.delete(e.projectId),this.notifyGone(e.projectId,{reason:"exited",code:t,signal:r});return}this.killProcessGroup(e,"SIGKILL"),this.killViteWatch(e),this.reapChildLedgerFor(e.projectId,e.spawnConfig.projectDir),e.pid=null;let i=Date.now(),s=i-e.lastFailureAt<=Yt;e.consecutiveFailures=s?e.consecutiveFailures+1:1,e.lastFailureAt=i,e.restarts+=1,e.lastError=`exited code=${t} signal=${r}`,e.restartTimestamps.push(i),e.restartTimestamps=e.restartTimestamps.filter((u)=>i-u<=Xt);let a=e.consecutiveFailures>=Vt,l=e.restartTimestamps.length>=qo;if(a||l){e.status="failed",e.breakerTrips+=1;let u=this.breakerCooldownMs(e.breakerTrips),p=a?`${e.consecutiveFailures} consecutive non-clean exits within ${Math.round(Yt/1000)}s`:`${e.restartTimestamps.length} non-clean exits within the last ${Math.round(Xt/60000)}m (slow-bleed loop — gaps between crashes kept clearing the tight-loop window)`;if(e.lastError=`Circuit breaker tripped (trip #${e.breakerTrips}): ${p} (last: code=${t} signal=${r}). Most recent on macOS is jetsam OOM (signal=SIGKILL with code=null); the previous incarnation's vite/tsserver/preview-manager children were reaped to prevent further RSS growth. Will auto-retry in ~${Math.round(u/1000)}s; call resetFailure(${e.projectId}) to retry immediately, or stop(${e.projectId}) to give up.`,this.releasePort(e.agentPort),e.agentPort=0,e.apiServerPort=0,e.restartTimer)clearTimeout(e.restartTimer),e.restartTimer=null;if(e.idleTimer)clearTimeout(e.idleTimer),e.idleTimer=null;this.log.error(`[WorkerRuntimeManager] ${e.lastError}`),this.notifyGone(e.projectId,{reason:"failed",code:t,signal:r});return}let c=this.restartBackoffMs(e.restarts);if(e.status="restarting",this.log.warn(`[WorkerRuntimeManager] restarting ${e.projectId} in ${Math.round(c/1000)}s (restart #${e.restarts}, consecutive failures ${e.consecutiveFailures}/${Vt})`),e.restartTimer)clearTimeout(e.restartTimer);e.restartTimer=setTimeout(()=>{if(e.restartTimer=null,e.status==="failed"||this.stopped)return;e.startPromise=this.doStart(e).then((u)=>(e.startPromise=null,u)).catch((u)=>(e.startPromise=null,this.log.error(`[WorkerRuntimeManager] restart of ${e.projectId} failed: ${u?.message??u}`),e))},c);try{e.restartTimer.unref?.()}catch{}}restartBackoffMs(e){let t=Math.min(Ko*Math.pow(2,Math.max(0,e-1)),Jo),r=t*0.2*Math.random();return t+r}armIdleTimer(e){if(e.idleTimer)clearTimeout(e.idleTimer),e.idleTimer=null;let t=this.opts.idleMs??Ho;if(!Number.isFinite(t)||t<=0)return;e.idleTimer=setTimeout(async()=>{e.idleTimer=null;let r=Date.now()-e.lastUsedAt;if(r<t){this.armIdleTimer(e);return}let o=await this.freshActivityDecision(e.projectId);if(this.runtimes.get(e.projectId)!==e||e.status!=="running"||e.idleTimer)return;if(o.dead){this.log.log(`[WorkerRuntimeManager] reaping dead ${e.projectId} (${le(o)})`),this.stop(e.projectId,"SIGTERM","dead").catch((n)=>{this.log.warn(`[WorkerRuntimeManager] reap stop failed: ${n?.message??n}`)});return}if(o.busy){this.log.log(`[WorkerRuntimeManager] not idle-evicting ${e.projectId}: ${o.reason} (${le(o)})`),this.touch(e.projectId);return}this.log.log(`[WorkerRuntimeManager] idle-evicting ${e.projectId} after ${Math.round(r/1000)}s`),this.stop(e.projectId,"SIGTERM","idle-evict").catch((n)=>{this.log.warn(`[WorkerRuntimeManager] idle stop failed: ${n?.message??n}`)})},t)}async hasActiveWork(e){let t=this.runtimes.get(e);if(!t)return!1;let r=await this.activeWork(t);return!!r&&r.turns+r.tasks+r.streams+r.children>0}async activeWork(e){if(e.status!=="running"||!e.agentPort)return null;try{let t=await fetch(`http://127.0.0.1:${e.agentPort}/pool/activity`,{headers:e.runtimeToken?{"x-runtime-token":e.runtimeToken}:{},signal:AbortSignal.timeout(Go)});if(!t.ok)return null;let r=await t.json();if(!(typeof r.activeTurns==="number"||typeof r.activeTasks==="number"||typeof r.activeStreams==="number"||typeof r.activeChildren==="number"))return null;return{turns:r.activeTurns??0,tasks:r.activeTasks??0,streams:r.activeStreams??0,children:r.activeChildren??0}}catch{return null}}async sampleActivity(e){let t=Date.now(),r=await this.activeWork(e),o=e.activity,n=r?{counts:r,at:t,ok:!0}:{counts:o?.ok?o.counts:qe,at:t,ok:!1,error:e.status!=="running"?`status=${e.status}`:e.agentPort?"no answer from /pool/activity":"no agent port",failingSince:o&&!o.ok?o.failingSince??t:t};return e.activity=n,n}async sampleAllActivity(){let e=[...this.runtimes.values()].filter((t)=>t.status==="running"&&!!t.agentPort);if(e.length===0)return;await Promise.all(e.map((t)=>this.sampleActivity(t).catch(()=>null)))}ensureActivitySampler(){if(this.activityTimer)return;let e=setInterval(()=>{this.sampleAllActivity()},Ht);e.unref?.(),this.activityTimer=e}activityDecision(e,t){let r=Date.now(),o=this.runtimes.get(e);if(!o)return Ve({now:r,startedAt:r,status:"stopped",processExited:!0});return Ve({now:r,startedAt:o.startedAt,status:o.status,sample:o.activity??null,terminalSessions:t?.terminalSessions,inflightCalls:t?.inflightCalls,processExited:this.processExited(o)})}processExited(e){if(e.status==="stopped"||e.status==="failed")return!0;if(e.proc)return!1;return e.status==="running"}async freshActivityDecision(e,t){let r=this.runtimes.get(e);if(r&&r.status==="running"&&r.agentPort){let o=r.activity;if(!o||Date.now()-o.at>Je)await this.sampleActivity(r).catch(()=>null)}return this.activityDecision(e,t)}async refreshActivity(e){let t=this.runtimes.get(e);if(!t||t.status!=="running"||!t.agentPort)return;let r=t.activity;if(r&&Date.now()-r.at<=Je)return;await this.sampleActivity(t).catch(()=>null)}async allocatePort(){let e=Ze-ze,t=Math.min(e,50);for(let r=0;r<t;r++){let o=ze+Math.floor(Math.random()*e);if(o+Te-1>Ze)continue;let n=!0;for(let a=0;a<Te;a++)if(this.usedPorts.has(o+a)){n=!1;break}if(!n)continue;let i=await this.isPortListening(o),s=await this.isPortListening(o+Jt);if(i||s)continue;for(let a=0;a<Te;a++)this.usedPorts.add(o+a);return o}throw Error(`Cannot allocate port in range ${ze}-${Ze} after ${t} attempts`)}releasePort(e){if(!e)return;for(let t=0;t<Te;t++)this.usedPorts.delete(e+t)}async isPortListening(e){let t=new AbortController,r=setTimeout(()=>t.abort(),250);try{return await fetch(`http://127.0.0.1:${e}/`,{method:"HEAD",signal:t.signal}),clearTimeout(r),!0}catch{return clearTimeout(r),!1}}tcpProbe(e){return new Promise((t)=>{let r=!1,o=(i)=>{if(r)return;r=!0;try{n.destroy()}catch{}t(i)},n=No({host:"127.0.0.1",port:e});n.setTimeout(en),n.once("connect",()=>o(!0)),n.once("error",()=>o(!1)),n.once("timeout",()=>o(!1))})}async waitForHealth(e,t){let{agentPort:r,proc:o}=e;if(!o)throw Error(`waitForHealth: slot ${e.projectId} has no spawned process`);let n=Date.now(),i=n+t,s=null,a=!1,l=0,c=0,u=0,p=0,d=n;while(Date.now()<i){if(p++,o.exitCode!==null||o.signalCode!=null||o.killed)throw Error(`agent-runtime exited (code=${o.exitCode}, signal=${o.signalCode}) before becoming healthy on port ${r}`);c++;let h=new AbortController,m=setTimeout(()=>h.abort(),1500);try{let g=await fetch(`http://127.0.0.1:${r}/health`,{method:"GET",signal:h.signal});if(clearTimeout(m),g.ok){this.log.log(`[WorkerRuntimeManager] /health ready for ${e.projectId} on port ${r} (HTTP ${g.status} after ${Date.now()-n}ms, ${p} iter, ${c} http)`);return}s=`HTTP /health returned ${g.status}`}catch(g){clearTimeout(m);let b=g?.name??"Error",Q=g?.code??g?.cause?.code;s=`HTTP /health failed: ${b}${Q?`(${Q})`:""}: ${g?.message??g}`}if(u++,a=await this.tcpProbe(r),a){l=Date.now();let g=Date.now()-e.lastStdoutAt;if(g>=zt)throw Error(`agent-runtime wedged on port ${r}: TCP listening but stdout silent for ${g}ms (> ${zt}ms window); ${c} /health attempts, last error: ${s??"n/a"}`)}let w=Date.now();if(w-d>=Qo){let g=w-n,b=w-e.lastStdoutAt,Q=l>0?w-l:null;this.log.log(`[WorkerRuntimeManager] still waiting for /health on ${e.projectId} port ${r} (${(g/1000).toFixed(1)}s elapsed, tcpListening=${a}${Q!=null?`(${Q}ms ago)`:""}, lastStdout=${b}ms ago, ${c} http, ${u} tcp, lastError=${s??"n/a"})`),d=w}await new Promise((g)=>setTimeout(g,zo))}if(o.exitCode!==null||o.signalCode!=null||o.killed)throw Error(`agent-runtime exited (code=${o.exitCode}, signal=${o.signalCode}) before becoming healthy on port ${r}`);throw Error(`Timeout waiting for agent-runtime /health on port ${r} after ${p} iter (httpAttempts=${c}, tcpAttempts=${u}, tcpListening=${a}, lastError=${s??"n/a"})`)}async waitForExit(e,t){if(e.exitCode!==null||e.signalCode!=null||e.killed)return;await new Promise((r)=>{let o=setTimeout(()=>{try{e.kill("SIGKILL")}catch{}r()},t);e.once("exit",()=>{clearTimeout(o),r()})})}snapshot(e){return{projectId:e.projectId,status:e.status,agentPort:e.agentPort||void 0,apiServerPort:e.apiServerPort||void 0,pid:e.proc?.pid,startedAt:e.startedAt||void 0,lastUsedAt:e.lastUsedAt,restarts:e.restarts,lastError:e.lastError}}}import{hostname as rt,platform as ot,arch as nt}from"node:os";function Y(){if(typeof __SHOGO_WORKER_VERSION__==="string"&&__SHOGO_WORKER_VERSION__.length>0)return`shogo-cli/${__SHOGO_WORKER_VERSION__}`;return"shogo-cli/unknown"}class ar extends Error{code="TUNNEL_WS_HEADERS_UNSUPPORTED";constructor(){super("Tunnel WebSocket auth requires a runtime with WebSocket header support (Bun or Node >= 21). Current runtime does not support WebSocket constructor headers.");this.name="TunnelWebSocketHeaderSupportError"}}var ce=60,Ee=300,it=3,nn=it,sn=1800000,an=25000,nr=1000,ir=60000,sr=3;class st{opts;log;pollTimer=null;ws=null;wsIdleTimer=null;heartbeatTimer=null;stopped=!1;currentPollInterval=ce;wsReconnectAttempt=0;lastHeartbeatError=null;consecutiveAuthFailures=0;consecutiveAuthSuccesses=0;serverPublishedWsUrl=null;lastInstanceId=null;activeAbortControllers=new Map;constructor(e){this.opts=e,this.log=e.logger??console}get instanceId(){return this.lastInstanceId}start(){if(!this.opts.apiKey){this.log.log("[WorkerTunnel] No API key set, skipping tunnel");return}this.stopped=!1,this.wsReconnectAttempt=0,this.currentPollInterval=ce,this.log.log("[WorkerTunnel] Starting heartbeat polling to Shogo Cloud..."),this.heartbeatLoop()}stop(){if(this.stopped=!0,this.pollTimer)clearTimeout(this.pollTimer),this.pollTimer=null;this.cleanupWs(),this.log.log("[WorkerTunnel] Tunnel stopped")}isConnected(){if(this.ws!==null&&this.ws.readyState===WebSocket.OPEN)return!0;return!this.stopped&&!!this.opts.apiKey&&this.lastHeartbeatError===null&&this.pollTimer!==null}getCloudUrl(){return this.opts.cloudUrl.replace(/\/$/,"")}getWsBaseUrl(){let e=(this.opts.wsUrlOverride||process.env.SHOGO_TUNNEL_WS_URL||"").trim();if(e)return e.replace(/\/$/,"");if(this.serverPublishedWsUrl)return this.serverPublishedWsUrl.replace(/\/$/,"");return this.getCloudUrl().replace(/^http/,"ws")}buildWsUrl(){return`${this.getWsBaseUrl()}/api/instances/ws`}supportsWebSocketConstructorHeaders(e=globalThis){if(typeof e.Bun<"u"||typeof e.process?.versions?.bun==="string")return!0;let t=e.process?.versions?.node;if(t){if(parseInt(t.split(".")[0]??"0",10)>=21)return!0}return!1}createTunnelWebSocket(e,t,r=globalThis){if(!this.supportsWebSocketConstructorHeaders(r))throw new ar;return new WebSocket(e,t)}getReconnectDelayMs(){let e=Math.min(nr*Math.pow(2,this.wsReconnectAttempt),ir),t=e*0.2*Math.random();return e+t}async collectMetadata(){try{await this.opts.resolver.refresh?.()}catch{}let e={hostname:rt(),os:ot(),arch:nt(),uptime:process.uptime(),protocolVersion:sr,tunnelStatus:this.ws?.readyState===WebSocket.OPEN?"connected":"polling",kind:this.opts.kind??"cli-worker"};try{let t=this.opts.resolver.getActiveProjects();e.activeProjects=t.length,e.projects=t.map((r)=>{let o=this.opts.resolver.status(r);return{projectId:r,status:o?.status??"unknown",agentPort:o?.agentPort}})}catch{e.activeProjects=0}try{let t=await this.opts.resolver.machineSnapshot?.();if(t)e.machine=t}catch{}return e}async sendHeartbeat(){let e=this.getCloudUrl(),t=await this.collectMetadata(),r=rt(),o=this.opts.name??r,n=await fetch(`${e}/api/instances/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json","User-Agent":Y(),"x-api-key":this.opts.apiKey,"x-shogo-kind":this.opts.kind??"cli-worker"},body:JSON.stringify({hostname:r,name:o,os:ot(),arch:nt(),kind:this.opts.kind??"cli-worker",metadata:t})});if(!n.ok)throw Error(`Heartbeat failed: HTTP ${n.status}`);let i=await n.json();if(typeof i.instanceId==="string"&&i.instanceId.length>0)this.lastInstanceId=i.instanceId;if(typeof i.wsUrl==="string"&&i.wsUrl.length>0){if(i.wsUrl!==this.serverPublishedWsUrl)this.log.log(`[WorkerTunnel] Cloud advertised tunnel WS URL: ${i.wsUrl}`);this.serverPublishedWsUrl=i.wsUrl}return i}scheduleNextPoll(e){if(this.stopped)return;if(this.pollTimer)clearTimeout(this.pollTimer);let t=(e??this.currentPollInterval)*1000;this.pollTimer=setTimeout(()=>void this.heartbeatLoop(),t)}async heartbeatLoop(){if(this.stopped)return;if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.scheduleNextPoll(this.currentPollInterval);return}try{let e=await this.sendHeartbeat(),t=e.nextPollIn||ce;if(this.consecutiveAuthFailures>=it){if(this.consecutiveAuthSuccesses++,this.consecutiveAuthSuccesses<nn){this.currentPollInterval=Ee,this.scheduleNextPoll();return}}if(this.currentPollInterval=t,this.lastHeartbeatError)this.log.log("[WorkerTunnel] Heartbeat recovered"),this.lastHeartbeatError=null;if(this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0,e.wsRequested&&!this.ws){this.log.log("[WorkerTunnel] Cloud requested WebSocket — connecting..."),this.connectWs();return}}catch(e){let t=e?.message??String(e);if(/HTTP 40[13]\b/.test(t))this.consecutiveAuthFailures++,this.consecutiveAuthSuccesses=0;else this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0;if(t!==this.lastHeartbeatError)this.log.error(`[WorkerTunnel] Heartbeat error: ${t}`),this.lastHeartbeatError=t;if(this.consecutiveAuthFailures>=it){let o=`tunnel saw ${this.consecutiveAuthFailures} consecutive auth failures from Shogo Cloud`;try{this.opts.onAuthRevoked?.(o)}catch(n){this.log.warn(`[WorkerTunnel] onAuthRevoked threw: ${n?.message??n}`)}if(this.currentPollInterval!==Ee)this.log.warn(`[WorkerTunnel] ${this.consecutiveAuthFailures} consecutive auth failures — `+`backing off to ${Ee}s. Re-authenticate with \`shogo login\`.`);this.currentPollInterval=Ee}else this.currentPollInterval=ce}this.scheduleNextPoll()}async handleRequest(e){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;this.resetWsIdleTimer();let t=new AbortController;this.activeAbortControllers.set(e.requestId,t);try{let r=await this.resolveLocalUrl(e.path,e.projectId);if(!r){let s=this.opts.resolver.describeRejection?await this.opts.resolver.describeRejection(e.path,e.projectId):{code:"NO_LOCAL_RUNTIME",message:`no local runtime available for path: ${e.path}`};this.sendFrame({type:"response",requestId:e.requestId,status:502,headers:{"content-type":"application/json"},body:JSON.stringify({code:s.code,message:s.message,path:e.path})});return}let o={...e.headers??{}};if(e.projectId&&(e.path.startsWith("/agent/")||e.path==="/agent")){let s=await this.opts.resolver.deriveRuntimeToken(e.projectId);if(s)o["x-runtime-token"]=s}let n={method:e.method,headers:o,signal:t.signal};if(e.body&&e.method!=="GET"&&e.method!=="HEAD")n.body=e.body;let i=await fetch(r,n);if(e.stream){let s=i.body?.getReader();if(!s){this.sendFrame({type:"stream-error",requestId:e.requestId,error:"No response body for stream"});return}let a=new TextDecoder;try{while(!0){let{done:l,value:c}=await s.read();if(l)break;if(this.ws?.readyState!==WebSocket.OPEN)break;this.sendFrame({type:"stream-chunk",requestId:e.requestId,data:a.decode(c,{stream:!0})})}if(this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-end",requestId:e.requestId})}catch(l){if(l?.name!=="AbortError"&&this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-error",requestId:e.requestId,error:l?.message??String(l)})}}else{let s=await i.text(),a={};i.headers.forEach((l,c)=>{a[c]=l}),this.sendFrame({type:"response",requestId:e.requestId,status:i.status,headers:a,body:s})}}catch(r){if(r?.name==="AbortError")return;if(this.ws?.readyState!==WebSocket.OPEN)return;let o=e.stream?{type:"stream-error",requestId:e.requestId,error:r?.message??String(r)}:{type:"response",requestId:e.requestId,status:502,body:JSON.stringify({error:r?.message??String(r)})};this.sendFrame(o)}finally{this.activeAbortControllers.delete(e.requestId)}}async resolveLocalUrl(e,t){return this.opts.resolver.resolveLocalUrl(e,t)}sendFrame(e){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{this.ws.send(JSON.stringify(e))}catch(t){this.log.warn(`[WorkerTunnel] Frame send failed: ${t?.message??t}`)}}resetWsIdleTimer(){if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer);this.wsIdleTimer=setTimeout(()=>{if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.log.log("[WorkerTunnel] WebSocket idle timeout — closing, returning to polling");try{this.ws.close(1000,"Idle timeout")}catch{}}},sn)}startWsHeartbeat(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer);this.heartbeatTimer=setInterval(async()=>{if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{let e=await this.collectMetadata();this.sendFrame({type:"heartbeat",metadata:e})}catch{}},an)}connectWs(){if(this.stopped||this.ws)return;let e=this.buildWsUrl(),t=rt(),r=ot(),o=nt(),n=this.opts.name??t;this.log.log(`[WorkerTunnel] Opening WebSocket to ${e} (hostname=${t})`);let i={headers:{Authorization:`Bearer ${this.opts.apiKey}`,"User-Agent":Y(),"x-shogo-hostname":t,"x-shogo-name":n,"x-shogo-os":r,"x-shogo-arch":o,"x-shogo-kind":this.opts.kind??"cli-worker"}},s;try{s=this.createTunnelWebSocket(e,i)}catch(a){this.log.error(`[WorkerTunnel] WebSocket creation failed: ${a?.message??a}`),this.ws=null,this.scheduleNextPoll(5);return}this.ws=s,s.onopen=()=>{this.log.log("[WorkerTunnel] WebSocket connected — session active"),this.wsReconnectAttempt=0,this.startWsHeartbeat(),this.resetWsIdleTimer()},s.onmessage=(a)=>{let l;try{let c=typeof a.data==="string"?a.data:a.data.toString();l=JSON.parse(c)}catch{return}if(l.type==="ping"){this.sendFrame({type:"pong"}),this.resetWsIdleTimer();return}if(l.type==="cancel"){let c=this.activeAbortControllers.get(l.requestId);if(c)c.abort();return}if(l.type==="request"){this.handleRequest(l).catch((c)=>{this.log.error(`[WorkerTunnel] Error handling request: ${c?.message??c}`)});return}},s.onclose=(a)=>{if(this.log.log(`[WorkerTunnel] WebSocket closed: code=${a.code} reason=${a.reason||"none"}`),this.cleanupWs(),this.stopped)return;if(a.code===1000||a.code===4000)this.scheduleNextPoll(this.currentPollInterval);else{this.wsReconnectAttempt++;let l=this.getReconnectDelayMs();this.log.log(`[WorkerTunnel] Reconnecting in ${Math.round(l/1000)}s (attempt ${this.wsReconnectAttempt})`),this.scheduleNextPoll(Math.ceil(l/1000))}},s.onerror=(a)=>{this.log.error("[WorkerTunnel] WebSocket error:",a?.message??"unknown")}}cleanupWs(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer),this.heartbeatTimer=null;if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer),this.wsIdleTimer=null;for(let[,e]of this.activeAbortControllers)try{e.abort()}catch{}this.activeAbortControllers.clear(),this.ws=null}_testing(){let e=this;return{sendHeartbeat:()=>e.sendHeartbeat(),heartbeatLoop:()=>e.heartbeatLoop(),connectWs:()=>e.connectWs(),cleanupWs:()=>e.cleanupWs(),handleRequest:(t)=>e.handleRequest(t),installFakeWs:(t)=>{e.ws=t},getCloudUrl:()=>e.getCloudUrl(),getWsBaseUrl:()=>e.getWsBaseUrl(),buildWsUrl:()=>e.buildWsUrl(),getReconnectDelayMs:()=>e.getReconnectDelayMs(),supportsWebSocketConstructorHeaders:(t)=>e.supportsWebSocketConstructorHeaders(t),createTunnelWebSocket:(t,r,o)=>e.createTunnelWebSocket(t,r,o),DEFAULT_POLL_INTERVAL_S:ce,BACKOFF_BASE_MS:nr,BACKOFF_MAX_MS:ir,TUNNEL_PROTOCOL_VERSION:sr,get currentPollInterval(){return e.currentPollInterval},set currentPollInterval(t){e.currentPollInterval=t},get wsReconnectAttempt(){return e.wsReconnectAttempt},set wsReconnectAttempt(t){e.wsReconnectAttempt=t},get ws(){return e.ws},get stopped(){return e.stopped},get serverPublishedWsUrl(){return e.serverPublishedWsUrl},set serverPublishedWsUrl(t){e.serverPublishedWsUrl=t}}}}async function lr(e){let t=D({name:e.name,workerDir:e.workerDir,apiKey:e.apiKey,cloudUrl:e.cloudUrl,port:e.port?parseInt(e.port,10):void 0,projectsDir:e.projectsDir}),r=Tt(e.proxy);if(!e.foreground)return cn({cfg:t,proxy:r,flags:e});let o=G({flag:e.runtimeBin});if(!o)console.error(f.red(q({flag:e.runtimeBin}))),process.exit(1);if(Ne(process.env,r),e.debug){if(!await xt(It({cloudUrl:t.cloudUrl,apiKey:t.apiKey,workerDir:t.workerDir,proxy:r})))process.exit(1)}let n=!e.noAutoPull,i=!e.noGit;if(console.log(f.bold(`
16
- Shogo Worker — Starting`)),console.log(f.dim(" name ")+t.name),console.log(f.dim(" worker-dir ")+t.workerDir),console.log(f.dim(" cloud ")+t.cloudUrl),console.log(f.dim(" runtime ")+`${o.path} ${f.dim(`(via ${o.source})`)}`),console.log(f.dim(" auto-pull ")+(n?f.green("on")+f.dim(` → ${t.projectsDir}`):f.yellow("off"))),console.log(f.dim(" sync mode ")+(i?f.green("git")+f.dim(" (falls back to file transport if git is missing)"):f.yellow("files-only"))),e.project)console.log(f.dim(" project ")+e.project);if(r)console.log(f.dim(" proxy ")+`${r.url} ${f.dim(`(from ${r.source})`)}`);console.log("");let s=`${t.cloudUrl.replace(/\/+$/,"")}/api/ai/v1`,a={cloudUrl:t.cloudUrl,apiKey:t.apiKey,aiProxyUrl:s,aiProxyToken:t.apiKey,extraEnv:{SHOGO_PERMISSION_SCOPE:"cloud"}},l=new tt({runtimeBin:e.runtimeBin,defaultSpawnConfig:a,autoPull:{enabled:n,projectsDir:t.projectsDir,watch:!0,useGit:i}});if(!l.resolveBinary())console.error(f.red(q({flag:e.runtimeBin}))),process.exit(1);let c=new st({apiKey:t.apiKey,cloudUrl:t.cloudUrl,name:t.name,kind:"cli-worker",resolver:l,onAuthRevoked:(d)=>{console.error(f.red(`✗ Cloud auth revoked: ${d}`)),console.error(f.dim(" Run `shogo login` to re-authenticate; this worker will keep polling at the auth-failure backoff until then."))}}),u=!1,p=async(d)=>{if(u)return;u=!0,console.log(f.dim(`
17
- Received ${d} — shutting down...`));try{c.stop()}catch{}try{await l.stopAll()}catch(h){console.warn(f.yellow(`stopAll: ${h?.message??h}`))}process.exit(0)};process.once("SIGINT",()=>void p("SIGINT")),process.once("SIGTERM",()=>void p("SIGTERM")),process.once("SIGHUP",()=>void p("SIGHUP")),c.start(),console.log(f.green("✓ Worker running. Ctrl-C to stop.")),await new Promise(()=>{})}function cn({cfg:e,proxy:t,flags:r}){let{entry:o,runner:n}=pn(),i=Ne({...process.env,SHOGO_API_KEY:e.apiKey,SHOGO_CLOUD_URL:e.cloudUrl,SHOGO_INSTANCE_NAME:e.name,SHOGO_WORKER_DIR:e.workerDir,SHOGO_LOCAL_MODE:"true",PORT:String(e.port)},t),s=un(r),{pid:a}=kt({entry:o,runner:n,env:{...i,SHOGO_DETACHED_ARGS:s.join(" ")},cwd:e.workerDir,detach:!0,inheritStdio:!1});console.log(f.bold(`
18
- Shogo Worker — Started`)),console.log(f.dim(" pid: ")+a),console.log(f.dim(" name: ")+e.name),console.log(f.dim(" logs: ")+"~/.shogo/logs/worker.log"),console.log(f.dim(" stop: ")+"shogo worker stop")}function un(e){let t=["worker","start","--foreground"];if(e.name)t.push("--name",e.name);if(e.workerDir)t.push("--worker-dir",e.workerDir);if(e.apiKey)t.push("--api-key",e.apiKey);if(e.cloudUrl)t.push("--cloud-url",e.cloudUrl);if(e.port)t.push("--port",e.port);if(e.proxy)t.push("--proxy",e.proxy);if(e.project)t.push("--project",e.project);if(e.runtimeBin)t.push("--runtime-bin",e.runtimeBin);if(e.debug)t.push("--debug");if(e.noAutoPull)t.push("--no-auto-pull");if(e.projectsDir)t.push("--projects-dir",e.projectsDir);if(e.noGit)t.push("--no-git");return t}function pn(){let e=process.execPath,t=/\bbun(?:-[^/\\]*)?$/.test(e)||typeof globalThis.Bun<"u",r=process.argv[1];if(r&&ln(r)){let n=/\.ts$/.test(r);return{entry:r,runner:t?"bun":n?"tsx":"node"}}return{entry:new URL("../../bin/shogo.mjs",import.meta.url).pathname,runner:t?"bun":"node"}}import cr from"picocolors";async function ur(){let{killedPid:e}=Rt("SIGTERM");if(e===null){console.log(cr.dim("No worker running."));return}console.log(cr.green(`✓ Worker stopped (pid=${e}).`))}import j from"picocolors";async function pr(){let e=ye();if(!e){console.log(j.yellow("● stopped")+j.dim(" (no pid file)"));return}if(!ve(e)){console.log(j.red("● dead")+j.dim(` (stale pid ${e})`));return}let t=B();if(console.log(j.green("● running")+j.dim(` (pid ${e})`)),t.name)console.log(j.dim(" name: ")+t.name);if(t.cloudUrl)console.log(j.dim(" cloud: ")+t.cloudUrl);if(t.workerDir)console.log(j.dim(" dir: ")+t.workerDir)}W();import{existsSync as dn,statSync as mn}from"node:fs";import dr from"picocolors";async function mr(e){let t=e.err?ge:me;if(!dn(t)){console.log(dr.dim("No logs yet."));return}let r=e.follow?["-F","-n","200",t]:["-n","200",t];if(T("tail",r,{stdio:"inherit"}).on("exit",(n)=>process.exit(n??0)),!e.follow){if(mn(t).size===0)console.log(dr.dim("(log file is empty)"))}}import I from"picocolors";W();import{hostname as gn,platform as hn,arch as fn}from"node:os";import K from"picocolors";class _ extends Error{kind;constructor(e,t){super(e);this.kind=t;this.name="CloudLoginError"}}async function gr(e){let t=e.cloudUrl.replace(/\/$/,""),r=e.client??"cli",o=e.deviceName??gn(),n=e.devicePlatform??`${hn()}-${fn()}`,i=e.appVersion??vn(),s=e.log??((g)=>console.log(g)),a=e.fetchImpl??fetch,l=await a(`${t}/api/cli/login/start`,{method:"POST",headers:{"content-type":"application/json","user-agent":Y()},body:JSON.stringify({deviceId:e.deviceId,deviceName:o,devicePlatform:n,deviceAppVersion:i,workspaceId:e.workspaceId,client:r}),signal:AbortSignal.timeout(1e4)}).catch((g)=>{throw new _(`Cannot reach Shogo Cloud at ${t}: ${g?.message??g}`,"transport")});if(!l.ok){let g=await l.text().catch(()=>"");throw new _(`Cloud rejected /api/cli/login/start (HTTP ${l.status}): ${g||"no body"}`,"transport")}let c=await l.json().catch(()=>({}));if(!c?.ok||!c.state||!c.authUrl)throw new _(`Cloud returned a malformed start response: ${c?.error??JSON.stringify(c)}`,"transport");let u=wn(e.pollIntervalMs??c.pollIntervalMs),p=e.timeoutMs??c.expiresInMs,d=Date.now()+p;if(s(""),s(K.bold("Sign in to Shogo Cloud")),s(K.dim(" cloud: ")+t),s(K.dim(" device: ")+`${o} (${n})`),s(K.dim(" user code: ")+K.cyan(c.userCode)),s(""),s(" Open this URL in your browser to approve:"),s(" "+K.cyan(c.authUrl)),s(""),e.openBrowser!==!1)if(typeof e.openBrowser==="function")try{await e.openBrowser(c.authUrl)}catch{}else Sn(c.authUrl).catch(()=>{});s(K.dim("Waiting for approval..."));let h=e.installSignalHandlers??!0,m=()=>{w.abort()},w=new AbortController;if(e.abortSignal)if(e.abortSignal.aborted)w.abort();else e.abortSignal.addEventListener("abort",()=>w.abort(),{once:!0});if(h)process.once("SIGINT",m),process.once("SIGTERM",m);try{while(!0){if(w.signal.aborted)throw new _("Aborted by caller.","cancelled");if(Date.now()>=d)throw new _(`Timed out after ${Math.round(p/1000)}s waiting for approval.`,"timeout");let g=await a(`${t}/api/cli/login/poll?state=${encodeURIComponent(c.state)}`,{method:"GET",headers:{accept:"application/json","user-agent":Y()},signal:AbortSignal.timeout(1e4)}).catch((b)=>(s(K.dim(` (poll error: ${b?.message??b} — retrying)`)),null));if(g&&g.ok){let b=await g.json().catch(()=>({}));if(b?.status==="approved"&&b.key)return{key:b.key,email:b.email??null,workspace:b.workspace??null,deviceId:b.deviceId??e.deviceId,workspaces:b.workspaces??null};if(b?.status==="denied")throw new _("Sign-in was denied in the browser.","denied");if(b?.status==="expired")throw new _("Sign-in request expired before it was approved.","expired")}await yn(u,w.signal)}}finally{if(h)process.removeListener("SIGINT",m),process.removeListener("SIGTERM",m)}}function wn(e){if(!Number.isFinite(e)||e<=0)return 2000;return Math.min(Math.max(e,1000),1e4)}function yn(e,t){return new Promise((r,o)=>{if(t?.aborted)return o(new _("Aborted by caller.","cancelled"));let n=setTimeout(()=>{t?.removeEventListener("abort",i),r()},e),i=()=>{clearTimeout(n),o(new _("Aborted by caller.","cancelled"))};t?.addEventListener("abort",i,{once:!0})})}function vn(){return Y()}function Sn(e){return new Promise((t)=>{let r=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",o=process.platform==="win32"?["/c","start",'""',e]:[e];try{let n=T(r,o,{stdio:"ignore",detached:!0});n.on("error",()=>t()),n.unref(),t()}catch{t()}})}import{readFileSync as bn,writeFileSync as kn,existsSync as Rn,chmodSync as Pn}from"node:fs";import{randomUUID as Tn}from"node:crypto";function hr(){if(Rn(ee)){let t=bn(ee,"utf-8").trim();if(t)return t}H();let e=Tn();return kn(ee,e,{mode:384}),Pn(ee,384),e}var $n="https://studio.shogo.ai";async function fr(e){let t=B(),r=(e.cloudUrl||t.cloudUrl||$n).replace(/\/$/,""),o=e.apiKey||process.env.SHOGO_API_KEY;if(o){await En({key:o,cloudUrl:r,cfg:t,name:e.name});return}let n=hr(),i;try{i=await gr({cloudUrl:r,deviceId:n,deviceName:e.name,workspaceId:e.workspace,openBrowser:!e.noBrowser})}catch(s){if(s instanceof _){if(console.error(I.red(`✗ ${s.message}`)),s.kind==="transport")console.error(I.dim(" If your network blocks browsers, run with --api-key <key> instead."));process.exit(1)}throw s}if(t.apiKey=i.key,t.cloudUrl=r,e.name)t.name=e.name;if(ne(t),console.log(""),console.log(I.green("✓ Signed in to Shogo Cloud")),i.workspace)console.log(I.dim(" workspace: ")+i.workspace);if(i.email)console.log(I.dim(" email: ")+i.email);console.log(I.dim(" saved to: ")+"~/.shogo/config.json"),console.log(""),console.log(I.dim(" next: ")+"shogo worker start")}async function En({key:e,cloudUrl:t,cfg:r,name:o}){if(!/^shogo_sk_/.test(e))console.error(I.red('✗ API key should start with "shogo_sk_". Copy it verbatim from the API Keys page.')),process.exit(1);let n;try{let i=await fetch(`${t}/api/api-keys/validate`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({key:e}),signal:AbortSignal.timeout(1e4)});if(n=await i.json().catch(()=>({})),!i.ok||!n?.valid)console.error(I.red(`✗ ${n?.error||`Cloud rejected the key (HTTP ${i.status}).`}`)),process.exit(1)}catch(i){console.error(I.red(`✗ Cannot reach Shogo Cloud at ${t}: ${i?.message??i}`)),process.exit(1)}if(r.apiKey=e,r.cloudUrl=t,o)r.name=o;if(ne(r),console.log(I.green("✓ API key saved to ~/.shogo/config.json")),n.workspace?.name)console.log(I.dim(" workspace: ")+n.workspace.name);if(n.user?.email)console.log(I.dim(" email: ")+n.user.email);console.log(I.dim(" next: ")+"shogo worker start")}import ue from"picocolors";async function wr(){let e=B();if(Object.keys(e).length===0){console.log(ue.dim("(no config — run `shogo login` or `shogo config set`)"));return}let t={...e,apiKey:e.apiKey?`***${e.apiKey.slice(-4)}`:void 0};console.log(ue.dim(`file: ${L}`)),console.log(JSON.stringify(t,null,2))}async function yr(e,t){let r=["apiKey","cloudUrl","name","workerDir","port"];if(!r.includes(e))console.error(ue.red(`Unknown key: ${e}`)),console.error(ue.dim(`Allowed: ${r.join(", ")}`)),process.exit(1);let o=B();o[e]=e==="port"?parseInt(t,10):t,ne(o),console.log(ue.green(`✓ ${e} set`))}import S from"picocolors";W();import{createHash as In}from"node:crypto";import{createWriteStream as xn,existsSync as pe,mkdirSync as at,readFileSync as br,renameSync as vr,rmSync as lt,writeFileSync as An}from"node:fs";import{tmpdir as Cn}from"node:os";import{dirname as _n,join as Ie}from"node:path";import{Readable as On}from"node:stream";import{pipeline as Mn}from"node:stream/promises";var Dn="https://github.com/shogo-labs/shogo-ai/releases/download";function kr(){let e=process.platform,t=process.arch,r;if(e==="darwin")r="darwin";else if(e==="linux")r="linux";else if(e==="win32")r="windows";else throw Error(`Unsupported platform: ${e}`);let o;if(t==="arm64")o="arm64";else if(t==="x64")o="x64";else throw Error(`Unsupported arch: ${t} (need arm64 or x64)`);return`${r}-${o}`}function xe(){if(!pe(oe))return null;try{let e=br(oe,"utf-8");return JSON.parse(e)}catch{return null}}async function Wn(e,t){let r=t.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/releases\/download/);if(!r)throw Error(`Cannot auto-resolve latest version from non-GitHub baseUrl '${t}'. Pass --version explicitly.`);let o=r[1],n=r[2];if(e==="stable"){let u=`https://api.github.com/repos/${o}/${n}/releases/latest`,p=await fetch(u,{headers:{Accept:"application/vnd.github+json"}});if(!p.ok)throw Error(`GitHub API ${p.status} for ${u}`);let d=await p.json();if(!d.tag_name)throw Error("GitHub API did not return tag_name");return Sr(d.tag_name)}let i=`https://api.github.com/repos/${o}/${n}/releases?per_page=30`,s=await fetch(i,{headers:{Accept:"application/vnd.github+json"}});if(!s.ok)throw Error(`GitHub API ${s.status} for ${i}`);let a=await s.json(),l=e==="beta"?"-beta":"-nightly",c=a.find((u)=>u.prerelease&&/^v\d+\.\d+\.\d+-/.test(u.tag_name)&&u.tag_name.includes(l));if(!c)throw Error(`No ${e} runtime release found`);return Sr(c.tag_name)}function Sr(e){if(!/^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(e))throw Error(`Unexpected app tag '${e}' (expected vX.Y.Z[-prerelease])`);return e.slice(1)}function Un(e,t,r){let o=`shogo-agent-runtime-${t}.tar.gz`,n=`v${e}`,i=`${r.replace(/\/$/,"")}/${n}/${o}`;return{tarball:i,sha256:`${i}.sha256`,assetName:o}}async function Nn(e,t){let r=await fetch(e,{redirect:"follow"});if(!r.ok)throw Error(`Download failed: HTTP ${r.status} for ${e}`);if(!r.body)throw Error(`Download failed: empty body for ${e}`);await Mn(On.fromWeb(r.body),xn(t))}async function jn(e){let t=await fetch(e,{redirect:"follow"});if(!t.ok)throw Error(`SHA256 sidecar fetch failed: HTTP ${t.status} for ${e}`);let o=(await t.text()).trim().split(/\s+/)[0];if(!/^[0-9a-f]{64}$/i.test(o))throw Error(`SHA256 sidecar at ${e} did not contain a 64-char hex digest`);return o.toLowerCase()}function Fn(e){let t=br(e);return In("sha256").update(t).digest("hex")}async function Ln(e,t){await new Promise((r,o)=>{let n=T("tar",["-xzf",e,"-C",t],{stdio:["ignore","pipe","pipe"]}),i="";n.stderr.on("data",(s)=>{i+=s.toString()}),n.on("error",o),n.on("exit",(s)=>{if(s===0)r();else o(Error(`tar exited ${s}: ${i.trim()}`))})})}function Hn(e,t){mt();let r=_n(t);if(!pe(r))at(r,{recursive:!0,mode:448});let o=`${t}.next`;if(pe(o))lt(o,{force:!0});vr(e,o);try{if(process.platform!=="win32"){let{chmodSync:n}=Oe("node:fs");n(o,493)}}catch{}if(pe(t))lt(t,{force:!0});vr(o,t)}async function Rr(e={}){let t=e.logger??console,r=e.channel??"stable",o=e.baseUrl??process.env.SHOGO_RUNTIME_RELEASES_URL??Dn,n=e.target??kr(),i=e.version;if(!i)t.log(`[runtime install] Resolving latest ${r} version...`),i=await Wn(r,o),t.log(`[runtime install] Latest ${r} = ${i}`);let s=xe();if(s&&s.version===i&&s.target===n&&!e.force)return t.log(`[runtime install] ${i} (${n}) already installed at ${M} — pass --force to reinstall`),{version:i,target:n,binPath:M,source:s.source,sha256:s.sha256,channel:r};let a=Un(i,n,o);t.log(`[runtime install] Downloading ${a.tarball}`);let l=Ie(Cn(),`shogo-runtime-install-${process.pid}-${Date.now()}`);at(l,{recursive:!0});try{let c=Ie(l,a.assetName);await Nn(a.tarball,c),t.log("[runtime install] Verifying SHA-256...");let u=await jn(a.sha256),p=Fn(c);if(p!==u)throw Error(`SHA-256 mismatch for ${a.assetName}
15
+ `)}`:" (nothing on stderr)";this.log.warn(`[WorkerRuntimeManager] runtime ${e.projectId} exited WITHOUT a stop request (code=${t}, signal=${r}, uptime ${Math.round((Date.now()-e.startedAt)/1000)}s, last activity ${this.activityDecision(e.projectId).countsText}) — last stderr:${u}`)}if(e.proc=null,e.graceTimer)clearTimeout(e.graceTimer),e.graceTimer=null;if(e.limitWatchdog)e.limitWatchdog.stop(),e.limitWatchdog=null;if(this.killViteWatch(e),e.status==="stopping"||this.stopped){e.status="stopped",e.pid=null,this.releasePort(e.agentPort),e.agentPort=0,e.apiServerPort=0;return}if(o){e.status="stopped",e.pid=null,this.releasePort(e.agentPort),e.agentPort=0,e.apiServerPort=0,this.runtimes.delete(e.projectId),this.notifyGone(e.projectId,{reason:"exited",code:t,signal:r});return}this.killProcessGroup(e,"SIGKILL"),this.killViteWatch(e),this.reapChildLedgerFor(e.projectId,e.spawnConfig.projectDir),e.pid=null;let i=Date.now(),s=i-e.lastFailureAt<=er;e.consecutiveFailures=s?e.consecutiveFailures+1:1,e.lastFailureAt=i,e.restarts+=1,e.lastError=`exited code=${t} signal=${r}`,e.restartTimestamps.push(i),e.restartTimestamps=e.restartTimestamps.filter((u)=>i-u<=tr);let a=e.consecutiveFailures>=Qt,l=e.restartTimestamps.length>=en;if(a||l){e.status="failed",e.breakerTrips+=1;let u=this.breakerCooldownMs(e.breakerTrips),p=a?`${e.consecutiveFailures} consecutive non-clean exits within ${Math.round(er/1000)}s`:`${e.restartTimestamps.length} non-clean exits within the last ${Math.round(tr/60000)}m (slow-bleed loop — gaps between crashes kept clearing the tight-loop window)`;if(e.lastError=`Circuit breaker tripped (trip #${e.breakerTrips}): ${p} (last: code=${t} signal=${r}). Most recent on macOS is jetsam OOM (signal=SIGKILL with code=null); the previous incarnation's vite/tsserver/preview-manager children were reaped to prevent further RSS growth. Will auto-retry in ~${Math.round(u/1000)}s; call resetFailure(${e.projectId}) to retry immediately, or stop(${e.projectId}) to give up.`,this.releasePort(e.agentPort),e.agentPort=0,e.apiServerPort=0,e.restartTimer)clearTimeout(e.restartTimer),e.restartTimer=null;if(e.idleTimer)clearTimeout(e.idleTimer),e.idleTimer=null;this.log.error(`[WorkerRuntimeManager] ${e.lastError}`),this.notifyGone(e.projectId,{reason:"failed",code:t,signal:r});return}let c=this.restartBackoffMs(e.restarts);if(e.status="restarting",this.log.warn(`[WorkerRuntimeManager] restarting ${e.projectId} in ${Math.round(c/1000)}s (restart #${e.restarts}, consecutive failures ${e.consecutiveFailures}/${Qt})`),e.restartTimer)clearTimeout(e.restartTimer);e.restartTimer=setTimeout(()=>{if(e.restartTimer=null,e.status==="failed"||this.stopped)return;e.startPromise=this.doStart(e).then((u)=>(e.startPromise=null,u)).catch((u)=>(e.startPromise=null,this.log.error(`[WorkerRuntimeManager] restart of ${e.projectId} failed: ${u?.message??u}`),e))},c);try{e.restartTimer.unref?.()}catch{}}restartBackoffMs(e){let t=Math.min(Zo*Math.pow(2,Math.max(0,e-1)),Qo),r=t*0.2*Math.random();return t+r}armIdleTimer(e){if(e.idleTimer)clearTimeout(e.idleTimer),e.idleTimer=null;let t=this.opts.idleMs??Yo;if(!Number.isFinite(t)||t<=0)return;e.idleTimer=setTimeout(async()=>{e.idleTimer=null;let r=Date.now()-e.lastUsedAt;if(r<t){this.armIdleTimer(e);return}let o=await this.freshActivityDecision(e.projectId);if(this.runtimes.get(e.projectId)!==e||e.status!=="running"||e.idleTimer)return;if(o.dead){this.log.log(`[WorkerRuntimeManager] reaping dead ${e.projectId} (${ue(o)})`),this.stop(e.projectId,"SIGTERM","dead").catch((n)=>{this.log.warn(`[WorkerRuntimeManager] reap stop failed: ${n?.message??n}`)});return}if(o.busy){this.log.log(`[WorkerRuntimeManager] not idle-evicting ${e.projectId}: ${o.reason} (${ue(o)})`),this.touch(e.projectId);return}this.log.log(`[WorkerRuntimeManager] idle-evicting ${e.projectId} after ${Math.round(r/1000)}s`),this.stop(e.projectId,"SIGTERM","idle-evict").catch((n)=>{this.log.warn(`[WorkerRuntimeManager] idle stop failed: ${n?.message??n}`)})},t)}async hasActiveWork(e){let t=this.runtimes.get(e);if(!t)return!1;let r=await this.activeWork(t);return!!r&&r.turns+r.tasks+r.streams+r.children>0}async activeWork(e){if(e.status!=="running"||!e.agentPort)return null;try{let t=await fetch(`http://127.0.0.1:${e.agentPort}/pool/activity`,{headers:e.runtimeToken?{"x-runtime-token":e.runtimeToken}:{},signal:AbortSignal.timeout(zo)});if(!t.ok)return null;let r=await t.json();if(!(typeof r.activeTurns==="number"||typeof r.activeTasks==="number"||typeof r.activeStreams==="number"||typeof r.activeChildren==="number"))return null;return{turns:r.activeTurns??0,tasks:r.activeTasks??0,streams:r.activeStreams??0,children:r.activeChildren??0}}catch{return null}}async sampleActivity(e){let t=Date.now(),r=await this.activeWork(e),o=e.activity,n=r?{counts:r,at:t,ok:!0}:{counts:o?.ok?o.counts:Ye,at:t,ok:!1,error:e.status!=="running"?`status=${e.status}`:e.agentPort?"no answer from /pool/activity":"no agent port",failingSince:o&&!o.ok?o.failingSince??t:t};return e.activity=n,n}async sampleAllActivity(){let e=[...this.runtimes.values()].filter((t)=>t.status==="running"&&!!t.agentPort);if(e.length===0)return;await Promise.all(e.map((t)=>this.sampleActivity(t).catch(()=>null)))}ensureActivitySampler(){if(this.activityTimer)return;let e=setInterval(()=>{this.sampleAllActivity()},qt);e.unref?.(),this.activityTimer=e}activityDecision(e,t){let r=Date.now(),o=this.runtimes.get(e);if(!o)return Xe({now:r,startedAt:r,status:"stopped",processExited:!0});return Xe({now:r,startedAt:o.startedAt,status:o.status,sample:o.activity??null,terminalSessions:t?.terminalSessions,inflightCalls:t?.inflightCalls,processExited:this.processExited(o)})}processExited(e){if(e.status==="stopped"||e.status==="failed")return!0;if(e.proc)return!1;return e.status==="running"}async freshActivityDecision(e,t){let r=this.runtimes.get(e);if(r&&r.status==="running"&&r.agentPort){let o=r.activity;if(!o||Date.now()-o.at>Ve)await this.sampleActivity(r).catch(()=>null)}return this.activityDecision(e,t)}async refreshActivity(e){let t=this.runtimes.get(e);if(!t||t.status!=="running"||!t.agentPort)return;let r=t.activity;if(r&&Date.now()-r.at<=Ve)return;await this.sampleActivity(t).catch(()=>null)}async allocatePort(){let e=et-Qe,t=Math.min(e,50);for(let r=0;r<t;r++){let o=Qe+Math.floor(Math.random()*e);if(o+Te-1>et)continue;let n=!0;for(let a=0;a<Te;a++)if(this.usedPorts.has(o+a)){n=!1;break}if(!n)continue;let i=await this.isPortListening(o),s=await this.isPortListening(o+zt);if(i||s)continue;for(let a=0;a<Te;a++)this.usedPorts.add(o+a);return o}throw Error(`Cannot allocate port in range ${Qe}-${et} after ${t} attempts`)}releasePort(e){if(!e)return;for(let t=0;t<Te;t++)this.usedPorts.delete(e+t)}async isPortListening(e){let t=new AbortController,r=setTimeout(()=>t.abort(),250);try{return await fetch(`http://127.0.0.1:${e}/`,{method:"HEAD",signal:t.signal}),clearTimeout(r),!0}catch{return clearTimeout(r),!1}}tcpProbe(e){return new Promise((t)=>{let r=!1,o=(i)=>{if(r)return;r=!0;try{n.destroy()}catch{}t(i)},n=Ko({host:"127.0.0.1",port:e});n.setTimeout(ln),n.once("connect",()=>o(!0)),n.once("error",()=>o(!1)),n.once("timeout",()=>o(!1))})}async waitForHealth(e,t){let{agentPort:r,proc:o}=e;if(!o)throw Error(`waitForHealth: slot ${e.projectId} has no spawned process`);let n=Date.now(),i=n+t,s=null,a=!1,l=0,c=0,u=0,p=0,m=n;while(Date.now()<i){if(p++,o.exitCode!==null||o.signalCode!=null||o.killed)throw Error(`agent-runtime exited (code=${o.exitCode}, signal=${o.signalCode}) before becoming healthy on port ${r}`);c++;let f=new AbortController,y=setTimeout(()=>f.abort(),1500);try{let d=await fetch(`http://127.0.0.1:${r}/health`,{method:"GET",signal:f.signal});if(clearTimeout(y),d.ok){this.log.log(`[WorkerRuntimeManager] /health ready for ${e.projectId} on port ${r} (HTTP ${d.status} after ${Date.now()-n}ms, ${p} iter, ${c} http)`);return}s=`HTTP /health returned ${d.status}`}catch(d){clearTimeout(y);let v=d?.name??"Error",Q=d?.code??d?.cause?.code;s=`HTTP /health failed: ${v}${Q?`(${Q})`:""}: ${d?.message??d}`}if(u++,a=await this.tcpProbe(r),a){l=Date.now();let d=Date.now()-e.lastStdoutAt;if(d>=rr)throw Error(`agent-runtime wedged on port ${r}: TCP listening but stdout silent for ${d}ms (> ${rr}ms window); ${c} /health attempts, last error: ${s??"n/a"}`)}let g=Date.now();if(g-m>=an){let d=g-n,v=g-e.lastStdoutAt,Q=l>0?g-l:null;this.log.log(`[WorkerRuntimeManager] still waiting for /health on ${e.projectId} port ${r} (${(d/1000).toFixed(1)}s elapsed, tcpListening=${a}${Q!=null?`(${Q}ms ago)`:""}, lastStdout=${v}ms ago, ${c} http, ${u} tcp, lastError=${s??"n/a"})`),m=g}await new Promise((d)=>setTimeout(d,nn))}if(o.exitCode!==null||o.signalCode!=null||o.killed)throw Error(`agent-runtime exited (code=${o.exitCode}, signal=${o.signalCode}) before becoming healthy on port ${r}`);throw Error(`Timeout waiting for agent-runtime /health on port ${r} after ${p} iter (httpAttempts=${c}, tcpAttempts=${u}, tcpListening=${a}, lastError=${s??"n/a"})`)}async waitForExit(e,t){if(e.exitCode!==null||e.signalCode!=null||e.killed)return;await new Promise((r)=>{let o=setTimeout(()=>{try{e.kill("SIGKILL")}catch{}r()},t);e.once("exit",()=>{clearTimeout(o),r()})})}snapshot(e){return{projectId:e.projectId,status:e.status,agentPort:e.agentPort||void 0,apiServerPort:e.apiServerPort||void 0,pid:e.proc?.pid,startedAt:e.startedAt||void 0,lastUsedAt:e.lastUsedAt,restarts:e.restarts,lastError:e.lastError}}}import{hostname as nt,platform as it,arch as st}from"node:os";function X(){if(typeof __SHOGO_WORKER_VERSION__==="string"&&__SHOGO_WORKER_VERSION__.length>0)return`shogo-cli/${__SHOGO_WORKER_VERSION__}`;return"shogo-cli/unknown"}class dr extends Error{code="TUNNEL_WS_HEADERS_UNSUPPORTED";constructor(){super("Tunnel WebSocket auth requires a runtime with WebSocket header support (Bun or Node >= 21). Current runtime does not support WebSocket constructor headers.");this.name="TunnelWebSocketHeaderSupportError"}}var pe=60,xe=300,at=3,dn=at,mn=1800000,gn=25000,cr=1000,ur=60000,pr=3;class lt{opts;log;pollTimer=null;ws=null;wsIdleTimer=null;heartbeatTimer=null;stopped=!1;currentPollInterval=pe;wsReconnectAttempt=0;lastHeartbeatError=null;consecutiveAuthFailures=0;consecutiveAuthSuccesses=0;serverPublishedWsUrl=null;lastInstanceId=null;activeAbortControllers=new Map;constructor(e){this.opts=e,this.log=e.logger??console}get instanceId(){return this.lastInstanceId}start(){if(!this.opts.apiKey){this.log.log("[WorkerTunnel] No API key set, skipping tunnel");return}this.stopped=!1,this.wsReconnectAttempt=0,this.currentPollInterval=pe,this.log.log("[WorkerTunnel] Starting heartbeat polling to Shogo Cloud..."),this.heartbeatLoop()}stop(){if(this.stopped=!0,this.pollTimer)clearTimeout(this.pollTimer),this.pollTimer=null;this.cleanupWs(),this.log.log("[WorkerTunnel] Tunnel stopped")}isConnected(){if(this.ws!==null&&this.ws.readyState===WebSocket.OPEN)return!0;return!this.stopped&&!!this.opts.apiKey&&this.lastHeartbeatError===null&&this.pollTimer!==null}getCloudUrl(){return this.opts.cloudUrl.replace(/\/$/,"")}getWsBaseUrl(){let e=(this.opts.wsUrlOverride||process.env.SHOGO_TUNNEL_WS_URL||"").trim();if(e)return e.replace(/\/$/,"");if(this.serverPublishedWsUrl)return this.serverPublishedWsUrl.replace(/\/$/,"");return this.getCloudUrl().replace(/^http/,"ws")}buildWsUrl(){return`${this.getWsBaseUrl()}/api/instances/ws`}supportsWebSocketConstructorHeaders(e=globalThis){if(typeof e.Bun<"u"||typeof e.process?.versions?.bun==="string")return!0;let t=e.process?.versions?.node;if(t){if(parseInt(t.split(".")[0]??"0",10)>=21)return!0}return!1}createTunnelWebSocket(e,t,r=globalThis){if(!this.supportsWebSocketConstructorHeaders(r))throw new dr;return new WebSocket(e,t)}getReconnectDelayMs(){let e=Math.min(cr*Math.pow(2,this.wsReconnectAttempt),ur),t=e*0.2*Math.random();return e+t}async collectMetadata(){try{await this.opts.resolver.refresh?.()}catch{}let e={hostname:nt(),os:it(),arch:st(),uptime:process.uptime(),protocolVersion:pr,tunnelStatus:this.ws?.readyState===WebSocket.OPEN?"connected":"polling",kind:this.opts.kind??"cli-worker"};try{let t=this.opts.resolver.getActiveProjects();e.activeProjects=t.length,e.projects=t.map((r)=>{let o=this.opts.resolver.status(r);return{projectId:r,status:o?.status??"unknown",agentPort:o?.agentPort}})}catch{e.activeProjects=0}try{let t=await this.opts.resolver.machineSnapshot?.();if(t)e.machine=t}catch{}return e}async sendHeartbeat(){let e=this.getCloudUrl(),t=await this.collectMetadata(),r=nt(),o=this.opts.name??r,n=await fetch(`${e}/api/instances/heartbeat`,{method:"POST",headers:{"Content-Type":"application/json","User-Agent":X(),"x-api-key":this.opts.apiKey,"x-shogo-kind":this.opts.kind??"cli-worker"},body:JSON.stringify({hostname:r,name:o,os:it(),arch:st(),kind:this.opts.kind??"cli-worker",metadata:t})});if(!n.ok)throw Error(`Heartbeat failed: HTTP ${n.status}`);let i=await n.json();if(typeof i.instanceId==="string"&&i.instanceId.length>0)this.lastInstanceId=i.instanceId;if(typeof i.wsUrl==="string"&&i.wsUrl.length>0){if(i.wsUrl!==this.serverPublishedWsUrl)this.log.log(`[WorkerTunnel] Cloud advertised tunnel WS URL: ${i.wsUrl}`);this.serverPublishedWsUrl=i.wsUrl}return i}scheduleNextPoll(e){if(this.stopped)return;if(this.pollTimer)clearTimeout(this.pollTimer);let t=(e??this.currentPollInterval)*1000;this.pollTimer=setTimeout(()=>void this.heartbeatLoop(),t)}async heartbeatLoop(){if(this.stopped)return;if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.scheduleNextPoll(this.currentPollInterval);return}try{let e=await this.sendHeartbeat(),t=e.nextPollIn||pe;if(this.consecutiveAuthFailures>=at){if(this.consecutiveAuthSuccesses++,this.consecutiveAuthSuccesses<dn){this.currentPollInterval=xe,this.scheduleNextPoll();return}}if(this.currentPollInterval=t,this.lastHeartbeatError)this.log.log("[WorkerTunnel] Heartbeat recovered"),this.lastHeartbeatError=null;if(this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0,e.wsRequested&&!this.ws){this.log.log("[WorkerTunnel] Cloud requested WebSocket — connecting..."),this.connectWs();return}}catch(e){let t=e?.message??String(e);if(/HTTP 40[13]\b/.test(t))this.consecutiveAuthFailures++,this.consecutiveAuthSuccesses=0;else this.consecutiveAuthFailures=0,this.consecutiveAuthSuccesses=0;if(t!==this.lastHeartbeatError)this.log.error(`[WorkerTunnel] Heartbeat error: ${t}`),this.lastHeartbeatError=t;if(this.consecutiveAuthFailures>=at){let o=`tunnel saw ${this.consecutiveAuthFailures} consecutive auth failures from Shogo Cloud`;try{this.opts.onAuthRevoked?.(o)}catch(n){this.log.warn(`[WorkerTunnel] onAuthRevoked threw: ${n?.message??n}`)}if(this.currentPollInterval!==xe)this.log.warn(`[WorkerTunnel] ${this.consecutiveAuthFailures} consecutive auth failures — `+`backing off to ${xe}s. Re-authenticate with \`shogo login\`.`);this.currentPollInterval=xe}else this.currentPollInterval=pe}this.scheduleNextPoll()}async handleRequest(e){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;this.resetWsIdleTimer();let t=new AbortController;this.activeAbortControllers.set(e.requestId,t);try{let r=await this.resolveLocalUrl(e.path,e.projectId);if(!r){let s=this.opts.resolver.describeRejection?await this.opts.resolver.describeRejection(e.path,e.projectId):{code:"NO_LOCAL_RUNTIME",message:`no local runtime available for path: ${e.path}`};this.sendFrame({type:"response",requestId:e.requestId,status:502,headers:{"content-type":"application/json"},body:JSON.stringify({code:s.code,message:s.message,path:e.path})});return}let o={...e.headers??{}};if(e.projectId&&(e.path.startsWith("/agent/")||e.path==="/agent")){let s=await this.opts.resolver.deriveRuntimeToken(e.projectId);if(s)o["x-runtime-token"]=s}let n={method:e.method,headers:o,signal:t.signal};if(e.body&&e.method!=="GET"&&e.method!=="HEAD")n.body=e.body;let i=await fetch(r,n);if(e.stream){let s=i.body?.getReader();if(!s){this.sendFrame({type:"stream-error",requestId:e.requestId,error:"No response body for stream"});return}let a=new TextDecoder;try{while(!0){let{done:l,value:c}=await s.read();if(l)break;if(this.ws?.readyState!==WebSocket.OPEN)break;this.sendFrame({type:"stream-chunk",requestId:e.requestId,data:a.decode(c,{stream:!0})})}if(this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-end",requestId:e.requestId})}catch(l){if(l?.name!=="AbortError"&&this.ws?.readyState===WebSocket.OPEN)this.sendFrame({type:"stream-error",requestId:e.requestId,error:l?.message??String(l)})}}else{let s=await i.text(),a={};i.headers.forEach((l,c)=>{a[c]=l}),this.sendFrame({type:"response",requestId:e.requestId,status:i.status,headers:a,body:s})}}catch(r){if(r?.name==="AbortError")return;if(this.ws?.readyState!==WebSocket.OPEN)return;let o=e.stream?{type:"stream-error",requestId:e.requestId,error:r?.message??String(r)}:{type:"response",requestId:e.requestId,status:502,body:JSON.stringify({error:r?.message??String(r)})};this.sendFrame(o)}finally{this.activeAbortControllers.delete(e.requestId)}}async resolveLocalUrl(e,t){return this.opts.resolver.resolveLocalUrl(e,t)}sendFrame(e){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{this.ws.send(JSON.stringify(e))}catch(t){this.log.warn(`[WorkerTunnel] Frame send failed: ${t?.message??t}`)}}resetWsIdleTimer(){if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer);this.wsIdleTimer=setTimeout(()=>{if(this.ws&&this.ws.readyState===WebSocket.OPEN){this.log.log("[WorkerTunnel] WebSocket idle timeout — closing, returning to polling");try{this.ws.close(1000,"Idle timeout")}catch{}}},mn)}startWsHeartbeat(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer);this.heartbeatTimer=setInterval(async()=>{if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return;try{let e=await this.collectMetadata();this.sendFrame({type:"heartbeat",metadata:e})}catch{}},gn)}connectWs(){if(this.stopped||this.ws)return;let e=this.buildWsUrl(),t=nt(),r=it(),o=st(),n=this.opts.name??t;this.log.log(`[WorkerTunnel] Opening WebSocket to ${e} (hostname=${t})`);let i={headers:{Authorization:`Bearer ${this.opts.apiKey}`,"User-Agent":X(),"x-shogo-hostname":t,"x-shogo-name":n,"x-shogo-os":r,"x-shogo-arch":o,"x-shogo-kind":this.opts.kind??"cli-worker"}},s;try{s=this.createTunnelWebSocket(e,i)}catch(a){this.log.error(`[WorkerTunnel] WebSocket creation failed: ${a?.message??a}`),this.ws=null,this.scheduleNextPoll(5);return}this.ws=s,s.onopen=()=>{this.log.log("[WorkerTunnel] WebSocket connected — session active"),this.wsReconnectAttempt=0,this.startWsHeartbeat(),this.resetWsIdleTimer()},s.onmessage=(a)=>{let l;try{let c=typeof a.data==="string"?a.data:a.data.toString();l=JSON.parse(c)}catch{return}if(l.type==="ping"){this.sendFrame({type:"pong"}),this.resetWsIdleTimer();return}if(l.type==="cancel"){let c=this.activeAbortControllers.get(l.requestId);if(c)c.abort();return}if(l.type==="request"){this.handleRequest(l).catch((c)=>{this.log.error(`[WorkerTunnel] Error handling request: ${c?.message??c}`)});return}},s.onclose=(a)=>{if(this.log.log(`[WorkerTunnel] WebSocket closed: code=${a.code} reason=${a.reason||"none"}`),this.cleanupWs(),this.stopped)return;if(a.code===1000||a.code===4000)this.scheduleNextPoll(this.currentPollInterval);else{this.wsReconnectAttempt++;let l=this.getReconnectDelayMs();this.log.log(`[WorkerTunnel] Reconnecting in ${Math.round(l/1000)}s (attempt ${this.wsReconnectAttempt})`),this.scheduleNextPoll(Math.ceil(l/1000))}},s.onerror=(a)=>{this.log.error("[WorkerTunnel] WebSocket error:",a?.message??"unknown")}}cleanupWs(){if(this.heartbeatTimer)clearInterval(this.heartbeatTimer),this.heartbeatTimer=null;if(this.wsIdleTimer)clearTimeout(this.wsIdleTimer),this.wsIdleTimer=null;for(let[,e]of this.activeAbortControllers)try{e.abort()}catch{}this.activeAbortControllers.clear(),this.ws=null}_testing(){let e=this;return{sendHeartbeat:()=>e.sendHeartbeat(),heartbeatLoop:()=>e.heartbeatLoop(),connectWs:()=>e.connectWs(),cleanupWs:()=>e.cleanupWs(),handleRequest:(t)=>e.handleRequest(t),installFakeWs:(t)=>{e.ws=t},getCloudUrl:()=>e.getCloudUrl(),getWsBaseUrl:()=>e.getWsBaseUrl(),buildWsUrl:()=>e.buildWsUrl(),getReconnectDelayMs:()=>e.getReconnectDelayMs(),supportsWebSocketConstructorHeaders:(t)=>e.supportsWebSocketConstructorHeaders(t),createTunnelWebSocket:(t,r,o)=>e.createTunnelWebSocket(t,r,o),DEFAULT_POLL_INTERVAL_S:pe,BACKOFF_BASE_MS:cr,BACKOFF_MAX_MS:ur,TUNNEL_PROTOCOL_VERSION:pr,get currentPollInterval(){return e.currentPollInterval},set currentPollInterval(t){e.currentPollInterval=t},get wsReconnectAttempt(){return e.wsReconnectAttempt},set wsReconnectAttempt(t){e.wsReconnectAttempt=t},get ws(){return e.ws},get stopped(){return e.stopped},get serverPublishedWsUrl(){return e.serverPublishedWsUrl},set serverPublishedWsUrl(t){e.serverPublishedWsUrl=t}}}}async function mr(e){let t=D({name:e.name,workerDir:e.workerDir,apiKey:e.apiKey,cloudUrl:e.cloudUrl,port:e.port?parseInt(e.port,10):void 0,projectsDir:e.projectsDir}),r=xt(e.proxy);if(!e.foreground)return fn({cfg:t,proxy:r,flags:e});let o=K({flag:e.runtimeBin});if(!o)console.error(h.red(V({flag:e.runtimeBin}))),process.exit(1);if(Fe(process.env,r),e.debug){if(!await _t(At({cloudUrl:t.cloudUrl,apiKey:t.apiKey,workerDir:t.workerDir,proxy:r})))process.exit(1)}let n=!e.noAutoPull,i=!e.noGit;if(console.log(h.bold(`
16
+ Shogo Worker — Starting`)),console.log(h.dim(" name ")+t.name),console.log(h.dim(" worker-dir ")+t.workerDir),console.log(h.dim(" cloud ")+t.cloudUrl),console.log(h.dim(" runtime ")+`${o.path} ${h.dim(`(via ${o.source})`)}`),console.log(h.dim(" auto-pull ")+(n?h.green("on")+h.dim(` → ${t.projectsDir}`):h.yellow("off"))),console.log(h.dim(" sync mode ")+(i?h.green("git")+h.dim(" (falls back to file transport if git is missing)"):h.yellow("files-only"))),e.project)console.log(h.dim(" project ")+e.project);if(r)console.log(h.dim(" proxy ")+`${r.url} ${h.dim(`(from ${r.source})`)}`);console.log("");let s=`${t.cloudUrl.replace(/\/+$/,"")}/api/ai/v1`,a={cloudUrl:t.cloudUrl,apiKey:t.apiKey,aiProxyUrl:s,aiProxyToken:t.apiKey,extraEnv:{SHOGO_PERMISSION_SCOPE:"cloud"}},l=new ot({runtimeBin:e.runtimeBin,defaultSpawnConfig:a,autoPull:{enabled:n,projectsDir:t.projectsDir,watch:!0,useGit:i}});if(!l.resolveBinary())console.error(h.red(V({flag:e.runtimeBin}))),process.exit(1);let c=new lt({apiKey:t.apiKey,cloudUrl:t.cloudUrl,name:t.name,kind:"cli-worker",resolver:l,onAuthRevoked:(m)=>{console.error(h.red(`✗ Cloud auth revoked: ${m}`)),console.error(h.dim(" Run `shogo login` to re-authenticate; this worker will keep polling at the auth-failure backoff until then."))}}),u=!1,p=async(m)=>{if(u)return;u=!0,console.log(h.dim(`
17
+ Received ${m} — shutting down...`));try{c.stop()}catch{}try{await l.stopAll()}catch(f){console.warn(h.yellow(`stopAll: ${f?.message??f}`))}process.exit(0)};process.once("SIGINT",()=>void p("SIGINT")),process.once("SIGTERM",()=>void p("SIGTERM")),process.once("SIGHUP",()=>void p("SIGHUP")),c.start(),console.log(h.green("✓ Worker running. Ctrl-C to stop.")),await new Promise(()=>{})}function fn({cfg:e,proxy:t,flags:r}){let{entry:o,runner:n}=wn(),i=Fe({...process.env,SHOGO_API_KEY:e.apiKey,SHOGO_CLOUD_URL:e.cloudUrl,SHOGO_INSTANCE_NAME:e.name,SHOGO_WORKER_DIR:e.workerDir,SHOGO_LOCAL_MODE:"true",PORT:String(e.port)},t),s=yn(r),{pid:a}=Pt({entry:o,runner:n,env:{...i,SHOGO_DETACHED_ARGS:s.join(" ")},cwd:e.workerDir,detach:!0,inheritStdio:!1});console.log(h.bold(`
18
+ Shogo Worker — Started`)),console.log(h.dim(" pid: ")+a),console.log(h.dim(" name: ")+e.name),console.log(h.dim(" logs: ")+"~/.shogo/logs/worker.log"),console.log(h.dim(" stop: ")+"shogo worker stop")}function yn(e){let t=["worker","start","--foreground"];if(e.name)t.push("--name",e.name);if(e.workerDir)t.push("--worker-dir",e.workerDir);if(e.apiKey)t.push("--api-key",e.apiKey);if(e.cloudUrl)t.push("--cloud-url",e.cloudUrl);if(e.port)t.push("--port",e.port);if(e.proxy)t.push("--proxy",e.proxy);if(e.project)t.push("--project",e.project);if(e.runtimeBin)t.push("--runtime-bin",e.runtimeBin);if(e.debug)t.push("--debug");if(e.noAutoPull)t.push("--no-auto-pull");if(e.projectsDir)t.push("--projects-dir",e.projectsDir);if(e.noGit)t.push("--no-git");return t}function wn(){let e=process.execPath,t=/\bbun(?:-[^/\\]*)?$/.test(e)||typeof globalThis.Bun<"u",r=process.argv[1];if(r&&hn(r)){let n=/\.ts$/.test(r);return{entry:r,runner:t?"bun":n?"tsx":"node"}}return{entry:new URL("../../bin/shogo.mjs",import.meta.url).pathname,runner:t?"bun":"node"}}import gr from"picocolors";async function hr(){let{killedPid:e}=Tt("SIGTERM");if(e===null){console.log(gr.dim("No worker running."));return}console.log(gr.green(`✓ Worker stopped (pid=${e}).`))}import L from"picocolors";async function fr(){let e=ve();if(!e){console.log(L.yellow("● stopped")+L.dim(" (no pid file)"));return}if(!Se(e)){console.log(L.red("● dead")+L.dim(` (stale pid ${e})`));return}let t=G();if(console.log(L.green("● running")+L.dim(` (pid ${e})`)),t.name)console.log(L.dim(" name: ")+t.name);if(t.cloudUrl)console.log(L.dim(" cloud: ")+t.cloudUrl);if(t.workerDir)console.log(L.dim(" dir: ")+t.workerDir)}W();import{existsSync as vn,statSync as Sn}from"node:fs";import yr from"picocolors";async function wr(e){let t=e.err?fe:he;if(!vn(t)){console.log(yr.dim("No logs yet."));return}let r=e.follow?["-F","-n","200",t]:["-n","200",t];if(T("tail",r,{stdio:"inherit"}).on("exit",(n)=>process.exit(n??0)),!e.follow){if(Sn(t).size===0)console.log(yr.dim("(log file is empty)"))}}import I from"picocolors";W();import{hostname as bn,platform as kn,arch as Rn}from"node:os";import J from"picocolors";class M extends Error{kind;constructor(e,t){super(e);this.kind=t;this.name="CloudLoginError"}}async function vr(e){let t=e.cloudUrl.replace(/\/$/,""),r=e.client??"cli",o=e.deviceName??bn(),n=e.devicePlatform??`${kn()}-${Rn()}`,i=e.appVersion??$n(),s=e.log??((d)=>console.log(d)),a=e.fetchImpl??fetch,l=await a(`${t}/api/cli/login/start`,{method:"POST",headers:{"content-type":"application/json","user-agent":X()},body:JSON.stringify({deviceId:e.deviceId,deviceName:o,devicePlatform:n,deviceAppVersion:i,workspaceId:e.workspaceId,client:r}),signal:AbortSignal.timeout(1e4)}).catch((d)=>{throw new M(`Cannot reach Shogo Cloud at ${t}: ${d?.message??d}`,"transport")});if(!l.ok){let d=await l.text().catch(()=>"");throw new M(`Cloud rejected /api/cli/login/start (HTTP ${l.status}): ${d||"no body"}`,"transport")}let c=await l.json().catch(()=>({}));if(!c?.ok||!c.state||!c.authUrl)throw new M(`Cloud returned a malformed start response: ${c?.error??JSON.stringify(c)}`,"transport");let u=Pn(e.pollIntervalMs??c.pollIntervalMs),p=e.timeoutMs??c.expiresInMs,m=Date.now()+p;if(s(""),s(J.bold("Sign in to Shogo Cloud")),s(J.dim(" cloud: ")+t),s(J.dim(" device: ")+`${o} (${n})`),s(J.dim(" user code: ")+J.cyan(c.userCode)),s(""),s(" Open this URL in your browser to approve:"),s(" "+J.cyan(c.authUrl)),s(""),e.openBrowser!==!1)if(typeof e.openBrowser==="function")try{await e.openBrowser(c.authUrl)}catch{}else xn(c.authUrl).catch(()=>{});s(J.dim("Waiting for approval..."));let f=e.installSignalHandlers??!0,y=()=>{g.abort()},g=new AbortController;if(e.abortSignal)if(e.abortSignal.aborted)g.abort();else e.abortSignal.addEventListener("abort",()=>g.abort(),{once:!0});if(f)process.once("SIGINT",y),process.once("SIGTERM",y);try{while(!0){if(g.signal.aborted)throw new M("Aborted by caller.","cancelled");if(Date.now()>=m)throw new M(`Timed out after ${Math.round(p/1000)}s waiting for approval.`,"timeout");let d=await a(`${t}/api/cli/login/poll?state=${encodeURIComponent(c.state)}`,{method:"GET",headers:{accept:"application/json","user-agent":X()},signal:AbortSignal.timeout(1e4)}).catch((v)=>(s(J.dim(` (poll error: ${v?.message??v} — retrying)`)),null));if(d&&d.ok){let v=await d.json().catch(()=>({}));if(v?.status==="approved"&&v.key)return{key:v.key,email:v.email??null,workspace:v.workspace??null,deviceId:v.deviceId??e.deviceId,workspaces:v.workspaces??null};if(v?.status==="denied")throw new M("Sign-in was denied in the browser.","denied");if(v?.status==="expired")throw new M("Sign-in request expired before it was approved.","expired")}await Tn(u,g.signal)}}finally{if(f)process.removeListener("SIGINT",y),process.removeListener("SIGTERM",y)}}function Pn(e){if(!Number.isFinite(e)||e<=0)return 2000;return Math.min(Math.max(e,1000),1e4)}function Tn(e,t){return new Promise((r,o)=>{if(t?.aborted)return o(new M("Aborted by caller.","cancelled"));let n=setTimeout(()=>{t?.removeEventListener("abort",i),r()},e),i=()=>{clearTimeout(n),o(new M("Aborted by caller.","cancelled"))};t?.addEventListener("abort",i,{once:!0})})}function $n(){return X()}function xn(e){return new Promise((t)=>{let r=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",o=process.platform==="win32"?["/c","start",'""',e]:[e];try{let n=T(r,o,{stdio:"ignore",detached:!0});n.on("error",()=>t()),n.unref(),t()}catch{t()}})}import{readFileSync as In,writeFileSync as En,existsSync as An,chmodSync as _n}from"node:fs";import{randomUUID as Mn}from"node:crypto";function Sr(){if(An(ee)){let t=In(ee,"utf-8").trim();if(t)return t}B();let e=Mn();return En(ee,e,{mode:384}),_n(ee,384),e}var Cn="https://studio.shogo.ai";async function br(e){let t=G(),r=(e.cloudUrl||t.cloudUrl||Cn).replace(/\/$/,""),o=e.apiKey||process.env.SHOGO_API_KEY;if(o){await On({key:o,cloudUrl:r,cfg:t,name:e.name});return}let n=Sr(),i;try{i=await vr({cloudUrl:r,deviceId:n,deviceName:e.name,workspaceId:e.workspace,openBrowser:!e.noBrowser})}catch(s){if(s instanceof M){if(console.error(I.red(`✗ ${s.message}`)),s.kind==="transport")console.error(I.dim(" If your network blocks browsers, run with --api-key <key> instead."));process.exit(1)}throw s}if(t.apiKey=i.key,t.cloudUrl=r,e.name)t.name=e.name;if(ne(t),console.log(""),console.log(I.green("✓ Signed in to Shogo Cloud")),i.workspace)console.log(I.dim(" workspace: ")+i.workspace);if(i.email)console.log(I.dim(" email: ")+i.email);console.log(I.dim(" saved to: ")+"~/.shogo/config.json"),console.log(""),console.log(I.dim(" next: ")+"shogo worker start")}async function On({key:e,cloudUrl:t,cfg:r,name:o}){if(!/^shogo_sk_/.test(e))console.error(I.red('✗ API key should start with "shogo_sk_". Copy it verbatim from the API Keys page.')),process.exit(1);let n;try{let i=await fetch(`${t}/api/api-keys/validate`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({key:e}),signal:AbortSignal.timeout(1e4)});if(n=await i.json().catch(()=>({})),!i.ok||!n?.valid)console.error(I.red(`✗ ${n?.error||`Cloud rejected the key (HTTP ${i.status}).`}`)),process.exit(1)}catch(i){console.error(I.red(`✗ Cannot reach Shogo Cloud at ${t}: ${i?.message??i}`)),process.exit(1)}if(r.apiKey=e,r.cloudUrl=t,o)r.name=o;if(ne(r),console.log(I.green("✓ API key saved to ~/.shogo/config.json")),n.workspace?.name)console.log(I.dim(" workspace: ")+n.workspace.name);if(n.user?.email)console.log(I.dim(" email: ")+n.user.email);console.log(I.dim(" next: ")+"shogo worker start")}import de from"picocolors";async function kr(){let e=G();if(Object.keys(e).length===0){console.log(de.dim("(no config — run `shogo login` or `shogo config set`)"));return}let t={...e,apiKey:e.apiKey?`***${e.apiKey.slice(-4)}`:void 0};console.log(de.dim(`file: ${H}`)),console.log(JSON.stringify(t,null,2))}async function Rr(e,t){let r=["apiKey","cloudUrl","name","workerDir","port"];if(!r.includes(e))console.error(de.red(`Unknown key: ${e}`)),console.error(de.dim(`Allowed: ${r.join(", ")}`)),process.exit(1);let o=G();o[e]=e==="port"?parseInt(t,10):t,ne(o),console.log(de.green(`✓ ${e} set`))}import b from"picocolors";W();import{createHash as Dn}from"node:crypto";import{createWriteStream as Wn,existsSync as me,mkdirSync as ct,readFileSync as $r,renameSync as Pr,rmSync as ut,writeFileSync as Un}from"node:fs";import{tmpdir as Fn}from"node:os";import{dirname as Nn,join as Ie}from"node:path";import{Readable as Ln}from"node:stream";import{pipeline as jn}from"node:stream/promises";var Hn="https://github.com/shogo-labs/shogo-ai/releases/download";function xr(){let e=process.platform,t=process.arch,r;if(e==="darwin")r="darwin";else if(e==="linux")r="linux";else if(e==="win32")r="windows";else throw Error(`Unsupported platform: ${e}`);let o;if(t==="arm64")o="arm64";else if(t==="x64")o="x64";else throw Error(`Unsupported arch: ${t} (need arm64 or x64)`);return`${r}-${o}`}function Ee(){if(!me(oe))return null;try{let e=$r(oe,"utf-8");return JSON.parse(e)}catch{return null}}async function Bn(e,t){let r=t.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/releases\/download/);if(!r)throw Error(`Cannot auto-resolve latest version from non-GitHub baseUrl '${t}'. Pass --version explicitly.`);let o=r[1],n=r[2];if(e==="stable"){let u=`https://api.github.com/repos/${o}/${n}/releases/latest`,p=await fetch(u,{headers:{Accept:"application/vnd.github+json"}});if(!p.ok)throw Error(`GitHub API ${p.status} for ${u}`);let m=await p.json();if(!m.tag_name)throw Error("GitHub API did not return tag_name");return Tr(m.tag_name)}let i=`https://api.github.com/repos/${o}/${n}/releases?per_page=30`,s=await fetch(i,{headers:{Accept:"application/vnd.github+json"}});if(!s.ok)throw Error(`GitHub API ${s.status} for ${i}`);let a=await s.json(),l=e==="beta"?"-beta":"-nightly",c=a.find((u)=>u.prerelease&&/^v\d+\.\d+\.\d+-/.test(u.tag_name)&&u.tag_name.includes(l));if(!c)throw Error(`No ${e} runtime release found`);return Tr(c.tag_name)}function Tr(e){if(!/^v\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(e))throw Error(`Unexpected app tag '${e}' (expected vX.Y.Z[-prerelease])`);return e.slice(1)}function Gn(e,t,r){let o=`shogo-agent-runtime-${t}.tar.gz`,n=`v${e}`,i=`${r.replace(/\/$/,"")}/${n}/${o}`;return{tarball:i,sha256:`${i}.sha256`,assetName:o}}async function Kn(e,t){let r=await fetch(e,{redirect:"follow"});if(!r.ok)throw Error(`Download failed: HTTP ${r.status} for ${e}`);if(!r.body)throw Error(`Download failed: empty body for ${e}`);await jn(Ln.fromWeb(r.body),Wn(t))}async function Jn(e){let t=await fetch(e,{redirect:"follow"});if(!t.ok)throw Error(`SHA256 sidecar fetch failed: HTTP ${t.status} for ${e}`);let o=(await t.text()).trim().split(/\s+/)[0];if(!/^[0-9a-f]{64}$/i.test(o))throw Error(`SHA256 sidecar at ${e} did not contain a 64-char hex digest`);return o.toLowerCase()}function qn(e){let t=$r(e);return Dn("sha256").update(t).digest("hex")}async function Vn(e,t){await new Promise((r,o)=>{let n=T("tar",["-xzf",e,"-C",t],{stdio:["ignore","pipe","pipe"]}),i="";n.stderr.on("data",(s)=>{i+=s.toString()}),n.on("error",o),n.on("exit",(s)=>{if(s===0)r();else o(Error(`tar exited ${s}: ${i.trim()}`))})})}function Yn(e,t){ht();let r=Nn(t);if(!me(r))ct(r,{recursive:!0,mode:448});let o=`${t}.next`;if(me(o))ut(o,{force:!0});Pr(e,o);try{if(process.platform!=="win32"){let{chmodSync:n}=Ce("node:fs");n(o,493)}}catch{}if(me(t))ut(t,{force:!0});Pr(o,t)}async function Ir(e={}){let t=e.logger??console,r=e.channel??"stable",o=e.baseUrl??process.env.SHOGO_RUNTIME_RELEASES_URL??Hn,n=e.target??xr(),i=e.version;if(!i)t.log(`[runtime install] Resolving latest ${r} version...`),i=await Bn(r,o),t.log(`[runtime install] Latest ${r} = ${i}`);let s=Ee();if(s&&s.version===i&&s.target===n&&!e.force)return t.log(`[runtime install] ${i} (${n}) already installed at ${O} — pass --force to reinstall`),{version:i,target:n,binPath:O,source:s.source,sha256:s.sha256,channel:r};let a=Gn(i,n,o);t.log(`[runtime install] Downloading ${a.tarball}`);let l=Ie(Fn(),`shogo-runtime-install-${process.pid}-${Date.now()}`);ct(l,{recursive:!0});try{let c=Ie(l,a.assetName);await Kn(a.tarball,c),t.log("[runtime install] Verifying SHA-256...");let u=await Jn(a.sha256),p=qn(c);if(p!==u)throw Error(`SHA-256 mismatch for ${a.assetName}
19
19
  expected: ${u}
20
- actual: ${p}`);let d=Ie(l,"extract");at(d,{recursive:!0}),await Ln(c,d);let h=Ie(d,"agent-runtime");if(!pe(h))throw Error(`Tarball ${a.assetName} did not contain ./agent-runtime`);Hn(h,M);let m={version:i,target:n,installedAt:new Date().toISOString(),channel:r,source:a.tarball,sha256:p};return An(oe,JSON.stringify(m,null,2)+`
21
- `,{mode:384}),t.log(`[runtime install] Installed agent-runtime ${i} (${n}) to ${M}`),{version:i,target:n,binPath:M,source:a.tarball,sha256:p,channel:r}}finally{try{lt(l,{recursive:!0,force:!0})}catch{}}}function Pr(){return{runtimeDir:re,runtimeBin:M,versionFile:oe}}async function ct(e={}){let t=await Rr({channel:e.channel,version:e.version,baseUrl:e.baseUrl,force:e.force});console.log(),console.log(S.green("✓"),`agent-runtime ${S.bold(t.version)} installed (${t.target})`),console.log(` ${S.dim("path: ")} ${t.binPath}`),console.log(` ${S.dim("source: ")} ${t.source}`),console.log(` ${S.dim("sha256: ")} ${t.sha256}`),console.log(` ${S.dim("channel: ")} ${t.channel}`)}function Tr(){let e=xe();if(!e){console.log(S.yellow("No agent-runtime installed.")),console.log(`Run ${S.cyan("shogo runtime install")} to download the latest.`),process.exitCode=1;return}console.log(`${S.bold("agent-runtime")} ${e.version}`),console.log(` ${S.dim("target: ")} ${e.target}`),console.log(` ${S.dim("channel: ")} ${e.channel}`),console.log(` ${S.dim("installed at:")} ${e.installedAt}`),console.log(` ${S.dim("source: ")} ${e.source}`)}function $r(){let e=G(),t=Pr();if(!e){console.log(S.yellow("agent-runtime binary not found on this machine.")),console.log(),console.log(S.dim("Default install path:"),t.runtimeBin),console.log(),console.log(q()),process.exitCode=1;return}if(console.log(e.path),process.env.SHOGO_DEBUG||process.env.VERBOSE)console.log(S.dim(` (resolved via: ${e.source})`))}async function Er(e={}){let t=xe(),r=e.channel??t?.channel??"stable";if(t)console.log(`${S.dim("current:")} ${t.version} (${t.target}, ${t.channel})`);else console.log(S.dim("No existing install — installing fresh..."));await ct({channel:r,baseUrl:e.baseUrl,force:!0})}import{existsSync as Bn,mkdirSync as Gn}from"node:fs";import{resolve as Kn}from"node:path";import y from"picocolors";import{CloudFileTransport as Jn}from"@shogo-ai/sdk/cloud-file-transport";async function Ir(e,t){if(!e)throw Error("projectId is required");let r=D({apiKey:t.apiKey,cloudUrl:t.cloudUrl});gt(r.projectsDir);let o=Kn(t.into??X(e,r.projectsDir));if(!Bn(o))Gn(o,{recursive:!0});let n=t.include?.split(",").map((a)=>a.trim()).filter(Boolean);if(console.log(y.bold(`
22
- shogo project pull ${y.cyan(e)}`)),console.log(y.dim(" cloud ")+r.cloudUrl),console.log(y.dim(" into ")+o),n?.length)console.log(y.dim(" include ")+n.join(", "));console.log("");let i=new Jn({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o,include:n,onProgress:qn()}),s=await i.downloadAll();if(Vn("Pull",s),t.watch){console.log(y.bold(`
23
- Watching for local changes...`));let a=new ae({rootDir:o,transport:i,onFlush:({uploaded:u,errors:p})=>{let d=u.length===1?u[0]:`${u.length} files`,h=p>0?y.red(` (${p} errors)`):"";console.log(y.dim(` ↑ ${d}${h}`))}});a.start();let l=!1,c=async(u)=>{if(l)return;l=!0,console.log(y.dim(`
24
- Received ${u} — flushing pending uploads...`));try{await a.stop()}catch(p){console.warn(y.yellow(`watcher.stop: ${p?.message??p}`))}process.exit(0)};process.once("SIGINT",()=>void c("SIGINT")),process.once("SIGTERM",()=>void c("SIGTERM")),process.once("SIGHUP",()=>void c("SIGHUP")),await new Promise(()=>{})}}function qn(){return(e)=>{let t=e.kind==="download"?"↓":e.kind==="upload"?"↑":e.kind==="delete"?"✗":"·",r=e.total>0?` ${e.index+1}/${e.total}`:"",o=e.bytes!=null?y.dim(` (${Yn(e.bytes)})`):"";console.log(y.dim(` ${t}${r} ${e.path}${o}`))}}function Vn(e,t){let r=t.errors.length===0,o=r?y.green(`✓ ${e} complete`):y.red(`✗ ${e} completed with errors`);if(console.log(`
25
- ${o}`),console.log(y.dim(" downloaded: ")+t.downloaded),t.uploaded)console.log(y.dim(" uploaded: ")+t.uploaded);if(t.deleted)console.log(y.dim(" deleted: ")+t.deleted);if(t.skipped)console.log(y.dim(" skipped: ")+t.skipped);if(!r){console.log(y.dim(" errors: ")+t.errors.length);for(let n of t.errors.slice(0,5))console.log(y.dim(" ")+y.red(`${n.path}: ${n.message}`));if(t.errors.length>5)console.log(y.dim(` ... and ${t.errors.length-5} more`))}}function Yn(e){if(e<1024)return`${e}B`;if(e<1048576)return`${(e/1024).toFixed(1)}KB`;return`${(e/1024/1024).toFixed(2)}MB`}import{existsSync as Xn}from"node:fs";import{resolve as zn}from"node:path";import k from"picocolors";import{CloudFileTransport as Zn}from"@shogo-ai/sdk/cloud-file-transport";async function xr(e,t){if(!e)throw Error("projectId is required");let r=D({apiKey:t.apiKey,cloudUrl:t.cloudUrl}),o=zn(t.from??X(e,r.projectsDir));if(!Xn(o))throw Error(`Source directory does not exist: ${o}`);let n=t.include?.split(",").map((a)=>a.trim()).filter(Boolean);if(console.log(k.bold(`
26
- shogo project push ${k.cyan(e)}`)),console.log(k.dim(" cloud ")+r.cloudUrl),console.log(k.dim(" from ")+o),n?.length)console.log(k.dim(" include ")+n.join(", "));if(t.deleteRemote)console.log(k.yellow(" --delete-remote: remote files not present locally will be DELETED"));console.log("");let s=await new Zn({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o,include:n,onProgress:(a)=>{let l=a.kind==="upload"?"↑":a.kind==="delete"?"✗":"·",c=a.total>0?` ${a.index+1}/${a.total}`:"",u=a.bytes!=null?k.dim(` (${ei(a.bytes)})`):"";console.log(k.dim(` ${l}${c} ${a.path}${u}`))}}).uploadAll({deleteRemote:t.deleteRemote});Qn("Push",s)}function Qn(e,t){let r=t.errors.length===0,o=r?k.green(`✓ ${e} complete`):k.red(`✗ ${e} completed with errors`);if(console.log(`
27
- ${o}`),console.log(k.dim(" uploaded: ")+t.uploaded),t.deleted)console.log(k.dim(" deleted: ")+t.deleted);if(!r){console.log(k.dim(" errors: ")+t.errors.length);for(let n of t.errors.slice(0,5))console.log(k.dim(" ")+k.red(`${n.path}: ${n.message}`));if(t.errors.length>5)console.log(k.dim(` ... and ${t.errors.length-5} more`))}}function ei(e){if(e<1024)return`${e}B`;if(e<1048576)return`${(e/1024).toFixed(1)}KB`;return`${(e/1024/1024).toFixed(2)}MB`}import{existsSync as ti}from"node:fs";import{resolve as ri}from"node:path";import U from"picocolors";Pe();async function Ar(e,t){if(!e)throw Error("projectId is required");let r=D({apiKey:t.apiKey,cloudUrl:t.cloudUrl}),o=ri(t.into??X(e,r.projectsDir));if(!ti(o))throw Error(`Local project dir does not exist: ${o} — run \`shogo project pull\` first`);if(!Re(o))throw Error(`${o} is not a git repo. \`shogo project checkout\` requires the git sync path; run \`shogo project pull\` after installing git, or use \`shogo project pull --include\` for file-only restore.`);if(console.log(U.bold(`
28
- shogo project checkout ${U.cyan(e)}`)),console.log(U.dim(" cloud ")+r.cloudUrl),console.log(U.dim(" local ")+o),t.at)console.log(U.dim(" at ")+t.at);if(console.log(""),t.unshallow)console.log(U.dim("Unshallowing repo (this may take a moment)...")),await Ke({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o});if(!t.at){let i=await ke({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o});console.log(U.green(`✓ Reset to ${i.commitSha.slice(0,8)} (remote HEAD)`));return}let n;try{n=(await E(["rev-parse","--verify",`${t.at}^{commit}`],{cwd:o})).stdout.trim()}catch{n=await oi(r.cloudUrl,r.apiKey,e,t.at),console.log(U.dim(` resolved checkpoint "${t.at}" → ${n.slice(0,8)}`))}try{await ke({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o,branch:n})}catch{if(!t.unshallow)console.log(U.yellow(" fetch failed; unshallowing and retrying...")),await Ke({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o}),await ke({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o,branch:n});else throw Error(`Cannot reach commit ${n} in local clone`)}await E(["reset","--hard",n],{cwd:o}),console.log(U.green(`✓ Checked out ${n.slice(0,8)}`))}async function oi(e,t,r,o){let n=`${e.replace(/\/+$/,"")}/api/projects/${encodeURIComponent(r)}/checkpoints?limit=100`,i=await fetch(n,{headers:{Authorization:`Bearer ${t}`}});if(!i.ok)throw Error(`Failed to list checkpoints: HTTP ${i.status}`);let s=await i.json(),a=o.toLowerCase(),l=s.checkpoints.find((c)=>{if(c.name&&c.name.toLowerCase()===a)return!0;if(c.commitSha.startsWith(o))return!0;if(c.commitMessage&&c.commitMessage.toLowerCase().includes(a))return!0;return!1});if(!l)throw Error(`No checkpoint matches "${o}" (searched ${s.checkpoints.length} most recent)`);return l.commitSha}import{createInterface as li}from"node:readline";import v from"picocolors";W();import{existsSync as Z,copyFileSync as Cr}from"node:fs";import O from"node:path";import{homedir as _r,platform as Or}from"node:os";function Mr(e,t,r){let n=We(e,["-e",r],{env:{...process.env,DBP:t},encoding:"utf-8",stdio:["ignore","pipe","pipe"],timeout:5000}).trim();if(!n)throw Error(`bun script returned empty output for ${t}`);return JSON.parse(n)}function Ae(e,t){if(!Z(t))return[];return Mr(e,t,`
20
+ actual: ${p}`);let m=Ie(l,"extract");ct(m,{recursive:!0}),await Vn(c,m);let f=Ie(m,"agent-runtime");if(!me(f))throw Error(`Tarball ${a.assetName} did not contain ./agent-runtime`);Yn(f,O);let y={version:i,target:n,installedAt:new Date().toISOString(),channel:r,source:a.tarball,sha256:p};return Un(oe,JSON.stringify(y,null,2)+`
21
+ `,{mode:384}),t.log(`[runtime install] Installed agent-runtime ${i} (${n}) to ${O}`),{version:i,target:n,binPath:O,source:a.tarball,sha256:p,channel:r}}finally{try{ut(l,{recursive:!0,force:!0})}catch{}}}function Er(){return{runtimeDir:re,runtimeBin:O,versionFile:oe}}async function pt(e={}){let t=await Ir({channel:e.channel,version:e.version,baseUrl:e.baseUrl,force:e.force});console.log(),console.log(b.green("✓"),`agent-runtime ${b.bold(t.version)} installed (${t.target})`),console.log(` ${b.dim("path: ")} ${t.binPath}`),console.log(` ${b.dim("source: ")} ${t.source}`),console.log(` ${b.dim("sha256: ")} ${t.sha256}`),console.log(` ${b.dim("channel: ")} ${t.channel}`)}function Ar(){let e=Ee();if(!e){console.log(b.yellow("No agent-runtime installed.")),console.log(`Run ${b.cyan("shogo runtime install")} to download the latest.`),process.exitCode=1;return}console.log(`${b.bold("agent-runtime")} ${e.version}`),console.log(` ${b.dim("target: ")} ${e.target}`),console.log(` ${b.dim("channel: ")} ${e.channel}`),console.log(` ${b.dim("installed at:")} ${e.installedAt}`),console.log(` ${b.dim("source: ")} ${e.source}`)}function _r(){let e=K(),t=Er();if(!e){console.log(b.yellow("agent-runtime binary not found on this machine.")),console.log(),console.log(b.dim("Default install path:"),t.runtimeBin),console.log(),console.log(V()),process.exitCode=1;return}if(console.log(e.path),process.env.SHOGO_DEBUG||process.env.VERBOSE)console.log(b.dim(` (resolved via: ${e.source})`))}async function Mr(e={}){let t=Ee(),r=e.channel??t?.channel??"stable";if(t)console.log(`${b.dim("current:")} ${t.version} (${t.target}, ${t.channel})`);else console.log(b.dim("No existing install — installing fresh..."));await pt({channel:r,baseUrl:e.baseUrl,force:!0})}import{existsSync as Xn,mkdirSync as zn}from"node:fs";import{resolve as Zn}from"node:path";import w from"picocolors";import{CloudFileTransport as Qn}from"@shogo-ai/sdk/cloud-file-transport";async function Cr(e,t){if(!e)throw Error("projectId is required");let r=D({apiKey:t.apiKey,cloudUrl:t.cloudUrl});ft(r.projectsDir);let o=Zn(t.into??z(e,r.projectsDir));if(!Xn(o))zn(o,{recursive:!0});let n=t.include?.split(",").map((a)=>a.trim()).filter(Boolean);if(console.log(w.bold(`
22
+ shogo project pull ${w.cyan(e)}`)),console.log(w.dim(" cloud ")+r.cloudUrl),console.log(w.dim(" into ")+o),n?.length)console.log(w.dim(" include ")+n.join(", "));console.log("");let i=new Qn({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o,include:n,onProgress:ei()}),s=await i.downloadAll();if(ti("Pull",s),t.watch){console.log(w.bold(`
23
+ Watching for local changes...`));let a=new ce({rootDir:o,transport:i,onFlush:({uploaded:u,errors:p})=>{let m=u.length===1?u[0]:`${u.length} files`,f=p>0?w.red(` (${p} errors)`):"";console.log(w.dim(` ↑ ${m}${f}`))}});a.start();let l=!1,c=async(u)=>{if(l)return;l=!0,console.log(w.dim(`
24
+ Received ${u} — flushing pending uploads...`));try{await a.stop()}catch(p){console.warn(w.yellow(`watcher.stop: ${p?.message??p}`))}process.exit(0)};process.once("SIGINT",()=>void c("SIGINT")),process.once("SIGTERM",()=>void c("SIGTERM")),process.once("SIGHUP",()=>void c("SIGHUP")),await new Promise(()=>{})}}function ei(){return(e)=>{let t=e.kind==="download"?"↓":e.kind==="upload"?"↑":e.kind==="delete"?"✗":"·",r=e.total>0?` ${e.index+1}/${e.total}`:"",o=e.bytes!=null?w.dim(` (${ri(e.bytes)})`):"";console.log(w.dim(` ${t}${r} ${e.path}${o}`))}}function ti(e,t){let r=t.errors.length===0,o=r?w.green(`✓ ${e} complete`):w.red(`✗ ${e} completed with errors`);if(console.log(`
25
+ ${o}`),console.log(w.dim(" downloaded: ")+t.downloaded),t.uploaded)console.log(w.dim(" uploaded: ")+t.uploaded);if(t.deleted)console.log(w.dim(" deleted: ")+t.deleted);if(t.skipped)console.log(w.dim(" skipped: ")+t.skipped);if(!r){console.log(w.dim(" errors: ")+t.errors.length);for(let n of t.errors.slice(0,5))console.log(w.dim(" ")+w.red(`${n.path}: ${n.message}`));if(t.errors.length>5)console.log(w.dim(` ... and ${t.errors.length-5} more`))}}function ri(e){if(e<1024)return`${e}B`;if(e<1048576)return`${(e/1024).toFixed(1)}KB`;return`${(e/1024/1024).toFixed(2)}MB`}import{existsSync as oi}from"node:fs";import{resolve as ni}from"node:path";import k from"picocolors";import{CloudFileTransport as ii}from"@shogo-ai/sdk/cloud-file-transport";async function Or(e,t){if(!e)throw Error("projectId is required");let r=D({apiKey:t.apiKey,cloudUrl:t.cloudUrl}),o=ni(t.from??z(e,r.projectsDir));if(!oi(o))throw Error(`Source directory does not exist: ${o}`);let n=t.include?.split(",").map((a)=>a.trim()).filter(Boolean);if(console.log(k.bold(`
26
+ shogo project push ${k.cyan(e)}`)),console.log(k.dim(" cloud ")+r.cloudUrl),console.log(k.dim(" from ")+o),n?.length)console.log(k.dim(" include ")+n.join(", "));if(t.deleteRemote)console.log(k.yellow(" --delete-remote: remote files not present locally will be DELETED"));console.log("");let s=await new ii({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o,include:n,onProgress:(a)=>{let l=a.kind==="upload"?"↑":a.kind==="delete"?"✗":"·",c=a.total>0?` ${a.index+1}/${a.total}`:"",u=a.bytes!=null?k.dim(` (${ai(a.bytes)})`):"";console.log(k.dim(` ${l}${c} ${a.path}${u}`))}}).uploadAll({deleteRemote:t.deleteRemote});si("Push",s)}function si(e,t){let r=t.errors.length===0,o=r?k.green(`✓ ${e} complete`):k.red(`✗ ${e} completed with errors`);if(console.log(`
27
+ ${o}`),console.log(k.dim(" uploaded: ")+t.uploaded),t.deleted)console.log(k.dim(" deleted: ")+t.deleted);if(!r){console.log(k.dim(" errors: ")+t.errors.length);for(let n of t.errors.slice(0,5))console.log(k.dim(" ")+k.red(`${n.path}: ${n.message}`));if(t.errors.length>5)console.log(k.dim(` ... and ${t.errors.length-5} more`))}}function ai(e){if(e<1024)return`${e}B`;if(e<1048576)return`${(e/1024).toFixed(1)}KB`;return`${(e/1024/1024).toFixed(2)}MB`}import{existsSync as li}from"node:fs";import{resolve as ci}from"node:path";import F from"picocolors";Pe();async function Dr(e,t){if(!e)throw Error("projectId is required");let r=D({apiKey:t.apiKey,cloudUrl:t.cloudUrl}),o=ci(t.into??z(e,r.projectsDir));if(!li(o))throw Error(`Local project dir does not exist: ${o} — run \`shogo project pull\` first`);if(!Re(o))throw Error(`${o} is not a git repo. \`shogo project checkout\` requires the git sync path; run \`shogo project pull\` after installing git, or use \`shogo project pull --include\` for file-only restore.`);if(console.log(F.bold(`
28
+ shogo project checkout ${F.cyan(e)}`)),console.log(F.dim(" cloud ")+r.cloudUrl),console.log(F.dim(" local ")+o),t.at)console.log(F.dim(" at ")+t.at);if(console.log(""),t.unshallow)console.log(F.dim("Unshallowing repo (this may take a moment)...")),await qe({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o});if(!t.at){let i=await ke({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o});console.log(F.green(`✓ Reset to ${i.commitSha.slice(0,8)} (remote HEAD)`));return}let n;try{n=(await x(["rev-parse","--verify",`${t.at}^{commit}`],{cwd:o})).stdout.trim()}catch{n=await ui(r.cloudUrl,r.apiKey,e,t.at),console.log(F.dim(` resolved checkpoint "${t.at}" → ${n.slice(0,8)}`))}try{await ke({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o,branch:n})}catch{if(!t.unshallow)console.log(F.yellow(" fetch failed; unshallowing and retrying...")),await qe({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o}),await ke({apiUrl:r.cloudUrl,apiKey:r.apiKey,projectId:e,localDir:o,branch:n});else throw Error(`Cannot reach commit ${n} in local clone`)}await x(["reset","--hard",n],{cwd:o}),console.log(F.green(`✓ Checked out ${n.slice(0,8)}`))}async function ui(e,t,r,o){let n=`${e.replace(/\/+$/,"")}/api/projects/${encodeURIComponent(r)}/checkpoints?limit=100`,i=await fetch(n,{headers:{Authorization:`Bearer ${t}`}});if(!i.ok)throw Error(`Failed to list checkpoints: HTTP ${i.status}`);let s=await i.json(),a=o.toLowerCase(),l=s.checkpoints.find((c)=>{if(c.name&&c.name.toLowerCase()===a)return!0;if(c.commitSha.startsWith(o))return!0;if(c.commitMessage&&c.commitMessage.toLowerCase().includes(a))return!0;return!1});if(!l)throw Error(`No checkpoint matches "${o}" (searched ${s.checkpoints.length} most recent)`);return l.commitSha}import{createInterface as hi}from"node:readline";import S from"picocolors";W();import{existsSync as Z,copyFileSync as Wr}from"node:fs";import C from"node:path";import{homedir as Ur,platform as Fr}from"node:os";function Nr(e,t,r){let n=We(e,["-e",r],{env:{...process.env,DBP:t},encoding:"utf-8",stdio:["ignore","pipe","pipe"],timeout:5000}).trim();if(!n)throw Error(`bun script returned empty output for ${t}`);return JSON.parse(n)}function Ae(e,t){if(!Z(t))return[];return Nr(e,t,`
29
29
  import { Database } from 'bun:sqlite';
30
30
  try {
31
31
  const db = new Database(process.env.DBP, { readonly: true });
@@ -39,7 +39,7 @@ shogo project checkout ${U.cyan(e)}`)),console.log(U.dim(" cloud ")+r.cloudUrl
39
39
  console.error(String(e?.stack || e));
40
40
  process.exit(2);
41
41
  }
42
- `)}function ni(e){if(!Z(e))throw Error(`Database does not exist at ${e} — refusing to back up nothing`);let t=O.dirname(e),r=O.basename(e),o=new Date().toISOString().replace(/[:.]/g,"-"),n=O.join(t,`${r}.bak-${o}`);Cr(e,n);for(let i of["-wal","-shm"]){let s=`${e}${i}`;if(Z(s))Cr(s,`${n}${i}`)}return n}function ii(e,t,r){if(r.length===0)return 0;if(!Z(t))throw Error(`Cannot repair: database does not exist at ${t}`);let n=`
42
+ `)}function pi(e){if(!Z(e))throw Error(`Database does not exist at ${e} — refusing to back up nothing`);let t=C.dirname(e),r=C.basename(e),o=new Date().toISOString().replace(/[:.]/g,"-"),n=C.join(t,`${r}.bak-${o}`);Wr(e,n);for(let i of["-wal","-shm"]){let s=`${e}${i}`;if(Z(s))Wr(s,`${n}${i}`)}return n}function di(e,t,r){if(r.length===0)return 0;if(!Z(t))throw Error(`Cannot repair: database does not exist at ${t}`);let n=`
43
43
  import { Database } from 'bun:sqlite';
44
44
  const names = ${JSON.stringify(r)};
45
45
  if (!Array.isArray(names) || names.some(n => typeof n !== 'string')) {
@@ -58,8 +58,8 @@ shogo project checkout ${U.cyan(e)}`)),console.log(U.dim(" cloud ")+r.cloudUrl
58
58
  console.error(String(e?.stack || e));
59
59
  process.exit(3);
60
60
  }
61
- `;return Mr(e,t,n).deleted}function Dr(e){let t=e.log??(()=>{}),{bunPath:r,dbPath:o}=e;if(!Z(o))return{status:"no-database",detected:[],cleared:[],remaining:[],message:`No database found at ${o}. Nothing to repair — launch Shogo once to create it.`};let n=Ae(r,o);if(n.length===0)return{status:"healthy",detected:[],cleared:[],remaining:[],message:"No failed migrations detected — the local database looks healthy."};t(`Found ${n.length} failed migration(s): ${n.map((p)=>p.name).join(", ")}`);let i;if(!e.skipBackup)i=ni(o),t(`Backed up database to ${i}`);let s=n.map((p)=>p.name),a=ii(r,o,s);t(`Cleared ${a} failed migration row(s).`);let l=Ae(r,o),c=l.length===0?"repaired":"failed",u=c==="repaired"?"Cleared the failed migration record. Relaunch Shogo to re-apply migrations cleanly.":`Repair incomplete — ${l.length} migration(s) still failed: ${l.map((p)=>p.name).join(", ")}.`;return{status:c,detected:n,backupPath:i,cleared:s.slice(0,a),remaining:l,message:u}}function si(){let e=_r(),t=Or(),r;if(t==="darwin")r=O.join(e,"Library","Application Support");else if(t==="win32")r=process.env.APPDATA??O.join(e,"AppData","Roaming");else r=process.env.XDG_CONFIG_HOME??O.join(e,".config");return O.join(r,"Shogo","data")}function Wr(){return O.join(si(),"shogo.db")}function ai(){let e=Or(),t=e==="win32"?"bun.exe":"bun",r=_r(),o=[];if(e==="darwin")o.push(O.join("/Applications","Shogo.app","Contents","Resources","bun",t),O.join(r,"Applications","Shogo.app","Contents","Resources","bun",t));else if(e==="linux")o.push(O.join("/opt","Shogo","resources","bun",t),O.join("/usr","lib","shogo","resources","bun",t));return o}function ut(e){try{return We(e,["--version"],{stdio:"ignore",timeout:5000}),!0}catch{return!1}}function Ur(e){if(e)return ut(e)?e:null;if(process.versions.bun&&process.execPath)return process.execPath;for(let t of ai())if(Z(t)&&ut(t))return t;if(ut("bun"))return"bun";return null}function ci(e){for(let t of e){let r=Number.isFinite(t.startedAt)?new Date(t.startedAt).toISOString():"unknown time",o=(t.errorExcerpt??"").split(`
62
- `)[0]?.trim();if(console.log(` ${v.yellow("•")} ${v.bold(t.name)} ${v.dim(`(attempted ${r})`)}`),o)console.log(` ${v.dim(o)}`)}}async function ui(e){if(!process.stdin.isTTY)return console.log(v.dim("(stdin is not a TTY — re-run with --yes to repair non-interactively)")),!1;let t=li({input:process.stdin,output:process.stdout});try{let r=await new Promise((o)=>t.question(e,o));return/^y(es)?$/i.test(r.trim())}finally{t.close()}}async function Nr(e={}){let t=e.db??Wr(),r=Ur(e.bun);if(!r)throw Error("Could not find a usable `bun` binary (needed to inspect the SQLite database).\n"+" • Install Bun (https://bun.sh) so `bun` is on your PATH, or\n"+` • pass --bun <path> pointing at the bun shipped inside the Shogo app
63
- `+" (e.g. /Applications/Shogo.app/Contents/Resources/bun/bun on macOS).");console.log(v.dim(`Database: ${t}`)),console.log(v.dim(`Bun: ${r}`)),console.log();let o=Ae(r,t);if(o.length===0){console.log(v.green("✓ No failed migrations detected — your local database looks healthy."));return}if(console.log(v.red(`✗ Found ${o.length} failed migration(s):`)),ci(o),console.log(),e.check){console.log(v.dim("Run `shogo doctor` (without --check) to back up the database and clear these records.")),process.exitCode=1;return}if(!e.yes){let i=e.backup===!1?v.red("This will NOT create a backup (--no-backup)."):"Your database will be backed up to a .bak-<timestamp> file first.";if(console.log(i),!await ui(v.bold("Clear these failed migration records and repair? [y/N] "))){console.log(v.dim("Aborted — no changes made.")),process.exitCode=1;return}console.log()}let n=Dr({bunPath:r,dbPath:t,skipBackup:e.backup===!1,log:(i)=>console.log(v.dim(` ${i}`))});if(console.log(),n.status==="repaired"){if(console.log(v.green(`✓ ${n.message}`)),n.backupPath)console.log(v.dim(` Backup: ${n.backupPath}`));console.log(v.bold(`
64
- → Relaunch the Shogo app to finish applying migrations.`))}else{if(console.log(v.red(`✗ ${n.message}`)),n.backupPath)console.log(v.dim(` Backup: ${n.backupPath}`));process.exitCode=1}}W();import{resolve as pi}from"node:path";import jr from"picocolors";function di(e){let{flags:t,config:r,runtime:o}=e,n=t.cwd?pi(t.cwd):process.cwd(),i=["interactive"];if(t.print!==void 0)i.push("-p",t.print);if(t.noTui)i.push("--no-tui");let s=r.cloudUrl.replace(/\/$/,""),a={...e.baseEnv??process.env,SHOGO_INTERACTIVE:"1",SHOGO_INTERACTIVE_CWD:n,PROJECT_DIR:n,WORKSPACE_DIR:n,SHOGO_API_URL:s,SHOGO_CLOUD_URL:s,SHOGO_API_KEY:r.apiKey,AI_PROXY_URL:`${s}/api/ai/v1`,AI_PROXY_TOKEN:r.apiKey,NODE_ENV:"production"};if(t.model)a.SHOGO_MODEL=t.model;if(t.print!==void 0)a.SHOGO_PRINT_PROMPT=t.print;return{bin:o.path,args:i,env:a,cwd:n}}async function Fr(e={}){let t=D({apiKey:e.apiKey,cloudUrl:e.cloudUrl}),r=G({flag:e.runtimeBin});if(!r)console.error(jr.red(q({flag:e.runtimeBin}))),process.exit(1);let o=di({flags:e,config:{apiKey:t.apiKey,cloudUrl:t.cloudUrl},runtime:r}),n=await new Promise((i)=>{let s=T(o.bin,o.args,{cwd:o.cwd,env:o.env,stdio:"inherit"});s.on("error",(a)=>{console.error(jr.red(`✗ Failed to launch agent-runtime: ${a.message}`)),i(1)}),s.on("exit",(a)=>i(a??0))});process.exit(n)}var gi="0.1.0",N=new mi;N.name("shogo").description("Shogo Cloud Agent Worker — run Shogo agents on your own machine.").version(gi);N.command("login").description("Pair this machine with Shogo Cloud (browser device flow, or --api-key for CI)").option("--api-key <key>","CI escape hatch — skip the browser flow and use a key directly").option("--cloud-url <url>","Shogo Cloud URL (default: https://studio.shogo.ai)").option("--name <name>","Device label shown in the dashboard (default: hostname)").option("--workspace <id>","Pre-select a workspace on the bridge picker").option("--no-browser","Do not auto-open the browser; print the URL instead").action((e)=>R(()=>fr(e)));var Ce=N.command("worker").description("Manage the local worker process");Ce.command("start").description("Start the worker and pair with Shogo Cloud").option("--name <name>","Instance name shown in the dashboard (default: hostname)").option("--worker-dir <path>","Working directory for the worker (default: $PWD)").option("--api-key <key>","API key (overrides config/env)").option("--cloud-url <url>","Shogo Cloud URL").option("--port <port>","Local HTTP port for the embedded API").option("--proxy <url>","HTTPS proxy (overrides HTTPS_PROXY env)").option("--project <id>","Pin to a single project (default: multi-project on demand)").option("--runtime-bin <path>","Override the agent-runtime binary path").option("--debug","Run preflight checks before starting").option("--foreground","Run in foreground (don't detach)").option("--no-auto-pull","Disable auto-clone of project workspaces on first request").option("--projects-dir <path>","Root directory for cloned project workspaces (default: ~/.shogo/projects)").option("--no-git","Force the file-transport sync path even when git is available").action((e)=>R(()=>lr(e)));Ce.command("stop").description("Stop the running worker").action(()=>R(ur));Ce.command("status").description("Show worker status").action(()=>R(pr));Ce.command("logs").description("Tail worker logs from ~/.shogo/logs/worker.log").option("-f, --follow","Follow the log (tail -F)").option("--err","Show stderr log instead").action((e)=>R(()=>mr(e)));var _e=N.command("runtime").description("Install / inspect the local agent-runtime binary");_e.command("install").description("Download + verify the agent-runtime tarball into ~/.shogo/runtime/").option("--channel <channel>","stable | beta | nightly (default: stable)").option("--version <version>","install a specific version (e.g. 0.1.0)").option("--base-url <url>","override release base URL (default: GitHub Releases)").option("--force","reinstall even if the same version is already on disk").action((e)=>R(()=>ct(e)));_e.command("version").description("Print the installed agent-runtime version").action(()=>R(()=>Tr()));_e.command("where").description("Print the resolved agent-runtime binary path").action(()=>R(()=>$r()));_e.command("update").description("Update agent-runtime to the latest in its channel").option("--channel <channel>","override channel (defaults to whatever is installed)").option("--base-url <url>","override release base URL").action((e)=>R(()=>Er(e)));var Hr=N.command("config").description("Inspect or modify ~/.shogo/config.json");Hr.command("show").description("Print current config (API key masked)").action(()=>R(wr));Hr.command("set <key> <value>").description("Set apiKey / cloudUrl / name / workerDir / port / projectsDir").action((e,t)=>R(()=>yr(e,t)));var pt=N.command("project").description("Clone/sync project workspaces between Shogo Cloud and this machine");pt.command("pull <projectId>").description("Clone a project from Shogo Cloud into ~/.shogo/projects/<projectId>/").option("--into <dir>","Destination directory (default: ~/.shogo/projects/<projectId>)").option("--watch","After pull, watch the local dir and push edits back to cloud").option("--include <patterns>",'Comma-separated glob patterns (e.g. "src/**,*.md")').option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action((e,t)=>R(()=>Ir(e,t)));pt.command("push <projectId>").description("Upload local workspace edits back to Shogo Cloud").option("--from <dir>","Source directory (default: ~/.shogo/projects/<projectId>)").option("--delete-remote","Mirror local deletions to cloud (DESTRUCTIVE)").option("--include <patterns>","Comma-separated glob patterns").option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action((e,t)=>R(()=>xr(e,t)));pt.command("checkout <projectId>").description("Roll the local workspace to a specific git checkpoint (SHA or named checkpoint)").option("--at <ref>","Target SHA or checkpoint name (default: remote HEAD)").option("--unshallow","Fetch full history before checking out (needed for old SHAs)").option("--into <dir>","Local dir override (default: ~/.shogo/projects/<projectId>)").option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action((e,t)=>R(()=>Ar(e,t)));N.command("doctor").description("Diagnose & repair a wedged local Shogo database (clears failed migrations so the app can reboot)").option("--check","Detect only — never modify the database").option("--yes","Repair without the confirmation prompt").option("--db <path>","Path to shogo.db (default: the desktop app's local database)").option("--bun <path>","Path to a bun binary (default: bundled/PATH bun)").option("--no-backup","Skip the pre-repair database backup (discouraged)").action((e)=>R(()=>Nr(e)));N.command("chat",{isDefault:!0}).description("Start the interactive coding agent in the current directory").argument("[prompt...]","Optional one-shot prompt (runs non-interactively, like -p)").option("-p, --print <prompt>","Headless one-shot: run a single turn and print the result").option("--model <model>","Model id to use for new turns").option("--cwd <dir>","Working directory to operate in (default: $PWD)").option("--runtime-bin <path>","Override the agent-runtime binary path").option("--no-tui","Disable the rich TUI; use a plain readline renderer").option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action((e,t)=>R(()=>Fr({print:t.print??(e.length?e.join(" "):void 0),model:t.model,cwd:t.cwd,runtimeBin:t.runtimeBin,noTui:t.tui===!1,apiKey:t.apiKey,cloudUrl:t.cloudUrl})));N.showHelpAfterError(Lr.dim(`
65
- (use --help for usage)`));N.parseAsync(process.argv);async function R(e){try{await e()}catch(t){if(console.error(Lr.red(`✗ ${t?.message??t}`)),process.env.SHOGO_DEBUG)console.error(t?.stack);process.exit(1)}}
61
+ `;return Nr(e,t,n).deleted}function Lr(e){let t=e.log??(()=>{}),{bunPath:r,dbPath:o}=e;if(!Z(o))return{status:"no-database",detected:[],cleared:[],remaining:[],message:`No database found at ${o}. Nothing to repair — launch Shogo once to create it.`};let n=Ae(r,o);if(n.length===0)return{status:"healthy",detected:[],cleared:[],remaining:[],message:"No failed migrations detected — the local database looks healthy."};t(`Found ${n.length} failed migration(s): ${n.map((p)=>p.name).join(", ")}`);let i;if(!e.skipBackup)i=pi(o),t(`Backed up database to ${i}`);let s=n.map((p)=>p.name),a=di(r,o,s);t(`Cleared ${a} failed migration row(s).`);let l=Ae(r,o),c=l.length===0?"repaired":"failed",u=c==="repaired"?"Cleared the failed migration record. Relaunch Shogo to re-apply migrations cleanly.":`Repair incomplete — ${l.length} migration(s) still failed: ${l.map((p)=>p.name).join(", ")}.`;return{status:c,detected:n,backupPath:i,cleared:s.slice(0,a),remaining:l,message:u}}function mi(){let e=Ur(),t=Fr(),r;if(t==="darwin")r=C.join(e,"Library","Application Support");else if(t==="win32")r=process.env.APPDATA??C.join(e,"AppData","Roaming");else r=process.env.XDG_CONFIG_HOME??C.join(e,".config");return C.join(r,"Shogo","data")}function jr(){return C.join(mi(),"shogo.db")}function gi(){let e=Fr(),t=e==="win32"?"bun.exe":"bun",r=Ur(),o=[];if(e==="darwin")o.push(C.join("/Applications","Shogo.app","Contents","Resources","bun",t),C.join(r,"Applications","Shogo.app","Contents","Resources","bun",t));else if(e==="linux")o.push(C.join("/opt","Shogo","resources","bun",t),C.join("/usr","lib","shogo","resources","bun",t));return o}function dt(e){try{return We(e,["--version"],{stdio:"ignore",timeout:5000}),!0}catch{return!1}}function Hr(e){if(e)return dt(e)?e:null;if(process.versions.bun&&process.execPath)return process.execPath;for(let t of gi())if(Z(t)&&dt(t))return t;if(dt("bun"))return"bun";return null}function fi(e){for(let t of e){let r=Number.isFinite(t.startedAt)?new Date(t.startedAt).toISOString():"unknown time",o=(t.errorExcerpt??"").split(`
62
+ `)[0]?.trim();if(console.log(` ${S.yellow("•")} ${S.bold(t.name)} ${S.dim(`(attempted ${r})`)}`),o)console.log(` ${S.dim(o)}`)}}async function yi(e){if(!process.stdin.isTTY)return console.log(S.dim("(stdin is not a TTY — re-run with --yes to repair non-interactively)")),!1;let t=hi({input:process.stdin,output:process.stdout});try{let r=await new Promise((o)=>t.question(e,o));return/^y(es)?$/i.test(r.trim())}finally{t.close()}}async function Br(e={}){let t=e.db??jr(),r=Hr(e.bun);if(!r)throw Error("Could not find a usable `bun` binary (needed to inspect the SQLite database).\n"+" • Install Bun (https://bun.sh) so `bun` is on your PATH, or\n"+` • pass --bun <path> pointing at the bun shipped inside the Shogo app
63
+ `+" (e.g. /Applications/Shogo.app/Contents/Resources/bun/bun on macOS).");console.log(S.dim(`Database: ${t}`)),console.log(S.dim(`Bun: ${r}`)),console.log();let o=Ae(r,t);if(o.length===0){console.log(S.green("✓ No failed migrations detected — your local database looks healthy."));return}if(console.log(S.red(`✗ Found ${o.length} failed migration(s):`)),fi(o),console.log(),e.check){console.log(S.dim("Run `shogo doctor` (without --check) to back up the database and clear these records.")),process.exitCode=1;return}if(!e.yes){let i=e.backup===!1?S.red("This will NOT create a backup (--no-backup)."):"Your database will be backed up to a .bak-<timestamp> file first.";if(console.log(i),!await yi(S.bold("Clear these failed migration records and repair? [y/N] "))){console.log(S.dim("Aborted — no changes made.")),process.exitCode=1;return}console.log()}let n=Lr({bunPath:r,dbPath:t,skipBackup:e.backup===!1,log:(i)=>console.log(S.dim(` ${i}`))});if(console.log(),n.status==="repaired"){if(console.log(S.green(`✓ ${n.message}`)),n.backupPath)console.log(S.dim(` Backup: ${n.backupPath}`));console.log(S.bold(`
64
+ → Relaunch the Shogo app to finish applying migrations.`))}else{if(console.log(S.red(`✗ ${n.message}`)),n.backupPath)console.log(S.dim(` Backup: ${n.backupPath}`));process.exitCode=1}}W();import{resolve as wi}from"node:path";import Gr from"picocolors";function vi(e){let{flags:t,config:r,runtime:o}=e,n=t.cwd?wi(t.cwd):process.cwd(),i=["interactive"];if(t.print!==void 0)i.push("-p",t.print);if(t.noTui)i.push("--no-tui");let s=r.cloudUrl.replace(/\/$/,""),a={...e.baseEnv??process.env,SHOGO_INTERACTIVE:"1",SHOGO_INTERACTIVE_CWD:n,PROJECT_DIR:n,WORKSPACE_DIR:n,SHOGO_API_URL:s,SHOGO_CLOUD_URL:s,SHOGO_API_KEY:r.apiKey,AI_PROXY_URL:`${s}/api/ai/v1`,AI_PROXY_TOKEN:r.apiKey,NODE_ENV:"production"};if(t.model)a.SHOGO_MODEL=t.model;if(t.print!==void 0)a.SHOGO_PRINT_PROMPT=t.print;return{bin:o.path,args:i,env:a,cwd:n}}async function Kr(e={}){let t=D({apiKey:e.apiKey,cloudUrl:e.cloudUrl}),r=K({flag:e.runtimeBin});if(!r)console.error(Gr.red(V({flag:e.runtimeBin}))),process.exit(1);let o=vi({flags:e,config:{apiKey:t.apiKey,cloudUrl:t.cloudUrl},runtime:r}),n=await new Promise((i)=>{let s=T(o.bin,o.args,{cwd:o.cwd,env:o.env,stdio:"inherit"});s.on("error",(a)=>{console.error(Gr.red(`✗ Failed to launch agent-runtime: ${a.message}`)),i(1)}),s.on("exit",(a)=>i(a??0))});process.exit(n)}var bi="0.1.0",N=new Si;N.name("shogo").description("Shogo Cloud Agent Worker — run Shogo agents on your own machine.").version(bi);N.command("login").description("Pair this machine with Shogo Cloud (browser device flow, or --api-key for CI)").option("--api-key <key>","CI escape hatch — skip the browser flow and use a key directly").option("--cloud-url <url>","Shogo Cloud URL (default: https://studio.shogo.ai)").option("--name <name>","Device label shown in the dashboard (default: hostname)").option("--workspace <id>","Pre-select a workspace on the bridge picker").option("--no-browser","Do not auto-open the browser; print the URL instead").action((e)=>R(()=>br(e)));var _e=N.command("worker").description("Manage the local worker process");_e.command("start").description("Start the worker and pair with Shogo Cloud").option("--name <name>","Instance name shown in the dashboard (default: hostname)").option("--worker-dir <path>","Working directory for the worker (default: $PWD)").option("--api-key <key>","API key (overrides config/env)").option("--cloud-url <url>","Shogo Cloud URL").option("--port <port>","Local HTTP port for the embedded API").option("--proxy <url>","HTTPS proxy (overrides HTTPS_PROXY env)").option("--project <id>","Pin to a single project (default: multi-project on demand)").option("--runtime-bin <path>","Override the agent-runtime binary path").option("--debug","Run preflight checks before starting").option("--foreground","Run in foreground (don't detach)").option("--no-auto-pull","Disable auto-clone of project workspaces on first request").option("--projects-dir <path>","Root directory for cloned project workspaces (default: ~/.shogo/projects)").option("--no-git","Force the file-transport sync path even when git is available").action((e)=>R(()=>mr(e)));_e.command("stop").description("Stop the running worker").action(()=>R(hr));_e.command("status").description("Show worker status").action(()=>R(fr));_e.command("logs").description("Tail worker logs from ~/.shogo/logs/worker.log").option("-f, --follow","Follow the log (tail -F)").option("--err","Show stderr log instead").action((e)=>R(()=>wr(e)));var Me=N.command("runtime").description("Install / inspect the local agent-runtime binary");Me.command("install").description("Download + verify the agent-runtime tarball into ~/.shogo/runtime/").option("--channel <channel>","stable | beta | nightly (default: stable)").option("--version <version>","install a specific version (e.g. 0.1.0)").option("--base-url <url>","override release base URL (default: GitHub Releases)").option("--force","reinstall even if the same version is already on disk").action((e)=>R(()=>pt(e)));Me.command("version").description("Print the installed agent-runtime version").action(()=>R(()=>Ar()));Me.command("where").description("Print the resolved agent-runtime binary path").action(()=>R(()=>_r()));Me.command("update").description("Update agent-runtime to the latest in its channel").option("--channel <channel>","override channel (defaults to whatever is installed)").option("--base-url <url>","override release base URL").action((e)=>R(()=>Mr(e)));var qr=N.command("config").description("Inspect or modify ~/.shogo/config.json");qr.command("show").description("Print current config (API key masked)").action(()=>R(kr));qr.command("set <key> <value>").description("Set apiKey / cloudUrl / name / workerDir / port / projectsDir").action((e,t)=>R(()=>Rr(e,t)));var mt=N.command("project").description("Clone/sync project workspaces between Shogo Cloud and this machine");mt.command("pull <projectId>").description("Clone a project from Shogo Cloud into ~/.shogo/projects/<projectId>/").option("--into <dir>","Destination directory (default: ~/.shogo/projects/<projectId>)").option("--watch","After pull, watch the local dir and push edits back to cloud").option("--include <patterns>",'Comma-separated glob patterns (e.g. "src/**,*.md")').option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action((e,t)=>R(()=>Cr(e,t)));mt.command("push <projectId>").description("Upload local workspace edits back to Shogo Cloud").option("--from <dir>","Source directory (default: ~/.shogo/projects/<projectId>)").option("--delete-remote","Mirror local deletions to cloud (DESTRUCTIVE)").option("--include <patterns>","Comma-separated glob patterns").option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action((e,t)=>R(()=>Or(e,t)));mt.command("checkout <projectId>").description("Roll the local workspace to a specific git checkpoint (SHA or named checkpoint)").option("--at <ref>","Target SHA or checkpoint name (default: remote HEAD)").option("--unshallow","Fetch full history before checking out (needed for old SHAs)").option("--into <dir>","Local dir override (default: ~/.shogo/projects/<projectId>)").option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action((e,t)=>R(()=>Dr(e,t)));N.command("doctor").description("Diagnose & repair a wedged local Shogo database (clears failed migrations so the app can reboot)").option("--check","Detect only — never modify the database").option("--yes","Repair without the confirmation prompt").option("--db <path>","Path to shogo.db (default: the desktop app's local database)").option("--bun <path>","Path to a bun binary (default: bundled/PATH bun)").option("--no-backup","Skip the pre-repair database backup (discouraged)").action((e)=>R(()=>Br(e)));N.command("chat",{isDefault:!0}).description("Start the interactive coding agent in the current directory").argument("[prompt...]","Optional one-shot prompt (runs non-interactively, like -p)").option("-p, --print <prompt>","Headless one-shot: run a single turn and print the result").option("--model <model>","Model id to use for new turns").option("--cwd <dir>","Working directory to operate in (default: $PWD)").option("--runtime-bin <path>","Override the agent-runtime binary path").option("--no-tui","Disable the rich TUI; use a plain readline renderer").option("--api-key <key>","Override API key for this run").option("--cloud-url <url>","Override Shogo Cloud URL for this run").action((e,t)=>R(()=>Kr({print:t.print??(e.length?e.join(" "):void 0),model:t.model,cwd:t.cwd,runtimeBin:t.runtimeBin,noTui:t.tui===!1,apiKey:t.apiKey,cloudUrl:t.cloudUrl})));N.showHelpAfterError(Jr.dim(`
65
+ (use --help for usage)`));N.parseAsync(process.argv);async function R(e){try{await e()}catch(t){if(console.error(Jr.red(`✗ ${t?.message??t}`)),process.env.SHOGO_DEBUG)console.error(t?.stack);process.exit(1)}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shogo-ai/worker",
3
- "version": "2.3.13",
3
+ "version": "2.4.1",
4
4
  "description": "Shogo Cloud Agent Worker — run Shogo agents on your own machine (laptop, devbox, CI).",
5
5
  "license": "MIT",
6
6
  "author": "Shogo Technologies, Inc.",
@@ -0,0 +1,116 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * SHO-203: the agent-command budget is separate from the runtime's own leak
5
+ * ceiling, and the kernel cap has to sit above it so the guard (which writes a
6
+ * note) always trips before the kernel (which writes nothing).
7
+ */
8
+ import { afterEach, describe, expect, it } from 'bun:test';
9
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import {
13
+ applyLiveMemoryCap,
14
+ resolveExecMemoryMB,
15
+ resolveResourceLimits,
16
+ type RuntimeResourceLimits,
17
+ } from '../resource-limits';
18
+
19
+ const dirs: string[] = [];
20
+ function limitsFile(contents: unknown): string {
21
+ const dir = mkdtempSync(join(tmpdir(), 'host-limits-'));
22
+ dirs.push(dir);
23
+ const file = join(dir, 'host-limits.json');
24
+ writeFileSync(file, typeof contents === 'string' ? contents : JSON.stringify(contents));
25
+ return file;
26
+ }
27
+
28
+ afterEach(() => {
29
+ while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true });
30
+ });
31
+
32
+ const baseEnv = { RUNTIME_MEMORY_MB: '8192' };
33
+
34
+ describe('resolveResourceLimits', () => {
35
+ it('keeps the runtime ceiling unchanged when no command budget is known', () => {
36
+ const limits = resolveResourceLimits({ ...baseEnv });
37
+ expect(limits?.memoryMB).toBe(8192);
38
+ expect(limits?.osMemoryMB).toBe(8192);
39
+ });
40
+
41
+ it('adds an explicit SHOGO_EXEC_MEMORY_MB to the OS cap only', () => {
42
+ const limits = resolveResourceLimits({ ...baseEnv, SHOGO_EXEC_MEMORY_MB: '131072' });
43
+ expect(limits?.memoryMB).toBe(8192);
44
+ expect(limits?.osMemoryMB).toBe(8192 + 131072);
45
+ });
46
+
47
+ it('reads the command budget from the watched host-limits file', () => {
48
+ const file = limitsFile({ execMemoryMB: 122880, totalMemMB: 131072, source: 'auto' });
49
+ const limits = resolveResourceLimits({ ...baseEnv, SHOGO_HOST_LIMITS_FILE: file });
50
+ expect(limits?.osMemoryMB).toBe(8192 + 122880);
51
+ });
52
+
53
+ it('prefers an explicit env budget over the file', () => {
54
+ const file = limitsFile({ execMemoryMB: 4096 });
55
+ const limits = resolveResourceLimits({
56
+ ...baseEnv,
57
+ SHOGO_HOST_LIMITS_FILE: file,
58
+ SHOGO_EXEC_MEMORY_MB: '2048',
59
+ });
60
+ expect(limits?.osMemoryMB).toBe(8192 + 2048);
61
+ });
62
+
63
+ it('ignores a missing or malformed limits file', () => {
64
+ expect(resolveExecMemoryMB({ SHOGO_HOST_LIMITS_FILE: join(tmpdir(), 'nope.json') })).toBe(0);
65
+ expect(resolveExecMemoryMB({ SHOGO_HOST_LIMITS_FILE: limitsFile('{oops') })).toBe(0);
66
+ expect(resolveExecMemoryMB({ SHOGO_HOST_LIMITS_FILE: limitsFile({ execMemoryMB: -5 }) })).toBe(0);
67
+ });
68
+ });
69
+
70
+ describe('applyLiveMemoryCap', () => {
71
+ const limits: RuntimeResourceLimits = { memoryMB: 8192, osMemoryMB: 8192 + 122880, cpuPercent: 0, tasksMax: 4096 };
72
+
73
+ it('raises a live systemd scope with set-property, using the combined cap', () => {
74
+ const calls: Array<{ command: string; args: string[] }> = [];
75
+ const ok = applyLiveMemoryCap(
76
+ { scopeName: 'shogo-rt-abc12345-m1', limits },
77
+ { platform: 'linux', run: (command, args) => calls.push({ command, args }) },
78
+ );
79
+ expect(ok).toBe(true);
80
+ expect(calls).toHaveLength(1);
81
+ expect(calls[0]!.command).toBe('systemctl');
82
+ expect(calls[0]!.args).toEqual([
83
+ '--user',
84
+ 'set-property',
85
+ '--runtime',
86
+ 'shogo-rt-abc12345-m1',
87
+ 'MemoryMax=131072M',
88
+ 'MemoryHigh=117964M',
89
+ ]);
90
+ });
91
+
92
+ it('rewrites a direct cgroup memory.max, in bytes', () => {
93
+ const writes: Array<{ path: string; data: string }> = [];
94
+ const ok = applyLiveMemoryCap(
95
+ { pid: 4242, limits },
96
+ {
97
+ platform: 'linux',
98
+ exists: (path) => path.endsWith('memory.max') || path.endsWith('memory.high'),
99
+ writeFile: (path, data) => writes.push({ path, data }),
100
+ },
101
+ );
102
+ expect(ok).toBe(true);
103
+ expect(writes).toEqual([
104
+ { path: '/sys/fs/cgroup/shogo/rt-4242/memory.max', data: String(131072 * 1024 * 1024) },
105
+ { path: '/sys/fs/cgroup/shogo/rt-4242/memory.high', data: String(117964 * 1024 * 1024) },
106
+ ]);
107
+ });
108
+
109
+ it('does nothing where the limit cannot be changed in place', () => {
110
+ expect(applyLiveMemoryCap({ scopeName: 'x', limits }, { platform: 'win32' })).toBe(false);
111
+ expect(applyLiveMemoryCap({ scopeName: 'x', limits }, { platform: 'darwin' })).toBe(false);
112
+ expect(
113
+ applyLiveMemoryCap({ pid: 7, limits }, { platform: 'linux', exists: () => false }),
114
+ ).toBe(false);
115
+ });
116
+ });
@@ -0,0 +1,87 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (C) 2026 Shogo Technologies, Inc.
3
+ /**
4
+ * SHO-203: the agent-command budget must not be borrowed from the runtime's own
5
+ * leak ceiling. When the desktop publishes `host-limits.json`, the runtime gets
6
+ * the file's path and *no* `SHOGO_EXEC_MEMORY_MB` — otherwise the env value,
7
+ * baked in at spawn, would win and a settings change would need a restart.
8
+ *
9
+ * `buildEnv` is private; it is driven through a typed cast, the same way
10
+ * `runtime-manager-tree-sitter-env.test.ts` does.
11
+ */
12
+ import { afterAll, describe, expect, it } from 'bun:test';
13
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { WorkerRuntimeManager } from '../runtime-manager';
17
+
18
+ const dir = mkdtempSync(join(tmpdir(), 'rm-exec-budget-'));
19
+ afterAll(() => rmSync(dir, { recursive: true, force: true }));
20
+
21
+ const BIN = '/opt/shogo/runtime/shogo-agent-runtime-linux-x64';
22
+
23
+ type EnvBuilder = { buildEnv(slot: unknown, runtimeBinPath: string): NodeJS.ProcessEnv };
24
+
25
+ /** The fields `buildEnv` reads, plus an idempotent port for the PID file path. */
26
+ function fakeSlot(): unknown {
27
+ return {
28
+ projectId: 'proj-exec-budget-test',
29
+ agentPort: 41000,
30
+ apiServerPort: 41100,
31
+ spawnConfig: { cloudUrl: 'https://cloud.test', apiKey: 'api-key' },
32
+ };
33
+ }
34
+
35
+ function limitsFile(): string {
36
+ const file = join(dir, 'host-limits.json');
37
+ writeFileSync(file, JSON.stringify({ execMemoryMB: 122880, totalMemMB: 131072, source: 'auto' }));
38
+ return file;
39
+ }
40
+
41
+ describe('WorkerRuntimeManager.buildEnv — agent-command budget', () => {
42
+ it('hands the desktop limits file through and leaves the env budget unset', async () => {
43
+ const file = limitsFile();
44
+ const mgr = new WorkerRuntimeManager({
45
+ env: { RUNTIME_MEMORY_MB: '8192', SHOGO_HOST_LIMITS_FILE: file } as NodeJS.ProcessEnv,
46
+ });
47
+ const env = (mgr as unknown as EnvBuilder).buildEnv(fakeSlot(), BIN);
48
+
49
+ expect(env.SHOGO_HOST_LIMITS_FILE).toBe(file);
50
+ // The bug: this used to be `String(limits.memoryMB)`.
51
+ expect(env.SHOGO_EXEC_MEMORY_MB).toBeUndefined();
52
+ expect(env.SHOGO_EXEC_PIDS_FILE).toBeTruthy();
53
+ await mgr.stopAll();
54
+ });
55
+
56
+ it('falls back to the runtime ceiling for a headless worker with no limits file', () => {
57
+ const mgr = new WorkerRuntimeManager({
58
+ env: { RUNTIME_MEMORY_MB: '8192' } as NodeJS.ProcessEnv,
59
+ });
60
+ const env = (mgr as unknown as EnvBuilder).buildEnv(fakeSlot(), BIN);
61
+ expect(env.SHOGO_EXEC_MEMORY_MB).toBe('8192');
62
+ });
63
+
64
+ it('keeps an explicit SHOGO_EXEC_MEMORY_MB in front of the file', async () => {
65
+ const file = limitsFile();
66
+ const mgr = new WorkerRuntimeManager({
67
+ env: {
68
+ RUNTIME_MEMORY_MB: '8192',
69
+ SHOGO_EXEC_MEMORY_MB: '4096',
70
+ SHOGO_HOST_LIMITS_FILE: file,
71
+ } as NodeJS.ProcessEnv,
72
+ });
73
+ const env = (mgr as unknown as EnvBuilder).buildEnv(fakeSlot(), BIN);
74
+ expect(env.SHOGO_EXEC_MEMORY_MB).toBe('4096');
75
+ await mgr.stopAll();
76
+ });
77
+
78
+ it('leaves the runtime ceiling itself alone', () => {
79
+ const mgr = new WorkerRuntimeManager({
80
+ env: { RUNTIME_MEMORY_MB: '8192' } as NodeJS.ProcessEnv,
81
+ });
82
+ const env = (mgr as unknown as EnvBuilder).buildEnv(fakeSlot(), BIN);
83
+ // The heap hint still comes from the runtime ceiling, not the command budget.
84
+ expect(env.NODE_OPTIONS).toContain('--max-old-space-size=6144');
85
+ expect(env.RUNTIME_MEMORY_MB).toBe('8192');
86
+ });
87
+ });
@@ -26,8 +26,13 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
26
26
  import { join } from 'node:path';
27
27
 
28
28
  export interface RuntimeResourceLimits {
29
- /** Per-project RAM ceiling in MB. Always > 0 when limits are active. */
29
+ /** Per-project RAM ceiling in MB for the runtime's OWN processes. Always > 0
30
+ * when limits are active. Not the budget for the commands an agent runs. */
30
31
  memoryMB: number;
32
+ /** Hard OS cap for the whole process tree in MB: {@link memoryMB} plus the
33
+ * agent-command budget. The exec guard (which writes a note) must trip before
34
+ * the kernel (which writes nothing), so the kernel cap has to sit above it. */
35
+ osMemoryMB: number;
31
36
  /** CPU ceiling as percent of ONE core (100 = one full core). 0 = no cap. */
32
37
  cpuPercent: number;
33
38
  /** Max tasks/pids in the group (cgroup pids.max). */
@@ -39,10 +44,47 @@ type Logger = Pick<Console, 'log' | 'warn' | 'error'>;
39
44
  /** cgroup CPU period in microseconds (100ms), the systemd/cgroup default. */
40
45
  const CPU_PERIOD_US = 100_000;
41
46
 
47
+ /** `host-limits.json`, as published by the desktop app (SHO-203). */
48
+ export interface HostLimitsFile {
49
+ /** MB the agent's `exec` commands may use. */
50
+ execMemoryMB?: number;
51
+ /** Total physical RAM of the machine, for messages. */
52
+ totalMemMB?: number;
53
+ source?: 'auto' | 'settings';
54
+ }
55
+
56
+ /** Parse `host-limits.json`. Missing or malformed reads as `null`. */
57
+ export function readHostLimitsFile(path: string): HostLimitsFile | null {
58
+ try {
59
+ const parsed = JSON.parse(readFileSync(path, 'utf-8')) as HostLimitsFile;
60
+ return parsed && typeof parsed === 'object' ? parsed : null;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * The command budget from the environment: an explicit `SHOGO_EXEC_MEMORY_MB`
68
+ * (headless worker, tests) wins, otherwise the watched `host-limits.json`.
69
+ * 0 = no budget known.
70
+ */
71
+ export function resolveExecMemoryMB(env: NodeJS.ProcessEnv): number {
72
+ const explicit = parseInt(env.SHOGO_EXEC_MEMORY_MB || '', 10);
73
+ if (Number.isFinite(explicit) && explicit > 0) return explicit;
74
+ if (!env.SHOGO_HOST_LIMITS_FILE) return 0;
75
+ const mb = readHostLimitsFile(env.SHOGO_HOST_LIMITS_FILE)?.execMemoryMB;
76
+ return typeof mb === 'number' && Number.isFinite(mb) && mb > 0 ? Math.floor(mb) : 0;
77
+ }
78
+
42
79
  /**
43
80
  * Resolve the active resource limits from the environment. Returns `null` when
44
81
  * no ceiling is configured (`RUNTIME_MEMORY_MB` unset or <= 0), which disables
45
82
  * all enforcement for backwards compatibility.
83
+ *
84
+ * `memoryMB` stays the runtime's own leak ceiling (the RSS watchdog and the heap
85
+ * cap use it). `osMemoryMB` is the kernel-level cap for the whole tree, raised
86
+ * by the agent-command budget so a big `exec` command is stopped by the guard —
87
+ * which writes an actionable note — instead of silently OOM-killed (SHO-203).
46
88
  */
47
89
  export function resolveResourceLimits(env: NodeJS.ProcessEnv): RuntimeResourceLimits | null {
48
90
  const mb = parseInt(env.RUNTIME_MEMORY_MB || '', 10);
@@ -54,7 +96,7 @@ export function resolveResourceLimits(env: NodeJS.ProcessEnv): RuntimeResourceLi
54
96
  const tasksRaw = parseInt(env.RUNTIME_TASKS_MAX || '', 10);
55
97
  const tasksMax = Number.isFinite(tasksRaw) && tasksRaw > 0 ? tasksRaw : 4096;
56
98
 
57
- return { memoryMB: mb, cpuPercent, tasksMax };
99
+ return { memoryMB: mb, osMemoryMB: mb + resolveExecMemoryMB(env), cpuPercent, tasksMax };
58
100
  }
59
101
 
60
102
  /**
@@ -98,6 +140,11 @@ export function hasSystemdRun(): boolean {
98
140
  * command untouched (`wrapped: false`) on non-Linux or when systemd-run is
99
141
  * absent — the caller then falls back to {@link attachDirectCgroup} or the
100
142
  * watchdog.
143
+ *
144
+ * The cap is `limits.osMemoryMB` (runtime ceiling + agent-command budget): the
145
+ * scope holds the runtime's own processes *and* the `exec` shells it spawns, so
146
+ * sized to the runtime ceiling alone it killed a 22.5 GB command on a 128 GB
147
+ * machine with no message (SHO-203).
101
148
  */
102
149
  export function wrapSpawnForCgroup(opts: {
103
150
  command: string;
@@ -112,10 +159,10 @@ export function wrapSpawnForCgroup(opts: {
112
159
  }
113
160
 
114
161
  const props = [
115
- '-p', `MemoryMax=${limits.memoryMB}M`,
162
+ '-p', `MemoryMax=${limits.osMemoryMB}M`,
116
163
  // MemoryHigh throttles + reclaims before the hard OOM at MemoryMax, so a
117
164
  // spiky build is slowed rather than instantly killed.
118
- '-p', `MemoryHigh=${Math.max(128, Math.floor(limits.memoryMB * 0.9))}M`,
165
+ '-p', `MemoryHigh=${memoryHighMB(limits.osMemoryMB)}M`,
119
166
  '-p', `TasksMax=${limits.tasksMax}`,
120
167
  ];
121
168
  if (limits.cpuPercent > 0) props.push('-p', `CPUQuota=${limits.cpuPercent}%`);
@@ -131,10 +178,20 @@ export function wrapSpawnForCgroup(opts: {
131
178
  command,
132
179
  ...args,
133
180
  ];
134
- logger?.log(`[resource-limits] wrapping spawn in systemd scope ${scopeName} (MemoryMax=${limits.memoryMB}M)`);
181
+ logger?.log(`[resource-limits] wrapping spawn in systemd scope ${scopeName} (MemoryMax=${limits.osMemoryMB}M)`);
135
182
  return { command: 'systemd-run', args: wrappedArgs, wrapped: true };
136
183
  }
137
184
 
185
+ /** Soft cap for a hard cap: 10% below, so reclaim starts before the OOM. */
186
+ function memoryHighMB(memoryMB: number): number {
187
+ return Math.max(128, Math.floor(memoryMB * 0.9));
188
+ }
189
+
190
+ /** The cgroup v2 directory {@link attachDirectCgroup} puts a runtime in. */
191
+ export function directCgroupDir(pid: number): string {
192
+ return join('/sys/fs/cgroup', 'shogo', `rt-${pid}`);
193
+ }
194
+
138
195
  /**
139
196
  * Fallback for Linux hosts without systemd: write a cgroup v2 directly and move
140
197
  * `pid` (plus its future children) into it. Best-effort — returns `false` when
@@ -153,10 +210,10 @@ export function attachDirectCgroup(opts: {
153
210
  // cgroup v2 unified hierarchy exposes cgroup.controllers at the root.
154
211
  if (!existsSync(join(root, 'cgroup.controllers'))) return false;
155
212
 
156
- const dir = join(root, 'shogo', `rt-${pid}`);
213
+ const dir = directCgroupDir(pid);
157
214
  try {
158
215
  mkdirSync(dir, { recursive: true });
159
- writeFileSync(join(dir, 'memory.max'), String(limits.memoryMB * 1024 * 1024));
216
+ writeFileSync(join(dir, 'memory.max'), String(limits.osMemoryMB * 1024 * 1024));
160
217
  if (limits.cpuPercent > 0) {
161
218
  const quota = Math.floor((limits.cpuPercent / 100) * CPU_PERIOD_US);
162
219
  writeFileSync(join(dir, 'cpu.max'), `${quota} ${CPU_PERIOD_US}`);
@@ -164,7 +221,7 @@ export function attachDirectCgroup(opts: {
164
221
  try { writeFileSync(join(dir, 'pids.max'), String(limits.tasksMax)); } catch { /* controller may be absent */ }
165
222
  // Moving the pid in also captures children forked after this point.
166
223
  writeFileSync(join(dir, 'cgroup.procs'), String(pid));
167
- logger?.log(`[resource-limits] attached pid ${pid} to cgroup ${dir} (memory.max=${limits.memoryMB}M)`);
224
+ logger?.log(`[resource-limits] attached pid ${pid} to cgroup ${dir} (memory.max=${limits.osMemoryMB}M)`);
168
225
  return true;
169
226
  } catch (err: any) {
170
227
  logger?.warn(`[resource-limits] direct cgroup attach failed for pid ${pid}: ${err?.message ?? err}`);
@@ -172,6 +229,92 @@ export function attachDirectCgroup(opts: {
172
229
  }
173
230
  }
174
231
 
232
+ // ─── Live re-apply (SHO-203) ─────────────────────────────────────────
233
+
234
+ export interface LiveMemoryCapOptions {
235
+ /** Runtime process group id, used for a direct cgroup. */
236
+ pid?: number | null;
237
+ /** `systemd-run --scope` unit name, when the spawn was cgroup-wrapped. */
238
+ scopeName?: string | null;
239
+ limits: RuntimeResourceLimits;
240
+ logger?: Logger;
241
+ }
242
+
243
+ /** Swappable effects, so the generated `systemctl` args / file write are testable. */
244
+ export interface LiveMemoryCapDeps {
245
+ platform?: NodeJS.Platform;
246
+ run?: (command: string, args: string[]) => void;
247
+ writeFile?: (path: string, data: string) => void;
248
+ exists?: (path: string) => boolean;
249
+ }
250
+
251
+ /**
252
+ * Re-apply the memory cap to a runtime that is already running, after the
253
+ * agent-command budget changed. Without this a raised budget would only land
254
+ * on the next spawn — an app restart — which is exactly the complaint in
255
+ * SHO-203.
256
+ *
257
+ * - Linux + systemd scope: `systemctl --user set-property --runtime`.
258
+ * - Linux + direct cgroup: rewrite `memory.max` (and `memory.high`).
259
+ * - Windows: the Job Object's limit is fixed at assign time; there is no API
260
+ * to change it in place, so it lands on the next spawn (the RSS watchdog
261
+ * covers the gap).
262
+ * - macOS: the watchdog reads the budget from the limits file itself, so
263
+ * nothing to do here.
264
+ */
265
+ export function applyLiveMemoryCap(
266
+ opts: LiveMemoryCapOptions,
267
+ deps: LiveMemoryCapDeps = {},
268
+ ): boolean {
269
+ const platform = deps.platform ?? process.platform;
270
+ const { limits, logger } = opts;
271
+ if (platform === 'win32') {
272
+ logger?.log('[resource-limits] Job Object memory limit applies on the next runtime spawn');
273
+ return false;
274
+ }
275
+ if (platform !== 'linux') return false;
276
+
277
+ if (opts.scopeName) {
278
+ const args = [
279
+ '--user',
280
+ 'set-property',
281
+ '--runtime',
282
+ opts.scopeName,
283
+ `MemoryMax=${limits.osMemoryMB}M`,
284
+ `MemoryHigh=${memoryHighMB(limits.osMemoryMB)}M`,
285
+ ];
286
+ if (deps.run) {
287
+ deps.run('systemctl', args);
288
+ return true;
289
+ }
290
+ execFile('systemctl', args, { timeout: 5000 }, (err) => {
291
+ if (err) {
292
+ logger?.warn(`[resource-limits] set-property ${opts.scopeName} failed: ${err.message}`);
293
+ } else {
294
+ logger?.log(`[resource-limits] ${opts.scopeName} MemoryMax=${limits.osMemoryMB}M`);
295
+ }
296
+ });
297
+ return true;
298
+ }
299
+
300
+ if (!opts.pid) return false;
301
+ const dir = directCgroupDir(opts.pid);
302
+ const exists = deps.exists ?? existsSync;
303
+ const write = deps.writeFile ?? writeFileSync;
304
+ try {
305
+ if (!exists(join(dir, 'memory.max'))) return false;
306
+ write(join(dir, 'memory.max'), String(limits.osMemoryMB * 1024 * 1024));
307
+ if (exists(join(dir, 'memory.high'))) {
308
+ write(join(dir, 'memory.high'), String(memoryHighMB(limits.osMemoryMB) * 1024 * 1024));
309
+ }
310
+ logger?.log(`[resource-limits] raised cgroup ${dir} memory.max to ${limits.osMemoryMB}M`);
311
+ return true;
312
+ } catch (err: any) {
313
+ logger?.warn(`[resource-limits] live cgroup update failed for ${dir}: ${err?.message ?? err}`);
314
+ return false;
315
+ }
316
+ }
317
+
175
318
  // ─── Windows: Job Object (optional native helper) ────────────────────
176
319
 
177
320
  /**
@@ -203,11 +346,11 @@ export function tryAttachJobObject(opts: {
203
346
  };
204
347
  if (typeof helper.limitProcess !== 'function') return false;
205
348
  const ok = helper.limitProcess(pid, {
206
- memoryBytes: limits.memoryMB * 1024 * 1024,
349
+ memoryBytes: limits.osMemoryMB * 1024 * 1024,
207
350
  killOnClose: true,
208
351
  });
209
352
  if (ok) {
210
- logger?.log(`[resource-limits] attached pid ${pid} to Job Object (${limits.memoryMB}M)`);
353
+ logger?.log(`[resource-limits] attached pid ${pid} to Job Object (${limits.osMemoryMB}M)`);
211
354
  }
212
355
  return ok;
213
356
  } catch {
@@ -33,7 +33,7 @@
33
33
  */
34
34
  import { type ChildProcess, spawn, spawnSync } from './child-process.ts';
35
35
  import { createHmac, randomBytes } from 'node:crypto';
36
- import { existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
36
+ import { existsSync, mkdirSync, readdirSync, rmSync, unwatchFile, watchFile } from 'node:fs';
37
37
  import { createConnection } from 'node:net';
38
38
  import { tmpdir } from 'node:os';
39
39
  import { dirname, join } from 'node:path';
@@ -41,6 +41,7 @@ import { resolveRuntime, type ResolvedRuntime } from './runtime-resolver.ts';
41
41
  import {
42
42
  RssWatchdog,
43
43
  applyHeapEnvCap,
44
+ applyLiveMemoryCap,
44
45
  attachDirectCgroup,
45
46
  readExecPidsFile,
46
47
  resolveResourceLimits,
@@ -538,6 +539,9 @@ interface InternalRuntime {
538
539
  * or when no limit is configured. Stopped on exit/stop.
539
540
  */
540
541
  limitWatchdog: RssWatchdog | null;
542
+ /** `systemd-run --scope` unit name when the spawn was cgroup-wrapped, so a
543
+ * later budget change can be re-applied to the live scope (SHO-203). */
544
+ cgroupScope?: string | null;
541
545
  lastError?: string;
542
546
  spawnConfig: ProjectSpawnConfig;
543
547
  /**
@@ -688,6 +692,9 @@ export class WorkerRuntimeManager implements RuntimeResolver {
688
692
  /** Timer that refreshes every running runtime's cached activity sample. */
689
693
  private activityTimer: ReturnType<typeof setInterval> | null = null;
690
694
 
695
+ /** `host-limits.json` this manager watches for live limit changes (SHO-203). */
696
+ private hostLimitsFile: string | null = null;
697
+
691
698
  constructor(opts: WorkerRuntimeManagerOptions = {}) {
692
699
  this.opts = opts;
693
700
  this.log = opts.logger ?? console;
@@ -697,6 +704,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
697
704
  // time — see runtime-activity.ts. Unref'd so it never holds the process
698
705
  // open.
699
706
  this.ensureActivitySampler();
707
+ this.watchHostLimits();
700
708
  }
701
709
 
702
710
  /**
@@ -1339,6 +1347,7 @@ export class WorkerRuntimeManager implements RuntimeResolver {
1339
1347
 
1340
1348
  async stopAll(signal: NodeJS.Signals = 'SIGTERM'): Promise<void> {
1341
1349
  this.stopped = true;
1350
+ this.unwatchHostLimits();
1342
1351
  if (this.activityTimer) {
1343
1352
  clearInterval(this.activityTimer);
1344
1353
  this.activityTimer = null;
@@ -1420,18 +1429,22 @@ export class WorkerRuntimeManager implements RuntimeResolver {
1420
1429
  let command = baseSpawn.command;
1421
1430
  let args = baseSpawn.args;
1422
1431
  let cgroupWrapped = false;
1432
+ let cgroupScope: string | null = null;
1423
1433
  if (limits) {
1434
+ const scopeName = `shogo-rt-${slot.projectId.slice(0, 8)}-${Date.now().toString(36)}`;
1424
1435
  const wrapped = wrapSpawnForCgroup({
1425
1436
  command: baseSpawn.command,
1426
1437
  args: baseSpawn.args,
1427
1438
  limits,
1428
- scopeName: `shogo-rt-${slot.projectId.slice(0, 8)}-${Date.now().toString(36)}`,
1439
+ scopeName,
1429
1440
  logger: this.log,
1430
1441
  });
1431
1442
  command = wrapped.command;
1432
1443
  args = wrapped.args;
1433
1444
  cgroupWrapped = wrapped.wrapped;
1445
+ cgroupScope = wrapped.wrapped ? scopeName : null;
1434
1446
  }
1447
+ slot.cgroupScope = cgroupScope;
1435
1448
 
1436
1449
  this.log.log(
1437
1450
  `[WorkerRuntimeManager] Spawning agent-runtime for ${slot.projectId} ` +
@@ -1605,6 +1618,60 @@ export class WorkerRuntimeManager implements RuntimeResolver {
1605
1618
  return this.resolvedLimits;
1606
1619
  }
1607
1620
 
1621
+ /**
1622
+ * Watch `host-limits.json` so a Settings change to the agent-command budget
1623
+ * lands on the app that is already running (SHO-203). An env var can only
1624
+ * change at spawn; a file can change now. `fs.watchFile` polls, which is why
1625
+ * it also picks the file up on a fresh install — no IPC plumbing needed.
1626
+ */
1627
+ private watchHostLimits(): void {
1628
+ const file = (this.opts.env ?? process.env).SHOGO_HOST_LIMITS_FILE;
1629
+ if (!file || this.hostLimitsFile) return;
1630
+ this.hostLimitsFile = file;
1631
+ watchFile(file, { interval: 2000 }, () => this.applyHostLimitsChange());
1632
+ }
1633
+
1634
+ /** Re-resolve the limits and push the new OS cap onto every live runtime. */
1635
+ private applyHostLimitsChange(): void {
1636
+ const env = this.opts.env ?? process.env;
1637
+ let limits: RuntimeResourceLimits | null;
1638
+ try {
1639
+ limits = resolveResourceLimits(env);
1640
+ } catch (err: any) {
1641
+ this.log.warn(`[WorkerRuntimeManager] host limits re-read failed: ${err?.message ?? err}`);
1642
+ return;
1643
+ }
1644
+ // Future spawns read the memo, so keep it in step with the file.
1645
+ this.resolvedLimits = limits;
1646
+ if (!limits) return;
1647
+ const live = [...this.runtimes.values()].filter((slot) => slot.pid && slot.status !== 'stopped');
1648
+ if (live.length === 0) return;
1649
+ this.log.log(
1650
+ `[WorkerRuntimeManager] agent-command budget changed — memory cap now ${limits.osMemoryMB}M ` +
1651
+ `(${limits.memoryMB}M runtime + ${limits.osMemoryMB - limits.memoryMB}M commands)`,
1652
+ );
1653
+ for (const slot of live) {
1654
+ try {
1655
+ applyLiveMemoryCap({
1656
+ pid: slot.pid,
1657
+ scopeName: slot.cgroupScope ?? null,
1658
+ limits,
1659
+ logger: this.log,
1660
+ });
1661
+ } catch (err: any) {
1662
+ this.log.warn(
1663
+ `[WorkerRuntimeManager] live memory cap for ${slot.projectId} failed: ${err?.message ?? err}`,
1664
+ );
1665
+ }
1666
+ }
1667
+ }
1668
+
1669
+ private unwatchHostLimits(): void {
1670
+ if (!this.hostLimitsFile) return;
1671
+ unwatchFile(this.hostLimitsFile);
1672
+ this.hostLimitsFile = null;
1673
+ }
1674
+
1608
1675
  /**
1609
1676
  * Apply the strongest available OS-level cap to a freshly-spawned runtime,
1610
1677
  * or start the RSS watchdog as a fallback. Called right after spawn so most
@@ -1803,7 +1870,15 @@ export class WorkerRuntimeManager implements RuntimeResolver {
1803
1870
  // the runtime publishes their PIDs so the watchdog leaves them out, and
1804
1871
  // budgets them itself, stopping the largest command instead of the turn.
1805
1872
  env.SHOGO_EXEC_PIDS_FILE = this.execPidsFile(slot);
1806
- env.SHOGO_EXEC_MEMORY_MB ??= String(limits.memoryMB);
1873
+ const limitSource = this.opts.env ?? process.env;
1874
+ if (limitSource.SHOGO_HOST_LIMITS_FILE) {
1875
+ // Desktop: the budget is the user's setting, published in a file the
1876
+ // runtime watches, so it changes on a running app (SHO-203). Never
1877
+ // default it to the runtime ceiling — that is the bug this fixed.
1878
+ env.SHOGO_HOST_LIMITS_FILE = limitSource.SHOGO_HOST_LIMITS_FILE;
1879
+ } else {
1880
+ env.SHOGO_EXEC_MEMORY_MB ??= String(limits.memoryMB);
1881
+ }
1807
1882
  }
1808
1883
 
1809
1884
  return env;