@aicommander/agent 1.0.26 → 1.0.30

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/README.md CHANGED
@@ -69,23 +69,32 @@ Manage the service:
69
69
  | `sudo aicommander-agent uninstall --force` | Stop, disable, and remove everything |
70
70
  | `journalctl -u aicommander-agent -f` | Follow service logs |
71
71
 
72
- ## npm channel vs the curl installer
72
+ Service logs are payload-free: journald may receive operation start/end, exit
73
+ code, duration, and the printable executable basename for secure exec. It never
74
+ receives the full shell command, secure-exec argv, cwd, env, input, stdout, or
75
+ stderr. The library controller used by the desktop app is silent; an explicit
76
+ foreground TTY run uses the same safe summary and does not print command payloads.
77
+ Remote stdout/stderr still streams to the requesting client.
73
78
 
74
- | | `npx` / `npm i -g` (this package) | `curl -fsSL https://aicommander.dev/install \| sudo bash` |
79
+ ## npm channel vs the signed native installer
80
+
81
+ | | `npx` / `npm i -g` (this package) | verified native release installer |
75
82
  |---|---|---|
76
83
  | Runtime | Your Node ≥ 18 | Self-contained native binary (no Node) |
77
- | Best for | Ephemeral / dev / CI / Node-managed hosts | Production root installs |
84
+ | Best for | Ephemeral / dev / CI / Node-managed hosts | Linux systemd installs |
78
85
  | Integrity | npm's tarball integrity + registry-side checksums | **SHA-256 checksum enforced** before install |
79
- | Authenticity | | **Ed25519 signature verified against a pinned key** |
80
- | Update | `npm i -g @aicommander/agent@latest` | Re-run the curl command |
86
+ | Authenticity | npm registry trust | Installer + binary **Ed25519 signatures**, when the key fingerprint is independently confirmed |
87
+ | Update | `npm i -g @aicommander/agent@latest` | Repeat the verified download flow |
81
88
 
82
89
  ## Security
83
90
 
84
- This npm package relies on **npm's tarball integrity** for trust. The
85
- `curl | sudo bash` installer additionally verifies the downloaded binary
86
- against a **pinned Ed25519 public key** before installing it as root — so the
87
- **curl installer remains the canonical, recommended path for production root
88
- installs**.
91
+ This npm package relies on **npm registry/tarball integrity** for trust. The
92
+ native release flow verifies a signed, versioned installer before `sudo`, then
93
+ verifies the downloaded binary against the same Ed25519 key and enforces its
94
+ SHA-256 checksum. Follow the current steps at
95
+ <https://aicommander.dev/howto/#install-agent>; never pipe `/install` directly
96
+ into `sudo bash`. The Ed25519 guarantee is meaningful only after confirming the
97
+ key fingerprint through a channel independent of the download website.
89
98
 
90
99
  Prefer this npm package for **ephemeral / dev use** and Node-managed
91
100
  environments. In all cases: the session code is a **root-exec credential** —
@@ -93,6 +102,36 @@ anyone who has it can run commands as root on the machine. Keep it secret, don't
93
102
  paste it into shared chats/screenshots/tickets, and rotate it with
94
103
  `aicommander-agent change-code` if it leaks.
95
104
 
105
+ The relay connection does not put that code or the reusable `agentToken` in its
106
+ WebSocket URL. The agent authenticates a fixed-URL POST with
107
+ `Authorization: Bearer`, receives a 30-second single-use opaque connection
108
+ ticket, then opens the ticket URL with the same Bearer header. Relay errors and
109
+ local reconnect output do not include the Authorization value.
110
+
111
+ **Local credential files:** the Linux CLI and systemd service persist
112
+ `session.json` (session code + agent token) under `/etc/aicommander-agent` with
113
+ a `~/.config` fallback, and stamp live metadata in `/var/run/aicommander-agent/state.json`.
114
+ Both use atomic writes and owner-only `0700`/`0600` modes; systemd installs set
115
+ `AICOMMANDER_SERVICE=1` and fail closed if persistence breaks. The desktop app
116
+ keeps the session code in `session.json` but stores the reusable agent token in
117
+ OS-protected Electron `safeStorage` (`session.token`); it refuses to start when
118
+ that encryption is unavailable.
119
+
120
+ The separate service-token **secure exec** path is Linux-only and drops every
121
+ command to the installer-created `aicommander-exec` account. New accounts have no
122
+ supplementary groups and an owner-only (`0700`) home. Before any target process
123
+ spawns, the agent rejects known shells/interpreters/argument runners and
124
+ privilege/container/orchestration clients even if a hostile relay claims they
125
+ are allowlisted. It also parses local `/etc/passwd` and `/etc/group` without a
126
+ shell and refuses privileged primary or supplementary membership (`root`,
127
+ `wheel`, `sudo`, `docker`, `podman`, `lxd`, `incus`, `disk`, `libvirt`, or gid
128
+ `0`). Missing, unreadable, malformed, duplicate, or otherwise ambiguous identity
129
+ data fails closed.
130
+
131
+ The command denylist is intentionally not a complete sandbox policy: ordinary
132
+ tools can gain subprocess features over time. The non-root uid/group boundary,
133
+ not the basename allowlist or denylist, remains the containment mechanism.
134
+
96
135
  ## License
97
136
 
98
137
  MIT
