@vibecontrols/vibe-plugin-session-tmux 2026.509.2 → 2026.509.3

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/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * Migrated to consume @vibecontrols/plugin-sdk@2026.509.1 — inline contract
9
9
  * stubs and subprocess/storage helpers replaced by SDK imports.
10
10
  */
11
- import type { VibePlugin } from "@vibecontrols/plugin-sdk/contract";
11
+ import type { VibePluginFactory } from "@vibecontrols/plugin-sdk/contract";
12
12
  type SessionStatus = "active" | "inactive" | "terminated" | "error";
13
13
  interface SessionConfig {
14
14
  /** Optional external ID (e.g. backend UUID). If provided the plugin uses it instead of generating one. */
@@ -139,6 +139,12 @@ interface SessionProviderCapabilities {
139
139
  };
140
140
  platform: string[];
141
141
  }
142
- declare const vibePlugin: VibePlugin;
143
- export { vibePlugin };
142
+ /**
143
+ * Plugin contract V2 factory. The agent calls this once per profile at
144
+ * load time. Lifecycle hooks and the VibePlugin object are constructed
145
+ * fresh per-call so that any per-profile context can be captured if
146
+ * needed in the future. The underlying provider is a process-wide
147
+ * singleton (see comment above) since tmux/ttyd PIDs are global.
148
+ */
149
+ export declare const createPlugin: VibePluginFactory;
144
150
  export type { SessionProvider, SessionProviderCapabilities, SessionConfig, SessionInfo, SessionStatus, TerminalInfo, HealthCheckResult, SystemSessionInfo, SystemTerminalInfo, };
package/dist/index.js CHANGED
@@ -5,4 +5,4 @@ import{Elysia}from"elysia";function createLifecycleHooks(spec){let{name,onInit,o
5
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
6
  `),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
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 createPrereqsRoutes(){let checks=["tmux","ttyd"],installCmds={tmux:process.platform==="darwin"?"brew install tmux":"sudo apt-get install -y tmux",ttyd:process.platform==="darwin"?"brew install ttyd":"sudo apt-get install -y ttyd"};return new Elysia({prefix:"/prereqs"}).get("/status",()=>{let missing=checks.filter((bin)=>!whichSync(bin)).map((name)=>({name,kind:"binary",requiresSudo:!0}));return{satisfied:missing.length===0,missing}}).post("/install",()=>{let pendingSudo=checks.filter((bin)=>!whichSync(bin)).map((name)=>({name,command:installCmds[name]??`(see install docs for ${name})`,reason:`${name} is required for tmux session backend.`}));return{ok:!0,installed:[],pendingSudo,errors:[]}}).post("/uninstall",()=>({ok:!0}))}var 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"})}}),vibePlugin={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:!0}],createRoutes:()=>createPrereqsRoutes(),onServerStart:lifecycle.onServerStart,onServerStop:lifecycle.onServerStop};export{vibePlugin};
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 createPrereqsRoutes(){let checks=["tmux","ttyd"],installCmds={tmux:process.platform==="darwin"?"brew install tmux":"sudo apt-get install -y tmux",ttyd:process.platform==="darwin"?"brew install ttyd":"sudo apt-get install -y ttyd"};return new Elysia({prefix:"/prereqs"}).get("/status",()=>{let missing=checks.filter((bin)=>!whichSync(bin)).map((name)=>({name,kind:"binary",requiresSudo:!0}));return{satisfied:missing.length===0,missing}}).post("/install",()=>{let pendingSudo=checks.filter((bin)=>!whichSync(bin)).map((name)=>({name,command:installCmds[name]??`(see install docs for ${name})`,reason:`${name} is required for tmux session backend.`}));return{ok:!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"})}});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:!0}],createRoutes:()=>createPrereqsRoutes(),onServerStart:lifecycle.onServerStart,onServerStop:lifecycle.onServerStop}};export{createPlugin};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecontrols/vibe-plugin-session-tmux",
3
- "version": "2026.509.2",
3
+ "version": "2026.509.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "description": "Tmux + ttyd session provider plugin for VibeControls Agent",