@vibecontrols/vibe-plugin-session-tmux 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.
Potentially problematic release.
This version of @vibecontrols/vibe-plugin-session-tmux might be problematic. Click here for more details.
- package/README.md +11 -20
- package/dist/index.d.ts +17 -0
- package/dist/index.js +6 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
# @vibecontrols/vibe-plugin-session-tmux
|
|
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
|
Tmux + ttyd session provider plugin for [VibeControls Agent](https://www.npmjs.com/package/@vibecontrols/agent).
|
|
11
4
|
|
|
12
5
|
## Installation
|
|
@@ -59,13 +52,14 @@ This plugin registers a `session` provider with the following capabilities:
|
|
|
59
52
|
|
|
60
53
|
---
|
|
61
54
|
|
|
62
|
-
##
|
|
63
|
-
|
|
64
|
-
Released under the [MIT License](./LICENSE).
|
|
55
|
+
## About VibeControls
|
|
65
56
|
|
|
66
|
-
|
|
57
|
+
**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.
|
|
67
58
|
|
|
68
|
-
|
|
59
|
+
- Website: <https://vibecontrols.com>
|
|
60
|
+
- Documentation: <https://docs.vibecontrols.com>
|
|
61
|
+
- Plugin SDK: <https://github.com/algoshred/vibecontrols-plugin-sdk>
|
|
62
|
+
- All plugins: <https://github.com/algoshred?q=vibe-plugin-&type=all>
|
|
69
63
|
|
|
70
64
|
## Credits
|
|
71
65
|
|
|
@@ -73,17 +67,14 @@ This plugin builds on the following upstream open-source projects. All trademark
|
|
|
73
67
|
|
|
74
68
|
- **tmux** — <https://github.com/tmux/tmux>
|
|
75
69
|
|
|
76
|
-
##
|
|
70
|
+
## License
|
|
77
71
|
|
|
78
|
-
|
|
72
|
+
Released under the [MIT License](./LICENSE).
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
- Documentation: <https://docs.vibecontrols.com>
|
|
82
|
-
- Plugin SDK: <https://github.com/algoshred/vibecontrols-plugin-sdk>
|
|
83
|
-
- All plugins: <https://github.com/algoshred?q=vibe-plugin-&type=all>
|
|
74
|
+
Copyright (c) 2026 Burdenoff Consultancy Services Private Limited, Algoshred Technologies Private Limited, and all its sister companies.
|
|
84
75
|
|
|
85
|
-
|
|
76
|
+
Maintainer: **Vignesh T.V** — <https://github.com/tvvignesh>
|
|
86
77
|
|
|
87
|
-
The `@vibecontrols/agent` runtime that loads and orchestrates
|
|
78
|
+
**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.
|
|
88
79
|
|
|
89
80
|
<!-- VIBECONTROLS_OSS_FOOTER_END -->
|
package/dist/index.d.ts
CHANGED
|
@@ -44,6 +44,23 @@ interface TerminalInfo {
|
|
|
44
44
|
url: string;
|
|
45
45
|
port: number;
|
|
46
46
|
pid: number;
|
|
47
|
+
/**
|
|
48
|
+
* Loopback host the terminal server (ttyd) listens on. The agent's terminal
|
|
49
|
+
* proxy connects here so it never has to assume a backend. ttyd binds to
|
|
50
|
+
* 127.0.0.1.
|
|
51
|
+
*/
|
|
52
|
+
host?: string;
|
|
53
|
+
/**
|
|
54
|
+
* WebSocket path ttyd exposes for the live PTY stream. The agent proxies the
|
|
55
|
+
* browser WS to `ws://{host}:{port}{wsPath}` without hardcoding the backend.
|
|
56
|
+
* ttyd serves the PTY at `/ws`.
|
|
57
|
+
*/
|
|
58
|
+
wsPath?: string;
|
|
59
|
+
/**
|
|
60
|
+
* WebSocket subprotocols ttyd negotiates. The agent forwards these verbatim,
|
|
61
|
+
* so it never hardcodes a provider-specific subprotocol. ttyd uses `["tty"]`.
|
|
62
|
+
*/
|
|
63
|
+
subprotocols?: string[];
|
|
47
64
|
}
|
|
48
65
|
interface HealthCheckResult {
|
|
49
66
|
ok: boolean;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
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}'
|
|
3
|
-
`);return}if(onInit)await onInit(hostServices);if(telemetryEventName)hostServices.telemetry?.emit(telemetryEventName,{plugin:name})},onServerStop:async(hostServices)=>{if(onShutdown)await onShutdown(hostServices)}}}var TypedStore=class{constructor(storage,namespace,key,logger,pluginName="plugin"){this.storage=storage,this.namespace=namespace,this.key=key,this.logger=logger,this.pluginName=pluginName}storage;namespace;key;logger;pluginName;async get(){let raw=await this.storage.get(this.namespace,this.key);if(raw===null||raw===void 0)return null;if(typeof raw!=="string")return raw;try{return JSON.parse(raw)}catch(err){return this.logger?.error?.(this.pluginName,"TypedStore: corrupt JSON",{namespace:this.namespace,key:this.key,message:err instanceof Error?err.message:String(err)}),null}}async set(value){await this.storage.set(this.namespace,this.key,JSON.stringify(value))}async delete(){return this.storage.delete(this.namespace,this.key)}};import{createServer}from"net";function sleep(ms){return new Promise((resolve)=>setTimeout(resolve,ms))}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}async function gracefulKill(pid,timeoutMs=3000,logger){if(!isProcessAlive(pid))return;try{process.kill(pid,"SIGTERM")}catch(err){logger?.warn?.("subprocess","SIGTERM failed",{pid,message:err instanceof Error?err.message:String(err)});return}let pollMs=50,elapsed=0;while(elapsed<timeoutMs)if(await sleep(pollMs),elapsed+=pollMs,!isProcessAlive(pid))return;try{process.kill(pid,"SIGKILL")}catch(err){logger?.warn?.("subprocess","SIGKILL failed",{pid,message:err instanceof Error?err.message:String(err)})}}async function findAvailablePort(start,range=200){for(let i=0;i<range;i++){let port=start+i;if(await isPortFree(port))return port}throw Error(`findAvailablePort: no port free in [${start}, ${start+range-1}]`)}function isPortFree(port){return new Promise((resolve)=>{let server=createServer();server.once("error",()=>{resolve(!1)}),server.once("listening",()=>{server.close(()=>resolve(!0))});try{server.listen({port,host:"127.0.0.1"})}catch{resolve(!1)}})}var BoundLogger=class{constructor(logger,source){this.logger=logger,this.source=source}logger;source;info(message,meta){this.logger?.info?.(this.source,message,meta)}warn(message,meta){this.logger?.warn?.(this.source,message,meta)}error(message,meta){this.logger?.error?.(this.source,message,meta)}debug(message,meta){this.logger?.debug?.(this.source,message,meta)}};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)}};var ProviderRegistry=class{constructor(hostServices){this.hostServices=hostServices}hostServices;getServiceRegistry(){return this.hostServices?.serviceRegistry}registerProvider(type,name,provider){this.hostServices?.serviceRegistry?.registerProvider?.(type,provider,name)}getProvider(type,name){let reg=this.hostServices?.serviceRegistry;if(!reg)return;if(name===void 0)return reg.getProvider?.(type);if(!reg.listProvidersForType)return;if(!(reg.listProvidersForType(type)??[]).some((entry)=>typeof entry==="string"?entry===name:entry.pluginName===name))return;return reg.getProvider?.(type)}listProviders(type){return(this.hostServices?.serviceRegistry?.listProvidersForType?.(type)??[]).map((entry)=>typeof entry==="string"?entry:entry.pluginName)}withCliContribution(contribution){let contributors=this.hostServices?.cliContributors;if(!contributors)return;for(let section of contribution.statusSections??[])contributors.addStatusSection?.(section);for(let check of contribution.doctorChecks??[])contributors.addDoctorCheck?.(check)}};var PLUGIN_NAME="session-tmux",PLUGIN_VERSION="2.3.0",PROVIDER_NAME="session-tmux",STORAGE_NAMESPACE="session-tmux",STORAGE_KEY_SESSIONS="sessions",STORAGE_KEY_TERMINALS="terminals",TTYD_BASE_PORT=7681,TTYD_PORT_RANGE=200;function generateId(){let bytes=new Uint8Array(4);return crypto.getRandomValues(bytes),[...bytes].map((b)=>b.toString(16).padStart(2,"0")).join("")}function tmuxExec(args){let result=Bun.spawnSync(["tmux",...args],{stdout:"pipe",stderr:"pipe",timeout:1e4});if(result.exitCode!==0){let stderr=result.stderr.toString().trim();throw Error(`tmux exited with code ${result.exitCode}: ${stderr}`)}return result.stdout.toString("utf-8").trimEnd()}function tmuxExecSilent(args){try{return Bun.spawnSync(["tmux",...args],{stdout:"pipe",stderr:"pipe",timeout:1e4}).exitCode===0}catch{return!1}}function nowISO(){return new Date().toISOString()}class TmuxSessionProvider{name=PROVIDER_NAME;log=new BoundLogger(void 0,PLUGIN_NAME);sessionsStore=null;terminalsStore=null;ttydPids=new Map;ttydPorts=new Map;async init(services){if(this.log=new BoundLogger(services.logger,PLUGIN_NAME),services.storage)this.sessionsStore=new TypedStore(services.storage,STORAGE_NAMESPACE,STORAGE_KEY_SESSIONS,services.logger,PLUGIN_NAME),this.terminalsStore=new TypedStore(services.storage,STORAGE_NAMESPACE,STORAGE_KEY_TERMINALS,services.logger,PLUGIN_NAME);this.log.info("TmuxSessionProvider initialising",{provider:this.name});try{let version=tmuxExec(["-V"]);this.log.info("tmux detected",{version})}catch{this.log.error("tmux is not installed or not in PATH \u2014 session provider will not function")}await this.reconcileSessions(),this.log.info("TmuxSessionProvider ready")}async shutdown(context){if(context?.reason==="reload"){this.log.info("Hot-reload: preserving tmux sessions and ttyd processes"),await this.persistTerminals(),this.ttydPids.clear(),this.ttydPorts.clear();return}this.log.info("TmuxSessionProvider shutting down \u2014 stopping terminals");let stopPromises=[];for(let[sessionId]of this.ttydPids)stopPromises.push(this.stopTerminal(sessionId));await Promise.allSettled(stopPromises);try{await this.terminalsStore?.delete()}catch{}this.log.info("TmuxSessionProvider shutdown complete")}async create(config){if(config.externalName){let target=config.externalName;if(target.length===0||target.length>128||/[\0\r\n:.\s]/.test(target))throw Error(`Invalid externalName: ${target}`);if(tmuxExecSilent(["has-session","-t",target]))return this.log.info("create(): attaching to existing tmux session",{target}),this.adopt(target,config.name);let now2=nowISO(),args=["new-session","-d","-s",target];if(config.workingDirectory)args.push("-c",config.workingDirectory);if(config.size)args.push("-x",String(config.size.cols),"-y",String(config.size.rows));if(config.shell)args.push(config.shell);if(tmuxExec(args),config.environment)for(let[k,v]of Object.entries(config.environment))tmuxExecSilent(["set-environment","-t",target,k,v]);if(tmuxExecSilent(["set","-t",target,"mouse","on"]),config.command)tmuxExecSilent(["send-keys","-t",target,config.command,"Enter"]);let id2=target.startsWith("vibe-")?target.slice(5)||target:target,pid2=this.getSessionPanePid(target),info2={id:id2,name:config.name,status:"active",provider:this.name,command:config.command,workingDirectory:config.workingDirectory,pid:pid2??void 0,projectId:config.projectId,createdAt:now2,updatedAt:now2,metadata:{tmuxSessionName:target,shell:config.shell,size:config.size,createdWithExplicitName:!0}};return await this.saveSession(info2),this.log.info("Tmux session created with explicit name",{id:id2,target}),info2}let id=config.id||generateId(),sessionName=`vibe-${id.substring(0,8)}`,now=nowISO();this.log.info("Creating tmux session",{id,name:config.name,sessionName});let alreadyRunning=tmuxExecSilent(["has-session","-t",sessionName]);if(alreadyRunning)this.log.info("Reusing existing tmux session on resume",{id,sessionName});else{let args=["new-session","-d","-s",sessionName];if(config.workingDirectory)args.push("-c",config.workingDirectory);if(config.size)args.push("-x",String(config.size.cols),"-y",String(config.size.rows));if(config.shell)args.push(config.shell);try{tmuxExec(args)}catch(err){throw this.log.error("Failed to create tmux session",{id,error:String(err)}),Error(`Failed to create tmux session: ${err}`,{cause:err})}}if(config.environment)for(let[key,value]of Object.entries(config.environment))tmuxExecSilent(["set-environment","-t",sessionName,key,value]);if(tmuxExecSilent(["set","-t",sessionName,"mouse","on"]),tmuxExecSilent(["set","-t",sessionName,"pane-border-format"," #[align=centre]#[bg=colour208,fg=black,bold] \u2B06 SCROLL MODE \u2014 press Esc or q to resume typing \u2B06 #[default]"]),tmuxExecSilent(["set-hook","-t",sessionName,"pane-mode-changed",'if -F "#{pane_in_mode}" "set-option -w pane-border-status top" "set-option -w pane-border-status off"']),config.command&&!alreadyRunning)tmuxExecSilent(["send-keys","-t",sessionName,config.command,"Enter"]);let pid=this.getSessionPanePid(sessionName),info={id,name:config.name,status:"active",provider:this.name,command:config.command,workingDirectory:config.workingDirectory,pid:pid??void 0,projectId:config.projectId,createdAt:now,updatedAt:now,metadata:{tmuxSessionName:sessionName,shell:config.shell,size:config.size}};return await this.saveSession(info),this.log.info("Tmux session created",{id,sessionName,pid}),info}async terminate(sessionId){let session=await this.getInfo(sessionId);if(!session){this.log.warn("Terminate called for unknown session",{sessionId});return}let tmuxName=this.getTmuxName(session);if(this.log.info("Terminating tmux session",{sessionId,tmuxName}),this.ttydPids.has(sessionId))await this.stopTerminal(sessionId);tmuxExecSilent(["kill-session","-t",tmuxName]),session.status="terminated",session.updatedAt=nowISO(),session.terminal=void 0,await this.saveSession(session),this.log.info("Tmux session terminated",{sessionId})}async getInfo(sessionId){let session=(await this.loadSessions()).find((s)=>s.id===sessionId)??null;if(session){let tmuxName=this.getTmuxName(session),exists=tmuxExecSilent(["has-session","-t",tmuxName]);if(!exists&&session.status==="active")session.status="inactive",session.updatedAt=nowISO(),await this.saveSession(session);else if(exists&&session.status==="inactive")session.status="active",session.updatedAt=nowISO(),await this.saveSession(session);let termInfo=this.getRunningTerminalInfo(sessionId);if(termInfo)session.terminal=termInfo}return session}async list(){let sessions=await this.loadSessions();for(let session of sessions){if(session.status==="terminated")continue;let tmuxName=this.getTmuxName(session),exists=tmuxExecSilent(["has-session","-t",tmuxName]);if(!exists&&session.status==="active")session.status="inactive",session.updatedAt=nowISO();else if(exists&&session.status!=="active")session.status="active",session.updatedAt=nowISO();let termInfo=this.getRunningTerminalInfo(session.id);if(termInfo)session.terminal=termInfo}return await this.saveSessions(sessions),sessions}async sendCommand(sessionId,command){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.debug("Sending command",{sessionId,command});try{tmuxExec(["send-keys","-t",tmuxName,command,"Enter"])}catch(err){throw this.log.error("Failed to send command",{sessionId,error:String(err)}),Error(`Failed to send command to session ${sessionId}: ${err}`,{cause:err})}}async sendKeys(sessionId,keys){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.debug("Sending keys",{sessionId,keys});try{tmuxExec(["send-keys","-t",tmuxName,keys])}catch(err){throw this.log.error("Failed to send keys",{sessionId,error:String(err)}),Error(`Failed to send keys to session ${sessionId}: ${err}`,{cause:err})}}async sendInterrupt(sessionId){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.debug("Sending interrupt (C-c)",{sessionId});try{tmuxExec(["send-keys","-t",tmuxName,"C-c"])}catch(err){throw this.log.error("Failed to send interrupt",{sessionId,error:String(err)}),Error(`Failed to send interrupt to session ${sessionId}: ${err}`,{cause:err})}}async captureOutput(sessionId){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.debug("Capturing output",{sessionId});try{return tmuxExec(["capture-pane","-t",tmuxName,"-p"])}catch(err){throw this.log.error("Failed to capture output",{sessionId,error:String(err)}),Error(`Failed to capture output from session ${sessionId}: ${err}`,{cause:err})}}async rename(sessionId,newName){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.info("Renaming session",{sessionId,from:session.name,to:newName});try{session.name=newName,session.updatedAt=nowISO(),await this.saveSession(session)}catch(err){throw this.log.error("Failed to rename session",{sessionId,error:String(err)}),Error(`Failed to rename session ${sessionId}: ${err}`,{cause:err})}let newTmuxName=`vibe-${session.id}`;tmuxExecSilent(["rename-session","-t",tmuxName,newTmuxName])}async toggleMouse(sessionId){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session),mouseOn;try{mouseOn=tmuxExec(["show-options","-t",tmuxName,"-v","mouse"]).trim()==="on"}catch{mouseOn=!1}let newState=mouseOn?"off":"on";return tmuxExecSilent(["set","-t",tmuxName,"mouse",newState]),this.log.debug("Toggled mouse",{sessionId,mouse:newState}),newState==="on"}async getTerminationStatus(sessionId){let session=(await this.loadSessions()).find((s)=>s.id===sessionId);if(!session)return{terminated:!0,exists:!1};let tmuxName=this.getTmuxName(session),exists=tmuxExecSilent(["has-session","-t",tmuxName]);return{terminated:session.status==="terminated"||!exists,exists}}async getTerminalInfo(sessionId){return this.getRunningTerminalInfo(sessionId)}async startTerminal(sessionId,port){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session),existing=this.getRunningTerminalInfo(sessionId);if(existing)return this.log.debug("Terminal already running",{sessionId,...existing}),existing;let assignedPort=port??await findAvailablePort(TTYD_BASE_PORT,TTYD_PORT_RANGE);this.log.info("Starting ttyd terminal",{sessionId,tmuxName,port:assignedPort});let ttydEnv={...process.env};ttydEnv.VIBECONTROLS_PROVIDER="tmux";let otherProviderVars={wezterm:["WEZTERM_PANE","WEZTERM_UNIX_SOCKET"],screen:["STY","WINDOW"],zellij:["ZELLIJ","ZELLIJ_SESSION_NAME","ZELLIJ_PANE_ID"]};for(let vars of Object.values(otherProviderVars))for(let v of vars)if(v in ttydEnv)delete ttydEnv[v];let child=Bun.spawn(["ttyd","-t","fontSize=14","-t",'theme={"background":"#1e1e1e","foreground":"#cccccc"}',"--writable","--port",String(assignedPort),"tmux","attach","-t",tmuxName],{stdout:"ignore",stderr:"ignore",stdin:"ignore",env:ttydEnv});if(!child.pid)throw Error("Failed to start ttyd \u2014 no PID returned");await sleep(500),this.ttydPids.set(sessionId,child.pid),this.ttydPorts.set(sessionId,assignedPort),await this.persistTerminals();let terminalInfo={url:`http://localhost:${assignedPort}`,port:assignedPort,pid:child.pid};return session.terminal=terminalInfo,session.updatedAt=nowISO(),await this.saveSession(session),this.log.info("ttyd terminal started",{sessionId,port:assignedPort,pid:child.pid}),terminalInfo}async stopTerminal(sessionId){let pid=this.ttydPids.get(sessionId);if(!pid){this.log.debug("No ttyd process found for session",{sessionId});return}this.log.info("Stopping ttyd terminal",{sessionId,pid}),await gracefulKill(pid),this.ttydPids.delete(sessionId),this.ttydPorts.delete(sessionId),await this.persistTerminals();let session=await this.getInfo(sessionId);if(session)session.terminal=void 0,session.updatedAt=nowISO(),await this.saveSession(session);this.log.info("ttyd terminal stopped",{sessionId})}async listSystemSessions(){try{let raw=tmuxExec(["list-sessions","-F","#{session_id}:#{session_name}:#{session_windows}:#{session_attached}:#{session_created}"]);if(!raw)return[];return raw.split(`
|
|
2
|
+
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}'
|
|
3
|
+
`);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)}}}var TypedStore=class{constructor(storage,namespace,key,logger,pluginName="plugin"){this.storage=storage,this.namespace=namespace,this.key=key,this.logger=logger,this.pluginName=pluginName}storage;namespace;key;logger;pluginName;async get(){let raw=await this.storage.get(this.namespace,this.key);if(raw===null||raw===void 0)return null;if(typeof raw!=="string")return raw;try{return JSON.parse(raw)}catch(err){return this.logger?.error?.(this.pluginName,"TypedStore: corrupt JSON",{namespace:this.namespace,key:this.key,message:err instanceof Error?err.message:String(err)}),null}}async set(value){await this.storage.set(this.namespace,this.key,JSON.stringify(value))}async delete(){return this.storage.delete(this.namespace,this.key)}};import{createServer}from"net";function sleep(ms){return new Promise((resolve)=>setTimeout(resolve,ms))}function isProcessAlive(pid){try{return process.kill(pid,0),!0}catch{return!1}}async function gracefulKill(pid,timeoutMs=3000,logger){if(!isProcessAlive(pid))return;try{process.kill(pid,"SIGTERM")}catch(err){logger?.warn?.("subprocess","SIGTERM failed",{pid,message:err instanceof Error?err.message:String(err)});return}let pollMs=50,elapsed=0;while(elapsed<timeoutMs)if(await sleep(pollMs),elapsed+=pollMs,!isProcessAlive(pid))return;try{process.kill(pid,"SIGKILL")}catch(err){logger?.warn?.("subprocess","SIGKILL failed",{pid,message:err instanceof Error?err.message:String(err)})}}async function findAvailablePort(start,range=200){for(let i=0;i<range;i++){let port=start+i;if(await isPortFree(port))return port}throw Error(`findAvailablePort: no port free in [${start}, ${start+range-1}]`)}function isPortFree(port){return new Promise((resolve)=>{let server=createServer();server.once("error",()=>{resolve(!1)}),server.once("listening",()=>{server.close(()=>resolve(!0))});try{server.listen({port,host:"127.0.0.1"})}catch{resolve(!1)}})}var BoundLogger=class{constructor(logger,source){this.logger=logger,this.source=source}logger;source;info(message,meta){this.logger?.info?.(this.source,message,meta)}warn(message,meta){this.logger?.warn?.(this.source,message,meta)}error(message,meta){this.logger?.error?.(this.source,message,meta)}debug(message,meta){this.logger?.debug?.(this.source,message,meta)}};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)}};var CONTEXT_PROVIDER_TYPE="context",REGISTRY_KEY=Symbol.for("@vibecontrols/plugin-sdk:contextProviders@1");function getRegistry(){let slots=globalThis,existing=slots[REGISTRY_KEY];if(existing)return existing;let created=new Map;return slots[REGISTRY_KEY]=created,created}function registerContextProvider(provider,hostServices){if(!provider||typeof provider.name!=="string"||provider.name.length===0)throw Error("ContextProvider.name is required");if(typeof provider.getContext!=="function")throw Error("ContextProvider.getContext must be a function");getRegistry().set(provider.name,provider),hostServices?.serviceRegistry?.registerProvider?.(CONTEXT_PROVIDER_TYPE,provider,provider.name)}function listContextProviders(){return Array.from(getRegistry().values())}function getContextProvider(name){return getRegistry().get(name)}var ProviderRegistry=class{constructor(hostServices){this.hostServices=hostServices}hostServices;registerContextProvider(provider){registerContextProvider(provider,this.hostServices)}listContextProviders(){return listContextProviders()}getContextProvider(name){return getContextProvider(name)}getServiceRegistry(){return this.hostServices?.serviceRegistry}registerProvider(type,name,provider){this.hostServices?.serviceRegistry?.registerProvider?.(type,provider,name)}getProvider(type,name){let reg=this.hostServices?.serviceRegistry;if(!reg)return;if(name===void 0)return reg.getProvider?.(type);if(reg.getProviderByName)return reg.getProviderByName(type,name);if(!reg.listProvidersForType)return;if(!(reg.listProvidersForType(type)??[]).some((entry)=>typeof entry==="string"?entry===name:entry.pluginName===name))return;return reg.getProvider?.(type)}listProviders(type){return(this.hostServices?.serviceRegistry?.listProvidersForType?.(type)??[]).map((entry)=>typeof entry==="string"?entry:entry.pluginName)}withCliContribution(contribution){let contributors=this.hostServices?.cliContributors;if(!contributors)return;for(let section of contribution.statusSections??[])contributors.addStatusSection?.(section);for(let check of contribution.doctorChecks??[])contributors.addDoctorCheck?.(check)}};import{spawn}from"child_process";import{createHash}from"crypto";import{existsSync,promises,createWriteStream}from"fs";import{homedir,tmpdir}from"os";import*as path from"path";import{Readable}from"stream";import{pipeline}from"stream/promises";function currentPlatform(){let arch=process.arch==="x64"?"x64":"arm64";return`${process.platform==="win32"?"win32":process.platform==="darwin"?"darwin":"linux"}-${arch}`}function toolsCacheRoot(){return path.join(homedir(),".boff","vibecontrols","tools")}function binFileName(name,binaryName){let base=binaryName??name;if(process.platform==="win32"&&!base.toLowerCase().endsWith(".exe"))return`${base}.exe`;return base}function cachedBinaryPath(name,binaryName,cacheRoot){return path.join(cacheRoot??toolsCacheRoot(),name,binFileName(name,binaryName))}function resolveBinary(name,binaryName,cacheRoot){let cached=cachedBinaryPath(name,binaryName,cacheRoot);if(existsSync(cached))return cached;try{let onPath=typeof Bun<"u"&&typeof Bun.which==="function"?Bun.which(binaryName??name,{PATH:process.env.PATH}):null;if(onPath)return onPath}catch{}return null}async function versionOk(binaryPath,spec){if(!spec.versionMatcher)return!0;return new Promise((resolve)=>{let child=spawn(binaryPath,spec.versionArgs??["--version"],{stdio:["ignore","pipe","pipe"]}),out="";child.stdout.on("data",(b)=>out+=b.toString()),child.stderr.on("data",(b)=>out+=b.toString()),child.on("close",()=>{try{resolve(new RegExp(spec.versionMatcher).test(out))}catch{resolve(!1)}}),child.on("error",()=>resolve(!1))})}async function installBinary(spec){let platform=spec.platform??currentPlatform(),binaryName=binFileName(spec.name,spec.binaryName),cached=cachedBinaryPath(spec.name,spec.binaryName,spec.cacheRoot);if(existsSync(cached)&&await versionOk(cached,spec))return cached;try{let onPath=typeof Bun<"u"&&typeof Bun.which==="function"?Bun.which(spec.binaryName??spec.name,{PATH:process.env.PATH}):null;if(onPath&&await versionOk(onPath,spec))return spec.log?.info?.(`[install] using PATH binary ${spec.name} (${onPath})`),onPath}catch{}let download=spec.downloads[platform];if(!download)throw Error(`[install] no download manifest entry for ${spec.name} on ${platform}`);return spec.log?.info?.(`[install] downloading ${spec.name} for ${platform}`),await promises.mkdir(path.dirname(cached),{recursive:!0}),await downloadAndInstall(download,cached,binaryName,spec.name),spec.log?.info?.(`[install] installed ${spec.name} \u2192 ${cached}`),cached}async function downloadAndInstall(d,destBinary,binaryName,toolName){let destDir=path.dirname(destBinary),tmp=path.join(tmpdir(),`vibe-install-${toolName}-${process.pid}`);await promises.mkdir(tmp,{recursive:!0});let archivePath=path.join(tmp,"artifact");try{let res=await fetch(d.url,{redirect:"follow"});if(!res.ok)throw Error(`download failed (${res.status}) for ${d.url}`);if(!res.body)throw Error(`empty response body for ${d.url}`);if(await pipeline(Readable.fromWeb(res.body),createWriteStream(archivePath)),d.sha256){let actual=createHash("sha256").update(await promises.readFile(archivePath)).digest("hex");if(actual!==d.sha256)throw Error(`sha256 mismatch for ${toolName}: expected ${d.sha256}, got ${actual}`)}let archiveType=d.archive??(d.url.endsWith(".tar.gz")||d.url.endsWith(".tgz")?"tar.gz":d.url.endsWith(".zip")?"zip":"raw");if(archiveType==="raw")await promises.copyFile(archivePath,destBinary);else{if(archiveType==="tar.gz")await runProcess("tar",["-xzf",archivePath,"-C",tmp]);else await extractZip(archivePath,tmp);let inner=d.binaryWithinArchive??binaryName;await promises.copyFile(path.join(tmp,inner),destBinary)}if(process.platform!=="win32")await promises.chmod(destBinary,493)}finally{await promises.rm(tmp,{recursive:!0,force:!0})}}async function extractZip(archive,dest){try{await runProcess("unzip",["-q","-o",archive,"-d",dest]);return}catch(err){if(process.platform!=="win32")throw err;await runProcess("tar",["-xf",archive,"-C",dest])}}async function runProcess(cmd,args){return new Promise((resolve,reject)=>{let child=spawn(cmd,args,{stdio:["ignore","ignore","pipe"]}),err="";child.stderr.on("data",(b)=>err+=b.toString()),child.on("close",(code)=>code===0?resolve():reject(Error(`${cmd} exit ${code}: ${err}`))),child.on("error",reject)})}var TTYD_DOWNLOADS={"linux-x64":{url:"https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.x86_64",archive:"raw"},"linux-arm64":{url:"https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.aarch64",archive:"raw"},"win32-x64":{url:"https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.win32.exe",archive:"raw"}};function resolveTtydCmd(){return resolveBinary("ttyd")??(process.platform==="win32"?"ttyd.exe":"ttyd")}var TTYD_HOST="127.0.0.1",TTYD_WS_PATH="/ws",TTYD_SUBPROTOCOLS=["tty"],PLUGIN_NAME="session-tmux",PLUGIN_VERSION="2.3.0",PROVIDER_NAME="session-tmux",STORAGE_NAMESPACE="session-tmux",STORAGE_KEY_SESSIONS="sessions",STORAGE_KEY_TERMINALS="terminals",TTYD_BASE_PORT=7681,TTYD_PORT_RANGE=200;function generateId(){let bytes=new Uint8Array(4);return crypto.getRandomValues(bytes),[...bytes].map((b)=>b.toString(16).padStart(2,"0")).join("")}function tmuxExec(args){let result=Bun.spawnSync(["tmux",...args],{stdout:"pipe",stderr:"pipe",timeout:1e4});if(result.exitCode!==0){let stderr=result.stderr.toString().trim();throw Error(`tmux exited with code ${result.exitCode}: ${stderr}`)}return result.stdout.toString("utf-8").trimEnd()}function tmuxExecSilent(args){try{return Bun.spawnSync(["tmux",...args],{stdout:"pipe",stderr:"pipe",timeout:1e4}).exitCode===0}catch{return!1}}function nowISO(){return new Date().toISOString()}class TmuxSessionProvider{name=PROVIDER_NAME;log=new BoundLogger(void 0,PLUGIN_NAME);sessionsStore=null;terminalsStore=null;ttydPids=new Map;ttydPorts=new Map;async init(services){if(this.log=new BoundLogger(services.logger,PLUGIN_NAME),services.storage)this.sessionsStore=new TypedStore(services.storage,STORAGE_NAMESPACE,STORAGE_KEY_SESSIONS,services.logger,PLUGIN_NAME),this.terminalsStore=new TypedStore(services.storage,STORAGE_NAMESPACE,STORAGE_KEY_TERMINALS,services.logger,PLUGIN_NAME);this.log.info("TmuxSessionProvider initialising",{provider:this.name});try{let version=tmuxExec(["-V"]);this.log.info("tmux detected",{version})}catch{this.log.error("tmux is not installed or not in PATH \u2014 session provider will not function")}await this.reconcileSessions(),this.log.info("TmuxSessionProvider ready")}async shutdown(context){if(context?.reason==="reload"){this.log.info("Hot-reload: preserving tmux sessions and ttyd processes"),await this.persistTerminals(),this.ttydPids.clear(),this.ttydPorts.clear();return}this.log.info("TmuxSessionProvider shutting down \u2014 stopping terminals");let stopPromises=[];for(let[sessionId]of this.ttydPids)stopPromises.push(this.stopTerminal(sessionId));await Promise.allSettled(stopPromises);try{await this.terminalsStore?.delete()}catch{}this.log.info("TmuxSessionProvider shutdown complete")}async nuke(){this.log.info("TmuxSessionProvider nuke \u2014 reaping spawned terminals");let tracked=[...this.ttydPids.keys()],terminalsReaped=0;await Promise.allSettled(tracked.map(async(sessionId)=>{let pid=this.ttydPids.get(sessionId);if(await this.stopTerminal(sessionId),pid!==void 0)terminalsReaped++}));try{let raw=Bun.spawnSync(["pgrep","-a","ttyd"],{stdout:"pipe",stderr:"pipe",timeout:5000}).stdout.toString().trim();if(raw){let stillTracked=new Set(this.ttydPids.values());for(let line of raw.split(`
|
|
4
|
+
`)){let parts=line.trim().split(/\s+/),pid=parseInt(parts[0]??"0",10);if(!pid||stillTracked.has(pid))continue;let tIdx=parts.indexOf("-t"),tmuxTarget=tIdx!==-1?parts[tIdx+1]:null;if(!tmuxTarget||!tmuxTarget.startsWith("vibe-"))continue;if(!isProcessAlive(pid))continue;this.log.info("Reaping untracked ttyd attached to vibe session",{pid,tmuxTarget}),await gracefulKill(pid),terminalsReaped++}}}catch{}this.ttydPids.clear(),this.ttydPorts.clear();try{await this.terminalsStore?.delete()}catch{}try{await this.sessionsStore?.delete()}catch{}return this.log.info("TmuxSessionProvider nuke complete",{terminalsReaped}),{terminalsReaped}}async create(config){if(config.externalName){let target=config.externalName;if(target.length===0||target.length>128||/[\0\r\n:.\s]/.test(target))throw Error(`Invalid externalName: ${target}`);if(tmuxExecSilent(["has-session","-t",target]))return this.log.info("create(): attaching to existing tmux session",{target}),this.adopt(target,config.name);let now2=nowISO(),args=["new-session","-d","-s",target];if(config.workingDirectory)args.push("-c",config.workingDirectory);if(config.size)args.push("-x",String(config.size.cols),"-y",String(config.size.rows));if(config.shell)args.push(config.shell);if(tmuxExec(args),config.environment)for(let[k,v]of Object.entries(config.environment))tmuxExecSilent(["set-environment","-t",target,k,v]);if(tmuxExecSilent(["set","-t",target,"mouse","on"]),config.command)tmuxExecSilent(["send-keys","-t",target,config.command,"Enter"]);let id2=target.startsWith("vibe-")?target.slice(5)||target:target,pid2=this.getSessionPanePid(target),info2={id:id2,name:config.name,status:"active",provider:this.name,command:config.command,workingDirectory:config.workingDirectory,pid:pid2??void 0,projectId:config.projectId,createdAt:now2,updatedAt:now2,metadata:{tmuxSessionName:target,shell:config.shell,size:config.size,createdWithExplicitName:!0}};return await this.saveSession(info2),this.log.info("Tmux session created with explicit name",{id:id2,target}),info2}let id=config.id||generateId(),sessionName=`vibe-${id.substring(0,8)}`,now=nowISO();this.log.info("Creating tmux session",{id,name:config.name,sessionName});let alreadyRunning=tmuxExecSilent(["has-session","-t",sessionName]);if(alreadyRunning)this.log.info("Reusing existing tmux session on resume",{id,sessionName});else{let args=["new-session","-d","-s",sessionName];if(config.workingDirectory)args.push("-c",config.workingDirectory);if(config.size)args.push("-x",String(config.size.cols),"-y",String(config.size.rows));if(config.shell)args.push(config.shell);try{tmuxExec(args)}catch(err){throw this.log.error("Failed to create tmux session",{id,error:String(err)}),Error(`Failed to create tmux session: ${err}`,{cause:err})}}if(config.environment)for(let[key,value]of Object.entries(config.environment))tmuxExecSilent(["set-environment","-t",sessionName,key,value]);if(tmuxExecSilent(["set","-t",sessionName,"mouse","on"]),tmuxExecSilent(["set","-t",sessionName,"pane-border-format"," #[align=centre]#[bg=colour208,fg=black,bold] \u2B06 SCROLL MODE \u2014 press Esc or q to resume typing \u2B06 #[default]"]),tmuxExecSilent(["set-hook","-t",sessionName,"pane-mode-changed",'if -F "#{pane_in_mode}" "set-option -w pane-border-status top" "set-option -w pane-border-status off"']),config.command&&!alreadyRunning)tmuxExecSilent(["send-keys","-t",sessionName,config.command,"Enter"]);let pid=this.getSessionPanePid(sessionName),info={id,name:config.name,status:"active",provider:this.name,command:config.command,workingDirectory:config.workingDirectory,pid:pid??void 0,projectId:config.projectId,createdAt:now,updatedAt:now,metadata:{tmuxSessionName:sessionName,shell:config.shell,size:config.size}};return await this.saveSession(info),this.log.info("Tmux session created",{id,sessionName,pid}),info}async terminate(sessionId){let session=await this.getInfo(sessionId);if(!session){this.log.warn("Terminate called for unknown session",{sessionId});return}let tmuxName=this.getTmuxName(session);if(this.log.info("Terminating tmux session",{sessionId,tmuxName}),this.ttydPids.has(sessionId))await this.stopTerminal(sessionId);tmuxExecSilent(["kill-session","-t",tmuxName]),session.status="terminated",session.updatedAt=nowISO(),session.terminal=void 0,await this.saveSession(session),this.log.info("Tmux session terminated",{sessionId})}async getInfo(sessionId){let session=(await this.loadSessions()).find((s)=>s.id===sessionId)??null;if(session){let tmuxName=this.getTmuxName(session),exists=tmuxExecSilent(["has-session","-t",tmuxName]);if(!exists&&session.status==="active")session.status="inactive",session.updatedAt=nowISO(),await this.saveSession(session);else if(exists&&session.status==="inactive")session.status="active",session.updatedAt=nowISO(),await this.saveSession(session);let termInfo=this.getRunningTerminalInfo(sessionId);if(termInfo)session.terminal=termInfo}return session}async list(){let sessions=await this.loadSessions();for(let session of sessions){if(session.status==="terminated")continue;let tmuxName=this.getTmuxName(session),exists=tmuxExecSilent(["has-session","-t",tmuxName]);if(!exists&&session.status==="active")session.status="inactive",session.updatedAt=nowISO();else if(exists&&session.status!=="active")session.status="active",session.updatedAt=nowISO();let termInfo=this.getRunningTerminalInfo(session.id);if(termInfo)session.terminal=termInfo}return await this.saveSessions(sessions),sessions}async sendCommand(sessionId,command){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.debug("Sending command",{sessionId,command});try{tmuxExec(["send-keys","-t",tmuxName,command,"Enter"])}catch(err){throw this.log.error("Failed to send command",{sessionId,error:String(err)}),Error(`Failed to send command to session ${sessionId}: ${err}`,{cause:err})}}async sendKeys(sessionId,keys){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.debug("Sending keys",{sessionId,keys});try{tmuxExec(["send-keys","-t",tmuxName,keys])}catch(err){throw this.log.error("Failed to send keys",{sessionId,error:String(err)}),Error(`Failed to send keys to session ${sessionId}: ${err}`,{cause:err})}}async sendInterrupt(sessionId){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.debug("Sending interrupt (C-c)",{sessionId});try{tmuxExec(["send-keys","-t",tmuxName,"C-c"])}catch(err){throw this.log.error("Failed to send interrupt",{sessionId,error:String(err)}),Error(`Failed to send interrupt to session ${sessionId}: ${err}`,{cause:err})}}async captureOutput(sessionId){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.debug("Capturing output",{sessionId});try{return tmuxExec(["capture-pane","-t",tmuxName,"-p"])}catch(err){throw this.log.error("Failed to capture output",{sessionId,error:String(err)}),Error(`Failed to capture output from session ${sessionId}: ${err}`,{cause:err})}}async rename(sessionId,newName){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.info("Renaming session",{sessionId,from:session.name,to:newName});try{session.name=newName,session.updatedAt=nowISO(),await this.saveSession(session)}catch(err){throw this.log.error("Failed to rename session",{sessionId,error:String(err)}),Error(`Failed to rename session ${sessionId}: ${err}`,{cause:err})}let newTmuxName=`vibe-${session.id}`;tmuxExecSilent(["rename-session","-t",tmuxName,newTmuxName])}async toggleMouse(sessionId){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session),mouseOn;try{mouseOn=tmuxExec(["show-options","-t",tmuxName,"-v","mouse"]).trim()==="on"}catch{mouseOn=!1}let newState=mouseOn?"off":"on";return tmuxExecSilent(["set","-t",tmuxName,"mouse",newState]),this.log.debug("Toggled mouse",{sessionId,mouse:newState}),newState==="on"}async getTerminationStatus(sessionId){let session=(await this.loadSessions()).find((s)=>s.id===sessionId);if(!session)return{terminated:!0,exists:!1};let tmuxName=this.getTmuxName(session),exists=tmuxExecSilent(["has-session","-t",tmuxName]);return{terminated:session.status==="terminated"||!exists,exists}}async getTerminalInfo(sessionId){return this.getRunningTerminalInfo(sessionId)}async startTerminal(sessionId,port){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session),existing=this.getRunningTerminalInfo(sessionId);if(existing)return this.log.debug("Terminal already running",{sessionId,...existing}),existing;let assignedPort=port??await findAvailablePort(TTYD_BASE_PORT,TTYD_PORT_RANGE);this.log.info("Starting ttyd terminal",{sessionId,tmuxName,port:assignedPort});let ttydEnv={...process.env};ttydEnv.VIBECONTROLS_PROVIDER="tmux";let otherProviderVars={wezterm:["WEZTERM_PANE","WEZTERM_UNIX_SOCKET"],screen:["STY","WINDOW"],zellij:["ZELLIJ","ZELLIJ_SESSION_NAME","ZELLIJ_PANE_ID"]};for(let vars of Object.values(otherProviderVars))for(let v of vars)if(v in ttydEnv)delete ttydEnv[v];let child=Bun.spawn([resolveTtydCmd(),"-t","fontSize=14","-t",'theme={"background":"#1e1e1e","foreground":"#cccccc"}',"--writable","--port",String(assignedPort),"tmux","attach","-t",tmuxName],{stdout:"ignore",stderr:"ignore",stdin:"ignore",env:ttydEnv});if(!child.pid)throw Error("Failed to start ttyd \u2014 no PID returned");await sleep(500),this.ttydPids.set(sessionId,child.pid),this.ttydPorts.set(sessionId,assignedPort),await this.persistTerminals();let terminalInfo={url:`http://localhost:${assignedPort}`,port:assignedPort,pid:child.pid,host:TTYD_HOST,wsPath:TTYD_WS_PATH,subprotocols:[...TTYD_SUBPROTOCOLS]};return session.terminal=terminalInfo,session.updatedAt=nowISO(),await this.saveSession(session),this.log.info("ttyd terminal started",{sessionId,port:assignedPort,pid:child.pid}),terminalInfo}async stopTerminal(sessionId){let pid=this.ttydPids.get(sessionId);if(!pid){this.log.debug("No ttyd process found for session",{sessionId});return}this.log.info("Stopping ttyd terminal",{sessionId,pid}),await gracefulKill(pid),this.ttydPids.delete(sessionId),this.ttydPorts.delete(sessionId),await this.persistTerminals();let session=await this.getInfo(sessionId);if(session)session.terminal=void 0,session.updatedAt=nowISO(),await this.saveSession(session);this.log.info("ttyd terminal stopped",{sessionId})}async listSystemSessions(){try{let raw=tmuxExec(["list-sessions","-F","#{session_id}:#{session_name}:#{session_windows}:#{session_attached}:#{session_created}"]);if(!raw)return[];return raw.split(`
|
|
4
5
|
`).map((line)=>{let[id,name,windows,attached,created]=line.split(":");return{id:id??"",name:name??"",windows:parseInt(windows??"0",10),attached:attached==="1",createdAt:created?new Date(parseInt(created,10)*1000).toISOString():void 0}})}catch{return[]}}async listSystemTerminals(){let terminals=[];for(let[sessionId,pid]of this.ttydPids){let port=this.ttydPorts.get(sessionId);if(pid&&port!==void 0)terminals.push({pid,port,sessionId})}try{let raw=Bun.spawnSync(["pgrep","-a","ttyd"],{stdout:"pipe",stderr:"pipe",timeout:5000}).stdout.toString().trim();if(raw)for(let line of raw.split(`
|
|
5
|
-
`)){let parts=line.trim().split(/\s+/),pid=parseInt(parts[0]??"0",10);if(!pid)continue;if([...this.ttydPids.values()].includes(pid))continue;let portIdx=parts.indexOf("--port"),port=portIdx!==-1?parseInt(parts[portIdx+1]??"0",10):0;terminals.push({pid,port:port||0})}}catch{}return terminals}async bulkKillSystemSessions(sessionIds){let killed=0,failed=0;for(let sid of sessionIds)if(tmuxExecSilent(["kill-session","-t",sid]))killed++;else failed++;return this.log.info("Bulk kill system sessions",{killed,failed}),{killed,failed}}async bulkKillSystemTerminals(pids){let killed=0,failed=0,killPromises=pids.map(async(pid)=>{try{await gracefulKill(pid),killed++;for(let[sessionId,trackedPid]of this.ttydPids)if(trackedPid===pid){this.ttydPids.delete(sessionId),this.ttydPorts.delete(sessionId);break}}catch{failed++}});return await Promise.allSettled(killPromises),this.log.info("Bulk kill system terminals",{killed,failed}),{killed,failed}}async healthCheck(){let tmuxOk,tmuxVersion,sessionCount=0,terminalCount=this.ttydPids.size;try{tmuxVersion=tmuxExec(["-V"]),tmuxOk=!0}catch{return{ok:!1,sessions:0,terminals:terminalCount,message:"tmux is not available"}}try{sessionCount=(await this.listSystemSessions()).length}catch{}let ttydOk=!1;try{ttydOk=Bun.which("ttyd")!==null}catch{}let messages=[tmuxVersion];if(!ttydOk)messages.push("ttyd not found \u2014 web terminals unavailable");return{ok:tmuxOk,sessions:sessionCount,terminals:terminalCount,message:messages.join("; ")}}async getSessionsByProject(projectId){return(await this.list()).filter((s)=>s.projectId===projectId)}async cleanup(){this.log.info("Running cleanup");let sessions=await this.loadSessions(),cleaned=0,kept=[];for(let session of sessions){let tmuxName=this.getTmuxName(session),exists=tmuxExecSilent(["has-session","-t",tmuxName]);if(session.status==="terminated"||!exists){if(this.ttydPids.has(session.id))await this.stopTerminal(session.id);if(exists&&session.status==="terminated")tmuxExecSilent(["kill-session","-t",tmuxName]);cleaned++,this.log.debug("Cleaned session",{id:session.id,name:session.name})}else kept.push(session)}return await this.saveSessions(kept),this.log.info("Cleanup complete",{cleaned,remaining:kept.length}),{cleaned}}async get(sessionId){return this.getInfo(sessionId)}async kill(sessionId){return this.terminate(sessionId)}async interrupt(sessionId){return this.sendInterrupt(sessionId)}async capture(sessionId){return this.captureOutput(sessionId)}async resize(sessionId,cols,rows){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);tmuxExecSilent(["resize-window","-t",tmuxName,"-x",String(cols),"-y",String(rows)])}async listSystem(){return this.listSystemSessions()}async killSystem(sessionId){tmuxExecSilent(["kill-session","-t",sessionId])}async killSystemTerminal(pid){await gracefulKill(pid);for(let[sessionId,trackedPid]of this.ttydPids)if(trackedPid===pid){this.ttydPids.delete(sessionId),this.ttydPorts.delete(sessionId);break}}async discoverOrphans(){let known=new Set((await this.loadSessions()).filter((s)=>s.status!=="terminated").map((s)=>this.getTmuxName(s)));return(await this.listSystemSessions()).filter((s)=>s.name.startsWith("vibe-")&&!known.has(s.name)).map((s)=>({externalName:s.name,provider:"tmux",windows:s.windows,attached:s.attached,createdAt:s.createdAt}))}async adopt(externalName,displayName){let tmuxName=externalName;if(!tmuxName.startsWith("vibe-"))throw Error(`Refusing to adopt tmux session "${tmuxName}" \u2014 only vibe-* sessions are adoptable`);if(!tmuxExecSilent(["has-session","-t",tmuxName]))throw Error(`Tmux session "${tmuxName}" does not exist`);let id=tmuxName.slice(5)||tmuxName,existing=(await this.loadSessions()).find((s)=>s.id===id);if(existing){if(this.log.info("Adopt: session already tracked",{id,tmuxName}),!this.ttydPids.has(id))try{await this.startTerminal(id)}catch(err){this.log.warn("Adopt: failed to start ttyd for tracked session",{id,error:String(err)})}return await this.getInfo(id)??existing}let now=nowISO(),pid=this.getSessionPanePid(tmuxName),info={id,name:displayName||tmuxName,status:"active",provider:this.name,pid:pid??void 0,createdAt:now,updatedAt:now,metadata:{tmuxSessionName:tmuxName,adopted:!0}};await this.saveSession(info),this.log.info("Adopted orphan tmux session",{id,tmuxName});try{let term=await this.startTerminal(id);info.terminal=term,await this.saveSession(info)}catch(err){this.log.warn("Adopt: ttyd start failed (session still adopted)",{id,error:String(err)})}return info}getCapabilities(){return{provider:"tmux",features:{mouse:!0,resize:!0,capture:!0,webTerminal:!0,splitPanes:!0,tabs:!1,scrollback:!0,clipboard:!0,search:!0},platform:["linux","macos"]}}async getScrollback(sessionId,lines){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.debug("Getting scrollback",{sessionId,lines});try{return tmuxExec(["capture-pane","-t",tmuxName,"-p","-S",`-${lines}`])}catch(err){throw this.log.error("Failed to get scrollback",{sessionId,error:String(err)}),Error(`Failed to get scrollback for session ${sessionId}: ${err}`,{cause:err})}}async searchOutput(sessionId,pattern){let lines=(await this.getScrollback(sessionId,1e4)).split(`
|
|
6
|
+
`)){let parts=line.trim().split(/\s+/),pid=parseInt(parts[0]??"0",10);if(!pid)continue;if([...this.ttydPids.values()].includes(pid))continue;let portIdx=parts.indexOf("--port"),port=portIdx!==-1?parseInt(parts[portIdx+1]??"0",10):0;terminals.push({pid,port:port||0})}}catch{}return terminals}async bulkKillSystemSessions(sessionIds){let killed=0,failed=0;for(let sid of sessionIds)if(tmuxExecSilent(["kill-session","-t",sid]))killed++;else failed++;return this.log.info("Bulk kill system sessions",{killed,failed}),{killed,failed}}async bulkKillSystemTerminals(pids){let killed=0,failed=0,killPromises=pids.map(async(pid)=>{try{await gracefulKill(pid),killed++;for(let[sessionId,trackedPid]of this.ttydPids)if(trackedPid===pid){this.ttydPids.delete(sessionId),this.ttydPorts.delete(sessionId);break}}catch{failed++}});return await Promise.allSettled(killPromises),this.log.info("Bulk kill system terminals",{killed,failed}),{killed,failed}}async healthCheck(){let tmuxOk,tmuxVersion,sessionCount=0,terminalCount=this.ttydPids.size;try{tmuxVersion=tmuxExec(["-V"]),tmuxOk=!0}catch{return{ok:!1,sessions:0,terminals:terminalCount,message:"tmux is not available"}}try{sessionCount=(await this.listSystemSessions()).length}catch{}let ttydOk=!1;try{ttydOk=resolveBinary("ttyd")!==null||Bun.which("ttyd")!==null}catch{}let messages=[tmuxVersion];if(!ttydOk)messages.push("ttyd not found \u2014 web terminals unavailable");return{ok:tmuxOk,sessions:sessionCount,terminals:terminalCount,message:messages.join("; ")}}async getSessionsByProject(projectId){return(await this.list()).filter((s)=>s.projectId===projectId)}async cleanup(){this.log.info("Running cleanup");let sessions=await this.loadSessions(),cleaned=0,kept=[];for(let session of sessions){let tmuxName=this.getTmuxName(session),exists=tmuxExecSilent(["has-session","-t",tmuxName]);if(session.status==="terminated"||!exists){if(this.ttydPids.has(session.id))await this.stopTerminal(session.id);if(exists&&session.status==="terminated")tmuxExecSilent(["kill-session","-t",tmuxName]);cleaned++,this.log.debug("Cleaned session",{id:session.id,name:session.name})}else kept.push(session)}return await this.saveSessions(kept),this.log.info("Cleanup complete",{cleaned,remaining:kept.length}),{cleaned}}async get(sessionId){return this.getInfo(sessionId)}async kill(sessionId){return this.terminate(sessionId)}async interrupt(sessionId){return this.sendInterrupt(sessionId)}async capture(sessionId){return this.captureOutput(sessionId)}async resize(sessionId,cols,rows){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);tmuxExecSilent(["resize-window","-t",tmuxName,"-x",String(cols),"-y",String(rows)])}async listSystem(){return this.listSystemSessions()}async killSystem(sessionId){tmuxExecSilent(["kill-session","-t",sessionId])}async killSystemTerminal(pid){await gracefulKill(pid);for(let[sessionId,trackedPid]of this.ttydPids)if(trackedPid===pid){this.ttydPids.delete(sessionId),this.ttydPorts.delete(sessionId);break}}async discoverOrphans(){let known=new Set((await this.loadSessions()).filter((s)=>s.status!=="terminated").map((s)=>this.getTmuxName(s)));return(await this.listSystemSessions()).filter((s)=>s.name.startsWith("vibe-")&&!known.has(s.name)).map((s)=>({externalName:s.name,provider:"tmux",windows:s.windows,attached:s.attached,createdAt:s.createdAt}))}async adopt(externalName,displayName){let tmuxName=externalName;if(!tmuxName.startsWith("vibe-"))throw Error(`Refusing to adopt tmux session "${tmuxName}" \u2014 only vibe-* sessions are adoptable`);if(!tmuxExecSilent(["has-session","-t",tmuxName]))throw Error(`Tmux session "${tmuxName}" does not exist`);let id=tmuxName.slice(5)||tmuxName,existing=(await this.loadSessions()).find((s)=>s.id===id);if(existing){if(this.log.info("Adopt: session already tracked",{id,tmuxName}),!this.ttydPids.has(id))try{await this.startTerminal(id)}catch(err){this.log.warn("Adopt: failed to start ttyd for tracked session",{id,error:String(err)})}return await this.getInfo(id)??existing}let now=nowISO(),pid=this.getSessionPanePid(tmuxName),info={id,name:displayName||tmuxName,status:"active",provider:this.name,pid:pid??void 0,createdAt:now,updatedAt:now,metadata:{tmuxSessionName:tmuxName,adopted:!0}};await this.saveSession(info),this.log.info("Adopted orphan tmux session",{id,tmuxName});try{let term=await this.startTerminal(id);info.terminal=term,await this.saveSession(info)}catch(err){this.log.warn("Adopt: ttyd start failed (session still adopted)",{id,error:String(err)})}return info}getCapabilities(){return{provider:"tmux",features:{mouse:!0,resize:!0,capture:!0,webTerminal:!0,splitPanes:!0,tabs:!1,scrollback:!0,clipboard:!0,search:!0},platform:["linux","macos"]}}async getScrollback(sessionId,lines){let session=await this.requireSession(sessionId),tmuxName=this.getTmuxName(session);this.log.debug("Getting scrollback",{sessionId,lines});try{return tmuxExec(["capture-pane","-t",tmuxName,"-p","-S",`-${lines}`])}catch(err){throw this.log.error("Failed to get scrollback",{sessionId,error:String(err)}),Error(`Failed to get scrollback for session ${sessionId}: ${err}`,{cause:err})}}async searchOutput(sessionId,pattern){let lines=(await this.getScrollback(sessionId,1e4)).split(`
|
|
6
7
|
`),results=[],regex;try{regex=new RegExp(pattern)}catch{regex=new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"))}for(let i=0;i<lines.length;i++)if(regex.test(lines[i]??""))results.push({line:i+1,content:lines[i]??""});return this.log.debug("Search output completed",{sessionId,pattern,matches:results.length}),results}async loadSessions(){if(!this.sessionsStore)return[];return await this.sessionsStore.get()??[]}async saveSessions(sessions){if(!this.sessionsStore)return;try{await this.sessionsStore.set(sessions)}catch(err){this.log.error("Failed to save sessions to storage",{error:String(err)})}}async saveSession(session){let sessions=await this.loadSessions(),idx=sessions.findIndex((s)=>s.id===session.id);if(idx>=0)sessions[idx]=session;else sessions.push(session);await this.saveSessions(sessions)}getTmuxName(session){let meta=session.metadata;if(meta&&typeof meta.tmuxSessionName==="string")return meta.tmuxSessionName;return`vibe-${session.id}`}getSessionPanePid(tmuxName){try{let raw=tmuxExec(["list-panes","-t",tmuxName,"-F","#{pane_pid}"]),pid=parseInt(raw.split(`
|
|
7
|
-
`)[0]??"",10);return isNaN(pid)?null:pid}catch{return null}}async requireSession(sessionId){let session=await this.getInfo(sessionId);if(!session)throw Error(`Session not found: ${sessionId}`);if(session.status==="terminated")throw Error(`Session is terminated: ${sessionId}`);return session}getRunningTerminalInfo(sessionId){let pid=this.ttydPids.get(sessionId),port=this.ttydPorts.get(sessionId);if(!pid||port===void 0)return null;if(!isProcessAlive(pid))return this.ttydPids.delete(sessionId),this.ttydPorts.delete(sessionId),null;return{url:`http://localhost:${port}`,port,pid}}async persistTerminals(){if(!this.terminalsStore)return;let data={};for(let[sessionId,pid]of this.ttydPids){let port=this.ttydPorts.get(sessionId);if(port!==void 0)data[sessionId]={pid,port}}try{await this.terminalsStore.set(data)}catch{this.log.warn("Failed to persist terminal state")}}async loadPersistedTerminals(){if(!this.terminalsStore)return{};return await this.terminalsStore.get()??{}}async reconcileSessions(){let sessions=await this.loadSessions(),persistedTerminals=await this.loadPersistedTerminals(),changed=!1;for(let session of sessions){if(session.status==="terminated")continue;let tmuxName=this.getTmuxName(session),exists=tmuxExecSilent(["has-session","-t",tmuxName]);if(!exists){if(session.status==="active")session.status="inactive",session.updatedAt=nowISO(),session.terminal=void 0,changed=!0,this.log.info("Reconciled: tmux session gone, marking inactive",{id:session.id});let termData2=persistedTerminals[session.id];if(termData2&&isProcessAlive(termData2.pid))this.log.info("Killing orphaned ttyd for dead tmux session",{sessionId:session.id,pid:termData2.pid}),await gracefulKill(termData2.pid);delete persistedTerminals[session.id];continue}let termData=persistedTerminals[session.id];if(termData)if(isProcessAlive(termData.pid))this.ttydPids.set(session.id,termData.pid),this.ttydPorts.set(session.id,termData.port),session.terminal={url:`http://localhost:${termData.port}`,port:termData.port,pid:termData.pid},session.status="active",session.updatedAt=nowISO(),changed=!0,this.log.info("Recovered ttyd process",{sessionId:session.id,pid:termData.pid,port:termData.port});else session.terminal=void 0,changed=!0,delete persistedTerminals[session.id],this.log.info("Stale ttyd record cleared (process dead)",{sessionId:session.id});if(exists&&session.status!=="active")session.status="active",session.updatedAt=nowISO(),changed=!0}try{let raw=Bun.spawnSync(["pgrep","-a","ttyd"],{stdout:"pipe",stderr:"pipe",timeout:5000}).stdout.toString().trim();if(raw)for(let line of raw.split(`
|
|
8
|
-
`)){let parts=line.trim().split(/\s+/),pid=parseInt(parts[0]??"0",10);if(!pid)continue;if([...this.ttydPids.values()].includes(pid))continue;let tIdx=parts.indexOf("-t"),tmuxTarget=tIdx!==-1?parts[tIdx+1]:null;if(!tmuxTarget)continue;let matchedSession=sessions.find((s)=>s.status!=="terminated"&&this.getTmuxName(s)===tmuxTarget);if(!matchedSession)continue;let portIdx=parts.indexOf("--port"),port=portIdx!==-1?parseInt(parts[portIdx+1]??"0",10):0;if(!port)continue;this.ttydPids.set(matchedSession.id,pid),this.ttydPorts.set(matchedSession.id,port),matchedSession.terminal={url:`http://localhost:${port}`,port,pid},matchedSession.status="active",matchedSession.updatedAt=nowISO(),changed=!0,this.log.info("Adopted orphaned ttyd process",{sessionId:matchedSession.id,pid,port,tmuxTarget})}}catch{}if(changed)await this.saveSessions(sessions);await this.persistTerminals()}}var provider=new TmuxSessionProvider;function whichSync(bin){return Bun.which(bin)??null}function
|
|
8
|
+
`)[0]??"",10);return isNaN(pid)?null:pid}catch{return null}}async requireSession(sessionId){let session=await this.getInfo(sessionId);if(!session)throw Error(`Session not found: ${sessionId}`);if(session.status==="terminated")throw Error(`Session is terminated: ${sessionId}`);return session}getRunningTerminalInfo(sessionId){let pid=this.ttydPids.get(sessionId),port=this.ttydPorts.get(sessionId);if(!pid||port===void 0)return null;if(!isProcessAlive(pid))return this.ttydPids.delete(sessionId),this.ttydPorts.delete(sessionId),null;return{url:`http://localhost:${port}`,port,pid,host:TTYD_HOST,wsPath:TTYD_WS_PATH,subprotocols:[...TTYD_SUBPROTOCOLS]}}async persistTerminals(){if(!this.terminalsStore)return;let data={};for(let[sessionId,pid]of this.ttydPids){let port=this.ttydPorts.get(sessionId);if(port!==void 0)data[sessionId]={pid,port}}try{await this.terminalsStore.set(data)}catch{this.log.warn("Failed to persist terminal state")}}async loadPersistedTerminals(){if(!this.terminalsStore)return{};return await this.terminalsStore.get()??{}}async reconcileSessions(){let sessions=await this.loadSessions(),persistedTerminals=await this.loadPersistedTerminals(),changed=!1;for(let session of sessions){if(session.status==="terminated")continue;let tmuxName=this.getTmuxName(session),exists=tmuxExecSilent(["has-session","-t",tmuxName]);if(!exists){if(session.status==="active")session.status="inactive",session.updatedAt=nowISO(),session.terminal=void 0,changed=!0,this.log.info("Reconciled: tmux session gone, marking inactive",{id:session.id});let termData2=persistedTerminals[session.id];if(termData2&&isProcessAlive(termData2.pid))this.log.info("Killing orphaned ttyd for dead tmux session",{sessionId:session.id,pid:termData2.pid}),await gracefulKill(termData2.pid);delete persistedTerminals[session.id];continue}let termData=persistedTerminals[session.id];if(termData)if(isProcessAlive(termData.pid))this.ttydPids.set(session.id,termData.pid),this.ttydPorts.set(session.id,termData.port),session.terminal={url:`http://localhost:${termData.port}`,port:termData.port,pid:termData.pid,host:TTYD_HOST,wsPath:TTYD_WS_PATH,subprotocols:[...TTYD_SUBPROTOCOLS]},session.status="active",session.updatedAt=nowISO(),changed=!0,this.log.info("Recovered ttyd process",{sessionId:session.id,pid:termData.pid,port:termData.port});else session.terminal=void 0,changed=!0,delete persistedTerminals[session.id],this.log.info("Stale ttyd record cleared (process dead)",{sessionId:session.id});if(exists&&session.status!=="active")session.status="active",session.updatedAt=nowISO(),changed=!0}try{let raw=Bun.spawnSync(["pgrep","-a","ttyd"],{stdout:"pipe",stderr:"pipe",timeout:5000}).stdout.toString().trim();if(raw)for(let line of raw.split(`
|
|
9
|
+
`)){let parts=line.trim().split(/\s+/),pid=parseInt(parts[0]??"0",10);if(!pid)continue;if([...this.ttydPids.values()].includes(pid))continue;let tIdx=parts.indexOf("-t"),tmuxTarget=tIdx!==-1?parts[tIdx+1]:null;if(!tmuxTarget)continue;let matchedSession=sessions.find((s)=>s.status!=="terminated"&&this.getTmuxName(s)===tmuxTarget);if(!matchedSession)continue;let portIdx=parts.indexOf("--port"),port=portIdx!==-1?parseInt(parts[portIdx+1]??"0",10):0;if(!port)continue;this.ttydPids.set(matchedSession.id,pid),this.ttydPorts.set(matchedSession.id,port),matchedSession.terminal={url:`http://localhost:${port}`,port,pid,host:TTYD_HOST,wsPath:TTYD_WS_PATH,subprotocols:[...TTYD_SUBPROTOCOLS]},matchedSession.status="active",matchedSession.updatedAt=nowISO(),changed=!0,this.log.info("Adopted orphaned ttyd process",{sessionId:matchedSession.id,pid,port,tmuxTarget})}}catch{}if(changed)await this.saveSessions(sessions);await this.persistTerminals()}}var provider=new TmuxSessionProvider;function whichSync(bin){return Bun.which(bin)??null}function ttydManualInstall(){return process.platform==="darwin"?"brew install ttyd":process.platform==="win32"?"winget install tsl0922.ttyd # or download from https://github.com/tsl0922/ttyd/releases":"sudo apt-get install -y ttyd # or build from source: https://github.com/tsl0922/ttyd"}function tmuxManualInstall(){return process.platform==="darwin"?"brew install tmux":"sudo apt-get install -y tmux"}function createPrereqsRoutes(){return new Elysia({prefix:"/prereqs"}).get("/status",()=>{let ttydOk=resolveBinary("ttyd")!==null||!!whichSync("ttyd"),tmuxOk=!!whichSync("tmux"),missing=[];if(!tmuxOk)missing.push({name:"tmux",kind:"binary",requiresSudo:!0});if(!ttydOk)missing.push({name:"ttyd",kind:"binary",requiresSudo:!1});return{satisfied:missing.length===0,missing}}).post("/install",async()=>{let installed=[],pendingSudo=[],errors=[];if(!whichSync("tmux"))pendingSudo.push({name:"tmux",command:tmuxManualInstall(),reason:"tmux is required for the tmux session backend."});if(resolveBinary("ttyd")||whichSync("ttyd"));else try{await installBinary({name:"ttyd",downloads:TTYD_DOWNLOADS,versionMatcher:"ttyd version"}),installed.push("ttyd")}catch(err){let message=err instanceof Error?err.message:String(err);pendingSudo.push({name:"ttyd",command:ttydManualInstall(),reason:`ttyd auto-download failed: ${message}`}),errors.push(message)}return{ok:errors.length===0,installed,pendingSudo,errors}}).post("/uninstall",()=>({ok:!0}))}var createPlugin=(_ctx)=>{let lifecycle=createLifecycleHooks({name:PLUGIN_NAME,skipPlatforms:["win32"],telemetryEventName:`${PLUGIN_NAME}.ready`,onInit:async(services)=>{new ProviderRegistry(services).registerProvider("session",PROVIDER_NAME,provider),new TelemetryEmitter(PLUGIN_NAME,PLUGIN_VERSION,services).emit("session.provider.ready",{provider:"tmux"}),await provider.init(services)},onShutdown:async()=>{await provider.shutdown({reason:"shutdown"})},onNuke:async(_hostServices,ctx)=>{if(ctx.dryRun)return{reaped:["ttyd terminal servers + session-tmux storage"]};let{terminalsReaped}=await provider.nuke();return{reaped:[`${terminalsReaped} ttyd terminal server${terminalsReaped===1?"":"s"} + session-tmux storage`]}}});return{capabilities:{storage:"rw",subprocess:!0,telemetry:!0},name:PLUGIN_NAME,version:PLUGIN_VERSION,description:"Tmux + ttyd session provider \u2014 manages terminal sessions via tmux and exposes web terminals via ttyd",tags:["backend","provider"],apiPrefix:"/api/session-tmux",prerequisites:[{name:"tmux",kind:"binary",requiresSudo:!0},{name:"ttyd",kind:"binary",requiresSudo:!1}],createRoutes:()=>createPrereqsRoutes(),onServerStart:lifecycle.onServerStart,onServerStop:lifecycle.onServerStop,onNuke:lifecycle.onNuke}};export{createPlugin};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibecontrols/vibe-plugin-session-tmux",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.530.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"description": "Tmux + ttyd session provider plugin for VibeControls Agent",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"sanity": "bun run format:check && bun run lint:sanity && bun run type:check && bun run build"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@vibecontrols/plugin-sdk": "2026.
|
|
29
|
+
"@vibecontrols/plugin-sdk": "2026.530.5",
|
|
30
30
|
"elysia": "^1.4.28"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|