@aicommander/agent 1.0.19 → 1.0.22

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/bin/agent.js CHANGED
@@ -1,14 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import{program as h}from"commander";var k="1.0.19";import Gt from"os";function Bt(e){try{let t=JSON.parse(e);return typeof t.code=="string"?t.code:null}catch{return null}}var fe=class extends Error{constructor(o,s){super(`Registration failed (${o}): ${s}`);this.status=o;this.body=s;this.name="RegistrationError",this.code=Bt(s)}status;body;code};async function pe(e,t,o={}){let s={hostname:Gt.hostname(),platform:process.platform,arch:process.arch,agentVersion:k,...t?{deviceId:t.deviceId,deviceSecret:t.deviceSecret}:{},...o.forceNew?{forceNew:!0}:o.currentCode?{currentCode:o.currentCode}:{}},i=await fetch(`${e}/api/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!i.ok){let r=await i.text();throw new fe(i.status,r)}return i.json()}import Kt from"ws";import Jt from"os";import{spawn as Pe}from"child_process";var Z="https://aicommander.dev";var Le="ABCDEFGHJKMNPQRSTVWXYZ0123456789";var Dn=Math.floor(256/Le.length)*Le.length;var Wt=/^AIC-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}$/;function Vt(e){return Wt.test(e.trim().toUpperCase())}function U(e){return Vt(e)?`AIC-${e.slice(4,8)}-***-***`:e}var $e=process.platform==="win32";function He(e,t,o,s){let i=Date.now(),r=Pe(e,[],{shell:!0,cwd:t??process.env.HOME,env:{...process.env,...o??{}},stdio:["ignore","pipe","pipe"],detached:!$e}),c=!1,a=null,l=()=>{a&&(clearTimeout(a),a=null)};r.stdout?.on("data",d=>{s.onOutput(d.toString("base64"),"stdout")}),r.stderr?.on("data",d=>{s.onOutput(d.toString("base64"),"stderr")}),r.on("close",d=>{c=!0,l(),s.onDone(d??-1,Date.now()-i)}),r.on("error",d=>{c=!0,l(),s.onError(d.message)});let m=d=>{if(!(c||r.pid==null))try{$e?Pe("taskkill",["/pid",String(r.pid),"/T","/F"],{stdio:"ignore"}):process.kill(-r.pid,d)}catch{}};return{kill:()=>{c||(m("SIGTERM"),l(),a=setTimeout(()=>{a=null,m("SIGKILL")},5e3),a.unref?.())}}}import g from"chalk";function Ue(e,t,o=!1){console.clear(),console.log(),console.log(g.bold.cyan(" AI Commander \u2014 Remote Agent")),console.log(),console.log(g.gray(" Session code:")),console.log(),console.log(g.bold.greenBright(` ${o?e:U(e)}`)),console.log(),o||console.log(g.gray(" Reveal the full code: sudo aicommander-agent status --reveal")),console.log(g.gray(" Then give it to your admin to connect.")),console.log(g.yellow(" \u26A0 Keep this code secret \u2014 anyone who has it can run commands as root here.")),console.log(g.gray(` Server: ${t}`)),console.log(g.gray(" Press Ctrl+C to disconnect.")),console.log(),console.log(g.yellow(" Waiting for admin connection...")),console.log()}function Fe(e,t){console.log(g.yellow(` Reconnecting in ${Math.round(t/1e3)}s (attempt ${e})...`))}function Ge(){console.log(g.green(" Admin connected."))}function Be(e){console.log(g.cyan(` > ${e}`))}function We(e,t){let o=e===0?g.green:g.red;console.log(o(` Done. Exit ${e} in ${t}ms`))}var ge={capable:!1,enabled:!1,expiresAt:null},Je=360*60*1e3;function Xt(e,t){return new Promise((o,s)=>{if(t.aborted){s(new DOMException("Aborted","AbortError"));return}let i=setTimeout(o,e);t.addEventListener("abort",()=>{clearTimeout(i),s(new DOMException("Aborted","AbortError"))},{once:!0})})}async function Xe(e){let t=1e3,o=3e4,s=0,i=e.agentToken;for(;!e.signal?.aborted;){s++;try{let r=await qt(e,i);if(t=1e3,s=0,r&&e.reauth&&!e.signal?.aborted)try{i=await e.reauth()}catch{}}catch(r){if(r.name==="AbortError")break;let c=.8+Math.random()*.4,a=Math.min(t*c,o);e.silent||Fe(s,a);try{await Xt(a,e.signal??new AbortController().signal)}catch(l){if(l.name==="AbortError")break}t=Math.min(t*2,o)}}}async function qt(e,t){let{serverUrl:o,sessionCode:s}=e,i=`${o.replace(/^http/,"ws")}/ws/agent/${s}?token=${t}`,r=l=>{try{e.onStatus?.(l)}catch{}},c=l=>{try{e.onActivity?.(l)}catch{}},a=()=>{try{e.onHeartbeat?.()}catch{}};return r("connecting"),new Promise((l,m)=>{let d=new Kt(i),f=null,S=null,D=null,$=!1,R=null,O=!1,X=()=>{D&&(clearTimeout(D),D=null)},y=()=>{R&&(clearInterval(R),R=null)},b=()=>{if(!e.reauthIntervalMs||!e.reauth)return;let u=()=>{if(f){D=setTimeout(u,3e4);return}$=!0,d.close(1e3,"reauth")};D=setTimeout(u,e.reauthIntervalMs)},H=()=>{O||(O=!0,X(),y(),l($))},N=u=>{O||(O=!0,X(),y(),m(u))},q=()=>{a(),S&&clearTimeout(S),S=setTimeout(()=>{d.terminate(),N(new Error("Ping timeout"))},9e4)};e.signal?.addEventListener("abort",()=>d.close(1e3,"disabled"),{once:!0});let de=()=>{if(O)return;let u={type:"agent:screen_state",screenShare:e.screenShare?.getState()??ge};try{d.send(JSON.stringify(u))}catch{}};d.on("open",()=>{r("connected"),q(),b();let u={type:"agent:register",hostname:Jt.hostname(),platform:process.platform,arch:process.arch,agentVersion:k,screenShare:e.screenShare?.getState()??ge};d.send(JSON.stringify(u)),e.screenShare?.on("change",de),e.silent||Ge()}),d.on("message",u=>{let v;try{v=JSON.parse(String(u))}catch{return}switch(v.type){case"do:ping":q();let $t={type:"agent:pong",ts:v.ts};d.send(JSON.stringify($t));break;case"do:exec":{Be(v.command),c(!0);let{commandId:Y,command:Ht,cwd:Ut,env:Ft}=v,Me=Date.now(),me=!1;y(),R=setInterval(()=>{let A=Date.now()-Me;if(A>=3e5&&!me){me=!0;let T={type:"agent:exec_idle",commandId:Y,idleMs:A};d.send(JSON.stringify(T))}},3e5),R.unref?.(),f=He(Ht,Ut,Ft,{onOutput:(A,T)=>{Me=Date.now(),me=!1;let ue={type:"agent:output",commandId:Y,chunk:A,stream:T};d.send(JSON.stringify(ue))},onDone:(A,T)=>{f=null,y(),c(!1),We(A,T);let ue={type:"agent:done",commandId:Y,exitCode:A,durationMs:T};d.send(JSON.stringify(ue))},onError:A=>{f=null,y(),c(!1);let T={type:"agent:error",commandId:Y,error:A};d.send(JSON.stringify(T))}}).kill;break}case"do:kill":f&&(f(),f=null);break;case"do:screenshot":Yt(d,v.requestId,e.screenShare);break}}),d.on("close",(u,v)=>{S&&clearTimeout(S),y(),e.screenShare?.off("change",de),f&&(f(),f=null,c(!1)),r("disconnected"),u===1e3||u===4001?H():N(new Error(`WS closed: ${u} ${String(v)}`))}),d.on("error",u=>{S&&clearTimeout(S),y(),e.screenShare?.off("change",de),r("disconnected"),N(u)}),d.on("unexpected-response",(u,v)=>{m(new Error(`Unexpected HTTP response: ${v.statusCode}`))})})}async function Yt(e,t,o){let s=c=>{try{e.send(JSON.stringify(c))}catch{}},i=c=>s({type:"agent:screenshot_error",requestId:t,error:c}),r=o?.getState()??ge;if(!o||!r.capable){i("This machine cannot share its screen (desktop macOS/Windows only).");return}if(!r.enabled){i("Screen sharing is turned off on this machine.");return}try{let{data:c,mimeType:a}=await o.capture();if(c.length>10485760){let m=(c.length/1048576).toFixed(1);i(`Screenshot is ${m} MB, which exceeds the ${10485760/(1024*1024)} MB limit.`);return}let l=c.toString("base64");for(let m=0;m<l.length;m+=524288)s({type:"agent:screenshot_chunk",requestId:t,chunk:l.slice(m,m+524288)});s({type:"agent:screenshot_done",requestId:t,mimeType:a,totalBytes:c.length})}catch(c){i(`Screenshot failed: ${c.message}`)}}import w from"node:fs/promises";import Ze from"node:path";var Q="/var/run/aicommander-agent",z=Ze.join(Q,"state.json"),qe=448,Ye=384;async function Qe(e){let t=null;try{await w.mkdir(Q,{recursive:!0,mode:qe}),await w.chmod(Q,qe),t=Ze.join(Q,`.state.${process.pid}.${Date.now()}.tmp`);let o=await w.open(t,"wx",Ye);try{await o.writeFile(JSON.stringify(e,null,2),"utf8")}finally{await o.close()}await w.rename(t,z),t=null,await w.chmod(z,Ye)}catch{}finally{t&&await w.rm(t,{force:!0}).catch(()=>{})}}async function ze(){try{await w.rm(z,{force:!0})}catch{}}async function E(){try{let e=await w.readFile(z,"utf8");return JSON.parse(e)}catch{return null}}import ye from"node:fs";import j from"node:path";import Zt from"node:os";import{randomUUID as Qt,randomBytes as zt}from"node:crypto";var et="/etc/aicommander-agent",tt=j.join(Zt.homedir(),".config","aicommander-agent"),ee="device.json";function nt(){return{deviceId:Qt(),deviceSecret:zt(32).toString("base64url")}}function en(e){return typeof e=="object"&&e!==null&&typeof e.deviceId=="string"&&e.deviceId.length>0&&typeof e.deviceSecret=="string"&&e.deviceSecret.length>0}function Se(e){try{let t=ye.readFileSync(e,"utf8"),o=JSON.parse(t);if(en(o))return{deviceId:o.deviceId,deviceSecret:o.deviceSecret}}catch{}return null}function he(e,t){try{return ye.mkdirSync(e,{recursive:!0,mode:448}),ye.writeFileSync(j.join(e,ee),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function F(e){if(e){let i=Se(j.join(e,ee));if(i)return i;let r=nt();return he(e,r),r}let t=Se(j.join(et,ee));if(t)return t;let o=Se(j.join(tt,ee));if(o)return o;let s=nt();return he(et,s)||he(tt,s),s}import I from"node:fs";import G from"node:path";import tn from"node:os";var nn="/etc/aicommander-agent",on=G.join(tn.homedir(),".config","aicommander-agent"),ve="session.json",ot=".rotate";function rn(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0&&typeof e.agentToken=="string"&&e.agentToken.length>0}function sn(e){try{let t=I.readFileSync(e,"utf8"),o=JSON.parse(t);if(rn(o))return{sessionCode:o.sessionCode,agentToken:o.agentToken}}catch{}return null}function an(e,t){try{return I.mkdirSync(e,{recursive:!0,mode:448}),I.writeFileSync(G.join(e,ve),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function K(e){return e?[e]:[nn,on]}function rt(e){for(let t of K(e)){let o=sn(G.join(t,ve));if(o)return o}return null}function Ee(e,t){for(let o of K(t))if(an(o,e))return}function st(e){for(let t of K(e))try{I.rmSync(G.join(t,ve),{force:!0})}catch{}}function it(e){for(let t of K(e))try{I.mkdirSync(t,{recursive:!0,mode:448}),I.writeFileSync(G.join(t,ot),"",{mode:384});return}catch{}}function at(e){let t=!1;for(let o of K(e)){let s=G.join(o,ot);try{I.existsSync(s)&&(t=!0,I.rmSync(s,{force:!0}))}catch{}}return t}import{spawn as pn}from"child_process";import te from"fs";import ct from"path";import ln from"os";function ne(e){if(e)return ct.join(e,"heartbeat");if(process.platform==="linux")try{return te.mkdirSync("/run/aicommander-agent",{recursive:!0}),"/run/aicommander-agent/heartbeat"}catch{}return ct.join(ln.tmpdir(),"aicommander-agent-heartbeat")}function lt(e){try{let t=`${e}.tmp`;te.writeFileSync(t,String(Date.now())),te.renameSync(t,e)}catch{}}var J=null;function dt(e,t=5e3){be(),lt(e),J=setInterval(()=>lt(e),t),J.unref?.()}function be(){J&&(clearInterval(J),J=null)}function mt(e){try{let t=te.readFileSync(e,"utf8").trim(),o=Number(t);return Number.isFinite(o)?o:null}catch{return null}}async function ft(e={}){let t=e.heartbeatPath??ne(e.configDir),o=e.spawnWorker??(d=>pn(process.execPath,process.argv.slice(1),{env:d,stdio:"inherit"})),s=null,i=0,r=!1,c=0,a=[],l=null,m=null;await new Promise(d=>{let f=null,S=null,D=()=>{l&&(clearInterval(l),l=null),m&&(clearTimeout(m),m=null),f&&(process.removeListener("SIGINT",f),f=null),S&&(process.removeListener("SIGTERM",S),S=null)},$=()=>{D(),d()},R=()=>{i=Date.now(),c++;let b={...process.env,AIC_ROLE:"worker",AIC_HEARTBEAT:t};s=o(b),s.on("exit",(H,N)=>{if(s=null,r){$();return}if(e.maxSpawns!=null&&c>=e.maxSpawns){$();return}O(`worker exited (code=${H??"null"} signal=${N??"null"})`)}),s.on("error",()=>{})},O=b=>{let H=Date.now();a=a.filter(q=>H-q<3e5),a.push(H);let N=a.length>5?6e4:0;m&&clearTimeout(m),m=setTimeout(()=>{m=null,r||R()},N),m.unref?.()},X=()=>{if(s)try{s.kill("SIGKILL")}catch{}};l=setInterval(()=>{if(r||!s||Date.now()-i<3e4)return;let b=mt(t);b!=null&&Date.now()-b>3e4&&X()},1e4),l.unref?.();let y=b=>{if(r=!0,l&&(clearInterval(l),l=null),m&&(clearTimeout(m),m=null),s)try{s.kill(b)}catch{}else $()};f=()=>y("SIGINT"),S=()=>y("SIGTERM"),process.on("SIGINT",f),process.on("SIGTERM",S),R()})}function gn(e,t){let o=e.split(".").map(r=>parseInt(r,10)||0),s=t.split(".").map(r=>parseInt(r,10)||0),i=Math.max(o.length,s.length);for(let r=0;r<i;r++){let c=(o[r]??0)-(s[r]??0);if(c!==0)return c>0?1:-1}return 0}function pt(e,t){return gn(e,t)>0}async function gt(e,t=1e4){let o=new AbortController,s=setTimeout(()=>o.abort(),t);try{let i=await fetch(`${e.replace(/\/+$/,"")}/dist/latest`,{signal:o.signal,headers:{Accept:"application/json"}});if(!i.ok)return null;let r=await i.json();return{version:typeof r.version=="string"?r.version:null,mac:typeof r.mac=="string"?r.mac:null,win:typeof r.win=="string"?r.win:null,agentLinuxX64:typeof r.agentLinuxX64=="string"?r.agentLinuxX64:null,agentLinuxArm64:typeof r.agentLinuxArm64=="string"?r.agentLinuxArm64:null}}catch{return null}finally{clearTimeout(s)}}var M=process.env.AICOMMANDER_SERVER??Z,Sn=1440*60*1e3;async function St(){let e=await gt(M);e?.version&&pt(e.version,k)&&console.warn(`
3
- \u2B06 A newer AI Commander agent is available (v${e.version}; you have v${k}).
4
- Update: curl -fsSL ${M}/install | sudo bash
5
- `)}async function ht(){process.env.AIC_ROLE==="worker"?await hn():await ft()}async function hn(){let e=process.env.AIC_ROLE==="worker";e&&dt(process.env.AIC_HEARTBEAT||ne());let t=()=>{e&&be(),ze(),process.exit(0)};process.on("SIGINT",t),process.on("SIGTERM",t),typeof process.getuid=="function"&&process.getuid()!==0&&console.warn(`
2
+ import{program as _}from"commander";var O="1.0.22";import cn from"os";function ln(e){try{let t=JSON.parse(e);return typeof t.code=="string"?t.code:null}catch{return null}}var we=class extends Error{constructor(r,s){super(`Registration failed (${r}): ${s}`);this.status=r;this.body=s;this.name="RegistrationError",this.code=ln(s)}status;body;code};async function Re(e,t,r={}){let s={hostname:cn.hostname(),platform:process.platform,arch:process.arch,agentVersion:O,...t?{deviceId:t.deviceId,deviceSecret:t.deviceSecret}:{},...r.forceNew?{forceNew:!0}:r.currentCode?{currentCode:r.currentCode}:{}},i=await fetch(`${e}/api/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!i.ok){let o=await i.text();throw new we(i.status,o)}return i.json()}import Tn from"ws";import xn from"os";import{spawn as Ze}from"child_process";var I="https://aicommander.dev";var v="aicommander-exec";var qe="ABCDEFGHJKMNPQRSTVWXYZ0123456789";var dr=Math.floor(256/qe.length)*qe.length;var dn=/^AIC-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}$/;function un(e){return dn.test(e.trim().toUpperCase())}function j(e){return un(e)?`AIC-${e.slice(4,8)}-***-***`:e}var Qe=process.platform==="win32";function ze(e,t,r,s){let i=Date.now(),o=Ze(e,[],{shell:!0,cwd:t??process.env.HOME,env:{...process.env,...r??{}},stdio:["ignore","pipe","pipe"],detached:!Qe}),c=!1,a=null,d=()=>{a&&(clearTimeout(a),a=null)};o.stdout?.on("data",p=>{s.onOutput(p.toString("base64"),"stdout")}),o.stderr?.on("data",p=>{s.onOutput(p.toString("base64"),"stderr")}),o.on("close",p=>{c=!0,d(),s.onDone(p??-1,Date.now()-i)}),o.on("error",p=>{c=!0,d(),s.onError(p.message)});let l=p=>{if(!(c||o.pid==null))try{Qe?Ze("taskkill",["/pid",String(o.pid),"/T","/F"],{stdio:"ignore"}):process.kill(-o.pid,p)}catch{}};return{kill:()=>{c||(l("SIGTERM"),d(),a=setTimeout(()=>{a=null,l("SIGKILL")},5e3),a.unref?.())}}}import{spawn as hn}from"node:child_process";import{readFileSync as yn}from"node:fs";import{isAbsolute as En}from"node:path";import{spawn as mn}from"node:child_process";import{createRequire as fn}from"node:module";import{constants as pn}from"node:os";var gn=fn(import.meta.url),Te="__secure-exec-drop",Sn="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";function et(){let e=!1;try{e=gn("node:sea").isSea()}catch{}return e}function tt(e){let t=process.argv[1],r=!et()&&t?[t,Te,...e]:[Te,...e];return{cmd:process.execPath,args:r}}function nt(e){let t=et()?1:2;if(e[t]!==Te)return!1;let[r,s,i,o,...c]=e.slice(t+1),a=Number(r),d=Number(s),l=process;try{if(!Number.isInteger(a)||!Number.isInteger(d)||a===0||!i||!o)throw new Error("invalid drop arguments");if(typeof l.initgroups!="function"||typeof l.setgid!="function"||typeof l.setuid!="function")throw new Error("privilege-drop syscalls unavailable on this platform");if(l.initgroups(i,d),l.setgid(d),l.setuid(a),l.getuid?.()===0||l.getgid?.()===0)throw new Error("privilege drop did not take effect");if(typeof l.getgroups=="function"){let f=l.getgroups();if(Array.isArray(f)&&f.includes(0))throw new Error("privilege drop left root's supplementary groups in place")}}catch(f){process.stderr.write(`secure-exec: privilege drop failed: ${f instanceof Error?f.message:String(f)}
3
+ `),process.exit(127)}let p={...process.env,PATH:Sn},m=mn(o,c,{stdio:"inherit",env:p,shell:!1}),u=!1;return m.on("error",f=>{u||(u=!0,process.stderr.write(`secure-exec: spawn failed: ${f.message}
4
+ `),process.exit(127))}),m.on("exit",(f,S)=>{if(!u){if(u=!0,S){let A=pn.signals[S];process.exit(128+(typeof A=="number"?A:0))}process.exit(f??0)}}),!0}function vn(){let e;try{e=yn("/etc/passwd","utf8")}catch{throw new Error("cannot read /etc/passwd to resolve the secure-exec user")}let t=e.split(`
5
+ `).filter(d=>d.startsWith(`${v}:`));if(t.length===0)throw new Error(`secure exec user "${v}" not found in /etc/passwd \u2014 reinstall the agent to create it`);if(t.length>1)throw new Error(`multiple /etc/passwd entries for "${v}" \u2014 refusing to guess which is the sandbox user`);let r=t[0].split(":");if(r.length!==7)throw new Error(`malformed passwd entry for ${v}`);let s=r[2],i=r[3];if(!/^\d+$/.test(s)||!/^\d+$/.test(i))throw new Error(`could not resolve uid/gid for ${v}`);let o=Number(s),c=Number(i),a=r[5]||"/tmp";if(!Number.isInteger(o)||!Number.isInteger(c))throw new Error(`could not resolve uid/gid for ${v}`);if(o===0)throw new Error("refusing to run secure exec as uid 0");if(c===0)throw new Error("refusing to run secure exec as gid 0");return{uid:o,gid:c,home:a,name:v}}var bn="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",wn=new Set(["NODE_OPTIONS","BASH_ENV","ENV","IFS","PYTHONSTARTUP","PYTHONPATH","PERL5OPT","RUBYOPT","GIT_EXTERNAL_DIFF","PATH","HOME","USER","LOGNAME"]),Rn=e=>/^LD_/.test(e)||wn.has(e);function ot(e,t){if(process.platform!=="linux")throw new Error("secure exec is only supported on Linux");if(typeof process.getuid=="function"&&process.getuid()!==0)throw new Error("secure exec requires the agent to run as root (to drop privilege)");let r=e.argv;if(!Array.isArray(r)||r.length===0||typeof r[0]!="string")throw new Error("secure exec requires a non-empty argv");let s=r[0];if(s.includes("/"))throw new Error(`command "${s}" must be a bare command name (no path separators)`);if(!e.allowedCommands.includes(s))throw new Error(`command "${s}" is not in this token's allowlist`);if(e.cwd!=null&&!En(e.cwd))throw new Error(`cwd "${e.cwd}" must be an absolute path`);let i=null;if(e.input!=null&&(i=Buffer.from(e.input,"base64"),i.length>262144))throw new Error(`stdin payload ${i.length}B exceeds limit ${262144}B`);let o=vn(),c={HOME:o.home,USER:o.name,LOGNAME:o.name,PATH:bn},a={};for(let[g,U]of Object.entries(e.env??{}))Rn(g)||(a[g]=U);let d={...a,...c},l=Date.now(),{cmd:p,args:m}=tt([String(o.uid),String(o.gid),o.name,s,...r.slice(1)]),u=hn(p,m,{cwd:e.cwd??o.home,env:d,stdio:["pipe","pipe","pipe"],detached:!0,shell:!1});i&&u.stdin?(u.stdin.on("error",()=>{}),u.stdin.end(i)):u.stdin?.end();let f=!1,S=null,A=()=>{S&&(clearTimeout(S),S=null)};u.stdout?.on("data",g=>{t.onOutput(g.toString("base64"),"stdout")}),u.stderr?.on("data",g=>{t.onOutput(g.toString("base64"),"stderr")}),u.on("close",g=>{f=!0,A(),t.onDone(g??-1,Date.now()-l)}),u.on("error",g=>{f=!0,A(),t.onError(g.message)});let T=g=>{if(!(f||u.pid==null))try{process.kill(-u.pid,g)}catch{}};return{kill:()=>{f||(T("SIGTERM"),A(),S=setTimeout(()=>{S=null,T("SIGKILL")},5e3),S.unref?.())}}}import w from"chalk";function st(e,t,r=!1){console.clear(),console.log(),console.log(w.bold.cyan(" AI Commander \u2014 Remote Agent")),console.log(),console.log(w.gray(" Session code:")),console.log(),console.log(w.bold.greenBright(` ${r?e:j(e)}`)),console.log(),r||console.log(w.gray(" Reveal the full code: sudo aicommander-agent status --reveal")),console.log(w.gray(" Then give it to your admin to connect.")),console.log(w.yellow(" \u26A0 Keep this code secret \u2014 anyone who has it can run commands as root here.")),console.log(w.gray(` Server: ${t}`)),console.log(w.gray(" Press Ctrl+C to disconnect.")),console.log(),console.log(w.yellow(" Waiting for admin connection...")),console.log()}function it(e,t){console.log(w.yellow(` Reconnecting in ${Math.round(t/1e3)}s (attempt ${e})...`))}function at(){console.log(w.green(" Connected to relay \u2014 ready for commands."))}function xe(e){console.log(w.cyan(` > ${e}`))}function _e(e,t){let r=e===0?w.green:w.red;console.log(r(` Done. Exit ${e} in ${t}ms`))}function An(){let e=process.env.AICOMMANDER_DEV;return e==="1"||e==="true"}function lt(e){let t=e.toLowerCase();return t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="[::1]"}function ct(){return new URL(I).origin}function dt(e){let t=(e??"").trim();if(!t)return I;let r;try{r=new URL(t)}catch{return console.warn(`
6
+ \u26A0 Ignoring malformed AICOMMANDER_SERVER (${t}); using ${I}.
7
+ `),I}if(r.origin===ct())return I;let s=lt(r.hostname);return!An()&&!s?(console.warn(`
8
+ \u26A0 AICOMMANDER_SERVER override to "${r.origin}" ignored for safety \u2014
9
+ the agent runs as root and is host-locked to ${ct()}.
10
+ Set AICOMMANDER_DEV=1 to allow a custom relay for development.
11
+ `),I):r.protocol!=="https:"&&!s?(console.warn(`
12
+ \u26A0 Refusing plaintext relay "${r.origin}" (https required); using ${I}.
13
+ `),I):t.replace(/\/+$/,"")}function ut(e){let t;try{t=new URL(e)}catch{throw new Error("Refusing to connect: malformed relay WebSocket URL.")}if(t.protocol!=="wss:"&&!(t.protocol==="ws:"&&lt(t.hostname)))throw new Error(`Refusing to open a plaintext relay socket (${t.protocol}//${t.host}); wss:// required.`)}var ke={capable:!1,enabled:!1,expiresAt:null},pt=360*60*1e3;function _n(e,t){return new Promise((r,s)=>{if(t.aborted){s(new DOMException("Aborted","AbortError"));return}let i=setTimeout(r,e);t.addEventListener("abort",()=>{clearTimeout(i),s(new DOMException("Aborted","AbortError"))},{once:!0})})}async function gt(e){let t=1e3,r=3e4,s=0,i=e.agentToken;for(;!e.signal?.aborted;){s++;try{let o=await kn(e,i);if(t=1e3,s=0,o&&e.reauth&&!e.signal?.aborted)try{i=await e.reauth()}catch{}}catch(o){if(o.name==="AbortError")break;let c=.8+Math.random()*.4,a=Math.min(t*c,r);e.silent||it(s,a);try{await _n(a,e.signal??new AbortController().signal)}catch(d){if(d.name==="AbortError")break}t=Math.min(t*2,r)}}}async function kn(e,t){let{serverUrl:r,sessionCode:s}=e,i=`${r.replace(/^http/,"ws")}/ws/agent/${s}?token=${t}`;ut(i);let o=l=>{try{e.onStatus?.(l)}catch{}},c=l=>{try{e.onActivity?.(l)}catch{}},a=l=>{if(l)try{e.onRemoteActivity?.(l)}catch{}},d=()=>{try{e.onHeartbeat?.()}catch{}};return o("connecting"),new Promise((l,p)=>{let m=new Tn(i),u=null,f=null,S=null,A=!1,T=null,g=!1,U=()=>{S&&(clearTimeout(S),S=null)},y=()=>{T&&(clearInterval(T),T=null)},V=()=>{if(!e.reauthIntervalMs||!e.reauth)return;let E=()=>{if(u){S=setTimeout(E,3e4);return}A=!0,m.close(1e3,"reauth")};S=setTimeout(E,e.reauthIntervalMs)},Z=()=>{g||(g=!0,U(),y(),l(A))},Q=E=>{g||(g=!0,U(),y(),p(E))},Xe=()=>{d(),f&&clearTimeout(f),f=setTimeout(()=>{m.terminate(),Q(new Error("Ping timeout"))},9e4)};e.signal?.addEventListener("abort",()=>m.close(1e3,"disabled"),{once:!0});let ye=()=>{if(g)return;let E={type:"agent:screen_state",screenShare:e.screenShare?.getState()??ke};try{m.send(JSON.stringify(E))}catch{}};m.on("open",()=>{o("connected"),Xe(),V();let E={type:"agent:register",hostname:xn.hostname(),platform:process.platform,arch:process.arch,agentVersion:O,screenShare:e.screenShare?.getState()??ke};m.send(JSON.stringify(E)),e.screenShare?.on("change",ye),e.silent||at()}),m.on("message",E=>{let R;try{R=JSON.parse(String(E))}catch{return}switch(R.type){case"do:ping":Xe();let sn={type:"agent:pong",ts:R.ts};m.send(JSON.stringify(sn));break;case"do:exec":{xe(R.command),c(!0),a(R.operator);let{commandId:D,command:W,cwd:Ee,env:ve}=R,oe=Date.now(),z=!1;y(),T=setInterval(()=>{let x=Date.now()-oe;if(x>=3e5&&!z){z=!0;let h={type:"agent:exec_idle",commandId:D,idleMs:x};m.send(JSON.stringify(h))}},3e5),T.unref?.(),u=ze(W,Ee,ve,{onOutput:(x,h)=>{oe=Date.now(),z=!1;let k={type:"agent:output",commandId:D,chunk:x,stream:h};m.send(JSON.stringify(k))},onDone:(x,h)=>{u=null,y(),c(!1),_e(x,h);let k={type:"agent:done",commandId:D,exitCode:x,durationMs:h};m.send(JSON.stringify(k))},onError:x=>{u=null,y(),c(!1);let h={type:"agent:error",commandId:D,error:x};m.send(JSON.stringify(h))}}).kill;break}case"do:secure_exec":{let{commandId:D,argv:W,allowedCommands:Ee,input:ve,cwd:oe,env:z}=R,Ye=(h,k)=>{u=null,y(),c(!1),_e(h,k);let ee={type:"agent:done",commandId:D,exitCode:h,durationMs:k};m.send(JSON.stringify(ee))},x=h=>{u=null,y(),c(!1);let k={type:"agent:error",commandId:D,error:h};m.send(JSON.stringify(k))};try{let h=Array.isArray(W)?W.join(" "):String(W);xe(h),c(!0);let k=Date.now(),ee=!1;y(),T=setInterval(()=>{let se=Date.now()-k;if(se>=3e5&&!ee){ee=!0;let be={type:"agent:exec_idle",commandId:D,idleMs:se};m.send(JSON.stringify(be))}},3e5),T.unref?.(),u=ot({argv:W,allowedCommands:Ee,input:ve,cwd:oe,env:z},{onOutput:(se,be)=>{k=Date.now(),ee=!1;let an={type:"agent:output",commandId:D,chunk:se,stream:be};m.send(JSON.stringify(an))},onDone:Ye,onError:x}).kill}catch(h){x(h instanceof Error?h.message:String(h))}break}case"do:kill":u&&(u(),u=null);break;case"do:screenshot":a(R.operator),In(m,R.requestId,e.screenShare);break}}),m.on("close",(E,R)=>{f&&clearTimeout(f),y(),e.screenShare?.off("change",ye),u&&(u(),u=null,c(!1)),o("disconnected"),E===1e3||E===4001?Z():Q(new Error(`WS closed: ${E} ${String(R)}`))}),m.on("error",E=>{f&&clearTimeout(f),y(),e.screenShare?.off("change",ye),o("disconnected"),Q(E)}),m.on("unexpected-response",(E,R)=>{p(new Error(`Unexpected HTTP response: ${R.statusCode}`))})})}async function In(e,t,r){let s=c=>{try{e.send(JSON.stringify(c))}catch{}},i=c=>s({type:"agent:screenshot_error",requestId:t,error:c}),o=r?.getState()??ke;if(!r||!o.capable){i("This machine cannot share its screen (desktop macOS/Windows only).");return}if(!o.enabled){i("Screen sharing is turned off on this machine.");return}try{let{data:c,mimeType:a}=await r.capture();if(c.length>10485760){let l=(c.length/1048576).toFixed(1);i(`Screenshot is ${l} MB, which exceeds the ${10485760/(1024*1024)} MB limit.`);return}let d=c.toString("base64");for(let l=0;l<d.length;l+=524288)s({type:"agent:screenshot_chunk",requestId:t,chunk:d.slice(l,l+524288)});s({type:"agent:screenshot_done",requestId:t,mimeType:a,totalBytes:c.length})}catch(c){i(`Screenshot failed: ${c.message}`)}}import N from"node:fs/promises";import yt from"node:path";var ae="/var/run/aicommander-agent",ce=yt.join(ae,"state.json"),St=448,ht=384;async function Et(e){let t=null;try{await N.mkdir(ae,{recursive:!0,mode:St}),await N.chmod(ae,St),t=yt.join(ae,`.state.${process.pid}.${Date.now()}.tmp`);let r=await N.open(t,"wx",ht);try{await r.writeFile(JSON.stringify(e,null,2),"utf8")}finally{await r.close()}await N.rename(t,ce),t=null,await N.chmod(ce,ht)}catch{}finally{t&&await N.rm(t,{force:!0}).catch(()=>{})}}async function vt(){try{await N.rm(ce,{force:!0})}catch{}}async function C(){try{let e=await N.readFile(ce,"utf8");return JSON.parse(e)}catch{return null}}import De from"node:fs";import te from"node:path";import Cn from"node:os";import{randomUUID as Dn,randomBytes as On}from"node:crypto";var bt="/etc/aicommander-agent",wt=te.join(Cn.homedir(),".config","aicommander-agent"),le="device.json";function Rt(){return{deviceId:Dn(),deviceSecret:On(32).toString("base64url")}}function Nn(e){return typeof e=="object"&&e!==null&&typeof e.deviceId=="string"&&e.deviceId.length>0&&typeof e.deviceSecret=="string"&&e.deviceSecret.length>0}function Ie(e){try{let t=De.readFileSync(e,"utf8"),r=JSON.parse(t);if(Nn(r))return{deviceId:r.deviceId,deviceSecret:r.deviceSecret}}catch{}return null}function Ce(e,t){try{return De.mkdirSync(e,{recursive:!0,mode:448}),De.writeFileSync(te.join(e,le),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function K(e){if(e){let i=Ie(te.join(e,le));if(i)return i;let o=Rt();return Ce(e,o),o}let t=Ie(te.join(bt,le));if(t)return t;let r=Ie(te.join(wt,le));if(r)return r;let s=Rt();return Ce(bt,s)||Ce(wt,s),s}import M from"node:fs";import J from"node:path";import Mn from"node:os";var Ln="/etc/aicommander-agent",$n=J.join(Mn.homedir(),".config","aicommander-agent"),Oe="session.json",At=".rotate";function Pn(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0&&typeof e.agentToken=="string"&&e.agentToken.length>0}function Un(e){try{let t=M.readFileSync(e,"utf8"),r=JSON.parse(t);if(Pn(r))return{sessionCode:r.sessionCode,agentToken:r.agentToken}}catch{}return null}function Hn(e,t){try{return M.mkdirSync(e,{recursive:!0,mode:448}),M.writeFileSync(J.join(e,Oe),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function ne(e){return e?[e]:[Ln,$n]}function Tt(e){for(let t of ne(e)){let r=Un(J.join(t,Oe));if(r)return r}return null}function Ne(e,t){for(let r of ne(t))if(Hn(r,e))return}function xt(e){for(let t of ne(e))try{M.rmSync(J.join(t,Oe),{force:!0})}catch{}}function _t(e){for(let t of ne(e))try{M.mkdirSync(t,{recursive:!0,mode:448}),M.writeFileSync(J.join(t,At),"",{mode:384});return}catch{}}function kt(e){let t=!1;for(let r of ne(e)){let s=J.join(r,At);try{M.existsSync(s)&&(t=!0,M.rmSync(s,{force:!0}))}catch{}}return t}import{spawn as Kn}from"child_process";import de from"fs";import It from"path";import Gn from"os";function ue(e){if(e)return It.join(e,"heartbeat");if(process.platform==="linux")try{return de.mkdirSync("/run/aicommander-agent",{recursive:!0}),"/run/aicommander-agent/heartbeat"}catch{}return It.join(Gn.tmpdir(),"aicommander-agent-heartbeat")}function Ct(e){try{let t=`${e}.tmp`;de.writeFileSync(t,String(Date.now())),de.renameSync(t,e)}catch{}}var re=null;function Dt(e,t=5e3){Me(),Ct(e),re=setInterval(()=>Ct(e),t),re.unref?.()}function Me(){re&&(clearInterval(re),re=null)}function Ot(e){try{let t=de.readFileSync(e,"utf8").trim(),r=Number(t);return Number.isFinite(r)?r:null}catch{return null}}async function Mt(e={}){let t=e.heartbeatPath??ue(e.configDir),r=e.spawnWorker??(p=>Kn(process.execPath,process.argv.slice(1),{env:p,stdio:"inherit"})),s=null,i=0,o=!1,c=0,a=[],d=null,l=null;await new Promise(p=>{let m=null,u=null,f=()=>{d&&(clearInterval(d),d=null),l&&(clearTimeout(l),l=null),m&&(process.removeListener("SIGINT",m),m=null),u&&(process.removeListener("SIGTERM",u),u=null)},S=()=>{f(),p()},A=()=>{i=Date.now(),c++;let y={...process.env,AIC_ROLE:"worker",AIC_HEARTBEAT:t};s=r(y),s.on("exit",(V,Z)=>{if(s=null,o){S();return}if(e.maxSpawns!=null&&c>=e.maxSpawns){S();return}T(`worker exited (code=${V??"null"} signal=${Z??"null"})`)}),s.on("error",()=>{})},T=y=>{let V=Date.now();a=a.filter(Q=>V-Q<3e5),a.push(V);let Z=a.length>5?6e4:0;l&&clearTimeout(l),l=setTimeout(()=>{l=null,o||A()},Z),l.unref?.()},g=()=>{if(s)try{s.kill("SIGKILL")}catch{}};d=setInterval(()=>{if(o||!s||Date.now()-i<3e4)return;let y=Ot(t);y!=null&&Date.now()-y>3e4&&g()},1e4),d.unref?.();let U=y=>{if(o=!0,d&&(clearInterval(d),d=null),l&&(clearTimeout(l),l=null),s)try{s.kill(y)}catch{}else S()};m=()=>U("SIGINT"),u=()=>U("SIGTERM"),process.on("SIGINT",m),process.on("SIGTERM",u),A()})}function Jn(e,t){let r=e.split(".").map(o=>parseInt(o,10)||0),s=t.split(".").map(o=>parseInt(o,10)||0),i=Math.max(r.length,s.length);for(let o=0;o<i;o++){let c=(r[o]??0)-(s[o]??0);if(c!==0)return c>0?1:-1}return 0}function Lt(e,t){return Jn(e,t)>0}async function $t(e,t=1e4){let r=new AbortController,s=setTimeout(()=>r.abort(),t);try{let i=await fetch(`${e.replace(/\/+$/,"")}/dist/latest`,{signal:r.signal,headers:{Accept:"application/json"}});if(!i.ok)return null;let o=await i.json();return{version:typeof o.version=="string"?o.version:null,mac:typeof o.mac=="string"?o.mac:null,win:typeof o.win=="string"?o.win:null,agentLinuxX64:typeof o.agentLinuxX64=="string"?o.agentLinuxX64:null,agentLinuxArm64:typeof o.agentLinuxArm64=="string"?o.agentLinuxArm64:null}}catch{return null}finally{clearTimeout(s)}}var H=dt(process.env.AICOMMANDER_SERVER),Xn=1440*60*1e3;async function Pt(){let e=await $t(H);e?.version&&Lt(e.version,O)&&console.warn(`
14
+ \u2B06 A newer AI Commander agent is available (v${e.version}; you have v${O}).
15
+ Update: curl -fsSL ${H}/install | sudo bash
16
+ `)}async function Ut(){process.env.AIC_ROLE==="worker"?await Yn():await Mt()}async function Yn(){let e=process.env.AIC_ROLE==="worker";e&&Dt(process.env.AIC_HEARTBEAT||ue());let t=()=>{e&&Me(),vt(),process.exit(0)};process.on("SIGINT",t),process.on("SIGTERM",t),typeof process.getuid=="function"&&process.getuid()!==0&&console.warn(`
6
17
  \u26A0 Warning: not running as root.
7
18
  Commands requiring sudo (apt, systemctl, brew cask\u2026) will fail.
8
19
  Recommended: sudo aicommander-agent
9
- `);let o=F(),s=at(),i=s?null:rt(),r,c;try{let l=await pe(M,o,{forceNew:s,...i?{currentCode:i.sessionCode}:{}});r=l.sessionCode,c=l.agentToken,Ee({sessionCode:r,agentToken:c})}catch(l){console.error(` Failed to register: ${String(l)}`),process.exit(1)}Ue(r,M,!0),Qe({sessionCode:r,pid:process.pid,startedAt:new Date().toISOString(),serverUrl:M}),St(),setInterval(()=>{St()},Sn).unref?.(),await Xe({serverUrl:M,sessionCode:r,agentToken:c,reauthIntervalMs:Je,reauth:async()=>{let l=await pe(M,o,{currentCode:r});return Ee({sessionCode:l.sessionCode,agentToken:l.agentToken}),l.agentToken}})}import ae from"node:fs";import bt from"chalk";import{execFileSync as Ae}from"node:child_process";var x="aicommander-agent";function L(e){Ae("systemctl",e,{stdio:"inherit"})}function B(){try{return Ae("systemctl",["is-active",x],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return"inactive"}}function yt(){try{return Ae("systemctl",["is-enabled",x],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()==="enabled"}catch{return!1}}function oe(){L(["start",x])}function W(){L(["stop",x])}function re(){L(["enable",x])}function se(){L(["disable",x])}function V(){L(["restart",x])}function ie(){L(["daemon-reload"])}function vt(){L(["kill","--signal=SIGKILL",x])}import _ from"chalk";import yn from"node:readline";var n={header:e=>console.log(_.bold.cyan(`
20
+ `);let r=K(),s=kt(),i=s?null:Tt(),o,c;try{let d=await Re(H,r,{forceNew:s,...i?{currentCode:i.sessionCode}:{}});o=d.sessionCode,c=d.agentToken,Ne({sessionCode:o,agentToken:c})}catch(d){console.error(` Failed to register: ${String(d)}`),process.exit(1)}st(o,H,!0),Et({sessionCode:o,pid:process.pid,startedAt:new Date().toISOString(),serverUrl:H}),Pt(),setInterval(()=>{Pt()},Xn).unref?.(),await gt({serverUrl:H,sessionCode:o,agentToken:c,reauthIntervalMs:pt,reauth:async()=>{let d=await Re(H,r,{currentCode:o});return Ne({sessionCode:d.sessionCode,agentToken:d.agentToken}),d.agentToken}})}import G from"node:fs";import{execFileSync as $e}from"node:child_process";import Bt from"chalk";import{execFileSync as Le}from"node:child_process";var L="aicommander-agent";function F(e){Le("systemctl",e,{stdio:"inherit"})}function X(){try{return Le("systemctl",["is-active",L],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return"inactive"}}function Ht(){try{return Le("systemctl",["is-enabled",L],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()==="enabled"}catch{return!1}}function me(){F(["start",L])}function Y(){F(["stop",L])}function fe(){F(["enable",L])}function pe(){F(["disable",L])}function q(){F(["restart",L])}function ge(){F(["daemon-reload"])}function Ft(){F(["kill","--signal=SIGKILL",L])}import $ from"chalk";import qn from"node:readline";var n={header:e=>console.log($.bold.cyan(`
10
21
  ${e}
11
- `)),ok:e=>console.log(_.green(` \u2713 ${e}`)),warn:e=>console.log(_.yellow(` \u26A0 ${e}`)),error:e=>console.error(_.red(` \u2717 ${e}`)),info:(e,t)=>console.log(` ${_.gray(e.padEnd(14))} ${_.white(t)}`),step:e=>console.log(_.gray(` ${e}`)),blank:()=>console.log()};function p(){process.getuid?.()!==0&&(n.error("This command must be run as root."),console.error(_.gray(" Re-run with: sudo aicommander-agent <command>")),process.exit(1))}async function Et(e){if(!process.stdin.isTTY)return!1;let t=yn.createInterface({input:process.stdin,output:process.stdout});try{let o=await new Promise(s=>t.question(` ${e} [y/N] `,s));return/^y(es)?$/i.test(o.trim())}finally{t.close()}}var At="/etc/systemd/system/aicommander-agent.service",ke="/etc/aicommander-agent";function vn(e){/[\u0000-\u001f\u007f]/.test(e)&&(n.error(`Invalid --server URL: contains control characters: ${JSON.stringify(e)}`),process.exit(1));let t;try{t=new URL(e)}catch{n.error(`Invalid --server URL: not a valid URL: ${JSON.stringify(e)}`),process.exit(1)}return t.protocol!=="http:"&&t.protocol!=="https:"&&(n.error(`Invalid --server URL: protocol must be http: or https: (got ${t.protocol})`),process.exit(1)),e.replace(/%/g,"%%")}function kt(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/%/g,"%%")}"`}async function Re(e){p(),process.platform!=="linux"&&(n.error(`The systemd service installer supports Linux only. Detected: ${process.platform}`),n.blank(),n.step("For an ephemeral foreground run on this platform, use:"),n.step(" npx @aicommander/agent (or: aicommander-agent run)"),n.step("For a production root install on Linux, the canonical path is:"),n.step(" curl -fsSL https://aicommander.dev/install | sudo bash"),process.exit(1));let t=vn(e.server??process.env.AICOMMANDER_SERVER??Z),o=ae.realpathSync(process.argv[1]),s=`${kt(process.execPath)} ${kt(o)} run`;n.header("AI Commander Agent \u2014 systemd install"),n.info("Server",t),n.info("ExecStart",s),n.blank(),n.step("Creating device identity dir\u2026"),ae.mkdirSync(ke,{recursive:!0}),ae.chmodSync(ke,448),n.ok(`Device identity dir ready (${ke}).`),n.step(`Writing systemd service to ${At}\u2026`);let i=`[Unit]
22
+ `)),ok:e=>console.log($.green(` \u2713 ${e}`)),warn:e=>console.log($.yellow(` \u26A0 ${e}`)),error:e=>console.error($.red(` \u2717 ${e}`)),info:(e,t)=>console.log(` ${$.gray(e.padEnd(14))} ${$.white(t)}`),step:e=>console.log($.gray(` ${e}`)),blank:()=>console.log()};function b(){process.getuid?.()!==0&&(n.error("This command must be run as root."),console.error($.gray(" Re-run with: sudo aicommander-agent <command>")),process.exit(1))}async function Gt(e){if(!process.stdin.isTTY)return!1;let t=qn.createInterface({input:process.stdin,output:process.stdout});try{let r=await new Promise(s=>t.question(` ${e} [y/N] `,s));return/^y(es)?$/i.test(r.trim())}finally{t.close()}}var Vt="/etc/systemd/system/aicommander-agent.service",Pe="/etc/aicommander-agent";function Zn(e){/[\u0000-\u001f\u007f]/.test(e)&&(n.error(`Invalid --server URL: contains control characters: ${JSON.stringify(e)}`),process.exit(1));let t;try{t=new URL(e)}catch{n.error(`Invalid --server URL: not a valid URL: ${JSON.stringify(e)}`),process.exit(1)}return t.protocol!=="http:"&&t.protocol!=="https:"&&(n.error(`Invalid --server URL: protocol must be http: or https: (got ${t.protocol})`),process.exit(1)),e.replace(/%/g,"%%")}function Wt(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/%/g,"%%")}"`}function Qn(){n.step(`Ensuring sandbox user "${v}"\u2026`);try{$e("getent",["passwd",v],{stdio:"ignore"}),n.ok(`Sandbox user "${v}" already exists.`);return}catch{}let e=["/usr/sbin/nologin","/sbin/nologin","/bin/false"].find(t=>G.existsSync(t))??"/bin/false";try{$e("useradd",["--system","--create-home","--shell",e,v],{stdio:"ignore"});try{let t=$e("getent",["passwd",v]).toString().trim().split(":")[5];t&&G.existsSync(t)&&G.chmodSync(t,448)}catch{}n.ok(`Sandbox user "${v}" created.`)}catch{n.warn(`Could not create sandbox user "${v}" \u2014 secure exec will be unavailable until it exists.`)}}async function Ue(e){b(),process.platform!=="linux"&&(n.error(`The systemd service installer supports Linux only. Detected: ${process.platform}`),n.blank(),n.step("For an ephemeral foreground run on this platform, use:"),n.step(" npx @aicommander/agent (or: aicommander-agent run)"),n.step("For a production root install on Linux, the canonical path is:"),n.step(" curl -fsSL https://aicommander.dev/install | sudo bash"),process.exit(1));let t=Zn(e.server??process.env.AICOMMANDER_SERVER??I),r=G.realpathSync(process.argv[1]),s=`${Wt(process.execPath)} ${Wt(r)} run`;n.header("AI Commander Agent \u2014 systemd install"),n.info("Server",t),n.info("ExecStart",s),n.blank(),n.step("Creating device identity dir\u2026"),G.mkdirSync(Pe,{recursive:!0}),G.chmodSync(Pe,448),n.ok(`Device identity dir ready (${Pe}).`),Qn(),n.step(`Writing systemd service to ${Vt}\u2026`);let i=`[Unit]
12
23
  Description=AI Commander Remote Agent
13
24
  After=network-online.target
14
25
  Wants=network-online.target
@@ -32,4 +43,4 @@ KillSignal=SIGINT
32
43
 
33
44
  [Install]
34
45
  WantedBy=multi-user.target
35
- `;ae.writeFileSync(At,i,{mode:420}),n.ok("Service file written."),n.step("Reloading systemd\u2026");try{ie()}catch{n.error("daemon-reload failed. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)}n.step("Enabling service on boot\u2026");try{re()}catch{n.warn("Could not enable the service on boot (non-critical).")}n.step("Restarting service\u2026");try{V()}catch{n.warn("Graceful restart failed \u2014 forcing stop then start\u2026");try{W()}catch{}try{vt()}catch{}try{oe()}catch{n.error("Could not start aicommander-agent. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)}}B()!=="active"&&(n.error("Service is not active after restart. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)),n.ok("Service started."),n.blank(),n.step("Waiting for session code\u2026");let r=null;for(let c=0;c<30;c++){let a=await E();if(a?.sessionCode){r=a.sessionCode;break}await new Promise(l=>setTimeout(l,500))}n.blank(),r?(n.ok(bt.bold("AI Commander ready!")),n.blank(),n.info("Session code",bt.bold.greenBright(U(r))),n.blank(),n.step("Reveal full code: sudo aicommander-agent status --reveal"),n.step("Then use in Claude: 'execute df -h on <code>'"),n.step("Service logs: journalctl -u aicommander-agent -f"),n.step("Manage: sudo aicommander-agent status"),n.step("Uninstall: sudo aicommander-agent uninstall --force"),n.blank(),n.warn("Keep this code secret. Anyone who has it can run commands as root on this machine.")):(n.warn("The session code did not appear within 15 seconds."),n.step("The service is running \u2014 the code should appear shortly."),n.step("Status: sudo aicommander-agent status"),n.step("Logs: journalctl -u aicommander-agent -f")),n.blank()}import C from"chalk";function En(e){if(e<60)return`${e}s`;if(e<3600)return`${Math.floor(e/60)}m`;let t=Math.floor(e/3600),o=Math.floor(e%3600/60);return o>0?`${t}h ${o}m`:`${t}h`}async function Te(e={}){n.header("AI Commander \u2014 Agent Status");let t;try{t=B()}catch{n.error("systemd is not available on this system."),process.exit(1)}let o=yt(),s=await E();if(n.info("State",t==="active"?C.green("running"):C.red(t)),s){e.reveal?(p(),n.info("Session code",C.bold.cyan(s.sessionCode)),n.step(C.yellow(" \u26A0 Keep this secret \u2014 anyone with it can run commands as root here."))):(n.info("Session code",C.bold.cyan(U(s.sessionCode))),n.step(C.gray(" (run with --reveal to show the full code)"))),n.info("PID",String(s.pid));let i=new Date(s.startedAt),r=Math.floor((Date.now()-i.getTime())/1e3);n.info("Started",`${s.startedAt} (${En(r)} ago)`),n.info("Server",s.serverUrl)}else t==="active"&&n.warn("Session code not yet available (agent still starting)");n.info("Enabled",o?C.green("yes"):C.yellow("no")),n.blank()}function we(){p(),B()==="active"?(n.warn("Service is already running \u2014 restarting to load the current binary."),V()):oe(),re(),n.ok("Service started and enabled to run on boot."),n.blank()}function Ie(){p(),W(),se(),n.ok("Service stopped and disabled."),n.blank()}import bn from"chalk";async function xe(e={}){if(p(),!e.yes){if(!process.stdin.isTTY){n.error("Refusing to reset the code non-interactively."),n.info("To proceed","re-run with --yes (this unlinks ALL linked accounts)"),process.exitCode=1;return}if(!await Et("Resetting the access code will UNLINK ALL linked accounts. Continue?")){n.info("Cancelled","no changes made");return}}n.step("Restarting agent to get a new session code\u2026"),it(),st(),V();let t=Date.now()+1e4,o=null;for(;Date.now()<t&&(await new Promise(s=>setTimeout(s,500)),o=await E(),!o););o?n.ok(`New session code: ${bn.bold.greenBright(o.sessionCode)}`):(n.warn("Service restarted. Session code not yet available."),n.info("Check","aicommander-agent status")),n.blank()}import _e from"node:fs";import An from"chalk";var Rt="/etc/systemd/system/aicommander-agent.service",Tt="/usr/local/bin/aicommander-agent",wt="/usr/local/bin/aicommander-ctl",It="/var/run/aicommander-agent",xt="/etc/aicommander-agent",_t="/etc/aicommander-agent/session.json",Ct="/etc/aicommander-agent/.rotate";function P(e,t){try{if(_e.existsSync(e)||_e.lstatSync(e)){_e.rmSync(e,{recursive:!0,force:!0}),n.ok(`${t} removed.`);return}}catch{}n.warn(`${t} not found (already removed?).`)}function Ce(e){p(),e.force||(n.warn("This will fully remove the AI Commander agent."),n.error("Add --force to confirm: sudo aicommander-agent uninstall --force"),process.exit(1)),n.step("Stopping and disabling service\u2026");try{W()}catch{}try{se()}catch{}n.ok("Service stopped."),n.step("Removing service file\u2026"),P(Rt,Rt),n.step("Reloading systemd\u2026");try{ie(),n.ok("daemon-reload complete.")}catch{n.warn("daemon-reload failed (non-critical).")}n.step("Removing agent binary\u2026"),P(wt,wt),P(Tt,Tt),P(It,It),P(_t,_t),P(Ct,Ct),P(xt,xt),n.blank(),n.ok(An.bold("AI Commander agent fully removed.")),n.blank()}function Dt(e){return{"X-Device-Id":e.deviceId,"X-Device-Secret":e.deviceSecret}}async function ce(e,t){let o=await fetch(`${e}/api/device/admins`,{headers:Dt(t)});if(!o.ok)throw new Error(`Failed to list linked accounts (HTTP ${o.status}).`);return await o.json()}async function Ot(e,t,o,s){let i=await fetch(`${e}/api/device/${o}`,{method:"POST",headers:{...Dt(t),"Content-Type":"application/json"},body:JSON.stringify({userId:s})}),r;try{r=await i.json()}catch{r={ok:!1,error:`http_${i.status}`}}return{...r,ok:i.ok}}function Nt(e,t,o){return Ot(e,t,"block",o)}function Mt(e,t,o){return Ot(e,t,"unblock",o)}function le(e){return[...e.filter(t=>!t.blocked),...e.filter(t=>t.blocked)]}function Lt(e,t){let o=t.trim();if(!o)return{kind:"not_found"};if(/^\d+$/.test(o)){let r=Number(o)-1;return r>=0&&r<e.length?{kind:"ok",admin:e[r]}:{kind:"not_found"}}let s=e.find(r=>r.userId===o);if(s)return{kind:"ok",admin:s};let i=e.filter(r=>r.userId.startsWith(o));return i.length===1?{kind:"ok",admin:i[0]}:i.length>1?{kind:"ambiguous",matches:i}:{kind:"not_found"}}async function kn(){return(await E())?.serverUrl??process.env.AICOMMANDER_SERVER??"https://aicommander.dev"}async function De(){p();let e=await kn(),t=F(),o;try{o=await ce(e,t)}catch(a){n.error(a.message),process.exitCode=1;return}if(o.admins.length===0){n.header("Linked accounts"),n.info("None","no accounts are linked to this device"),n.blank();return}let s=le(o.admins),i=(a,l)=>{let m=String(l+1).padStart(2," "),d=a.linkedAt.slice(0,10);n.info(`${m}. ${a.maskedEmail}`,`${a.alias} \xB7 linked ${d} \xB7 id ${a.userId.slice(0,8)}\u2026`)},r=s.filter(a=>!a.blocked),c=s.filter(a=>a.blocked);n.header("Linked accounts"),r.forEach(a=>i(a,s.indexOf(a))),r.length===0&&n.info("None active","every linked account is blocked"),n.blank(),c.length>0&&(n.header("Blocked"),c.forEach(a=>i(a,s.indexOf(a))),n.blank()),n.step("Block one with: aicommander-agent block-admin <number|id>"),c.length>0&&n.step("Unblock one with: aicommander-agent unblock-admin <number|id>"),n.blank()}async function Rn(){return(await E())?.serverUrl??process.env.AICOMMANDER_SERVER??"https://aicommander.dev"}async function Pt(e,t){p();let o=await Rn(),s=F(),i;try{i=await ce(o,s)}catch(m){n.error(m.message),process.exitCode=1;return}let r=le(i.admins),c=Lt(r,e);if(c.kind==="not_found"){n.error(`No linked account matches "${e}".`),n.info("See options","aicommander-agent list-admins"),process.exitCode=1;return}if(c.kind==="ambiguous"){n.error(`"${e}" is ambiguous \u2014 it matches ${c.matches.length} accounts.`),n.info("Disambiguate","use the list number or a longer id"),process.exitCode=1;return}let a=c.admin,l=t==="block"?await Nt(o,s,a.userId):await Mt(o,s,a.userId);l.ok?t==="block"?(n.ok(`Blocked ${a.maskedEmail} (${a.alias}).`),n.info("Note","their access is cut off; run unblock-admin anytime to restore it")):n.ok(`Unblocked ${a.maskedEmail} (${a.alias}) \u2014 access restored.`):l.error==="not_found"?n.warn(t==="block"?"That account isn't linked (or is already blocked).":"That account isn't blocked."):(n.error(l.message??`Failed to ${t} the account.`),process.exitCode=1),n.blank()}function Oe(e){return Pt(e,"block")}function Ne(e){return Pt(e,"unblock")}h.name("aicommander-agent").description("AI Commander remote agent + service controller").version(k);h.command("run",{isDefault:!0}).description("Run the agent: register with the relay and listen for commands").action(async()=>{await ht()});h.command("install").description("Install and start the agent as a systemd service (Linux, root)").option("--server <url>","Relay server URL baked into the service unit").action(async e=>{await Re(e)});h.command("status").description("Show agent status, session code, and uptime").option("--reveal","Show the full session code (otherwise masked)").action(async e=>{await Te(e)});h.command("enable").description("Start and enable the agent service").action(()=>{we()});h.command("disable").description("Stop and disable the agent service").action(()=>{Ie()});h.command("change-code").aliases(["reset-code"]).description("Reset the access code \u2014 mint a new code and remove ALL access (unlinks every linked account)").option("-y, --yes","Skip the confirmation prompt").action(async e=>{await xe(e)});h.command("list-admins").description("List the accounts (admins) linked to this device").action(async()=>{await De()});h.command("block-admin <account>").aliases(["revoke-admin"]).description("Block one account on this device (by list number or id) \u2014 refuses access; unblock anytime").action(async e=>{await Oe(e)});h.command("unblock-admin <account>").description("Restore a blocked account's access (by list number or id)").action(async e=>{await Ne(e)});h.command("uninstall").description("Fully remove the agent (stop, disable, delete files)").option("-f, --force","Skip confirmation requirement").action(e=>{Ce(e)});h.parseAsync().catch(e=>{console.error("Fatal error:",e),process.exit(1)});
46
+ `;G.writeFileSync(Vt,i,{mode:420}),n.ok("Service file written."),n.step("Reloading systemd\u2026");try{ge()}catch{n.error("daemon-reload failed. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)}n.step("Enabling service on boot\u2026");try{fe()}catch{n.warn("Could not enable the service on boot (non-critical).")}n.step("Restarting service\u2026");try{q()}catch{n.warn("Graceful restart failed \u2014 forcing stop then start\u2026");try{Y()}catch{}try{Ft()}catch{}try{me()}catch{n.error("Could not start aicommander-agent. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)}}X()!=="active"&&(n.error("Service is not active after restart. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)),n.ok("Service started."),n.blank(),n.step("Waiting for session code\u2026");let o=null;for(let c=0;c<30;c++){let a=await C();if(a?.sessionCode){o=a.sessionCode;break}await new Promise(d=>setTimeout(d,500))}n.blank(),o?(n.ok(Bt.bold("AI Commander ready!")),n.blank(),n.info("Session code",Bt.bold.greenBright(j(o))),n.blank(),n.step("Reveal full code: sudo aicommander-agent status --reveal"),n.step("Then use in Claude: 'execute df -h on <code>'"),n.step("Service logs: journalctl -u aicommander-agent -f"),n.step("Manage: sudo aicommander-agent status"),n.step("Uninstall: sudo aicommander-agent uninstall --force"),n.blank(),n.warn("Keep this code secret. Anyone who has it can run commands as root on this machine.")):(n.warn("The session code did not appear within 15 seconds."),n.step("The service is running \u2014 the code should appear shortly."),n.step("Status: sudo aicommander-agent status"),n.step("Logs: journalctl -u aicommander-agent -f")),n.blank()}import P from"chalk";function zn(e){if(e<60)return`${e}s`;if(e<3600)return`${Math.floor(e/60)}m`;let t=Math.floor(e/3600),r=Math.floor(e%3600/60);return r>0?`${t}h ${r}m`:`${t}h`}async function He(e={}){n.header("AI Commander \u2014 Agent Status");let t;try{t=X()}catch{n.error("systemd is not available on this system."),process.exit(1)}let r=Ht(),s=await C();if(n.info("State",t==="active"?P.green("running"):P.red(t)),s){e.reveal?(b(),n.info("Session code",P.bold.cyan(s.sessionCode)),n.step(P.yellow(" \u26A0 Keep this secret \u2014 anyone with it can run commands as root here."))):(n.info("Session code",P.bold.cyan(j(s.sessionCode))),n.step(P.gray(" (run with --reveal to show the full code)"))),n.info("PID",String(s.pid));let i=new Date(s.startedAt),o=Math.floor((Date.now()-i.getTime())/1e3);n.info("Started",`${s.startedAt} (${zn(o)} ago)`),n.info("Server",s.serverUrl)}else t==="active"&&n.warn("Session code not yet available (agent still starting)");n.info("Enabled",r?P.green("yes"):P.yellow("no")),n.blank()}function Fe(){b(),X()==="active"?(n.warn("Service is already running \u2014 restarting to load the current binary."),q()):me(),fe(),n.ok("Service started and enabled to run on boot."),n.blank()}function Ge(){b(),Y(),pe(),n.ok("Service stopped and disabled."),n.blank()}import er from"chalk";async function Be(e={}){if(b(),!e.yes){if(!process.stdin.isTTY){n.error("Refusing to reset the code non-interactively."),n.info("To proceed","re-run with --yes (this unlinks ALL linked accounts)"),process.exitCode=1;return}if(!await Gt("Resetting the access code will UNLINK ALL linked accounts. Continue?")){n.info("Cancelled","no changes made");return}}n.step("Restarting agent to get a new session code\u2026"),_t(),xt(),q();let t=Date.now()+1e4,r=null;for(;Date.now()<t&&(await new Promise(s=>setTimeout(s,500)),r=await C(),!r););r?n.ok(`New session code: ${er.bold.greenBright(r.sessionCode)}`):(n.warn("Service restarted. Session code not yet available."),n.info("Check","aicommander-agent status")),n.blank()}import Ve from"node:fs";import tr from"chalk";var jt="/etc/systemd/system/aicommander-agent.service",Kt="/usr/local/bin/aicommander-agent",Jt="/usr/local/bin/aicommander-ctl",Xt="/var/run/aicommander-agent",Yt="/etc/aicommander-agent",qt="/etc/aicommander-agent/session.json",Zt="/etc/aicommander-agent/.rotate";function B(e,t){try{if(Ve.existsSync(e)||Ve.lstatSync(e)){Ve.rmSync(e,{recursive:!0,force:!0}),n.ok(`${t} removed.`);return}}catch{}n.warn(`${t} not found (already removed?).`)}function We(e){b(),e.force||(n.warn("This will fully remove the AI Commander agent."),n.error("Add --force to confirm: sudo aicommander-agent uninstall --force"),process.exit(1)),n.step("Stopping and disabling service\u2026");try{Y()}catch{}try{pe()}catch{}n.ok("Service stopped."),n.step("Removing service file\u2026"),B(jt,jt),n.step("Reloading systemd\u2026");try{ge(),n.ok("daemon-reload complete.")}catch{n.warn("daemon-reload failed (non-critical).")}n.step("Removing agent binary\u2026"),B(Jt,Jt),B(Kt,Kt),B(Xt,Xt),B(qt,qt),B(Zt,Zt),B(Yt,Yt),n.blank(),n.ok(tr.bold("AI Commander agent fully removed.")),n.blank()}function Qt(e){return{"X-Device-Id":e.deviceId,"X-Device-Secret":e.deviceSecret}}var zt=1e4;async function Se(e,t){let r;try{r=await fetch(`${e}/api/device/admins`,{headers:Qt(t),signal:AbortSignal.timeout(zt)})}catch{throw new Error("Failed to list linked accounts (relay unreachable or timed out).")}if(!r.ok)throw new Error(`Failed to list linked accounts (HTTP ${r.status}).`);return await r.json()}async function en(e,t,r,s){let i;try{i=await fetch(`${e}/api/device/${r}`,{method:"POST",headers:{...Qt(t),"Content-Type":"application/json"},body:JSON.stringify({userId:s}),signal:AbortSignal.timeout(zt)})}catch{return{ok:!1,error:"unreachable"}}let o;try{o=await i.json()}catch{o={ok:!1,error:`http_${i.status}`}}return{...o,ok:i.ok}}function tn(e,t,r){return en(e,t,"block",r)}function nn(e,t,r){return en(e,t,"unblock",r)}function he(e){return[...e.filter(t=>!t.blocked),...e.filter(t=>t.blocked)]}function rn(e,t){let r=t.trim();if(!r)return{kind:"not_found"};if(/^\d+$/.test(r)){let o=Number(r)-1;return o>=0&&o<e.length?{kind:"ok",admin:e[o]}:{kind:"not_found"}}let s=e.find(o=>o.userId===r);if(s)return{kind:"ok",admin:s};let i=e.filter(o=>o.userId.startsWith(r));return i.length===1?{kind:"ok",admin:i[0]}:i.length>1?{kind:"ambiguous",matches:i}:{kind:"not_found"}}async function nr(){return(await C())?.serverUrl??process.env.AICOMMANDER_SERVER??"https://aicommander.dev"}async function je(){b();let e=await nr(),t=K(),r;try{r=await Se(e,t)}catch(a){n.error(a.message),process.exitCode=1;return}if(r.admins.length===0){n.header("Linked accounts"),n.info("None","no accounts are linked to this device"),n.blank();return}let s=he(r.admins),i=(a,d)=>{let l=String(d+1).padStart(2," "),p=a.linkedAt.slice(0,10);n.info(`${l}. ${a.maskedEmail}`,`${a.alias} \xB7 linked ${p} \xB7 id ${a.userId.slice(0,8)}\u2026`)},o=s.filter(a=>!a.blocked),c=s.filter(a=>a.blocked);n.header("Linked accounts"),o.forEach(a=>i(a,s.indexOf(a))),o.length===0&&n.info("None active","every linked account is blocked"),n.blank(),c.length>0&&(n.header("Blocked"),c.forEach(a=>i(a,s.indexOf(a))),n.blank()),n.step("Block one with: aicommander-agent block-admin <number|id>"),c.length>0&&n.step("Unblock one with: aicommander-agent unblock-admin <number|id>"),n.blank()}async function rr(){return(await C())?.serverUrl??process.env.AICOMMANDER_SERVER??"https://aicommander.dev"}async function on(e,t){b();let r=await rr(),s=K(),i;try{i=await Se(r,s)}catch(l){n.error(l.message),process.exitCode=1;return}let o=he(i.admins),c=rn(o,e);if(c.kind==="not_found"){n.error(`No linked account matches "${e}".`),n.info("See options","aicommander-agent list-admins"),process.exitCode=1;return}if(c.kind==="ambiguous"){n.error(`"${e}" is ambiguous \u2014 it matches ${c.matches.length} accounts.`),n.info("Disambiguate","use the list number or a longer id"),process.exitCode=1;return}let a=c.admin,d=t==="block"?await tn(r,s,a.userId):await nn(r,s,a.userId);d.ok?t==="block"?(n.ok(`Blocked ${a.maskedEmail} (${a.alias}).`),n.info("Note","their access is cut off; run unblock-admin anytime to restore it")):n.ok(`Unblocked ${a.maskedEmail} (${a.alias}) \u2014 access restored.`):d.error==="not_found"?n.warn(t==="block"?"That account isn't linked (or is already blocked).":"That account isn't blocked."):(n.error(d.message??`Failed to ${t} the account.`),process.exitCode=1),n.blank()}function Ke(e){return on(e,"block")}function Je(e){return on(e,"unblock")}nt(process.argv)||(_.name("aicommander-agent").description("AI Commander remote agent + service controller").version(O),_.command("run",{isDefault:!0}).description("Run the agent: register with the relay and listen for commands").action(async()=>{await Ut()}),_.command("install").description("Install and start the agent as a systemd service (Linux, root)").option("--server <url>","Relay server URL baked into the service unit").action(async e=>{await Ue(e)}),_.command("status").description("Show agent status, session code, and uptime").option("--reveal","Show the full session code (otherwise masked)").action(async e=>{await He(e)}),_.command("enable").description("Start and enable the agent service").action(()=>{Fe()}),_.command("disable").description("Stop and disable the agent service").action(()=>{Ge()}),_.command("change-code").aliases(["reset-code"]).description("Reset the access code \u2014 mint a new code and remove ALL access (unlinks every linked account)").option("-y, --yes","Skip the confirmation prompt").action(async e=>{await Be(e)}),_.command("list-admins").description("List the accounts (admins) linked to this device").action(async()=>{await je()}),_.command("block-admin <account>").aliases(["revoke-admin"]).description("Block one account on this device (by list number or id) \u2014 refuses access; unblock anytime").action(async e=>{await Ke(e)}),_.command("unblock-admin <account>").description("Restore a blocked account's access (by list number or id)").action(async e=>{await Je(e)}),_.command("uninstall").description("Fully remove the agent (stop, disable, delete files)").option("-f, --force","Skip confirmation requirement").action(e=>{We(e)}),_.parseAsync().catch(e=>{console.error("Fatal error:",e),process.exit(1)}));
@@ -1 +1 @@
1
- {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../bin/agent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../bin/agent.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1 +1,10 @@
1
- import je from"os";var D="1.0.19";var ue="device_secret_mismatch";function Ue(e){try{let t=JSON.parse(e);return typeof t.code=="string"?t.code:null}catch{return null}}var w=class extends Error{constructor(n,o){super(`Registration failed (${n}): ${o}`);this.status=n;this.body=o;this.name="RegistrationError",this.code=Ue(o)}status;body;code};async function F(e,t,n={}){let o={hostname:je.hostname(),platform:process.platform,arch:process.arch,agentVersion:D,...t?{deviceId:t.deviceId,deviceSecret:t.deviceSecret}:{},...n.forceNew?{forceNew:!0}:n.currentCode?{currentCode:n.currentCode}:{}},i=await fetch(`${e}/api/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});if(!i.ok){let r=await i.text();throw new w(i.status,r)}return i.json()}import Ke from"ws";import Je from"os";import{spawn as me}from"child_process";var de="ABCDEFGHJKMNPQRSTVWXYZ0123456789";var Nt=Math.floor(256/de.length)*de.length;var ge=process.platform==="win32";function fe(e,t,n,o){let i=Date.now(),r=me(e,[],{shell:!0,cwd:t??process.env.HOME,env:{...process.env,...n??{}},stdio:["ignore","pipe","pipe"],detached:!ge}),s=!1,l=null,c=()=>{l&&(clearTimeout(l),l=null)};r.stdout?.on("data",a=>{o.onOutput(a.toString("base64"),"stdout")}),r.stderr?.on("data",a=>{o.onOutput(a.toString("base64"),"stderr")}),r.on("close",a=>{s=!0,c(),o.onDone(a??-1,Date.now()-i)}),r.on("error",a=>{s=!0,c(),o.onError(a.message)});let u=a=>{if(!(s||r.pid==null))try{ge?me("taskkill",["/pid",String(r.pid),"/T","/F"],{stdio:"ignore"}):process.kill(-r.pid,a)}catch{}};return{kill:()=>{s||(u("SIGTERM"),c(),l=setTimeout(()=>{l=null,u("SIGKILL")},5e3),l.unref?.())}}}import O from"chalk";function pe(e,t){console.log(O.yellow(` Reconnecting in ${Math.round(t/1e3)}s (attempt ${e})...`))}function Se(){console.log(O.green(" Admin connected."))}function he(e){console.log(O.cyan(` > ${e}`))}function ye(e,t){let n=e===0?O.green:O.red;console.log(n(` Done. Exit ${e} in ${t}ms`))}var X={capable:!1,enabled:!1,expiresAt:null},_e=360*60*1e3;function Y(e,t){return new Promise((n,o)=>{if(t.aborted){o(new DOMException("Aborted","AbortError"));return}let i=setTimeout(n,e);t.addEventListener("abort",()=>{clearTimeout(i),o(new DOMException("Aborted","AbortError"))},{once:!0})})}async function Z(e){let t=1e3,n=3e4,o=0,i=e.agentToken;for(;!e.signal?.aborted;){o++;try{let r=await Xe(e,i);if(t=1e3,o=0,r&&e.reauth&&!e.signal?.aborted)try{i=await e.reauth()}catch{}}catch(r){if(r.name==="AbortError")break;let s=.8+Math.random()*.4,l=Math.min(t*s,n);e.silent||pe(o,l);try{await Y(l,e.signal??new AbortController().signal)}catch(c){if(c.name==="AbortError")break}t=Math.min(t*2,n)}}}async function Xe(e,t){let{serverUrl:n,sessionCode:o}=e,i=`${n.replace(/^http/,"ws")}/ws/agent/${o}?token=${t}`,r=c=>{try{e.onStatus?.(c)}catch{}},s=c=>{try{e.onActivity?.(c)}catch{}},l=()=>{try{e.onHeartbeat?.()}catch{}};return r("connecting"),new Promise((c,u)=>{let a=new Ke(i),m=null,g=null,_=null,T=!1,y=null,E=!1,L=()=>{_&&(clearTimeout(_),_=null)},f=()=>{y&&(clearInterval(y),y=null)},S=()=>{if(!e.reauthIntervalMs||!e.reauth)return;let d=()=>{if(m){_=setTimeout(d,3e4);return}T=!0,a.close(1e3,"reauth")};_=setTimeout(d,e.reauthIntervalMs)},C=()=>{E||(E=!0,L(),f(),c(T))},b=d=>{E||(E=!0,L(),f(),u(d))},$=()=>{l(),g&&clearTimeout(g),g=setTimeout(()=>{a.terminate(),b(new Error("Ping timeout"))},9e4)};e.signal?.addEventListener("abort",()=>a.close(1e3,"disabled"),{once:!0});let V=()=>{if(E)return;let d={type:"agent:screen_state",screenShare:e.screenShare?.getState()??X};try{a.send(JSON.stringify(d))}catch{}};a.on("open",()=>{r("connected"),$(),S();let d={type:"agent:register",hostname:Je.hostname(),platform:process.platform,arch:process.arch,agentVersion:D,screenShare:e.screenShare?.getState()??X};a.send(JSON.stringify(d)),e.screenShare?.on("change",V),e.silent||Se()}),a.on("message",d=>{let p;try{p=JSON.parse(String(d))}catch{return}switch(p.type){case"do:ping":$();let Be={type:"agent:pong",ts:p.ts};a.send(JSON.stringify(Be));break;case"do:exec":{he(p.command),s(!0);let{commandId:B,command:Fe,cwd:Ge,env:We}=p,le=Date.now(),K=!1;f(),y=setInterval(()=>{let h=Date.now()-le;if(h>=3e5&&!K){K=!0;let v={type:"agent:exec_idle",commandId:B,idleMs:h};a.send(JSON.stringify(v))}},3e5),y.unref?.(),m=fe(Fe,Ge,We,{onOutput:(h,v)=>{le=Date.now(),K=!1;let J={type:"agent:output",commandId:B,chunk:h,stream:v};a.send(JSON.stringify(J))},onDone:(h,v)=>{m=null,f(),s(!1),ye(h,v);let J={type:"agent:done",commandId:B,exitCode:h,durationMs:v};a.send(JSON.stringify(J))},onError:h=>{m=null,f(),s(!1);let v={type:"agent:error",commandId:B,error:h};a.send(JSON.stringify(v))}}).kill;break}case"do:kill":m&&(m(),m=null);break;case"do:screenshot":Ye(a,p.requestId,e.screenShare);break}}),a.on("close",(d,p)=>{g&&clearTimeout(g),f(),e.screenShare?.off("change",V),m&&(m(),m=null,s(!1)),r("disconnected"),d===1e3||d===4001?C():b(new Error(`WS closed: ${d} ${String(p)}`))}),a.on("error",d=>{g&&clearTimeout(g),f(),e.screenShare?.off("change",V),r("disconnected"),b(d)}),a.on("unexpected-response",(d,p)=>{u(new Error(`Unexpected HTTP response: ${p.statusCode}`))})})}async function Ye(e,t,n){let o=s=>{try{e.send(JSON.stringify(s))}catch{}},i=s=>o({type:"agent:screenshot_error",requestId:t,error:s}),r=n?.getState()??X;if(!n||!r.capable){i("This machine cannot share its screen (desktop macOS/Windows only).");return}if(!r.enabled){i("Screen sharing is turned off on this machine.");return}try{let{data:s,mimeType:l}=await n.capture();if(s.length>10485760){let u=(s.length/1048576).toFixed(1);i(`Screenshot is ${u} MB, which exceeds the ${10485760/(1024*1024)} MB limit.`);return}let c=s.toString("base64");for(let u=0;u<c.length;u+=524288)o({type:"agent:screenshot_chunk",requestId:t,chunk:c.slice(u,u+524288)});o({type:"agent:screenshot_done",requestId:t,mimeType:l,totalBytes:s.length})}catch(s){i(`Screenshot failed: ${s.message}`)}}import ct from"node:events";import G from"node:fs";import R from"node:path";import Ze from"node:os";import{randomUUID as qe,randomBytes as Qe}from"node:crypto";var W="/etc/aicommander-agent",j=R.join(Ze.homedir(),".config","aicommander-agent"),M="device.json";function Q(){return{deviceId:qe(),deviceSecret:Qe(32).toString("base64url")}}function ze(e){return typeof e=="object"&&e!==null&&typeof e.deviceId=="string"&&e.deviceId.length>0&&typeof e.deviceSecret=="string"&&e.deviceSecret.length>0}function q(e){try{let t=G.readFileSync(e,"utf8"),n=JSON.parse(t);if(ze(n))return{deviceId:n.deviceId,deviceSecret:n.deviceSecret}}catch{}return null}function Ee(e){try{G.rmSync(R.join(e,M),{force:!0})}catch{}}function x(e,t){try{return G.mkdirSync(e,{recursive:!0,mode:448}),G.writeFileSync(R.join(e,M),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function N(e){if(e){let i=q(R.join(e,M));if(i)return i;let r=Q();return x(e,r),r}let t=q(R.join(W,M));if(t)return t;let n=q(R.join(j,M));if(n)return n;let o=Q();return x(W,o)||x(j,o),o}function be(e){let t=Q();return e?(x(e,t),t):x(W,t)?(Ee(j),t):(x(j,t),Ee(W),t)}function Te(e){return{"X-Device-Id":e.deviceId,"X-Device-Secret":e.deviceSecret}}async function z(e,t){let n=await fetch(`${e}/api/device/admins`,{headers:Te(t)});if(!n.ok)throw new Error(`Failed to list linked accounts (HTTP ${n.status}).`);return await n.json()}async function Ce(e,t,n,o){let i=await fetch(`${e}/api/device/${n}`,{method:"POST",headers:{...Te(t),"Content-Type":"application/json"},body:JSON.stringify({userId:o})}),r;try{r=await i.json()}catch{r={ok:!1,error:`http_${i.status}`}}return{...r,ok:i.ok}}function ee(e,t,n){return Ce(e,t,"block",n)}function te(e,t,n){return Ce(e,t,"unblock",n)}function et(e){return[...e.filter(t=>!t.blocked),...e.filter(t=>t.blocked)]}function tt(e,t){let n=t.trim();if(!n)return{kind:"not_found"};if(/^\d+$/.test(n)){let r=Number(n)-1;return r>=0&&r<e.length?{kind:"ok",admin:e[r]}:{kind:"not_found"}}let o=e.find(r=>r.userId===n);if(o)return{kind:"ok",admin:o};let i=e.filter(r=>r.userId.startsWith(n));return i.length===1?{kind:"ok",admin:i[0]}:i.length>1?{kind:"ambiguous",matches:i}:{kind:"not_found"}}import A from"node:fs";import I from"node:path";import nt from"node:os";var rt="/etc/aicommander-agent",ot=I.join(nt.homedir(),".config","aicommander-agent"),ne="session.json",we=".rotate";function it(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0&&typeof e.agentToken=="string"&&e.agentToken.length>0}function st(e){try{let t=A.readFileSync(e,"utf8"),n=JSON.parse(t);if(it(n))return{sessionCode:n.sessionCode,agentToken:n.agentToken}}catch{}return null}function at(e,t){try{return A.mkdirSync(e,{recursive:!0,mode:448}),A.writeFileSync(I.join(e,ne),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function P(e){return e?[e]:[rt,ot]}function Re(e){for(let t of P(e)){let n=st(I.join(t,ne));if(n)return n}return null}function re(e,t){for(let n of P(t))if(at(n,e))return}function oe(e){for(let t of P(e))try{A.rmSync(I.join(t,ne),{force:!0})}catch{}}function xe(e){for(let t of P(e))try{A.mkdirSync(t,{recursive:!0,mode:448}),A.writeFileSync(I.join(t,we),"",{mode:384});return}catch{}}function Ie(e){let t=!1;for(let n of P(e)){let o=I.join(n,we);try{A.existsSync(o)&&(t=!0,A.rmSync(o,{force:!0}))}catch{}}return t}var ie=class extends ct{serverUrl;configDir;screenShare;abortController=null;_status="disabled";_code=null;_active=!1;_lastHeartbeatAt=0;constructor(t,n){super(),this.serverUrl=t,this.configDir=n?.configDir,this.screenShare=n?.screenShare,this.on("error",()=>{})}get status(){return this._status}get sessionCode(){return this._code}get active(){return this._active}get lastHeartbeatAt(){return this._lastHeartbeatAt}start(){this.abortController||(this._setStatus("connecting"),this.abortController=new AbortController,this._runLoop(this.abortController))}async _runLoop(t){let n=1e3,o=3e4,i=N(this.configDir),r=!1,s=Ie(this.configDir);for(;!t.signal.aborted;){this._setStatus("connecting");try{let l=s?null:Re(this.configDir),{sessionCode:c,agentToken:u}=await F(this.serverUrl,i,{forceNew:s,...l?{currentCode:l.sessionCode}:{}});if(t.signal.aborted)break;re({sessionCode:c,agentToken:u},this.configDir),s=!1,this._code=c;try{this.emit("code",c)}catch{}await Z({serverUrl:this.serverUrl,sessionCode:c,agentToken:u,signal:t.signal,silent:!0,...this.screenShare?{screenShare:this.screenShare}:{},onStatus:a=>this._setStatus(a),onActivity:a=>this._setActive(a),onHeartbeat:()=>this._onHeartbeat(),reauthIntervalMs:_e,reauth:async()=>{let a=await F(this.serverUrl,i,{currentCode:c});if(t.signal.aborted||this.abortController!==t)throw new DOMException("Aborted","AbortError");return re({sessionCode:a.sessionCode,agentToken:a.agentToken},this.configDir),a.agentToken}});break}catch(l){if(t.signal.aborted)break;if(l instanceof w&&l.status===403&&l.code===ue&&!r){r=!0,console.error(`[AIC] Relay rejected this device: ${l.message}. Regenerating device identity and retrying as a new device.`),i=be(this.configDir),oe(this.configDir),s=!1;continue}l instanceof w&&console.error(`[AIC] ${l.message}`),this._setStatus("disconnected");try{await Y(n,t.signal)}catch{break}n=Math.min(n*2,o)}}this.abortController===t&&(this._setStatus("disabled"),this.abortController=null)}stop(){this.abortController?.abort(),this.abortController=null,this._code=null,this._setStatus("disabled")}async restart(){this.stop(),await Promise.resolve(),this.start()}async changeCode(){oe(this.configDir),xe(this.configDir),await this.restart()}async listAdmins(){let t=N(this.configDir);return z(this.serverUrl,t)}async blockAdmin(t){let n=N(this.configDir);return ee(this.serverUrl,n,t)}async unblockAdmin(t){let n=N(this.configDir);return te(this.serverUrl,n,t)}_setStatus(t){if(t!=="connected"&&this._active&&this._setActive(!1),this._status!==t){this._status=t;try{this.emit("status",t)}catch{}}}_onHeartbeat(){this._lastHeartbeatAt=Date.now();try{this.emit("heartbeat",this._lastHeartbeatAt)}catch{}}_setActive(t){if(this._active!==t){this._active=t;try{this.emit("activity",t)}catch{}}}};import{spawn as lt}from"child_process";import{promises as se}from"fs";import ut from"os";import dt from"path";import mt from"crypto";function gt(){return process.platform==="darwin"||process.platform==="win32"}async function ft(){if(process.platform==="darwin")return pt();if(process.platform==="win32")return St();throw new Error("Screen capture is only supported on the macOS and Windows desktop apps.")}function De(e){return dt.join(ut.tmpdir(),`aic-shot-${mt.randomUUID()}.${e}`)}async function pt(){let e=De("png");try{return await Me("screencapture",["-x","-t","png",e]),await Oe(e,"image/png")}finally{await ke(e)}}async function St(){let e=De("png"),n=["Add-Type -AssemblyName System.Windows.Forms,System.Drawing;","$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds;","$bmp = New-Object System.Drawing.Bitmap($b.Width, $b.Height);","$g = [System.Drawing.Graphics]::FromImage($bmp);","$g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size);",`$bmp.Save('${e.replace(/'/g,"''")}', [System.Drawing.Imaging.ImageFormat]::Png);`,"$g.Dispose(); $bmp.Dispose();"].join(" ");try{return await Me("powershell",["-NoProfile","-NonInteractive","-Command",n]),await Oe(e,"image/png")}finally{await ke(e)}}async function Oe(e,t){let n=await se.stat(e);if(n.size>10485760){let i=(n.size/1048576).toFixed(1);throw new Error(`Screenshot is ${i} MB, which exceeds the ${10485760/(1024*1024)} MB limit.`)}return{data:await se.readFile(e),mimeType:t}}async function ke(e){try{await se.unlink(e)}catch{}}function Me(e,t){return new Promise((n,o)=>{let i=lt(e,t,{stdio:["ignore","ignore","pipe"]}),r="";i.stderr?.on("data",s=>{r+=s.toString()}),i.on("error",s=>o(s)),i.on("close",s=>{s===0?n():o(new Error(`${e} exited with code ${s}${r?`: ${r.trim()}`:""}`))})})}import{spawn as Tt}from"child_process";import U from"fs";import Ne from"path";import yt from"os";function ae(e){if(e)return Ne.join(e,"heartbeat");if(process.platform==="linux")try{return U.mkdirSync("/run/aicommander-agent",{recursive:!0}),"/run/aicommander-agent/heartbeat"}catch{}return Ne.join(yt.tmpdir(),"aicommander-agent-heartbeat")}function Pe(e){try{let t=`${e}.tmp`;U.writeFileSync(t,String(Date.now())),U.renameSync(t,e)}catch{}}var H=null;function vt(e,t=5e3){He(),Pe(e),H=setInterval(()=>Pe(e),t),H.unref?.()}function He(){H&&(clearInterval(H),H=null)}function ce(e){try{let t=U.readFileSync(e,"utf8").trim(),n=Number(t);return Number.isFinite(n)?n:null}catch{return null}}async function Ct(e={}){let t=e.heartbeatPath??ae(e.configDir),n=e.spawnWorker??(a=>Tt(process.execPath,process.argv.slice(1),{env:a,stdio:"inherit"})),o=null,i=0,r=!1,s=0,l=[],c=null,u=null;await new Promise(a=>{let m=null,g=null,_=()=>{c&&(clearInterval(c),c=null),u&&(clearTimeout(u),u=null),m&&(process.removeListener("SIGINT",m),m=null),g&&(process.removeListener("SIGTERM",g),g=null)},T=()=>{_(),a()},y=()=>{i=Date.now(),s++;let S={...process.env,AIC_ROLE:"worker",AIC_HEARTBEAT:t};o=n(S),o.on("exit",(C,b)=>{if(o=null,r){T();return}if(e.maxSpawns!=null&&s>=e.maxSpawns){T();return}E(`worker exited (code=${C??"null"} signal=${b??"null"})`)}),o.on("error",()=>{})},E=S=>{let C=Date.now();l=l.filter($=>C-$<3e5),l.push(C);let b=l.length>5?6e4:0;u&&clearTimeout(u),u=setTimeout(()=>{u=null,r||y()},b),u.unref?.()},L=()=>{if(o)try{o.kill("SIGKILL")}catch{}};c=setInterval(()=>{if(r||!o||Date.now()-i<3e4)return;let S=ce(t);S!=null&&Date.now()-S>3e4&&L()},1e4),c.unref?.();let f=S=>{if(r=!0,c&&(clearInterval(c),c=null),u&&(clearTimeout(u),u=null),o)try{o.kill(S)}catch{}else T()};m=()=>f("SIGINT"),g=()=>f("SIGTERM"),process.on("SIGINT",m),process.on("SIGTERM",g),y()})}function $e(e,t){let n=e.split(".").map(r=>parseInt(r,10)||0),o=t.split(".").map(r=>parseInt(r,10)||0),i=Math.max(n.length,o.length);for(let r=0;r<i;r++){let s=(n[r]??0)-(o[r]??0);if(s!==0)return s>0?1:-1}return 0}function wt(e,t){return $e(e,t)>0}async function Rt(e,t=1e4){let n=new AbortController,o=setTimeout(()=>n.abort(),t);try{let i=await fetch(`${e.replace(/\/+$/,"")}/dist/latest`,{signal:n.signal,headers:{Accept:"application/json"}});if(!i.ok)return null;let r=await i.json();return{version:typeof r.version=="string"?r.version:null,mac:typeof r.mac=="string"?r.mac:null,win:typeof r.win=="string"?r.win:null,agentLinuxX64:typeof r.agentLinuxX64=="string"?r.agentLinuxX64:null,agentLinuxArm64:typeof r.agentLinuxArm64=="string"?r.agentLinuxArm64:null}}catch{return null}finally{clearTimeout(o)}}export{D as AGENT_VERSION,ie as AgentController,ee as blockAdmin,gt as canCaptureScreenshot,ft as captureScreenshot,$e as compareVersions,ae as defaultHeartbeatPath,z as fetchAdmins,Rt as fetchLatestDist,wt as isNewerVersion,et as orderAdmins,ce as readHeartbeat,F as register,tt as resolveAdminIdentifier,Z as runConnectionLoop,Ct as runSupervisor,vt as startHeartbeat,He as stopHeartbeat,te as unblockAdmin};
1
+ import ct from"os";var U="1.0.22";var we="device_secret_mismatch";function lt(e){try{let t=JSON.parse(e);return typeof t.code=="string"?t.code:null}catch{return null}}var O=class extends Error{constructor(r,o){super(`Registration failed (${r}): ${o}`);this.status=r;this.body=o;this.name="RegistrationError",this.code=lt(o)}status;body;code};async function J(e,t,r={}){let o={hostname:ct.hostname(),platform:process.platform,arch:process.arch,agentVersion:U,...t?{deviceId:t.deviceId,deviceSecret:t.deviceSecret}:{},...r.forceNew?{forceNew:!0}:r.currentCode?{currentCode:r.currentCode}:{}},s=await fetch(`${e}/api/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});if(!s.ok){let n=await s.text();throw new O(s.status,n)}return s.json()}import vt from"ws";import At from"os";import{spawn as xe}from"child_process";var w="https://aicommander.dev";var R="aicommander-exec";var Re="ABCDEFGHJKMNPQRSTVWXYZ0123456789";var gr=Math.floor(256/Re.length)*Re.length;var Ce=process.platform==="win32";function Ie(e,t,r,o){let s=Date.now(),n=xe(e,[],{shell:!0,cwd:t??process.env.HOME,env:{...process.env,...r??{}},stdio:["ignore","pipe","pipe"],detached:!Ce}),i=!1,c=null,u=()=>{c&&(clearTimeout(c),c=null)};n.stdout?.on("data",d=>{o.onOutput(d.toString("base64"),"stdout")}),n.stderr?.on("data",d=>{o.onOutput(d.toString("base64"),"stderr")}),n.on("close",d=>{i=!0,u(),o.onDone(d??-1,Date.now()-s)}),n.on("error",d=>{i=!0,u(),o.onError(d.message)});let a=d=>{if(!(i||n.pid==null))try{Ce?xe("taskkill",["/pid",String(n.pid),"/T","/F"],{stdio:"ignore"}):process.kill(-n.pid,d)}catch{}};return{kill:()=>{i||(a("SIGTERM"),u(),c=setTimeout(()=>{c=null,a("SIGKILL")},5e3),c.unref?.())}}}import{spawn as gt}from"node:child_process";import{readFileSync as pt}from"node:fs";import{isAbsolute as ft}from"node:path";import{createRequire as ut}from"node:module";var dt=ut(import.meta.url),De="__secure-exec-drop";function mt(){let e=!1;try{e=dt("node:sea").isSea()}catch{}return e}function Oe(e){let t=process.argv[1],r=!mt()&&t?[t,De,...e]:[De,...e];return{cmd:process.execPath,args:r}}function ht(){let e;try{e=pt("/etc/passwd","utf8")}catch{throw new Error("cannot read /etc/passwd to resolve the secure-exec user")}let t=e.split(`
2
+ `).filter(u=>u.startsWith(`${R}:`));if(t.length===0)throw new Error(`secure exec user "${R}" not found in /etc/passwd \u2014 reinstall the agent to create it`);if(t.length>1)throw new Error(`multiple /etc/passwd entries for "${R}" \u2014 refusing to guess which is the sandbox user`);let r=t[0].split(":");if(r.length!==7)throw new Error(`malformed passwd entry for ${R}`);let o=r[2],s=r[3];if(!/^\d+$/.test(o)||!/^\d+$/.test(s))throw new Error(`could not resolve uid/gid for ${R}`);let n=Number(o),i=Number(s),c=r[5]||"/tmp";if(!Number.isInteger(n)||!Number.isInteger(i))throw new Error(`could not resolve uid/gid for ${R}`);if(n===0)throw new Error("refusing to run secure exec as uid 0");if(i===0)throw new Error("refusing to run secure exec as gid 0");return{uid:n,gid:i,home:c,name:R}}var St="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",Et=new Set(["NODE_OPTIONS","BASH_ENV","ENV","IFS","PYTHONSTARTUP","PYTHONPATH","PERL5OPT","RUBYOPT","GIT_EXTERNAL_DIFF","PATH","HOME","USER","LOGNAME"]),yt=e=>/^LD_/.test(e)||Et.has(e);function Me(e,t){if(process.platform!=="linux")throw new Error("secure exec is only supported on Linux");if(typeof process.getuid=="function"&&process.getuid()!==0)throw new Error("secure exec requires the agent to run as root (to drop privilege)");let r=e.argv;if(!Array.isArray(r)||r.length===0||typeof r[0]!="string")throw new Error("secure exec requires a non-empty argv");let o=r[0];if(o.includes("/"))throw new Error(`command "${o}" must be a bare command name (no path separators)`);if(!e.allowedCommands.includes(o))throw new Error(`command "${o}" is not in this token's allowlist`);if(e.cwd!=null&&!ft(e.cwd))throw new Error(`cwd "${e.cwd}" must be an absolute path`);let s=null;if(e.input!=null&&(s=Buffer.from(e.input,"base64"),s.length>262144))throw new Error(`stdin payload ${s.length}B exceeds limit ${262144}B`);let n=ht(),i={HOME:n.home,USER:n.name,LOGNAME:n.name,PATH:St},c={};for(let[g,C]of Object.entries(e.env??{}))yt(g)||(c[g]=C);let u={...c,...i},a=Date.now(),{cmd:d,args:m}=Oe([String(n.uid),String(n.gid),n.name,o,...r.slice(1)]),l=gt(d,m,{cwd:e.cwd??n.home,env:u,stdio:["pipe","pipe","pipe"],detached:!0,shell:!1});s&&l.stdin?(l.stdin.on("error",()=>{}),l.stdin.end(s)):l.stdin?.end();let E=!1,S=null,b=()=>{S&&(clearTimeout(S),S=null)};l.stdout?.on("data",g=>{t.onOutput(g.toString("base64"),"stdout")}),l.stderr?.on("data",g=>{t.onOutput(g.toString("base64"),"stderr")}),l.on("close",g=>{E=!0,b(),t.onDone(g??-1,Date.now()-a)}),l.on("error",g=>{E=!0,b(),t.onError(g.message)});let _=g=>{if(!(E||l.pid==null))try{process.kill(-l.pid,g)}catch{}};return{kill:()=>{E||(_("SIGTERM"),b(),S=setTimeout(()=>{S=null,_("SIGKILL")},5e3),S.unref?.())}}}import B from"chalk";function ke(e,t){console.log(B.yellow(` Reconnecting in ${Math.round(t/1e3)}s (attempt ${e})...`))}function Pe(){console.log(B.green(" Connected to relay \u2014 ready for commands."))}function ie(e){console.log(B.cyan(` > ${e}`))}function ae(e,t){let r=e===0?B.green:B.red;console.log(r(` Done. Exit ${e} in ${t}ms`))}function _t(){let e=process.env.AICOMMANDER_DEV;return e==="1"||e==="true"}function He(e){let t=e.toLowerCase();return t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="[::1]"}function Le(){return new URL(w).origin}function $e(e){let t=(e??"").trim();if(!t)return w;let r;try{r=new URL(t)}catch{return console.warn(`
3
+ \u26A0 Ignoring malformed AICOMMANDER_SERVER (${t}); using ${w}.
4
+ `),w}if(r.origin===Le())return w;let o=He(r.hostname);return!_t()&&!o?(console.warn(`
5
+ \u26A0 AICOMMANDER_SERVER override to "${r.origin}" ignored for safety \u2014
6
+ the agent runs as root and is host-locked to ${Le()}.
7
+ Set AICOMMANDER_DEV=1 to allow a custom relay for development.
8
+ `),w):r.protocol!=="https:"&&!o?(console.warn(`
9
+ \u26A0 Refusing plaintext relay "${r.origin}" (https required); using ${w}.
10
+ `),w):t.replace(/\/+$/,"")}function Ue(e){let t;try{t=new URL(e)}catch{throw new Error("Refusing to connect: malformed relay WebSocket URL.")}if(t.protocol!=="wss:"&&!(t.protocol==="ws:"&&He(t.hostname)))throw new Error(`Refusing to open a plaintext relay socket (${t.protocol}//${t.host}); wss:// required.`)}var ce={capable:!1,enabled:!1,expiresAt:null},Fe=360*60*1e3;function le(e,t){return new Promise((r,o)=>{if(t.aborted){o(new DOMException("Aborted","AbortError"));return}let s=setTimeout(r,e);t.addEventListener("abort",()=>{clearTimeout(s),o(new DOMException("Aborted","AbortError"))},{once:!0})})}async function ue(e){let t=1e3,r=3e4,o=0,s=e.agentToken;for(;!e.signal?.aborted;){o++;try{let n=await bt(e,s);if(t=1e3,o=0,n&&e.reauth&&!e.signal?.aborted)try{s=await e.reauth()}catch{}}catch(n){if(n.name==="AbortError")break;let i=.8+Math.random()*.4,c=Math.min(t*i,r);e.silent||ke(o,c);try{await le(c,e.signal??new AbortController().signal)}catch(u){if(u.name==="AbortError")break}t=Math.min(t*2,r)}}}async function bt(e,t){let{serverUrl:r,sessionCode:o}=e,s=`${r.replace(/^http/,"ws")}/ws/agent/${o}?token=${t}`;Ue(s);let n=a=>{try{e.onStatus?.(a)}catch{}},i=a=>{try{e.onActivity?.(a)}catch{}},c=a=>{if(a)try{e.onRemoteActivity?.(a)}catch{}},u=()=>{try{e.onHeartbeat?.()}catch{}};return n("connecting"),new Promise((a,d)=>{let m=new vt(s),l=null,E=null,S=null,b=!1,_=null,g=!1,C=()=>{S&&(clearTimeout(S),S=null)},f=()=>{_&&(clearInterval(_),_=null)},I=()=>{if(!e.reauthIntervalMs||!e.reauth)return;let h=()=>{if(l){S=setTimeout(h,3e4);return}b=!0,m.close(1e3,"reauth")};S=setTimeout(h,e.reauthIntervalMs)},P=()=>{g||(g=!0,C(),f(),a(b))},L=h=>{g||(g=!0,C(),f(),d(h))},be=()=>{u(),E&&clearTimeout(E),E=setTimeout(()=>{m.terminate(),L(new Error("Ping timeout"))},9e4)};e.signal?.addEventListener("abort",()=>m.close(1e3,"disabled"),{once:!0});let te=()=>{if(g)return;let h={type:"agent:screen_state",screenShare:e.screenShare?.getState()??ce};try{m.send(JSON.stringify(h))}catch{}};m.on("open",()=>{n("connected"),be(),I();let h={type:"agent:register",hostname:At.hostname(),platform:process.platform,arch:process.arch,agentVersion:U,screenShare:e.screenShare?.getState()??ce};m.send(JSON.stringify(h)),e.screenShare?.on("change",te),e.silent||Pe()}),m.on("message",h=>{let y;try{y=JSON.parse(String(h))}catch{return}switch(y.type){case"do:ping":be();let it={type:"agent:pong",ts:y.ts};m.send(JSON.stringify(it));break;case"do:exec":{ie(y.command),i(!0),c(y.operator);let{commandId:T,command:D,cwd:re,env:ne}=y,X=Date.now(),H=!1;f(),_=setInterval(()=>{let v=Date.now()-X;if(v>=3e5&&!H){H=!0;let p={type:"agent:exec_idle",commandId:T,idleMs:v};m.send(JSON.stringify(p))}},3e5),_.unref?.(),l=Ie(D,re,ne,{onOutput:(v,p)=>{X=Date.now(),H=!1;let A={type:"agent:output",commandId:T,chunk:v,stream:p};m.send(JSON.stringify(A))},onDone:(v,p)=>{l=null,f(),i(!1),ae(v,p);let A={type:"agent:done",commandId:T,exitCode:v,durationMs:p};m.send(JSON.stringify(A))},onError:v=>{l=null,f(),i(!1);let p={type:"agent:error",commandId:T,error:v};m.send(JSON.stringify(p))}}).kill;break}case"do:secure_exec":{let{commandId:T,argv:D,allowedCommands:re,input:ne,cwd:X,env:H}=y,Te=(p,A)=>{l=null,f(),i(!1),ae(p,A);let $={type:"agent:done",commandId:T,exitCode:p,durationMs:A};m.send(JSON.stringify($))},v=p=>{l=null,f(),i(!1);let A={type:"agent:error",commandId:T,error:p};m.send(JSON.stringify(A))};try{let p=Array.isArray(D)?D.join(" "):String(D);ie(p),i(!0);let A=Date.now(),$=!1;f(),_=setInterval(()=>{let K=Date.now()-A;if(K>=3e5&&!$){$=!0;let oe={type:"agent:exec_idle",commandId:T,idleMs:K};m.send(JSON.stringify(oe))}},3e5),_.unref?.(),l=Me({argv:D,allowedCommands:re,input:ne,cwd:X,env:H},{onOutput:(K,oe)=>{A=Date.now(),$=!1;let at={type:"agent:output",commandId:T,chunk:K,stream:oe};m.send(JSON.stringify(at))},onDone:Te,onError:v}).kill}catch(p){v(p instanceof Error?p.message:String(p))}break}case"do:kill":l&&(l(),l=null);break;case"do:screenshot":c(y.operator),Tt(m,y.requestId,e.screenShare);break}}),m.on("close",(h,y)=>{E&&clearTimeout(E),f(),e.screenShare?.off("change",te),l&&(l(),l=null,i(!1)),n("disconnected"),h===1e3||h===4001?P():L(new Error(`WS closed: ${h} ${String(y)}`))}),m.on("error",h=>{E&&clearTimeout(E),f(),e.screenShare?.off("change",te),n("disconnected"),L(h)}),m.on("unexpected-response",(h,y)=>{d(new Error(`Unexpected HTTP response: ${y.statusCode}`))})})}async function Tt(e,t,r){let o=i=>{try{e.send(JSON.stringify(i))}catch{}},s=i=>o({type:"agent:screenshot_error",requestId:t,error:i}),n=r?.getState()??ce;if(!r||!n.capable){s("This machine cannot share its screen (desktop macOS/Windows only).");return}if(!n.enabled){s("Screen sharing is turned off on this machine.");return}try{let{data:i,mimeType:c}=await r.capture();if(i.length>10485760){let a=(i.length/1048576).toFixed(1);s(`Screenshot is ${a} MB, which exceeds the ${10485760/(1024*1024)} MB limit.`);return}let u=i.toString("base64");for(let a=0;a<u.length;a+=524288)o({type:"agent:screenshot_chunk",requestId:t,chunk:u.slice(a,a+524288)});o({type:"agent:screenshot_done",requestId:t,mimeType:c,totalBytes:i.length})}catch(i){s(`Screenshot failed: ${i.message}`)}}import Ft from"node:events";import Z from"node:fs";import N from"node:path";import wt from"node:os";import{randomUUID as Rt,randomBytes as xt}from"node:crypto";var Q="/etc/aicommander-agent",q=N.join(wt.homedir(),".config","aicommander-agent"),G="device.json";function me(){return{deviceId:Rt(),deviceSecret:xt(32).toString("base64url")}}function Ct(e){return typeof e=="object"&&e!==null&&typeof e.deviceId=="string"&&e.deviceId.length>0&&typeof e.deviceSecret=="string"&&e.deviceSecret.length>0}function de(e){try{let t=Z.readFileSync(e,"utf8"),r=JSON.parse(t);if(Ct(r))return{deviceId:r.deviceId,deviceSecret:r.deviceSecret}}catch{}return null}function Ge(e){try{Z.rmSync(N.join(e,G),{force:!0})}catch{}}function M(e,t){try{return Z.mkdirSync(e,{recursive:!0,mode:448}),Z.writeFileSync(N.join(e,G),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function W(e){if(e){let s=de(N.join(e,G));if(s)return s;let n=me();return M(e,n),n}let t=de(N.join(Q,G));if(t)return t;let r=de(N.join(q,G));if(r)return r;let o=me();return M(Q,o)||M(q,o),o}function We(e){let t=me();return e?(M(e,t),t):M(Q,t)?(Ge(q),t):(M(q,t),Ge(Q),t)}function Ve(e){return{"X-Device-Id":e.deviceId,"X-Device-Secret":e.deviceSecret}}var je=1e4;async function ge(e,t){let r;try{r=await fetch(`${e}/api/device/admins`,{headers:Ve(t),signal:AbortSignal.timeout(je)})}catch{throw new Error("Failed to list linked accounts (relay unreachable or timed out).")}if(!r.ok)throw new Error(`Failed to list linked accounts (HTTP ${r.status}).`);return await r.json()}async function Xe(e,t,r,o){let s;try{s=await fetch(`${e}/api/device/${r}`,{method:"POST",headers:{...Ve(t),"Content-Type":"application/json"},body:JSON.stringify({userId:o}),signal:AbortSignal.timeout(je)})}catch{return{ok:!1,error:"unreachable"}}let n;try{n=await s.json()}catch{n={ok:!1,error:`http_${s.status}`}}return{...n,ok:s.ok}}function pe(e,t,r){return Xe(e,t,"block",r)}function fe(e,t,r){return Xe(e,t,"unblock",r)}function It(e){return[...e.filter(t=>!t.blocked),...e.filter(t=>t.blocked)]}function Dt(e,t){let r=t.trim();if(!r)return{kind:"not_found"};if(/^\d+$/.test(r)){let n=Number(r)-1;return n>=0&&n<e.length?{kind:"ok",admin:e[n]}:{kind:"not_found"}}let o=e.find(n=>n.userId===r);if(o)return{kind:"ok",admin:o};let s=e.filter(n=>n.userId.startsWith(r));return s.length===1?{kind:"ok",admin:s[0]}:s.length>1?{kind:"ambiguous",matches:s}:{kind:"not_found"}}import x from"node:fs";import k from"node:path";import Ot from"node:os";var Nt="/etc/aicommander-agent",Mt=k.join(Ot.homedir(),".config","aicommander-agent"),he="session.json",Ke=".rotate";function kt(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0&&typeof e.agentToken=="string"&&e.agentToken.length>0}function Pt(e){try{let t=x.readFileSync(e,"utf8"),r=JSON.parse(t);if(kt(r))return{sessionCode:r.sessionCode,agentToken:r.agentToken}}catch{}return null}function Lt(e,t){try{return x.mkdirSync(e,{recursive:!0,mode:448}),x.writeFileSync(k.join(e,he),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function V(e){return e?[e]:[Nt,Mt]}function Je(e){for(let t of V(e)){let r=Pt(k.join(t,he));if(r)return r}return null}function Se(e,t){for(let r of V(t))if(Lt(r,e))return}function Ee(e){for(let t of V(e))try{x.rmSync(k.join(t,he),{force:!0})}catch{}}function Ye(e){for(let t of V(e))try{x.mkdirSync(t,{recursive:!0,mode:448}),x.writeFileSync(k.join(t,Ke),"",{mode:384});return}catch{}}function Ze(e){let t=!1;for(let r of V(e)){let o=k.join(r,Ke);try{x.existsSync(o)&&(t=!0,x.rmSync(o,{force:!0}))}catch{}}return t}function Bt(e,t){if(!e)return!0;let r=t-e.lastNotifiedAt;if(r>=288e5)return!0;let o=t-e.lastActivityAt>6e5,s=r>=36e5;return o&&s}var z=class{seen=new Map;note(t,r){let o=this.seen.get(t),s=Bt(o,r);return this.seen.set(t,{lastActivityAt:r,lastNotifiedAt:s?r:o?.lastNotifiedAt??0}),s}};var ye=class extends Ft{serverUrl;configDir;screenShare;abortController=null;_status="disabled";_code=null;_active=!1;_lastHeartbeatAt=0;_remote=new z;constructor(t,r){super(),this.serverUrl=$e(t),this.configDir=r?.configDir,this.screenShare=r?.screenShare,this.on("error",()=>{})}get status(){return this._status}get sessionCode(){return this._code}get active(){return this._active}get lastHeartbeatAt(){return this._lastHeartbeatAt}start(){this.abortController||(this._setStatus("connecting"),this.abortController=new AbortController,this._runLoop(this.abortController))}async _runLoop(t){let r=1e3,o=3e4,s=W(this.configDir),n=!1,i=Ze(this.configDir);for(;!t.signal.aborted;){this._setStatus("connecting");try{let c=i?null:Je(this.configDir),{sessionCode:u,agentToken:a}=await J(this.serverUrl,s,{forceNew:i,...c?{currentCode:c.sessionCode}:{}});if(t.signal.aborted)break;Se({sessionCode:u,agentToken:a},this.configDir),i=!1,this._code=u;try{this.emit("code",u)}catch{}await ue({serverUrl:this.serverUrl,sessionCode:u,agentToken:a,signal:t.signal,silent:!0,...this.screenShare?{screenShare:this.screenShare}:{},onStatus:d=>this._setStatus(d),onActivity:d=>this._setActive(d),onRemoteActivity:d=>this._noteRemote(d),onHeartbeat:()=>this._onHeartbeat(),reauthIntervalMs:Fe,reauth:async()=>{let d=await J(this.serverUrl,s,{currentCode:u});if(t.signal.aborted||this.abortController!==t)throw new DOMException("Aborted","AbortError");return Se({sessionCode:d.sessionCode,agentToken:d.agentToken},this.configDir),d.agentToken}});break}catch(c){if(t.signal.aborted)break;if(c instanceof O&&c.status===403&&c.code===we&&!n){n=!0,console.error(`[AIC] Relay rejected this device: ${c.message}. Regenerating device identity and retrying as a new device.`),s=We(this.configDir),Ee(this.configDir),i=!1;continue}c instanceof O&&console.error(`[AIC] ${c.message}`),this._setStatus("disconnected");try{await le(r,t.signal)}catch{break}r=Math.min(r*2,o)}}this.abortController===t&&(this._setStatus("disabled"),this.abortController=null)}stop(){this.abortController?.abort(),this.abortController=null,this._code=null,this._setStatus("disabled")}async restart(){this.stop(),await Promise.resolve(),this.start()}async changeCode(){Ee(this.configDir),Ye(this.configDir),await this.restart()}async listAdmins(){let t=W(this.configDir);return ge(this.serverUrl,t)}async blockAdmin(t){let r=W(this.configDir);return pe(this.serverUrl,r,t)}async unblockAdmin(t){let r=W(this.configDir);return fe(this.serverUrl,r,t)}_setStatus(t){if(t!=="connected"&&this._active&&this._setActive(!1),this._status!==t){this._status=t;try{this.emit("status",t)}catch{}}}_noteRemote(t){let r=Date.now();if(this._remote.note(t.id,r))try{this.emit("remote-connect",{id:t.id,anonymous:t.anonymous})}catch{}}_onHeartbeat(){this._lastHeartbeatAt=Date.now();try{this.emit("heartbeat",this._lastHeartbeatAt)}catch{}}_setActive(t){if(this._active!==t){this._active=t;try{this.emit("activity",t)}catch{}}}};import{spawn as Gt}from"child_process";import{promises as _e}from"fs";import Wt from"os";import Vt from"path";import jt from"crypto";function Xt(){return process.platform==="darwin"||process.platform==="win32"}async function Kt(){if(process.platform==="darwin")return Jt();if(process.platform==="win32")return Yt();throw new Error("Screen capture is only supported on the macOS and Windows desktop apps.")}function Qe(e){return Vt.join(Wt.tmpdir(),`aic-shot-${jt.randomUUID()}.${e}`)}async function Jt(){let e=Qe("png");try{return await et("screencapture",["-x","-t","png",e]),await qe(e,"image/png")}finally{await ze(e)}}async function Yt(){let e=Qe("png"),r=["Add-Type -AssemblyName System.Windows.Forms,System.Drawing;","$b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds;","$bmp = New-Object System.Drawing.Bitmap($b.Width, $b.Height);","$g = [System.Drawing.Graphics]::FromImage($bmp);","$g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size);",`$bmp.Save('${e.replace(/'/g,"''")}', [System.Drawing.Imaging.ImageFormat]::Png);`,"$g.Dispose(); $bmp.Dispose();"].join(" ");try{return await et("powershell",["-NoProfile","-NonInteractive","-Command",r]),await qe(e,"image/png")}finally{await ze(e)}}async function qe(e,t){let r=await _e.stat(e);if(r.size>10485760){let s=(r.size/1048576).toFixed(1);throw new Error(`Screenshot is ${s} MB, which exceeds the ${10485760/(1024*1024)} MB limit.`)}return{data:await _e.readFile(e),mimeType:t}}async function ze(e){try{await _e.unlink(e)}catch{}}function et(e,t){return new Promise((r,o)=>{let s=Gt(e,t,{stdio:["ignore","ignore","pipe"]}),n="";s.stderr?.on("data",i=>{n+=i.toString()}),s.on("error",i=>o(i)),s.on("close",i=>{i===0?r():o(new Error(`${e} exited with code ${i}${n?`: ${n.trim()}`:""}`))})})}import{spawn as nr}from"child_process";import ee from"fs";import tt from"path";import Qt from"os";function ve(e){if(e)return tt.join(e,"heartbeat");if(process.platform==="linux")try{return ee.mkdirSync("/run/aicommander-agent",{recursive:!0}),"/run/aicommander-agent/heartbeat"}catch{}return tt.join(Qt.tmpdir(),"aicommander-agent-heartbeat")}function rt(e){try{let t=`${e}.tmp`;ee.writeFileSync(t,String(Date.now())),ee.renameSync(t,e)}catch{}}var j=null;function qt(e,t=5e3){nt(),rt(e),j=setInterval(()=>rt(e),t),j.unref?.()}function nt(){j&&(clearInterval(j),j=null)}function Ae(e){try{let t=ee.readFileSync(e,"utf8").trim(),r=Number(t);return Number.isFinite(r)?r:null}catch{return null}}async function or(e={}){let t=e.heartbeatPath??ve(e.configDir),r=e.spawnWorker??(d=>nr(process.execPath,process.argv.slice(1),{env:d,stdio:"inherit"})),o=null,s=0,n=!1,i=0,c=[],u=null,a=null;await new Promise(d=>{let m=null,l=null,E=()=>{u&&(clearInterval(u),u=null),a&&(clearTimeout(a),a=null),m&&(process.removeListener("SIGINT",m),m=null),l&&(process.removeListener("SIGTERM",l),l=null)},S=()=>{E(),d()},b=()=>{s=Date.now(),i++;let f={...process.env,AIC_ROLE:"worker",AIC_HEARTBEAT:t};o=r(f),o.on("exit",(I,P)=>{if(o=null,n){S();return}if(e.maxSpawns!=null&&i>=e.maxSpawns){S();return}_(`worker exited (code=${I??"null"} signal=${P??"null"})`)}),o.on("error",()=>{})},_=f=>{let I=Date.now();c=c.filter(L=>I-L<3e5),c.push(I);let P=c.length>5?6e4:0;a&&clearTimeout(a),a=setTimeout(()=>{a=null,n||b()},P),a.unref?.()},g=()=>{if(o)try{o.kill("SIGKILL")}catch{}};u=setInterval(()=>{if(n||!o||Date.now()-s<3e4)return;let f=Ae(t);f!=null&&Date.now()-f>3e4&&g()},1e4),u.unref?.();let C=f=>{if(n=!0,u&&(clearInterval(u),u=null),a&&(clearTimeout(a),a=null),o)try{o.kill(f)}catch{}else S()};m=()=>C("SIGINT"),l=()=>C("SIGTERM"),process.on("SIGINT",m),process.on("SIGTERM",l),b()})}function st(e,t){let r=e.split(".").map(n=>parseInt(n,10)||0),o=t.split(".").map(n=>parseInt(n,10)||0),s=Math.max(r.length,o.length);for(let n=0;n<s;n++){let i=(r[n]??0)-(o[n]??0);if(i!==0)return i>0?1:-1}return 0}function sr(e,t){return st(e,t)>0}async function ir(e,t=1e4){let r=new AbortController,o=setTimeout(()=>r.abort(),t);try{let s=await fetch(`${e.replace(/\/+$/,"")}/dist/latest`,{signal:r.signal,headers:{Accept:"application/json"}});if(!s.ok)return null;let n=await s.json();return{version:typeof n.version=="string"?n.version:null,mac:typeof n.mac=="string"?n.mac:null,win:typeof n.win=="string"?n.win:null,agentLinuxX64:typeof n.agentLinuxX64=="string"?n.agentLinuxX64:null,agentLinuxArm64:typeof n.agentLinuxArm64=="string"?n.agentLinuxArm64:null}}catch{return null}finally{clearTimeout(o)}}export{U as AGENT_VERSION,ye as AgentController,pe as blockAdmin,Xt as canCaptureScreenshot,Kt as captureScreenshot,st as compareVersions,ve as defaultHeartbeatPath,ge as fetchAdmins,ir as fetchLatestDist,sr as isNewerVersion,It as orderAdmins,Ae as readHeartbeat,J as register,Dt as resolveAdminIdentifier,ue as runConnectionLoop,or as runSupervisor,qt as startHeartbeat,nt as stopHeartbeat,fe as unblockAdmin};
@@ -3,6 +3,16 @@ import type { AdminsResult, BlockResult } from "./device-admin.js";
3
3
  import type { ScreenShareState } from "@aicommander/protocol";
4
4
  import type { CapturedScreenshot } from "./screenshot.js";
5
5
  export type AgentStatus = "connecting" | "connected" | "disconnected" | "disabled";
6
+ /**
7
+ * Emitted as the controller's "remote-connect" event when a remote operator's
8
+ * command clears the rate limit (see RemoteNotifier). The desktop app turns this
9
+ * into the "someone connected" notice; the headless agent simply has no listener.
10
+ */
11
+ export interface RemoteConnectInfo {
12
+ /** Stable operator key: an account userId, or "anon" for session-code callers. */
13
+ id: string;
14
+ anonymous: boolean;
15
+ }
6
16
  /**
7
17
  * Supplies the connection layer with the desktop screen-share grant state and a
8
18
  * way to capture the screen. Only the desktop app provides one; the headless
@@ -40,6 +50,7 @@ export declare class AgentController extends EventEmitter {
40
50
  private _code;
41
51
  private _active;
42
52
  private _lastHeartbeatAt;
53
+ private readonly _remote;
43
54
  constructor(serverUrl: string, opts?: AgentControllerOptions);
44
55
  get status(): AgentStatus;
45
56
  get sessionCode(): string | null;
@@ -68,6 +79,12 @@ export declare class AgentController extends EventEmitter {
68
79
  /** Unblock one account by userId — restores its access immediately. */
69
80
  unblockAdmin(userId: string): Promise<BlockResult>;
70
81
  private _setStatus;
82
+ /**
83
+ * Funnel an operator-initiated command through the connect-notice rate limiter;
84
+ * emit "remote-connect" only when a fresh notice is due. Never throws into the
85
+ * ws event handler that calls it.
86
+ */
87
+ private _noteRemote;
71
88
  private _onHeartbeat;
72
89
  private _setActive;
73
90
  }
@@ -1,4 +1,5 @@
1
1
  import type { ScreenShareProvider } from "./agent-controller.js";
2
+ import type { RemoteOperator } from "@aicommander/protocol";
2
3
  interface ConnectionOpts {
3
4
  serverUrl: string;
4
5
  sessionCode: string;
@@ -8,6 +9,12 @@ interface ConnectionOpts {
8
9
  screenShare?: ScreenShareProvider;
9
10
  onStatus?: (s: 'connecting' | 'connected' | 'disconnected') => void;
10
11
  onActivity?: (active: boolean) => void;
12
+ /**
13
+ * Fired with the driving operator on each operator-initiated command (exec /
14
+ * screenshot) that carries an identity. The controller rate-limits these into a
15
+ * desktop "someone connected" notice; absent on the headless agent.
16
+ */
17
+ onRemoteActivity?: (operator: RemoteOperator) => void;
11
18
  /**
12
19
  * Fired whenever the link proves alive (on open and on every server ping).
13
20
  * Lets a supervisor watch link liveness independently of command workload —
@@ -3,6 +3,11 @@ export interface AdminEntry {
3
3
  userId: string;
4
4
  /** Already masked by the server — a full address never leaves the relay. */
5
5
  maskedEmail: string;
6
+ /**
7
+ * The linked machine's hostname (e.g. "aic-studio"), NOT a label for the person.
8
+ * Show it as secondary machine info only — use {@link maskedEmail} to identify
9
+ * the account/operator.
10
+ */
6
11
  alias: string;
7
12
  linkedAt: string;
8
13
  lastSeenAt: string | null;
@@ -22,11 +27,18 @@ export interface BlockResult {
22
27
  error?: string;
23
28
  message?: string;
24
29
  }
25
- /** List the accounts linked to this device (masked emails). Throws on transport/auth failure. */
30
+ /** List the accounts linked to this device (masked emails). Throws on transport/auth/timeout failure. */
26
31
  export declare function fetchAdmins(serverUrl: string, device: DeviceIdentity): Promise<AdminsResult>;
27
- /** Block one account by userId. Returns a structured result (never throws on a 4xx). */
32
+ /**
33
+ * Block one account by userId. Never throws: returns a structured {@link BlockResult}
34
+ * for ANY outcome, including a 4xx, a transport failure, or a request timeout.
35
+ */
28
36
  export declare function blockAdmin(serverUrl: string, device: DeviceIdentity, userId: string): Promise<BlockResult>;
29
- /** Unblock one account by userId, restoring its access. */
37
+ /**
38
+ * Unblock one account by userId, restoring its access. Never throws: returns a
39
+ * structured {@link BlockResult} for ANY outcome, including a 4xx, a transport
40
+ * failure, or a request timeout.
41
+ */
30
42
  export declare function unblockAdmin(serverUrl: string, device: DeviceIdentity, userId: string): Promise<BlockResult>;
31
43
  /**
32
44
  * Stable display/resolution order: active accounts first, blocked ones last,
@@ -15,6 +15,6 @@
15
15
  */
16
16
  export declare function showCode(sessionCode: string, serverUrl: string, reveal?: boolean): void;
17
17
  export declare function showReconnecting(attempt: number, delayMs: number): void;
18
- export declare function showConnected(): void;
18
+ export declare function showRelayConnected(): void;
19
19
  export declare function showExecuting(command: string): void;
20
20
  export declare function showDone(exitCode: number, durationMs: number): void;
@@ -1,7 +1,7 @@
1
1
  export { register } from "./register.js";
2
2
  export { runConnectionLoop } from "./connection.js";
3
3
  export { AgentController } from "./agent-controller.js";
4
- export type { AgentStatus, ScreenShareProvider } from "./agent-controller.js";
4
+ export type { AgentStatus, ScreenShareProvider, RemoteConnectInfo } from "./agent-controller.js";
5
5
  export { fetchAdmins, blockAdmin, unblockAdmin, orderAdmins, resolveAdminIdentifier } from "./device-admin.js";
6
6
  export type { AdminEntry, AdminsResult, BlockResult } from "./device-admin.js";
7
7
  export { captureScreenshot, canCaptureScreenshot } from "./screenshot.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Resolve the effective, TRUSTED relay base URL from a raw candidate (typically
3
+ * `process.env.AICOMMANDER_SERVER`). Returns a URL string safe to register and
4
+ * open a root-exec socket against. Never throws — an unparseable or untrusted
5
+ * value falls back to DEFAULT_SERVER (with a loud warning) rather than crashing.
6
+ */
7
+ export declare function resolveTrustedServerUrl(raw?: string | null): string;
8
+ /**
9
+ * Assert the about-to-open agent WebSocket URL is encrypted (wss://) or an explicit
10
+ * loopback dev target (ws://localhost). Belt-and-suspenders on top of
11
+ * resolveTrustedServerUrl, evaluated at the actual connection point. Throws on a
12
+ * plaintext non-loopback URL so a root agent never opens an unauthenticated link.
13
+ */
14
+ export declare function assertSecureWsUrl(wsUrl: string): void;
@@ -0,0 +1,43 @@
1
+ /** Per-operator bookkeeping for the connect notice (see shouldNotify). */
2
+ export interface OperatorState {
3
+ /** Epoch-ms of this operator's most recent command (notified or not). */
4
+ lastActivityAt: number;
5
+ /** Epoch-ms the last notice for this operator fired, or 0 if never. */
6
+ lastNotifiedAt: number;
7
+ }
8
+ /**
9
+ * Decide whether an operator's command at `now` should raise a fresh "someone
10
+ * connected" notice, given their prior state (undefined = first contact ever):
11
+ *
12
+ * - first contact always notifies (`pierwsza notyfikacja powinna pójść`);
13
+ * - a NEW session notifies when BOTH hold:
14
+ * • the operator has been idle longer than REMOTE_ACTIVITY_GAP_MS — so a
15
+ * continuous working session (back-to-back commands) is one "connection",
16
+ * notified once at its start and never mid-stream; and
17
+ * • their last notice was at least REMOTE_NOTIFY_MIN_INTERVAL_MS ago — a hard
18
+ * one-per-hour-per-operator floor between session notices;
19
+ * - additionally, a continuous-session CEILING fires regardless of the idle gap
20
+ * once REMOTE_NOTIFY_MAX_INTERVAL_MS has passed since the last notice, so an
21
+ * operator working non-stop all day is still re-announced periodically (≤ once
22
+ * per that interval) instead of only the very first time.
23
+ *
24
+ * Pure: the caller supplies `now` and the prior state, so it is trivially testable.
25
+ */
26
+ export declare function shouldNotify(prev: OperatorState | undefined, now: number): boolean;
27
+ /**
28
+ * Tracks remote operators and applies the connect-notice rate limit. Holds the
29
+ * per-operator state in memory (reset on agent restart — a fresh start re-notifies,
30
+ * which is the safe direction). `note()` is the single entry point: record an
31
+ * operator's activity and learn whether to surface a notice. Time is injected so
32
+ * the whole thing is deterministic under test.
33
+ */
34
+ export declare class RemoteNotifier {
35
+ private readonly seen;
36
+ /**
37
+ * Record a command from operator `id` at `now`; returns true iff a notice should
38
+ * fire. Always advances `lastActivityAt`; advances `lastNotifiedAt` only when it
39
+ * returns true, so the one-per-hour ceiling is measured from the last NOTICE, not
40
+ * the last command.
41
+ */
42
+ note(id: string, now: number): boolean;
43
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Zero-dependency privilege drop for secure exec.
3
+ *
4
+ * Node's `spawn({uid,gid})` sets the primary uid/gid but does NOT clear root's
5
+ * SUPPLEMENTARY groups, so the child would keep gid 0 et al. The classic fix is
6
+ * the external `setpriv` (util-linux) binary — but that is NOT guaranteed to
7
+ * exist on minimal hosts (alpine/musl, distroless, slim images), and secure exec
8
+ * must work after a plain curl/npm install with NOTHING else to install.
9
+ *
10
+ * So we drop privilege IN-PROCESS using the agent's own bundled Node runtime:
11
+ * the agent re-execs ITSELF (`process.execPath`) with the SENTINEL sub-command;
12
+ * that child, still root, calls initgroups → setgid → setuid (exactly what
13
+ * `setpriv --init-groups` does: supplementary groups reset to the target user's
14
+ * own from /etc/group, root's groups dropped) and only THEN execs the target.
15
+ * Works identically for the npm install (`node agent.js …`) and the single
16
+ * self-contained binary (`agent …`) because execPath always points at a working
17
+ * runtime — no external program, no PATH dependency.
18
+ */
19
+ export declare const SECURE_EXEC_DROP_SENTINEL = "__secure-exec-drop";
20
+ /**
21
+ * Build the argv to re-exec THIS runtime with the drop sub-command. Handles both
22
+ * launch shapes:
23
+ * - single self-contained binary (SEA): `binary __secure-exec-drop …` — argv[1]
24
+ * is a normal arg, no script to re-pass.
25
+ * - `node /path/agent.js …`: argv[1] is the script we MUST re-pass so the child
26
+ * Node loads it again. We re-pass argv[1] unconditionally regardless of its
27
+ * extension (extensionless global/npm bin shims have no `.js` suffix).
28
+ */
29
+ export declare function buildDropReexec(extra: string[]): {
30
+ cmd: string;
31
+ args: string[];
32
+ };
33
+ /**
34
+ * If `argv` carries the drop sentinel IN THE EXPECTED POSITION, perform the
35
+ * privilege drop + exec the target and return true (the caller MUST then do
36
+ * nothing else — this process is now the sandboxed command and will process.exit
37
+ * with the child's code). Returns false for a normal agent launch.
38
+ *
39
+ * The sentinel is honoured ONLY as the FIRST real argument of our own re-exec —
40
+ * never anywhere else in argv — so a stray token in a normal CLI invocation
41
+ * (e.g. `status __secure-exec-drop …`) can NOT dispatch the drop parser:
42
+ * - single self-contained binary: `binary __secure-exec-drop …` → argv[1]
43
+ * - node+script: `node agent.js __secure-exec-drop …` → argv[2]
44
+ *
45
+ * Sentinel payload (immediately after the matched sentinel): uid gid username bin
46
+ * [args…]. Runs as root on entry; fails closed (exit 127) on any privilege-drop
47
+ * error so a misconfigured host can never silently run the command AS ROOT.
48
+ */
49
+ export declare function maybeRunSecureExecDrop(argv: string[]): boolean;
@@ -0,0 +1,16 @@
1
+ import type { CommandHandlers, RunningCommand } from "./executor.js";
2
+ export interface SecureCommandRequest {
3
+ argv: string[];
4
+ allowedCommands: string[];
5
+ input?: string;
6
+ cwd?: string;
7
+ env?: Record<string, string>;
8
+ }
9
+ /**
10
+ * Run a sandboxed command. Performs all pre-flight checks synchronously and
11
+ * throws on any failure (bad platform/privilege, unknown user, empty or
12
+ * disallowed argv, oversized input) — the caller maps the throw to agent:error.
13
+ * On success returns a RunningCommand whose kill() tears down the process tree,
14
+ * exactly like the legacy executor.
15
+ */
16
+ export declare function executeSecureCommand(req: SecureCommandRequest, handlers: CommandHandlers): RunningCommand;
@@ -1 +1 @@
1
- export declare const AGENT_VERSION = "1.0.19";
1
+ export declare const AGENT_VERSION = "1.0.22";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicommander/agent",
3
- "version": "1.0.19",
3
+ "version": "1.0.22",
4
4
  "description": "Remote-machine agent for AI Commander — runs on your server/laptop and lets your AI agent execute shell commands on it via the @aicommander/mcp server. An SSH alternative with no exposed SSH, open ports, or VPN; drive it by AIC-… session code.",
5
5
  "keywords": [
6
6
  "remote",