@nimbus21.ai/the-link 3.14.0 → 3.15.0
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/the-link.js +2 -2
- package/package.json +1 -1
package/bin/the-link.js
CHANGED
|
@@ -469,7 +469,7 @@ ${W}`:X||W}function bO(Z,{maxDepth:J=1000,numbersAsFloat:Q=!1}={}){if(iY(Z)!=="o
|
|
|
469
469
|
`),w=!1;for(let m of U){if(m.trim().startsWith("--------")){if(w)break;w=!0;continue}if(!w)continue;let l=m.trim().split(/\s+/).slice(3).join(" ");if(!l)continue;if(l.includes("../")||l.includes("..\\"))return{success:!1,error:`ZIP contains unsafe path (traversal): ${l}`};if(l.startsWith("/"))return{success:!1,error:`ZIP contains unsafe path (absolute): ${l}`};if(l.split(/[/\\]/).some((W6)=>W6===".."))return{success:!1,error:`ZIP contains unsafe path component (..): ${l}`}}await cO(K,{recursive:!0});let D=await Bun.$`unzip -o ${z} -d ${K} -x "__MACOSX/*"`.quiet().nothrow();if(D.exitCode!==0)return{success:!1,error:`ZIP extraction failed: ${D.stderr.toString()}`};let C=F.stdout.toString().split(`
|
|
470
470
|
`).map((m)=>m.trim().split(/\s+/).pop()||"").find((m)=>m.endsWith("/")&&!m.includes("__MACOSX")&&m.split("/").length===2),R=C?c9(K,C.replace(/\/$/,"")):K,T=await Bun.file(c9(R,"SKILL.md")).exists(),_=T?void 0:"No SKILL.md found in archive root. Skill may not be auto-detected by the coding agent.";return BZ.info(`Skill '${X}' extracted to ${K} (folder: ${C||"flat"}, SKILL.md: ${T})`),{success:!0,warning:_,filesWritten:[R||K]}}catch(G){return{success:!1,error:G.message}}finally{try{await Bun.$`rm -f ${z}`.quiet().nothrow()}catch{}}}async function _p0(Z,J){let{hubUrl:Q,hubToken:$,extensionId:Y,name:X,codingAgent:W}=Z;if(!Q||!$)return{success:!1,error:"Missing hubUrl or hubToken for file fetch"};let K=Ep0(W,J,X);try{let z=`${Q.replace(/^ws/,"http")}/api/extensions/${Y}/file`;BZ.info(`Fetching agent template from ${z}`);let G=await fetch(z,{headers:{Authorization:`Bearer ${$}`}});if(!G.ok)return{success:!1,error:`Failed to download agent template: HTTP ${G.status}`};let H=await G.text();await cO(h40(K),{recursive:!0});let V=await Bun.file(K).exists()?`Overwriting existing agent '${X}'`:void 0;return await dO(K,H),BZ.info(`Agent template '${X}' installed to ${K}`),{success:!0,warning:V,filesWritten:[K]}}catch(z){return{success:!1,error:z.message}}}async function hp0(Z,J,Q){let $={extensionId:Z.extensionId,extensionType:Z.extensionType,name:Z.name,config:Z.config,codingAgent:Z.codingAgent,hubUrl:Q.hub.url,hubToken:Q.hub.token};if(!$.extensionId||!$.name||!$.extensionType||!$.codingAgent)return{success:!1,error:"Missing required params: extensionId, name, extensionType, codingAgent"};if(!y40.includes($.codingAgent))return{success:!1,error:`Unknown coding agent: ${$.codingAgent}. Supported: ${y40.join(", ")}`};let Y;try{Y=await J?.terminal.getCurrentDir?.()||process.cwd()}catch{Y=process.cwd()}switch(BZ.info(`Installing extension '${$.name}' (${$.extensionType}) for ${$.codingAgent} in ${Y}`),$.extensionType){case"mcp_server":return Sp0($,Y);case"skill":return yp0($,Y);case"agent_template":return _p0($,Y);default:return{success:!1,error:`Unknown extension type: ${$.extensionType}`}}}var BZ,y40,Ap0;var v40=q(()=>{g1();S40();BZ=F0("extension-installer"),y40=["claude","opencode","codex"],Ap0={AGENT_TOKEN:()=>process.env.HUB_TOKEN||"",THE_LINK_TOKEN:()=>process.env.HUB_TOKEN||"",HUB_TOKEN:()=>process.env.HUB_TOKEN||"",HUB_URL:()=>process.env.HUB_URL||"",AGENT_NAME:()=>process.env.AGENT_NAME||""}});var x40={};q1(x40,{Bridge:()=>mO});class mO{config;backend;isShuttingDown=!1;gcInterval=null;spawnTelemetryInterval=null;allowedUserIds=new Set;userCache=new Map;userSessions=new Map;currentUserId=null;lastMessageAt=0;startTime=Date.now();restoredUserDirs={};constructor(){if(this.config=e70(),H50(this.config.debug),this.config.logDir)K50(this.config.logDir);if(console.log("=".repeat(60)),console.log("the-link: Hub 360 Terminal Agent"),console.log("=".repeat(60)),S.info(`Working dir: ${process.cwd()}`),S.info(`Hub URL: ${this.config.hub.url}`),S.info(`Agent name: ${this.config.hub.agentName}`),S.info(`Shell: ${this.config.terminal.shell}`),S.info(`Session home dir: ${this.config.terminal.homeDir}`),this.config.output.rawMode)S.info("Output mode: raw (no ANSI stripping, no markdown)");if(this.config.debug)S.info("Debug mode: enabled");if(this.config.healthPort>0)S.info(`Health check port: ${this.config.healthPort}`);if(this.config.files.downloadDir)S.info(`File download dir: ${this.config.files.downloadDir}`),S.info(`Auto-download: ${this.config.files.autoDownload?"enabled":"disabled"}`);if(this.config.auth.allowedUsers.length>0)S.info(`Allowed users: ${this.config.auth.allowedUsers.join(", ")}`);else S.info("Allowed users: (all - no restrictions)");this.backend=p50(this.config,{onMessage:(Z,J,Q,$,Y)=>{this.handleIncomingMessage(Z,J,Q,$,Y).catch((X)=>{S.error(`Failed to handle message from ${Q}: ${X}`)})},onResize:(Z,J)=>{this.handleResize(Z,J).catch((Q)=>{S.error(`Failed to handle resize: ${Q}`)})},onCommand:(Z,J)=>{this.handleControlAction(Z,J).catch((Q)=>{S.error(`Failed to handle command ${Z}: ${Q}`)})},onProxyRequest:(Z)=>{this.handleProxyRequest(Z).catch((J)=>{S.error(`Failed to handle proxy request ${Z.requestId}: ${J}`)})},onConnected:()=>{S.info("Hub 360 connected"),this.initializeHub360Session().catch((Z)=>{S.error(`Failed to initialize Hub 360 session: ${Z}`)})},onDisconnected:()=>{S.info("Hub 360 disconnected")},onSchedulerReady:(Z)=>{S.info(`scheduler: state=${Z} schedules loaded, decision loop started`)}})}async initializeHub360Session(){let J=this.config.hub.agentName;S.info(`Hub 360: Creating default terminal session for agent "${J}"`);let Q=await this.getOrCreateSession("system",J);if(Q&&!Q.initialized){if(Q.terminal.setStreamMode(!0),Q.terminal.setEmitFirstCapture(!0),await Q.terminal.start(),Q.initialized=!0,S.info("Hub 360: Terminal session started, pipe-pane streaming active"),zQ(this.backend))this.backend.client.setTerminal(Q.terminal),S.info("scheduler: terminal wired to decision loop")}}async handleResize(Z,J){let Q=this.getSession("system");if(Q)S.info(`Resizing terminal to ${Z}x${J}`),await Q.terminal.resize(Z,J)}sendTerminalStatus(Z){if(zQ(this.backend))this.backend.client.sendOutput(`\r
|
|
471
471
|
\x1B[33m${Z}\x1B[0m\r
|
|
472
|
-
`)}async handleControlAction(Z,J){let Q=this.getSession("system");switch(Z){case"kill":if(S.info("Kill command received \u2014 killing terminal session"),Q)await Q.terminal.kill();break;case"restart":{if(S.info("Restart command received"),this.sendTerminalStatus("\u27F3 Restarting terminal..."),Q)await Q.terminal.kill(),this.userSessions.delete("system");await new Promise(($)=>setTimeout($,500)),await this.initializeHub360Session(),this.sendTerminalStatus("\u2713 Terminal restarted");break}case"self-update":{S.info("Self-update command received");let{detectDeploymentMode:$,resolveUpdatePaths:Y,getCurrentSystem:X,applyPlatformFixes:W,getInstallType:K,performNpmUpdate:z}=await Promise.resolve().then(() => (mY(),W40));if(K()==="npm"){S.info("Updating via npm (bun update -g)"),this.sendTerminalStatus("\u27F3 Updating via npm...");try{let{APP_VERSION:U}=await Promise.resolve().then(() => XL),w=await z(U);if(w.success)this.sendTerminalStatus(`\u2713 Updated: v${w.previousVersion} \u2192 v${w.newVersion}`),S.info(`npm update complete: ${w.previousVersion} \u2192 ${w.newVersion}`),await new Promise((D)=>setTimeout(D,200)),process.exit(0);else throw Error(w.error||"npm update failed")}catch(U){S.error(`npm self-update failed: ${U}`),this.sendTerminalStatus(`\u2717 Update failed: ${U}`)}break}let H=$(),B=X(),F=`${this.config.hub.url.replace("ws://","http://").replace("wss://","https://")}/download/${B}`;S.info(`Updating from ${F} (mode: ${H}, system: ${B})`),this.sendTerminalStatus(`\u27F3 Starting update (${B})...`);try{let U=Y(),w=`/tmp/the-link-update-${Date.now()}`;this.sendTerminalStatus("\u2B07 Downloading new version...");let D=await fetch(F);if(!D.ok)throw Error(`Download failed: ${D.status}`);let L=await D.arrayBuffer();if(L.byteLength<1048576)throw Error("Downloaded file too small \u2014 likely an error page");let C=(L.byteLength/1024/1024).toFixed(1);this.sendTerminalStatus(`\u2713 Downloaded ${C} MB, verifying...`);let{$:R}=awaitPromise.resolve(globalThis.Bun);await Bun.write(w,L),await R`chmod +x ${w}`.quiet().nothrow(),await R`cp ${U.currentBinary} /tmp/the-link.backup`.quiet().nothrow(),await W(w),this.sendTerminalStatus("\u27F3 Replacing binary and restarting...");let T=U.currentBinary,_=T.substring(0,T.lastIndexOf("/")),m=`${T}.new`,l=!1;try{if(await Bun.write(m,L),await R`chmod +x ${m}`.quiet().nothrow(),await W(m),(await R`mv ${m} ${T}`.quiet().nothrow()).exitCode===0)l=!0;else await R`rm -f ${m}`.quiet().nothrow()}catch{}if(!l&&process.platform==="darwin"){if((await R`cp ${w} ${T} && chmod +x ${T}`.quiet().nothrow()).exitCode===0)l=!0}if(!l){if(S.info("Direct replace failed, trying sudo..."),(await R`sudo rm -f ${T} && sudo cp ${w} ${T} && sudo chmod +x ${T}`.quiet().nothrow()).exitCode===0)l=!0}if(await R`rm -f ${w}`.quiet().nothrow(),!l)throw Error("Failed to replace binary \u2014 insufficient permissions");S.info(`Updated binary (${C} MB). Restarting...`),await new Promise((_0)=>setTimeout(_0,200)),process.exit(0)}catch(U){S.error(`Self-update failed: ${U}`),this.sendTerminalStatus(`\u2717 Update failed: ${U}`)}break}case"session_info":{if(!Q)break;let $=Q.terminal.getSocketName(),Y=Q.terminal.getSessionName(),X=process.cwd();try{X=await Q.terminal.getCurrentDir?.()||X}catch{X=process.cwd()}this.backend.sendCommandResult?.("session_info",{tmuxCommand:`tmux -u -L ${$} attach -t ${Y}`,socketName:$,sessionName:Y,version:K6,cwd:X});break}case"capture_screen":{if(process.env.MCP_PROBE_TRACE==="1")S.info(`[TRACE] capture_screen entered (session=${!!Q}, params=${JSON.stringify(J)})`);if(!Q){if(process.env.MCP_PROBE_TRACE==="1")S.warn("[TRACE] capture_screen \u2014 !session, bailing");break}try{let $=J?.scrollback===!0,Y=typeof J?.window==="number"?J.window:void 0,X;if(Y!==void 0)X=$?await Q.terminal.captureFullScrollbackWindow(Y):await Q.terminal.captureScreenWindow(Y);else X=$?await Q.terminal.captureFullScrollback():await Q.terminal.captureScreen();if(process.env.MCP_PROBE_TRACE==="1")S.info(`[TRACE] capture_screen \u2014 content.length=${X.length}, sendCommandResult=${typeof this.backend.sendCommandResult}`);this.backend.sendCommandResult?.("capture_screen",{content:X,scrollback:$,window:Y,timestamp:Date.now()})}catch($){if(process.env.MCP_PROBE_TRACE==="1")S.error(`[TRACE] capture_screen threw: ${$.message}`);this.backend.sendCommandResult?.("capture_screen",{content:"",error:$.message})}break}case"send_to_window":{if(!Q)break;try{let $=J?.window,Y=J?.command;if(typeof $!=="number"||typeof Y!=="string"){this.backend.sendCommandResult?.("send_to_window",{success:!1,error:"Missing required params: window (number) and command (string)"});break}let X=await Q.terminal.writeToWindow($,Y);this.backend.sendCommandResult?.("send_to_window",X?{success:X,window:$}:{success:X,window:$,error:`tmux send-keys to window ${$} failed (session: ${Q.terminal.getSessionName()}, socket: ${Q.terminal.getSocketName()})`})}catch($){this.backend.sendCommandResult?.("send_to_window",{success:!1,error:$.message})}break}case"send_keys_to_window":{if(!Q)break;try{let $=J?.window,Y=J?.key;if(typeof $!=="number"||typeof Y!=="string"){this.backend.sendCommandResult?.("send_keys_to_window",{success:!1,error:"Missing required params: window (number) and key (string)"});break}let X=await Q.terminal.sendKeysToWindow($,Y);this.backend.sendCommandResult?.("send_keys_to_window",X?{success:X,window:$}:{success:X,window:$,error:`tmux send-keys to window ${$} failed`})}catch($){this.backend.sendCommandResult?.("send_keys_to_window",{success:!1,error:$.message})}break}case"git_scan":{try{let $=J?.path||null;if(!$&&Q)$=await Q.terminal.getCurrentDir();$=$||this.config.terminal.homeDir||process.env.HOME||"/";let Y=await this.scanGitRepos($);this.backend.sendCommandResult?.("git_scan",{repos:Y,scanDir:$})}catch($){this.backend.sendCommandResult?.("git_scan",{repos:[],error:$.message})}break}case"git_fetch":{try{let $=J?.path?.trim();if(!$)break;S.info(`Git fetch: ${$}`);let Y=Bun.spawnSync(["git","-C",$,"fetch","--all"],{stdout:"pipe",stderr:"pipe"}),X=Y.stdout.toString()+Y.stderr.toString();this.backend.sendCommandResult?.("git_action",{action:"fetch",path:$,success:Y.exitCode===0,output:X.trim()})}catch($){this.backend.sendCommandResult?.("git_action",{action:"fetch",success:!1,output:$.message})}break}case"git_pull":{try{let $=J?.path?.trim();if(!$)break;S.info(`Git pull: ${$}`);let Y=Bun.spawnSync(["git","-C",$,"pull"],{stdout:"pipe",stderr:"pipe"}),X=Y.stdout.toString()+Y.stderr.toString();this.backend.sendCommandResult?.("git_action",{action:"pull",path:$,success:Y.exitCode===0,output:X.trim()})}catch($){this.backend.sendCommandResult?.("git_action",{action:"pull",success:!1,output:$.message})}break}case"git_reset":{try{let $=J?.path?.trim();if(!$)break;S.info(`Git reset HEAD: ${$}`);let Y=Bun.spawnSync(["git","-C",$,"reset","HEAD"],{stdout:"pipe",stderr:"pipe"}),X=Y.stdout.toString()+Y.stderr.toString();this.backend.sendCommandResult?.("git_action",{action:"reset",path:$,success:Y.exitCode===0,output:X.trim()})}catch($){this.backend.sendCommandResult?.("git_action",{action:"reset",success:!1,output:$.message})}break}case"git_branches":{try{let $=J?.path?.trim();if(!$)break;let Y=Bun.spawnSync(["git","-C",$,"remote"],{stdout:"pipe",stderr:"pipe"}),X=Y.exitCode===0?Y.stdout.toString().trim().split(`
|
|
472
|
+
`)}async handleControlAction(Z,J){let Q=this.getSession("system");switch(Z){case"kill":if(S.info("Kill command received \u2014 killing terminal session"),Q)await Q.terminal.kill();break;case"restart":{if(S.info("Restart command received"),this.sendTerminalStatus("\u27F3 Restarting terminal..."),Q)await Q.terminal.kill(),this.userSessions.delete("system");await new Promise(($)=>setTimeout($,500)),await this.initializeHub360Session(),this.sendTerminalStatus("\u2713 Terminal restarted");break}case"self-update":{S.info("Self-update command received");let{detectDeploymentMode:$,resolveUpdatePaths:Y,getCurrentSystem:X,applyPlatformFixes:W,getInstallType:K,performNpmUpdate:z}=await Promise.resolve().then(() => (mY(),W40));if(K()==="npm"){S.info("Updating via npm (bun update -g)"),this.sendTerminalStatus("\u27F3 Updating via npm...");try{let{APP_VERSION:U}=await Promise.resolve().then(() => XL),w=await z(U);if(w.success)this.sendTerminalStatus(`\u2713 Updated: v${w.previousVersion} \u2192 v${w.newVersion}`),S.info(`npm update complete: ${w.previousVersion} \u2192 ${w.newVersion}`),await new Promise((D)=>setTimeout(D,200)),process.exit(0);else throw Error(w.error||"npm update failed")}catch(U){S.error(`npm self-update failed: ${U}`),this.sendTerminalStatus(`\u2717 Update failed: ${U}`)}break}let H=$(),B=X(),F=`${this.config.hub.url.replace("ws://","http://").replace("wss://","https://")}/download/${B}`;S.info(`Updating from ${F} (mode: ${H}, system: ${B})`),this.sendTerminalStatus(`\u27F3 Starting update (${B})...`);try{let U=Y(),w=`/tmp/the-link-update-${Date.now()}`;this.sendTerminalStatus("\u2B07 Downloading new version...");let D=await fetch(F);if(!D.ok)throw Error(`Download failed: ${D.status}`);let L=await D.arrayBuffer();if(L.byteLength<1048576)throw Error("Downloaded file too small \u2014 likely an error page");let C=(L.byteLength/1024/1024).toFixed(1);this.sendTerminalStatus(`\u2713 Downloaded ${C} MB, verifying...`);let{$:R}=awaitPromise.resolve(globalThis.Bun);await Bun.write(w,L),await R`chmod +x ${w}`.quiet().nothrow(),await R`cp ${U.currentBinary} /tmp/the-link.backup`.quiet().nothrow(),await W(w),this.sendTerminalStatus("\u27F3 Replacing binary and restarting...");let T=U.currentBinary,_=T.substring(0,T.lastIndexOf("/")),m=`${T}.new`,l=!1;try{if(await Bun.write(m,L),await R`chmod +x ${m}`.quiet().nothrow(),await W(m),(await R`mv ${m} ${T}`.quiet().nothrow()).exitCode===0)l=!0;else await R`rm -f ${m}`.quiet().nothrow()}catch{}if(!l&&process.platform==="darwin"){if((await R`cp ${w} ${T} && chmod +x ${T}`.quiet().nothrow()).exitCode===0)l=!0}if(!l){if(S.info("Direct replace failed, trying sudo..."),(await R`sudo rm -f ${T} && sudo cp ${w} ${T} && sudo chmod +x ${T}`.quiet().nothrow()).exitCode===0)l=!0}if(await R`rm -f ${w}`.quiet().nothrow(),!l)throw Error("Failed to replace binary \u2014 insufficient permissions");S.info(`Updated binary (${C} MB). Restarting...`),await new Promise((_0)=>setTimeout(_0,200)),process.exit(0)}catch(U){S.error(`Self-update failed: ${U}`),this.sendTerminalStatus(`\u2717 Update failed: ${U}`)}break}case"session_info":{if(!Q)break;let $=Q.terminal.getSocketName(),Y=Q.terminal.getSessionName(),X=process.cwd();try{X=await Q.terminal.getCurrentDir?.()||X}catch{X=process.cwd()}this.backend.sendCommandResult?.("session_info",{tmuxCommand:`tmux -u -L ${$} attach -t ${Y}`,socketName:$,sessionName:Y,version:K6,cwd:X});break}case"capture_screen":{if(process.env.MCP_PROBE_TRACE==="1")S.info(`[TRACE] capture_screen entered (session=${!!Q}, params=${JSON.stringify(J)})`);if(!Q){if(process.env.MCP_PROBE_TRACE==="1")S.warn("[TRACE] capture_screen \u2014 !session, bailing");break}try{let $=J?.scrollback===!0,Y=typeof J?.window==="number"?J.window:void 0,X;if(Y!==void 0)X=$?await Q.terminal.captureFullScrollbackWindow(Y):await Q.terminal.captureScreenWindow(Y);else X=$?await Q.terminal.captureFullScrollback():await Q.terminal.captureScreen();if(process.env.MCP_PROBE_TRACE==="1")S.info(`[TRACE] capture_screen \u2014 content.length=${X.length}, sendCommandResult=${typeof this.backend.sendCommandResult}`);this.backend.sendCommandResult?.("capture_screen",{content:X,scrollback:$,window:Y,timestamp:Date.now()})}catch($){if(process.env.MCP_PROBE_TRACE==="1")S.error(`[TRACE] capture_screen threw: ${$.message}`);this.backend.sendCommandResult?.("capture_screen",{content:"",error:$.message})}break}case"send_to_window":{if(!Q)break;try{let $=J?.window,Y=J?.command;if(typeof $!=="number"||typeof Y!=="string"){this.backend.sendCommandResult?.("send_to_window",{success:!1,error:"Missing required params: window (number) and command (string)"});break}let X=await Q.terminal.writeToWindow($,Y);this.backend.sendCommandResult?.("send_to_window",X?{success:X,window:$}:{success:X,window:$,error:`tmux send-keys to window ${$} failed (session: ${Q.terminal.getSessionName()}, socket: ${Q.terminal.getSocketName()})`})}catch($){this.backend.sendCommandResult?.("send_to_window",{success:!1,error:$.message})}break}case"send_keys":{if(!Q)break;try{let $=J?.key;if(typeof $!=="string"){this.backend.sendCommandResult?.("send_keys",{success:!1,error:"Missing required param: key (string)"});break}let Y=await Q.terminal.sendSpecialKey($);this.backend.sendCommandResult?.("send_keys",Y?{success:Y}:{success:Y,error:`tmux send-keys failed (key: ${$})`})}catch($){this.backend.sendCommandResult?.("send_keys",{success:!1,error:$.message})}break}case"send_keys_to_window":{if(!Q)break;try{let $=J?.window,Y=J?.key;if(typeof $!=="number"||typeof Y!=="string"){this.backend.sendCommandResult?.("send_keys_to_window",{success:!1,error:"Missing required params: window (number) and key (string)"});break}let X=await Q.terminal.sendKeysToWindow($,Y);this.backend.sendCommandResult?.("send_keys_to_window",X?{success:X,window:$}:{success:X,window:$,error:`tmux send-keys to window ${$} failed`})}catch($){this.backend.sendCommandResult?.("send_keys_to_window",{success:!1,error:$.message})}break}case"git_scan":{try{let $=J?.path||null;if(!$&&Q)$=await Q.terminal.getCurrentDir();$=$||this.config.terminal.homeDir||process.env.HOME||"/";let Y=await this.scanGitRepos($);this.backend.sendCommandResult?.("git_scan",{repos:Y,scanDir:$})}catch($){this.backend.sendCommandResult?.("git_scan",{repos:[],error:$.message})}break}case"git_fetch":{try{let $=J?.path?.trim();if(!$)break;S.info(`Git fetch: ${$}`);let Y=Bun.spawnSync(["git","-C",$,"fetch","--all"],{stdout:"pipe",stderr:"pipe"}),X=Y.stdout.toString()+Y.stderr.toString();this.backend.sendCommandResult?.("git_action",{action:"fetch",path:$,success:Y.exitCode===0,output:X.trim()})}catch($){this.backend.sendCommandResult?.("git_action",{action:"fetch",success:!1,output:$.message})}break}case"git_pull":{try{let $=J?.path?.trim();if(!$)break;S.info(`Git pull: ${$}`);let Y=Bun.spawnSync(["git","-C",$,"pull"],{stdout:"pipe",stderr:"pipe"}),X=Y.stdout.toString()+Y.stderr.toString();this.backend.sendCommandResult?.("git_action",{action:"pull",path:$,success:Y.exitCode===0,output:X.trim()})}catch($){this.backend.sendCommandResult?.("git_action",{action:"pull",success:!1,output:$.message})}break}case"git_reset":{try{let $=J?.path?.trim();if(!$)break;S.info(`Git reset HEAD: ${$}`);let Y=Bun.spawnSync(["git","-C",$,"reset","HEAD"],{stdout:"pipe",stderr:"pipe"}),X=Y.stdout.toString()+Y.stderr.toString();this.backend.sendCommandResult?.("git_action",{action:"reset",path:$,success:Y.exitCode===0,output:X.trim()})}catch($){this.backend.sendCommandResult?.("git_action",{action:"reset",success:!1,output:$.message})}break}case"git_branches":{try{let $=J?.path?.trim();if(!$)break;let Y=Bun.spawnSync(["git","-C",$,"remote"],{stdout:"pipe",stderr:"pipe"}),X=Y.exitCode===0?Y.stdout.toString().trim().split(`
|
|
473
473
|
`).filter(Boolean):["origin"],K=Bun.spawnSync(["git","-C",$,"branch","-a","--format=%(refname:short) %(HEAD)"],{stdout:"pipe",stderr:"pipe"}).stdout.toString().trim().split(`
|
|
474
474
|
`).filter(Boolean),z=new Map;for(let H of K){let B=H.endsWith(" *"),V=H.replace(/ \*$/,"").trim(),F=V,U=!1;for(let D of X)if(V.startsWith(`${D}/`)){F=V.slice(D.length+1),U=!0;break}if(F==="HEAD")continue;let w=z.get(F);if(w){if(!U)w.name=F;if(B)w.current=!0}else z.set(F,{name:F,current:B})}let G=Array.from(z.values());this.backend.sendCommandResult?.("git_branches",{path:$,branches:G})}catch($){this.backend.sendCommandResult?.("git_branches",{path:dir,branches:[],error:$.message})}break}case"git_checkout":{try{let $=J?.path?.trim(),Y=J?.branch?.trim();if(!$||!Y)break;let X=Y,W=Bun.spawnSync(["git","-C",$,"remote"],{stdout:"pipe",stderr:"pipe"});if(W.exitCode===0){for(let G of W.stdout.toString().trim().split(`
|
|
475
475
|
`).filter(Boolean))if(Y.startsWith(`${G}/`)){X=Y.slice(G.length+1);break}}S.info(`Git checkout: ${$} \u2192 ${X}`);let K=Bun.spawnSync(["git","-C",$,"checkout",X],{stdout:"pipe",stderr:"pipe"}),z=K.stdout.toString()+K.stderr.toString();this.backend.sendCommandResult?.("git_action",{action:"checkout",path:$,branch:Y,success:K.exitCode===0,output:z.trim()})}catch($){this.backend.sendCommandResult?.("git_action",{action:"checkout",success:!1,output:$.message})}break}case"git_diff":{try{let $=J?.path?.trim(),Y=J?.file?.trim(),X=J?.fileType?.trim();if(!$||!Y)break;let W;if(X==="staged"){W=Bun.spawnSync(["git","-C",$,"diff","--cached","--",Y],{stdout:"pipe",stderr:"pipe"});let K=W.stdout.toString().trim(),z="";if(!K&&W.exitCode===0){let H=Bun.spawnSync(["git","-C",$,"show",`:${Y}`],{stdout:"pipe",stderr:"pipe"}),B=H.stdout.toString();if(H.exitCode===0&&B.trim()){let V=B.endsWith(`\r
|
|
@@ -481,7 +481,7 @@ new file
|
|
|
481
481
|
+++ b/${Y}
|
|
482
482
|
@@ -0,0 +1,${F.length} @@
|
|
483
483
|
`+F.map((U)=>"+"+U).join(`
|
|
484
|
-
`)}else z=H.stderr.toString().trim()}if(!K)K=z||W.stderr.toString().trim();let G=W.exitCode===0||!!K;this.backend.sendCommandResult?.("git_diff",{path:$,file:Y,fileType:X,success:G,diff:K,...G?{}:{error:K||"Failed to get staged diff"}})}else if(X==="untracked"){W=Bun.spawnSync(["git","-C",$,"diff","--no-index","--","/dev/null",Y],{stdout:"pipe",stderr:"pipe"});let K=W.stdout.toString()+W.stderr.toString(),z=W.exitCode===0||W.exitCode===1;this.backend.sendCommandResult?.("git_diff",{path:$,file:Y,fileType:X,success:z,diff:K.trim()})}else{W=Bun.spawnSync(["git","-C",$,"diff","--",Y],{stdout:"pipe",stderr:"pipe"});let K=W.stdout.toString()+W.stderr.toString();this.backend.sendCommandResult?.("git_diff",{path:$,file:Y,fileType:X,success:W.exitCode===0,diff:K.trim()})}}catch($){this.backend.sendCommandResult?.("git_diff",{path:J?.path,file:J?.file,fileType:J?.fileType,success:!1,diff:"",error:$.message})}break}case"list_directory":{try{let $=typeof J?.path==="string"?J.path.trim():"",Y=await this.resolveExplorerCwdRoot(Q);if(Y===null&&!$){this.backend.sendCommandResult?.("list_directory",{path:"/",parent:null,entries:[],error:"Explorer cwdRoot is not set (no session, no HOME, no homeDir) and no explicit path was provided."});break}let X=Y??"";$=$||X;let W=s50($,X);this.backend.sendCommandResult?.("list_directory",W)}catch($){this.backend.sendCommandResult?.("list_directory",{path:typeof J?.path==="string"?J.path:"",parent:null,entries:[],error:$.message??"Unknown error"})}break}case"read_file":{try{if(typeof J?.path!=="string"){this.backend.sendCommandResult?.("read_file",{path:"",size:0,mtime:0,encoding:"utf8",content:"",isBinary:!1,truncated:!1,error:"path must be a string",errorCode:"io_error"});break}let $=J.path.trim();if(!$){this.backend.sendCommandResult?.("read_file",{path:"",size:0,mtime:0,encoding:"utf8",content:"",isBinary:!1,truncated:!1,error:"No path provided",errorCode:"io_error"});break}let X=await this.resolveExplorerCwdRoot(Q)??"",W=r50($,X);this.backend.sendCommandResult?.("read_file",W)}catch($){this.backend.sendCommandResult?.("read_file",{path:typeof J?.path==="string"?J.path:"",size:0,mtime:0,encoding:"utf8",content:"",isBinary:!1,truncated:!1,error:$.message??"Unknown error",errorCode:"io_error"})}break}case"write_file":{try{let $=typeof J?.path==="string"?J.path.trim():"";if(typeof J?.content!=="string"){this.backend.sendCommandResult?.("write_file",{path:$,success:!1,error:"content must be a string",errorCode:"io_error"});break}let Y=J.content,X=J?.expectedMtime,W;if(X===void 0||X===null)W=void 0;else{let H=Number(X);if(!Number.isFinite(H)){this.backend.sendCommandResult?.("write_file",{path:$,success:!1,error:"expectedMtime must be a finite number",errorCode:"io_error"});break}W=H}if(!$){this.backend.sendCommandResult?.("write_file",{path:"",success:!1,error:"No path provided",errorCode:"io_error"});break}let z=await this.resolveExplorerCwdRoot(Q)??"",G=t50($,Y,W,z);this.backend.sendCommandResult?.("write_file",G)}catch($){this.backend.sendCommandResult?.("write_file",{path:typeof J?.path==="string"?J.path:"",success:!1,error:$.message??"Unknown error",errorCode:"io_error"})}break}case"delete_file":{try{if(typeof J?.path!=="string"){this.backend.sendCommandResult?.("delete_file",{path:typeof J?.path==="string"?J.path:"",success:!1,error:"path parameter is required and must be a string",errorCode:"io_error"});break}let $=J.path.trim();if(!$){this.backend.sendCommandResult?.("delete_file",{path:"",success:!1,error:"No path provided",errorCode:"io_error"});break}let X=await this.resolveExplorerCwdRoot(Q)??"",W=e50($,X);this.backend.sendCommandResult?.("delete_file",W)}catch($){this.backend.sendCommandResult?.("delete_file",{path:typeof J?.path==="string"?J.path:"",success:!1,error:$.message??"Unknown error",errorCode:"io_error"})}break}case"list_panes":{if(!Q)break;let $=await Q.terminal.listPanes();this.backend.sendCommandResult?.("list_panes",{panes:$});break}case"select_pane":{if(!Q)break;let $=Number(J?.index??0);await Q.terminal.selectPane($),Q.terminal.invalidateReadCache();let Y=await Q.terminal.listPanes();this.backend.sendCommandResult?.("
|
|
484
|
+
`)}else z=H.stderr.toString().trim()}if(!K)K=z||W.stderr.toString().trim();let G=W.exitCode===0||!!K;this.backend.sendCommandResult?.("git_diff",{path:$,file:Y,fileType:X,success:G,diff:K,...G?{}:{error:K||"Failed to get staged diff"}})}else if(X==="untracked"){W=Bun.spawnSync(["git","-C",$,"diff","--no-index","--","/dev/null",Y],{stdout:"pipe",stderr:"pipe"});let K=W.stdout.toString()+W.stderr.toString(),z=W.exitCode===0||W.exitCode===1;this.backend.sendCommandResult?.("git_diff",{path:$,file:Y,fileType:X,success:z,diff:K.trim()})}else{W=Bun.spawnSync(["git","-C",$,"diff","--",Y],{stdout:"pipe",stderr:"pipe"});let K=W.stdout.toString()+W.stderr.toString();this.backend.sendCommandResult?.("git_diff",{path:$,file:Y,fileType:X,success:W.exitCode===0,diff:K.trim()})}}catch($){this.backend.sendCommandResult?.("git_diff",{path:J?.path,file:J?.file,fileType:J?.fileType,success:!1,diff:"",error:$.message})}break}case"list_directory":{try{let $=typeof J?.path==="string"?J.path.trim():"",Y=await this.resolveExplorerCwdRoot(Q);if(Y===null&&!$){this.backend.sendCommandResult?.("list_directory",{path:"/",parent:null,entries:[],error:"Explorer cwdRoot is not set (no session, no HOME, no homeDir) and no explicit path was provided."});break}let X=Y??"";$=$||X;let W=s50($,X);this.backend.sendCommandResult?.("list_directory",W)}catch($){this.backend.sendCommandResult?.("list_directory",{path:typeof J?.path==="string"?J.path:"",parent:null,entries:[],error:$.message??"Unknown error"})}break}case"read_file":{try{if(typeof J?.path!=="string"){this.backend.sendCommandResult?.("read_file",{path:"",size:0,mtime:0,encoding:"utf8",content:"",isBinary:!1,truncated:!1,error:"path must be a string",errorCode:"io_error"});break}let $=J.path.trim();if(!$){this.backend.sendCommandResult?.("read_file",{path:"",size:0,mtime:0,encoding:"utf8",content:"",isBinary:!1,truncated:!1,error:"No path provided",errorCode:"io_error"});break}let X=await this.resolveExplorerCwdRoot(Q)??"",W=r50($,X);this.backend.sendCommandResult?.("read_file",W)}catch($){this.backend.sendCommandResult?.("read_file",{path:typeof J?.path==="string"?J.path:"",size:0,mtime:0,encoding:"utf8",content:"",isBinary:!1,truncated:!1,error:$.message??"Unknown error",errorCode:"io_error"})}break}case"write_file":{try{let $=typeof J?.path==="string"?J.path.trim():"";if(typeof J?.content!=="string"){this.backend.sendCommandResult?.("write_file",{path:$,success:!1,error:"content must be a string",errorCode:"io_error"});break}let Y=J.content,X=J?.expectedMtime,W;if(X===void 0||X===null)W=void 0;else{let H=Number(X);if(!Number.isFinite(H)){this.backend.sendCommandResult?.("write_file",{path:$,success:!1,error:"expectedMtime must be a finite number",errorCode:"io_error"});break}W=H}if(!$){this.backend.sendCommandResult?.("write_file",{path:"",success:!1,error:"No path provided",errorCode:"io_error"});break}let z=await this.resolveExplorerCwdRoot(Q)??"",G=t50($,Y,W,z);this.backend.sendCommandResult?.("write_file",G)}catch($){this.backend.sendCommandResult?.("write_file",{path:typeof J?.path==="string"?J.path:"",success:!1,error:$.message??"Unknown error",errorCode:"io_error"})}break}case"delete_file":{try{if(typeof J?.path!=="string"){this.backend.sendCommandResult?.("delete_file",{path:typeof J?.path==="string"?J.path:"",success:!1,error:"path parameter is required and must be a string",errorCode:"io_error"});break}let $=J.path.trim();if(!$){this.backend.sendCommandResult?.("delete_file",{path:"",success:!1,error:"No path provided",errorCode:"io_error"});break}let X=await this.resolveExplorerCwdRoot(Q)??"",W=e50($,X);this.backend.sendCommandResult?.("delete_file",W)}catch($){this.backend.sendCommandResult?.("delete_file",{path:typeof J?.path==="string"?J.path:"",success:!1,error:$.message??"Unknown error",errorCode:"io_error"})}break}case"list_panes":{if(!Q)break;let $=await Q.terminal.listPanes();this.backend.sendCommandResult?.("list_panes",{panes:$});break}case"select_pane":{if(!Q)break;let $=Number(J?.index??0);await Q.terminal.selectPane($),Q.terminal.invalidateReadCache();let Y=await Q.terminal.listPanes();this.backend.sendCommandResult?.("select_pane",{panes:Y});break}case"next_pane":{if(!Q)break;await Q.terminal.nextPane(),Q.terminal.invalidateReadCache();let $=await Q.terminal.listPanes();this.backend.sendCommandResult?.("next_pane",{panes:$});break}case"prev_pane":{if(!Q)break;await Q.terminal.previousPane(),Q.terminal.invalidateReadCache();let $=await Q.terminal.listPanes();this.backend.sendCommandResult?.("prev_pane",{panes:$});break}case"new_window":{if(!Q)break;await Q.terminal.createWindow(),Q.terminal.invalidateReadCache();let $=await Q.terminal.listWindows();this.backend.sendCommandResult?.("new_window",{windows:$});break}case"kill_window":{if(!Q)break;if((await Q.terminal.listWindows()).length<=1){this.backend.sendCommandResult?.("kill_window",{error:"Cannot close the last window"});break}let Y=Number(J?.index??0);await Q.terminal.killWindow(Y),Q.terminal.invalidateReadCache();let X=await Q.terminal.listWindows();this.backend.sendCommandResult?.("kill_window",{windows:X});break}case"list_windows":{if(!Q)break;let $=await Q.terminal.listWindows();this.backend.sendCommandResult?.("list_windows",{windows:$});break}case"select_window":{if(!Q)break;let $=Number(J?.index??0);await Q.terminal.selectWindow($),Q.terminal.invalidateReadCache();let Y=await Q.terminal.listWindows();this.backend.sendCommandResult?.("select_window",{windows:Y});break}case"next_window":{if(!Q)break;await Q.terminal.nextWindow(),Q.terminal.invalidateReadCache();let $=await Q.terminal.listWindows();this.backend.sendCommandResult?.("next_window",{windows:$});break}case"prev_window":{if(!Q)break;await Q.terminal.previousWindow(),Q.terminal.invalidateReadCache();let $=await Q.terminal.listWindows();this.backend.sendCommandResult?.("prev_window",{windows:$});break}case"list_instances":{S.info("list_instances command received");try{let{listAllInstances:$}=await Promise.resolve().then(() => (VQ(),BQ));S.info("instance-operations module loaded");let Y=await $();S.info(`list_instances: returning ${Y.length} instances`);let X=Y.find((W)=>W.isDefault)?.name||this.config.hub.agentName;this.backend.sendCommandResult?.("list_instances",{instances:Y,defaultName:X})}catch($){S.error(`list_instances failed: ${$}`),this.backend.sendCommandResult?.("list_instances",{success:!1,message:String($),instances:[]})}break}case"create_instance":{try{let $=["name","workDir","token","hubUrl","role","tokenId"];for(let W of $)if(typeof J?.[W]!=="string"||!J[W]){this.backend.sendCommandResult?.("create_instance",{success:!1,message:`Missing or invalid param: '${W}'`});break}if($.some((W)=>typeof J?.[W]!=="string"||!J[W]))break;let{createInstance:Y}=await Promise.resolve().then(() => (VQ(),BQ)),X=await Y(J);this.backend.sendCommandResult?.("create_instance",X)}catch($){this.backend.sendCommandResult?.("create_instance",{success:!1,message:String($)})}break}case"start_instance":{if(typeof J?.name!=="string"||!J.name){this.backend.sendCommandResult?.("start_instance",{success:!1,message:"Missing or invalid param: 'name'"});break}try{let{startInstanceByName:$}=await Promise.resolve().then(() => (VQ(),BQ)),Y=await $(J.name);this.backend.sendCommandResult?.("start_instance",Y)}catch($){this.backend.sendCommandResult?.("start_instance",{success:!1,message:String($)})}break}case"stop_instance":{if(typeof J?.name!=="string"||!J.name){this.backend.sendCommandResult?.("stop_instance",{success:!1,message:"Missing or invalid param: 'name'"});break}try{let{stopInstanceByName:$}=await Promise.resolve().then(() => (VQ(),BQ)),Y=await $(J.name);this.backend.sendCommandResult?.("stop_instance",Y)}catch($){this.backend.sendCommandResult?.("stop_instance",{success:!1,message:String($)})}break}case"delete_instance":{if(typeof J?.name!=="string"||!J.name){this.backend.sendCommandResult?.("delete_instance",{success:!1,message:"Missing or invalid param: 'name'"});break}try{let{deleteInstance:$}=await Promise.resolve().then(() => (VQ(),BQ)),Y=await $(J.name);this.backend.sendCommandResult?.("delete_instance",Y)}catch($){this.backend.sendCommandResult?.("delete_instance",{success:!1,message:String($)})}break}case"install_extension":{try{let{installExtension:$}=await Promise.resolve().then(() => (v40(),b40)),Y=await $(J||{},Q,this.config);this.backend.sendCommandResult?.("install_extension",Y)}catch($){S.error(`install_extension failed: ${$}`),this.backend.sendCommandResult?.("install_extension",{success:!1,error:String($)})}break}default:S.warn(`Unknown control action: ${Z}`)}}async scanGitRepos(Z){Z=Z.trim(),S.info(`Git scan: scanning ${Z}`);let J=[];if(Bun.spawnSync(["git","-C",Z,"rev-parse","--git-dir"],{stdout:"pipe",stderr:"pipe"}).exitCode===0){S.info(`Git scan: ${Z} is a git repo`);let X=Z,W=X.split("/").pop()||X,K=await this.getGitRepoInfo(X,W);if(K)J.push(K)}let Y=Bun.spawnSync(["find",Z,"-mindepth","1","-maxdepth","3","-name",".git","-type","d"],{stdout:"pipe",stderr:"pipe"}).stdout.toString().trim();if(S.info(`Git scan: find children result="${Y}"`),Y){let X=Y.split(`
|
|
485
485
|
`).filter(Boolean);for(let W of X){let K=W.replace(/\/\.git$/,"");if(K===Z)continue;let z=K.split("/").pop()||K,G=await this.getGitRepoInfo(K,z);if(G)J.push(G)}}return S.info(`Git scan: found ${J.length} repos`),J}async getGitRepoInfo(Z,J){try{let $=Bun.spawnSync(["git","-C",Z,"branch","--show-current"],{stdout:"pipe",stderr:"pipe"}).stdout.toString().trim();if(!$)$=Bun.spawnSync(["git","-C",Z,"rev-parse","--short","HEAD"],{stdout:"pipe",stderr:"pipe"}).stdout.toString().trim()||"(detached)";let X=Bun.spawnSync(["git","-C",Z,"status","--porcelain"],{stdout:"pipe",stderr:"pipe"}).stdout.toString().trim().split(`
|
|
486
486
|
`).filter(Boolean),W=[],K=[],z=[];for(let G of X){let H=G[0],B=G[1],V=G.slice(3);if(H==="?"&&B==="?")z.push(V);else{if(H!==" "&&H!=="?")W.push(V);if(B!==" "&&B!=="?")K.push(V)}}return{path:Z,name:J,branch:$,staged:W,modified:K,untracked:z}}catch{return null}}getConfig(){return this.config}async getOrCreateSession(Z,J){let Q=this.userSessions.get(Z);if(Q)return Q.channelId=J,Q;let $=await this.resolveUsername(Z);S.info(`Creating new session for @${$} (${Z})`);let Y=this.createOutputFormatter(Z,J),X=new zG(this.config,{onData:(z)=>{this.handleTerminalOutputForUser(Z,z)},onExit:(z,G)=>{this.handleTerminalExitForUser(Z,z,G)}}),W=this.getSocketName();X.setSocketName(W),X.setSessionName($);let K=this.restoredUserDirs[$];if(K)X.setHomeDir(K),P7(`Restored working directory for @${$}: ${K}`);return Q={terminal:X,outputFormatter:Y,channelId:J,username:$,initialized:!1},this.userSessions.set(Z,Q),Q}getSession(Z){return this.userSessions.get(Z)||null}async resolveExplorerCwdRoot(Z){let J="";if(Z)J=await Z.terminal.getCurrentDir()??"";if(J=J||this.config.terminal.homeDir||process.env.HOME||"",GQ(J))return null;return J}createOutputFormatter(Z,J){if(!zQ(this.backend))throw Error("Unknown backend type");let Q=this.backend.client;return new WO(this.config,{userId:Z},{onSendMessage:async($)=>{return Q.sendOutput($),{postId:`hub360-${Date.now()}`}},onEditMessage:async($,Y)=>{return Q.sendOutput(Y),{postId:`hub360-${Date.now()}`}}})}static MAX_PROXY_RESPONSE_SIZE=921600;async handleProxyRequest(Z){let{requestId:J,tunnelId:Q,method:$,path:Y,headers:X,body:W,bodyEncoding:K}=Z,z=this.extractPortFromTunnel(Q,X);if(!z){this.sendProxyError(J,502,"Missing local port in proxy request");return}S.info(`Proxy ${$} localhost:${z}${Y} (tunnel=${Q.slice(0,8)}...)`);try{let G=`http://localhost:${z}${Y}`,H=null;if(W!==null)if(K==="base64")H=Buffer.from(W,"base64");else H=W;let B={...X};B.host=`localhost:${z}`,delete B["x-tunnel-local-port"];let V=await fetch(G,{method:$,headers:B,body:H,redirect:"manual",signal:AbortSignal.timeout(25000)}),F=await V.arrayBuffer();if(F.byteLength>mO.MAX_PROXY_RESPONSE_SIZE){this.sendProxyError(J,502,"Response body too large for tunnel proxy");return}let U=null,w="utf8";if(F.byteLength>0){let L=V.headers.get("content-type")||"";if(this.isTextContent(L))U=new TextDecoder().decode(F),w="utf8";else U=Buffer.from(F).toString("base64"),w="base64"}let D={};V.headers.forEach((L,C)=>{D[C]=L}),this.backend.sendProxyResponse?.({requestId:J,statusCode:V.status,headers:D,body:U,bodyEncoding:w})}catch(G){let H=G.message||String(G);if(H.includes("ECONNREFUSED")||H.includes("ConnectionRefused")||H.includes("fetch failed"))S.warn(`Proxy: port ${z} not listening (${H})`),this.sendProxyError(J,502,`Connection refused \u2014 nothing is listening on port ${z}`);else if(H.includes("timed out")||H.includes("AbortError")||H.includes("TimeoutError"))S.warn(`Proxy: request to localhost:${z} timed out`),this.sendProxyError(J,504,"Local service did not respond in time");else S.error(`Proxy: unexpected error \u2014 ${H}`),this.sendProxyError(J,502,`Proxy error: ${H}`)}}sendProxyError(Z,J,Q){this.backend.sendProxyResponse?.({requestId:Z,statusCode:J,headers:{"content-type":"application/json"},body:JSON.stringify({error:Q}),bodyEncoding:"utf8"})}extractPortFromTunnel(Z,J){let Q=J["x-tunnel-local-port"];if(Q){let $=parseInt(Q,10);if(!isNaN($)&&$>0&&$<=65535)return $}return S.warn("proxy_request missing x-tunnel-local-port header"),0}isTextContent(Z){let J=Z.toLowerCase();return J.includes("text/")||J.includes("application/json")||J.includes("application/xml")||J.includes("application/x-www-form-urlencoded")}getSocketName(){return this.config.hub.agentName||"link"}async start(){try{if(this.restoreState(),await this.backend.connect(),this.config.healthPort>0)q50(this.config.healthPort,()=>this.getHealthStatus());if(process.env.DEBUG_SCHEDULER==="1"&&zQ(this.backend)){let{scheduleState:Z,firedLog:J}=this.backend.client;A50({state:Z,firedLog:J,port:this.config.healthPort>0?Math.min(this.config.healthPort+1,65535):0}),S.info("DEBUG_SCHEDULER=1 \u2014 debug endpoint started on http://127.0.0.1 (see debug-port file)")}if(this.config.gcIntervalMs>0)this.gcInterval=setInterval(()=>{Bun.gc(!1)},this.config.gcIntervalMs),S.info(`Periodic GC: every ${this.config.gcIntervalMs/1000}s`);this.spawnTelemetryInterval=setInterval(()=>{let Z=0,J=0;for(let Q of this.userSessions.values())Z+=Q.terminal.getTmuxSpawnCount(),J++;S.debug(`[perf] tmuxSpawnCount total=${Z} sessions=${J}`)},30000),S.info(`Ready! Waiting for messages...
|
|
487
487
|
`)}catch(Z){throw S.error("Failed to start:",Z),Z}}getHealthStatus(){let Z=this.backend.isWebSocketConnected(),J=!1,Q=0,$=[];for(let W of this.userSessions.values())if(Q++,W.terminal.isSessionRunning())J=!0,$.push(W.terminal.getPid());let Y="healthy";if(!Z&&Q===0)Y="degraded";else if(!Z)Y="unhealthy";let X=process.memoryUsage();return{status:Y,uptime:j50(),backend:{connected:Z,lastPongAt:this.backend.getLastPongAt()>0?new Date(this.backend.getLastPongAt()).toISOString():null,lastMessageAt:this.lastMessageAt>0?new Date(this.lastMessageAt).toISOString():null},terminal:{running:J,pid:$.length>0?$[0]??null:null,sessions:Q},memory:{rss:X.rss,heapUsed:X.heapUsed,heapTotal:X.heapTotal,external:X.external},restartIn:this.config.restartIntervalMs>0?Math.max(0,Math.round((this.startTime+this.config.restartIntervalMs-Date.now())/1000)):void 0,timestamp:new Date().toISOString()}}async resolveUsername(Z){if(this.userCache.has(Z))return this.userCache.get(Z);return this.userCache.set(Z,Z),Z}async handleIncomingMessage(Z,J,Q,$,Y){if(S.info(`Received: "${Z}" from user ${Q}`),this.lastMessageAt=Date.now(),P7("Bridge",`postId=${$}, channelId=${J}, fileIds=${JSON.stringify(Y)}`),this.allowedUserIds.size>0&&!this.allowedUserIds.has(Q)){S.info(`Unauthorized user ${Q} - ignoring message`);return}try{this.currentUserId=Q;let X=await this.getOrCreateSession("system",J);if(!X.initialized)X.initialized=!0,S.info(`Starting terminal session for @${X.username}`),await X.terminal.start(),X.outputFormatter.clear();if(Z.length===0)return;P7("Bridge",`Raw input: ${JSON.stringify(Z)} (${Z.length} bytes)`),await X.terminal.writeRaw(Z)}catch(X){S.error(`Error processing message from ${Q}: ${X}`);try{await this.sendMessageToUser(Q,`\u26A0\uFE0F Error processing command: ${X}`)}catch{}}}handleTerminalOutputForUser(Z,J){let Q=this.getSession(Z);if(!Q){P7("Bridge",`No session for user ${Z}, ignoring output`);return}if(!Q.initialized){P7("Bridge",`Ignoring pre-init output for user ${Z}: ${J.length} bytes`);return}P7("Bridge",`Terminal output for ${Q.username}: ${J.length} bytes`),Q.outputFormatter.addOutput(J)}async sendMessageToUser(Z,J){try{if(zQ(this.backend))this.backend.client.sendOutput(J)}catch(Q){S.error(`Failed to send message to user ${Z}: ${Q}`)}}async handleTerminalExitForUser(Z,J,Q){let $=this.getSession(Z);if(!$)return;if(await $.outputFormatter.forceFlush(),$.terminal.isAutoRestartEnabled()&&!this.isShuttingDown){let Y=`Terminal exited (code: ${J}${Q?`, signal: ${Q}`:""}). Auto-restarting...`;try{await this.sendMessageToUser(Z,Y)}catch(X){S.error(`Failed to send exit message: ${X}`)}}else if(!this.isShuttingDown){let Y=`Terminal exited (code: ${J}${Q?`, signal: ${Q}`:""}). Session closed.`;try{await this.sendMessageToUser(Z,Y)}catch(X){S.error(`Failed to send exit message: ${X}`)}$.outputFormatter.destroy(),this.userSessions.delete(Z),S.info(`Session for ${$.username} cleaned up`)}}async shutdown(){if(S.info("Shutting down..."),this.isShuttingDown=!0,await this.saveState(),this.gcInterval)clearInterval(this.gcInterval),this.gcInterval=null;if(this.spawnTelemetryInterval)clearInterval(this.spawnTelemetryInterval),this.spawnTelemetryInterval=null;w50(),N50();for(let[Z,J]of this.userSessions)S.info(`Shutting down session for ${J.username}`),await J.outputFormatter.forceFlush(),await J.terminal.shutdown();await this.backend.disconnect(),await G50(),S.info("Shutdown complete")}async saveState(){try{let Z={};for(let[,$]of this.userSessions){let Y=await $.terminal.getCurrentDir()||$.terminal.getLastKnownDir();if(Y)Z[$.username]=Y}let J={workingDir:process.cwd(),userDirs:Z,timestamp:new Date().toISOString()};E("fs").writeFileSync(wQ,JSON.stringify(J,null,2)),S.info(`State saved: ${J.workingDir}, user dirs: ${Object.keys(Z).length}`)}catch(Z){S.warn(`Failed to save state: ${Z}`)}}restoreState(){try{let Z=E("fs");if(!Z.existsSync(wQ))return;let J=Z.readFileSync(wQ,"utf-8"),Q=JSON.parse(J);if(Q.workingDir&&Q.workingDir!==process.cwd())process.chdir(Q.workingDir),S.info(`Restored working directory: ${Q.workingDir}`);if(Q.userDirs)this.restoredUserDirs=Q.userDirs,S.info(`Restored user directories for: ${Object.keys(Q.userDirs).join(", ")}`)}catch{}}}var S;var g40=q(()=>{Z50();$50();F50();g1();D50();I50();i50();Z40();S=F0("Bridge")});import{randomBytes as bp0}from"crypto";import{existsSync as d40}from"fs";function uO(Z=process.env){let J={};for(let[X,W]of Object.entries(Z))if(W!==void 0)J[X]=W;if(process.platform!=="linux")return J;let $=`/run/user/${process.getuid?.()??0}`,Y=`${$}/bus`;if(!J.XDG_RUNTIME_DIR&&d40($))J.XDG_RUNTIME_DIR=$;if(!J.DBUS_SESSION_BUS_ADDRESS&&d40(Y))J.DBUS_SESSION_BUS_ADDRESS=`unix:path=${Y}`;return J}function AG(){if(process.platform!=="linux")return!1;if(oY!==void 0)return oY;if(Bun.spawnSync(["which","systemd-run"],{stdout:"ignore",stderr:"ignore"}).exitCode!==0)return oY=!1,!1;return oY=Bun.spawnSync(["systemctl","--user","show-environment"],{stdout:"ignore",stderr:"ignore",env:uO()}).exitCode===0,oY}function vp0(Z){return Z.replace(/[^A-Za-z0-9:_.\-]/g,"_")}function NG(Z,J,Q,$){let Y=bp0(4).toString("hex"),W=`the-link-${vp0(Q)}-${Y}.scope`,K=["systemd-run","--user","--scope","--quiet",`--unit=${W}`,"--slice=app.slice",`--description=the-link watchdog for ${Q}`];K.push("bash",Z);let z=Bun.spawn(K,{cwd:J,env:uO(),detached:!0,stdio:["ignore","ignore","ignore"]});return z.unref(),xp0(W,z.pid)}function xp0(Z,J){try{let Q=Bun.spawnSync(["systemctl","--user","show","--property=MainPID","--value",Z],{stdout:"pipe",stderr:"ignore",env:uO()});if(Q.exitCode===0){let $=Q.stdout.toString().trim(),Y=parseInt($,10);if(!isNaN(Y)&&Y>0)return Y}c40.warn(`[spawn-isolated] MainPID query for ${Z} failed or returned 0; falling back to proc.pid ${J}`)}catch(Q){c40.warn(`[spawn-isolated] MainPID query threw: ${Q}; falling back to proc.pid ${J}`)}return J}function m40(Z,J,Q){let $=Bun.spawn(["bash",Z],{cwd:J,detached:!0,stdio:["ignore","ignore","ignore"]});return $.unref(),$.pid}var c40,oY;var lO=q(()=>{g1();c40=F0("spawn-isolated")});var o40={};q1(o40,{maybeSelfMigrateToSystemdScope:()=>up0,maybeRewriteSystemdUnit:()=>lp0,buildSystemdUnitTemplate:()=>i40});import{existsSync as pO,readFileSync as iO,writeFileSync as p40,renameSync as gp0,unlinkSync as dp0}from"fs";import{join as oO}from"path";function i40(Z){let J=`"${Z}"`;return`[Unit]
|
package/package.json
CHANGED