clustr-ai 0.1.20 → 0.1.21

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/bin/clustr.js CHANGED
@@ -80,27 +80,45 @@ function start() {
80
80
 
81
81
  const port = process.env.CLUSTR_PORT || '3100';
82
82
 
83
- const server = spawn('node', [path.join(root, 'dist', 'server', 'index.js')], {
84
- cwd: root,
85
- stdio: 'inherit',
86
- env: {
87
- ...process.env,
88
- CLUSTR_PORT: port,
89
- CLUSTR_SERVE_CLIENT: hasClient ? clientDir : '',
90
- },
91
- });
92
-
93
- console.log(`\n Clustr is starting on http://localhost:${port}\n`);
94
-
95
83
  if (!hasClient) {
96
84
  console.log(' (Client not built — run "npm run build:client" for the full UI)');
97
85
  console.log(' API is still available at /api/*\n');
98
86
  }
99
87
 
100
- process.on('SIGINT', () => { server.kill('SIGINT'); process.exit(); });
101
- process.on('SIGTERM', () => { server.kill('SIGTERM'); process.exit(); });
88
+ let shuttingDown = false;
89
+ let restartDelay = 1000;
90
+ let currentServer = null;
91
+
92
+ function spawnServer() {
93
+ currentServer = spawn('node', [path.join(root, 'dist', 'server', 'index.js')], {
94
+ cwd: root,
95
+ stdio: 'inherit',
96
+ env: {
97
+ ...process.env,
98
+ CLUSTR_PORT: port,
99
+ CLUSTR_SERVE_CLIENT: hasClient ? clientDir : '',
100
+ },
101
+ });
102
+
103
+ // Reset restart delay after 30s of stability
104
+ const stabilityTimer = setTimeout(() => { restartDelay = 1000; }, 30000);
105
+
106
+ currentServer.on('exit', (code, signal) => {
107
+ clearTimeout(stabilityTimer);
108
+ if (shuttingDown) { process.exit(code ?? 0); return; }
109
+ if (signal === 'SIGINT' || signal === 'SIGTERM') { process.exit(0); return; }
110
+ console.error(`\n [clustr] Server exited unexpectedly (code: ${code ?? signal}). Restarting in ${restartDelay / 1000}s...\n`);
111
+ setTimeout(() => {
112
+ restartDelay = Math.min(restartDelay * 2, 30000);
113
+ spawnServer();
114
+ }, restartDelay);
115
+ });
116
+ }
102
117
 
103
- server.on('exit', (code) => {
104
- process.exit(code ?? 0);
105
- });
118
+ console.log(`\n Clustr is starting on http://localhost:${port}\n`);
119
+
120
+ spawnServer();
121
+
122
+ process.on('SIGINT', () => { shuttingDown = true; if (currentServer) currentServer.kill('SIGINT'); });
123
+ process.on('SIGTERM', () => { shuttingDown = true; if (currentServer) currentServer.kill('SIGTERM'); });
106
124
  }
@@ -170,7 +170,7 @@ Now execute the following task:
170
170
  agent_id TEXT,
171
171
  created_at TEXT DEFAULT (datetime('now'))
172
172
  )