package/dist/bin/agent.js CHANGED
@@ -1,25 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import{program as _}from"commander";var O="1.0.26";import dn from"os";function un(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=un(s)}status;body;code};async function Re(e,t,r={}){let s={hostname:dn.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 _n from"ws";import In from"os";import{spawn as ze}from"child_process";var k="https://aicommander.dev";var v="aicommander-exec";var qe="ABCDEFGHJKMNPQRSTVWXYZ0123456789";var ur=Math.floor(256/qe.length)*qe.length;var mn=/^AIC-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}$/;function pn(e){return mn.test(e.trim().toUpperCase())}function j(e){return pn(e)?`AIC-${e.slice(4,8)}-***-***`:e}function Ze(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 l=(r[o]??0)-(s[o]??0);if(l!==0)return l>0?1:-1}return 0}function Qe(e,t){return Ze(e,t)>0}var et=process.platform==="win32";function tt(e,t,r,s){let i=Date.now(),o=ze(e,[],{shell:!0,cwd:t??process.env.HOME,env:{...process.env,...r??{}},stdio:["ignore","pipe","pipe"],detached:!et}),l=!1,a=null,d=()=>{a&&(clearTimeout(a),a=null)};o.stdout?.on("data",f=>{s.onOutput(f.toString("base64"),"stdout")}),o.stderr?.on("data",f=>{s.onOutput(f.toString("base64"),"stderr")}),o.on("close",f=>{l=!0,d(),s.onDone(f??-1,Date.now()-i)}),o.on("error",f=>{l=!0,d(),s.onError(f.message)});let c=f=>{if(!(l||o.pid==null))try{et?ze("taskkill",["/pid",String(o.pid),"/T","/F"],{stdio:"ignore"}):process.kill(-o.pid,f)}catch{}};return{kill:()=>{l||(c("SIGTERM"),d(),a=setTimeout(()=>{a=null,c("SIGKILL")},5e3),a.unref?.())}}}import{spawn as En}from"node:child_process";import{readFileSync as vn}from"node:fs";import{isAbsolute as bn}from"node:path";import{spawn as fn}from"node:child_process";import{createRequire as gn}from"node:module";import{constants as Sn}from"node:os";var hn=gn(import.meta.url),Te="__secure-exec-drop",yn="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";function nt(){let e=!1;try{e=hn("node:sea").isSea()}catch{}return e}function rt(e){let t=process.argv[1],r=!nt()&&t?[t,Te,...e]:[Te,...e];return{cmd:process.execPath,args:r}}function ot(e){let t=nt()?1:2;if(e[t]!==Te)return!1;let[r,s,i,o,...l]=e.slice(t+1),a=Number(r),d=Number(s),c=process;try{if(!Number.isInteger(a)||!Number.isInteger(d)||a===0||!i||!o)throw new Error("invalid drop arguments");if(typeof c.initgroups!="function"||typeof c.setgid!="function"||typeof c.setuid!="function")throw new Error("privilege-drop syscalls unavailable on this platform");if(c.initgroups(i,d),c.setgid(d),c.setuid(a),c.getuid?.()===0||c.getgid?.()===0)throw new Error("privilege drop did not take effect");if(typeof c.getgroups=="function"){let p=c.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 f={...process.env,PATH:yn},m=fn(o,l,{stdio:"inherit",env:f,shell:!1}),u=!1;return m.on("error",p=>{u||(u=!0,process.stderr.write(`secure-exec: spawn failed: ${p.message}
4
- `),process.exit(127))}),m.on("exit",(p,S)=>{if(!u){if(u=!0,S){let A=Sn.signals[S];process.exit(128+(typeof A=="number"?A:0))}process.exit(p??0)}}),!0}function wn(){let e;try{e=vn("/etc/passwd","utf8")}catch{throw new Error("cannot read /etc/passwd to resolve the secure-exec user")}let t=e.split(`
5
- `).filter(d=>d.startsWith(`${v}:`));if(t.length===0)throw new Error(`secure exec user "${v}" not found in /etc/passwd \u2014 reinstall the agent to create it`);if(t.length>1)throw new Error(`multiple /etc/passwd entries for "${v}" \u2014 refusing to guess which is the sandbox user`);let r=t[0].split(":");if(r.length!==7)throw new Error(`malformed passwd entry for ${v}`);let s=r[2],i=r[3];if(!/^\d+$/.test(s)||!/^\d+$/.test(i))throw new Error(`could not resolve uid/gid for ${v}`);let o=Number(s),l=Number(i),a=r[5]||"/tmp";if(!Number.isInteger(o)||!Number.isInteger(l))throw new Error(`could not resolve uid/gid for ${v}`);if(o===0)throw new Error("refusing to run secure exec as uid 0");if(l===0)throw new Error("refusing to run secure exec as gid 0");return{uid:o,gid:l,home:a,name:v}}var Rn="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",An=new Set(["NODE_OPTIONS","BASH_ENV","ENV","IFS","PYTHONSTARTUP","PYTHONPATH","PERL5OPT","RUBYOPT","GIT_EXTERNAL_DIFF","PATH","HOME","USER","LOGNAME"]),Tn=e=>/^LD_/.test(e)||An.has(e);function it(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&&!bn(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=wn(),l={HOME:o.home,USER:o.name,LOGNAME:o.name,PATH:Rn},a={};for(let[g,U]of Object.entries(e.env??{}))Tn(g)||(a[g]=U);let d={...a,...l},c=Date.now(),{cmd:f,args:m}=rt([String(o.uid),String(o.gid),o.name,s,...r.slice(1)]),u=En(f,m,{cwd:e.cwd??o.home,env:d,stdio:["pipe","pipe","pipe"],detached:!0,shell:!1});i&&u.stdin?(u.stdin.on("error",()=>{}),u.stdin.end(i)):u.stdin?.end();let p=!1,S=null,A=()=>{S&&(clearTimeout(S),S=null)};u.stdout?.on("data",g=>{t.onOutput(g.toString("base64"),"stdout")}),u.stderr?.on("data",g=>{t.onOutput(g.toString("base64"),"stderr")}),u.on("close",g=>{p=!0,A(),t.onDone(g??-1,Date.now()-c)}),u.on("error",g=>{p=!0,A(),t.onError(g.message)});let T=g=>{if(!(p||u.pid==null))try{process.kill(-u.pid,g)}catch{}};return{kill:()=>{p||(T("SIGTERM"),A(),S=setTimeout(()=>{S=null,T("SIGKILL")},5e3),S.unref?.())}}}import w from"chalk";function at(e,t,r=!1){console.clear(),console.log(),console.log(w.bold.cyan(" AI Commander \u2014 Remote Agent")),console.log(),console.log(w.gray(" Session code:")),console.log(),console.log(w.bold.greenBright(` ${r?e:j(e)}`)),console.log(),r||console.log(w.gray(" Reveal the full code: sudo aicommander-agent status --reveal")),console.log(w.gray(" Then give it to your admin to connect.")),console.log(w.yellow(" \u26A0 Keep this code secret \u2014 anyone who has it can run commands as root here.")),console.log(w.gray(` Server: ${t}`)),console.log(w.gray(" Press Ctrl+C to disconnect.")),console.log(),console.log(w.yellow(" Waiting for admin connection...")),console.log()}function ct(e,t){console.log(w.yellow(` Reconnecting in ${Math.round(t/1e3)}s (attempt ${e})...`))}function lt(){console.log(w.green(" Connected to relay \u2014 ready for commands."))}function xe(e){console.log(w.cyan(` > ${e}`))}function _e(e,t){let r=e===0?w.green:w.red;console.log(r(` Done. Exit ${e} in ${t}ms`))}function xn(){let e=process.env.AICOMMANDER_DEV;return e==="1"||e==="true"}function ut(e){let t=e.toLowerCase();return t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="[::1]"}function dt(){return new URL(k).origin}function mt(e){let t=(e??"").trim();if(!t)return k;let r;try{r=new URL(t)}catch{return console.warn(`
6
- \u26A0 Ignoring malformed AICOMMANDER_SERVER (${t}); using ${k}.
7
- `),k}if(r.origin===dt())return k;let s=ut(r.hostname);return!xn()&&!s?(console.warn(`
8
- \u26A0 AICOMMANDER_SERVER override to "${r.origin}" ignored for safety \u2014
9
- the agent runs as root and is host-locked to ${dt()}.
2
+ import{program as k}from"commander";var L="1.0.30";import Mn from"os";function Ln(e){try{let t=JSON.parse(e);return typeof t.code=="string"?t.code:null}catch{return null}}var Me=class extends Error{constructor(n,r){super(`Registration failed (${n}): ${r}`);this.status=n;this.body=r;this.name="RegistrationError",this.code=Ln(r)}status;body;code};async function Le(e,t,n={}){let r={hostname:Mn.hostname(),platform:process.platform,arch:process.arch,agentVersion:L,...t?{deviceId:t.deviceId,deviceSecret:t.deviceSecret}:{},...n.forceNew?{forceNew:!0}:n.currentCode?{currentCode:n.currentCode}:{}},s=await fetch(`${e}/api/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)});if(!s.ok){let i=await s.text();throw new Me(s.status,i)}return s.json()}import rr from"ws";import or from"os";import{spawn as Rt}from"child_process";var C="https://aicommander.dev";var S="aicommander-exec",$n=["sh","bash","dash","zsh","fish","ash","ksh","mksh","csh","tcsh","python","python2","python3","pypy","pypy3","node","nodejs","perl","ruby","php","lua","env","busybox","find","xargs","awk","gawk","mawk","nawk","sed","sudo","su","doas","pkexec","docker","podman","nerdctl","kubectl"],Pn=new Set($n),Un=/^(?:python[23]\.\d+(?:\.\d+)*|pypy[23]\.\d+(?:\.\d+)*|perl\d+(?:\.\d+)*|ruby\d+(?:\.\d+)*|php\d+(?:\.\d+)*|lua\d+(?:\.\d+)*)$/;function ft(e){return Pn.has(e)||Un.test(e)}var pt=["root","wheel","sudo","docker","podman","lxd","incus","disk","libvirt"],$e=262144;var Pe=10*1024*1024,Ue=512*1024,Ur=1440*60*1e3,gt=5e3,St=1e4,Fe=3e4,ht=5,yt=3e5,Et=6e4,ue=5e3,re=3e5;var Fr=3600*1e3,Hr=600*1e3,Vr=480*60*1e3;var vt="ABCDEFGHJKMNPQRSTVWXYZ0123456789";var Br=Math.floor(256/vt.length)*vt.length;var Fn=/^AIC-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}-[ABCDEFGHJKMNPQRSTVWXYZ0-9]{4}$/;function Hn(e){return Fn.test(e.trim().toUpperCase())}function Y(e){return Hn(e)?`AIC-${e.slice(4,8)}-***-***`:e}function wt(e,t){let n=e.split(".").map(i=>parseInt(i,10)||0),r=t.split(".").map(i=>parseInt(i,10)||0),s=Math.max(n.length,r.length);for(let i=0;i<s;i++){let c=(n[i]??0)-(r[i]??0);if(c!==0)return c>0?1:-1}return 0}function bt(e,t){return wt(e,t)>0}var xt=process.platform==="win32";function At(e,t,n,r){let s=Date.now(),i=Rt(e,[],{shell:!0,cwd:t??process.env.HOME,env:{...process.env,...n??{}},stdio:["ignore","pipe","pipe"],detached:!xt}),c=!1,a=null,d=()=>{a&&(clearTimeout(a),a=null)};i.stdout?.on("data",m=>{r.onOutput(m.toString("base64"),"stdout")}),i.stderr?.on("data",m=>{r.onOutput(m.toString("base64"),"stderr")}),i.on("close",m=>{c=!0,d(),r.onDone(m??-1,Date.now()-s)}),i.on("error",m=>{c=!0,d(),r.onError(m.message)});let l=m=>{if(!(c||i.pid==null))try{xt?Rt("taskkill",["/pid",String(i.pid),"/T","/F"],{stdio:"ignore"}):process.kill(-i.pid,m)}catch{}};return{kill:()=>{c||(l("SIGTERM"),d(),a=setTimeout(()=>{a=null,l("SIGKILL")},ue),a.unref?.())}}}import{spawn as Kn}from"node:child_process";import{readFileSync as Ct}from"node:fs";import{isAbsolute as Xn}from"node:path";import{spawn as Vn}from"node:child_process";import{createRequire as Gn}from"node:module";import{constants as Bn}from"node:os";var Wn=Gn(import.meta.url),He="__secure-exec-drop",jn="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";function Tt(){let e=!1;try{e=Wn("node:sea").isSea()}catch{}return e}function kt(e){let t=process.argv[1],n=!Tt()&&t?[t,He,...e]:[He,...e];return{cmd:process.execPath,args:n}}function _t(e){let t=Tt()?1:2;if(e[t]!==He)return!1;let[n,r,s,i,...c]=e.slice(t+1),a=Number(n),d=Number(r),l=process;try{if(!/^\d+$/.test(n??"")||!/^\d+$/.test(r??"")||!Number.isSafeInteger(a)||!Number.isSafeInteger(d)||a<=0||d<=0||a>4294967295||d>4294967295||s!==S||!i)throw new Error("invalid drop arguments");if(typeof l.initgroups!="function"||typeof l.setgid!="function"||typeof l.setuid!="function"||typeof l.getuid!="function"||typeof l.getgid!="function"||typeof l.getgroups!="function")throw new Error("privilege-drop syscalls unavailable on this platform");if(l.initgroups(s,d),l.setgid(d),l.setuid(a),l.getuid()!==a||l.getgid()!==d)throw new Error("privilege drop did not take effect");let f=l.getgroups();if(!Array.isArray(f)||f.some(p=>!Number.isInteger(p)||p<0)||f.includes(0))throw new Error("privilege drop left unsafe supplementary groups in place")}catch(f){process.stderr.write(`secure-exec: privilege drop failed: ${f instanceof Error?f.message:String(f)}
3
+ `),process.exit(127)}let m={...process.env,PATH:jn},y=Vn(i,c,{stdio:"inherit",env:m,shell:!1}),u=!1;return y.on("error",f=>{u||(u=!0,process.stderr.write(`secure-exec: spawn failed: ${f.message}
4
+ `),process.exit(127))}),y.on("exit",(f,p)=>{if(!u){if(u=!0,p){let E=Bn.signals[p];process.exit(128+(typeof E=="number"?E:0))}process.exit(f??0)}}),!0}var Jn=4294967295,Yn=new Set(pt);function Ve(e,t){if(!/^\d+$/.test(e))throw new Error(`invalid numeric id in ${t}`);let n=Number(e);if(!Number.isSafeInteger(n)||n<0||n>Jn)throw new Error(`invalid numeric id in ${t}`);return n}function qn(){let e;try{e=Ct("/etc/group","utf8")}catch{throw new Error("cannot read /etc/group to verify secure-exec group membership")}let t=[],n=new Set,r=new Set;for(let[s,i]of e.split(`
5
+ `).entries()){if(i==="")continue;let c=i.split(":");if(c.length!==4)throw new Error(`malformed /etc/group entry at line ${s+1}`);let a=c[0],d=Ve(c[2],`/etc/group line ${s+1}`),l=c[3];if(!a||/[\s,\u0000-\u001f\u007f]/.test(a))throw new Error(`malformed /etc/group entry at line ${s+1}`);let m=l===""?[]:l.split(",");if(m.some(y=>!y||/[\s:\u0000-\u001f\u007f]/.test(y))||new Set(m).size!==m.length)throw new Error(`malformed /etc/group member list at line ${s+1}`);if(n.has(a)||r.has(d))throw new Error(`duplicate /etc/group name or gid at line ${s+1}`);n.add(a),r.add(d),t.push({name:a,gid:d,members:m})}return t}function Zn(e){let t=qn();if(!t.some(s=>s.gid===e.gid))throw new Error(`primary group ${e.gid} for ${S} is not in /etc/group`);let r=t.filter(s=>s.gid===e.gid||s.members.includes(e.name)).find(s=>s.gid===0||Yn.has(s.name));if(r)throw new Error(`secure exec user "${S}" belongs to privileged group "${r.name}" (gid ${r.gid})`)}function zn(){let e;try{e=Ct("/etc/passwd","utf8")}catch{throw new Error("cannot read /etc/passwd to resolve the secure-exec user")}let t=e.split(`
6
+ `).filter(a=>a.startsWith(`${S}:`));if(t.length===0)throw new Error(`secure exec user "${S}" not found in /etc/passwd \u2014 reinstall the agent to create it`);if(t.length>1)throw new Error(`multiple /etc/passwd entries for "${S}" \u2014 refusing to guess which is the sandbox user`);let n=t[0].split(":");if(n.length!==7)throw new Error(`malformed passwd entry for ${S}`);let r,s;try{r=Ve(n[2],"/etc/passwd uid"),s=Ve(n[3],"/etc/passwd gid")}catch{throw new Error(`could not resolve uid/gid for ${S}`)}let i=n[5]||"/tmp";if(r===0)throw new Error("refusing to run secure exec as uid 0");if(s===0)throw new Error("refusing to run secure exec as gid 0");let c={uid:r,gid:s,home:i,name:S};return Zn(c),c}var Qn="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",er=new Set(["NODE_OPTIONS","BASH_ENV","ENV","IFS","PYTHONSTARTUP","PYTHONPATH","PERL5OPT","RUBYOPT","GIT_EXTERNAL_DIFF","PATH","HOME","USER","LOGNAME"]),tr=e=>/^LD_/.test(e)||er.has(e);function It(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 n=e.argv;if(!Array.isArray(n)||n.length===0||typeof n[0]!="string")throw new Error("secure exec requires a non-empty argv");let r=n[0];if(!r||/[/\\\s]/.test(r))throw new Error(`command "${r}" must be a bare command name (no path separators or whitespace)`);if(ft(r))throw new Error(`command "${r}" is denied for secure exec`);if(!e.allowedCommands.includes(r))throw new Error(`command "${r}" is not in this token's allowlist`);if(e.cwd!=null&&!Xn(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>$e))throw new Error(`stdin payload ${s.length}B exceeds limit ${$e}B`);let i=zn(),c={HOME:i.home,USER:i.name,LOGNAME:i.name,PATH:Qn},a={};for(let[g,I]of Object.entries(e.env??{}))tr(g)||(a[g]=I);let d={...a,...c},l=Date.now(),{cmd:m,args:y}=kt([String(i.uid),String(i.gid),i.name,r,...n.slice(1)]),u=Kn(m,y,{cwd:e.cwd??i.home,env:d,stdio:["pipe","pipe","pipe"],detached:!0,shell:!1});s&&u.stdin?(u.stdin.on("error",()=>{}),u.stdin.end(s)):u.stdin?.end();let f=!1,p=null,E=()=>{p&&(clearTimeout(p),p=null)};u.stdout?.on("data",g=>{t.onOutput(g.toString("base64"),"stdout")}),u.stderr?.on("data",g=>{t.onOutput(g.toString("base64"),"stderr")}),u.on("close",g=>{f=!0,E(),t.onDone(g??-1,Date.now()-l)}),u.on("error",g=>{f=!0,E(),t.onError(g.message)});let G=g=>{if(!(f||u.pid==null))try{process.kill(-u.pid,g)}catch{}};return{kill:()=>{f||(G("SIGTERM"),E(),p=setTimeout(()=>{p=null,G("SIGKILL")},ue),p.unref?.())}}}import R from"chalk";function Dt(e,t,n=!1){console.clear(),console.log(),console.log(R.bold.cyan(" AI Commander \u2014 Remote Agent")),console.log(),console.log(R.gray(" Session code:")),console.log(),console.log(R.bold.greenBright(` ${n?e:Y(e)}`)),console.log(),n||console.log(R.gray(" Reveal the full code: sudo aicommander-agent status --reveal")),console.log(R.gray(" Then give it to your admin to connect.")),console.log(R.yellow(" \u26A0 Keep this code secret \u2014 anyone who has it can run commands as root here.")),console.log(R.gray(` Server: ${t}`)),console.log(R.gray(" Press Ctrl+C to disconnect.")),console.log(),console.log(R.yellow(" Waiting for admin connection...")),console.log()}function Ot(e,t){console.log(R.yellow(` Reconnecting in ${Math.round(t/1e3)}s (attempt ${e})...`))}function Nt(){console.log(R.green(" Connected to relay \u2014 ready for commands."))}function Ge(e,t){let n=e==="secure-exec"&&t!=null&&/^[A-Za-z0-9][A-Za-z0-9._+@-]{0,127}$/.test(t)?t:null,r=e==="secure-exec"?"Secure exec":"Command",s=n?` (${n})`:"";console.log(R.cyan(` ${r}${s} started.`))}function Be(e,t,n){let r=t===0?R.green:R.red;console.log(r(` ${e==="secure-exec"?"Secure exec":"Command"} finished. Exit ${t} in ${n}ms.`))}function nr(){let e=process.env.AICOMMANDER_DEV;return e==="1"||e==="true"}function Lt(e){let t=e.toLowerCase();return t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="[::1]"}function Mt(){return new URL(C).origin}function $t(e){let t=(e??"").trim();if(!t)return C;let n;try{n=new URL(t)}catch{return console.warn(`
7
+ \u26A0 Ignoring malformed AICOMMANDER_SERVER (${t}); using ${C}.
8
+ `),C}if(n.origin===Mt())return C;let r=Lt(n.hostname);return!nr()&&!r?(console.warn(`
9
+ \u26A0 AICOMMANDER_SERVER override to "${n.origin}" ignored for safety \u2014
10
+ the agent runs as root and is host-locked to ${Mt()}.
10
11
  Set AICOMMANDER_DEV=1 to allow a custom relay for development.
11
- `),k):r.protocol!=="https:"&&!s?(console.warn(`
12
- \u26A0 Refusing plaintext relay "${r.origin}" (https required); using ${k}.
13
- `),k):t.replace(/\/+$/,"")}function pt(e){let t;try{t=new URL(e)}catch{throw new Error("Refusing to connect: malformed relay WebSocket URL.")}if(t.protocol!=="wss:"&&!(t.protocol==="ws:"&&ut(t.hostname)))throw new Error(`Refusing to open a plaintext relay socket (${t.protocol}//${t.host}); wss:// required.`)}var Ie={capable:!1,enabled:!1,expiresAt:null},St=360*60*1e3;function kn(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 ht(e){let t=1e3,r=3e4,s=0,i=e.agentToken;for(;!e.signal?.aborted;){s++;try{let o=await Cn(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 l=.8+Math.random()*.4,a=Math.min(t*l,r);e.silent||ct(s,a);try{await kn(a,e.signal??new AbortController().signal)}catch(d){if(d.name==="AbortError")break}t=Math.min(t*2,r)}}}async function Cn(e,t){let{serverUrl:r,sessionCode:s}=e,i=`${r.replace(/^http/,"ws")}/ws/agent/${s}?token=${t}`;pt(i);let o=c=>{try{e.onStatus?.(c)}catch{}},l=c=>{try{e.onActivity?.(c)}catch{}},a=c=>{if(c)try{e.onRemoteActivity?.(c)}catch{}},d=()=>{try{e.onHeartbeat?.()}catch{}};return o("connecting"),new Promise((c,f)=>{let m=new _n(i),u=null,p=null,S=null,A=!1,T=null,g=!1,U=()=>{S&&(clearTimeout(S),S=null)},y=()=>{T&&(clearInterval(T),T=null)},V=()=>{if(!e.reauthIntervalMs||!e.reauth)return;let E=()=>{if(u){S=setTimeout(E,3e4);return}A=!0,m.close(1e3,"reauth")};S=setTimeout(E,e.reauthIntervalMs)},Z=()=>{g||(g=!0,U(),y(),c(A))},Q=E=>{g||(g=!0,U(),y(),f(E))},Xe=()=>{d(),p&&clearTimeout(p),p=setTimeout(()=>{m.terminate(),Q(new Error("Ping timeout"))},9e4)};e.signal?.addEventListener("abort",()=>m.close(1e3,"disabled"),{once:!0});let ye=()=>{if(g)return;let E={type:"agent:screen_state",screenShare:e.screenShare?.getState()??Ie};try{m.send(JSON.stringify(E))}catch{}};m.on("open",()=>{o("connected"),Xe(),V();let E={type:"agent:register",hostname:In.hostname(),platform:process.platform,arch:process.arch,agentVersion:O,screenShare:e.screenShare?.getState()??Ie};m.send(JSON.stringify(E)),e.screenShare?.on("change",ye),e.silent||lt()}),m.on("message",E=>{let R;try{R=JSON.parse(String(E))}catch{return}switch(R.type){case"do:ping":Xe();let cn={type:"agent:pong",ts:R.ts};m.send(JSON.stringify(cn));break;case"do:exec":{xe(R.command),l(!0),a(R.operator);let{commandId:D,command:W,cwd:Ee,env:ve}=R,oe=Date.now(),z=!1;y(),T=setInterval(()=>{let x=Date.now()-oe;if(x>=3e5&&!z){z=!0;let h={type:"agent:exec_idle",commandId:D,idleMs:x};m.send(JSON.stringify(h))}},3e5),T.unref?.(),u=tt(W,Ee,ve,{onOutput:(x,h)=>{oe=Date.now(),z=!1;let I={type:"agent:output",commandId:D,chunk:x,stream:h};m.send(JSON.stringify(I))},onDone:(x,h)=>{u=null,y(),l(!1),_e(x,h);let I={type:"agent:done",commandId:D,exitCode:x,durationMs:h};m.send(JSON.stringify(I))},onError:x=>{u=null,y(),l(!1);let h={type:"agent:error",commandId:D,error:x};m.send(JSON.stringify(h))}}).kill;break}case"do:secure_exec":{let{commandId:D,argv:W,allowedCommands:Ee,input:ve,cwd:oe,env:z}=R,Ye=(h,I)=>{u=null,y(),l(!1),_e(h,I);let ee={type:"agent:done",commandId:D,exitCode:h,durationMs:I};m.send(JSON.stringify(ee))},x=h=>{u=null,y(),l(!1);let I={type:"agent:error",commandId:D,error:h};m.send(JSON.stringify(I))};try{let h=Array.isArray(W)?W.join(" "):String(W);xe(h),l(!0);let I=Date.now(),ee=!1;y(),T=setInterval(()=>{let se=Date.now()-I;if(se>=3e5&&!ee){ee=!0;let be={type:"agent:exec_idle",commandId:D,idleMs:se};m.send(JSON.stringify(be))}},3e5),T.unref?.(),u=it({argv:W,allowedCommands:Ee,input:ve,cwd:oe,env:z},{onOutput:(se,be)=>{I=Date.now(),ee=!1;let ln={type:"agent:output",commandId:D,chunk:se,stream:be};m.send(JSON.stringify(ln))},onDone:Ye,onError:x}).kill}catch(h){x(h instanceof Error?h.message:String(h))}break}case"do:kill":u&&(u(),u=null);break;case"do:screenshot":a(R.operator),Dn(m,R.requestId,e.screenShare);break}}),m.on("close",(E,R)=>{p&&clearTimeout(p),y(),e.screenShare?.off("change",ye),u&&(u(),u=null,l(!1)),o("disconnected"),E===1e3||E===4001?Z():Q(new Error(`WS closed: ${E} ${String(R)}`))}),m.on("error",E=>{p&&clearTimeout(p),y(),e.screenShare?.off("change",ye),o("disconnected"),Q(E)}),m.on("unexpected-response",(E,R)=>{f(new Error(`Unexpected HTTP response: ${R.statusCode}`))})})}async function Dn(e,t,r){let s=l=>{try{e.send(JSON.stringify(l))}catch{}},i=l=>s({type:"agent:screenshot_error",requestId:t,error:l}),o=r?.getState()??Ie;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:l,mimeType:a}=await r.capture();if(l.length>10485760){let c=(l.length/1048576).toFixed(1);i(`Screenshot is ${c} MB, which exceeds the ${10485760/(1024*1024)} MB limit.`);return}let d=l.toString("base64");for(let c=0;c<d.length;c+=524288)s({type:"agent:screenshot_chunk",requestId:t,chunk:d.slice(c,c+524288)});s({type:"agent:screenshot_done",requestId:t,mimeType:a,totalBytes:l.length})}catch(l){i(`Screenshot failed: ${l.message}`)}}import N from"node:fs/promises";import vt from"node:path";var ae="/var/run/aicommander-agent",ce=vt.join(ae,"state.json"),yt=448,Et=384;async function bt(e){let t=null;try{await N.mkdir(ae,{recursive:!0,mode:yt}),await N.chmod(ae,yt),t=vt.join(ae,`.state.${process.pid}.${Date.now()}.tmp`);let r=await N.open(t,"wx",Et);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,Et)}catch{}finally{t&&await N.rm(t,{force:!0}).catch(()=>{})}}async function wt(){try{await N.rm(ce,{force:!0})}catch{}}async function C(){try{let e=await N.readFile(ce,"utf8");return JSON.parse(e)}catch{return null}}import De from"node:fs";import te from"node:path";import On from"node:os";import{randomUUID as Nn,randomBytes as Mn}from"node:crypto";var Rt="/etc/aicommander-agent",At=te.join(On.homedir(),".config","aicommander-agent"),le="device.json";function Tt(){return{deviceId:Nn(),deviceSecret:Mn(32).toString("base64url")}}function Ln(e){return typeof e=="object"&&e!==null&&typeof e.deviceId=="string"&&e.deviceId.length>0&&typeof e.deviceSecret=="string"&&e.deviceSecret.length>0}function ke(e){try{let t=De.readFileSync(e,"utf8"),r=JSON.parse(t);if(Ln(r))return{deviceId:r.deviceId,deviceSecret:r.deviceSecret}}catch{}return null}function Ce(e,t){try{return De.mkdirSync(e,{recursive:!0,mode:448}),De.writeFileSync(te.join(e,le),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function K(e){if(e){let i=ke(te.join(e,le));if(i)return i;let o=Tt();return Ce(e,o),o}let t=ke(te.join(Rt,le));if(t)return t;let r=ke(te.join(At,le));if(r)return r;let s=Tt();return Ce(Rt,s)||Ce(At,s),s}import M from"node:fs";import J from"node:path";import $n from"node:os";var Pn="/etc/aicommander-agent",Un=J.join($n.homedir(),".config","aicommander-agent"),Oe="session.json",xt=".rotate";function Hn(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0&&typeof e.agentToken=="string"&&e.agentToken.length>0}function Fn(e){try{let t=M.readFileSync(e,"utf8"),r=JSON.parse(t);if(Hn(r))return{sessionCode:r.sessionCode,agentToken:r.agentToken}}catch{}return null}function Gn(e,t){try{return M.mkdirSync(e,{recursive:!0,mode:448}),M.writeFileSync(J.join(e,Oe),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function ne(e){return e?[e]:[Pn,Un]}function _t(e){for(let t of ne(e)){let r=Fn(J.join(t,Oe));if(r)return r}return null}function Ne(e,t){for(let r of ne(t))if(Gn(r,e))return}function It(e){for(let t of ne(e))try{M.rmSync(J.join(t,Oe),{force:!0})}catch{}}function kt(e){for(let t of ne(e))try{M.mkdirSync(t,{recursive:!0,mode:448}),M.writeFileSync(J.join(t,xt),"",{mode:384});return}catch{}}function Ct(e){let t=!1;for(let r of ne(e)){let s=J.join(r,xt);try{M.existsSync(s)&&(t=!0,M.rmSync(s,{force:!0}))}catch{}}return t}import{spawn as Xn}from"child_process";import de from"fs";import Dt from"path";import Vn from"os";function ue(e){if(e)return Dt.join(e,"heartbeat");if(process.platform==="linux")try{return de.mkdirSync("/run/aicommander-agent",{recursive:!0}),"/run/aicommander-agent/heartbeat"}catch{}return Dt.join(Vn.tmpdir(),"aicommander-agent-heartbeat")}function Ot(e){try{let t=`${e}.tmp`;de.writeFileSync(t,String(Date.now())),de.renameSync(t,e)}catch{}}var re=null;function Nt(e,t=5e3){Me(),Ot(e),re=setInterval(()=>Ot(e),t),re.unref?.()}function Me(){re&&(clearInterval(re),re=null)}function Mt(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??(f=>Xn(process.execPath,process.argv.slice(1),{env:f,stdio:"inherit"})),s=null,i=0,o=!1,l=0,a=[],d=null,c=null;await new Promise(f=>{let m=null,u=null,p=()=>{d&&(clearInterval(d),d=null),c&&(clearTimeout(c),c=null),m&&(process.removeListener("SIGINT",m),m=null),u&&(process.removeListener("SIGTERM",u),u=null)},S=()=>{p(),f()},A=()=>{i=Date.now(),l++;let y={...process.env,AIC_ROLE:"worker",AIC_HEARTBEAT:t};s=r(y),s.on("exit",(V,Z)=>{if(s=null,o){S();return}if(e.maxSpawns!=null&&l>=e.maxSpawns){S();return}T(`worker exited (code=${V??"null"} signal=${Z??"null"})`)}),s.on("error",()=>{})},T=y=>{let V=Date.now();a=a.filter(Q=>V-Q<3e5),a.push(V);let Z=a.length>5?6e4:0;c&&clearTimeout(c),c=setTimeout(()=>{c=null,o||A()},Z),c.unref?.()},g=()=>{if(s)try{s.kill("SIGKILL")}catch{}};d=setInterval(()=>{if(o||!s||Date.now()-i<3e4)return;let y=Mt(t);y!=null&&Date.now()-y>3e4&&g()},1e4),d.unref?.();let U=y=>{if(o=!0,d&&(clearInterval(d),d=null),c&&(clearTimeout(c),c=null),s)try{s.kill(y)}catch{}else S()};m=()=>U("SIGINT"),u=()=>U("SIGTERM"),process.on("SIGINT",m),process.on("SIGTERM",u),A()})}function Pt(e,t){return Qe(e,t)}async function Ut(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=mt(process.env.AICOMMANDER_SERVER),Yn=1440*60*1e3;async function Ht(){let e=await Ut(H);e?.version&&Pt(e.version,O)&&console.warn(`
14
- \u2B06 A newer AI Commander agent is available (v${e.version}; you have v${O}).
15
- Update: curl -fsSL ${H}/install | sudo bash
16
- `)}async function Ft(){process.env.AIC_ROLE==="worker"?await qn():await $t()}async function qn(){let e=process.env.AIC_ROLE==="worker";e&&Nt(process.env.AIC_HEARTBEAT||ue());let t=()=>{e&&Me(),wt(),process.exit(0)};process.on("SIGINT",t),process.on("SIGTERM",t),typeof process.getuid=="function"&&process.getuid()!==0&&console.warn(`
12
+ `),C):n.protocol!=="https:"&&!r?(console.warn(`
13
+ \u26A0 Refusing plaintext relay "${n.origin}" (https required); using ${C}.
14
+ `),C):t.replace(/\/+$/,"")}function We(e){let t;try{t=new URL(e)}catch{throw new Error("Refusing to connect: malformed relay WebSocket URL.")}if(t.protocol!=="wss:"&&!(t.protocol==="ws:"&&Lt(t.hostname)))throw new Error(`Refusing to open a plaintext relay socket (${t.protocol}//${t.host}); wss:// required.`)}var je={capable:!1,enabled:!1,expiresAt:null},Pt=360*60*1e3,sr=/^[a-f0-9]{64}$/;function ir(e,t){return new Promise((n,r)=>{if(t.aborted){r(new DOMException("Aborted","AbortError"));return}let s=setTimeout(n,e);t.addEventListener("abort",()=>{clearTimeout(s),r(new DOMException("Aborted","AbortError"))},{once:!0})})}async function Ut(e){let t=1e3,n=3e4,r=0,s=e.agentToken;for(;!e.signal?.aborted;){r++;try{let i=await cr(e,s);if(t=1e3,r=0,i&&e.reauth&&!e.signal?.aborted)try{s=await e.reauth()}catch{}}catch(i){if(i.name==="AbortError")break;let c=.8+Math.random()*.4,a=Math.min(t*c,n);e.silent||Ot(r,a);try{await ir(a,e.signal??new AbortController().signal)}catch(d){if(d.name==="AbortError")break}t=Math.min(t*2,n)}}}async function ar(e,t){let n;try{n=await fetch(`${e.serverUrl}/api/agent/ws-ticket`,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify({sessionCode:e.sessionCode}),...e.signal?{signal:e.signal}:{}})}catch(r){throw r.name==="AbortError"?r:new Error("Failed to request WebSocket connection ticket.")}if(!n.ok)throw new Error(`WebSocket connection ticket request failed (${n.status}).`);try{let r=await n.json();if(typeof r.ticket!="string"||!sr.test(r.ticket))throw new Error("invalid ticket");return r.ticket}catch{throw new Error("Relay returned an invalid WebSocket connection ticket.")}}async function cr(e,t){let{serverUrl:n}=e,r=`${n.replace(/^http/,"ws")}/ws/agent`;We(r);let s=await ar(e,t),i=`${r}?ticket=${s}`;We(i);let c=m=>{try{e.onStatus?.(m)}catch{}},a=m=>{try{e.onActivity?.(m)}catch{}},d=m=>{if(m)try{e.onRemoteActivity?.(m)}catch{}},l=()=>{try{e.onHeartbeat?.()}catch{}};return c("connecting"),new Promise((m,y)=>{let u=new rr(i,{headers:{Authorization:`Bearer ${t}`}}),f=null,p=null,E=null,G=!1,g=null,I=!1,D=()=>{E&&(clearTimeout(E),E=null)},v=()=>{g&&(clearInterval(g),g=null)},ne=()=>{if(!e.reauthIntervalMs||!e.reauth)return;let w=()=>{if(f){E=setTimeout(w,3e4);return}G=!0,u.close(1e3,"reauth")};E=setTimeout(w,e.reauthIntervalMs)},_e=()=>{I||(I=!0,D(),v(),m(G))},le=w=>{I||(I=!0,D(),v(),y(w))},mt=()=>{l(),p&&clearTimeout(p),p=setTimeout(()=>{u.terminate(),le(new Error("Ping timeout"))},9e4)};e.signal?.addEventListener("abort",()=>u.close(1e3,"disabled"),{once:!0});let Ce=()=>{if(I)return;let w={type:"agent:screen_state",screenShare:e.screenShare?.getState()??je};try{u.send(JSON.stringify(w))}catch{}};u.on("open",()=>{c("connected"),mt(),ne();let w={type:"agent:register",hostname:or.hostname(),platform:process.platform,arch:process.arch,agentVersion:L,screenShare:e.screenShare?.getState()??je};u.send(JSON.stringify(w)),e.screenShare?.on("change",Ce),e.silent||Nt()}),u.on("message",w=>{let O;try{O=JSON.parse(String(w))}catch{return}switch(O.type){case"do:ping":mt();let On={type:"agent:pong",ts:O.ts};u.send(JSON.stringify(On));break;case"do:exec":{let{commandId:M,command:X,cwd:Ie,env:De}=O,Oe=(x,_)=>{f=null,v(),a(!1),e.silent||Be("command",x,_);let b={type:"agent:done",commandId:M,exitCode:x,durationMs:_};u.send(JSON.stringify(b))},de=x=>{f=null,v(),a(!1);let _={type:"agent:error",commandId:M,error:x};u.send(JSON.stringify(_))};try{e.silent||Ge("command"),a(!0),d(O.operator);let x=Date.now(),_=!1;v(),g=setInterval(()=>{let A=Date.now()-x;if(A>=re&&!_){_=!0;let J={type:"agent:exec_idle",commandId:M,idleMs:A};u.send(JSON.stringify(J))}},re),g.unref?.(),f=At(X,Ie,De,{onOutput:(A,J)=>{x=Date.now(),_=!1;let $={type:"agent:output",commandId:M,chunk:A,stream:J};u.send(JSON.stringify($))},onDone:Oe,onError:de}).kill}catch(x){de(x instanceof Error?x.message:String(x))}break}case"do:secure_exec":{let{commandId:M,argv:X,allowedCommands:Ie,input:De,cwd:Oe,env:de}=O,x=(b,A)=>{f=null,v(),a(!1),e.silent||Be("secure-exec",b,A);let J={type:"agent:done",commandId:M,exitCode:b,durationMs:A};u.send(JSON.stringify(J))},_=b=>{f=null,v(),a(!1);let A={type:"agent:error",commandId:M,error:b};u.send(JSON.stringify(A))};try{if(!e.silent){let $=Array.isArray(X)&&typeof X[0]=="string"?X[0]:void 0;Ge("secure-exec",$)}a(!0);let b=Date.now(),A=!1;v(),g=setInterval(()=>{let $=Date.now()-b;if($>=re&&!A){A=!0;let Ne={type:"agent:exec_idle",commandId:M,idleMs:$};u.send(JSON.stringify(Ne))}},re),g.unref?.(),f=It({argv:X,allowedCommands:Ie,input:De,cwd:Oe,env:de},{onOutput:($,Ne)=>{b=Date.now(),A=!1;let Nn={type:"agent:output",commandId:M,chunk:$,stream:Ne};u.send(JSON.stringify(Nn))},onDone:x,onError:_}).kill}catch(b){_(b instanceof Error?b.message:String(b))}break}case"do:kill":f&&(f(),f=null);break;case"do:screenshot":d(O.operator),lr(u,O.requestId,e.screenShare);break}}),u.on("close",w=>{p&&clearTimeout(p),v(),e.screenShare?.off("change",Ce),f&&(f(),f=null,a(!1)),c("disconnected"),w===1e3||w===4001?_e():le(new Error(`WebSocket closed (${w}).`))}),u.on("error",()=>{p&&clearTimeout(p),v(),e.screenShare?.off("change",Ce),c("disconnected"),le(new Error("WebSocket connection error."))}),u.on("unexpected-response",(w,O)=>{le(new Error(`Unexpected WebSocket HTTP response (${O.statusCode}).`))})})}async function lr(e,t,n){let r=c=>{try{e.send(JSON.stringify(c))}catch{}},s=c=>r({type:"agent:screenshot_error",requestId:t,error:c}),i=n?.getState()??je;if(!n||!i.capable){s("This machine cannot share its screen (desktop macOS/Windows only).");return}if(!i.enabled){s("Screen sharing is turned off on this machine.");return}try{let{data:c,mimeType:a}=await n.capture();if(c.length>Pe){let l=(c.length/1048576).toFixed(1);s(`Screenshot is ${l} MB, which exceeds the ${Pe/(1024*1024)} MB limit.`);return}let d=c.toString("base64");for(let l=0;l<d.length;l+=Ue)r({type:"agent:screenshot_chunk",requestId:t,chunk:d.slice(l,l+Ue)});r({type:"agent:screenshot_done",requestId:t,mimeType:a,totalBytes:c.length})}catch(c){s(`Screenshot failed: ${c.message}`)}}import P from"node:fs/promises";import Wt from"node:path";import T from"node:fs";import Ft from"node:path";var Ke=448,me=384;function dr(e){T.mkdirSync(e,{recursive:!0,mode:Ke});try{T.chmodSync(e,Ke)}catch{}}function q(e,t){try{T.chmodSync(e,Ke)}catch{}try{T.chmodSync(t,me)}catch{}}function Ht(e,t,n){dr(e);let r=Ft.join(e,t),s=Ft.join(e,`.${t}.${process.pid}.${Date.now()}.tmp`);try{n(s),T.renameSync(s,r);try{T.chmodSync(r,me)}catch{}}catch(i){try{T.rmSync(s,{force:!0})}catch{}throw i}}function fe(e,t,n){Ht(e,t,r=>{let s=T.openSync(r,"wx",me);try{T.writeFileSync(s,n,{encoding:"utf8"})}finally{T.closeSync(s)}})}function Vt(e,t,n){Ht(e,t,r=>{let s=T.openSync(r,"wx",me);try{T.writeFileSync(s,n)}finally{T.closeSync(s)}})}var Xe=class extends Error{constructor(t){super(t),this.name="CredentialStorageError"}};function pe(){return!!(process.env.AICOMMANDER_SERVICE==="1"||process.env.NODE_ENV==="production"||process.env.INVOCATION_ID||process.env.JOURNAL_STREAM)}function ge(e,t){let n=t instanceof Error?t.message:String(t);throw new Xe(`Failed to persist ${e} with required filesystem permissions: ${n}`)}var oe="/var/run/aicommander-agent",se=Wt.join(oe,"state.json"),Gt=448,Bt=384;async function jt(e){let t=null;try{await P.mkdir(oe,{recursive:!0,mode:Gt}),await P.chmod(oe,Gt),t=Wt.join(oe,`.state.${process.pid}.${Date.now()}.tmp`);let n=await P.open(t,"wx",Bt);try{await n.writeFile(JSON.stringify(e,null,2),"utf8")}finally{await n.close()}await P.rename(t,se),t=null,await P.chmod(se,Bt)}catch(n){pe()&&ge("runtime state",n)}finally{t&&await P.rm(t,{force:!0}).catch(()=>{})}}async function Kt(){try{await P.rm(se,{force:!0})}catch{}}async function N(){try{q(oe,se);let e=await P.readFile(se,"utf8");return JSON.parse(e)}catch{return null}}import qe from"node:fs";import ie from"node:path";import ur from"node:os";import{randomUUID as mr,randomBytes as fr}from"node:crypto";var Xt="/etc/aicommander-agent",Jt=ie.join(ur.homedir(),".config","aicommander-agent"),Se="device.json";function Yt(){return{deviceId:mr(),deviceSecret:fr(32).toString("base64url")}}function pr(e){return typeof e=="object"&&e!==null&&typeof e.deviceId=="string"&&e.deviceId.length>0&&typeof e.deviceSecret=="string"&&e.deviceSecret.length>0}function Je(e){try{let t=qe.readFileSync(e,"utf8"),n=JSON.parse(t);if(pr(n))return{deviceId:n.deviceId,deviceSecret:n.deviceSecret}}catch{}return null}function Ye(e,t){try{return qe.mkdirSync(e,{recursive:!0,mode:448}),qe.writeFileSync(ie.join(e,Se),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function Z(e){if(e){let s=Je(ie.join(e,Se));if(s)return s;let i=Yt();return Ye(e,i),i}let t=Je(ie.join(Xt,Se));if(t)return t;let n=Je(ie.join(Jt,Se));if(n)return n;let r=Yt();return Ye(Xt,r)||Ye(Jt,r),r}import U from"node:fs";import z from"node:path";import gr from"node:os";var Sr="/etc/aicommander-agent",hr=z.join(gr.homedir(),".config","aicommander-agent"),he="session.json",ye="session.token",qt=".rotate";function ae(e){return typeof e=="string"?{configDir:e}:e??{}}function yr(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0&&typeof e.agentToken=="string"&&e.agentToken.length>0}function Er(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0}function vr(e,t){let n=z.join(e,he);try{if(!U.existsSync(n))return null;q(e,n);let r=U.readFileSync(n,"utf8"),s=JSON.parse(r);if(!Er(s))return null;if(s.tokenProtected){let i=z.join(e,ye);if(!U.existsSync(i)||(q(e,i),!t?.isAvailable()))return null;let c=U.readFileSync(i),a=t.decrypt(c);return a?{sessionCode:s.sessionCode,agentToken:a}:null}if(yr(s))return{sessionCode:s.sessionCode,agentToken:s.agentToken}}catch{}return null}function wr(e,t,n){if(n?.isAvailable()){let r=n.encrypt(t.agentToken);fe(e,he,JSON.stringify({sessionCode:t.sessionCode,tokenProtected:!0},null,2)),Vt(e,ye,r);return}fe(e,he,JSON.stringify({sessionCode:t.sessionCode,agentToken:t.agentToken},null,2));try{U.rmSync(z.join(e,ye),{force:!0})}catch{}}function Zt(e,t,n){let r=Ee(t),s;for(let i of r)try{n(i);return}catch(c){s=c}pe()&&ge(e,s??new Error("no writable session directory"))}function Ee(e){return e?[e]:[Sr,hr]}function zt(e){let{configDir:t,tokenVault:n}=ae(e);for(let r of Ee(t)){let s=vr(r,n);if(s)return s}return null}function Ze(e,t){let{configDir:n,tokenVault:r}=ae(t);Zt("session credentials",n,s=>{wr(s,e,r)})}function Qt(e){let{configDir:t}=ae(e);for(let n of Ee(t))for(let r of[he,ye])try{U.rmSync(z.join(n,r),{force:!0})}catch{}}function en(e){let{configDir:t}=ae(e);Zt("session rotate marker",t,n=>{fe(n,qt,"")})}function tn(e){let{configDir:t}=ae(e),n=!1;for(let r of Ee(t)){let s=z.join(r,qt);try{U.existsSync(s)&&(n=!0,q(r,s),U.rmSync(s,{force:!0}))}catch{}}return n}import{spawn as Rr}from"child_process";import ve from"fs";import nn from"path";import br from"os";function we(e){if(e)return nn.join(e,"heartbeat");if(process.platform==="linux")try{return ve.mkdirSync("/run/aicommander-agent",{recursive:!0}),"/run/aicommander-agent/heartbeat"}catch{}return nn.join(br.tmpdir(),"aicommander-agent-heartbeat")}function rn(e){try{let t=`${e}.tmp`;ve.writeFileSync(t,String(Date.now())),ve.renameSync(t,e)}catch{}}var ce=null;function on(e,t=gt){ze(),rn(e),ce=setInterval(()=>rn(e),t),ce.unref?.()}function ze(){ce&&(clearInterval(ce),ce=null)}function sn(e){try{let t=ve.readFileSync(e,"utf8").trim(),n=Number(t);return Number.isFinite(n)?n:null}catch{return null}}async function an(e={}){let t=e.heartbeatPath??we(e.configDir),n=e.spawnWorker??(m=>Rr(process.execPath,process.argv.slice(1),{env:m,stdio:"inherit"})),r=null,s=0,i=!1,c=0,a=[],d=null,l=null;await new Promise(m=>{let y=null,u=null,f=()=>{d&&(clearInterval(d),d=null),l&&(clearTimeout(l),l=null),y&&(process.removeListener("SIGINT",y),y=null),u&&(process.removeListener("SIGTERM",u),u=null)},p=()=>{f(),m()},E=()=>{s=Date.now(),c++;let D={...process.env,AIC_ROLE:"worker",AIC_HEARTBEAT:t};r=n(D),r.on("exit",(v,ne)=>{if(r=null,i){p();return}if(e.maxSpawns!=null&&c>=e.maxSpawns){p();return}G(`worker exited (code=${v??"null"} signal=${ne??"null"})`)}),r.on("error",()=>{})},G=D=>{let v=Date.now();a=a.filter(_e=>v-_e<yt),a.push(v);let ne=a.length>ht?Et:0;l&&clearTimeout(l),l=setTimeout(()=>{l=null,i||E()},ne),l.unref?.()},g=()=>{if(r)try{r.kill("SIGKILL")}catch{}};d=setInterval(()=>{if(i||!r||Date.now()-s<Fe)return;let D=sn(t);D!=null&&Date.now()-D>Fe&&g()},St),d.unref?.();let I=D=>{if(i=!0,d&&(clearInterval(d),d=null),l&&(clearTimeout(l),l=null),r)try{r.kill(D)}catch{}else p()};y=()=>I("SIGINT"),u=()=>I("SIGTERM"),process.on("SIGINT",y),process.on("SIGTERM",u),E()})}function cn(e,t){return bt(e,t)}async function ln(e,t=1e4){let n=new AbortController,r=setTimeout(()=>n.abort(),t);try{let s=await fetch(`${e.replace(/\/+$/,"")}/dist/latest`,{signal:n.signal,headers:{Accept:"application/json"}});if(!s.ok)return null;let i=await s.json();return{version:typeof i.version=="string"?i.version:null,mac:typeof i.mac=="string"?i.mac:null,macPkg:typeof i.macPkg=="string"?i.macPkg:null,win:typeof i.win=="string"?i.win:null,agentLinuxX64:typeof i.agentLinuxX64=="string"?i.agentLinuxX64:null,agentLinuxArm64:typeof i.agentLinuxArm64=="string"?i.agentLinuxArm64:null}}catch{return null}finally{clearTimeout(r)}}var B=$t(process.env.AICOMMANDER_SERVER),xr=1440*60*1e3;async function dn(){let e=await ln(B);e?.version&&cn(e.version,L)&&console.warn(`
15
+ \u2B06 A newer AI Commander agent is available (v${e.version}; you have v${L}).
16
+ Update safely (verify before sudo): ${B}/howto/#install-agent
17
+ `)}async function un(){process.env.AIC_ROLE==="worker"?await Ar():await an()}async function Ar(){let e=process.env.AIC_ROLE==="worker";e&&on(process.env.AIC_HEARTBEAT||we());let t=()=>{e&&ze(),Kt(),process.exit(0)};process.on("SIGINT",t),process.on("SIGTERM",t),typeof process.getuid=="function"&&process.getuid()!==0&&console.warn(`
17
18
  \u26A0 Warning: not running as root.
18
19
  Commands requiring sudo (apt, systemctl, brew cask\u2026) will fail.
19
20
  Recommended: sudo aicommander-agent
20
- `);let r=K(),s=Ct(),i=s?null:_t(),o,l;try{let c=await Re(H,r,{forceNew:s,...i?{currentCode:i.sessionCode}:{}});o=c.sessionCode,l=c.agentToken,Ne({sessionCode:o,agentToken:l})}catch(c){console.error(` Failed to register: ${String(c)}`),process.exit(1)}let a=!!(process.env.INVOCATION_ID||process.env.JOURNAL_STREAM);at(o,H,process.stdout.isTTY===!0||!a),bt({sessionCode:o,pid:process.pid,startedAt:new Date().toISOString(),serverUrl:H}),Ht(),setInterval(()=>{Ht()},Yn).unref?.(),await ht({serverUrl:H,sessionCode:o,agentToken:l,reauthIntervalMs:St,reauth:async()=>{let c=await Re(H,r,{currentCode:o});return Ne({sessionCode:c.sessionCode,agentToken:c.agentToken}),c.agentToken}})}import G from"node:fs";import{execFileSync as $e}from"node:child_process";import Wt from"chalk";import{execFileSync as Le}from"node:child_process";var L="aicommander-agent";function F(e){Le("systemctl",e,{stdio:"inherit"})}function X(){try{return Le("systemctl",["is-active",L],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return"inactive"}}function Gt(){try{return Le("systemctl",["is-enabled",L],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()==="enabled"}catch{return!1}}function me(){F(["start",L])}function Y(){F(["stop",L])}function pe(){F(["enable",L])}function fe(){F(["disable",L])}function q(){F(["restart",L])}function ge(){F(["daemon-reload"])}function Bt(){F(["kill","--signal=SIGKILL",L])}import $ from"chalk";import Zn from"node:readline";var n={header:e=>console.log($.bold.cyan(`
21
+ `);let n=Z(),r=tn(),s=r?null:zt(),i,c;try{let l=await Le(B,n,{forceNew:r,...s?{currentCode:s.sessionCode}:{}});i=l.sessionCode,c=l.agentToken}catch(l){console.error(` Failed to register: ${String(l)}`),process.exit(1)}try{Ze({sessionCode:i,agentToken:c})}catch(l){console.error(` Failed to persist session credentials: ${String(l)}`),process.exit(1)}let a=!!(process.env.INVOCATION_ID||process.env.JOURNAL_STREAM);Dt(i,B,process.stdout.isTTY===!0||!a);try{await jt({sessionCode:i,pid:process.pid,startedAt:new Date().toISOString(),serverUrl:B})}catch(l){console.error(` Failed to persist runtime state: ${String(l)}`),process.exit(1)}dn(),setInterval(()=>{dn()},xr).unref?.(),await Ut({serverUrl:B,sessionCode:i,agentToken:c,reauthIntervalMs:Pt,reauth:async()=>{let l=await Le(B,n,{currentCode:i});try{Ze({sessionCode:l.sessionCode,agentToken:l.agentToken})}catch(m){console.error(` Failed to persist rotated session credentials: ${String(m)}`),process.exit(1)}return l.agentToken}})}import j from"node:fs";import{execFileSync as et}from"node:child_process";import gn from"chalk";import{execFileSync as Qe}from"node:child_process";var F="aicommander-agent";function W(e){Qe("systemctl",e,{stdio:"inherit"})}function Q(){try{return Qe("systemctl",["is-active",F],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}catch{return"inactive"}}function mn(){try{return Qe("systemctl",["is-enabled",F],{encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()==="enabled"}catch{return!1}}function be(){W(["start",F])}function ee(){W(["stop",F])}function Re(){W(["enable",F])}function xe(){W(["disable",F])}function te(){W(["restart",F])}function Ae(){W(["daemon-reload"])}function fn(){W(["kill","--signal=SIGKILL",F])}import H from"chalk";import Tr from"node:readline";var o={header:e=>console.log(H.bold.cyan(`
21
22
  ${e}
22
- `)),ok:e=>console.log($.green(` \u2713 ${e}`)),warn:e=>console.log($.yellow(` \u26A0 ${e}`)),error:e=>console.error($.red(` \u2717 ${e}`)),info:(e,t)=>console.log(` ${$.gray(e.padEnd(14))} ${$.white(t)}`),step:e=>console.log($.gray(` ${e}`)),blank:()=>console.log()};function b(){process.getuid?.()!==0&&(n.error("This command must be run as root."),console.error($.gray(" Re-run with: sudo aicommander-agent <command>")),process.exit(1))}async function Vt(e){if(!process.stdin.isTTY)return!1;let t=Zn.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 jt="/etc/systemd/system/aicommander-agent.service",Pe="/etc/aicommander-agent";function Qn(e){/[\u0000-\u001f\u007f]/.test(e)&&(n.error(`Invalid --server URL: contains control characters: ${JSON.stringify(e)}`),process.exit(1));let t;try{t=new URL(e)}catch{n.error(`Invalid --server URL: not a valid URL: ${JSON.stringify(e)}`),process.exit(1)}return t.protocol!=="http:"&&t.protocol!=="https:"&&(n.error(`Invalid --server URL: protocol must be http: or https: (got ${t.protocol})`),process.exit(1)),e.replace(/%/g,"%%")}function Kt(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/%/g,"%%")}"`}function zn(){n.step(`Ensuring sandbox user "${v}"\u2026`);try{$e("getent",["passwd",v],{stdio:"ignore"}),n.ok(`Sandbox user "${v}" already exists.`);return}catch{}let e=["/usr/sbin/nologin","/sbin/nologin","/bin/false"].find(t=>G.existsSync(t))??"/bin/false";try{$e("useradd",["--system","--create-home","--shell",e,v],{stdio:"ignore"});try{let t=$e("getent",["passwd",v]).toString().trim().split(":")[5];t&&G.existsSync(t)&&G.chmodSync(t,448)}catch{}n.ok(`Sandbox user "${v}" created.`)}catch{n.warn(`Could not create sandbox user "${v}" \u2014 secure exec will be unavailable until it exists.`)}}async function Ue(e){b(),process.platform!=="linux"&&(n.error(`The systemd service installer supports Linux only. Detected: ${process.platform}`),n.blank(),n.step("For an ephemeral foreground run on this platform, use:"),n.step(" npx @aicommander/agent (or: aicommander-agent run)"),n.step("For a production root install on Linux, the canonical path is:"),n.step(" curl -fsSL https://aicommander.dev/install | sudo bash"),process.exit(1));let t=Qn(e.server??process.env.AICOMMANDER_SERVER??k),r=G.realpathSync(process.argv[1]),s=`${Kt(process.execPath)} ${Kt(r)} run`;n.header("AI Commander Agent \u2014 systemd install"),n.info("Server",t),n.info("ExecStart",s),n.blank(),n.step("Creating device identity dir\u2026"),G.mkdirSync(Pe,{recursive:!0}),G.chmodSync(Pe,448),n.ok(`Device identity dir ready (${Pe}).`),zn(),n.step(`Writing systemd service to ${jt}\u2026`);let i=`[Unit]
23
+ `)),ok:e=>console.log(H.green(` \u2713 ${e}`)),warn:e=>console.log(H.yellow(` \u26A0 ${e}`)),error:e=>console.error(H.red(` \u2717 ${e}`)),info:(e,t)=>console.log(` ${H.gray(e.padEnd(14))} ${H.white(t)}`),step:e=>console.log(H.gray(` ${e}`)),blank:()=>console.log()};function h(){process.getuid?.()!==0&&(o.error("This command must be run as root."),console.error(H.gray(" Re-run with: sudo aicommander-agent <command>")),process.exit(1))}async function pn(e){if(!process.stdin.isTTY)return!1;let t=Tr.createInterface({input:process.stdin,output:process.stdout});try{let n=await new Promise(r=>t.question(` ${e} [y/N] `,r));return/^y(es)?$/i.test(n.trim())}finally{t.close()}}var Sn="/etc/systemd/system/aicommander-agent.service",tt="/etc/aicommander-agent";function kr(e){/[\u0000-\u001f\u007f]/.test(e)&&(o.error(`Invalid --server URL: contains control characters: ${JSON.stringify(e)}`),process.exit(1));let t;try{t=new URL(e)}catch{o.error(`Invalid --server URL: not a valid URL: ${JSON.stringify(e)}`),process.exit(1)}return t.protocol!=="http:"&&t.protocol!=="https:"&&(o.error(`Invalid --server URL: protocol must be http: or https: (got ${t.protocol})`),process.exit(1)),e.replace(/%/g,"%%")}function hn(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/%/g,"%%")}"`}function _r(){o.step(`Ensuring sandbox user "${S}"\u2026`);try{et("getent",["passwd",S],{stdio:"ignore"}),o.ok(`Sandbox user "${S}" already exists.`),o.warn(`Existing "${S}" permissions and groups were preserved; secure exec will refuse to run if its runtime group check is unsafe or inconclusive.`);return}catch{}let e=["/usr/sbin/nologin","/sbin/nologin","/bin/false"].find(t=>j.existsSync(t))??"/bin/false";try{et("useradd",["--system","--create-home","--shell",e,S],{stdio:"ignore"});try{let t=et("getent",["passwd",S]).toString().trim().split(":")[5];t&&j.existsSync(t)&&j.chmodSync(t,448)}catch{}o.ok(`Sandbox user "${S}" created.`)}catch{o.warn(`Could not create sandbox user "${S}" \u2014 secure exec will be unavailable until it exists.`)}}async function nt(e){h(),process.platform!=="linux"&&(o.error(`The systemd service installer supports Linux only. Detected: ${process.platform}`),o.blank(),o.step("For an ephemeral foreground run on this platform, use:"),o.step(" npx @aicommander/agent (or: aicommander-agent run)"),o.step("For the signed Linux root-install flow (verify before sudo), see:"),o.step(" https://aicommander.dev/howto/#install-agent"),process.exit(1));let t=kr(e.server??process.env.AICOMMANDER_SERVER??C),n=j.realpathSync(process.argv[1]),r=`${hn(process.execPath)} ${hn(n)} run`;o.header("AI Commander Agent \u2014 systemd install"),o.info("Server",t),o.info("ExecStart",r),o.blank(),o.step("Creating device identity dir\u2026"),j.mkdirSync(tt,{recursive:!0}),j.chmodSync(tt,448),o.ok(`Device identity dir ready (${tt}).`),_r(),o.step(`Writing systemd service to ${Sn}\u2026`);let s=`[Unit]
23
24
  Description=AI Commander Remote Agent
24
25
  After=network-online.target
25
26
  Wants=network-online.target
@@ -32,7 +33,9 @@ RuntimeDirectory=aicommander-agent
32
33
  # credential); owner-only blocks local unprivileged users from reading it.
33
34
  RuntimeDirectoryMode=0700
34
35
  Environment=AICOMMANDER_SERVER=${t}
35
- ExecStart=${s}
36
+ Environment=AICOMMANDER_SERVICE=1
37
+ Environment=NODE_ENV=production
38
+ ExecStart=${r}
36
39
  Restart=always
37
40
  RestartSec=10
38
41
  StandardOutput=journal
@@ -43,4 +46,4 @@ KillSignal=SIGINT
43
46
 
44
47
  [Install]
45
48
  WantedBy=multi-user.target
46
- `;G.writeFileSync(jt,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{pe()}catch{n.warn("Could not enable the service on boot (non-critical).")}n.step("Restarting service\u2026");try{q()}catch{n.warn("Graceful restart failed \u2014 forcing stop then start\u2026");try{Y()}catch{}try{Bt()}catch{}try{me()}catch{n.error("Could not start aicommander-agent. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)}}X()!=="active"&&(n.error("Service is not active after restart. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)),n.ok("Service started."),n.blank(),n.step("Waiting for session code\u2026");let o=null;for(let l=0;l<30;l++){let a=await C();if(a?.sessionCode){o=a.sessionCode;break}await new Promise(d=>setTimeout(d,500))}n.blank(),o?(n.ok(Wt.bold("AI Commander ready!")),n.blank(),n.info("Session code",Wt.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 er(e){if(e<60)return`${e}s`;if(e<3600)return`${Math.floor(e/60)}m`;let t=Math.floor(e/3600),r=Math.floor(e%3600/60);return r>0?`${t}h ${r}m`:`${t}h`}async function He(e={}){n.header("AI Commander \u2014 Agent Status");let t;try{t=X()}catch{n.error("systemd is not available on this system."),process.exit(1)}let r=Gt(),s=await C();if(n.info("State",t==="active"?P.green("running"):P.red(t)),s){e.reveal?(b(),n.info("Session code",P.bold.cyan(s.sessionCode)),n.step(P.yellow(" \u26A0 Keep this secret \u2014 anyone with it can run commands as root here."))):(n.info("Session code",P.bold.cyan(j(s.sessionCode))),n.step(P.gray(" (run with --reveal to show the full code)"))),n.info("PID",String(s.pid));let i=new Date(s.startedAt),o=Math.floor((Date.now()-i.getTime())/1e3);n.info("Started",`${s.startedAt} (${er(o)} ago)`),n.info("Server",s.serverUrl)}else t==="active"&&n.warn("Session code not yet available (agent still starting)");n.info("Enabled",r?P.green("yes"):P.yellow("no")),n.blank()}function Fe(){b(),X()==="active"?(n.warn("Service is already running \u2014 restarting to load the current binary."),q()):me(),pe(),n.ok("Service started and enabled to run on boot."),n.blank()}function Ge(){b(),Y(),fe(),n.ok("Service stopped and disabled."),n.blank()}import tr from"chalk";async function Be(e={}){if(b(),!e.yes){if(!process.stdin.isTTY){n.error("Refusing to reset the code non-interactively."),n.info("To proceed","re-run with --yes (this unlinks ALL linked accounts)"),process.exitCode=1;return}if(!await Vt("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"),kt(),It(),q();let t=Date.now()+1e4,r=null;for(;Date.now()<t&&(await new Promise(s=>setTimeout(s,500)),r=await C(),!r););r?n.ok(`New session code: ${tr.bold.greenBright(r.sessionCode)}`):(n.warn("Service restarted. Session code not yet available."),n.info("Check","aicommander-agent status")),n.blank()}import Ve from"node:fs";import nr from"chalk";var Jt="/etc/systemd/system/aicommander-agent.service",Xt="/usr/local/bin/aicommander-agent",Yt="/usr/local/bin/aicommander-ctl",qt="/var/run/aicommander-agent",Zt="/etc/aicommander-agent",Qt="/etc/aicommander-agent/session.json",zt="/etc/aicommander-agent/.rotate";function B(e,t){try{if(Ve.existsSync(e)||Ve.lstatSync(e)){Ve.rmSync(e,{recursive:!0,force:!0}),n.ok(`${t} removed.`);return}}catch{}n.warn(`${t} not found (already removed?).`)}function We(e){b(),e.force||(n.warn("This will fully remove the AI Commander agent."),n.error("Add --force to confirm: sudo aicommander-agent uninstall --force"),process.exit(1)),n.step("Stopping and disabling service\u2026");try{Y()}catch{}try{fe()}catch{}n.ok("Service stopped."),n.step("Removing service file\u2026"),B(Jt,Jt),n.step("Reloading systemd\u2026");try{ge(),n.ok("daemon-reload complete.")}catch{n.warn("daemon-reload failed (non-critical).")}n.step("Removing agent binary\u2026"),B(Yt,Yt),B(Xt,Xt),B(qt,qt),B(Qt,Qt),B(zt,zt),B(Zt,Zt),n.blank(),n.ok(nr.bold("AI Commander agent fully removed.")),n.blank()}function en(e){return{"X-Device-Id":e.deviceId,"X-Device-Secret":e.deviceSecret}}var tn=1e4;async function Se(e,t){let r;try{r=await fetch(`${e}/api/device/admins`,{headers:en(t),signal:AbortSignal.timeout(tn)})}catch{throw new Error("Failed to list linked accounts (relay unreachable or timed out).")}if(!r.ok)throw new Error(`Failed to list linked accounts (HTTP ${r.status}).`);return await r.json()}async function nn(e,t,r,s){let i;try{i=await fetch(`${e}/api/device/${r}`,{method:"POST",headers:{...en(t),"Content-Type":"application/json"},body:JSON.stringify({userId:s}),signal:AbortSignal.timeout(tn)})}catch{return{ok:!1,error:"unreachable"}}let o;try{o=await i.json()}catch{o={ok:!1,error:`http_${i.status}`}}return{...o,ok:i.ok}}function rn(e,t,r){return nn(e,t,"block",r)}function on(e,t,r){return nn(e,t,"unblock",r)}function he(e){return[...e.filter(t=>!t.blocked),...e.filter(t=>t.blocked)]}function sn(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 rr(){return(await C())?.serverUrl??process.env.AICOMMANDER_SERVER??"https://aicommander.dev"}async function je(){b();let e=await rr(),t=K(),r;try{r=await Se(e,t)}catch(a){n.error(a.message),process.exitCode=1;return}if(r.admins.length===0){n.header("Linked accounts"),n.info("None","no accounts are linked to this device"),n.blank();return}let s=he(r.admins),i=(a,d)=>{let c=String(d+1).padStart(2," "),f=a.linkedAt.slice(0,10);n.info(`${c}. ${a.maskedEmail}`,`${a.alias} \xB7 linked ${f} \xB7 id ${a.userId.slice(0,8)}\u2026`)},o=s.filter(a=>!a.blocked),l=s.filter(a=>a.blocked);n.header("Linked accounts"),o.forEach(a=>i(a,s.indexOf(a))),o.length===0&&n.info("None active","every linked account is blocked"),n.blank(),l.length>0&&(n.header("Blocked"),l.forEach(a=>i(a,s.indexOf(a))),n.blank()),n.step("Block one with: aicommander-agent block-admin <number|id>"),l.length>0&&n.step("Unblock one with: aicommander-agent unblock-admin <number|id>"),n.blank()}async function or(){return(await C())?.serverUrl??process.env.AICOMMANDER_SERVER??"https://aicommander.dev"}async function an(e,t){b();let r=await or(),s=K(),i;try{i=await Se(r,s)}catch(c){n.error(c.message),process.exitCode=1;return}let o=he(i.admins),l=sn(o,e);if(l.kind==="not_found"){n.error(`No linked account matches "${e}".`),n.info("See options","aicommander-agent list-admins"),process.exitCode=1;return}if(l.kind==="ambiguous"){n.error(`"${e}" is ambiguous \u2014 it matches ${l.matches.length} accounts.`),n.info("Disambiguate","use the list number or a longer id"),process.exitCode=1;return}let a=l.admin,d=t==="block"?await rn(r,s,a.userId):await on(r,s,a.userId);d.ok?t==="block"?(n.ok(`Blocked ${a.maskedEmail} (${a.alias}).`),n.info("Note","their access is cut off; run unblock-admin anytime to restore it")):n.ok(`Unblocked ${a.maskedEmail} (${a.alias}) \u2014 access restored.`):d.error==="not_found"?n.warn(t==="block"?"That account isn't linked (or is already blocked).":"That account isn't blocked."):(n.error(d.message??`Failed to ${t} the account.`),process.exitCode=1),n.blank()}function Ke(e){return an(e,"block")}function Je(e){return an(e,"unblock")}ot(process.argv)||(_.name("aicommander-agent").description("AI Commander remote agent + service controller").version(O),_.command("run",{isDefault:!0}).description("Run the agent: register with the relay and listen for commands").action(async()=>{await Ft()}),_.command("install").description("Install and start the agent as a systemd service (Linux, root)").option("--server <url>","Relay server URL baked into the service unit").action(async e=>{await Ue(e)}),_.command("status").description("Show agent status, session code, and uptime").option("--reveal","Show the full session code (otherwise masked)").action(async e=>{await He(e)}),_.command("enable").description("Start and enable the agent service").action(()=>{Fe()}),_.command("disable").description("Stop and disable the agent service").action(()=>{Ge()}),_.command("change-code").aliases(["reset-code"]).description("Reset the access code \u2014 mint a new code and remove ALL access (unlinks every linked account)").option("-y, --yes","Skip the confirmation prompt").action(async e=>{await Be(e)}),_.command("list-admins").description("List the accounts (admins) linked to this device").action(async()=>{await je()}),_.command("block-admin <account>").aliases(["revoke-admin"]).description("Block one account on this device (by list number or id) \u2014 refuses access; unblock anytime").action(async e=>{await Ke(e)}),_.command("unblock-admin <account>").description("Restore a blocked account's access (by list number or id)").action(async e=>{await Je(e)}),_.command("uninstall").description("Fully remove the agent (stop, disable, delete files)").option("-f, --force","Skip confirmation requirement").action(e=>{We(e)}),_.parseAsync().catch(e=>{console.error("Fatal error:",e),process.exit(1)}));
49
+ `;j.writeFileSync(Sn,s,{mode:420}),o.ok("Service file written."),o.step("Reloading systemd\u2026");try{Ae()}catch{o.error("daemon-reload failed. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)}o.step("Enabling service on boot\u2026");try{Re()}catch{o.warn("Could not enable the service on boot (non-critical).")}o.step("Restarting service\u2026");try{te()}catch{o.warn("Graceful restart failed \u2014 forcing stop then start\u2026");try{ee()}catch{}try{fn()}catch{}try{be()}catch{o.error("Could not start aicommander-agent. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)}}Q()!=="active"&&(o.error("Service is not active after restart. Inspect: journalctl -u aicommander-agent -n 50"),process.exit(1)),o.ok("Service started."),o.blank(),o.step("Waiting for session code\u2026");let i=null;for(let c=0;c<30;c++){let a=await N();if(a?.sessionCode){i=a.sessionCode;break}await new Promise(d=>setTimeout(d,500))}o.blank(),i?(o.ok(gn.bold("AI Commander ready!")),o.blank(),o.info("Session code",gn.bold.greenBright(Y(i))),o.blank(),o.step("Reveal full code: sudo aicommander-agent status --reveal"),o.step("Then use in Claude: 'execute df -h on <code>'"),o.step("Service logs: journalctl -u aicommander-agent -f"),o.step("Manage: sudo aicommander-agent status"),o.step("Uninstall: sudo aicommander-agent uninstall --force"),o.blank(),o.warn("Keep this code secret. Anyone who has it can run commands as root on this machine.")):(o.warn("The session code did not appear within 15 seconds."),o.step("The service is running \u2014 the code should appear shortly."),o.step("Status: sudo aicommander-agent status"),o.step("Logs: journalctl -u aicommander-agent -f")),o.blank()}import V from"chalk";function Cr(e){if(e<60)return`${e}s`;if(e<3600)return`${Math.floor(e/60)}m`;let t=Math.floor(e/3600),n=Math.floor(e%3600/60);return n>0?`${t}h ${n}m`:`${t}h`}async function rt(e={}){o.header("AI Commander \u2014 Agent Status");let t;try{t=Q()}catch{o.error("systemd is not available on this system."),process.exit(1)}let n=mn(),r=await N();if(o.info("State",t==="active"?V.green("running"):V.red(t)),r){e.reveal?(h(),o.info("Session code",V.bold.cyan(r.sessionCode)),o.step(V.yellow(" \u26A0 Keep this secret \u2014 anyone with it can run commands as root here."))):(o.info("Session code",V.bold.cyan(Y(r.sessionCode))),o.step(V.gray(" (run with --reveal to show the full code)"))),o.info("PID",String(r.pid));let s=new Date(r.startedAt),i=Math.floor((Date.now()-s.getTime())/1e3);o.info("Started",`${r.startedAt} (${Cr(i)} ago)`),o.info("Server",r.serverUrl)}else t==="active"&&o.warn("Session code not yet available (agent still starting)");o.info("Enabled",n?V.green("yes"):V.yellow("no")),o.blank()}function ot(){h(),Q()==="active"?(o.warn("Service is already running \u2014 restarting to load the current binary."),te()):be(),Re(),o.ok("Service started and enabled to run on boot."),o.blank()}function st(){h(),ee(),xe(),o.ok("Service stopped and disabled."),o.blank()}import Ir from"chalk";async function it(e={}){if(h(),!e.yes){if(!process.stdin.isTTY){o.error("Refusing to reset the code non-interactively."),o.info("To proceed","re-run with --yes (this unlinks ALL linked accounts)"),process.exitCode=1;return}if(!await pn("Resetting the access code will UNLINK ALL linked accounts. Continue?")){o.info("Cancelled","no changes made");return}}o.step("Restarting agent to get a new session code\u2026"),en(),Qt(),te();let t=Date.now()+1e4,n=null;for(;Date.now()<t&&(await new Promise(r=>setTimeout(r,500)),n=await N(),!n););n?o.ok(`New session code: ${Ir.bold.greenBright(n.sessionCode)}`):(o.warn("Service restarted. Session code not yet available."),o.info("Check","aicommander-agent status")),o.blank()}import at from"node:fs";import Dr from"chalk";var yn="/etc/systemd/system/aicommander-agent.service",En="/usr/local/bin/aicommander-agent",vn="/usr/local/bin/aicommander-ctl",wn="/var/run/aicommander-agent",bn="/etc/aicommander-agent",Rn="/etc/aicommander-agent/session.json",xn="/etc/aicommander-agent/.rotate";function K(e,t){try{if(at.existsSync(e)||at.lstatSync(e)){at.rmSync(e,{recursive:!0,force:!0}),o.ok(`${t} removed.`);return}}catch{}o.warn(`${t} not found (already removed?).`)}function ct(e){h(),e.force||(o.warn("This will fully remove the AI Commander agent."),o.error("Add --force to confirm: sudo aicommander-agent uninstall --force"),process.exit(1)),o.step("Stopping and disabling service\u2026");try{ee()}catch{}try{xe()}catch{}o.ok("Service stopped."),o.step("Removing service file\u2026"),K(yn,yn),o.step("Reloading systemd\u2026");try{Ae(),o.ok("daemon-reload complete.")}catch{o.warn("daemon-reload failed (non-critical).")}o.step("Removing agent binary\u2026"),K(vn,vn),K(En,En),K(wn,wn),K(Rn,Rn),K(xn,xn),K(bn,bn),o.blank(),o.ok(Dr.bold("AI Commander agent fully removed.")),o.blank()}function An(e){return{"X-Device-Id":e.deviceId,"X-Device-Secret":e.deviceSecret}}var Tn=1e4;async function Te(e,t){let n;try{n=await fetch(`${e}/api/device/admins`,{headers:An(t),signal:AbortSignal.timeout(Tn)})}catch{throw new Error("Failed to list linked accounts (relay unreachable or timed out).")}if(!n.ok)throw new Error(`Failed to list linked accounts (HTTP ${n.status}).`);return await n.json()}async function kn(e,t,n,r){let s;try{s=await fetch(`${e}/api/device/${n}`,{method:"POST",headers:{...An(t),"Content-Type":"application/json"},body:JSON.stringify({userId:r}),signal:AbortSignal.timeout(Tn)})}catch{return{ok:!1,error:"unreachable"}}let i;try{i=await s.json()}catch{i={ok:!1,error:`http_${s.status}`}}return{...i,ok:s.ok}}function _n(e,t,n){return kn(e,t,"block",n)}function Cn(e,t,n){return kn(e,t,"unblock",n)}function ke(e){return[...e.filter(t=>!t.blocked),...e.filter(t=>t.blocked)]}function In(e,t){let n=t.trim();if(!n)return{kind:"not_found"};if(/^\d+$/.test(n)){let i=Number(n)-1;return i>=0&&i<e.length?{kind:"ok",admin:e[i]}:{kind:"not_found"}}let r=e.find(i=>i.userId===n);if(r)return{kind:"ok",admin:r};let s=e.filter(i=>i.userId.startsWith(n));return s.length===1?{kind:"ok",admin:s[0]}:s.length>1?{kind:"ambiguous",matches:s}:{kind:"not_found"}}async function Or(){return(await N())?.serverUrl??process.env.AICOMMANDER_SERVER??"https://aicommander.dev"}async function lt(){h();let e=await Or(),t=Z(),n;try{n=await Te(e,t)}catch(a){o.error(a.message),process.exitCode=1;return}if(n.admins.length===0){o.header("Linked accounts"),o.info("None","no accounts are linked to this device"),o.blank();return}let r=ke(n.admins),s=(a,d)=>{let l=String(d+1).padStart(2," "),m=a.linkedAt.slice(0,10);o.info(`${l}. ${a.maskedEmail}`,`${a.alias} \xB7 linked ${m} \xB7 id ${a.userId.slice(0,8)}\u2026`)},i=r.filter(a=>!a.blocked),c=r.filter(a=>a.blocked);o.header("Linked accounts"),i.forEach(a=>s(a,r.indexOf(a))),i.length===0&&o.info("None active","every linked account is blocked"),o.blank(),c.length>0&&(o.header("Blocked"),c.forEach(a=>s(a,r.indexOf(a))),o.blank()),o.step("Block one with: aicommander-agent block-admin <number|id>"),c.length>0&&o.step("Unblock one with: aicommander-agent unblock-admin <number|id>"),o.blank()}async function Nr(){return(await N())?.serverUrl??process.env.AICOMMANDER_SERVER??"https://aicommander.dev"}async function Dn(e,t){h();let n=await Nr(),r=Z(),s;try{s=await Te(n,r)}catch(l){o.error(l.message),process.exitCode=1;return}let i=ke(s.admins),c=In(i,e);if(c.kind==="not_found"){o.error(`No linked account matches "${e}".`),o.info("See options","aicommander-agent list-admins"),process.exitCode=1;return}if(c.kind==="ambiguous"){o.error(`"${e}" is ambiguous \u2014 it matches ${c.matches.length} accounts.`),o.info("Disambiguate","use the list number or a longer id"),process.exitCode=1;return}let a=c.admin,d=t==="block"?await _n(n,r,a.userId):await Cn(n,r,a.userId);d.ok?t==="block"?(o.ok(`Blocked ${a.maskedEmail} (${a.alias}).`),o.info("Note","their access is cut off; run unblock-admin anytime to restore it")):o.ok(`Unblocked ${a.maskedEmail} (${a.alias}) \u2014 access restored.`):d.error==="not_found"?o.warn(t==="block"?"That account isn't linked (or is already blocked).":"That account isn't blocked."):(o.error(d.message??`Failed to ${t} the account.`),process.exitCode=1),o.blank()}function dt(e){return Dn(e,"block")}function ut(e){return Dn(e,"unblock")}_t(process.argv)||(k.name("aicommander-agent").description("AI Commander remote agent + service controller").version(L),k.command("run",{isDefault:!0}).description("Run the agent: register with the relay and listen for commands").action(async()=>{await un()}),k.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 nt(e)}),k.command("status").description("Show agent status, session code, and uptime").option("--reveal","Show the full session code (otherwise masked)").action(async e=>{await rt(e)}),k.command("enable").description("Start and enable the agent service").action(()=>{ot()}),k.command("disable").description("Stop and disable the agent service").action(()=>{st()}),k.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 it(e)}),k.command("list-admins").description("List the accounts (admins) linked to this device").action(async()=>{await lt()}),k.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 dt(e)}),k.command("unblock-admin <account>").description("Restore a blocked account's access (by list number or id)").action(async e=>{await ut(e)}),k.command("uninstall").description("Fully remove the agent (stop, disable, delete files)").option("-f, --force","Skip confirmation requirement").action(e=>{ct(e)}),k.parseAsync().catch(e=>{console.error("Fatal error:",e),process.exit(1)}));
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
- import lt from"os";var U="1.0.26";var Re="device_secret_mismatch";function ut(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=ut(o)}status;body;code};async function J(e,t,r={}){let o={hostname:lt.hostname(),platform:process.platform,arch:process.arch,agentVersion:U,...t?{deviceId:t.deviceId,deviceSecret:t.deviceSecret}:{},...r.forceNew?{forceNew:!0}:r.currentCode?{currentCode:r.currentCode}:{}},s=await fetch(`${e}/api/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});if(!s.ok){let n=await s.text();throw new O(s.status,n)}return s.json()}import At from"ws";import bt from"os";import{spawn as Ie}from"child_process";var T="https://aicommander.dev";var R="aicommander-exec";var xe="ABCDEFGHJKMNPQRSTVWXYZ0123456789";var fr=Math.floor(256/xe.length)*xe.length;function se(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 Ce(e,t){return se(e,t)>0}var De=process.platform==="win32";function Oe(e,t,r,o){let s=Date.now(),n=Ie(e,[],{shell:!0,cwd:t??process.env.HOME,env:{...process.env,...r??{}},stdio:["ignore","pipe","pipe"],detached:!De}),i=!1,c=null,u=()=>{c&&(clearTimeout(c),c=null)};n.stdout?.on("data",d=>{o.onOutput(d.toString("base64"),"stdout")}),n.stderr?.on("data",d=>{o.onOutput(d.toString("base64"),"stderr")}),n.on("close",d=>{i=!0,u(),o.onDone(d??-1,Date.now()-s)}),n.on("error",d=>{i=!0,u(),o.onError(d.message)});let a=d=>{if(!(i||n.pid==null))try{De?Ie("taskkill",["/pid",String(n.pid),"/T","/F"],{stdio:"ignore"}):process.kill(-n.pid,d)}catch{}};return{kill:()=>{i||(a("SIGTERM"),u(),c=setTimeout(()=>{c=null,a("SIGKILL")},5e3),c.unref?.())}}}import{spawn as gt}from"node:child_process";import{readFileSync as ft}from"node:fs";import{isAbsolute as ht}from"node:path";import{createRequire as dt}from"node:module";var mt=dt(import.meta.url),Ne="__secure-exec-drop";function pt(){let e=!1;try{e=mt("node:sea").isSea()}catch{}return e}function Me(e){let t=process.argv[1],r=!pt()&&t?[t,Ne,...e]:[Ne,...e];return{cmd:process.execPath,args:r}}function St(){let e;try{e=ft("/etc/passwd","utf8")}catch{throw new Error("cannot read /etc/passwd to resolve the secure-exec user")}let t=e.split(`
2
- `).filter(u=>u.startsWith(`${R}:`));if(t.length===0)throw new Error(`secure exec user "${R}" not found in /etc/passwd \u2014 reinstall the agent to create it`);if(t.length>1)throw new Error(`multiple /etc/passwd entries for "${R}" \u2014 refusing to guess which is the sandbox user`);let r=t[0].split(":");if(r.length!==7)throw new Error(`malformed passwd entry for ${R}`);let o=r[2],s=r[3];if(!/^\d+$/.test(o)||!/^\d+$/.test(s))throw new Error(`could not resolve uid/gid for ${R}`);let n=Number(o),i=Number(s),c=r[5]||"/tmp";if(!Number.isInteger(n)||!Number.isInteger(i))throw new Error(`could not resolve uid/gid for ${R}`);if(n===0)throw new Error("refusing to run secure exec as uid 0");if(i===0)throw new Error("refusing to run secure exec as gid 0");return{uid:n,gid:i,home:c,name:R}}var Et="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",yt=new Set(["NODE_OPTIONS","BASH_ENV","ENV","IFS","PYTHONSTARTUP","PYTHONPATH","PERL5OPT","RUBYOPT","GIT_EXTERNAL_DIFF","PATH","HOME","USER","LOGNAME"]),_t=e=>/^LD_/.test(e)||yt.has(e);function Pe(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&&!ht(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=St(),i={HOME:n.home,USER:n.name,LOGNAME:n.name,PATH:Et},c={};for(let[p,C]of Object.entries(e.env??{}))_t(p)||(c[p]=C);let u={...c,...i},a=Date.now(),{cmd:d,args:m}=Me([String(n.uid),String(n.gid),n.name,o,...r.slice(1)]),l=gt(d,m,{cwd:e.cwd??n.home,env:u,stdio:["pipe","pipe","pipe"],detached:!0,shell:!1});s&&l.stdin?(l.stdin.on("error",()=>{}),l.stdin.end(s)):l.stdin?.end();let E=!1,S=null,b=()=>{S&&(clearTimeout(S),S=null)};l.stdout?.on("data",p=>{t.onOutput(p.toString("base64"),"stdout")}),l.stderr?.on("data",p=>{t.onOutput(p.toString("base64"),"stderr")}),l.on("close",p=>{E=!0,b(),t.onDone(p??-1,Date.now()-a)}),l.on("error",p=>{E=!0,b(),t.onError(p.message)});let _=p=>{if(!(E||l.pid==null))try{process.kill(-l.pid,p)}catch{}};return{kill:()=>{E||(_("SIGTERM"),b(),S=setTimeout(()=>{S=null,_("SIGKILL")},5e3),S.unref?.())}}}import B from"chalk";function Le(e,t){console.log(B.yellow(` Reconnecting in ${Math.round(t/1e3)}s (attempt ${e})...`))}function He(){console.log(B.green(" Connected to relay \u2014 ready for commands."))}function ae(e){console.log(B.cyan(` > ${e}`))}function ce(e,t){let r=e===0?B.green:B.red;console.log(r(` Done. Exit ${e} in ${t}ms`))}function vt(){let e=process.env.AICOMMANDER_DEV;return e==="1"||e==="true"}function Ue(e){let t=e.toLowerCase();return t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="[::1]"}function $e(){return new URL(T).origin}function Be(e){let t=(e??"").trim();if(!t)return T;let r;try{r=new URL(t)}catch{return console.warn(`
3
- \u26A0 Ignoring malformed AICOMMANDER_SERVER (${t}); using ${T}.
4
- `),T}if(r.origin===$e())return T;let o=Ue(r.hostname);return!vt()&&!o?(console.warn(`
1
+ import Lt from"os";var F="1.0.30";var Fe="device_secret_mismatch";function $t(e){try{let t=JSON.parse(e);return typeof t.code=="string"?t.code:null}catch{return null}}var P=class extends Error{constructor(r,n){super(`Registration failed (${r}): ${n}`);this.status=r;this.body=n;this.name="RegistrationError",this.code=$t(n)}status;body;code};async function Z(e,t,r={}){let n={hostname:Lt.hostname(),platform:process.platform,arch:process.arch,agentVersion:F,...t?{deviceId:t.deviceId,deviceSecret:t.deviceSecret}:{},...r.forceNew?{forceNew:!0}:r.currentCode?{currentCode:r.currentCode}:{}},o=await fetch(`${e}/api/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!o.ok){let s=await o.text();throw new P(o.status,s)}return o.json()}import tr from"ws";import rr from"os";import{spawn as et}from"child_process";var I="https://aicommander.dev";var C="aicommander-exec",Ht=["sh","bash","dash","zsh","fish","ash","ksh","mksh","csh","tcsh","python","python2","python3","pypy","pypy3","node","nodejs","perl","ruby","php","lua","env","busybox","find","xargs","awk","gawk","mawk","nawk","sed","sudo","su","doas","pkexec","docker","podman","nerdctl","kubectl"],Ut=new Set(Ht),Vt=/^(?:python[23]\.\d+(?:\.\d+)*|pypy[23]\.\d+(?:\.\d+)*|perl\d+(?:\.\d+)*|ruby\d+(?:\.\d+)*|php\d+(?:\.\d+)*|lua\d+(?:\.\d+)*)$/;function Ge(e){return Ut.has(e)||Vt.test(e)}var Be=["root","wheel","sudo","docker","podman","lxd","incus","disk","libvirt"],Se=262144;var L=10*1024*1024,he=512*1024,Gr=1440*60*1e3,We=5e3,je=1e4,Ee=3e4,Xe=5,Ke=3e5,Je=6e4,z=5e3,G=3e5;var Ye=3600*1e3,Ze=600*1e3,ze=480*60*1e3;var qe="ABCDEFGHJKMNPQRSTVWXYZ0123456789";var Wr=Math.floor(256/qe.length)*qe.length;function ye(e,t){let r=e.split(".").map(s=>parseInt(s,10)||0),n=t.split(".").map(s=>parseInt(s,10)||0),o=Math.max(r.length,n.length);for(let s=0;s<o;s++){let i=(r[s]??0)-(n[s]??0);if(i!==0)return i>0?1:-1}return 0}function Qe(e,t){return ye(e,t)>0}var tt=process.platform==="win32";function rt(e,t,r,n){let o=Date.now(),s=et(e,[],{shell:!0,cwd:t??process.env.HOME,env:{...process.env,...r??{}},stdio:["ignore","pipe","pipe"],detached:!tt}),i=!1,a=null,u=()=>{a&&(clearTimeout(a),a=null)};s.stdout?.on("data",c=>{n.onOutput(c.toString("base64"),"stdout")}),s.stderr?.on("data",c=>{n.onOutput(c.toString("base64"),"stderr")}),s.on("close",c=>{i=!0,u(),n.onDone(c??-1,Date.now()-o)}),s.on("error",c=>{i=!0,u(),n.onError(c.message)});let d=c=>{if(!(i||s.pid==null))try{tt?et("taskkill",["/pid",String(s.pid),"/T","/F"],{stdio:"ignore"}):process.kill(-s.pid,c)}catch{}};return{kill:()=>{i||(d("SIGTERM"),u(),a=setTimeout(()=>{a=null,d("SIGKILL")},z),a.unref?.())}}}import{spawn as Wt}from"node:child_process";import{readFileSync as st}from"node:fs";import{isAbsolute as jt}from"node:path";import{createRequire as Ft}from"node:module";var Gt=Ft(import.meta.url),nt="__secure-exec-drop";function Bt(){let e=!1;try{e=Gt("node:sea").isSea()}catch{}return e}function ot(e){let t=process.argv[1],r=!Bt()&&t?[t,nt,...e]:[nt,...e];return{cmd:process.execPath,args:r}}var Xt=4294967295,Kt=new Set(Be);function _e(e,t){if(!/^\d+$/.test(e))throw new Error(`invalid numeric id in ${t}`);let r=Number(e);if(!Number.isSafeInteger(r)||r<0||r>Xt)throw new Error(`invalid numeric id in ${t}`);return r}function Jt(){let e;try{e=st("/etc/group","utf8")}catch{throw new Error("cannot read /etc/group to verify secure-exec group membership")}let t=[],r=new Set,n=new Set;for(let[o,s]of e.split(`
2
+ `).entries()){if(s==="")continue;let i=s.split(":");if(i.length!==4)throw new Error(`malformed /etc/group entry at line ${o+1}`);let a=i[0],u=_e(i[2],`/etc/group line ${o+1}`),d=i[3];if(!a||/[\s,\u0000-\u001f\u007f]/.test(a))throw new Error(`malformed /etc/group entry at line ${o+1}`);let c=d===""?[]:d.split(",");if(c.some(w=>!w||/[\s:\u0000-\u001f\u007f]/.test(w))||new Set(c).size!==c.length)throw new Error(`malformed /etc/group member list at line ${o+1}`);if(r.has(a)||n.has(u))throw new Error(`duplicate /etc/group name or gid at line ${o+1}`);r.add(a),n.add(u),t.push({name:a,gid:u,members:c})}return t}function Yt(e){let t=Jt();if(!t.some(o=>o.gid===e.gid))throw new Error(`primary group ${e.gid} for ${C} is not in /etc/group`);let n=t.filter(o=>o.gid===e.gid||o.members.includes(e.name)).find(o=>o.gid===0||Kt.has(o.name));if(n)throw new Error(`secure exec user "${C}" belongs to privileged group "${n.name}" (gid ${n.gid})`)}function Zt(){let e;try{e=st("/etc/passwd","utf8")}catch{throw new Error("cannot read /etc/passwd to resolve the secure-exec user")}let t=e.split(`
3
+ `).filter(a=>a.startsWith(`${C}:`));if(t.length===0)throw new Error(`secure exec user "${C}" not found in /etc/passwd \u2014 reinstall the agent to create it`);if(t.length>1)throw new Error(`multiple /etc/passwd entries for "${C}" \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 ${C}`);let n,o;try{n=_e(r[2],"/etc/passwd uid"),o=_e(r[3],"/etc/passwd gid")}catch{throw new Error(`could not resolve uid/gid for ${C}`)}let s=r[5]||"/tmp";if(n===0)throw new Error("refusing to run secure exec as uid 0");if(o===0)throw new Error("refusing to run secure exec as gid 0");let i={uid:n,gid:o,home:s,name:C};return Yt(i),i}var zt="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",qt=new Set(["NODE_OPTIONS","BASH_ENV","ENV","IFS","PYTHONSTARTUP","PYTHONPATH","PERL5OPT","RUBYOPT","GIT_EXTERNAL_DIFF","PATH","HOME","USER","LOGNAME"]),Qt=e=>/^LD_/.test(e)||qt.has(e);function it(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 n=r[0];if(!n||/[/\\\s]/.test(n))throw new Error(`command "${n}" must be a bare command name (no path separators or whitespace)`);if(Ge(n))throw new Error(`command "${n}" is denied for secure exec`);if(!e.allowedCommands.includes(n))throw new Error(`command "${n}" is not in this token's allowlist`);if(e.cwd!=null&&!jt(e.cwd))throw new Error(`cwd "${e.cwd}" must be an absolute path`);let o=null;if(e.input!=null&&(o=Buffer.from(e.input,"base64"),o.length>Se))throw new Error(`stdin payload ${o.length}B exceeds limit ${Se}B`);let s=Zt(),i={HOME:s.home,USER:s.name,LOGNAME:s.name,PATH:zt},a={};for(let[f,A]of Object.entries(e.env??{}))Qt(f)||(a[f]=A);let u={...a,...i},d=Date.now(),{cmd:c,args:w}=ot([String(s.uid),String(s.gid),s.name,n,...r.slice(1)]),l=Wt(c,w,{cwd:e.cwd??s.home,env:u,stdio:["pipe","pipe","pipe"],detached:!0,shell:!1});o&&l.stdin?(l.stdin.on("error",()=>{}),l.stdin.end(o)):l.stdin?.end();let m=!1,p=null,b=()=>{p&&(clearTimeout(p),p=null)};l.stdout?.on("data",f=>{t.onOutput(f.toString("base64"),"stdout")}),l.stderr?.on("data",f=>{t.onOutput(f.toString("base64"),"stderr")}),l.on("close",f=>{m=!0,b(),t.onDone(f??-1,Date.now()-d)}),l.on("error",f=>{m=!0,b(),t.onError(f.message)});let O=f=>{if(!(m||l.pid==null))try{process.kill(-l.pid,f)}catch{}};return{kill:()=>{m||(O("SIGTERM"),b(),p=setTimeout(()=>{p=null,O("SIGKILL")},z),p.unref?.())}}}import B from"chalk";function at(e,t){console.log(B.yellow(` Reconnecting in ${Math.round(t/1e3)}s (attempt ${e})...`))}function ct(){console.log(B.green(" Connected to relay \u2014 ready for commands."))}function we(e,t){let r=e==="secure-exec"&&t!=null&&/^[A-Za-z0-9][A-Za-z0-9._+@-]{0,127}$/.test(t)?t:null,n=e==="secure-exec"?"Secure exec":"Command",o=r?` (${r})`:"";console.log(B.cyan(` ${n}${o} started.`))}function be(e,t,r){let n=t===0?B.green:B.red;console.log(n(` ${e==="secure-exec"?"Secure exec":"Command"} finished. Exit ${t} in ${r}ms.`))}function er(){let e=process.env.AICOMMANDER_DEV;return e==="1"||e==="true"}function ut(e){let t=e.toLowerCase();return t==="localhost"||t==="127.0.0.1"||t==="::1"||t==="[::1]"}function lt(){return new URL(I).origin}function dt(e){let t=(e??"").trim();if(!t)return I;let r;try{r=new URL(t)}catch{return console.warn(`
4
+ \u26A0 Ignoring malformed AICOMMANDER_SERVER (${t}); using ${I}.
5
+ `),I}if(r.origin===lt())return I;let n=ut(r.hostname);return!er()&&!n?(console.warn(`
5
6
  \u26A0 AICOMMANDER_SERVER override to "${r.origin}" ignored for safety \u2014
6
- the agent runs as root and is host-locked to ${$e()}.
7
+ the agent runs as root and is host-locked to ${lt()}.
7
8
  Set AICOMMANDER_DEV=1 to allow a custom relay for development.
8
- `),T):r.protocol!=="https:"&&!o?(console.warn(`
9
- \u26A0 Refusing plaintext relay "${r.origin}" (https required); using ${T}.
10
- `),T):t.replace(/\/+$/,"")}function Fe(e){let t;try{t=new URL(e)}catch{throw new Error("Refusing to connect: malformed relay WebSocket URL.")}if(t.protocol!=="wss:"&&!(t.protocol==="ws:"&&Ue(t.hostname)))throw new Error(`Refusing to open a plaintext relay socket (${t.protocol}//${t.host}); wss:// required.`)}var le={capable:!1,enabled:!1,expiresAt:null},Ve=360*60*1e3;function ue(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 de(e){let t=1e3,r=3e4,o=0,s=e.agentToken;for(;!e.signal?.aborted;){o++;try{let n=await wt(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||Le(o,c);try{await ue(c,e.signal??new AbortController().signal)}catch(u){if(u.name==="AbortError")break}t=Math.min(t*2,r)}}}async function wt(e,t){let{serverUrl:r,sessionCode:o}=e,s=`${r.replace(/^http/,"ws")}/ws/agent/${o}?token=${t}`;Fe(s);let n=a=>{try{e.onStatus?.(a)}catch{}},i=a=>{try{e.onActivity?.(a)}catch{}},c=a=>{if(a)try{e.onRemoteActivity?.(a)}catch{}},u=()=>{try{e.onHeartbeat?.()}catch{}};return n("connecting"),new Promise((a,d)=>{let m=new At(s),l=null,E=null,S=null,b=!1,_=null,p=!1,C=()=>{S&&(clearTimeout(S),S=null)},f=()=>{_&&(clearInterval(_),_=null)},I=()=>{if(!e.reauthIntervalMs||!e.reauth)return;let h=()=>{if(l){S=setTimeout(h,3e4);return}b=!0,m.close(1e3,"reauth")};S=setTimeout(h,e.reauthIntervalMs)},P=()=>{p||(p=!0,C(),f(),a(b))},L=h=>{p||(p=!0,C(),f(),d(h))},we=()=>{u(),E&&clearTimeout(E),E=setTimeout(()=>{m.terminate(),L(new Error("Ping timeout"))},9e4)};e.signal?.addEventListener("abort",()=>m.close(1e3,"disabled"),{once:!0});let te=()=>{if(p)return;let h={type:"agent:screen_state",screenShare:e.screenShare?.getState()??le};try{m.send(JSON.stringify(h))}catch{}};m.on("open",()=>{n("connected"),we(),I();let h={type:"agent:register",hostname:bt.hostname(),platform:process.platform,arch:process.arch,agentVersion:U,screenShare:e.screenShare?.getState()??le};m.send(JSON.stringify(h)),e.screenShare?.on("change",te),e.silent||He()}),m.on("message",h=>{let y;try{y=JSON.parse(String(h))}catch{return}switch(y.type){case"do:ping":we();let at={type:"agent:pong",ts:y.ts};m.send(JSON.stringify(at));break;case"do:exec":{ae(y.command),i(!0),c(y.operator);let{commandId:w,command:D,cwd:re,env:ne}=y,X=Date.now(),H=!1;f(),_=setInterval(()=>{let v=Date.now()-X;if(v>=3e5&&!H){H=!0;let g={type:"agent:exec_idle",commandId:w,idleMs:v};m.send(JSON.stringify(g))}},3e5),_.unref?.(),l=Oe(D,re,ne,{onOutput:(v,g)=>{X=Date.now(),H=!1;let A={type:"agent:output",commandId:w,chunk:v,stream:g};m.send(JSON.stringify(A))},onDone:(v,g)=>{l=null,f(),i(!1),ce(v,g);let A={type:"agent:done",commandId:w,exitCode:v,durationMs:g};m.send(JSON.stringify(A))},onError:v=>{l=null,f(),i(!1);let g={type:"agent:error",commandId:w,error:v};m.send(JSON.stringify(g))}}).kill;break}case"do:secure_exec":{let{commandId:w,argv:D,allowedCommands:re,input:ne,cwd:X,env:H}=y,Te=(g,A)=>{l=null,f(),i(!1),ce(g,A);let $={type:"agent:done",commandId:w,exitCode:g,durationMs:A};m.send(JSON.stringify($))},v=g=>{l=null,f(),i(!1);let A={type:"agent:error",commandId:w,error:g};m.send(JSON.stringify(A))};try{let g=Array.isArray(D)?D.join(" "):String(D);ae(g),i(!0);let A=Date.now(),$=!1;f(),_=setInterval(()=>{let K=Date.now()-A;if(K>=3e5&&!$){$=!0;let oe={type:"agent:exec_idle",commandId:w,idleMs:K};m.send(JSON.stringify(oe))}},3e5),_.unref?.(),l=Pe({argv:D,allowedCommands:re,input:ne,cwd:X,env:H},{onOutput:(K,oe)=>{A=Date.now(),$=!1;let ct={type:"agent:output",commandId:w,chunk:K,stream:oe};m.send(JSON.stringify(ct))},onDone:Te,onError:v}).kill}catch(g){v(g instanceof Error?g.message:String(g))}break}case"do:kill":l&&(l(),l=null);break;case"do:screenshot":c(y.operator),Tt(m,y.requestId,e.screenShare);break}}),m.on("close",(h,y)=>{E&&clearTimeout(E),f(),e.screenShare?.off("change",te),l&&(l(),l=null,i(!1)),n("disconnected"),h===1e3||h===4001?P():L(new Error(`WS closed: ${h} ${String(y)}`))}),m.on("error",h=>{E&&clearTimeout(E),f(),e.screenShare?.off("change",te),n("disconnected"),L(h)}),m.on("unexpected-response",(h,y)=>{d(new Error(`Unexpected HTTP response: ${y.statusCode}`))})})}async function Tt(e,t,r){let o=i=>{try{e.send(JSON.stringify(i))}catch{}},s=i=>o({type:"agent:screenshot_error",requestId:t,error:i}),n=r?.getState()??le;if(!r||!n.capable){s("This machine cannot share its screen (desktop macOS/Windows only).");return}if(!n.enabled){s("Screen sharing is turned off on this machine.");return}try{let{data:i,mimeType:c}=await r.capture();if(i.length>10485760){let a=(i.length/1048576).toFixed(1);s(`Screenshot is ${a} MB, which exceeds the ${10485760/(1024*1024)} MB limit.`);return}let u=i.toString("base64");for(let a=0;a<u.length;a+=524288)o({type:"agent:screenshot_chunk",requestId:t,chunk:u.slice(a,a+524288)});o({type:"agent:screenshot_done",requestId:t,mimeType:c,totalBytes:i.length})}catch(i){s(`Screenshot failed: ${i.message}`)}}import Gt from"node:events";import Z from"node:fs";import N from"node:path";import Rt from"node:os";import{randomUUID as xt,randomBytes as Ct}from"node:crypto";var Q="/etc/aicommander-agent",q=N.join(Rt.homedir(),".config","aicommander-agent"),G="device.json";function pe(){return{deviceId:xt(),deviceSecret:Ct(32).toString("base64url")}}function It(e){return typeof e=="object"&&e!==null&&typeof e.deviceId=="string"&&e.deviceId.length>0&&typeof e.deviceSecret=="string"&&e.deviceSecret.length>0}function me(e){try{let t=Z.readFileSync(e,"utf8"),r=JSON.parse(t);if(It(r))return{deviceId:r.deviceId,deviceSecret:r.deviceSecret}}catch{}return null}function We(e){try{Z.rmSync(N.join(e,G),{force:!0})}catch{}}function M(e,t){try{return Z.mkdirSync(e,{recursive:!0,mode:448}),Z.writeFileSync(N.join(e,G),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function V(e){if(e){let s=me(N.join(e,G));if(s)return s;let n=pe();return M(e,n),n}let t=me(N.join(Q,G));if(t)return t;let r=me(N.join(q,G));if(r)return r;let o=pe();return M(Q,o)||M(q,o),o}function je(e){let t=pe();return e?(M(e,t),t):M(Q,t)?(We(q),t):(M(q,t),We(Q),t)}function Xe(e){return{"X-Device-Id":e.deviceId,"X-Device-Secret":e.deviceSecret}}var Ke=1e4;async function ge(e,t){let r;try{r=await fetch(`${e}/api/device/admins`,{headers:Xe(t),signal:AbortSignal.timeout(Ke)})}catch{throw new Error("Failed to list linked accounts (relay unreachable or timed out).")}if(!r.ok)throw new Error(`Failed to list linked accounts (HTTP ${r.status}).`);return await r.json()}async function Je(e,t,r,o){let s;try{s=await fetch(`${e}/api/device/${r}`,{method:"POST",headers:{...Xe(t),"Content-Type":"application/json"},body:JSON.stringify({userId:o}),signal:AbortSignal.timeout(Ke)})}catch{return{ok:!1,error:"unreachable"}}let n;try{n=await s.json()}catch{n={ok:!1,error:`http_${s.status}`}}return{...n,ok:s.ok}}function fe(e,t,r){return Je(e,t,"block",r)}function he(e,t,r){return Je(e,t,"unblock",r)}function Dt(e){return[...e.filter(t=>!t.blocked),...e.filter(t=>t.blocked)]}function Ot(e,t){let r=t.trim();if(!r)return{kind:"not_found"};if(/^\d+$/.test(r)){let n=Number(r)-1;return n>=0&&n<e.length?{kind:"ok",admin:e[n]}:{kind:"not_found"}}let o=e.find(n=>n.userId===r);if(o)return{kind:"ok",admin:o};let s=e.filter(n=>n.userId.startsWith(r));return s.length===1?{kind:"ok",admin:s[0]}:s.length>1?{kind:"ambiguous",matches:s}:{kind:"not_found"}}import x from"node:fs";import k from"node:path";import Nt from"node:os";var Mt="/etc/aicommander-agent",kt=k.join(Nt.homedir(),".config","aicommander-agent"),Se="session.json",Ye=".rotate";function Pt(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0&&typeof e.agentToken=="string"&&e.agentToken.length>0}function Lt(e){try{let t=x.readFileSync(e,"utf8"),r=JSON.parse(t);if(Pt(r))return{sessionCode:r.sessionCode,agentToken:r.agentToken}}catch{}return null}function Ht(e,t){try{return x.mkdirSync(e,{recursive:!0,mode:448}),x.writeFileSync(k.join(e,Se),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function W(e){return e?[e]:[Mt,kt]}function Ze(e){for(let t of W(e)){let r=Lt(k.join(t,Se));if(r)return r}return null}function Ee(e,t){for(let r of W(t))if(Ht(r,e))return}function ye(e){for(let t of W(e))try{x.rmSync(k.join(t,Se),{force:!0})}catch{}}function Qe(e){for(let t of W(e))try{x.mkdirSync(t,{recursive:!0,mode:448}),x.writeFileSync(k.join(t,Ye),"",{mode:384});return}catch{}}function qe(e){let t=!1;for(let r of W(e)){let o=k.join(r,Ye);try{x.existsSync(o)&&(t=!0,x.rmSync(o,{force:!0}))}catch{}}return t}function Ft(e,t){if(!e)return!0;let r=t-e.lastNotifiedAt;if(r>=288e5)return!0;let o=t-e.lastActivityAt>6e5,s=r>=36e5;return o&&s}var z=class{seen=new Map;note(t,r){let o=this.seen.get(t),s=Ft(o,r);return this.seen.set(t,{lastActivityAt:r,lastNotifiedAt:s?r:o?.lastNotifiedAt??0}),s}};var _e=class extends Gt{serverUrl;configDir;screenShare;abortController=null;_status="disabled";_code=null;_active=!1;_lastHeartbeatAt=0;_remote=new z;constructor(t,r){super(),this.serverUrl=Be(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=V(this.configDir),n=!1,i=qe(this.configDir);for(;!t.signal.aborted;){this._setStatus("connecting");try{let c=i?null:Ze(this.configDir),{sessionCode:u,agentToken:a}=await J(this.serverUrl,s,{forceNew:i,...c?{currentCode:c.sessionCode}:{}});if(t.signal.aborted)break;Ee({sessionCode:u,agentToken:a},this.configDir),i=!1,this._code=u;try{this.emit("code",u)}catch{}await de({serverUrl:this.serverUrl,sessionCode:u,agentToken:a,signal:t.signal,silent:!0,...this.screenShare?{screenShare:this.screenShare}:{},onStatus:d=>this._setStatus(d),onActivity:d=>this._setActive(d),onRemoteActivity:d=>this._noteRemote(d),onHeartbeat:()=>this._onHeartbeat(),reauthIntervalMs:Ve,reauth:async()=>{let d=await J(this.serverUrl,s,{currentCode:u});if(t.signal.aborted||this.abortController!==t)throw new DOMException("Aborted","AbortError");return Ee({sessionCode:d.sessionCode,agentToken:d.agentToken},this.configDir),d.agentToken}});break}catch(c){if(t.signal.aborted)break;if(c instanceof O&&c.status===403&&c.code===Re&&!n){n=!0,console.error(`[AIC] Relay rejected this device: ${c.message}. Regenerating device identity and retrying as a new device.`),s=je(this.configDir),ye(this.configDir),i=!1;continue}c instanceof O&&console.error(`[AIC] ${c.message}`),this._setStatus("disconnected");try{await ue(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(){ye(this.configDir),Qe(this.configDir),await this.restart()}async listAdmins(){let t=V(this.configDir);return ge(this.serverUrl,t)}async blockAdmin(t){let r=V(this.configDir);return fe(this.serverUrl,r,t)}async unblockAdmin(t){let r=V(this.configDir);return he(this.serverUrl,r,t)}_setStatus(t){if(t!=="connected"&&this._active&&this._setActive(!1),this._status!==t){this._status=t;try{this.emit("status",t)}catch{}}}_noteRemote(t){let r=Date.now();if(this._remote.note(t.id,r))try{this.emit("remote-connect",{id:t.id,anonymous:t.anonymous})}catch{}}_onHeartbeat(){this._lastHeartbeatAt=Date.now();try{this.emit("heartbeat",this._lastHeartbeatAt)}catch{}}_setActive(t){if(this._active!==t){this._active=t;try{this.emit("activity",t)}catch{}}}};import{spawn as Vt}from"child_process";import{promises as ve}from"fs";import Wt from"os";import jt from"path";import Xt from"crypto";function Kt(){return process.platform==="darwin"||process.platform==="win32"}async function Jt(){if(process.platform==="darwin")return Yt();if(process.platform==="win32")return Zt();throw new Error("Screen capture is only supported on the macOS and Windows desktop apps.")}function ze(e){return jt.join(Wt.tmpdir(),`aic-shot-${Xt.randomUUID()}.${e}`)}async function Yt(){let e=ze("png");try{return await rt("screencapture",["-x","-t","png",e]),await et(e,"image/png")}finally{await tt(e)}}async function Zt(){let e=ze("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 rt("powershell",["-NoProfile","-NonInteractive","-Command",r]),await et(e,"image/png")}finally{await tt(e)}}async function et(e,t){let r=await ve.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 ve.readFile(e),mimeType:t}}async function tt(e){try{await ve.unlink(e)}catch{}}function rt(e,t){return new Promise((r,o)=>{let s=Vt(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 or}from"child_process";import ee from"fs";import nt from"path";import qt from"os";function Ae(e){if(e)return nt.join(e,"heartbeat");if(process.platform==="linux")try{return ee.mkdirSync("/run/aicommander-agent",{recursive:!0}),"/run/aicommander-agent/heartbeat"}catch{}return nt.join(qt.tmpdir(),"aicommander-agent-heartbeat")}function ot(e){try{let t=`${e}.tmp`;ee.writeFileSync(t,String(Date.now())),ee.renameSync(t,e)}catch{}}var j=null;function zt(e,t=5e3){st(),ot(e),j=setInterval(()=>ot(e),t),j.unref?.()}function st(){j&&(clearInterval(j),j=null)}function be(e){try{let t=ee.readFileSync(e,"utf8").trim(),r=Number(t);return Number.isFinite(r)?r:null}catch{return null}}async function sr(e={}){let t=e.heartbeatPath??Ae(e.configDir),r=e.spawnWorker??(d=>or(process.execPath,process.argv.slice(1),{env:d,stdio:"inherit"})),o=null,s=0,n=!1,i=0,c=[],u=null,a=null;await new Promise(d=>{let m=null,l=null,E=()=>{u&&(clearInterval(u),u=null),a&&(clearTimeout(a),a=null),m&&(process.removeListener("SIGINT",m),m=null),l&&(process.removeListener("SIGTERM",l),l=null)},S=()=>{E(),d()},b=()=>{s=Date.now(),i++;let f={...process.env,AIC_ROLE:"worker",AIC_HEARTBEAT:t};o=r(f),o.on("exit",(I,P)=>{if(o=null,n){S();return}if(e.maxSpawns!=null&&i>=e.maxSpawns){S();return}_(`worker exited (code=${I??"null"} signal=${P??"null"})`)}),o.on("error",()=>{})},_=f=>{let I=Date.now();c=c.filter(L=>I-L<3e5),c.push(I);let P=c.length>5?6e4:0;a&&clearTimeout(a),a=setTimeout(()=>{a=null,n||b()},P),a.unref?.()},p=()=>{if(o)try{o.kill("SIGKILL")}catch{}};u=setInterval(()=>{if(n||!o||Date.now()-s<3e4)return;let f=be(t);f!=null&&Date.now()-f>3e4&&p()},1e4),u.unref?.();let C=f=>{if(n=!0,u&&(clearInterval(u),u=null),a&&(clearTimeout(a),a=null),o)try{o.kill(f)}catch{}else S()};m=()=>C("SIGINT"),l=()=>C("SIGTERM"),process.on("SIGINT",m),process.on("SIGTERM",l),b()})}function ir(e,t){return se(e,t)}function ar(e,t){return Ce(e,t)}async function cr(e,t=1e4){let r=new AbortController,o=setTimeout(()=>r.abort(),t);try{let s=await fetch(`${e.replace(/\/+$/,"")}/dist/latest`,{signal:r.signal,headers:{Accept:"application/json"}});if(!s.ok)return null;let n=await s.json();return{version:typeof n.version=="string"?n.version:null,mac:typeof n.mac=="string"?n.mac:null,win:typeof n.win=="string"?n.win:null,agentLinuxX64:typeof n.agentLinuxX64=="string"?n.agentLinuxX64:null,agentLinuxArm64:typeof n.agentLinuxArm64=="string"?n.agentLinuxArm64:null}}catch{return null}finally{clearTimeout(o)}}export{U as AGENT_VERSION,_e as AgentController,fe as blockAdmin,Kt as canCaptureScreenshot,Jt as captureScreenshot,ir as compareVersions,Ae as defaultHeartbeatPath,ge as fetchAdmins,cr as fetchLatestDist,ar as isNewerVersion,Dt as orderAdmins,be as readHeartbeat,J as register,Ot as resolveAdminIdentifier,de as runConnectionLoop,sr as runSupervisor,zt as startHeartbeat,st as stopHeartbeat,he as unblockAdmin};
9
+ `),I):r.protocol!=="https:"&&!n?(console.warn(`
10
+ \u26A0 Refusing plaintext relay "${r.origin}" (https required); using ${I}.
11
+ `),I):t.replace(/\/+$/,"")}function ve(e){let t;try{t=new URL(e)}catch{throw new Error("Refusing to connect: malformed relay WebSocket URL.")}if(t.protocol!=="wss:"&&!(t.protocol==="ws:"&&ut(t.hostname)))throw new Error(`Refusing to open a plaintext relay socket (${t.protocol}//${t.host}); wss:// required.`)}var Ae={capable:!1,enabled:!1,expiresAt:null},ft=360*60*1e3,nr=/^[a-f0-9]{64}$/;function Te(e,t){return new Promise((r,n)=>{if(t.aborted){n(new DOMException("Aborted","AbortError"));return}let o=setTimeout(r,e);t.addEventListener("abort",()=>{clearTimeout(o),n(new DOMException("Aborted","AbortError"))},{once:!0})})}async function xe(e){let t=1e3,r=3e4,n=0,o=e.agentToken;for(;!e.signal?.aborted;){n++;try{let s=await sr(e,o);if(t=1e3,n=0,s&&e.reauth&&!e.signal?.aborted)try{o=await e.reauth()}catch{}}catch(s){if(s.name==="AbortError")break;let i=.8+Math.random()*.4,a=Math.min(t*i,r);e.silent||at(n,a);try{await Te(a,e.signal??new AbortController().signal)}catch(u){if(u.name==="AbortError")break}t=Math.min(t*2,r)}}}async function or(e,t){let r;try{r=await fetch(`${e.serverUrl}/api/agent/ws-ticket`,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify({sessionCode:e.sessionCode}),...e.signal?{signal:e.signal}:{}})}catch(n){throw n.name==="AbortError"?n:new Error("Failed to request WebSocket connection ticket.")}if(!r.ok)throw new Error(`WebSocket connection ticket request failed (${r.status}).`);try{let n=await r.json();if(typeof n.ticket!="string"||!nr.test(n.ticket))throw new Error("invalid ticket");return n.ticket}catch{throw new Error("Relay returned an invalid WebSocket connection ticket.")}}async function sr(e,t){let{serverUrl:r}=e,n=`${r.replace(/^http/,"ws")}/ws/agent`;ve(n);let o=await or(e,t),s=`${n}?ticket=${o}`;ve(s);let i=c=>{try{e.onStatus?.(c)}catch{}},a=c=>{try{e.onActivity?.(c)}catch{}},u=c=>{if(c)try{e.onRemoteActivity?.(c)}catch{}},d=()=>{try{e.onHeartbeat?.()}catch{}};return i("connecting"),new Promise((c,w)=>{let l=new tr(s,{headers:{Authorization:`Bearer ${t}`}}),m=null,p=null,b=null,O=!1,f=null,A=!1,T=()=>{b&&(clearTimeout(b),b=null)},g=()=>{f&&(clearInterval(f),f=null)},V=()=>{if(!e.reauthIntervalMs||!e.reauth)return;let S=()=>{if(m){b=setTimeout(S,3e4);return}O=!0,l.close(1e3,"reauth")};b=setTimeout(S,e.reauthIntervalMs)},ue=()=>{A||(A=!0,T(),g(),c(O))},J=S=>{A||(A=!0,T(),g(),w(S))},Ve=()=>{d(),p&&clearTimeout(p),p=setTimeout(()=>{l.terminate(),J(new Error("Ping timeout"))},9e4)};e.signal?.addEventListener("abort",()=>l.close(1e3,"disabled"),{once:!0});let de=()=>{if(A)return;let S={type:"agent:screen_state",screenShare:e.screenShare?.getState()??Ae};try{l.send(JSON.stringify(S))}catch{}};l.on("open",()=>{i("connected"),Ve(),V();let S={type:"agent:register",hostname:rr.hostname(),platform:process.platform,arch:process.arch,agentVersion:F,screenShare:e.screenShare?.getState()??Ae};l.send(JSON.stringify(S)),e.screenShare?.on("change",de),e.silent||ct()}),l.on("message",S=>{let x;try{x=JSON.parse(String(S))}catch{return}switch(x.type){case"do:ping":Ve();let Mt={type:"agent:pong",ts:x.ts};l.send(JSON.stringify(Mt));break;case"do:exec":{let{commandId:R,command:N,cwd:fe,env:me}=x,pe=(E,v)=>{m=null,g(),a(!1),e.silent||be("command",E,v);let h={type:"agent:done",commandId:R,exitCode:E,durationMs:v};l.send(JSON.stringify(h))},Y=E=>{m=null,g(),a(!1);let v={type:"agent:error",commandId:R,error:E};l.send(JSON.stringify(v))};try{e.silent||we("command"),a(!0),u(x.operator);let E=Date.now(),v=!1;g(),f=setInterval(()=>{let y=Date.now()-E;if(y>=G&&!v){v=!0;let M={type:"agent:exec_idle",commandId:R,idleMs:y};l.send(JSON.stringify(M))}},G),f.unref?.(),m=rt(N,fe,me,{onOutput:(y,M)=>{E=Date.now(),v=!1;let D={type:"agent:output",commandId:R,chunk:y,stream:M};l.send(JSON.stringify(D))},onDone:pe,onError:Y}).kill}catch(E){Y(E instanceof Error?E.message:String(E))}break}case"do:secure_exec":{let{commandId:R,argv:N,allowedCommands:fe,input:me,cwd:pe,env:Y}=x,E=(h,y)=>{m=null,g(),a(!1),e.silent||be("secure-exec",h,y);let M={type:"agent:done",commandId:R,exitCode:h,durationMs:y};l.send(JSON.stringify(M))},v=h=>{m=null,g(),a(!1);let y={type:"agent:error",commandId:R,error:h};l.send(JSON.stringify(y))};try{if(!e.silent){let D=Array.isArray(N)&&typeof N[0]=="string"?N[0]:void 0;we("secure-exec",D)}a(!0);let h=Date.now(),y=!1;g(),f=setInterval(()=>{let D=Date.now()-h;if(D>=G&&!y){y=!0;let ge={type:"agent:exec_idle",commandId:R,idleMs:D};l.send(JSON.stringify(ge))}},G),f.unref?.(),m=it({argv:N,allowedCommands:fe,input:me,cwd:pe,env:Y},{onOutput:(D,ge)=>{h=Date.now(),y=!1;let Pt={type:"agent:output",commandId:R,chunk:D,stream:ge};l.send(JSON.stringify(Pt))},onDone:E,onError:v}).kill}catch(h){v(h instanceof Error?h.message:String(h))}break}case"do:kill":m&&(m(),m=null);break;case"do:screenshot":u(x.operator),ir(l,x.requestId,e.screenShare);break}}),l.on("close",S=>{p&&clearTimeout(p),g(),e.screenShare?.off("change",de),m&&(m(),m=null,a(!1)),i("disconnected"),S===1e3||S===4001?ue():J(new Error(`WebSocket closed (${S}).`))}),l.on("error",()=>{p&&clearTimeout(p),g(),e.screenShare?.off("change",de),i("disconnected"),J(new Error("WebSocket connection error."))}),l.on("unexpected-response",(S,x)=>{J(new Error(`Unexpected WebSocket HTTP response (${x.statusCode}).`))})})}async function ir(e,t,r){let n=i=>{try{e.send(JSON.stringify(i))}catch{}},o=i=>n({type:"agent:screenshot_error",requestId:t,error:i}),s=r?.getState()??Ae;if(!r||!s.capable){o("This machine cannot share its screen (desktop macOS/Windows only).");return}if(!s.enabled){o("Screen sharing is turned off on this machine.");return}try{let{data:i,mimeType:a}=await r.capture();if(i.length>L){let d=(i.length/1048576).toFixed(1);o(`Screenshot is ${d} MB, which exceeds the ${L/(1024*1024)} MB limit.`);return}let u=i.toString("base64");for(let d=0;d<u.length;d+=he)n({type:"agent:screenshot_chunk",requestId:t,chunk:u.slice(d,d+he)});n({type:"agent:screenshot_done",requestId:t,mimeType:a,totalBytes:i.length})}catch(i){o(`Screenshot failed: ${i.message}`)}}import br from"node:events";import q from"node:fs";import $ from"node:path";import ar from"node:os";import{randomUUID as cr,randomBytes as lr}from"node:crypto";var Q="/etc/aicommander-agent",ee=$.join(ar.homedir(),".config","aicommander-agent"),W="device.json";function Re(){return{deviceId:cr(),deviceSecret:lr(32).toString("base64url")}}function ur(e){return typeof e=="object"&&e!==null&&typeof e.deviceId=="string"&&e.deviceId.length>0&&typeof e.deviceSecret=="string"&&e.deviceSecret.length>0}function Ce(e){try{let t=q.readFileSync(e,"utf8"),r=JSON.parse(t);if(ur(r))return{deviceId:r.deviceId,deviceSecret:r.deviceSecret}}catch{}return null}function mt(e){try{q.rmSync($.join(e,W),{force:!0})}catch{}}function H(e,t){try{return q.mkdirSync(e,{recursive:!0,mode:448}),q.writeFileSync($.join(e,W),JSON.stringify(t,null,2),{encoding:"utf8",mode:384}),!0}catch{return!1}}function j(e){if(e){let o=Ce($.join(e,W));if(o)return o;let s=Re();return H(e,s),s}let t=Ce($.join(Q,W));if(t)return t;let r=Ce($.join(ee,W));if(r)return r;let n=Re();return H(Q,n)||H(ee,n),n}function pt(e){let t=Re();return e?(H(e,t),t):H(Q,t)?(mt(ee),t):(H(ee,t),mt(Q),t)}function gt(e){return{"X-Device-Id":e.deviceId,"X-Device-Secret":e.deviceSecret}}var St=1e4;async function Ie(e,t){let r;try{r=await fetch(`${e}/api/device/admins`,{headers:gt(t),signal:AbortSignal.timeout(St)})}catch{throw new Error("Failed to list linked accounts (relay unreachable or timed out).")}if(!r.ok)throw new Error(`Failed to list linked accounts (HTTP ${r.status}).`);return await r.json()}async function ht(e,t,r,n){let o;try{o=await fetch(`${e}/api/device/${r}`,{method:"POST",headers:{...gt(t),"Content-Type":"application/json"},body:JSON.stringify({userId:n}),signal:AbortSignal.timeout(St)})}catch{return{ok:!1,error:"unreachable"}}let s;try{s=await o.json()}catch{s={ok:!1,error:`http_${o.status}`}}return{...s,ok:o.ok}}function De(e,t,r){return ht(e,t,"block",r)}function ke(e,t,r){return ht(e,t,"unblock",r)}function dr(e){return[...e.filter(t=>!t.blocked),...e.filter(t=>t.blocked)]}function fr(e,t){let r=t.trim();if(!r)return{kind:"not_found"};if(/^\d+$/.test(r)){let s=Number(r)-1;return s>=0&&s<e.length?{kind:"ok",admin:e[s]}:{kind:"not_found"}}let n=e.find(s=>s.userId===r);if(n)return{kind:"ok",admin:n};let o=e.filter(s=>s.userId.startsWith(r));return o.length===1?{kind:"ok",admin:o[0]}:o.length>1?{kind:"ambiguous",matches:o}:{kind:"not_found"}}import k from"node:fs";import U from"node:path";import pr from"node:os";import _ from"node:fs";import Et from"node:path";var Oe=448,te=384;function mr(e){_.mkdirSync(e,{recursive:!0,mode:Oe});try{_.chmodSync(e,Oe)}catch{}}function re(e,t){try{_.chmodSync(e,Oe)}catch{}try{_.chmodSync(t,te)}catch{}}function yt(e,t,r){mr(e);let n=Et.join(e,t),o=Et.join(e,`.${t}.${process.pid}.${Date.now()}.tmp`);try{r(o),_.renameSync(o,n);try{_.chmodSync(n,te)}catch{}}catch(s){try{_.rmSync(o,{force:!0})}catch{}throw s}}function ne(e,t,r){yt(e,t,n=>{let o=_.openSync(n,"wx",te);try{_.writeFileSync(o,r,{encoding:"utf8"})}finally{_.closeSync(o)}})}function _t(e,t,r){yt(e,t,n=>{let o=_.openSync(n,"wx",te);try{_.writeFileSync(o,r)}finally{_.closeSync(o)}})}var oe=class extends Error{constructor(t){super(t),this.name="CredentialStorageError"}};function Ne(){return!!(process.env.AICOMMANDER_SERVICE==="1"||process.env.NODE_ENV==="production"||process.env.INVOCATION_ID||process.env.JOURNAL_STREAM)}function wt(e,t){let r=t instanceof Error?t.message:String(t);throw new oe(`Failed to persist ${e} with required filesystem permissions: ${r}`)}var gr="/etc/aicommander-agent",Sr=U.join(pr.homedir(),".config","aicommander-agent"),se="session.json",ie="session.token",bt=".rotate";function X(e){return typeof e=="string"?{configDir:e}:e??{}}function hr(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0&&typeof e.agentToken=="string"&&e.agentToken.length>0}function Er(e){return typeof e=="object"&&e!==null&&typeof e.sessionCode=="string"&&e.sessionCode.length>0}function yr(e,t){let r=U.join(e,se);try{if(!k.existsSync(r))return null;re(e,r);let n=k.readFileSync(r,"utf8"),o=JSON.parse(n);if(!Er(o))return null;if(o.tokenProtected){let s=U.join(e,ie);if(!k.existsSync(s)||(re(e,s),!t?.isAvailable()))return null;let i=k.readFileSync(s),a=t.decrypt(i);return a?{sessionCode:o.sessionCode,agentToken:a}:null}if(hr(o))return{sessionCode:o.sessionCode,agentToken:o.agentToken}}catch{}return null}function _r(e,t,r){if(r?.isAvailable()){let n=r.encrypt(t.agentToken);ne(e,se,JSON.stringify({sessionCode:t.sessionCode,tokenProtected:!0},null,2)),_t(e,ie,n);return}ne(e,se,JSON.stringify({sessionCode:t.sessionCode,agentToken:t.agentToken},null,2));try{k.rmSync(U.join(e,ie),{force:!0})}catch{}}function vt(e,t,r){let n=ae(t),o;for(let s of n)try{r(s);return}catch(i){o=i}Ne()&&wt(e,o??new Error("no writable session directory"))}function ae(e){return e?[e]:[gr,Sr]}function At(e){let{configDir:t,tokenVault:r}=X(e);for(let n of ae(t)){let o=yr(n,r);if(o)return o}return null}function Me(e,t){let{configDir:r,tokenVault:n}=X(t);vt("session credentials",r,o=>{_r(o,e,n)})}function Pe(e){let{configDir:t}=X(e);for(let r of ae(t))for(let n of[se,ie])try{k.rmSync(U.join(r,n),{force:!0})}catch{}}function Tt(e){let{configDir:t}=X(e);vt("session rotate marker",t,r=>{ne(r,bt,"")})}function xt(e){let{configDir:t}=X(e),r=!1;for(let n of ae(t)){let o=U.join(n,bt);try{k.existsSync(o)&&(r=!0,re(n,o),k.rmSync(o,{force:!0}))}catch{}}return r}function wr(e,t){if(!e)return!0;let r=t-e.lastNotifiedAt;if(r>=ze)return!0;let n=t-e.lastActivityAt>Ze,o=r>=Ye;return n&&o}var ce=class{seen=new Map;note(t,r){let n=this.seen.get(t),o=wr(n,r);return this.seen.set(t,{lastActivityAt:r,lastNotifiedAt:o?r:n?.lastNotifiedAt??0}),o}};var Le=class extends br{serverUrl;configDir;tokenVault;screenShare;abortController=null;_status="disabled";_code=null;_active=!1;_lastHeartbeatAt=0;_remote=new ce;constructor(t,r){super(),this.serverUrl=dt(t),this.configDir=r?.configDir,this.tokenVault=r?.tokenVault,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}sessionStoreCtx(){return{configDir:this.configDir,tokenVault:this.tokenVault}}start(){this.abortController||(this._setStatus("connecting"),this.abortController=new AbortController,this._runLoop(this.abortController))}async _runLoop(t){let r=1e3,n=3e4,o=j(this.configDir),s=!1,i=xt(this.sessionStoreCtx());for(;!t.signal.aborted;){this._setStatus("connecting");try{let a=i?null:At(this.sessionStoreCtx()),{sessionCode:u,agentToken:d}=await Z(this.serverUrl,o,{forceNew:i,...a?{currentCode:a.sessionCode}:{}});if(t.signal.aborted)break;Me({sessionCode:u,agentToken:d},this.sessionStoreCtx()),i=!1,this._code=u;try{this.emit("code",u)}catch{}await xe({serverUrl:this.serverUrl,sessionCode:u,agentToken:d,signal:t.signal,silent:!0,...this.screenShare?{screenShare:this.screenShare}:{},onStatus:c=>this._setStatus(c),onActivity:c=>this._setActive(c),onRemoteActivity:c=>this._noteRemote(c),onHeartbeat:()=>this._onHeartbeat(),reauthIntervalMs:ft,reauth:async()=>{let c=await Z(this.serverUrl,o,{currentCode:u});if(t.signal.aborted||this.abortController!==t)throw new DOMException("Aborted","AbortError");return Me({sessionCode:c.sessionCode,agentToken:c.agentToken},this.sessionStoreCtx()),c.agentToken}});break}catch(a){if(t.signal.aborted)break;if(a instanceof P&&a.status===403&&a.code===Fe&&!s){s=!0,console.error(`[AIC] Relay rejected this device: ${a.message}. Regenerating device identity and retrying as a new device.`),o=pt(this.configDir),Pe(this.sessionStoreCtx()),i=!1;continue}a instanceof P&&console.error(`[AIC] ${a.message}`),this._setStatus("disconnected");try{await Te(r,t.signal)}catch{break}r=Math.min(r*2,n)}}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(){Pe(this.sessionStoreCtx()),Tt(this.sessionStoreCtx()),await this.restart()}async listAdmins(){let t=j(this.configDir);return Ie(this.serverUrl,t)}async blockAdmin(t){let r=j(this.configDir);return De(this.serverUrl,r,t)}async unblockAdmin(t){let r=j(this.configDir);return ke(this.serverUrl,r,t)}_setStatus(t){if(t!=="connected"&&this._active&&this._setActive(!1),this._status!==t){this._status=t;try{this.emit("status",t)}catch{}}}_noteRemote(t){let r=Date.now();if(this._remote.note(t.id,r))try{this.emit("remote-connect",{id:t.id,anonymous:t.anonymous})}catch{}}_onHeartbeat(){this._lastHeartbeatAt=Date.now();try{this.emit("heartbeat",this._lastHeartbeatAt)}catch{}}_setActive(t){if(this._active!==t){this._active=t;try{this.emit("activity",t)}catch{}}}};import{spawn as vr}from"child_process";import{promises as $e}from"fs";import Ar from"os";import Tr from"path";import xr from"crypto";function Cr(){return process.platform==="darwin"||process.platform==="win32"}async function Rr(){if(process.platform==="darwin")return Ir();if(process.platform==="win32")return Dr();throw new Error("Screen capture is only supported on the macOS and Windows desktop apps.")}function Ct(e){return Tr.join(Ar.tmpdir(),`aic-shot-${xr.randomUUID()}.${e}`)}async function Ir(){let e=Ct("png");try{return await Dt("screencapture",["-x","-t","png",e]),await Rt(e,"image/png")}finally{await It(e)}}async function Dr(){let e=Ct("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 Dt("powershell",["-NoProfile","-NonInteractive","-Command",r]),await Rt(e,"image/png")}finally{await It(e)}}async function Rt(e,t){let r=await $e.stat(e);if(r.size>L){let o=(r.size/1048576).toFixed(1);throw new Error(`Screenshot is ${o} MB, which exceeds the ${L/(1024*1024)} MB limit.`)}return{data:await $e.readFile(e),mimeType:t}}async function It(e){try{await $e.unlink(e)}catch{}}function Dt(e,t){return new Promise((r,n)=>{let o=vr(e,t,{stdio:["ignore","ignore","pipe"]}),s="";o.stderr?.on("data",i=>{s+=i.toString()}),o.on("error",i=>n(i)),o.on("close",i=>{i===0?r():n(new Error(`${e} exited with code ${i}${s?`: ${s.trim()}`:""}`))})})}import{spawn as Nr}from"child_process";import le from"fs";import kt from"path";import kr from"os";function He(e){if(e)return kt.join(e,"heartbeat");if(process.platform==="linux")try{return le.mkdirSync("/run/aicommander-agent",{recursive:!0}),"/run/aicommander-agent/heartbeat"}catch{}return kt.join(kr.tmpdir(),"aicommander-agent-heartbeat")}function Ot(e){try{let t=`${e}.tmp`;le.writeFileSync(t,String(Date.now())),le.renameSync(t,e)}catch{}}var K=null;function Or(e,t=We){Nt(),Ot(e),K=setInterval(()=>Ot(e),t),K.unref?.()}function Nt(){K&&(clearInterval(K),K=null)}function Ue(e){try{let t=le.readFileSync(e,"utf8").trim(),r=Number(t);return Number.isFinite(r)?r:null}catch{return null}}async function Mr(e={}){let t=e.heartbeatPath??He(e.configDir),r=e.spawnWorker??(c=>Nr(process.execPath,process.argv.slice(1),{env:c,stdio:"inherit"})),n=null,o=0,s=!1,i=0,a=[],u=null,d=null;await new Promise(c=>{let w=null,l=null,m=()=>{u&&(clearInterval(u),u=null),d&&(clearTimeout(d),d=null),w&&(process.removeListener("SIGINT",w),w=null),l&&(process.removeListener("SIGTERM",l),l=null)},p=()=>{m(),c()},b=()=>{o=Date.now(),i++;let T={...process.env,AIC_ROLE:"worker",AIC_HEARTBEAT:t};n=r(T),n.on("exit",(g,V)=>{if(n=null,s){p();return}if(e.maxSpawns!=null&&i>=e.maxSpawns){p();return}O(`worker exited (code=${g??"null"} signal=${V??"null"})`)}),n.on("error",()=>{})},O=T=>{let g=Date.now();a=a.filter(ue=>g-ue<Ke),a.push(g);let V=a.length>Xe?Je:0;d&&clearTimeout(d),d=setTimeout(()=>{d=null,s||b()},V),d.unref?.()},f=()=>{if(n)try{n.kill("SIGKILL")}catch{}};u=setInterval(()=>{if(s||!n||Date.now()-o<Ee)return;let T=Ue(t);T!=null&&Date.now()-T>Ee&&f()},je),u.unref?.();let A=T=>{if(s=!0,u&&(clearInterval(u),u=null),d&&(clearTimeout(d),d=null),n)try{n.kill(T)}catch{}else p()};w=()=>A("SIGINT"),l=()=>A("SIGTERM"),process.on("SIGINT",w),process.on("SIGTERM",l),b()})}function Pr(e,t){return ye(e,t)}function Lr(e,t){return Qe(e,t)}async function $r(e,t=1e4){let r=new AbortController,n=setTimeout(()=>r.abort(),t);try{let o=await fetch(`${e.replace(/\/+$/,"")}/dist/latest`,{signal:r.signal,headers:{Accept:"application/json"}});if(!o.ok)return null;let s=await o.json();return{version:typeof s.version=="string"?s.version:null,mac:typeof s.mac=="string"?s.mac:null,macPkg:typeof s.macPkg=="string"?s.macPkg:null,win:typeof s.win=="string"?s.win:null,agentLinuxX64:typeof s.agentLinuxX64=="string"?s.agentLinuxX64:null,agentLinuxArm64:typeof s.agentLinuxArm64=="string"?s.agentLinuxArm64:null}}catch{return null}finally{clearTimeout(n)}}export{F as AGENT_VERSION,Le as AgentController,oe as CredentialStorageError,De as blockAdmin,Cr as canCaptureScreenshot,Rr as captureScreenshot,Pr as compareVersions,He as defaultHeartbeatPath,Ie as fetchAdmins,$r as fetchLatestDist,Lr as isNewerVersion,Ne as isStrictCredentialStorage,dr as orderAdmins,Ue as readHeartbeat,Z as register,fr as resolveAdminIdentifier,xe as runConnectionLoop,Mr as runSupervisor,Or as startHeartbeat,Nt as stopHeartbeat,ke as unblockAdmin};
@@ -1,5 +1,6 @@
1
1
  import EventEmitter from "node:events";
2
2
  import type { AdminsResult, BlockResult } from "./device-admin.js";
3
+ import { type TokenVault } from "./session-store.js";
3
4
  import type { ScreenShareState } from "@aicommander/protocol";
4
5
  import type { CapturedScreenshot } from "./screenshot.js";
5
6
  export type AgentStatus = "connecting" | "connected" | "disconnected" | "disabled";
@@ -35,6 +36,8 @@ export interface AgentControllerOptions {
35
36
  * (/etc with ~/.config fallback) — the Linux CLI behavior.
36
37
  */
37
38
  configDir?: string;
39
+ /** Desktop-only OS-protected storage for the reusable agent token. */
40
+ tokenVault?: TokenVault;
38
41
  /**
39
42
  * Desktop-only screen-share provider. When supplied, the agent advertises
40
43
  * screen-share capability/state and can answer screenshot requests.
@@ -44,6 +47,7 @@ export interface AgentControllerOptions {
44
47
  export declare class AgentController extends EventEmitter {
45
48
  private serverUrl;
46
49
  private configDir?;
50
+ private tokenVault?;
47
51
  private screenShare?;
48
52
  private abortController;
49
53
  private _status;
@@ -58,6 +62,7 @@ export declare class AgentController extends EventEmitter {
58
62
  get active(): boolean;
59
63
  /** Epoch-ms of the last proof the link is alive (open or server ping), or 0. */
60
64
  get lastHeartbeatAt(): number;
65
+ private sessionStoreCtx;
61
66
  start(): void;
62
67
  private _runLoop;
63
68
  stop(): void;
@@ -0,0 +1,13 @@
1
+ export declare const PRIVATE_DIR_MODE = 448;
2
+ export declare const PRIVATE_FILE_MODE = 384;
3
+ /** Create (or reuse) a directory and reassert owner-only traversal mode. */
4
+ export declare function ensurePrivateDir(dir: string): void;
5
+ /** Best-effort chmod reassert when reading an existing credential file. */
6
+ export declare function reassertPrivateFileModes(dir: string, filePath: string): void;
7
+ /**
8
+ * Write via an exclusive temp file and atomic rename so readers never observe a
9
+ * partial file or a transient wider mode. Cleans up the temp file on failure.
10
+ */
11
+ export declare function atomicWriteFile(dir: string, fileName: string, writeTemp: (tmpPath: string) => void): void;
12
+ export declare function atomicWriteUtf8(dir: string, fileName: string, contents: string): void;
13
+ export declare function atomicWriteBuffer(dir: string, fileName: string, data: Buffer): void;
@@ -0,0 +1,11 @@
1
+ /** Thrown when root-exec credentials cannot be persisted in strict (service) mode. */
2
+ export declare class CredentialStorageError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /**
6
+ * Production/service installs must fail closed when session or runtime state
7
+ * cannot be written with owner-only permissions. Dev and ad-hoc foreground runs
8
+ * keep the previous best-effort behavior.
9
+ */
10
+ export declare function isStrictCredentialStorage(): boolean;
11
+ export declare function enforceCredentialStorageWrite(label: string, err: unknown): never;
@@ -20,5 +20,13 @@
20
20
  export declare function showCode(sessionCode: string, serverUrl: string, reveal?: boolean): void;
21
21
  export declare function showReconnecting(attempt: number, delayMs: number): void;
22
22
  export declare function showRelayConnected(): void;
23
- export declare function showExecuting(command: string): void;
24
- export declare function showDone(exitCode: number, durationMs: number): void;
23
+ export type LocalOperation = "command" | "secure-exec";
24
+ /**
25
+ * Render command lifecycle metadata without rendering relay-controlled payloads.
26
+ *
27
+ * Shell command text is never accepted by this API. Secure exec may include its
28
+ * allowlisted executable basename, but only when it is a short, printable bare
29
+ * name; argv, cwd, env, input, and output are never display inputs.
30
+ */
31
+ export declare function showExecuting(operation: LocalOperation, executableBasename?: string): void;
32
+ export declare function showDone(operation: LocalOperation, exitCode: number, durationMs: number): void;
@@ -12,3 +12,5 @@ export { startHeartbeat, stopHeartbeat, readHeartbeat, defaultHeartbeatPath, } f
12
12
  export { compareVersions, isNewerVersion, fetchLatestDist, } from "./update-check.js";
13
13
  export type { LatestDist } from "./update-check.js";
14
14
  export { AGENT_VERSION } from "./version.js";
15
+ export type { TokenVault, SessionStoreContext } from "./session-store.js";
16
+ export { CredentialStorageError, isStrictCredentialStorage } from "./credential-storage.js";
@@ -2,17 +2,27 @@ export interface StoredSession {
2
2
  sessionCode: string;
3
3
  agentToken: string;
4
4
  }
5
+ /** OS-protected storage for the reusable agent token (desktop safeStorage). */
6
+ export interface TokenVault {
7
+ isAvailable(): boolean;
8
+ encrypt(plaintext: string): Buffer;
9
+ decrypt(ciphertext: Buffer): string;
10
+ }
11
+ export type SessionStoreContext = string | {
12
+ configDir?: string;
13
+ tokenVault?: TokenVault;
14
+ };
5
15
  /** Load the persisted session (primary first), or null if none. */
6
- export declare function loadSession(configDir?: string): StoredSession | null;
16
+ export declare function loadSession(ctx?: SessionStoreContext): StoredSession | null;
7
17
  /** Persist the session to the primary location, falling back to the user dir. */
8
- export declare function saveSession(session: StoredSession, configDir?: string): void;
18
+ export declare function saveSession(session: StoredSession, ctx?: SessionStoreContext): void;
9
19
  /** Remove the persisted session from both locations (best-effort). */
10
- export declare function clearSession(configDir?: string): void;
20
+ export declare function clearSession(ctx?: SessionStoreContext): void;
11
21
  /** Write the one-shot rotate marker (forces a new code on next startup). */
12
- export declare function writeRotateMarker(configDir?: string): void;
22
+ export declare function writeRotateMarker(ctx?: SessionStoreContext): void;
13
23
  /**
14
24
  * If the rotate marker exists, delete it and return true. Used at startup to
15
25
  * decide whether to force a brand-new code (change-code path) vs re-assert the
16
26
  * existing one (reboot/restart path).
17
27
  */
18
- export declare function consumeRotateMarker(configDir?: string): boolean;
28
+ export declare function consumeRotateMarker(ctx?: SessionStoreContext): boolean;
@@ -10,6 +10,13 @@ export interface LatestDist {
10
10
  version: string | null;
11
11
  /** Direct download paths (relative to the server origin), null if missing. */
12
12
  mac: string | null;
13
+ /**
14
+ * The macOS .pkg installer (the primary mac download once published) —
15
+ * null until a pkg release exists, or when talking to an older server.
16
+ * Optional so existing consumers constructing LatestDist keep compiling;
17
+ * `fetchLatestDist` always fills it in (with an explicit null when absent).
18
+ */
19
+ macPkg?: string | null;
13
20
  win: string | null;
14
21
  agentLinuxX64: string | null;
15
22
  agentLinuxArm64: string | null;
@@ -1 +1 @@
1
- export declare const AGENT_VERSION = "1.0.26";
1
+ export declare const AGENT_VERSION = "1.0.30";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicommander/agent",
3
- "version": "1.0.26",
3
+ "version": "1.0.30",
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
+ }
@@ -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,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EACL,UAAU,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,EACb,YAAY,EACZ,aAAa,EACb,aAAa,EACb,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAE7B,kFAAkF;AAClF,gFAAgF;AAChF,sFAAsF;AACtF,IAAI,sBAAsB,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;IACzC,gFAAgF;IAChF,2DAA2D;AAC7D,CAAC;KAAM,CAAC;IAER,OAAO;SACJ,IAAI,CAAC,mBAAmB,CAAC;SACzB,WAAW,CAAC,gDAAgD,CAAC;SAC7D,OAAO,CAAC,aAAa,CAAC,CAAC;IAE1B,kEAAkE;IAClE,OAAO;SACJ,OAAO,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;SACnC,WAAW,CAAC,gEAAgE,CAAC;SAC7E,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,UAAU,EAAE,CAAC;IACrB,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,SAAS,CAAC;SAClB,WAAW,CAAC,gEAAgE,CAAC;SAC7E,MAAM,CAAC,gBAAgB,EAAE,8CAA8C,CAAC;SACxE,MAAM,CAAC,KAAK,EAAE,IAAyB,EAAE,EAAE;QAC1C,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,6CAA6C,CAAC;SAC1D,MAAM,CAAC,UAAU,EAAE,+CAA+C,CAAC;SACnE,MAAM,CAAC,KAAK,EAAE,IAA0B,EAAE,EAAE;QAC3C,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,oCAAoC,CAAC;SACjD,MAAM,CAAC,GAAG,EAAE;QACX,SAAS,EAAE,CAAC;IACd,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,SAAS,CAAC;SAClB,WAAW,CAAC,oCAAoC,CAAC;SACjD,MAAM,CAAC,GAAG,EAAE;QACX,UAAU,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,aAAa,CAAC;SACtB,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;SACvB,WAAW,CAAC,8FAA8F,CAAC;SAC3G,MAAM,CAAC,WAAW,EAAE,8BAA8B,CAAC;SACnD,MAAM,CAAC,KAAK,EAAE,IAAuB,EAAE,EAAE;QACxC,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,aAAa,CAAC;SACtB,WAAW,CAAC,kDAAkD,CAAC;SAC/D,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,aAAa,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,uBAAuB,CAAC;SAChC,OAAO,CAAC,CAAC,cAAc,CAAC,CAAC;SACzB,WAAW,CAAC,2FAA2F,CAAC;SACxG,MAAM,CAAC,KAAK,EAAE,OAAe,EAAE,EAAE;QAChC,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,yBAAyB,CAAC;SAClC,WAAW,CAAC,2DAA2D,CAAC;SACxE,MAAM,CAAC,KAAK,EAAE,OAAe,EAAE,EAAE;QAChC,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,WAAW,CAAC;SACpB,WAAW,CAAC,sDAAsD,CAAC;SACnE,MAAM,CAAC,aAAa,EAAE,+BAA+B,CAAC;SACtD,MAAM,CAAC,CAAC,IAAwB,EAAE,EAAE;QACnC,YAAY,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC,CAAC,CAAC;IAEL,OAAO,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACjC,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;QACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AAEH,CAAC"}
@@ -1 +0,0 @@
1
- export declare function cmdRevokeAdmin(identifier: string): Promise<void>;