@vibecontrols/vibe-plugin-tool-ssh 2026.529.1 → 2026.530.2

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
@@ -1,12 +1,5 @@
1
1
  # @vibecontrols/vibe-plugin-ssh
2
2
 
3
- <!-- VIBECONTROLS_OSS_HEADER_START -->
4
-
5
- > **License**: MIT — see [LICENSE](./LICENSE).
6
- > **Note**: This plugin is open source. The `@vibecontrols/agent` runtime that loads it is **not** open source — it is a proprietary product of Burdenoff Consultancy Services Pvt. Ltd. See [vibecontrols.com](https://vibecontrols.com) for the agent.
7
-
8
- <!-- VIBECONTROLS_OSS_HEADER_END -->
9
-
10
3
  SSH connections & port forwarding plugin for [VibeControls Agent](https://www.npmjs.com/package/@vibecontrols/agent).
11
4
 
12
5
  ## Platform support
@@ -85,13 +78,14 @@ vibe forward stop -i <id> # Stop forwarding
85
78
 
86
79
  ---
87
80
 
88
- ## License
89
-
90
- Released under the [MIT License](./LICENSE).
81
+ ## About VibeControls
91
82
 
92
- Copyright (c) 2026 Burdenoff Consultancy Services Private Limited, Algoshred Technologies Private Limited, and all its sister companies.
83
+ **VibeControls** is the agentic engineering mission control for AI-native teams. Vibe-plugins extend the VibeControls agent with new providers, tools, sessions, tunnels, storage backends, and security stages.
93
84
 
94
- Maintainer: **Vignesh T.V** — <https://github.com/tvvignesh>
85
+ - Website: <https://vibecontrols.com>
86
+ - Documentation: <https://docs.vibecontrols.com>
87
+ - Plugin SDK: <https://github.com/algoshred/vibecontrols-plugin-sdk>
88
+ - All plugins: <https://github.com/algoshred?q=vibe-plugin-&type=all>
95
89
 
96
90
  ## Credits
97
91
 
@@ -99,17 +93,14 @@ This plugin builds on the following upstream open-source projects. All trademark
99
93
 
100
94
  - **OpenSSH** — <https://www.openssh.com/>
101
95
 
102
- ## About VibeControls
96
+ ## License
103
97
 
104
- **VibeControls** is the agentic engineering mission control for AI-native teams. Vibe-plugins extend the VibeControls agent with new providers, tools, sessions, tunnels, storage backends, and security stages.
98
+ Released under the [MIT License](./LICENSE).
105
99
 
106
- - Website: <https://vibecontrols.com>
107
- - Documentation: <https://docs.vibecontrols.com>
108
- - Plugin SDK: <https://github.com/algoshred/vibecontrols-plugin-sdk>
109
- - All plugins: <https://github.com/algoshred?q=vibe-plugin-&type=all>
100
+ Copyright (c) 2026 Burdenoff Consultancy Services Private Limited, Algoshred Technologies Private Limited, and all its sister companies.
110
101
 
111
- ## Important: agent is not open source
102
+ Maintainer: **Vignesh T.V** <https://github.com/tvvignesh>
112
103
 
113
- The `@vibecontrols/agent` runtime that loads and orchestrates these plugins is **closed source** and proprietary to Burdenoff Consultancy Services Pvt. Ltd. Only the plugin contract and the plugins themselves are released under MIT. If you want a fully self-hostable agent, please open an issue or contact the maintainer.
104
+ **Note**: this plugin is open source under MIT. The `@vibecontrols/agent` runtime that loads and orchestrates plugins is **closed source** and proprietary to Burdenoff Consultancy Services Pvt. Ltd. If you want a fully self-hostable agent, please open an issue or contact the maintainer.
114
105
 
115
106
  <!-- VIBECONTROLS_OSS_FOOTER_END -->
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ ${suffix}`}};var MAX_32BIT_INT=4294967296,MAX_32BIT_BIGINT=(()=>{try{return Func
16
16
  `,publicB64,cipherName=Buffer.from(encrypted?encrypted.cipherName:"none"),kdfName=Buffer.from(encrypted?encrypted.kdfName:"none"),kdfOptions=encrypted?encrypted.kdfOptions:Buffer.alloc(0),blockLen=encrypted?encrypted.cipher.blockLen:8,parsed=parseDERs(keyType,pub,priv),checkInt=randomBytes(4),commentBin=Buffer.from(comment),privBlobLen=8+parsed.priv.length+4+commentBin.length,padding=[];for(let i=1;(privBlobLen+padding.length)%blockLen;++i)padding.push(i&255);padding=Buffer.from(padding);let privBlob=Buffer.allocUnsafe(privBlobLen+padding.length),extra;{let pos=0;privBlob.set(checkInt,pos+=0),privBlob.set(checkInt,pos+=4),privBlob.set(parsed.priv,pos+=4),privBlob.writeUInt32BE(commentBin.length,pos+=parsed.priv.length),privBlob.set(commentBin,pos+=4),privBlob.set(padding,pos+=commentBin.length)}if(encrypted){let options={authTagLength:encrypted.cipher.authLen},cipher=createCipheriv(encrypted.cipher.sslName,encrypted.key,encrypted.iv,options);if(cipher.setAutoPadding(!1),privBlob=Buffer.concat([cipher.update(privBlob),cipher.final()]),encrypted.cipher.authLen>0)extra=cipher.getAuthTag();else extra=Buffer.alloc(0);encrypted.key.fill(0),encrypted.iv.fill(0)}else extra=Buffer.alloc(0);let magicBytes=Buffer.from("openssh-key-v1\x00"),privBin=Buffer.allocUnsafe(magicBytes.length+4+cipherName.length+4+kdfName.length+4+kdfOptions.length+4+4+parsed.pub.length+4+privBlob.length+extra.length);{let pos=0;privBin.set(magicBytes,pos+=0),privBin.writeUInt32BE(cipherName.length,pos+=magicBytes.length),privBin.set(cipherName,pos+=4),privBin.writeUInt32BE(kdfName.length,pos+=cipherName.length),privBin.set(kdfName,pos+=4),privBin.writeUInt32BE(kdfOptions.length,pos+=kdfName.length),privBin.set(kdfOptions,pos+=4),privBin.writeUInt32BE(1,pos+=kdfOptions.length),privBin.writeUInt32BE(parsed.pub.length,pos+=4),privBin.set(parsed.pub,pos+=4),privBin.writeUInt32BE(privBlob.length,pos+=parsed.pub.length),privBin.set(privBlob,pos+=4),privBin.set(extra,pos+=privBlob.length)}{let b64=privBin.base64Slice(0,privBin.length),formatted=b64.replace(/.{64}/g,`$&
17
17
  `);if(b64.length&63)formatted+=`
18
18
  `;privateB64+=formatted}{let b64=parsed.pub.base64Slice(0,parsed.pub.length);publicB64=`${parsed.sshName} ${b64}${comment?` ${comment}`:""}`}return privateB64+=`-----END OPENSSH PRIVATE KEY-----
19
- `,{private:privateB64,public:publicB64}}default:throw Error("Invalid output key format")}}function noop(){}module.exports={generateKeyPair:(keyType,opts,cb)=>{if(typeof opts==="function")cb=opts,opts=void 0;if(typeof cb!=="function")cb=noop;let args=makeArgs(keyType,opts);generateKeyPair_(...args,(err,pub,priv)=>{if(err)return cb(err);let ret;try{ret=convertKeys(args[0],pub,priv,opts)}catch(ex){return cb(ex)}cb(null,ret)})},generateKeyPairSync:(keyType,opts)=>{let args=makeArgs(keyType,opts),{publicKey:pub,privateKey:priv}=generateKeyPairSync_(...args);return convertKeys(args[0],pub,priv,opts)}}});var require_lib3=__commonJS((exports,module)=>{var{AgentProtocol,BaseAgent,createAgent,CygwinAgent,OpenSSHAgent,PageantAgent}=require_agent(),{SSHTTPAgent:HTTPAgent,SSHTTPSAgent:HTTPSAgent}=require_http_agents(),{parseKey}=require_keyParser(),{flagsToString,OPEN_MODE,STATUS_CODE,stringToFlags}=require_SFTP();module.exports={AgentProtocol,BaseAgent,createAgent,Client:require_client(),CygwinAgent,HTTPAgent,HTTPSAgent,OpenSSHAgent,PageantAgent,Server:require_server(),utils:{parseKey,...require_keygen(),sftp:{flagsToString,OPEN_MODE,STATUS_CODE,stringToFlags}}}});import{homedir}from"os";import{resolve}from"path";function expandPath(p){if(!p)return p;if(p==="~")return homedir();if(p.startsWith("~/"))return resolve(homedir(),p.slice(2));return resolve(p)}var init_expand_path=()=>{};var exports_ssh={};__export(exports_ssh,{createSSHRoutes:()=>createSSHRoutes});import{Elysia as Elysia2}from"elysia";async function getAllConnections(storage){let raw=await storage.get("ssh","connections");if(!raw)return[];return JSON.parse(raw)}async function getConnectionById(storage,id){return(await getAllConnections(storage)).find((c)=>c.id===id)}async function saveConnections(storage,connections){await storage.set("ssh","connections",JSON.stringify(connections))}function sanitise(conn){let{password:_pw,privateKeyPath:_pk,...rest}=conn;return{...rest,privateKeyPath:conn.privateKeyPath?"***":void 0}}async function buildConnectConfig(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function createSSHRoutes(hostServices){let{storage,eventBus}=hostServices;return new Elysia2({prefix:"/api/ssh"}).get("/connections",async()=>{return{connections:(await getAllConnections(storage)).map(sanitise)}}).post("/connections",async({body,set})=>{let{serverName,host,port=22,username,privateKeyPath,password}=body;try{let connections=await getAllConnections(storage),newConn={id:globalThis.crypto.randomUUID(),serverName,host,port,username,privateKeyPath,password,createdAt:new Date().toISOString()};return connections.push(newConn),await saveConnections(storage,connections),{connection:sanitise(newConn)}}catch(error){return set.status=500,{error:"Failed to create SSH connection",details:error instanceof Error?error.message:"Unknown error"}}}).post("/execute",async({body,set})=>{let{connectionId,command,workingDirectory}=body;try{let connectionConfig=await getConnectionById(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let conn=new import_ssh2.Client,output="",errorOutput="",connectConfig=await buildConnectConfig(connectionConfig);return new Promise((resolve2)=>{conn.on("ready",()=>{let fullCommand=workingDirectory?`cd ${workingDirectory} && ${command}`:command;conn.exec(fullCommand,(err,stream)=>{if(err){conn.end(),set.status=500,resolve2({error:"Failed to execute command",details:err.message});return}stream.on("close",(code)=>{conn.end(),resolve2({output,errorOutput,exitCode:code,success:code===0})}),stream.on("data",(data)=>{if(output+=data.toString(),eventBus)eventBus.emit("ssh:output",{connectionId,data:data.toString(),type:"stdout"});else console.log(`[ssh:stdout] ${connectionId}: ${data.toString().trimEnd()}`)}),stream.stderr.on("data",(data)=>{if(errorOutput+=data.toString(),eventBus)eventBus.emit("ssh:output",{connectionId,data:data.toString(),type:"stderr"});else console.error(`[ssh:stderr] ${connectionId}: ${data.toString().trimEnd()}`)})})}),conn.on("error",(err)=>{set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),conn.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to execute SSH command",details:error instanceof Error?error.message:"Unknown error"}}}).post("/test/:connectionId",async({params,set})=>{let{connectionId}=params;try{let connectionConfig=await getConnectionById(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let conn=new import_ssh2.Client,testConfig=await buildConnectConfig(connectionConfig);return new Promise((resolve2)=>{conn.on("ready",()=>{conn.end(),resolve2({success:!0,message:"Connection successful"})}),conn.on("error",(err)=>{set.status=500,resolve2({success:!1,error:"Connection failed",details:err.message})}),conn.connect({...testConfig,readyTimeout:1e4})})}catch(error){return set.status=500,{success:!1,error:"Failed to test connection",details:error instanceof Error?error.message:"Unknown error"}}}).get("/connections/:id",async({params,set})=>{let conn=await getConnectionById(storage,params.id);if(!conn)return set.status=404,{error:"Connection not found"};return{connection:sanitise(conn)}}).put("/connections/:id",async({params,body,set})=>{let{id}=params,patch=body;try{let connections=await getAllConnections(storage),idx=connections.findIndex((c)=>c.id===id);if(idx===-1)return set.status=404,{error:"Connection not found"};let conn=connections[idx];if(patch.serverName!==void 0)conn.serverName=patch.serverName;if(patch.host!==void 0)conn.host=patch.host;if(patch.port!==void 0)conn.port=patch.port;if(patch.username!==void 0)conn.username=patch.username;if(patch.privateKeyPath!==void 0)conn.privateKeyPath=patch.privateKeyPath;if(patch.password!==void 0)conn.password=patch.password;return await saveConnections(storage,connections),{connection:sanitise(connections[idx])}}catch(error){return set.status=500,{error:"Failed to update connection",details:error instanceof Error?error.message:"Unknown error"}}}).delete("/connections/:id",async({params,set})=>{let{id}=params;try{let connections=await getAllConnections(storage),idx=connections.findIndex((c)=>c.id===id);if(idx===-1)return set.status=404,{error:"Connection not found"};return connections.splice(idx,1),await saveConnections(storage,connections),{success:!0}}catch(error){return set.status=500,{error:"Failed to delete connection",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh2;var init_ssh=__esm(()=>{init_expand_path();import_ssh2=__toESM(require_lib3(),1)});var exports_port_forward={};__export(exports_port_forward,{createPortForwardRoutes:()=>createPortForwardRoutes,cleanupAllTunnels:()=>cleanupAllTunnels});import{Elysia as Elysia3}from"elysia";import{createServer}from"net";async function getConnectionById2(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function getConnectionByName(storage,serverName){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.serverName===serverName)}async function getAllPortForwards(storage){let raw=await storage.get("ssh","port-forwards");if(!raw)return[];return JSON.parse(raw)}async function getPortForwardById(storage,id){return(await getAllPortForwards(storage)).find((pf)=>pf.id===id)}async function savePortForwards(storage,forwards){await storage.set("ssh","port-forwards",JSON.stringify(forwards))}async function updatePortForward(storage,id,patch){let all=await getAllPortForwards(storage),idx=all.findIndex((pf)=>pf.id===id);if(idx!==-1)all[idx]={...all[idx],...patch},await savePortForwards(storage,all)}async function deletePortForwardById(storage,id){let filtered=(await getAllPortForwards(storage)).filter((pf)=>pf.id!==id);await savePortForwards(storage,filtered)}async function buildConnectConfig2(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function cleanupAllTunnels(){for(let[,{client,server}]of activeConnections)server.close(),client.end();activeConnections.clear()}function createPortForwardRoutes(hostServices){let{storage,eventBus}=hostServices;return new Elysia3({prefix:"/api/port-forward"}).get("/",async()=>{return{portForwards:await getAllPortForwards(storage)}}).post("/",async({body,set})=>{let{localPort,remoteHost,remotePort,connectionId}=body;try{if((await getAllPortForwards(storage)).find((pf)=>pf.localPort===localPort&&pf.status==="active"))return set.status=409,{error:"Local port is already in use"};let connectionConfig=await getConnectionById2(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let newPf={id:globalThis.crypto.randomUUID(),localPort,remoteHost,remotePort,serverName:connectionConfig.serverName,connectionId,status:"inactive",createdAt:new Date().toISOString()},all=await getAllPortForwards(storage);return all.push(newPf),await savePortForwards(storage,all),{portForward:newPf}}catch(error){return set.status=500,{error:"Failed to create port forward",details:error instanceof Error?error.message:"Unknown error"}}}).post("/:id/start",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status==="active")return set.status=400,{error:"Port forward is already active"};let connectionConfig=portForward.connectionId?await getConnectionById2(storage,portForward.connectionId):await getConnectionByName(storage,portForward.serverName);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let sshClient=new import_ssh22.Client,connectConfig=await buildConnectConfig2(connectionConfig),server=createServer((localSocket)=>{sshClient.forwardOut("localhost",portForward.localPort,portForward.remoteHost,portForward.remotePort,(err,stream)=>{if(err){localSocket.end(),console.error("Forward error:",err);return}localSocket.pipe(stream).pipe(localSocket),localSocket.on("close",()=>{stream.end()}),stream.on("close",()=>{localSocket.end()})})});return new Promise((resolve2)=>{sshClient.on("ready",()=>{server.listen(portForward.localPort,()=>{if(activeConnections.set(id,{client:sshClient,server}),updatePortForward(storage,id,{status:"active"}),eventBus)eventBus.emit("portforward:started",{id,localPort:portForward.localPort});else console.log(`[port-forward] Started tunnel ${id} on localhost:${portForward.localPort}`);resolve2({success:!0,message:`Port forwarding started on localhost:${portForward.localPort}`})}),server.on("error",(err)=>{sshClient.end(),set.status=500,resolve2({error:"Failed to start local server",details:err.message})})}),sshClient.on("error",(err)=>{set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),sshClient.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to start port forward",details:error instanceof Error?error.message:"Unknown error"}}}).post("/:id/stop",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status!=="active")return set.status=400,{error:"Port forward is not active"};let active=activeConnections.get(id);if(active)active.server.close(),active.client.end(),activeConnections.delete(id);if(await updatePortForward(storage,id,{status:"inactive"}),eventBus)eventBus.emit("portforward:stopped",{id,localPort:portForward.localPort});else console.log(`[port-forward] Stopped tunnel ${id} (localhost:${portForward.localPort})`);return{success:!0}}catch(error){return set.status=500,{error:"Failed to stop port forward",details:error instanceof Error?error.message:"Unknown error"}}}).delete("/:id",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status==="active"){let active=activeConnections.get(id);if(active)active.server.close(),active.client.end(),activeConnections.delete(id)}return await deletePortForwardById(storage,id),{success:!0}}catch(error){return set.status=500,{error:"Failed to delete port forward",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh22,activeConnections;var init_port_forward=__esm(()=>{init_expand_path();import_ssh22=__toESM(require_lib3(),1),activeConnections=new Map});var exports_remote_terminal={};__export(exports_remote_terminal,{listTerminalSessions:()=>listTerminalSessions,getTerminalInfo:()=>getTerminalInfo,createRemoteTerminalRoutes:()=>createRemoteTerminalRoutes,cleanupAllTerminals:()=>cleanupAllTerminals});import{Elysia as Elysia4}from"elysia";import{createServer as createServer2}from"net";async function getConnectionById3(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function persistSessions(storage){let sessions=Array.from(activeTerminals.values()).map((t)=>t.session);await storage.set("ssh","terminal-sessions",JSON.stringify(sessions))}async function buildConnectConfig3(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function sshExec(client,command){return new Promise((resolve2,reject)=>{client.exec(command,(err,stream)=>{if(err)return reject(err);let stdout="",stderr="";stream.on("data",(data)=>{stdout+=data.toString()}),stream.stderr.on("data",(data)=>{stderr+=data.toString()}),stream.on("close",(code)=>{resolve2({stdout:stdout.trim(),stderr:stderr.trim(),code})})})})}async function findFreeRemotePort(client,start=7881,end=8080){for(let port=start;port<=end;port++){let{code}=await sshExec(client,`ss -tlnp 2>/dev/null | grep -q ':${port} ' && echo taken || echo free`),{stdout}=await sshExec(client,`ss -tlnp 2>/dev/null | grep -q ':${port} ' && echo taken || echo free`);if(stdout==="free")return port}throw Error(`No free port found on remote server in range ${start}-${end}`)}function findFreeLocalPort(start=7681,end=7880){return new Promise((resolve2,reject)=>{let current=start;function tryPort(){if(current>end)return reject(Error(`No free local port found in range ${start}-${end}`));for(let[,t]of activeTerminals)if(t.session.localPort===current)return current++,tryPort();let srv=createServer2();srv.once("error",()=>{current++,tryPort()}),srv.once("listening",()=>{srv.close(()=>resolve2(current))}),srv.listen(current,"127.0.0.1")}tryPort()})}async function ensureTtydInstalled(client){let{code}=await sshExec(client,"which ttyd");if(code===0)return!0;let{stdout:osRelease}=await sshExec(client,"cat /etc/os-release 2>/dev/null || echo unknown"),installCmd;if(osRelease.includes("debian")||osRelease.includes("ubuntu")||osRelease.includes("ID=debian")||osRelease.includes("ID=ubuntu"))installCmd="sudo apt-get update -qq && sudo apt-get install -y -qq ttyd 2>/dev/null";else if(osRelease.includes("centos")||osRelease.includes("fedora")||osRelease.includes("rhel")||osRelease.includes("ID=centos")||osRelease.includes("ID=fedora"))installCmd="sudo yum install -y -q ttyd 2>/dev/null";else if(osRelease.includes("alpine")||osRelease.includes("ID=alpine"))installCmd="sudo apk add --quiet ttyd 2>/dev/null";else{let{stdout:arch}=await sshExec(client,"uname -m");installCmd=`curl -fsSL "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.${{x86_64:"x86_64",aarch64:"aarch64",arm64:"aarch64"}[arch]||"x86_64"}" -o /tmp/ttyd && chmod +x /tmp/ttyd && sudo mv /tmp/ttyd /usr/local/bin/ttyd`}if((await sshExec(client,installCmd)).code!==0){let{code:retryCode}=await sshExec(client,installCmd.replace(/sudo /g,""));return retryCode===0}return!0}function cleanupAllTerminals(){for(let[id,terminal]of activeTerminals){try{if(terminal.sshTunnelProc)terminal.sshTunnelProc.kill()}catch{}try{terminal.sshClient.end()}catch{}activeTerminals.delete(id)}}function getTerminalInfo(sessionId){let terminal=activeTerminals.get(sessionId);if(!terminal||terminal.session.status!=="active")return null;return{url:`http://127.0.0.1:${terminal.session.localPort}`,port:terminal.session.localPort,pid:process.pid}}function listTerminalSessions(){return Array.from(activeTerminals.values()).map((t)=>t.session)}function createRemoteTerminalRoutes(hostServices){let{storage,broadcast}=hostServices;return new Elysia4({prefix:"/api/ssh/terminal"}).get("/sessions",()=>{return{sessions:listTerminalSessions()}}).get("/sessions/:id",({params,set})=>{let terminal=activeTerminals.get(params.id);if(!terminal)return set.status=404,{error:"Terminal session not found"};return{session:terminal.session}}).post("/start",async({body,set})=>{let{connectionId,shell="bash"}=body;try{let connConfig=await getConnectionById3(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let sessionId=globalThis.crypto.randomUUID(),session={id:sessionId,connectionId,remotePort:0,localPort:0,remotePid:null,shell,status:"starting",startedAt:new Date().toISOString()},sshClient=new import_ssh23.Client,connectConfig=await buildConnectConfig3(connConfig);return new Promise((resolve2)=>{sshClient.on("error",(err)=>{session.status="error",session.error=err.message,set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),sshClient.on("ready",async()=>{try{if(!await ensureTtydInstalled(sshClient))return sshClient.end(),session.status="error",session.error="Failed to install ttyd on remote server",set.status=500,resolve2({error:"ttyd is not installed on the remote server and auto-install failed"});let remotePort=await findFreeRemotePort(sshClient);session.remotePort=remotePort;let{stdout:pidOutput,code:startCode}=await sshExec(sshClient,`nohup ttyd --writable --port ${remotePort} ${shell} > /dev/null 2>&1 & echo $!`);if(startCode!==0||!pidOutput)return sshClient.end(),session.status="error",session.error="Failed to start ttyd on remote",set.status=500,resolve2({error:"Failed to start ttyd on remote"});let remotePid=parseInt(pidOutput,10);session.remotePid=remotePid,await new Promise((r)=>setTimeout(r,1500));let{stdout:listenCheck}=await sshExec(sshClient,`ss -tlnp 2>/dev/null | grep ':${remotePort} ' || echo NOT_LISTENING`);if(listenCheck.includes("NOT_LISTENING"))await new Promise((r)=>setTimeout(r,2000));let localPort=await findFreeLocalPort();session.localPort=localPort;try{let sshArgs=["-N","-L",`${localPort}:127.0.0.1:${remotePort}`,"-o","StrictHostKeyChecking=accept-new","-o","ServerAliveInterval=30","-o","ExitOnForwardFailure=yes","-p",String(connConfig.port)];if(connConfig.privateKeyPath)sshArgs.push("-i",expandPath(connConfig.privateKeyPath));sshArgs.push(`${connConfig.username}@${connConfig.host}`);let sshProc=Bun.spawn(["ssh",...sshArgs],{stdout:"ignore",stderr:"pipe"});await new Promise((r)=>setTimeout(r,2000));let checkSrv=createServer2();if(!await new Promise((res)=>{checkSrv.once("error",()=>res(!0)),checkSrv.once("listening",()=>{checkSrv.close(),res(!1)}),checkSrv.listen(localPort,"127.0.0.1")}))await new Promise((r)=>setTimeout(r,2000));if(session.status="active",activeTerminals.set(sessionId,{sshClient,sshTunnelProc:sshProc,session}),persistSessions(storage),broadcast)broadcast("ssh:terminal:started",{sessionId,connectionId,localPort,remotePort,host:connConfig.host});resolve2({sessionId,connectionId,localPort,remotePort,host:connConfig.host,status:"active"})}catch(bindErr){sshExec(sshClient,`kill ${remotePid} 2>/dev/null`),sshClient.end(),session.status="error",session.error=bindErr instanceof Error?bindErr.message:"Port bind failed",set.status=500,resolve2({error:"Failed to bind local port",details:session.error})}}catch(err){sshClient.end(),session.status="error",session.error=err instanceof Error?err.message:"Unknown error",set.status=500,resolve2({error:"Failed to start remote terminal",details:session.error})}}),sshClient.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to start remote terminal",details:error instanceof Error?error.message:"Unknown error"}}}).post("/stop",async({body,set})=>{let{sessionId}=body,terminal=activeTerminals.get(sessionId);if(!terminal)return set.status=404,{error:"Terminal session not found"};try{if(terminal.session.status="stopping",terminal.session.remotePid)await sshExec(terminal.sshClient,`kill ${terminal.session.remotePid} 2>/dev/null`).catch(()=>{});if(terminal.sshTunnelProc)terminal.sshTunnelProc.kill();if(terminal.sshClient.end(),terminal.session.status="stopped",activeTerminals.delete(sessionId),persistSessions(storage),broadcast)broadcast("ssh:terminal:stopped",{sessionId,connectionId:terminal.session.connectionId});return{success:!0,sessionId}}catch(error){return set.status=500,{error:"Failed to stop terminal session",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh23,activeTerminals;var init_remote_terminal=__esm(()=>{init_expand_path();import_ssh23=__toESM(require_lib3(),1),activeTerminals=new Map});var exports_remote_agent_install={};__export(exports_remote_agent_install,{createRemoteAgentInstallRoutes:()=>createRemoteAgentInstallRoutes});import{Elysia as Elysia5}from"elysia";import{homedir as homedir2,tmpdir}from"os";import{join as joinPath}from"path";async function getConnectionById4(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function buildConnectConfig4(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function sshExec2(client,command,timeoutMs=60000){return new Promise((resolve2,reject)=>{let timer=setTimeout(()=>{reject(Error(`Command timed out after ${timeoutMs}ms`))},timeoutMs);client.exec(command,(err,stream)=>{if(err)return clearTimeout(timer),reject(err);let stdout="",stderr="";stream.on("data",(data)=>{stdout+=data.toString()}),stream.stderr.on("data",(data)=>{stderr+=data.toString()}),stream.on("close",(code)=>{clearTimeout(timer),resolve2({stdout:stdout.trim(),stderr:stderr.trim(),code})})})})}async function runInstallation(job,connConfig,agentPort,hostServices,options={}){let{broadcast}=hostServices,sshClient=new import_ssh24.Client,registryUrl=hostServices.getPluginRegistry?.()??DEFAULT_REGISTRY;function emitProgress(){if(broadcast)broadcast("ssh:install:progress",{...job})}function updateStep(stepIdx,status,message){if(job.steps[stepIdx].status=status,message)job.steps[stepIdx].message=message;job.currentStep=stepIdx,emitProgress()}function fail(stepIdx,error){if(updateStep(stepIdx,"failed",error),job.status="failed",job.error=error,job.completedAt=new Date().toISOString(),broadcast)broadcast("ssh:install:failed",{...job});sshClient.end()}try{let connectConfig=await buildConnectConfig4(connConfig);updateStep(0,"running"),await new Promise((resolve2,reject)=>{sshClient.on("ready",()=>resolve2()),sshClient.on("error",(err)=>reject(err)),sshClient.connect({...connectConfig,readyTimeout:15000})}),updateStep(0,"completed","Connected successfully"),updateStep(1,"running");let{stdout:osInfo}=await sshExec2(sshClient,"uname -s && uname -m"),[osName,arch]=osInfo.split(`
19
+ `,{private:privateB64,public:publicB64}}default:throw Error("Invalid output key format")}}function noop(){}module.exports={generateKeyPair:(keyType,opts,cb)=>{if(typeof opts==="function")cb=opts,opts=void 0;if(typeof cb!=="function")cb=noop;let args=makeArgs(keyType,opts);generateKeyPair_(...args,(err,pub,priv)=>{if(err)return cb(err);let ret;try{ret=convertKeys(args[0],pub,priv,opts)}catch(ex){return cb(ex)}cb(null,ret)})},generateKeyPairSync:(keyType,opts)=>{let args=makeArgs(keyType,opts),{publicKey:pub,privateKey:priv}=generateKeyPairSync_(...args);return convertKeys(args[0],pub,priv,opts)}}});var require_lib3=__commonJS((exports,module)=>{var{AgentProtocol,BaseAgent,createAgent,CygwinAgent,OpenSSHAgent,PageantAgent}=require_agent(),{SSHTTPAgent:HTTPAgent,SSHTTPSAgent:HTTPSAgent}=require_http_agents(),{parseKey}=require_keyParser(),{flagsToString,OPEN_MODE,STATUS_CODE,stringToFlags}=require_SFTP();module.exports={AgentProtocol,BaseAgent,createAgent,Client:require_client(),CygwinAgent,HTTPAgent,HTTPSAgent,OpenSSHAgent,PageantAgent,Server:require_server(),utils:{parseKey,...require_keygen(),sftp:{flagsToString,OPEN_MODE,STATUS_CODE,stringToFlags}}}});import{homedir}from"os";import{resolve}from"path";function expandPath(p){if(!p)return p;if(p==="~")return homedir();if(p.startsWith("~/"))return resolve(homedir(),p.slice(2));return resolve(p)}var init_expand_path=()=>{};var exports_ssh={};__export(exports_ssh,{createSSHRoutes:()=>createSSHRoutes});import{Elysia as Elysia2}from"elysia";async function getAllConnections(storage){let raw=await storage.get("ssh","connections");if(!raw)return[];return JSON.parse(raw)}async function getConnectionById(storage,id){return(await getAllConnections(storage)).find((c)=>c.id===id)}async function saveConnections(storage,connections){await storage.set("ssh","connections",JSON.stringify(connections))}function sanitise(conn){let{password:_pw,privateKeyPath:_pk,...rest}=conn;return{...rest,privateKeyPath:conn.privateKeyPath?"***":void 0}}async function buildConnectConfig(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function createSSHRoutes(hostServices){let{storage,eventBus}=hostServices;return new Elysia2({prefix:"/api/ssh"}).get("/connections",async()=>{return{connections:(await getAllConnections(storage)).map(sanitise)}}).post("/connections",async({body,set})=>{let{serverName,host,port=22,username,privateKeyPath,password}=body;try{let connections=await getAllConnections(storage),newConn={id:globalThis.crypto.randomUUID(),serverName,host,port,username,privateKeyPath,password,createdAt:new Date().toISOString()};return connections.push(newConn),await saveConnections(storage,connections),{connection:sanitise(newConn)}}catch(error){return set.status=500,{error:"Failed to create SSH connection",details:error instanceof Error?error.message:"Unknown error"}}}).post("/execute",async({body,set})=>{let{connectionId,command,workingDirectory}=body;try{let connectionConfig=await getConnectionById(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let conn=new import_ssh2.Client,output="",errorOutput="",connectConfig=await buildConnectConfig(connectionConfig);return new Promise((resolve2)=>{conn.on("ready",()=>{let fullCommand=workingDirectory?`cd ${workingDirectory} && ${command}`:command;conn.exec(fullCommand,(err,stream)=>{if(err){conn.end(),set.status=500,resolve2({error:"Failed to execute command",details:err.message});return}stream.on("close",(code)=>{conn.end(),resolve2({output,errorOutput,exitCode:code,success:code===0})}),stream.on("data",(data)=>{if(output+=data.toString(),eventBus)eventBus.emit("ssh:output",{connectionId,data:data.toString(),type:"stdout"});else console.log(`[ssh:stdout] ${connectionId}: ${data.toString().trimEnd()}`)}),stream.stderr.on("data",(data)=>{if(errorOutput+=data.toString(),eventBus)eventBus.emit("ssh:output",{connectionId,data:data.toString(),type:"stderr"});else console.error(`[ssh:stderr] ${connectionId}: ${data.toString().trimEnd()}`)})})}),conn.on("error",(err)=>{set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),conn.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to execute SSH command",details:error instanceof Error?error.message:"Unknown error"}}}).post("/test/:connectionId",async({params,set})=>{let{connectionId}=params;try{let connectionConfig=await getConnectionById(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let conn=new import_ssh2.Client,testConfig=await buildConnectConfig(connectionConfig);return new Promise((resolve2)=>{conn.on("ready",()=>{conn.end(),resolve2({success:!0,message:"Connection successful"})}),conn.on("error",(err)=>{set.status=500,resolve2({success:!1,error:"Connection failed",details:err.message})}),conn.connect({...testConfig,readyTimeout:1e4})})}catch(error){return set.status=500,{success:!1,error:"Failed to test connection",details:error instanceof Error?error.message:"Unknown error"}}}).get("/connections/:id",async({params,set})=>{let conn=await getConnectionById(storage,params.id);if(!conn)return set.status=404,{error:"Connection not found"};return{connection:sanitise(conn)}}).put("/connections/:id",async({params,body,set})=>{let{id}=params,patch=body;try{let connections=await getAllConnections(storage),idx=connections.findIndex((c)=>c.id===id);if(idx===-1)return set.status=404,{error:"Connection not found"};let conn=connections[idx];if(patch.serverName!==void 0)conn.serverName=patch.serverName;if(patch.host!==void 0)conn.host=patch.host;if(patch.port!==void 0)conn.port=patch.port;if(patch.username!==void 0)conn.username=patch.username;if(patch.privateKeyPath!==void 0)conn.privateKeyPath=patch.privateKeyPath;if(patch.password!==void 0)conn.password=patch.password;return await saveConnections(storage,connections),{connection:sanitise(connections[idx])}}catch(error){return set.status=500,{error:"Failed to update connection",details:error instanceof Error?error.message:"Unknown error"}}}).delete("/connections/:id",async({params,set})=>{let{id}=params;try{let connections=await getAllConnections(storage),idx=connections.findIndex((c)=>c.id===id);if(idx===-1)return set.status=404,{error:"Connection not found"};return connections.splice(idx,1),await saveConnections(storage,connections),{success:!0}}catch(error){return set.status=500,{error:"Failed to delete connection",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh2;var init_ssh=__esm(()=>{init_expand_path();import_ssh2=__toESM(require_lib3(),1)});var exports_port_forward={};__export(exports_port_forward,{createPortForwardRoutes:()=>createPortForwardRoutes,cleanupAllTunnels:()=>cleanupAllTunnels});import{Elysia as Elysia3}from"elysia";import{createServer}from"net";async function getConnectionById2(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function getConnectionByName(storage,serverName){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.serverName===serverName)}async function getAllPortForwards(storage){let raw=await storage.get("ssh","port-forwards");if(!raw)return[];return JSON.parse(raw)}async function getPortForwardById(storage,id){return(await getAllPortForwards(storage)).find((pf)=>pf.id===id)}async function savePortForwards(storage,forwards){await storage.set("ssh","port-forwards",JSON.stringify(forwards))}async function updatePortForward(storage,id,patch){let all=await getAllPortForwards(storage),idx=all.findIndex((pf)=>pf.id===id);if(idx!==-1)all[idx]={...all[idx],...patch},await savePortForwards(storage,all)}async function deletePortForwardById(storage,id){let filtered=(await getAllPortForwards(storage)).filter((pf)=>pf.id!==id);await savePortForwards(storage,filtered)}async function buildConnectConfig2(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function cleanupAllTunnels(){for(let[,{client,server}]of activeConnections)server.close(),client.end();activeConnections.clear()}function createPortForwardRoutes(hostServices){let{storage,eventBus}=hostServices;return new Elysia3({prefix:"/api/port-forward"}).get("/",async()=>{return{portForwards:await getAllPortForwards(storage)}}).post("/",async({body,set})=>{let{localPort,remoteHost,remotePort,connectionId}=body;try{if((await getAllPortForwards(storage)).find((pf)=>pf.localPort===localPort&&pf.status==="active"))return set.status=409,{error:"Local port is already in use"};let connectionConfig=await getConnectionById2(storage,connectionId);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let newPf={id:globalThis.crypto.randomUUID(),localPort,remoteHost,remotePort,serverName:connectionConfig.serverName,connectionId,status:"inactive",createdAt:new Date().toISOString()},all=await getAllPortForwards(storage);return all.push(newPf),await savePortForwards(storage,all),{portForward:newPf}}catch(error){return set.status=500,{error:"Failed to create port forward",details:error instanceof Error?error.message:"Unknown error"}}}).post("/:id/start",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status==="active")return set.status=400,{error:"Port forward is already active"};let connectionConfig=portForward.connectionId?await getConnectionById2(storage,portForward.connectionId):await getConnectionByName(storage,portForward.serverName);if(!connectionConfig)return set.status=404,{error:"SSH connection not found"};let sshClient=new import_ssh22.Client,connectConfig=await buildConnectConfig2(connectionConfig),server=createServer((localSocket)=>{sshClient.forwardOut("localhost",portForward.localPort,portForward.remoteHost,portForward.remotePort,(err,stream)=>{if(err){localSocket.end(),console.error("Forward error:",err);return}localSocket.pipe(stream).pipe(localSocket),localSocket.on("close",()=>{stream.end()}),stream.on("close",()=>{localSocket.end()})})});return new Promise((resolve2)=>{sshClient.on("ready",()=>{server.listen(portForward.localPort,()=>{if(activeConnections.set(id,{client:sshClient,server}),updatePortForward(storage,id,{status:"active"}),eventBus)eventBus.emit("portforward:started",{id,localPort:portForward.localPort});else console.log(`[port-forward] Started tunnel ${id} on localhost:${portForward.localPort}`);resolve2({success:!0,message:`Port forwarding started on localhost:${portForward.localPort}`})}),server.on("error",(err)=>{sshClient.end(),set.status=500,resolve2({error:"Failed to start local server",details:err.message})})}),sshClient.on("error",(err)=>{set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),sshClient.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to start port forward",details:error instanceof Error?error.message:"Unknown error"}}}).post("/:id/stop",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status!=="active")return set.status=400,{error:"Port forward is not active"};let active=activeConnections.get(id);if(active)active.server.close(),active.client.end(),activeConnections.delete(id);if(await updatePortForward(storage,id,{status:"inactive"}),eventBus)eventBus.emit("portforward:stopped",{id,localPort:portForward.localPort});else console.log(`[port-forward] Stopped tunnel ${id} (localhost:${portForward.localPort})`);return{success:!0}}catch(error){return set.status=500,{error:"Failed to stop port forward",details:error instanceof Error?error.message:"Unknown error"}}}).delete("/:id",async({params,set})=>{let{id}=params;try{let portForward=await getPortForwardById(storage,id);if(!portForward)return set.status=404,{error:"Port forward not found"};if(portForward.status==="active"){let active=activeConnections.get(id);if(active)active.server.close(),active.client.end(),activeConnections.delete(id)}return await deletePortForwardById(storage,id),{success:!0}}catch(error){return set.status=500,{error:"Failed to delete port forward",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh22,activeConnections;var init_port_forward=__esm(()=>{init_expand_path();import_ssh22=__toESM(require_lib3(),1),activeConnections=new Map});var exports_remote_terminal={};__export(exports_remote_terminal,{listTerminalSessions:()=>listTerminalSessions,getTerminalInfo:()=>getTerminalInfo,createRemoteTerminalRoutes:()=>createRemoteTerminalRoutes,cleanupAllTerminals:()=>cleanupAllTerminals});import{Elysia as Elysia4}from"elysia";import{createServer as createServer2}from"net";async function getConnectionById3(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function persistSessions(storage){let sessions=Array.from(activeTerminals.values()).map((t)=>t.session);await storage.set("ssh","terminal-sessions",JSON.stringify(sessions))}async function buildConnectConfig3(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function sshExec(client,command){return new Promise((resolve2,reject)=>{client.exec(command,(err,stream)=>{if(err)return reject(err);let stdout="",stderr="";stream.on("data",(data)=>{stdout+=data.toString()}),stream.stderr.on("data",(data)=>{stderr+=data.toString()}),stream.on("close",(code)=>{resolve2({stdout:stdout.trim(),stderr:stderr.trim(),code})})})})}async function findFreeRemotePort(client,start=7881,end=8080){for(let port=start;port<=end;port++){let{code}=await sshExec(client,`ss -tlnp 2>/dev/null | grep -q ':${port} ' && echo taken || echo free`),{stdout}=await sshExec(client,`ss -tlnp 2>/dev/null | grep -q ':${port} ' && echo taken || echo free`);if(stdout==="free")return port}throw Error(`No free port found on remote server in range ${start}-${end}`)}function findFreeLocalPort(start=7681,end=7880){return new Promise((resolve2,reject)=>{let current=start;function tryPort(){if(current>end)return reject(Error(`No free local port found in range ${start}-${end}`));for(let[,t]of activeTerminals)if(t.session.localPort===current)return current++,tryPort();let srv=createServer2();srv.once("error",()=>{current++,tryPort()}),srv.once("listening",()=>{srv.close(()=>resolve2(current))}),srv.listen(current,"127.0.0.1")}tryPort()})}async function ensureTtydInstalled(client){let{code}=await sshExec(client,"which ttyd");if(code===0)return!0;let{stdout:osRelease}=await sshExec(client,"cat /etc/os-release 2>/dev/null || echo unknown"),installCmd;if(osRelease.includes("debian")||osRelease.includes("ubuntu")||osRelease.includes("ID=debian")||osRelease.includes("ID=ubuntu"))installCmd="sudo apt-get update -qq && sudo apt-get install -y -qq ttyd 2>/dev/null";else if(osRelease.includes("centos")||osRelease.includes("fedora")||osRelease.includes("rhel")||osRelease.includes("ID=centos")||osRelease.includes("ID=fedora"))installCmd="sudo yum install -y -q ttyd 2>/dev/null";else if(osRelease.includes("alpine")||osRelease.includes("ID=alpine"))installCmd="sudo apk add --quiet ttyd 2>/dev/null";else{let{stdout:arch}=await sshExec(client,"uname -m");installCmd=`curl -fsSL "https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.${{x86_64:"x86_64",aarch64:"aarch64",arm64:"aarch64"}[arch]||"x86_64"}" -o /tmp/ttyd && chmod +x /tmp/ttyd && sudo mv /tmp/ttyd /usr/local/bin/ttyd`}if((await sshExec(client,installCmd)).code!==0){let{code:retryCode}=await sshExec(client,installCmd.replace(/sudo /g,""));return retryCode===0}return!0}function cleanupAllTerminals(){for(let[id,terminal]of activeTerminals){try{if(terminal.sshTunnelProc)terminal.sshTunnelProc.kill()}catch{}try{terminal.sshClient.end()}catch{}activeTerminals.delete(id)}}function getTerminalInfo(sessionId){let terminal=activeTerminals.get(sessionId);if(!terminal||terminal.session.status!=="active")return null;return{url:`http://127.0.0.1:${terminal.session.localPort}`,port:terminal.session.localPort,pid:process.pid,host:"127.0.0.1",wsPath:"/ws",subprotocols:["tty"]}}function listTerminalSessions(){return Array.from(activeTerminals.values()).map((t)=>t.session)}function createRemoteTerminalRoutes(hostServices){let{storage,broadcast}=hostServices;return new Elysia4({prefix:"/api/ssh/terminal"}).get("/sessions",()=>{return{sessions:listTerminalSessions()}}).get("/sessions/:id",({params,set})=>{let terminal=activeTerminals.get(params.id);if(!terminal)return set.status=404,{error:"Terminal session not found"};return{session:terminal.session}}).post("/start",async({body,set})=>{let{connectionId,shell="bash"}=body;try{let connConfig=await getConnectionById3(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let sessionId=globalThis.crypto.randomUUID(),session={id:sessionId,connectionId,remotePort:0,localPort:0,remotePid:null,shell,status:"starting",startedAt:new Date().toISOString()},sshClient=new import_ssh23.Client,connectConfig=await buildConnectConfig3(connConfig);return new Promise((resolve2)=>{sshClient.on("error",(err)=>{session.status="error",session.error=err.message,set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),sshClient.on("ready",async()=>{try{if(!await ensureTtydInstalled(sshClient))return sshClient.end(),session.status="error",session.error="Failed to install ttyd on remote server",set.status=500,resolve2({error:"ttyd is not installed on the remote server and auto-install failed"});let remotePort=await findFreeRemotePort(sshClient);session.remotePort=remotePort;let{stdout:pidOutput,code:startCode}=await sshExec(sshClient,`nohup ttyd --writable --port ${remotePort} ${shell} > /dev/null 2>&1 & echo $!`);if(startCode!==0||!pidOutput)return sshClient.end(),session.status="error",session.error="Failed to start ttyd on remote",set.status=500,resolve2({error:"Failed to start ttyd on remote"});let remotePid=parseInt(pidOutput,10);session.remotePid=remotePid,await new Promise((r)=>setTimeout(r,1500));let{stdout:listenCheck}=await sshExec(sshClient,`ss -tlnp 2>/dev/null | grep ':${remotePort} ' || echo NOT_LISTENING`);if(listenCheck.includes("NOT_LISTENING"))await new Promise((r)=>setTimeout(r,2000));let localPort=await findFreeLocalPort();session.localPort=localPort;try{let sshArgs=["-N","-L",`${localPort}:127.0.0.1:${remotePort}`,"-o","StrictHostKeyChecking=accept-new","-o","ServerAliveInterval=30","-o","ExitOnForwardFailure=yes","-p",String(connConfig.port)];if(connConfig.privateKeyPath)sshArgs.push("-i",expandPath(connConfig.privateKeyPath));sshArgs.push(`${connConfig.username}@${connConfig.host}`);let sshProc=Bun.spawn(["ssh",...sshArgs],{stdout:"ignore",stderr:"pipe"});await new Promise((r)=>setTimeout(r,2000));let checkSrv=createServer2();if(!await new Promise((res)=>{checkSrv.once("error",()=>res(!0)),checkSrv.once("listening",()=>{checkSrv.close(),res(!1)}),checkSrv.listen(localPort,"127.0.0.1")}))await new Promise((r)=>setTimeout(r,2000));if(session.status="active",activeTerminals.set(sessionId,{sshClient,sshTunnelProc:sshProc,session}),persistSessions(storage),broadcast)broadcast("ssh:terminal:started",{sessionId,connectionId,localPort,remotePort,host:connConfig.host});resolve2({sessionId,connectionId,localPort,remotePort,host:connConfig.host,status:"active"})}catch(bindErr){sshExec(sshClient,`kill ${remotePid} 2>/dev/null`),sshClient.end(),session.status="error",session.error=bindErr instanceof Error?bindErr.message:"Port bind failed",set.status=500,resolve2({error:"Failed to bind local port",details:session.error})}}catch(err){sshClient.end(),session.status="error",session.error=err instanceof Error?err.message:"Unknown error",set.status=500,resolve2({error:"Failed to start remote terminal",details:session.error})}}),sshClient.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to start remote terminal",details:error instanceof Error?error.message:"Unknown error"}}}).post("/stop",async({body,set})=>{let{sessionId}=body,terminal=activeTerminals.get(sessionId);if(!terminal)return set.status=404,{error:"Terminal session not found"};try{if(terminal.session.status="stopping",terminal.session.remotePid)await sshExec(terminal.sshClient,`kill ${terminal.session.remotePid} 2>/dev/null`).catch(()=>{});if(terminal.sshTunnelProc)terminal.sshTunnelProc.kill();if(terminal.sshClient.end(),terminal.session.status="stopped",activeTerminals.delete(sessionId),persistSessions(storage),broadcast)broadcast("ssh:terminal:stopped",{sessionId,connectionId:terminal.session.connectionId});return{success:!0,sessionId}}catch(error){return set.status=500,{error:"Failed to stop terminal session",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh23,activeTerminals;var init_remote_terminal=__esm(()=>{init_expand_path();import_ssh23=__toESM(require_lib3(),1),activeTerminals=new Map});var exports_remote_agent_install={};__export(exports_remote_agent_install,{createRemoteAgentInstallRoutes:()=>createRemoteAgentInstallRoutes});import{Elysia as Elysia5}from"elysia";import{homedir as homedir2,tmpdir}from"os";import{join as joinPath}from"path";async function getConnectionById4(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function buildConnectConfig4(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function sshExec2(client,command,timeoutMs=60000){return new Promise((resolve2,reject)=>{let timer=setTimeout(()=>{reject(Error(`Command timed out after ${timeoutMs}ms`))},timeoutMs);client.exec(command,(err,stream)=>{if(err)return clearTimeout(timer),reject(err);let stdout="",stderr="";stream.on("data",(data)=>{stdout+=data.toString()}),stream.stderr.on("data",(data)=>{stderr+=data.toString()}),stream.on("close",(code)=>{clearTimeout(timer),resolve2({stdout:stdout.trim(),stderr:stderr.trim(),code})})})})}async function runInstallation(job,connConfig,agentPort,hostServices,options={}){let{broadcast}=hostServices,sshClient=new import_ssh24.Client,registryUrl=hostServices.getPluginRegistry?.()??DEFAULT_REGISTRY;function emitProgress(){if(broadcast)broadcast("ssh:install:progress",{...job})}function updateStep(stepIdx,status,message){if(job.steps[stepIdx].status=status,message)job.steps[stepIdx].message=message;job.currentStep=stepIdx,emitProgress()}function fail(stepIdx,error){if(updateStep(stepIdx,"failed",error),job.status="failed",job.error=error,job.completedAt=new Date().toISOString(),broadcast)broadcast("ssh:install:failed",{...job});sshClient.end()}try{let connectConfig=await buildConnectConfig4(connConfig);updateStep(0,"running"),await new Promise((resolve2,reject)=>{sshClient.on("ready",()=>resolve2()),sshClient.on("error",(err)=>reject(err)),sshClient.connect({...connectConfig,readyTimeout:15000})}),updateStep(0,"completed","Connected successfully"),updateStep(1,"running");let{stdout:osInfo}=await sshExec2(sshClient,"uname -s && uname -m"),[osName,arch]=osInfo.split(`
20
20
  `);if(updateStep(1,"completed",`${osName} ${arch}`),osName!=="Linux"&&osName!=="Darwin")return fail(1,`Unsupported OS: ${osName}. Only Linux and macOS.`);updateStep(2,"running");let{code:bunCheck}=await sshExec2(sshClient,`${REMOTE_PATH} && which bun 2>/dev/null`);if(bunCheck!==0){updateStep(2,"running","Installing prerequisites..."),await sshExec2(sshClient,"which unzip >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq unzip 2>/dev/null || sudo apt-get update -qq && sudo apt-get install -y -qq unzip 2>/dev/null || yum install -y -q unzip 2>/dev/null || sudo yum install -y -q unzip 2>/dev/null || apk add --quiet unzip 2>/dev/null || true)",60000),updateStep(2,"running","Installing Bun...");let{code:bunInstall,stderr:bunErr}=await sshExec2(sshClient,`curl -fsSL https://bun.sh/install | bash && ${REMOTE_PATH} && bun --version`,120000);if(bunInstall!==0)return fail(2,`Failed to install Bun: ${bunErr}`);updateStep(2,"completed","Bun installed")}else updateStep(2,"skipped","Bun already installed");updateStep(3,"running");let{code:agentCheck}=await sshExec2(sshClient,`${REMOTE_PATH} && which vibe 2>/dev/null`);if(agentCheck!==0){updateStep(3,"running",`Installing agent (registry: ${registryUrl})...`);let{code:installCode}=await sshExec2(sshClient,`${REMOTE_PATH} && bun install -g @vibecontrols/vibecontrols-agent --registry ${registryUrl}`,120000);if(installCode!==0){updateStep(3,"running","Registry unavailable, transferring directly...");try{let agentDir=hostServices.getConfig?.("agent:packageDir")||`${homedir2()}/products/vibecontrols/vibecontrols-agent`,localTmpDir=tmpdir(),tgzName=Bun.spawnSync(["npm","pack","--pack-destination",localTmpDir],{cwd:agentDir,stdout:"pipe",stderr:"pipe"}).stdout.toString().trim().split(`
21
21
  `).pop(),tgzPath=tgzName?joinPath(localTmpDir,tgzName):"";if(!tgzPath||!await Bun.file(tgzPath).exists())return fail(3,"Failed to pack agent for transfer");let scpArgs=["-o","StrictHostKeyChecking=accept-new","-P",String(connConfig.port)];if(connConfig.privateKeyPath)scpArgs.push("-i",expandPath(connConfig.privateKeyPath));if(scpArgs.push(tgzPath,`${connConfig.username}@${connConfig.host}:/tmp/vibecontrols-agent.tgz`),Bun.spawnSync(["scp",...scpArgs]).exitCode!==0)return fail(3,"Failed to transfer agent package via SCP");let{code:setupCode2,stderr:setupErr2}=await sshExec2(sshClient,`${REMOTE_PATH} && mkdir -p $HOME/.vibecontrols/agent && cd $HOME/.vibecontrols/agent && tar xzf /tmp/vibecontrols-agent.tgz --strip-components=1 && bun install --production --ignore-scripts 2>/dev/null; mkdir -p $HOME/.local/bin && ln -sf $HOME/.vibecontrols/agent/dist/cli.js $HOME/.local/bin/vibe && chmod +x $HOME/.vibecontrols/agent/dist/cli.js && rm -f /tmp/vibecontrols-agent.tgz`,120000);if(setupCode2!==0)return fail(3,`Failed to set up agent: ${setupErr2?.slice(0,200)}`);let{code:vibeCheck}=await sshExec2(sshClient,`${REMOTE_PATH} && which vibe`);if(vibeCheck!==0)return fail(3,"Agent installed but 'vibe' command not found in PATH")}catch(transferErr){return fail(3,`Failed to install agent: ${transferErr instanceof Error?transferErr.message:"transfer failed"}`)}}updateStep(3,"completed","Agent installed")}else updateStep(3,"skipped","Agent already installed");updateStep(4,"running");let{code:cfCheck}=await sshExec2(sshClient,"which cloudflared 2>/dev/null"),hasCloudflared=cfCheck===0,{code:setupCode,stderr:setupErr}=await sshExec2(sshClient,`${REMOTE_PATH} && vibe setup --non-interactive --port ${agentPort}`,30000);if(setupCode!==0)updateStep(4,"completed",setupErr?`Warning: ${setupErr.slice(0,120)}`:"Configured with defaults");else updateStep(4,"completed","Agent configured");updateStep(5,"running");let{code:portCheck}=await sshExec2(sshClient,`curl -sf http://127.0.0.1:${agentPort}/health`,5000);if(portCheck===0)updateStep(5,"skipped","Agent already running on this port");else{let tunnelEnv=hasCloudflared?"":"AGENT_TUNNEL=false ",{code:startCode,stderr:startErr}=await sshExec2(sshClient,`${REMOTE_PATH} && ${tunnelEnv}PORT=${agentPort} nohup vibe start > /dev/null 2>&1 & sleep 3 && echo started || (cd $HOME/.vibecontrols/agent && ${tunnelEnv}PORT=${agentPort} nohup bun run dist/index.js > /tmp/vibe-agent.log 2>&1 & sleep 3 && echo started)`,30000);if(startCode!==0&&!startErr.includes("already running"))return fail(5,`Failed to start agent: ${startErr}`);updateStep(5,"completed","Agent started")}updateStep(6,"running"),await new Promise((r)=>setTimeout(r,3000));let healthy=!1;for(let attempt=0;attempt<8;attempt++){let{code:healthCode}=await sshExec2(sshClient,`curl -sf http://127.0.0.1:${agentPort}/health`,5000);if(healthCode===0){healthy=!0;break}await new Promise((r)=>setTimeout(r,2000))}if(!healthy)return fail(6,"Agent health check failed after 8 attempts");updateStep(6,"completed","Agent is healthy"),updateStep(7,"running");let apiKey,tunnelUrl,hostname=connConfig.host,platform="linux",architecture="x86_64",{stdout:keyOut}=await sshExec2(sshClient,`curl -sf http://127.0.0.1:${agentPort}/api/agent/api-key`,5000).catch(()=>({stdout:"",stderr:"",code:1}));if(keyOut)try{apiKey=JSON.parse(keyOut).apiKey}catch{}let{stdout:identityOut}=await sshExec2(sshClient,`curl -sf http://127.0.0.1:${agentPort}/api/agent/identity`,5000).catch(()=>({stdout:"",stderr:"",code:1}));if(identityOut)try{let id=JSON.parse(identityOut);hostname=id.hostname||hostname,platform=id.platform||platform,architecture=id.arch||architecture}catch{}if(hasCloudflared)for(let i=0;i<6;i++){await new Promise((r)=>setTimeout(r,5000));let{stdout:statusOut}=await sshExec2(sshClient,`curl -sf http://127.0.0.1:${agentPort}/api/agent/status`,5000).catch(()=>({stdout:"",stderr:"",code:1}));if(statusOut)try{let s=JSON.parse(statusOut);if(s.tunnelUrl){tunnelUrl=s.tunnelUrl;break}}catch{}}if(updateStep(7,"completed",tunnelUrl?`Tunnel: ${tunnelUrl}`:"Details retrieved (no tunnel)"),job.result={agentUrl:tunnelUrl||`http://${connConfig.host}:${agentPort}`,agentPort,apiKey,tunnelUrl,hostname,platform,architecture},options.autoRegister&&hostServices.isGatewayConfigured?.()&&hostServices.workspaceQuery){updateStep(8,"running");let workspaceId=hostServices.getWorkspaceId?.();if(!workspaceId)updateStep(8,"skipped","No workspace ID configured");else try{let agentName=options.agentName||hostname||connConfig.serverName,mutation=`
22
22
  mutation RegisterInstalledAgent($workspaceId: ID!, $input: RegisterInstalledAgentInput!) {
@@ -31,14 +31,14 @@ ${suffix}`}};var MAX_32BIT_INT=4294967296,MAX_32BIT_BIGINT=(()=>{try{return Func
31
31
  }
32
32
  }
33
33
  `,{workspaceId,input:{name:agentName,hostname,platform,architecture,apiUrl:`http://${connConfig.host}:${agentPort}`,tunnelUrl:tunnelUrl||void 0,agentApiKey:apiKey||void 0}});if(result.errors?.length)updateStep(8,"failed",result.errors[0].message);else{let agentRecord=result.data?.registerInstalledAgent;if(agentRecord){if(job.result.backendAgentId=agentRecord.id,apiKey&&hostServices.getConfig){let gwPayload=JSON.stringify({tenantApiUrl:hostServices.getConfig("gateway-auth:globalGatewayUrl")||"",workspacesApiUrl:hostServices.getConfig("gateway-auth:workspaceGatewayUrl")||"",appClientId:hostServices.getConfig("gateway-auth:clientId")||"",appClientSecret:hostServices.getConfig("gateway-auth:clientSecret")||"",workspaceId,agentRecordId:agentRecord.id});await sshExec2(sshClient,`echo '${gwPayload.replace(/'/g,"'\\''")}' > /tmp/.vc-gw-auth.json && curl -sf -X POST http://127.0.0.1:${agentPort}/api/agent/gateway-auth -H 'Content-Type: application/json' -H 'x-agent-api-key: ${apiKey}' -d @/tmp/.vc-gw-auth.json && rm -f /tmp/.vc-gw-auth.json`,15000).catch(()=>{})}updateStep(8,"completed",`Registered as "${agentRecord.name}" (${agentRecord.id.slice(0,8)})`)}}}catch(regErr){updateStep(8,"failed",regErr instanceof Error?regErr.message:"Registration failed")}}else updateStep(8,"skipped","Auto-registration not requested or gateway not configured");if(job.status="completed",job.completedAt=new Date().toISOString(),broadcast)broadcast("ssh:install:complete",{...job});sshClient.end()}catch(error){let errMsg=error instanceof Error?error.message:"Unknown error";if(job.status="failed",job.error=errMsg,job.completedAt=new Date().toISOString(),broadcast)broadcast("ssh:install:failed",{...job});sshClient.end()}}function createRemoteAgentInstallRoutes(hostServices){let{storage,broadcast}=hostServices;return new Elysia5({prefix:"/api/ssh/agent-install"}).get("/jobs",()=>{return{jobs:Array.from(installJobs.values())}}).get("/jobs/:id",({params,set})=>{let job=installJobs.get(params.id);if(!job)return set.status=404,{error:"Install job not found"};return{job}}).post("/start",async({body,set})=>{let{connectionId,agentPort=3005,autoRegister=!1,agentName}=body,connConfig=await getConnectionById4(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let jobId=globalThis.crypto.randomUUID(),job={id:jobId,connectionId,status:"running",steps:INSTALL_STEPS.map((s)=>({name:s.name,status:"pending"})),currentStep:0,startedAt:new Date().toISOString()};return installJobs.set(jobId,job),runInstallation(job,connConfig,agentPort,hostServices,{autoRegister,agentName}).catch(()=>{}),{jobId,status:"running"}}).post("/batch-start",async({body,set})=>{let{connectionIds,agentPort=3005,autoRegister=!1}=body;if(!connectionIds?.length)return set.status=400,{error:"No connection IDs provided"};let jobs=[];for(let connectionId of connectionIds){let connConfig=await getConnectionById4(storage,connectionId);if(!connConfig)continue;let jobId=globalThis.crypto.randomUUID(),job={id:jobId,connectionId,status:"running",steps:INSTALL_STEPS.map((s)=>({name:s.name,status:"pending"})),currentStep:0,startedAt:new Date().toISOString()};installJobs.set(jobId,job),jobs.push({connectionId,jobId}),runInstallation(job,connConfig,agentPort,hostServices,{autoRegister}).catch(()=>{})}return{jobs,total:jobs.length}}).post("/uninstall",async({body,set})=>{let{connectionId}=body,connConfig=await getConnectionById4(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let sshClient=new import_ssh24.Client,connectConfig=await buildConnectConfig4(connConfig);await new Promise((resolve2,reject)=>{sshClient.on("ready",()=>resolve2()),sshClient.on("error",(err)=>reject(err)),sshClient.connect({...connectConfig,readyTimeout:15000})});try{await sshExec2(sshClient,'pkill -f "bun.*index.ts" 2>/dev/null; pkill -f "vibe" 2>/dev/null',1e4).catch(()=>{}),await sshExec2(sshClient,"rm -rf $HOME/.vibecontrols/agent",1e4),await sshExec2(sshClient,"rm -f $HOME/.local/bin/vibe",1e4),await sshExec2(sshClient,"rm -rf $HOME/.vibecontrols",1e4)}finally{sshClient.end()}return{success:!0,message:"Agent uninstalled"}})}var import_ssh24,REMOTE_PATH='export BUN_INSTALL="$HOME/.bun" && export PATH="$BUN_INSTALL/bin:$HOME/.local/bin:$PATH"',DEFAULT_REGISTRY="https://registry.npmjs.org",installJobs,INSTALL_STEPS;var init_remote_agent_install=__esm(()=>{init_expand_path();import_ssh24=__toESM(require_lib3(),1),installJobs=new Map;INSTALL_STEPS=[{name:"connect"},{name:"detect_os"},{name:"install_bun"},{name:"install_agent"},{name:"configure"},{name:"start"},{name:"verify"},{name:"retrieve"},{name:"register"}]});var exports_ssh_config_scan={};__export(exports_ssh_config_scan,{createSSHConfigScanRoutes:()=>createSSHConfigScanRoutes});import{homedir as homedir3}from"os";import{Elysia as Elysia6}from"elysia";function parseSSHConfig(content){let hosts=[],current=null;for(let rawLine of content.split(`
34
- `)){let line=rawLine.trim();if(!line||line.startsWith("#"))continue;let match=line.match(/^(\S+)\s+(.+)$/);if(!match)continue;let[,key,value]=match,keyLower=key.toLowerCase();if(keyLower==="host"){if(current?.name&&current.name!=="*")hosts.push({name:current.name,hostname:current.hostname||current.name,port:current.port||22,user:current.user||"root",identityFile:current.identityFile,proxyJump:current.proxyJump,extra:current.extra||{}});current={name:value,extra:{}}}else if(current)switch(keyLower){case"hostname":current.hostname=value;break;case"port":current.port=parseInt(value,10);break;case"user":current.user=value;break;case"identityfile":current.identityFile=value.replace(/^~/,homedir3());break;case"proxyjump":current.proxyJump=value;break;default:if(!current.extra)current.extra={};current.extra[key]=value}}if(current?.name&&current.name!=="*")hosts.push({name:current.name,hostname:current.hostname||current.name,port:current.port||22,user:current.user||"root",identityFile:current.identityFile,proxyJump:current.proxyJump,extra:current.extra||{}});return hosts}async function getConnectionById5(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function buildConnectConfig5(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function sshExec3(client,command){return new Promise((resolve2,reject)=>{client.exec(command,(err,stream)=>{if(err)return reject(err);let stdout="",stderr="";stream.on("data",(data)=>{stdout+=data.toString()}),stream.stderr.on("data",(data)=>{stderr+=data.toString()}),stream.on("close",(code)=>{resolve2({stdout:stdout.trim(),stderr:stderr.trim(),code})})})})}function createSSHConfigScanRoutes(hostServices){let{storage}=hostServices;return new Elysia6({prefix:"/api/ssh/config-scan"}).post("/local",async({body,set})=>{let{configPath="~/.ssh/config"}=body,resolved=configPath.replace(/^~/,homedir3());try{let file=Bun.file(resolved);if(!await file.exists())return set.status=404,{error:`SSH config not found at ${resolved}`};let content=await file.text(),hosts=parseSSHConfig(content);return{configPath:resolved,hosts,total:hosts.length}}catch(error){return set.status=500,{error:"Failed to read SSH config",details:error instanceof Error?error.message:"Unknown error"}}}).post("/remote",async({body,set})=>{let{connectionId,configPath="~/.ssh/config"}=body;try{let connConfig=await getConnectionById5(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let sshClient=new import_ssh25.Client,connectConfig=await buildConnectConfig5(connConfig);return new Promise((resolve2)=>{sshClient.on("ready",async()=>{try{let{stdout,code}=await sshExec3(sshClient,`cat ${configPath} 2>/dev/null`);if(code!==0||!stdout)return sshClient.end(),set.status=404,resolve2({error:`SSH config not found at ${configPath} on remote server`});let hosts=parseSSHConfig(stdout);sshClient.end(),resolve2({configPath,remoteHost:connConfig.host,hosts,total:hosts.length})}catch(err){sshClient.end(),set.status=500,resolve2({error:"Failed to read remote SSH config",details:err instanceof Error?err.message:"Unknown error"})}}),sshClient.on("error",(err)=>{set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),sshClient.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to scan remote SSH config",details:error instanceof Error?error.message:"Unknown error"}}}).post("/batch-create",async({body,set})=>{let{hosts}=body;if(!hosts||!Array.isArray(hosts)||hosts.length===0)return set.status=400,{error:"No hosts provided"};try{let raw=await storage.get("ssh","connections"),connections=raw?JSON.parse(raw):[],created=[],skipped=[];for(let host of hosts){if(connections.find((c)=>c.serverName===host.name)){skipped.push(host.name);continue}let newConn={id:globalThis.crypto.randomUUID(),serverName:host.name,host:host.hostname,port:host.port,username:host.user,privateKeyPath:host.identityFile,createdAt:new Date().toISOString()};connections.push(newConn),created.push(newConn)}return await storage.set("ssh","connections",JSON.stringify(connections)),{created:created.length,skipped:skipped.length,skippedNames:skipped,connections:created.map((c)=>({id:c.id,serverName:c.serverName,host:c.host,port:c.port,username:c.username}))}}catch(error){return set.status=500,{error:"Failed to batch create connections",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh25;var init_ssh_config_scan=__esm(()=>{init_expand_path();import_ssh25=__toESM(require_lib3(),1)});import{Elysia}from"elysia";function createLifecycleHooks(spec){let{name,onInit,onShutdown,telemetryEventName,skipPlatforms}=spec;return{onServerStart:async(_app,hostServices)=>{if(skipPlatforms&&skipPlatforms.includes(process.platform)){process.stderr.write(`[${name}] skipping init on unsupported platform '${process.platform}'
35
- `);return}if(onInit)await onInit(hostServices);if(telemetryEventName)hostServices.telemetry?.emit(telemetryEventName,{plugin:name})},onServerStop:async(hostServices)=>{if(onShutdown)await onShutdown(hostServices)}}}function pickOutputMode(flags){if(flags.json)return"json";if(flags.plain)return"plain";if(flags.interactive)return"interactive";return"auto"}function isCi(){return!!process.env.CI||!!process.env.NO_COLOR||process.env.TERM==="dumb"}function stdoutIsTty(){return Boolean(process.stdout.isTTY)}async function runMultimode(opts){let data=await opts.fetchData(),mode=opts.mode??"auto";if(mode==="json"){let shaped=opts.json?opts.json(data):data;process.stdout.write(`${JSON.stringify(shaped,null,2)}
34
+ `)){let line=rawLine.trim();if(!line||line.startsWith("#"))continue;let match=line.match(/^(\S+)\s+(.+)$/);if(!match)continue;let[,key,value]=match,keyLower=key.toLowerCase();if(keyLower==="host"){if(current?.name&&current.name!=="*")hosts.push({name:current.name,hostname:current.hostname||current.name,port:current.port||22,user:current.user||"root",identityFile:current.identityFile,proxyJump:current.proxyJump,extra:current.extra||{}});current={name:value,extra:{}}}else if(current)switch(keyLower){case"hostname":current.hostname=value;break;case"port":current.port=parseInt(value,10);break;case"user":current.user=value;break;case"identityfile":current.identityFile=value.replace(/^~/,homedir3());break;case"proxyjump":current.proxyJump=value;break;default:if(!current.extra)current.extra={};current.extra[key]=value}}if(current?.name&&current.name!=="*")hosts.push({name:current.name,hostname:current.hostname||current.name,port:current.port||22,user:current.user||"root",identityFile:current.identityFile,proxyJump:current.proxyJump,extra:current.extra||{}});return hosts}async function getConnectionById5(storage,id){let raw=await storage.get("ssh","connections");if(!raw)return;return JSON.parse(raw).find((c)=>c.id===id)}async function buildConnectConfig5(conn){let cfg={host:conn.host,port:conn.port,username:conn.username};if(conn.privateKeyPath){let file=Bun.file(expandPath(conn.privateKeyPath));cfg.privateKey=Buffer.from(await file.arrayBuffer())}else if(conn.password)cfg.password=conn.password;return cfg}function sshExec3(client,command){return new Promise((resolve2,reject)=>{client.exec(command,(err,stream)=>{if(err)return reject(err);let stdout="",stderr="";stream.on("data",(data)=>{stdout+=data.toString()}),stream.stderr.on("data",(data)=>{stderr+=data.toString()}),stream.on("close",(code)=>{resolve2({stdout:stdout.trim(),stderr:stderr.trim(),code})})})})}function createSSHConfigScanRoutes(hostServices){let{storage}=hostServices;return new Elysia6({prefix:"/api/ssh/config-scan"}).post("/local",async({body,set})=>{let{configPath="~/.ssh/config"}=body,resolved=configPath.replace(/^~/,homedir3());try{let file=Bun.file(resolved);if(!await file.exists())return set.status=404,{error:`SSH config not found at ${resolved}`};let content=await file.text(),hosts=parseSSHConfig(content);return{configPath:resolved,hosts,total:hosts.length}}catch(error){return set.status=500,{error:"Failed to read SSH config",details:error instanceof Error?error.message:"Unknown error"}}}).post("/remote",async({body,set})=>{let{connectionId,configPath="~/.ssh/config"}=body;try{let connConfig=await getConnectionById5(storage,connectionId);if(!connConfig)return set.status=404,{error:"SSH connection not found"};let sshClient=new import_ssh25.Client,connectConfig=await buildConnectConfig5(connConfig);return new Promise((resolve2)=>{sshClient.on("ready",async()=>{try{let{stdout,code}=await sshExec3(sshClient,`cat ${configPath} 2>/dev/null`);if(code!==0||!stdout)return sshClient.end(),set.status=404,resolve2({error:`SSH config not found at ${configPath} on remote server`});let hosts=parseSSHConfig(stdout);sshClient.end(),resolve2({configPath,remoteHost:connConfig.host,hosts,total:hosts.length})}catch(err){sshClient.end(),set.status=500,resolve2({error:"Failed to read remote SSH config",details:err instanceof Error?err.message:"Unknown error"})}}),sshClient.on("error",(err)=>{set.status=500,resolve2({error:"SSH connection failed",details:err.message})}),sshClient.connect(connectConfig)})}catch(error){return set.status=500,{error:"Failed to scan remote SSH config",details:error instanceof Error?error.message:"Unknown error"}}}).post("/batch-create",async({body,set})=>{let{hosts}=body;if(!hosts||!Array.isArray(hosts)||hosts.length===0)return set.status=400,{error:"No hosts provided"};try{let raw=await storage.get("ssh","connections"),connections=raw?JSON.parse(raw):[],created=[],skipped=[];for(let host of hosts){if(connections.find((c)=>c.serverName===host.name)){skipped.push(host.name);continue}let newConn={id:globalThis.crypto.randomUUID(),serverName:host.name,host:host.hostname,port:host.port,username:host.user,privateKeyPath:host.identityFile,createdAt:new Date().toISOString()};connections.push(newConn),created.push(newConn)}return await storage.set("ssh","connections",JSON.stringify(connections)),{created:created.length,skipped:skipped.length,skippedNames:skipped,connections:created.map((c)=>({id:c.id,serverName:c.serverName,host:c.host,port:c.port,username:c.username}))}}catch(error){return set.status=500,{error:"Failed to batch create connections",details:error instanceof Error?error.message:"Unknown error"}}})}var import_ssh25;var init_ssh_config_scan=__esm(()=>{init_expand_path();import_ssh25=__toESM(require_lib3(),1)});import{Elysia}from"elysia";function createLifecycleHooks(spec){let{name,onInit,onShutdown,onNuke,telemetryEventName,skipPlatforms}=spec;return{onServerStart:async(_app,hostServices)=>{if(skipPlatforms&&skipPlatforms.includes(process.platform)){process.stderr.write(`[${name}] skipping init on unsupported platform '${process.platform}'
35
+ `);return}if(onInit)await onInit(hostServices);if(telemetryEventName)hostServices.telemetry?.emit(telemetryEventName,{plugin:name})},onServerStop:async(hostServices)=>{if(onShutdown)await onShutdown(hostServices)},onNuke:async(hostServices,ctx)=>{if(onNuke)return onNuke(hostServices,ctx)}}}function pickOutputMode(flags){if(flags.json)return"json";if(flags.plain)return"plain";if(flags.interactive)return"interactive";return"auto"}function isCi(){return!!process.env.CI||!!process.env.NO_COLOR||process.env.TERM==="dumb"}function stdoutIsTty(){return Boolean(process.stdout.isTTY)}async function runMultimode(opts){let data=await opts.fetchData(),mode=opts.mode??"auto";if(mode==="json"){let shaped=opts.json?opts.json(data):data;process.stdout.write(`${JSON.stringify(shaped,null,2)}
36
36
  `);return}if(mode==="plain"){await opts.plain(data);return}if((mode==="interactive"||stdoutIsTty()&&!isCi())&&!!opts.interactive&&opts.interactive)try{await opts.interactive(data);return}catch{}await opts.plain(data)}function maybePrintJson(flags,data){if(!flags.json)return!1;return process.stdout.write(`${JSON.stringify(data,null,2)}
37
37
  `),!0}var SENSITIVE_KEY_RE=/(token|secret|password|apikey|api_key|key|auth|credential|email)/i;function redact(value){return redactInner(value)}function redactInner(value){if(value===null||value===void 0)return value;if(Array.isArray(value))return value.map(redactInner);if(typeof value==="object"){let out={};for(let[k,v]of Object.entries(value)){if(SENSITIVE_KEY_RE.test(k)){out[k]="[redacted]";continue}out[k]=redactInner(v)}return out}return value}var TelemetryEmitter=class{constructor(pluginName,pluginVersion,hostServices){this.pluginName=pluginName,this.pluginVersion=pluginVersion,this.hostServices=hostServices}pluginName;pluginVersion;hostServices;emit(eventName,payload){let target=this.hostServices?.telemetry;if(!target)return;target.emit(eventName,{plugin:this.pluginName,version:this.pluginVersion,timestamp:new Date().toISOString(),...payload??{}})}emitReady(context){this.emit(`${this.pluginName}.ready`,context)}emitError(error,context){this.emit(`${this.pluginName}.error`,{message:error.message,...context??{}})}emitEvent(type,payload){this.emit(type,payload)}};async function loadCore(){return await import("@opentui/core")}async function interactiveTable(opts){if(opts.rows.length===0)return null;let core=await loadCore(),{createCliRenderer,BoxRenderable,TextRenderable,SelectRenderable,SelectRenderableEvents}=core,renderer=await createCliRenderer({exitOnCtrlC:!0,targetFps:30}),ctx=renderer.root.ctx,root=new BoxRenderable(ctx,{width:"100%",height:"100%",flexDirection:"column",backgroundColor:"#0b0d12"}),title=new TextRenderable(ctx,{content:` ${opts.title}`,fg:"#8be9fd",height:1}),footer=new TextRenderable(ctx,{content:` ${opts.footer??"\u2191/\u2193 navigate \xB7 Enter select \xB7 q quit"}`,fg:"#6c7086",height:1}),body=new BoxRenderable(ctx,{width:"100%",flexGrow:1,flexDirection:"row"}),list=new SelectRenderable(ctx,{width:opts.listWidth??28,height:"100%",options:opts.rows.map((r)=>({name:r.label,description:r.hint??"",value:r.id})),selectedIndex:0,showDescription:!0,backgroundColor:"#0b0d12",textColor:"#cdd6f4",focusedBackgroundColor:"#0b0d12",focusedTextColor:"#cdd6f4",selectedBackgroundColor:"#1f2335",selectedTextColor:"#a6e3a1",descriptionColor:"#6c7086",selectedDescriptionColor:"#9399b2",showScrollIndicator:!0,wrapSelection:!0}),detailPane=new BoxRenderable(ctx,{flexGrow:1,height:"100%",flexDirection:"column",paddingLeft:2,paddingRight:2,backgroundColor:"#11141c"}),detailText=new TextRenderable(ctx,{content:"",fg:"#cdd6f4"});detailPane.add(detailText),body.add(list),body.add(detailPane),root.add(title),root.add(body),root.add(footer),renderer.root.add(root),list.focus();let renderDetail=(idx)=>{let row=opts.rows[idx];if(!row)return;detailText.content=`
38
38
  ${row.detail}
39
39
  `};return renderDetail(0),list.on(SelectRenderableEvents.SELECTION_CHANGED,(idx)=>{renderDetail(idx)}),await new Promise((resolve)=>{let cleanup=(chosen)=>{try{renderer.destroy()}catch{}resolve(chosen)};list.on(SelectRenderableEvents.ITEM_SELECTED,()=>{let row=opts.rows[list.getSelectedIndex()];cleanup(row??null)}),renderer.keyInput.on("keypress",(key)=>{if(key.name==="escape"||key.name==="q")cleanup(null)})}).then(async(chosen)=>{if(chosen&&opts.onSelect)await opts.onSelect(chosen);return chosen})}async function interactiveDetail(opts){let core=await loadCore(),{createCliRenderer,BoxRenderable,TextRenderable}=core,renderer=await createCliRenderer({exitOnCtrlC:!0,targetFps:30}),ctx=renderer.root.ctx,root=new BoxRenderable(ctx,{width:"100%",height:"100%",flexDirection:"column",backgroundColor:"#0b0d12"}),title=new TextRenderable(ctx,{content:` ${opts.title}`,fg:"#8be9fd",height:1}),bodyBox=new BoxRenderable(ctx,{width:"100%",flexGrow:1,paddingLeft:2,paddingRight:2,backgroundColor:"#11141c"}),bodyText=new TextRenderable(ctx,{content:`
40
40
  ${opts.body}
41
- `,fg:"#cdd6f4"}),footer=new TextRenderable(ctx,{content:` ${opts.footer??"q to quit"}`,fg:"#6c7086",height:1});bodyBox.add(bodyText),root.add(title),root.add(bodyBox),root.add(footer),renderer.root.add(root),await new Promise((resolve)=>{let cleanup=()=>{try{renderer.destroy()}catch{}resolve()};renderer.keyInput.on("keypress",(key)=>{if(key.name==="escape"||key.name==="q"||key.name==="return")cleanup()})})}var AGENT_BASE_URL=process.env.VIBE_AGENT_URL??"http://localhost:3005",API_KEY=process.env.VIBE_AGENT_API_KEY??"";async function apiFetch(urlPath,options){return fetch(`${AGENT_BASE_URL}${urlPath}`,{...options,headers:{"Content-Type":"application/json","x-agent-api-key":API_KEY,...options?.headers}})}var cleanupPortForwards,cleanupTerminals,PLUGIN_NAME="ssh",PLUGIN_VERSION="2026.509.1",createPlugin=(_ctx)=>{let lifecycle=createLifecycleHooks({name:PLUGIN_NAME,telemetryEventName:"tool.ready",onInit:(hostServices)=>{new TelemetryEmitter(PLUGIN_NAME,PLUGIN_VERSION,hostServices).emitEvent("tool.ready",{provider:"ssh"})}});return{capabilities:{storage:"rw",subprocess:!0,audit:!0,telemetry:!0,broadcast:!0},name:PLUGIN_NAME,version:PLUGIN_VERSION,description:"SSH connection management, remote terminals, port forwarding, and remote agent installation",tags:["backend","cli","integration","provider"],cliCommand:"ssh",apiPrefix:"/api/ssh",async onServerStart(app,hostServices){if(await lifecycle.onServerStart(app,hostServices),process.platform==="win32"){process.stderr.write(" Plugin 'ssh' is not supported on Windows yet \u2014 skipping route + provider registration. "+`See https://github.com/algoshred/vibe-plugin-tool-ssh for status.
41
+ `,fg:"#cdd6f4"}),footer=new TextRenderable(ctx,{content:` ${opts.footer??"q to quit"}`,fg:"#6c7086",height:1});bodyBox.add(bodyText),root.add(title),root.add(bodyBox),root.add(footer),renderer.root.add(root),await new Promise((resolve)=>{let cleanup=()=>{try{renderer.destroy()}catch{}resolve()};renderer.keyInput.on("keypress",(key)=>{if(key.name==="escape"||key.name==="q"||key.name==="return")cleanup()})})}var STORAGE_NS="ssh";async function clearStorageNamespace(storage){let keys=await storage.keys(STORAGE_NS);for(let key of keys)await storage.delete(STORAGE_NS,key)}var AGENT_BASE_URL=process.env.VIBE_AGENT_URL??"http://localhost:3005",API_KEY=process.env.VIBE_AGENT_API_KEY??"";async function apiFetch(urlPath,options){return fetch(`${AGENT_BASE_URL}${urlPath}`,{...options,headers:{"Content-Type":"application/json","x-agent-api-key":API_KEY,...options?.headers}})}var cleanupPortForwards,cleanupTerminals,PLUGIN_NAME="ssh",PLUGIN_VERSION="2026.509.1",createPlugin=(_ctx)=>{let lifecycle=createLifecycleHooks({name:PLUGIN_NAME,telemetryEventName:"tool.ready",onInit:(hostServices)=>{new TelemetryEmitter(PLUGIN_NAME,PLUGIN_VERSION,hostServices).emitEvent("tool.ready",{provider:"ssh"})},onNuke:async(hostServices,ctx)=>{let reaped=["ssh -L tunnel subprocesses + ssh clients","ssh storage"];if(ctx.dryRun)return{reaped};if(cleanupTerminals)cleanupTerminals(),cleanupTerminals=void 0;if(cleanupPortForwards)cleanupPortForwards(),cleanupPortForwards=void 0;let storage=hostServices.storage;return await clearStorageNamespace(storage),{reaped}}}),plugin={capabilities:{storage:"rw",subprocess:!0,audit:!0,telemetry:!0,broadcast:!0},name:PLUGIN_NAME,version:PLUGIN_VERSION,description:"SSH connection management, remote terminals, port forwarding, and remote agent installation",tags:["backend","cli","integration","provider"],cliCommand:"ssh",apiPrefix:"/api/ssh",async onServerStart(app,hostServices){if(await lifecycle.onServerStart(app,hostServices),process.platform==="win32"){process.stderr.write(" Plugin 'ssh' is not supported on Windows yet \u2014 skipping route + provider registration. "+`See https://github.com/algoshred/vibe-plugin-tool-ssh for status.
42
42
  `);return}let elysiaApp=app,agentHost=hostServices,{createSSHRoutes:createSSHRoutes2}=await Promise.resolve().then(() => (init_ssh(),exports_ssh)),{createPortForwardRoutes:createPortForwardRoutes2,cleanupAllTunnels:cleanupAllTunnels2}=await Promise.resolve().then(() => (init_port_forward(),exports_port_forward)),{createRemoteTerminalRoutes:createRemoteTerminalRoutes2,cleanupAllTerminals:cleanupAllTerminals2,getTerminalInfo:getTerminalInfo2,listTerminalSessions:listTerminalSessions2}=await Promise.resolve().then(() => (init_remote_terminal(),exports_remote_terminal)),{createRemoteAgentInstallRoutes:createRemoteAgentInstallRoutes2}=await Promise.resolve().then(() => (init_remote_agent_install(),exports_remote_agent_install)),{createSSHConfigScanRoutes:createSSHConfigScanRoutes2}=await Promise.resolve().then(() => (init_ssh_config_scan(),exports_ssh_config_scan));if(elysiaApp.use(createSSHRoutes2(agentHost)),elysiaApp.use(createPortForwardRoutes2(agentHost)),elysiaApp.use(createRemoteTerminalRoutes2(agentHost)),elysiaApp.use(createRemoteAgentInstallRoutes2(agentHost)),elysiaApp.use(createSSHConfigScanRoutes2(agentHost)),cleanupPortForwards=cleanupAllTunnels2,cleanupTerminals=cleanupAllTerminals2,agentHost.serviceRegistry){let sshSessionProvider={name:"ssh",getTerminalInfo:(sessionId)=>getTerminalInfo2(sessionId),list:async()=>{return listTerminalSessions2().map((s)=>({id:s.id,name:`ssh-terminal-${s.id.slice(0,8)}`,status:s.status==="active"?"active":"inactive",provider:"ssh",createdAt:s.startedAt}))},get:async(sessionId)=>{let s=listTerminalSessions2().find((x)=>x.id===sessionId);if(!s)return null;return{id:s.id,name:`ssh-terminal-${s.id.slice(0,8)}`,status:s.status==="active"?"active":"inactive",provider:"ssh",createdAt:s.startedAt}}};agentHost.serviceRegistry.registerProvider("session",sshSessionProvider,"ssh")}process.stdout.write(` Plugin 'ssh' registered routes: /api/ssh, /api/port-forward, /api/ssh/terminal, /api/ssh/agent-install, /api/ssh/config-scan
43
43
  `)},async onServerStop(){if(cleanupTerminals)cleanupTerminals(),cleanupTerminals=void 0;if(cleanupPortForwards)cleanupPortForwards(),cleanupPortForwards=void 0;process.stdout.write(` Plugin 'ssh' cleaned up active connections and terminals
44
44
  `)},onCliSetup(programArg){let ssh=programArg.command("ssh").description("SSH connection and remote terminal management");ssh.hook("preAction",()=>{if(process.platform==="win32")process.stderr.write(`SSH plugin is not supported on Windows yet \u2014 see issue tracker.
@@ -47,4 +47,4 @@ ${opts.body}
47
47
  `)},interactive:async(rows)=>{if(!rows||rows.length===0){await interactiveDetail({title:"ssh \u2014 connections",body:"No saved SSH connections."});return}let tableRows=rows.map((r)=>({id:String(r.id??r.name??""),label:String(r.name??r.host??r.id??"(unnamed)"),hint:r.host?`${r.username??""}@${r.host}`:void 0,detail:JSON.stringify(redact(r),null,2)}));await interactiveTable({title:`ssh list \u2014 ${rows.length} connection(s)`,rows:tableRows})},json:(rows)=>redact(rows)})}),ssh.command("terminals").description("List active remote terminal sessions").option("--json","Emit JSON").option("--plain","Force plain text output").action(async(opts)=>{await runMultimode({mode:pickOutputMode(opts),fetchData:async()=>{let data=await(await apiFetch("/api/ssh/terminal/sessions")).json();return Array.isArray(data)?data:data.sessions??[]},plain:(rows)=>{if(!rows||rows.length===0){process.stdout.write(`Use the agent API to list terminal sessions: GET /api/ssh/terminal/sessions
48
48
  `);return}process.stdout.write(`${JSON.stringify(rows,null,2)}
49
49
  `)},interactive:async(rows)=>{if(!rows||rows.length===0){await interactiveDetail({title:"ssh \u2014 terminals",body:"No active terminal sessions."});return}let tableRows=rows.map((r)=>({id:String(r.id??""),label:String(r.name??r.id??"(terminal)"),hint:r.status?String(r.status):void 0,detail:JSON.stringify(redact(r),null,2)}));await interactiveTable({title:`ssh terminals \u2014 ${rows.length} session(s)`,rows:tableRows})},json:(rows)=>redact(rows)})}),ssh.command("install-jobs").description("List remote agent installation jobs").option("--json","Emit JSON").action(async(opts)=>{if(maybePrintJson(opts,{ok:!0,action:"install-jobs",message:"Use the agent API to list install jobs: GET /api/ssh/agent-install/jobs"}))return;process.stdout.write(`Use the agent API to list install jobs: GET /api/ssh/agent-install/jobs
50
- `)})}}},src_default=createPlugin;export{src_default as default,createPlugin};
50
+ `)})}};return plugin.onNuke=lifecycle.onNuke,plugin},src_default=createPlugin;export{src_default as default,createPlugin};
@@ -8,13 +8,9 @@
8
8
  * "terminal-sessions" → JSON array of SSHTerminalSession objects
9
9
  */
10
10
  import { Elysia } from "elysia";
11
- import type { HostServices, SSHTerminalSession } from "../types.js";
11
+ import type { HostServices, SSHTerminalSession, TerminalInfo } from "../types.js";
12
12
  export declare function cleanupAllTerminals(): void;
13
- export declare function getTerminalInfo(sessionId: string): {
14
- url: string;
15
- port: number;
16
- pid: number;
17
- } | null;
13
+ export declare function getTerminalInfo(sessionId: string): TerminalInfo | null;
18
14
  /** List all active terminal sessions (for the session provider shim). */
19
15
  export declare function listTerminalSessions(): SSHTerminalSession[];
20
16
  export declare function createRemoteTerminalRoutes(hostServices: HostServices): Elysia<"/api/ssh/terminal", {
package/dist/types.d.ts CHANGED
@@ -100,6 +100,35 @@ export interface SSHTerminalSession {
100
100
  startedAt: string;
101
101
  error?: string;
102
102
  }
103
+ /**
104
+ * Terminal transport descriptor returned to the agent's terminal proxy.
105
+ *
106
+ * The proxy connects the browser WebSocket to `ws://{host}:{port}{wsPath}`
107
+ * negotiating `subprotocols`, WITHOUT assuming any particular terminal
108
+ * backend. For this SSH provider the backend is a remote `ttyd` reached over
109
+ * an `ssh -L` port forward; ttyd serves the live PTY at `/ws` with the `tty`
110
+ * subprotocol, bound to loopback on the agent side of the tunnel.
111
+ */
112
+ export interface TerminalInfo {
113
+ url: string;
114
+ port: number;
115
+ pid: number;
116
+ /**
117
+ * Loopback host the (forwarded) terminal server listens on. The agent's
118
+ * terminal proxy connects here. Defaults to `127.0.0.1` when omitted.
119
+ */
120
+ host?: string;
121
+ /**
122
+ * WebSocket path the terminal server exposes for the live PTY stream, e.g.
123
+ * `/ws` for ttyd. Defaults to `/ws` when omitted.
124
+ */
125
+ wsPath?: string;
126
+ /**
127
+ * WebSocket subprotocols the terminal server negotiates (e.g. `["tty"]` for
128
+ * ttyd). Forwarded verbatim by the agent. Defaults to `["tty"]` when omitted.
129
+ */
130
+ subprotocols?: string[];
131
+ }
103
132
  export type InstallJobStatus = "pending" | "running" | "completed" | "failed";
104
133
  export type InstallStepStatus = "pending" | "running" | "completed" | "failed" | "skipped";
105
134
  export interface RemoteAgentInstallStep {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecontrols/vibe-plugin-tool-ssh",
3
- "version": "2026.529.1",
3
+ "version": "2026.530.2",
4
4
  "main": "./dist/index.js",
5
5
  "type": "module",
6
6
  "engines": {
@@ -34,7 +34,7 @@
34
34
  "license": "MIT",
35
35
  "description": "SSH connections, remote terminals, port forwarding & remote agent installation for VibeControls Agent",
36
36
  "dependencies": {
37
- "@vibecontrols/plugin-sdk": "2026.509.2",
37
+ "@vibecontrols/plugin-sdk": "2026.530.3",
38
38
  "ssh2": "^1.16.0"
39
39
  },
40
40
  "devDependencies": {