@aicommander/agent 1.0.19 → 1.0.20
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 +10 -7
- package/dist/index.js +2 -1
- package/dist/src/display.d.ts +1 -1
- package/dist/src/secure-exec-drop.d.ts +49 -0
- package/dist/src/secure-executor.d.ts +16 -0
- package/dist/src/version.d.ts +1 -1
- package/package.json +17 -16
- package/dist/bin/agent.js.map +0 -1
package/dist/bin/agent.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
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
|
-
|
|
4
|
-
|
|
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 T}from"commander";var O="1.0.20";import tn from"os";function nn(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=nn(s)}status;body;code};async function Ae(e,t,r={}){let s={hostname:tn.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 yn from"ws";import En from"os";import{spawn as Ye}from"child_process";var se="https://aicommander.dev";var w="aicommander-exec";var Xe="ABCDEFGHJKMNPQRSTVWXYZ0123456789";var rr=Math.floor(256/Xe.length)*Xe.length;var rn=/^AIC-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}$/;function on(e){return rn.test(e.trim().toUpperCase())}function j(e){return on(e)?`AIC-${e.slice(4,8)}-***-***`:e}var qe=process.platform==="win32";function Ze(e,t,r,s){let i=Date.now(),o=Ye(e,[],{shell:!0,cwd:t??process.env.HOME,env:{...process.env,...r??{}},stdio:["ignore","pipe","pipe"],detached:!qe}),a=!1,c=null,l=()=>{c&&(clearTimeout(c),c=null)};o.stdout?.on("data",u=>{s.onOutput(u.toString("base64"),"stdout")}),o.stderr?.on("data",u=>{s.onOutput(u.toString("base64"),"stderr")}),o.on("close",u=>{a=!0,l(),s.onDone(u??-1,Date.now()-i)}),o.on("error",u=>{a=!0,l(),s.onError(u.message)});let d=u=>{if(!(a||o.pid==null))try{qe?Ye("taskkill",["/pid",String(o.pid),"/T","/F"],{stdio:"ignore"}):process.kill(-o.pid,u)}catch{}};return{kill:()=>{a||(d("SIGTERM"),l(),c=setTimeout(()=>{c=null,d("SIGKILL")},5e3),c.unref?.())}}}import{spawn as un}from"node:child_process";import{readFileSync as mn}from"node:fs";import{isAbsolute as fn}from"node:path";import{spawn as sn}from"node:child_process";import{createRequire as an}from"node:module";import{constants as cn}from"node:os";var ln=an(import.meta.url),Te="__secure-exec-drop",dn="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";function Qe(){let e=!1;try{e=ln("node:sea").isSea()}catch{}return e}function ze(e){let t=process.argv[1],r=!Qe()&&t?[t,Te,...e]:[Te,...e];return{cmd:process.execPath,args:r}}function et(e){let t=Qe()?1:2;if(e[t]!==Te)return!1;let[r,s,i,o,...a]=e.slice(t+1),c=Number(r),l=Number(s),d=process;try{if(!Number.isInteger(c)||!Number.isInteger(l)||c===0||!i||!o)throw new Error("invalid drop arguments");if(typeof d.initgroups!="function"||typeof d.setgid!="function"||typeof d.setuid!="function")throw new Error("privilege-drop syscalls unavailable on this platform");if(d.initgroups(i,l),d.setgid(l),d.setuid(c),d.getuid?.()===0||d.getgid?.()===0)throw new Error("privilege drop did not take effect");if(typeof d.getgroups=="function"){let p=d.getgroups();if(Array.isArray(p)&&p.includes(0))throw new Error("privilege drop left root's supplementary groups in place")}}catch(p){process.stderr.write(`secure-exec: privilege drop failed: ${p instanceof Error?p.message:String(p)}
|
|
3
|
+
`),process.exit(127)}let u={...process.env,PATH:dn},f=sn(o,a,{stdio:"inherit",env:u,shell:!1}),m=!1;return f.on("error",p=>{m||(m=!0,process.stderr.write(`secure-exec: spawn failed: ${p.message}
|
|
4
|
+
`),process.exit(127))}),f.on("exit",(p,E)=>{if(!m){if(m=!0,E){let S=cn.signals[E];process.exit(128+(typeof S=="number"?S:0))}process.exit(p??0)}}),!0}function pn(){let e;try{e=mn("/etc/passwd","utf8")}catch{throw new Error("cannot read /etc/passwd to resolve the secure-exec user")}let t=e.split(`
|
|
5
|
+
`).filter(l=>l.startsWith(`${w}:`));if(t.length===0)throw new Error(`secure exec user "${w}" not found in /etc/passwd \u2014 reinstall the agent to create it`);if(t.length>1)throw new Error(`multiple /etc/passwd entries for "${w}" \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 ${w}`);let s=r[2],i=r[3];if(!/^\d+$/.test(s)||!/^\d+$/.test(i))throw new Error(`could not resolve uid/gid for ${w}`);let o=Number(s),a=Number(i),c=r[5]||"/tmp";if(!Number.isInteger(o)||!Number.isInteger(a))throw new Error(`could not resolve uid/gid for ${w}`);if(o===0)throw new Error("refusing to run secure exec as uid 0");if(a===0)throw new Error("refusing to run secure exec as gid 0");return{uid:o,gid:a,home:c,name:w}}var gn="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",Sn=new Set(["NODE_OPTIONS","BASH_ENV","ENV","IFS","PYTHONSTARTUP","PYTHONPATH","PERL5OPT","RUBYOPT","GIT_EXTERNAL_DIFF","PATH","HOME","USER","LOGNAME"]),hn=e=>/^LD_/.test(e)||Sn.has(e);function nt(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&&!fn(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=pn(),a={HOME:o.home,USER:o.name,LOGNAME:o.name,PATH:gn},c={};for(let[h,b]of Object.entries(e.env??{}))hn(h)||(c[h]=b);let l={...c,...a},d=Date.now(),{cmd:u,args:f}=ze([String(o.uid),String(o.gid),o.name,s,...r.slice(1)]),m=un(u,f,{cwd:e.cwd??o.home,env:l,stdio:["pipe","pipe","pipe"],detached:!0,shell:!1});i&&m.stdin?(m.stdin.on("error",()=>{}),m.stdin.end(i)):m.stdin?.end();let p=!1,E=null,S=()=>{E&&(clearTimeout(E),E=null)};m.stdout?.on("data",h=>{t.onOutput(h.toString("base64"),"stdout")}),m.stderr?.on("data",h=>{t.onOutput(h.toString("base64"),"stderr")}),m.on("close",h=>{p=!0,S(),t.onDone(h??-1,Date.now()-d)}),m.on("error",h=>{p=!0,S(),t.onError(h.message)});let _=h=>{if(!(p||m.pid==null))try{process.kill(-m.pid,h)}catch{}};return{kill:()=>{p||(_("SIGTERM"),S(),E=setTimeout(()=>{E=null,_("SIGKILL")},5e3),E.unref?.())}}}import A from"chalk";function rt(e,t,r=!1){console.clear(),console.log(),console.log(A.bold.cyan(" AI Commander \u2014 Remote Agent")),console.log(),console.log(A.gray(" Session code:")),console.log(),console.log(A.bold.greenBright(` ${r?e:j(e)}`)),console.log(),r||console.log(A.gray(" Reveal the full code: sudo aicommander-agent status --reveal")),console.log(A.gray(" Then give it to your admin to connect.")),console.log(A.yellow(" \u26A0 Keep this code secret \u2014 anyone who has it can run commands as root here.")),console.log(A.gray(` Server: ${t}`)),console.log(A.gray(" Press Ctrl+C to disconnect.")),console.log(),console.log(A.yellow(" Waiting for admin connection...")),console.log()}function ot(e,t){console.log(A.yellow(` Reconnecting in ${Math.round(t/1e3)}s (attempt ${e})...`))}function st(){console.log(A.green(" Connected to relay \u2014 ready for commands."))}function xe(e){console.log(A.cyan(` > ${e}`))}function ke(e,t){let r=e===0?A.green:A.red;console.log(r(` Done. Exit ${e} in ${t}ms`))}var _e={capable:!1,enabled:!1,expiresAt:null},ct=360*60*1e3;function vn(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 lt(e){let t=1e3,r=3e4,s=0,i=e.agentToken;for(;!e.signal?.aborted;){s++;try{let o=await bn(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 a=.8+Math.random()*.4,c=Math.min(t*a,r);e.silent||ot(s,c);try{await vn(c,e.signal??new AbortController().signal)}catch(l){if(l.name==="AbortError")break}t=Math.min(t*2,r)}}}async function bn(e,t){let{serverUrl:r,sessionCode:s}=e,i=`${r.replace(/^http/,"ws")}/ws/agent/${s}?token=${t}`,o=l=>{try{e.onStatus?.(l)}catch{}},a=l=>{try{e.onActivity?.(l)}catch{}},c=()=>{try{e.onHeartbeat?.()}catch{}};return o("connecting"),new Promise((l,d)=>{let u=new yn(i),f=null,m=null,p=null,E=!1,S=null,_=!1,h=()=>{p&&(clearTimeout(p),p=null)},b=()=>{S&&(clearInterval(S),S=null)},C=()=>{if(!e.reauthIntervalMs||!e.reauth)return;let y=()=>{if(f){p=setTimeout(y,3e4);return}E=!0,u.close(1e3,"reauth")};p=setTimeout(y,e.reauthIntervalMs)},G=()=>{_||(_=!0,h(),b(),l(E))},U=y=>{_||(_=!0,h(),b(),d(y))},ne=()=>{c(),m&&clearTimeout(m),m=setTimeout(()=>{u.terminate(),U(new Error("Ping timeout"))},9e4)};e.signal?.addEventListener("abort",()=>u.close(1e3,"disabled"),{once:!0});let ye=()=>{if(_)return;let y={type:"agent:screen_state",screenShare:e.screenShare?.getState()??_e};try{u.send(JSON.stringify(y))}catch{}};u.on("open",()=>{o("connected"),ne(),C();let y={type:"agent:register",hostname:En.hostname(),platform:process.platform,arch:process.arch,agentVersion:O,screenShare:e.screenShare?.getState()??_e};u.send(JSON.stringify(y)),e.screenShare?.on("change",ye),e.silent||st()}),u.on("message",y=>{let x;try{x=JSON.parse(String(y))}catch{return}switch(x.type){case"do:ping":ne();let zt={type:"agent:pong",ts:x.ts};u.send(JSON.stringify(zt));break;case"do:exec":{xe(x.command),a(!0);let{commandId:D,command:V,cwd:Ee,env:ve}=x,re=Date.now(),q=!1;b(),S=setInterval(()=>{let R=Date.now()-re;if(R>=3e5&&!q){q=!0;let g={type:"agent:exec_idle",commandId:D,idleMs:R};u.send(JSON.stringify(g))}},3e5),S.unref?.(),f=Ze(V,Ee,ve,{onOutput:(R,g)=>{re=Date.now(),q=!1;let k={type:"agent:output",commandId:D,chunk:R,stream:g};u.send(JSON.stringify(k))},onDone:(R,g)=>{f=null,b(),a(!1),ke(R,g);let k={type:"agent:done",commandId:D,exitCode:R,durationMs:g};u.send(JSON.stringify(k))},onError:R=>{f=null,b(),a(!1);let g={type:"agent:error",commandId:D,error:R};u.send(JSON.stringify(g))}}).kill;break}case"do:secure_exec":{let{commandId:D,argv:V,allowedCommands:Ee,input:ve,cwd:re,env:q}=x,Je=(g,k)=>{f=null,b(),a(!1),ke(g,k);let Z={type:"agent:done",commandId:D,exitCode:g,durationMs:k};u.send(JSON.stringify(Z))},R=g=>{f=null,b(),a(!1);let k={type:"agent:error",commandId:D,error:g};u.send(JSON.stringify(k))};try{let g=Array.isArray(V)?V.join(" "):String(V);xe(g),a(!0);let k=Date.now(),Z=!1;b(),S=setInterval(()=>{let oe=Date.now()-k;if(oe>=3e5&&!Z){Z=!0;let be={type:"agent:exec_idle",commandId:D,idleMs:oe};u.send(JSON.stringify(be))}},3e5),S.unref?.(),f=nt({argv:V,allowedCommands:Ee,input:ve,cwd:re,env:q},{onOutput:(oe,be)=>{k=Date.now(),Z=!1;let en={type:"agent:output",commandId:D,chunk:oe,stream:be};u.send(JSON.stringify(en))},onDone:Je,onError:R}).kill}catch(g){R(g instanceof Error?g.message:String(g))}break}case"do:kill":f&&(f(),f=null);break;case"do:screenshot":wn(u,x.requestId,e.screenShare);break}}),u.on("close",(y,x)=>{m&&clearTimeout(m),b(),e.screenShare?.off("change",ye),f&&(f(),f=null,a(!1)),o("disconnected"),y===1e3||y===4001?G():U(new Error(`WS closed: ${y} ${String(x)}`))}),u.on("error",y=>{m&&clearTimeout(m),b(),e.screenShare?.off("change",ye),o("disconnected"),U(y)}),u.on("unexpected-response",(y,x)=>{d(new Error(`Unexpected HTTP response: ${x.statusCode}`))})})}async function wn(e,t,r){let s=a=>{try{e.send(JSON.stringify(a))}catch{}},i=a=>s({type:"agent:screenshot_error",requestId:t,error:a}),o=r?.getState()??_e;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:a,mimeType:c}=await r.capture();if(a.length>10485760){let d=(a.length/1048576).toFixed(1);i(`Screenshot is ${d} MB, which exceeds the ${10485760/(1024*1024)} MB limit.`);return}let l=a.toString("base64");for(let d=0;d<l.length;d+=524288)s({type:"agent:screenshot_chunk",requestId:t,chunk:l.slice(d,d+524288)});s({type:"agent:screenshot_done",requestId:t,mimeType:c,totalBytes:a.length})}catch(a){i(`Screenshot failed: ${a.message}`)}}import N from"node:fs/promises";import mt from"node:path";var ae="/var/run/aicommander-agent",ce=mt.join(ae,"state.json"),dt=448,ut=384;async function ft(e){let t=null;try{await N.mkdir(ae,{recursive:!0,mode:dt}),await N.chmod(ae,dt),t=mt.join(ae,`.state.${process.pid}.${Date.now()}.tmp`);let r=await N.open(t,"wx",ut);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,ut)}catch{}finally{t&&await N.rm(t,{force:!0}).catch(()=>{})}}async function pt(){try{await N.rm(ce,{force:!0})}catch{}}async function I(){try{let e=await N.readFile(ce,"utf8");return JSON.parse(e)}catch{return null}}import De from"node:fs";import Q from"node:path";import An from"node:os";import{randomUUID as Rn,randomBytes as Tn}from"node:crypto";var gt="/etc/aicommander-agent",St=Q.join(An.homedir(),".config","aicommander-agent"),le="device.json";function ht(){return{deviceId:Rn(),deviceSecret:Tn(32).toString("base64url")}}function xn(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(xn(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(Q.join(e,le),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function K(e){if(e){let i=Ie(Q.join(e,le));if(i)return i;let o=ht();return Ce(e,o),o}let t=Ie(Q.join(gt,le));if(t)return t;let r=Ie(Q.join(St,le));if(r)return r;let s=ht();return Ce(gt,s)||Ce(St,s),s}import M from"node:fs";import W from"node:path";import kn from"node:os";var _n="/etc/aicommander-agent",In=W.join(kn.homedir(),".config","aicommander-agent"),Oe="session.json",yt=".rotate";function Cn(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0&&typeof e.agentToken=="string"&&e.agentToken.length>0}function Dn(e){try{let t=M.readFileSync(e,"utf8"),r=JSON.parse(t);if(Cn(r))return{sessionCode:r.sessionCode,agentToken:r.agentToken}}catch{}return null}function On(e,t){try{return M.mkdirSync(e,{recursive:!0,mode:448}),M.writeFileSync(W.join(e,Oe),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function z(e){return e?[e]:[_n,In]}function Et(e){for(let t of z(e)){let r=Dn(W.join(t,Oe));if(r)return r}return null}function Ne(e,t){for(let r of z(t))if(On(r,e))return}function vt(e){for(let t of z(e))try{M.rmSync(W.join(t,Oe),{force:!0})}catch{}}function bt(e){for(let t of z(e))try{M.mkdirSync(t,{recursive:!0,mode:448}),M.writeFileSync(W.join(t,yt),"",{mode:384});return}catch{}}function wt(e){let t=!1;for(let r of z(e)){let s=W.join(r,yt);try{M.existsSync(s)&&(t=!0,M.rmSync(s,{force:!0}))}catch{}}return t}import{spawn as Hn}from"child_process";import de from"fs";import At from"path";import Mn from"os";function ue(e){if(e)return At.join(e,"heartbeat");if(process.platform==="linux")try{return de.mkdirSync("/run/aicommander-agent",{recursive:!0}),"/run/aicommander-agent/heartbeat"}catch{}return At.join(Mn.tmpdir(),"aicommander-agent-heartbeat")}function Rt(e){try{let t=`${e}.tmp`;de.writeFileSync(t,String(Date.now())),de.renameSync(t,e)}catch{}}var ee=null;function Tt(e,t=5e3){Me(),Rt(e),ee=setInterval(()=>Rt(e),t),ee.unref?.()}function Me(){ee&&(clearInterval(ee),ee=null)}function xt(e){try{let t=de.readFileSync(e,"utf8").trim(),r=Number(t);return Number.isFinite(r)?r:null}catch{return null}}async function _t(e={}){let t=e.heartbeatPath??ue(e.configDir),r=e.spawnWorker??(u=>Hn(process.execPath,process.argv.slice(1),{env:u,stdio:"inherit"})),s=null,i=0,o=!1,a=0,c=[],l=null,d=null;await new Promise(u=>{let f=null,m=null,p=()=>{l&&(clearInterval(l),l=null),d&&(clearTimeout(d),d=null),f&&(process.removeListener("SIGINT",f),f=null),m&&(process.removeListener("SIGTERM",m),m=null)},E=()=>{p(),u()},S=()=>{i=Date.now(),a++;let C={...process.env,AIC_ROLE:"worker",AIC_HEARTBEAT:t};s=r(C),s.on("exit",(G,U)=>{if(s=null,o){E();return}if(e.maxSpawns!=null&&a>=e.maxSpawns){E();return}_(`worker exited (code=${G??"null"} signal=${U??"null"})`)}),s.on("error",()=>{})},_=C=>{let G=Date.now();c=c.filter(ne=>G-ne<3e5),c.push(G);let U=c.length>5?6e4:0;d&&clearTimeout(d),d=setTimeout(()=>{d=null,o||S()},U),d.unref?.()},h=()=>{if(s)try{s.kill("SIGKILL")}catch{}};l=setInterval(()=>{if(o||!s||Date.now()-i<3e4)return;let C=xt(t);C!=null&&Date.now()-C>3e4&&h()},1e4),l.unref?.();let b=C=>{if(o=!0,l&&(clearInterval(l),l=null),d&&(clearTimeout(d),d=null),s)try{s.kill(C)}catch{}else E()};f=()=>b("SIGINT"),m=()=>b("SIGTERM"),process.on("SIGINT",f),process.on("SIGTERM",m),S()})}function Fn(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 a=(r[o]??0)-(s[o]??0);if(a!==0)return a>0?1:-1}return 0}function It(e,t){return Fn(e,t)>0}async function Ct(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=process.env.AICOMMANDER_SERVER??se,Bn=1440*60*1e3;async function Dt(){let e=await Ct(H);e?.version&&It(e.version,O)&&console.warn(`
|
|
6
|
+
\u2B06 A newer AI Commander agent is available (v${e.version}; you have v${O}).
|
|
7
|
+
Update: curl -fsSL ${H}/install | sudo bash
|
|
8
|
+
`)}async function Ot(){process.env.AIC_ROLE==="worker"?await Gn():await _t()}async function Gn(){let e=process.env.AIC_ROLE==="worker";e&&Tt(process.env.AIC_HEARTBEAT||ue());let t=()=>{e&&Me(),pt(),process.exit(0)};process.on("SIGINT",t),process.on("SIGTERM",t),typeof process.getuid=="function"&&process.getuid()!==0&&console.warn(`
|
|
6
9
|
\u26A0 Warning: not running as root.
|
|
7
10
|
Commands requiring sudo (apt, systemctl, brew cask\u2026) will fail.
|
|
8
11
|
Recommended: sudo aicommander-agent
|
|
9
|
-
`);let
|
|
12
|
+
`);let r=K(),s=wt(),i=s?null:Et(),o,a;try{let l=await Ae(H,r,{forceNew:s,...i?{currentCode:i.sessionCode}:{}});o=l.sessionCode,a=l.agentToken,Ne({sessionCode:o,agentToken:a})}catch(l){console.error(` Failed to register: ${String(l)}`),process.exit(1)}rt(o,H,!0),ft({sessionCode:o,pid:process.pid,startedAt:new Date().toISOString(),serverUrl:H}),Dt(),setInterval(()=>{Dt()},Bn).unref?.(),await lt({serverUrl:H,sessionCode:o,agentToken:a,reauthIntervalMs:ct,reauth:async()=>{let l=await Ae(H,r,{currentCode:o});return Ne({sessionCode:l.sessionCode,agentToken:l.agentToken}),l.agentToken}})}import te from"node:fs";import{execFileSync as $t}from"node:child_process";import Pt from"chalk";import{execFileSync as Le}from"node:child_process";var L="aicommander-agent";function F(e){Le("systemctl",e,{stdio:"inherit"})}function J(){try{return Le("systemctl",["is-active",L],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return"inactive"}}function Nt(){try{return Le("systemctl",["is-enabled",L],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()==="enabled"}catch{return!1}}function me(){F(["start",L])}function X(){F(["stop",L])}function fe(){F(["enable",L])}function pe(){F(["disable",L])}function Y(){F(["restart",L])}function ge(){F(["daemon-reload"])}function Mt(){F(["kill","--signal=SIGKILL",L])}import $ from"chalk";import Vn from"node:readline";var n={header:e=>console.log($.bold.cyan(`
|
|
10
13
|
${e}
|
|
11
|
-
`)),ok:e=>console.log(
|
|
14
|
+
`)),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 v(){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 Lt(e){if(!process.stdin.isTTY)return!1;let t=Vn.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 Ut="/etc/systemd/system/aicommander-agent.service",$e="/etc/aicommander-agent";function jn(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 Ht(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/%/g,"%%")}"`}function Kn(){n.step(`Ensuring sandbox user "${w}"\u2026`);try{$t("getent",["passwd",w],{stdio:"ignore"}),n.ok(`Sandbox user "${w}" already exists.`);return}catch{}let e=["/usr/sbin/nologin","/sbin/nologin","/bin/false"].find(t=>te.existsSync(t))??"/bin/false";try{$t("useradd",["--system","--create-home","--shell",e,w],{stdio:"ignore"}),n.ok(`Sandbox user "${w}" created.`)}catch{n.warn(`Could not create sandbox user "${w}" \u2014 secure exec will be unavailable until it exists.`)}}async function Pe(e){v(),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=jn(e.server??process.env.AICOMMANDER_SERVER??se),r=te.realpathSync(process.argv[1]),s=`${Ht(process.execPath)} ${Ht(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"),te.mkdirSync($e,{recursive:!0}),te.chmodSync($e,448),n.ok(`Device identity dir ready (${$e}).`),Kn(),n.step(`Writing systemd service to ${Ut}\u2026`);let i=`[Unit]
|
|
12
15
|
Description=AI Commander Remote Agent
|
|
13
16
|
After=network-online.target
|
|
14
17
|
Wants=network-online.target
|
|
@@ -32,4 +35,4 @@ KillSignal=SIGINT
|
|
|
32
35
|
|
|
33
36
|
[Install]
|
|
34
37
|
WantedBy=multi-user.target
|
|
35
|
-
`;
|
|
38
|
+
`;te.writeFileSync(Ut,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{Y()}catch{n.warn("Graceful restart failed \u2014 forcing stop then start\u2026");try{X()}catch{}try{Mt()}catch{}try{me()}catch{n.error("Could not start aicommander-agent. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)}}J()!=="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 a=0;a<30;a++){let c=await I();if(c?.sessionCode){o=c.sessionCode;break}await new Promise(l=>setTimeout(l,500))}n.blank(),o?(n.ok(Pt.bold("AI Commander ready!")),n.blank(),n.info("Session code",Pt.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 Wn(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 Ue(e={}){n.header("AI Commander \u2014 Agent Status");let t;try{t=J()}catch{n.error("systemd is not available on this system."),process.exit(1)}let r=Nt(),s=await I();if(n.info("State",t==="active"?P.green("running"):P.red(t)),s){e.reveal?(v(),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} (${Wn(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 He(){v(),J()==="active"?(n.warn("Service is already running \u2014 restarting to load the current binary."),Y()):me(),fe(),n.ok("Service started and enabled to run on boot."),n.blank()}function Fe(){v(),X(),pe(),n.ok("Service stopped and disabled."),n.blank()}import Jn from"chalk";async function Be(e={}){if(v(),!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 Lt("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"),bt(),vt(),Y();let t=Date.now()+1e4,r=null;for(;Date.now()<t&&(await new Promise(s=>setTimeout(s,500)),r=await I(),!r););r?n.ok(`New session code: ${Jn.bold.greenBright(r.sessionCode)}`):(n.warn("Service restarted. Session code not yet available."),n.info("Check","aicommander-agent status")),n.blank()}import Ge from"node:fs";import Xn from"chalk";var Ft="/etc/systemd/system/aicommander-agent.service",Bt="/usr/local/bin/aicommander-agent",Gt="/usr/local/bin/aicommander-ctl",Vt="/var/run/aicommander-agent",jt="/etc/aicommander-agent",Kt="/etc/aicommander-agent/session.json",Wt="/etc/aicommander-agent/.rotate";function B(e,t){try{if(Ge.existsSync(e)||Ge.lstatSync(e)){Ge.rmSync(e,{recursive:!0,force:!0}),n.ok(`${t} removed.`);return}}catch{}n.warn(`${t} not found (already removed?).`)}function Ve(e){v(),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{X()}catch{}try{pe()}catch{}n.ok("Service stopped."),n.step("Removing service file\u2026"),B(Ft,Ft),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(Gt,Gt),B(Bt,Bt),B(Vt,Vt),B(Kt,Kt),B(Wt,Wt),B(jt,jt),n.blank(),n.ok(Xn.bold("AI Commander agent fully removed.")),n.blank()}function Jt(e){return{"X-Device-Id":e.deviceId,"X-Device-Secret":e.deviceSecret}}async function Se(e,t){let r=await fetch(`${e}/api/device/admins`,{headers:Jt(t)});if(!r.ok)throw new Error(`Failed to list linked accounts (HTTP ${r.status}).`);return await r.json()}async function Xt(e,t,r,s){let i=await fetch(`${e}/api/device/${r}`,{method:"POST",headers:{...Jt(t),"Content-Type":"application/json"},body:JSON.stringify({userId:s})}),o;try{o=await i.json()}catch{o={ok:!1,error:`http_${i.status}`}}return{...o,ok:i.ok}}function Yt(e,t,r){return Xt(e,t,"block",r)}function qt(e,t,r){return Xt(e,t,"unblock",r)}function he(e){return[...e.filter(t=>!t.blocked),...e.filter(t=>t.blocked)]}function Zt(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 Yn(){return(await I())?.serverUrl??process.env.AICOMMANDER_SERVER??"https://aicommander.dev"}async function je(){v();let e=await Yn(),t=K(),r;try{r=await Se(e,t)}catch(c){n.error(c.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=(c,l)=>{let d=String(l+1).padStart(2," "),u=c.linkedAt.slice(0,10);n.info(`${d}. ${c.maskedEmail}`,`${c.alias} \xB7 linked ${u} \xB7 id ${c.userId.slice(0,8)}\u2026`)},o=s.filter(c=>!c.blocked),a=s.filter(c=>c.blocked);n.header("Linked accounts"),o.forEach(c=>i(c,s.indexOf(c))),o.length===0&&n.info("None active","every linked account is blocked"),n.blank(),a.length>0&&(n.header("Blocked"),a.forEach(c=>i(c,s.indexOf(c))),n.blank()),n.step("Block one with: aicommander-agent block-admin <number|id>"),a.length>0&&n.step("Unblock one with: aicommander-agent unblock-admin <number|id>"),n.blank()}async function qn(){return(await I())?.serverUrl??process.env.AICOMMANDER_SERVER??"https://aicommander.dev"}async function Qt(e,t){v();let r=await qn(),s=K(),i;try{i=await Se(r,s)}catch(d){n.error(d.message),process.exitCode=1;return}let o=he(i.admins),a=Zt(o,e);if(a.kind==="not_found"){n.error(`No linked account matches "${e}".`),n.info("See options","aicommander-agent list-admins"),process.exitCode=1;return}if(a.kind==="ambiguous"){n.error(`"${e}" is ambiguous \u2014 it matches ${a.matches.length} accounts.`),n.info("Disambiguate","use the list number or a longer id"),process.exitCode=1;return}let c=a.admin,l=t==="block"?await Yt(r,s,c.userId):await qt(r,s,c.userId);l.ok?t==="block"?(n.ok(`Blocked ${c.maskedEmail} (${c.alias}).`),n.info("Note","their access is cut off; run unblock-admin anytime to restore it")):n.ok(`Unblocked ${c.maskedEmail} (${c.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 Ke(e){return Qt(e,"block")}function We(e){return Qt(e,"unblock")}et(process.argv)||(T.name("aicommander-agent").description("AI Commander remote agent + service controller").version(O),T.command("run",{isDefault:!0}).description("Run the agent: register with the relay and listen for commands").action(async()=>{await Ot()}),T.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 Pe(e)}),T.command("status").description("Show agent status, session code, and uptime").option("--reveal","Show the full session code (otherwise masked)").action(async e=>{await Ue(e)}),T.command("enable").description("Start and enable the agent service").action(()=>{He()}),T.command("disable").description("Stop and disable the agent service").action(()=>{Fe()}),T.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)}),T.command("list-admins").description("List the accounts (admins) linked to this device").action(async()=>{await je()}),T.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)}),T.command("unblock-admin <account>").description("Restore a blocked account's access (by list number or id)").action(async e=>{await We(e)}),T.command("uninstall").description("Fully remove the agent (stop, disable, delete files)").option("-f, --force","Skip confirmation requirement").action(e=>{Ve(e)}),T.parseAsync().catch(e=>{console.error("Fatal error:",e),process.exit(1)}));
|
package/dist/index.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
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 et from"os";var H="1.0.20";var _e="device_secret_mismatch";function tt(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=tt(o)}status;body;code};async function J(e,t,r={}){let o={hostname:et.hostname(),platform:process.platform,arch:process.arch,agentVersion:H,...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 mt from"ws";import gt from"os";import{spawn as we}from"child_process";var x="aicommander-exec";var Ae="ABCDEFGHJKMNPQRSTVWXYZ0123456789";var er=Math.floor(256/Ae.length)*Ae.length;var Te=process.platform==="win32";function xe(e,t,r,o){let s=Date.now(),n=we(e,[],{shell:!0,cwd:t??process.env.HOME,env:{...process.env,...r??{}},stdio:["ignore","pipe","pipe"],detached:!Te}),i=!1,c=null,l=()=>{c&&(clearTimeout(c),c=null)};n.stdout?.on("data",a=>{o.onOutput(a.toString("base64"),"stdout")}),n.stderr?.on("data",a=>{o.onOutput(a.toString("base64"),"stderr")}),n.on("close",a=>{i=!0,l(),o.onDone(a??-1,Date.now()-s)}),n.on("error",a=>{i=!0,l(),o.onError(a.message)});let u=a=>{if(!(i||n.pid==null))try{Te?we("taskkill",["/pid",String(n.pid),"/T","/F"],{stdio:"ignore"}):process.kill(-n.pid,a)}catch{}};return{kill:()=>{i||(u("SIGTERM"),l(),c=setTimeout(()=>{c=null,u("SIGKILL")},5e3),c.unref?.())}}}import{spawn as st}from"node:child_process";import{readFileSync as it}from"node:fs";import{isAbsolute as at}from"node:path";import{createRequire as rt}from"node:module";var nt=rt(import.meta.url),Ce="__secure-exec-drop";function ot(){let e=!1;try{e=nt("node:sea").isSea()}catch{}return e}function Re(e){let t=process.argv[1],r=!ot()&&t?[t,Ce,...e]:[Ce,...e];return{cmd:process.execPath,args:r}}function ct(){let e;try{e=it("/etc/passwd","utf8")}catch{throw new Error("cannot read /etc/passwd to resolve the secure-exec user")}let t=e.split(`
|
|
2
|
+
`).filter(l=>l.startsWith(`${x}:`));if(t.length===0)throw new Error(`secure exec user "${x}" not found in /etc/passwd \u2014 reinstall the agent to create it`);if(t.length>1)throw new Error(`multiple /etc/passwd entries for "${x}" \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 ${x}`);let o=r[2],s=r[3];if(!/^\d+$/.test(o)||!/^\d+$/.test(s))throw new Error(`could not resolve uid/gid for ${x}`);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 ${x}`);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:x}}var lt="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",ut=new Set(["NODE_OPTIONS","BASH_ENV","ENV","IFS","PYTHONSTARTUP","PYTHONPATH","PERL5OPT","RUBYOPT","GIT_EXTERNAL_DIFF","PATH","HOME","USER","LOGNAME"]),dt=e=>/^LD_/.test(e)||ut.has(e);function De(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&&!at(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=ct(),i={HOME:n.home,USER:n.name,LOGNAME:n.name,PATH:lt},c={};for(let[p,S]of Object.entries(e.env??{}))dt(p)||(c[p]=S);let l={...c,...i},u=Date.now(),{cmd:a,args:m}=Re([String(n.uid),String(n.gid),n.name,o,...r.slice(1)]),d=st(a,m,{cwd:e.cwd??n.home,env:l,stdio:["pipe","pipe","pipe"],detached:!0,shell:!1});s&&d.stdin?(d.stdin.on("error",()=>{}),d.stdin.end(s)):d.stdin?.end();let y=!1,E=null,h=()=>{E&&(clearTimeout(E),E=null)};d.stdout?.on("data",p=>{t.onOutput(p.toString("base64"),"stdout")}),d.stderr?.on("data",p=>{t.onOutput(p.toString("base64"),"stderr")}),d.on("close",p=>{y=!0,h(),t.onDone(p??-1,Date.now()-u)}),d.on("error",p=>{y=!0,h(),t.onError(p.message)});let A=p=>{if(!(y||d.pid==null))try{process.kill(-d.pid,p)}catch{}};return{kill:()=>{y||(A("SIGTERM"),h(),E=setTimeout(()=>{E=null,A("SIGKILL")},5e3),E.unref?.())}}}import $ from"chalk";function Oe(e,t){console.log($.yellow(` Reconnecting in ${Math.round(t/1e3)}s (attempt ${e})...`))}function Ne(){console.log($.green(" Connected to relay \u2014 ready for commands."))}function oe(e){console.log($.cyan(` > ${e}`))}function se(e,t){let r=e===0?$.green:$.red;console.log(r(` Done. Exit ${e} in ${t}ms`))}var ie={capable:!1,enabled:!1,expiresAt:null},Me=360*60*1e3;function ae(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 ce(e){let t=1e3,r=3e4,o=0,s=e.agentToken;for(;!e.signal?.aborted;){o++;try{let n=await pt(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||Oe(o,c);try{await ae(c,e.signal??new AbortController().signal)}catch(l){if(l.name==="AbortError")break}t=Math.min(t*2,r)}}}async function pt(e,t){let{serverUrl:r,sessionCode:o}=e,s=`${r.replace(/^http/,"ws")}/ws/agent/${o}?token=${t}`,n=l=>{try{e.onStatus?.(l)}catch{}},i=l=>{try{e.onActivity?.(l)}catch{}},c=()=>{try{e.onHeartbeat?.()}catch{}};return n("connecting"),new Promise((l,u)=>{let a=new mt(s),m=null,d=null,y=null,E=!1,h=null,A=!1,p=()=>{y&&(clearTimeout(y),y=null)},S=()=>{h&&(clearInterval(h),h=null)},w=()=>{if(!e.reauthIntervalMs||!e.reauth)return;let f=()=>{if(m){y=setTimeout(f,3e4);return}E=!0,a.close(1e3,"reauth")};y=setTimeout(f,e.reauthIntervalMs)},I=()=>{A||(A=!0,p(),S(),l(E))},R=f=>{A||(A=!0,p(),S(),u(f))},j=()=>{c(),d&&clearTimeout(d),d=setTimeout(()=>{a.terminate(),R(new Error("Ping timeout"))},9e4)};e.signal?.addEventListener("abort",()=>a.close(1e3,"disabled"),{once:!0});let q=()=>{if(A)return;let f={type:"agent:screen_state",screenShare:e.screenShare?.getState()??ie};try{a.send(JSON.stringify(f))}catch{}};a.on("open",()=>{n("connected"),j(),w();let f={type:"agent:register",hostname:gt.hostname(),platform:process.platform,arch:process.arch,agentVersion:H,screenShare:e.screenShare?.getState()??ie};a.send(JSON.stringify(f)),e.screenShare?.on("change",q),e.silent||Ne()}),a.on("message",f=>{let b;try{b=JSON.parse(String(f))}catch{return}switch(b.type){case"do:ping":j();let ze={type:"agent:pong",ts:b.ts};a.send(JSON.stringify(ze));break;case"do:exec":{oe(b.command),i(!0);let{commandId:T,command:D,cwd:ee,env:te}=b,K=Date.now(),P=!1;S(),h=setInterval(()=>{let v=Date.now()-K;if(v>=3e5&&!P){P=!0;let g={type:"agent:exec_idle",commandId:T,idleMs:v};a.send(JSON.stringify(g))}},3e5),h.unref?.(),m=xe(D,ee,te,{onOutput:(v,g)=>{K=Date.now(),P=!1;let _={type:"agent:output",commandId:T,chunk:v,stream:g};a.send(JSON.stringify(_))},onDone:(v,g)=>{m=null,S(),i(!1),se(v,g);let _={type:"agent:done",commandId:T,exitCode:v,durationMs:g};a.send(JSON.stringify(_))},onError:v=>{m=null,S(),i(!1);let g={type:"agent:error",commandId:T,error:v};a.send(JSON.stringify(g))}}).kill;break}case"do:secure_exec":{let{commandId:T,argv:D,allowedCommands:ee,input:te,cwd:K,env:P}=b,be=(g,_)=>{m=null,S(),i(!1),se(g,_);let L={type:"agent:done",commandId:T,exitCode:g,durationMs:_};a.send(JSON.stringify(L))},v=g=>{m=null,S(),i(!1);let _={type:"agent:error",commandId:T,error:g};a.send(JSON.stringify(_))};try{let g=Array.isArray(D)?D.join(" "):String(D);oe(g),i(!0);let _=Date.now(),L=!1;S(),h=setInterval(()=>{let X=Date.now()-_;if(X>=3e5&&!L){L=!0;let re={type:"agent:exec_idle",commandId:T,idleMs:X};a.send(JSON.stringify(re))}},3e5),h.unref?.(),m=De({argv:D,allowedCommands:ee,input:te,cwd:K,env:P},{onOutput:(X,re)=>{_=Date.now(),L=!1;let qe={type:"agent:output",commandId:T,chunk:X,stream:re};a.send(JSON.stringify(qe))},onDone:be,onError:v}).kill}catch(g){v(g instanceof Error?g.message:String(g))}break}case"do:kill":m&&(m(),m=null);break;case"do:screenshot":ft(a,b.requestId,e.screenShare);break}}),a.on("close",(f,b)=>{d&&clearTimeout(d),S(),e.screenShare?.off("change",q),m&&(m(),m=null,i(!1)),n("disconnected"),f===1e3||f===4001?I():R(new Error(`WS closed: ${f} ${String(b)}`))}),a.on("error",f=>{d&&clearTimeout(d),S(),e.screenShare?.off("change",q),n("disconnected"),R(f)}),a.on("unexpected-response",(f,b)=>{u(new Error(`Unexpected HTTP response: ${b.statusCode}`))})})}async function ft(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()??ie;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 u=(i.length/1048576).toFixed(1);s(`Screenshot is ${u} MB, which exceeds the ${10485760/(1024*1024)} MB limit.`);return}let l=i.toString("base64");for(let u=0;u<l.length;u+=524288)o({type:"agent:screenshot_chunk",requestId:t,chunk:l.slice(u,u+524288)});o({type:"agent:screenshot_done",requestId:t,mimeType:c,totalBytes:i.length})}catch(i){s(`Screenshot failed: ${i.message}`)}}import Rt from"node:events";import Y from"node:fs";import N from"node:path";import St from"node:os";import{randomUUID as ht,randomBytes as Et}from"node:crypto";var Z="/etc/aicommander-agent",Q=N.join(St.homedir(),".config","aicommander-agent"),U="device.json";function ue(){return{deviceId:ht(),deviceSecret:Et(32).toString("base64url")}}function yt(e){return typeof e=="object"&&e!==null&&typeof e.deviceId=="string"&&e.deviceId.length>0&&typeof e.deviceSecret=="string"&&e.deviceSecret.length>0}function le(e){try{let t=Y.readFileSync(e,"utf8"),r=JSON.parse(t);if(yt(r))return{deviceId:r.deviceId,deviceSecret:r.deviceSecret}}catch{}return null}function Pe(e){try{Y.rmSync(N.join(e,U),{force:!0})}catch{}}function k(e,t){try{return Y.mkdirSync(e,{recursive:!0,mode:448}),Y.writeFileSync(N.join(e,U),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function G(e){if(e){let s=le(N.join(e,U));if(s)return s;let n=ue();return k(e,n),n}let t=le(N.join(Z,U));if(t)return t;let r=le(N.join(Q,U));if(r)return r;let o=ue();return k(Z,o)||k(Q,o),o}function Le(e){let t=ue();return e?(k(e,t),t):k(Z,t)?(Pe(Q),t):(k(Q,t),Pe(Z),t)}function He(e){return{"X-Device-Id":e.deviceId,"X-Device-Secret":e.deviceSecret}}async function de(e,t){let r=await fetch(`${e}/api/device/admins`,{headers:He(t)});if(!r.ok)throw new Error(`Failed to list linked accounts (HTTP ${r.status}).`);return await r.json()}async function $e(e,t,r,o){let s=await fetch(`${e}/api/device/${r}`,{method:"POST",headers:{...He(t),"Content-Type":"application/json"},body:JSON.stringify({userId:o})}),n;try{n=await s.json()}catch{n={ok:!1,error:`http_${s.status}`}}return{...n,ok:s.ok}}function me(e,t,r){return $e(e,t,"block",r)}function ge(e,t,r){return $e(e,t,"unblock",r)}function vt(e){return[...e.filter(t=>!t.blocked),...e.filter(t=>t.blocked)]}function bt(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 C from"node:fs";import M from"node:path";import _t from"node:os";var At="/etc/aicommander-agent",wt=M.join(_t.homedir(),".config","aicommander-agent"),pe="session.json",Be=".rotate";function Tt(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0&&typeof e.agentToken=="string"&&e.agentToken.length>0}function xt(e){try{let t=C.readFileSync(e,"utf8"),r=JSON.parse(t);if(Tt(r))return{sessionCode:r.sessionCode,agentToken:r.agentToken}}catch{}return null}function Ct(e,t){try{return C.mkdirSync(e,{recursive:!0,mode:448}),C.writeFileSync(M.join(e,pe),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function F(e){return e?[e]:[At,wt]}function Ue(e){for(let t of F(e)){let r=xt(M.join(t,pe));if(r)return r}return null}function fe(e,t){for(let r of F(t))if(Ct(r,e))return}function Se(e){for(let t of F(e))try{C.rmSync(M.join(t,pe),{force:!0})}catch{}}function Ge(e){for(let t of F(e))try{C.mkdirSync(t,{recursive:!0,mode:448}),C.writeFileSync(M.join(t,Be),"",{mode:384});return}catch{}}function Fe(e){let t=!1;for(let r of F(e)){let o=M.join(r,Be);try{C.existsSync(o)&&(t=!0,C.rmSync(o,{force:!0}))}catch{}}return t}var he=class extends Rt{serverUrl;configDir;screenShare;abortController=null;_status="disabled";_code=null;_active=!1;_lastHeartbeatAt=0;constructor(t,r){super(),this.serverUrl=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=G(this.configDir),n=!1,i=Fe(this.configDir);for(;!t.signal.aborted;){this._setStatus("connecting");try{let c=i?null:Ue(this.configDir),{sessionCode:l,agentToken:u}=await J(this.serverUrl,s,{forceNew:i,...c?{currentCode:c.sessionCode}:{}});if(t.signal.aborted)break;fe({sessionCode:l,agentToken:u},this.configDir),i=!1,this._code=l;try{this.emit("code",l)}catch{}await ce({serverUrl:this.serverUrl,sessionCode:l,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:Me,reauth:async()=>{let a=await J(this.serverUrl,s,{currentCode:l});if(t.signal.aborted||this.abortController!==t)throw new DOMException("Aborted","AbortError");return fe({sessionCode:a.sessionCode,agentToken:a.agentToken},this.configDir),a.agentToken}});break}catch(c){if(t.signal.aborted)break;if(c instanceof O&&c.status===403&&c.code===_e&&!n){n=!0,console.error(`[AIC] Relay rejected this device: ${c.message}. Regenerating device identity and retrying as a new device.`),s=Le(this.configDir),Se(this.configDir),i=!1;continue}c instanceof O&&console.error(`[AIC] ${c.message}`),this._setStatus("disconnected");try{await ae(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(){Se(this.configDir),Ge(this.configDir),await this.restart()}async listAdmins(){let t=G(this.configDir);return de(this.serverUrl,t)}async blockAdmin(t){let r=G(this.configDir);return me(this.serverUrl,r,t)}async unblockAdmin(t){let r=G(this.configDir);return ge(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{}}}_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 It}from"child_process";import{promises as Ee}from"fs";import Dt from"os";import Ot from"path";import Nt from"crypto";function kt(){return process.platform==="darwin"||process.platform==="win32"}async function Mt(){if(process.platform==="darwin")return Pt();if(process.platform==="win32")return Lt();throw new Error("Screen capture is only supported on the macOS and Windows desktop apps.")}function We(e){return Ot.join(Dt.tmpdir(),`aic-shot-${Nt.randomUUID()}.${e}`)}async function Pt(){let e=We("png");try{return await Xe("screencapture",["-x","-t","png",e]),await je(e,"image/png")}finally{await Ke(e)}}async function Lt(){let e=We("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 Xe("powershell",["-NoProfile","-NonInteractive","-Command",r]),await je(e,"image/png")}finally{await Ke(e)}}async function je(e,t){let r=await Ee.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 Ee.readFile(e),mimeType:t}}async function Ke(e){try{await Ee.unlink(e)}catch{}}function Xe(e,t){return new Promise((r,o)=>{let s=It(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 jt}from"child_process";import z from"fs";import Je from"path";import $t from"os";function ye(e){if(e)return Je.join(e,"heartbeat");if(process.platform==="linux")try{return z.mkdirSync("/run/aicommander-agent",{recursive:!0}),"/run/aicommander-agent/heartbeat"}catch{}return Je.join($t.tmpdir(),"aicommander-agent-heartbeat")}function Ve(e){try{let t=`${e}.tmp`;z.writeFileSync(t,String(Date.now())),z.renameSync(t,e)}catch{}}var W=null;function Bt(e,t=5e3){Ye(),Ve(e),W=setInterval(()=>Ve(e),t),W.unref?.()}function Ye(){W&&(clearInterval(W),W=null)}function ve(e){try{let t=z.readFileSync(e,"utf8").trim(),r=Number(t);return Number.isFinite(r)?r:null}catch{return null}}async function Kt(e={}){let t=e.heartbeatPath??ye(e.configDir),r=e.spawnWorker??(a=>jt(process.execPath,process.argv.slice(1),{env:a,stdio:"inherit"})),o=null,s=0,n=!1,i=0,c=[],l=null,u=null;await new Promise(a=>{let m=null,d=null,y=()=>{l&&(clearInterval(l),l=null),u&&(clearTimeout(u),u=null),m&&(process.removeListener("SIGINT",m),m=null),d&&(process.removeListener("SIGTERM",d),d=null)},E=()=>{y(),a()},h=()=>{s=Date.now(),i++;let w={...process.env,AIC_ROLE:"worker",AIC_HEARTBEAT:t};o=r(w),o.on("exit",(I,R)=>{if(o=null,n){E();return}if(e.maxSpawns!=null&&i>=e.maxSpawns){E();return}A(`worker exited (code=${I??"null"} signal=${R??"null"})`)}),o.on("error",()=>{})},A=w=>{let I=Date.now();c=c.filter(j=>I-j<3e5),c.push(I);let R=c.length>5?6e4:0;u&&clearTimeout(u),u=setTimeout(()=>{u=null,n||h()},R),u.unref?.()},p=()=>{if(o)try{o.kill("SIGKILL")}catch{}};l=setInterval(()=>{if(n||!o||Date.now()-s<3e4)return;let w=ve(t);w!=null&&Date.now()-w>3e4&&p()},1e4),l.unref?.();let S=w=>{if(n=!0,l&&(clearInterval(l),l=null),u&&(clearTimeout(u),u=null),o)try{o.kill(w)}catch{}else E()};m=()=>S("SIGINT"),d=()=>S("SIGTERM"),process.on("SIGINT",m),process.on("SIGTERM",d),h()})}function Qe(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 Xt(e,t){return Qe(e,t)>0}async function Jt(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{H as AGENT_VERSION,he as AgentController,me as blockAdmin,kt as canCaptureScreenshot,Mt as captureScreenshot,Qe as compareVersions,ye as defaultHeartbeatPath,de as fetchAdmins,Jt as fetchLatestDist,Xt as isNewerVersion,vt as orderAdmins,ve as readHeartbeat,J as register,bt as resolveAdminIdentifier,ce as runConnectionLoop,Kt as runSupervisor,Bt as startHeartbeat,Ye as stopHeartbeat,ge as unblockAdmin};
|
package/dist/src/display.d.ts
CHANGED
|
@@ -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
|
|
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;
|
|
@@ -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;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AGENT_VERSION = "1.0.
|
|
1
|
+
export declare const AGENT_VERSION = "1.0.20";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aicommander/agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
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",
|
|
@@ -40,6 +40,19 @@
|
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc && node scripts/build-npm.mjs",
|
|
45
|
+
"build:npm": "node scripts/build-npm.mjs",
|
|
46
|
+
"prepublishOnly": "tsc && node scripts/build-npm.mjs",
|
|
47
|
+
"compile": "pnpm run compile:linux-x64 && pnpm run compile:linux-arm64",
|
|
48
|
+
"compile:linux-x64": "bun build ./bin/agent.ts --compile --minify --target=bun-linux-x64 --outfile dist-bin/aicommander-agent-linux-x64",
|
|
49
|
+
"compile:linux-arm64": "bun build ./bin/agent.ts --compile --minify --target=bun-linux-arm64 --outfile dist-bin/aicommander-agent-linux-arm64",
|
|
50
|
+
"dev": "tsx bin/agent.ts",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"start": "AICOMMANDER_SERVER=https://aicommander.dev node dist/bin/agent.js",
|
|
53
|
+
"start:root": "sudo -E AICOMMANDER_SERVER=https://aicommander.dev node dist/bin/agent.js",
|
|
54
|
+
"test": "vitest run"
|
|
55
|
+
},
|
|
43
56
|
"dependencies": {
|
|
44
57
|
"chalk": "5.6.2",
|
|
45
58
|
"commander": "12.1.0",
|
|
@@ -47,27 +60,15 @@
|
|
|
47
60
|
"ws": "8.21.0"
|
|
48
61
|
},
|
|
49
62
|
"devDependencies": {
|
|
63
|
+
"@aicommander/protocol": "workspace:*",
|
|
50
64
|
"@types/node": "22.19.21",
|
|
51
65
|
"@types/ws": "8.18.1",
|
|
52
66
|
"esbuild": "0.28.1",
|
|
53
67
|
"tsx": "^4.22.4",
|
|
54
68
|
"typescript": "5.9.3",
|
|
55
|
-
"vitest": "3.2.6"
|
|
56
|
-
"@aicommander/protocol": "1.0.0"
|
|
69
|
+
"vitest": "3.2.6"
|
|
57
70
|
},
|
|
58
71
|
"engines": {
|
|
59
72
|
"node": ">=18.0.0"
|
|
60
|
-
},
|
|
61
|
-
"scripts": {
|
|
62
|
-
"build": "tsc && node scripts/build-npm.mjs",
|
|
63
|
-
"build:npm": "node scripts/build-npm.mjs",
|
|
64
|
-
"compile": "pnpm run compile:linux-x64 && pnpm run compile:linux-arm64",
|
|
65
|
-
"compile:linux-x64": "bun build ./bin/agent.ts --compile --minify --target=bun-linux-x64 --outfile dist-bin/aicommander-agent-linux-x64",
|
|
66
|
-
"compile:linux-arm64": "bun build ./bin/agent.ts --compile --minify --target=bun-linux-arm64 --outfile dist-bin/aicommander-agent-linux-arm64",
|
|
67
|
-
"dev": "tsx bin/agent.ts",
|
|
68
|
-
"typecheck": "tsc --noEmit",
|
|
69
|
-
"start": "AICOMMANDER_SERVER=https://aicommander.dev node dist/bin/agent.js",
|
|
70
|
-
"start:root": "sudo -E AICOMMANDER_SERVER=https://aicommander.dev node dist/bin/agent.js",
|
|
71
|
-
"test": "vitest run"
|
|
72
73
|
}
|
|
73
|
-
}
|
|
74
|
+
}
|
package/dist/bin/agent.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../bin/agent.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,EACb,YAAY,EACZ,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAE7B,OAAO;KACJ,IAAI,CAAC,mBAAmB,CAAC;KACzB,WAAW,CAAC,gDAAgD,CAAC;KAC7D,OAAO,CAAC,aAAa,CAAC,CAAC;AAE1B,kEAAkE;AAClE,OAAO;KACJ,OAAO,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;KACnC,WAAW,CAAC,gEAAgE,CAAC;KAC7E,MAAM,CAAC,KAAK,IAAI,EAAE;IACjB,MAAM,UAAU,EAAE,CAAC;AACrB,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,SAAS,CAAC;KAClB,WAAW,CAAC,gEAAgE,CAAC;KAC7E,MAAM,CAAC,gBAAgB,EAAE,8CAA8C,CAAC;KACxE,MAAM,CAAC,KAAK,EAAE,IAAyB,EAAE,EAAE;IAC1C,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,6CAA6C,CAAC;KAC1D,MAAM,CAAC,UAAU,EAAE,+CAA+C,CAAC;KACnE,MAAM,CAAC,KAAK,EAAE,IAA0B,EAAE,EAAE;IAC3C,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,oCAAoC,CAAC;KACjD,MAAM,CAAC,GAAG,EAAE;IACX,SAAS,EAAE,CAAC;AACd,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,SAAS,CAAC;KAClB,WAAW,CAAC,oCAAoC,CAAC;KACjD,MAAM,CAAC,GAAG,EAAE;IACX,UAAU,EAAE,CAAC;AACf,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,aAAa,CAAC;KACtB,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;KACvB,WAAW,CAAC,8FAA8F,CAAC;KAC3G,MAAM,CAAC,WAAW,EAAE,8BAA8B,CAAC;KACnD,MAAM,CAAC,KAAK,EAAE,IAAuB,EAAE,EAAE;IACxC,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,aAAa,CAAC;KACtB,WAAW,CAAC,kDAAkD,CAAC;KAC/D,MAAM,CAAC,KAAK,IAAI,EAAE;IACjB,MAAM,aAAa,EAAE,CAAC;AACxB,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,uBAAuB,CAAC;KAChC,OAAO,CAAC,CAAC,cAAc,CAAC,CAAC;KACzB,WAAW,CAAC,2FAA2F,CAAC;KACxG,MAAM,CAAC,KAAK,EAAE,OAAe,EAAE,EAAE;IAChC,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC;AAC/B,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,yBAAyB,CAAC;KAClC,WAAW,CAAC,2DAA2D,CAAC;KACxE,MAAM,CAAC,KAAK,EAAE,OAAe,EAAE,EAAE;IAChC,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;AACjC,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,WAAW,CAAC;KACpB,WAAW,CAAC,sDAAsD,CAAC;KACnE,MAAM,CAAC,aAAa,EAAE,+BAA+B,CAAC;KACtD,MAAM,CAAC,CAAC,IAAwB,EAAE,EAAE;IACnC,YAAY,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACjC,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|