173
- `).run();try{sr.prepare("ALTER TABLE file_changes ADD COLUMN agent_id TEXT").run()}catch{}var Sq=sr.prepare("INSERT INTO file_changes (file_path, change_type, diff_text, agent_id) VALUES (?, ?, ?, ?)"),kq=sr.prepare("SELECT id FROM agents WHERE agent_cwd IS NOT NULL AND status = 'running' ORDER BY length(agent_cwd) DESC"),Tq=sr.prepare("SELECT * FROM file_changes ORDER BY created_at DESC LIMIT ?"),Cq=sr.prepare("DELETE FROM file_changes");async function Aq(t){for(let e of["master","main"])try{return await ME("git",["rev-parse","--verify",e],{cwd:t}),e}catch{}return"master"}async function Oq(t,e,r){try{let{stdout:n}=await ME("git",["show",`${r}:${e}`],{cwd:t,maxBuffer:5242880});return n}catch{return""}}function zE(t){UE=t}var Rq=new Set([".git","node_modules","dist","build","out",".next",".nuxt",".svelte-kit","target","coverage",".coverage",".cache",".tmp",".temp","tmp","temp","logs","log","__pycache__",".pytest_cache",".mypy_cache",".ruff_cache",".tox",".venv","venv","env",".gradle",".idea",".vscode",".terraform",".vercel",".turbo",".parcel-cache",".yarn",".pnpm-store","vendor","bower_components"]),Pq=new Set(["package-lock.json","yarn.lock","pnpm-lock.yaml","bun.lockb",".DS_Store","Thumbs.db"]),Iq=new Set([".pyc",".log",".swp",".swo"]);function Nq(t,e){let r=Hr.relative(t,e);if(r.startsWith(".."))return!0;let n=r.split(Hr.sep);for(let a of n)if(Rq.has(a))return!0;let i=Hr.basename(r);if(Pq.has(i))return!0;let s=Hr.extname(i);return!!Iq.has(s)}function HE(t){let e=Hr.resolve(t);if(af.has(e))return;for(let n of af.keys())if(e===n||e.startsWith(n+Hr.sep))return;Aq(e).then(n=>{cf.set(e,n)}).catch(()=>{cf.set(e,"master")});let r;try{r=bc.watch(e,{ignored:n=>Nq(e,n),persistent:!0,ignoreInitial:!0,depth:8,awaitWriteFinish:{stabilityThreshold:300,pollInterval:100}})}catch(n){console.error(`[filewatcher] failed to start watcher for ${e}:`,n.message);return}r.on("add",n=>of(n,"add",e)),r.on("change",n=>of(n,"change",e)),r.on("unlink",n=>of(n,"unlink",e)),r.on("error",n=>{let i=n;i?.code==="EMFILE"||i?.code==="ENOSPC"?console.error(`[filewatcher] ${i.code}: too many open files. Raise the FD limit (\`ulimit -n 65536\`) or narrow the agent cwd. Watcher for ${e} is degraded.`):console.error(`[filewatcher] watcher error for ${e}:`,i?.message||i)}),af.set(e,r)}async function of(t,e,r){let n=Hr.relative(r,t),i=cf.get(r)||"master",s=null;try{let p=await Oq(r,n,i);if(e==="unlink")p&&(s=kc(n,p,"",i,"deleted"));else{let u=wq.readFileSync(t,"utf-8");p!==u&&(s=kc(n,p,u,i,"working"))}}catch{return}if(!s)return;let a=null,o=kq.all();for(let p of o){let u=sr.prepare("SELECT agent_cwd FROM agents WHERE id = ?").get(p.id);if(u?.agent_cwd&&t.startsWith(u.agent_cwd)){a=p.id;break}}let l={id:Sq.run(n,e,s,a).lastInsertRowid,file_path:n,change_type:e,diff_text:s,agent_id:a,created_at:new Date().toISOString()};UE?.emit("file:changed",l)}function WE(t=50){return Tq.all(t)}function $E(){Cq.run()}import{execFile as Lq}from"child_process";import{promisify as Bq}from"util";var qq=Bq(Lq);async function Tc(t,e){return qq("gh",t,{cwd:e,maxBuffer:10*1024*1024})}async function GE(t){try{return await Tc(["auth","status"],t),!0}catch{return!1}}async function VE(t){try{let{stdout:e}=await Tc(["repo","view","--json","nameWithOwner,url"],t);return JSON.parse(e)}catch{return null}}var YE="number,title,state,author,headRefName,baseRefName,createdAt,url,body,isDraft,reviewDecision,statusCheckRollup";async function lf(t,e="all"){try{let r=["pr","list","--state",e,"--json",YE,"--limit","50"],{stdout:n}=await Tc(r,t);return JSON.parse(n)}catch{return[]}}var Dq=YE+",reviews,comments";async function KE(t,e){try{let{stdout:r}=await Tc(["pr","view",String(e),"--json",Dq],t);return JSON.parse(r)}catch{return null}}import{execFile as lF}from"child_process";import{promisify as pF}from"util";import x8 from"os";import jq from"crypto";import Ps from"fs";import XE from"path";import Fq from"os";var uf=XE.join(Fq.homedir(),".clustr"),pf=XE.join(uf,"config.json"),Cc=null;function JE(){if(Cc)return Cc;Ps.existsSync(uf)||Ps.mkdirSync(uf,{recursive:!0});let t;if(Ps.existsSync(pf))try{t=JSON.parse(Ps.readFileSync(pf,"utf8"))}catch{t={authToken:""}}else t={authToken:""};return t.authToken||(t.authToken=jq.randomBytes(32).toString("hex"),Ps.writeFileSync(pf,JSON.stringify(t,null,2),{mode:384})),Cc=t,Cc}var o8=Ys(a8(),1);import tF from"os";function rF(){let t=tF.networkInterfaces();for(let e of Object.keys(t))for(let r of t[e]||[])if(r.family==="IPv4"&&!r.internal)return r.address;return"127.0.0.1"}async function c8(t,e,r){let i=`http://${rF()}:${t}`,a=`${r||i}?token=${e}`,o=await o8.default.toString(a,{type:"svg",color:{dark:"#ffffff",light:"#00000000"},margin:1});return{localUrl:i,remoteUrl:r,token:e,qrSvg:o}}import{spawn as nF}from"child_process";import{execFile as iF}from"child_process";import{promisify as sF}from"util";var aF=sF(iF),mr=null,Ws=null,At=null;function l8(){return Ws}async function oF(){try{return await aF("which",["cloudflared"]),!0}catch{return!1}}async function p8(t){return await oF()?new Promise(e=>{At=e,mr=nF("cloudflared",["tunnel","--url",`http://localhost:${t}`],{stdio:["ignore","pipe","pipe"]});let r=n=>{let s=n.toString().match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);s&&At&&(Ws=s[0],At(Ws),At=null)};mr.stdout?.on("data",r),mr.stderr?.on("data",r),mr.on("exit",()=>{Ws=null,mr=null,At&&(At(null),At=null)}),setTimeout(()=>{At&&(At(null),At=null)},15e3)}):null}function rh(){mr&&(mr.kill(),mr=null,Ws=null)}var nh=pF(lF),Gs=parseInt(process.env.CLUSTR_PORT||"3100",10),Wc=JE();process.env.CLUSTR_TUNNEL==="1"&&p8(Gs).then(t=>{console.log(t?`Clustr tunnel active: ${t}`:"Clustr tunnel: cloudflared not found or timed out")});process.on("SIGINT",()=>{rh(),process.exit(0)});process.on("SIGTERM",()=>{rh(),process.exit(0)});process.on("uncaughtException",t=>{if(t?.code==="EMFILE"||t?.code==="ENOSPC"){console.error(`[clustr] ${t.code}: file-descriptor limit reached. Raise with \`ulimit -n 65536\` or narrow agent cwd. Server continues.`);return}console.error("[clustr] uncaught exception:",t)});process.on("unhandledRejection",t=>{console.error("[clustr] unhandled rejection:",t)});var P=(0,Hc.default)();P.use((0,v8.default)());P.use(Hc.default.json({limit:"20mb"}));var u8=process.env.CLUSTR_SERVE_CLIENT,zc=u8==="true"?zt.resolve(zt.dirname(sh(import.meta.url)),"..","client"):u8||"";zc&&P.use(Hc.default.static(zc));var d8=zt.resolve(zt.dirname(sh(import.meta.url)),"connect.html"),f8=zt.resolve(zt.dirname(sh(import.meta.url)),"..","server","connect.html"),h8=$s.existsSync(d8)?d8:$s.existsSync(f8)?f8:null;P.get("/connect",(t,e)=>{h8?e.sendFile(h8):e.status(404).send("Connect page not found")});P.get("/api/pair",async(t,e)=>{if(!(t.socket.localAddress==="127.0.0.1"||t.socket.localAddress==="::1"||t.socket.localAddress==="::ffff:127.0.0.1")){e.status(403).json({error:"Pairing info only accessible from localhost"});return}let n=await c8(Gs,Wc.authToken,l8());e.json(n)});P.use("/api",(t,e,r)=>{if(t.path==="/pair"||t.socket.localAddress==="127.0.0.1"||t.socket.localAddress==="::1"||t.socket.localAddress==="::ffff:127.0.0.1")return r();if((t.query.token||t.headers.authorization?.replace("Bearer ",""))!==Wc.authToken){e.status(401).json({error:"Unauthorized"});return}r()});var y8=cF(P),xe=new T_(y8,{cors:{origin:"*"}});xe.use((t,e)=>{let r=t.handshake.address;if(r==="127.0.0.1"||r==="::1"||r==="::ffff:127.0.0.1")return e();if((t.handshake.auth?.token||t.handshake.query?.token)!==Wc.authToken)return e(new Error("Unauthorized"));e()});I_();PE(xe);LE(xe);fE(xe);zE(xe);kE(xe,()=>{xe.emit("agents:updated",St())});P.post("/api/agents",(t,e)=>{try{let{id:r,name:n,service:i="claude",task:s}=t.body;if(r)He(r,{status:"running"}),xe.emit("agents:updated",St()),e.json({id:r,status:"registered"});else{let a=F_(n,i,s,null);xe.emit("agents:updated",St()),e.json({id:a,status:"registered"})}}catch(r){e.status(400).json({error:r.message})}});P.get("/api/agents",(t,e)=>{e.json(St())});P.post("/api/agents/:id/ping",(t,e)=>{M_(t.params.id),e.json({status:"ok"})});P.delete("/api/agents/:id",(t,e)=>{U_(t.params.id),Qd(t.params.id),xe.emit("agents:updated",St()),e.json({status:"deregistered"})});P.delete("/api/agents/:id/remove",(t,e)=>{Qd(t.params.id),R_(t.params.id),xe.emit("agents:updated",St()),e.json({status:"removed"})});P.post("/api/messages",(t,e)=>{try{let{from:r,to:n,content:i}=t.body;if(n==="all"||n==="*"){let s=NE(r,i);e.json(s)}else{let s=n;if(!Mt(n)){let c=P_(n);c&&(s=c.id)}let o=ef(r,s,i);e.json(o)}}catch(r){e.status(400).json({error:r.message})}});P.get("/api/messages/:agentId",(t,e)=>{e.json(IE(t.params.agentId))});P.get("/api/messages",(t,e)=>{e.json(pc())});P.delete("/api/messages",(t,e)=>{B_(),xe.emit("messages:all",[]),e.json({status:"cleared"})});P.get("/api/context/:key?",(t,e)=>{let r=t.params.key;e.json(BE(r))});P.put("/api/context",(t,e)=>{try{let{key:r,value:n,updatedBy:i}=t.body;qE(r,n,i),e.json({status:"ok"})}catch(r){e.status(400).json({error:r.message})}});P.delete("/api/context/:key",(t,e)=>{try{DE(t.params.key),e.json({status:"removed"})}catch(r){e.status(400).json({error:r.message})}});P.get("/api/crewmd",(t,e)=>{e.json({content:As()})});P.put("/api/crewmd",(t,e)=>{try{let{content:r}=t.body;gE(r),e.json({status:"ok"})}catch(r){e.status(400).json({error:r.message})}});P.get("/api/agents/:id/cost",(t,e)=>{let r=Mt(t.params.id);if(!r){e.status(404).json({error:"Agent not found"});return}e.json({total_tokens:r.total_tokens||0,total_cost:r.total_cost||0})});P.get("/api/agents/:id/diff",async(t,e)=>{let r=Mt(t.params.id);if(!r||!r.checkpoint_hash||!r.agent_cwd){e.json({diff:""});return}let n=await H_(r.agent_cwd,r.checkpoint_hash);e.json({diff:n})});P.post("/api/agents/:id/rollback",async(t,e)=>{let r=Mt(t.params.id);if(!r||!r.checkpoint_hash||!r.agent_cwd){e.status(400).json({error:"No checkpoint available for this agent"});return}let n=await $_(r.agent_cwd,r.checkpoint_hash);n.success?e.json({status:"rolled_back",message:n.message}):e.status(400).json({error:n.message})});P.get("/api/file-changes",(t,e)=>{let r=parseInt(t.query.limit)||50;e.json(WE(r))});P.delete("/api/file-changes",(t,e)=>{$E(),xe.emit("file:changes:cleared"),e.json({status:"cleared"})});function Vs(){return St().find(r=>r.agent_cwd)?.agent_cwd||process.cwd()}P.get("/api/github/status",async(t,e)=>{let r=Vs(),n=await GE(r),i=n?await VE(r):null;e.json({available:n,repo:i})});P.get("/api/github/prs",async(t,e)=>{let r=Vs(),n=t.query.state||"all",i=await lf(r,n);e.json(i)});P.get("/api/github/prs/:number",async(t,e)=>{let r=Vs(),n=parseInt(t.params.number,10);if(isNaN(n)){e.status(400).json({error:"Invalid PR number"});return}let i=await KE(r,n);i?e.json(i):e.status(404).json({error:"PR not found"})});P.get("/api/git/branches",async(t,e)=>{let r=Vs(),n=await Dd(r);e.json(n)});P.post("/api/pick-folder",async(t,e)=>{try{let r=x8.platform(),n="";if(r==="darwin"){let{stdout:i}=await nh("osascript",["-e",'set chosenFolder to choose folder with prompt "Select project directory"',"-e","return POSIX path of chosenFolder"]);n=i.trim().replace(/\/$/,"")}else if(r==="win32"){let{stdout:i}=await nh("powershell",["-NoProfile","-Command","Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description = 'Select project directory'; if ($d.ShowDialog() -eq 'OK') { $d.SelectedPath } else { '' }"]);n=i.trim()}else{let{stdout:i}=await nh("zenity",["--file-selection","--directory","--title=Select project directory"]);n=i.trim()}n?e.json({path:n}):e.json({path:null,cancelled:!0})}catch{e.json({path:null,cancelled:!0})}});var ih=zt.join(x8.homedir(),".clustr","uploads");P.post("/api/upload-image",(t,e)=>{try{let{data:r,mimeType:n}=t.body;if(!r||!n){e.status(400).json({error:"Missing data or mimeType"});return}$s.existsSync(ih)||$s.mkdirSync(ih,{recursive:!0});let i=(n.split("/")[1]||"png").replace(/[^a-zA-Z0-9]/g,"")||"png",s=`paste-${Date.now()}.${i}`,a=zt.join(ih,s),o=Buffer.from(r,"base64");$s.writeFileSync(a,o),e.json({path:a,filename:s})}catch(r){e.status(500).json({error:r.message})}});P.post("/api/spawn",(t,e)=>{try{let{name:r,task:n,cwd:i,service:s}=t.body,a=i||process.cwd();HE(a);let o=TE(r,n,{cwd:i,service:s});xe.emit("agents:updated",St()),e.json({id:o,status:"spawning"})}catch(r){e.status(400).json({error:r.message})}});P.post("/api/agents/:id/message",(t,e)=>{let{content:r}=t.body;Zd(t.params.id,r+"\r")?e.json({status:"sent"}):e.status(404).json({error:"Agent not running"})});P.get("/api/agents/:id/scrollback",(t,e)=>{e.json({scrollback:OE(t.params.id)})});xe.on("connection",t=>{t.emit("agents:updated",St()),t.emit("messages:all",pc()),t.emit("crewmd:updated",As()),t.on("agent:input",(e,r)=>{Zd(e,r)}),t.on("agent:resize",(e,r,n)=>{AE(e,r,n)})});zc&&P.get("*",(t,e)=>{e.sendFile(zt.join(zc,"index.html"))});var m8="",g8="";setInterval(async()=>{if(xe.engine.clientsCount===0)return;let t=Vs();try{let e=await lf(t,"all"),r=JSON.stringify(e.map(n=>`${n.number}:${n.state}:${n.reviewDecision}`));r!==m8&&(m8=r,xe.emit("github:prs:updated",e))}catch{}try{let e=await Dd(t),r=JSON.stringify(e.branches.map(n=>n.name));r!==g8&&(g8=r,xe.emit("git:branches:updated",e))}catch{}},3e4);y8.listen(Gs,"0.0.0.0",()=>{console.log(`Clustr server running on http://localhost:${Gs}`),console.log(`Connect from phone: http://localhost:${Gs}/connect`),console.log(`Auth token: ${Wc.authToken}`)});
173
+ `).run();try{sr.prepare("ALTER TABLE file_changes ADD COLUMN agent_id TEXT").run()}catch{}var Sq=sr.prepare("INSERT INTO file_changes (file_path, change_type, diff_text, agent_id) VALUES (?, ?, ?, ?)"),kq=sr.prepare("SELECT id FROM agents WHERE agent_cwd IS NOT NULL AND status = 'running' ORDER BY length(agent_cwd) DESC"),Tq=sr.prepare("SELECT * FROM file_changes ORDER BY created_at DESC LIMIT ?"),Cq=sr.prepare("DELETE FROM file_changes");async function Aq(t){for(let e of["master","main"])try{return await ME("git",["rev-parse","--verify",e],{cwd:t}),e}catch{}return"master"}async function Oq(t,e,r){try{let{stdout:n}=await ME("git",["show",`${r}:${e}`],{cwd:t,maxBuffer:5242880});return n}catch{return""}}function zE(t){UE=t}var Rq=new Set([".git","node_modules","dist","build","out",".next",".nuxt",".svelte-kit","target","coverage",".coverage",".cache",".tmp",".temp","tmp","temp","logs","log","__pycache__",".pytest_cache",".mypy_cache",".ruff_cache",".tox",".venv","venv","env",".gradle",".idea",".vscode",".terraform",".vercel",".turbo",".parcel-cache",".yarn",".pnpm-store","vendor","bower_components"]),Pq=new Set(["package-lock.json","yarn.lock","pnpm-lock.yaml","bun.lockb",".DS_Store","Thumbs.db"]),Iq=new Set([".pyc",".log",".swp",".swo"]);function Nq(t,e){let r=Hr.relative(t,e);if(r.startsWith(".."))return!0;let n=r.split(Hr.sep);for(let a of n)if(Rq.has(a))return!0;let i=Hr.basename(r);if(Pq.has(i))return!0;let s=Hr.extname(i);return!!Iq.has(s)}function HE(t){let e=Hr.resolve(t);if(af.has(e))return;for(let n of af.keys())if(e===n||e.startsWith(n+Hr.sep))return;Aq(e).then(n=>{cf.set(e,n)}).catch(()=>{cf.set(e,"master")});let r;try{r=bc.watch(e,{ignored:n=>Nq(e,n),persistent:!0,ignoreInitial:!0,depth:8,awaitWriteFinish:{stabilityThreshold:300,pollInterval:100}})}catch(n){console.error(`[filewatcher] failed to start watcher for ${e}:`,n.message);return}r.on("add",n=>of(n,"add",e)),r.on("change",n=>of(n,"change",e)),r.on("unlink",n=>of(n,"unlink",e)),r.on("error",n=>{let i=n;i?.code==="EMFILE"||i?.code==="ENOSPC"?console.error(`[filewatcher] ${i.code}: too many open files. Raise the FD limit (\`ulimit -n 65536\`) or narrow the agent cwd. Watcher for ${e} is degraded.`):console.error(`[filewatcher] watcher error for ${e}:`,i?.message||i)}),af.set(e,r)}async function of(t,e,r){let n=Hr.relative(r,t),i=cf.get(r)||"master",s=null;try{let p=await Oq(r,n,i);if(e==="unlink")p&&(s=kc(n,p,"",i,"deleted"));else{let u=wq.readFileSync(t,"utf-8");p!==u&&(s=kc(n,p,u,i,"working"))}}catch{return}if(!s)return;let a=null,o=kq.all();for(let p of o){let u=sr.prepare("SELECT agent_cwd FROM agents WHERE id = ?").get(p.id);if(u?.agent_cwd&&t.startsWith(u.agent_cwd)){a=p.id;break}}let l={id:Sq.run(n,e,s,a).lastInsertRowid,file_path:n,change_type:e,diff_text:s,agent_id:a,created_at:new Date().toISOString()};UE?.emit("file:changed",l)}function WE(t=50){return Tq.all(t)}function $E(){Cq.run()}import{execFile as Lq}from"child_process";import{promisify as Bq}from"util";var qq=Bq(Lq);async function Tc(t,e){return qq("gh",t,{cwd:e,maxBuffer:10*1024*1024})}async function GE(t){try{return await Tc(["auth","status"],t),!0}catch{return!1}}async function VE(t){try{let{stdout:e}=await Tc(["repo","view","--json","nameWithOwner,url"],t);return JSON.parse(e)}catch{return null}}var YE="number,title,state,author,headRefName,baseRefName,createdAt,url,body,isDraft,reviewDecision,statusCheckRollup";async function lf(t,e="all"){try{let r=["pr","list","--state",e,"--json",YE,"--limit","50"],{stdout:n}=await Tc(r,t);return JSON.parse(n)}catch{return[]}}var Dq=YE+",reviews,comments";async function KE(t,e){try{let{stdout:r}=await Tc(["pr","view",String(e),"--json",Dq],t);return JSON.parse(r)}catch{return null}}import{execFile as lF}from"child_process";import{promisify as pF}from"util";import x8 from"os";import jq from"crypto";import Ps from"fs";import XE from"path";import Fq from"os";var uf=XE.join(Fq.homedir(),".clustr"),pf=XE.join(uf,"config.json"),Cc=null;function JE(){if(Cc)return Cc;Ps.existsSync(uf)||Ps.mkdirSync(uf,{recursive:!0});let t;if(Ps.existsSync(pf))try{t=JSON.parse(Ps.readFileSync(pf,"utf8"))}catch{t={authToken:""}}else t={authToken:""};return t.authToken||(t.authToken=jq.randomBytes(32).toString("hex"),Ps.writeFileSync(pf,JSON.stringify(t,null,2),{mode:384})),Cc=t,Cc}var o8=Ys(a8(),1);import tF from"os";function rF(){let t=tF.networkInterfaces();for(let e of Object.keys(t))for(let r of t[e]||[])if(r.family==="IPv4"&&!r.internal)return r.address;return"127.0.0.1"}async function c8(t,e,r){let i=`http://${rF()}:${t}`,a=`${r||i}?token=${e}`,o=await o8.default.toString(a,{type:"svg",color:{dark:"#ffffff",light:"#00000000"},margin:1});return{localUrl:i,remoteUrl:r,token:e,qrSvg:o}}import{spawn as nF}from"child_process";import{execFile as iF}from"child_process";import{promisify as sF}from"util";var aF=sF(iF),mr=null,Ws=null,At=null;function l8(){return Ws}async function oF(){try{return await aF("which",["cloudflared"]),!0}catch{return!1}}async function p8(t){return await oF()?new Promise(e=>{At=e,mr=nF("cloudflared",["tunnel","--url",`http://localhost:${t}`],{stdio:["ignore","pipe","pipe"]});let r=n=>{let s=n.toString().match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);s&&At&&(Ws=s[0],At(Ws),At=null)};mr.stdout?.on("data",r),mr.stderr?.on("data",r),mr.on("exit",()=>{Ws=null,mr=null,At&&(At(null),At=null)}),setTimeout(()=>{At&&(At(null),At=null)},15e3)}):null}function rh(){mr&&(mr.kill(),mr=null,Ws=null)}var nh=pF(lF),Gs=parseInt(process.env.CLUSTR_PORT||"3100",10),Wc=JE();process.env.CLUSTR_TUNNEL==="1"&&p8(Gs).then(t=>{console.log(t?`Clustr tunnel active: ${t}`:"Clustr tunnel: cloudflared not found or timed out")});process.on("SIGINT",()=>{rh(),process.exit(0)});process.on("SIGTERM",()=>{rh(),process.exit(0)});process.on("uncaughtException",t=>{t?.code==="EMFILE"||t?.code==="ENOSPC"?console.error(`[clustr] ${t.code}: file-descriptor limit reached. Raise with \`ulimit -n 65536\` or narrow agent cwd. Server continues.`):console.error("[clustr] uncaught exception (server continues):",t)});process.on("unhandledRejection",t=>{console.error("[clustr] unhandled rejection (server continues):",t)});var P=(0,Hc.default)();P.use((0,v8.default)());P.use(Hc.default.json({limit:"20mb"}));var u8=process.env.CLUSTR_SERVE_CLIENT,zc=u8==="true"?zt.resolve(zt.dirname(sh(import.meta.url)),"..","client"):u8||"";zc&&P.use(Hc.default.static(zc));var d8=zt.resolve(zt.dirname(sh(import.meta.url)),"connect.html"),f8=zt.resolve(zt.dirname(sh(import.meta.url)),"..","server","connect.html"),h8=$s.existsSync(d8)?d8:$s.existsSync(f8)?f8:null;P.get("/connect",(t,e)=>{h8?e.sendFile(h8):e.status(404).send("Connect page not found")});P.get("/api/pair",async(t,e)=>{if(!(t.socket.localAddress==="127.0.0.1"||t.socket.localAddress==="::1"||t.socket.localAddress==="::ffff:127.0.0.1")){e.status(403).json({error:"Pairing info only accessible from localhost"});return}let n=await c8(Gs,Wc.authToken,l8());e.json(n)});P.use("/api",(t,e,r)=>{if(t.path==="/pair"||t.socket.localAddress==="127.0.0.1"||t.socket.localAddress==="::1"||t.socket.localAddress==="::ffff:127.0.0.1")return r();if((t.query.token||t.headers.authorization?.replace("Bearer ",""))!==Wc.authToken){e.status(401).json({error:"Unauthorized"});return}r()});var y8=cF(P),xe=new T_(y8,{cors:{origin:"*"}});xe.use((t,e)=>{let r=t.handshake.address;if(r==="127.0.0.1"||r==="::1"||r==="::ffff:127.0.0.1")return e();if((t.handshake.auth?.token||t.handshake.query?.token)!==Wc.authToken)return e(new Error("Unauthorized"));e()});I_();PE(xe);LE(xe);fE(xe);zE(xe);kE(xe,()=>{xe.emit("agents:updated",St())});P.post("/api/agents",(t,e)=>{try{let{id:r,name:n,service:i="claude",task:s}=t.body;if(r)He(r,{status:"running"}),xe.emit("agents:updated",St()),e.json({id:r,status:"registered"});else{let a=F_(n,i,s,null);xe.emit("agents:updated",St()),e.json({id:a,status:"registered"})}}catch(r){e.status(400).json({error:r.message})}});P.get("/api/agents",(t,e)=>{e.json(St())});P.post("/api/agents/:id/ping",(t,e)=>{M_(t.params.id),e.json({status:"ok"})});P.delete("/api/agents/:id",(t,e)=>{U_(t.params.id),Qd(t.params.id),xe.emit("agents:updated",St()),e.json({status:"deregistered"})});P.delete("/api/agents/:id/remove",(t,e)=>{Qd(t.params.id),R_(t.params.id),xe.emit("agents:updated",St()),e.json({status:"removed"})});P.post("/api/messages",(t,e)=>{try{let{from:r,to:n,content:i}=t.body;if(n==="all"||n==="*"){let s=NE(r,i);e.json(s)}else{let s=n;if(!Mt(n)){let c=P_(n);c&&(s=c.id)}let o=ef(r,s,i);e.json(o)}}catch(r){e.status(400).json({error:r.message})}});P.get("/api/messages/:agentId",(t,e)=>{e.json(IE(t.params.agentId))});P.get("/api/messages",(t,e)=>{e.json(pc())});P.delete("/api/messages",(t,e)=>{B_(),xe.emit("messages:all",[]),e.json({status:"cleared"})});P.get("/api/context/:key?",(t,e)=>{let r=t.params.key;e.json(BE(r))});P.put("/api/context",(t,e)=>{try{let{key:r,value:n,updatedBy:i}=t.body;qE(r,n,i),e.json({status:"ok"})}catch(r){e.status(400).json({error:r.message})}});P.delete("/api/context/:key",(t,e)=>{try{DE(t.params.key),e.json({status:"removed"})}catch(r){e.status(400).json({error:r.message})}});P.get("/api/crewmd",(t,e)=>{e.json({content:As()})});P.put("/api/crewmd",(t,e)=>{try{let{content:r}=t.body;gE(r),e.json({status:"ok"})}catch(r){e.status(400).json({error:r.message})}});P.get("/api/agents/:id/cost",(t,e)=>{let r=Mt(t.params.id);if(!r){e.status(404).json({error:"Agent not found"});return}e.json({total_tokens:r.total_tokens||0,total_cost:r.total_cost||0})});P.get("/api/agents/:id/diff",async(t,e)=>{let r=Mt(t.params.id);if(!r||!r.checkpoint_hash||!r.agent_cwd){e.json({diff:""});return}let n=await H_(r.agent_cwd,r.checkpoint_hash);e.json({diff:n})});P.post("/api/agents/:id/rollback",async(t,e)=>{let r=Mt(t.params.id);if(!r||!r.checkpoint_hash||!r.agent_cwd){e.status(400).json({error:"No checkpoint available for this agent"});return}let n=await $_(r.agent_cwd,r.checkpoint_hash);n.success?e.json({status:"rolled_back",message:n.message}):e.status(400).json({error:n.message})});P.get("/api/file-changes",(t,e)=>{let r=parseInt(t.query.limit)||50;e.json(WE(r))});P.delete("/api/file-changes",(t,e)=>{$E(),xe.emit("file:changes:cleared"),e.json({status:"cleared"})});function Vs(){return St().find(r=>r.agent_cwd)?.agent_cwd||process.cwd()}P.get("/api/github/status",async(t,e)=>{let r=Vs(),n=await GE(r),i=n?await VE(r):null;e.json({available:n,repo:i})});P.get("/api/github/prs",async(t,e)=>{let r=Vs(),n=t.query.state||"all",i=await lf(r,n);e.json(i)});P.get("/api/github/prs/:number",async(t,e)=>{let r=Vs(),n=parseInt(t.params.number,10);if(isNaN(n)){e.status(400).json({error:"Invalid PR number"});return}let i=await KE(r,n);i?e.json(i):e.status(404).json({error:"PR not found"})});P.get("/api/git/branches",async(t,e)=>{let r=Vs(),n=await Dd(r);e.json(n)});P.post("/api/pick-folder",async(t,e)=>{try{let r=x8.platform(),n="";if(r==="darwin"){let{stdout:i}=await nh("osascript",["-e",'set chosenFolder to choose folder with prompt "Select project directory"',"-e","return POSIX path of chosenFolder"]);n=i.trim().replace(/\/$/,"")}else if(r==="win32"){let{stdout:i}=await nh("powershell",["-NoProfile","-Command","Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description = 'Select project directory'; if ($d.ShowDialog() -eq 'OK') { $d.SelectedPath } else { '' }"]);n=i.trim()}else{let{stdout:i}=await nh("zenity",["--file-selection","--directory","--title=Select project directory"]);n=i.trim()}n?e.json({path:n}):e.json({path:null,cancelled:!0})}catch{e.json({path:null,cancelled:!0})}});var ih=zt.join(x8.homedir(),".clustr","uploads");P.post("/api/upload-image",(t,e)=>{try{let{data:r,mimeType:n}=t.body;if(!r||!n){e.status(400).json({error:"Missing data or mimeType"});return}$s.existsSync(ih)||$s.mkdirSync(ih,{recursive:!0});let i=(n.split("/")[1]||"png").replace(/[^a-zA-Z0-9]/g,"")||"png",s=`paste-${Date.now()}.${i}`,a=zt.join(ih,s),o=Buffer.from(r,"base64");$s.writeFileSync(a,o),e.json({path:a,filename:s})}catch(r){e.status(500).json({error:r.message})}});P.post("/api/spawn",(t,e)=>{try{let{name:r,task:n,cwd:i,service:s}=t.body,a=i||process.cwd();HE(a);let o=TE(r,n,{cwd:i,service:s});xe.emit("agents:updated",St()),e.json({id:o,status:"spawning"})}catch(r){e.status(400).json({error:r.message})}});P.post("/api/agents/:id/message",(t,e)=>{let{content:r}=t.body;Zd(t.params.id,r+"\r")?e.json({status:"sent"}):e.status(404).json({error:"Agent not running"})});P.get("/api/agents/:id/scrollback",(t,e)=>{e.json({scrollback:OE(t.params.id)})});xe.on("connection",t=>{t.emit("agents:updated",St()),t.emit("messages:all",pc()),t.emit("crewmd:updated",As()),t.on("agent:input",(e,r)=>{Zd(e,r)}),t.on("agent:resize",(e,r,n)=>{AE(e,r,n)})});zc&&P.get("*",(t,e)=>{e.sendFile(zt.join(zc,"index.html"))});var m8="",g8="";setInterval(async()=>{if(xe.engine.clientsCount===0)return;let t=Vs();try{let e=await lf(t,"all"),r=JSON.stringify(e.map(n=>`${n.number}:${n.state}:${n.reviewDecision}`));r!==m8&&(m8=r,xe.emit("github:prs:updated",e))}catch{}try{let e=await Dd(t),r=JSON.stringify(e.branches.map(n=>n.name));r!==g8&&(g8=r,xe.emit("git:branches:updated",e))}catch{}},3e4);y8.listen(Gs,"0.0.0.0",()=>{console.log(`Clustr server running on http://localhost:${Gs}`),console.log(`Connect from phone: http://localhost:${Gs}/connect`),console.log(`Auth token: ${Wc.authToken}`)});
174
174
  /*! Bundled license information:
175
175
 
176
176
  depd/index.js:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clustr-ai",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Multi-agent workspace for AI coding sessions",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Clustr",