@vibecontrols/vibe-plugin-tool-ssh 2026.529.1 → 2026.530.1
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 +11 -20
- package/dist/index.js +4 -4
- package/package.json +2 -2
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
|
-
##
|
|
89
|
-
|
|
90
|
-
Released under the [MIT License](./LICENSE).
|
|
81
|
+
## About VibeControls
|
|
91
82
|
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
96
|
+
## License
|
|
103
97
|
|
|
104
|
-
|
|
98
|
+
Released under the [MIT License](./LICENSE).
|
|
105
99
|
|
|
106
|
-
|
|
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
|
-
|
|
102
|
+
Maintainer: **Vignesh T.V** — <https://github.com/tvvignesh>
|
|
112
103
|
|
|
113
|
-
The `@vibecontrols/agent` runtime that loads and orchestrates
|
|
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
|
@@ -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&¤t.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&¤t.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&¤t.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&¤t.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};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibecontrols/vibe-plugin-tool-ssh",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.530.1",
|
|
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.
|
|
37
|
+
"@vibecontrols/plugin-sdk": "2026.530.3",
|
|
38
38
|
"ssh2": "^1.16.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|