@vibecontrols/vibe-plugin-session-wezterm 2026.601.1 → 2026.731.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.
Files changed (2) hide show
  1. package/dist/index.js +1 -1
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import{spawn as spawnChild}from"child_process";import{existsSync as existsSync2,
5
5
  `;return`set "${key}=${value.replace(/"/g,'""')}"\r
6
6
  `}function formatWeztermFailure(args,result){let stdout=result.stdout?.toString("utf-8").trim()??"",stderr=result.stderr?.toString("utf-8").trim()??"",details=[`exitCode=${result.exitCode??"null"}`,result.signalCode?`signal=${result.signalCode}`:null,stderr?`stderr=${stderr}`:null,stdout?`stdout=${stdout}`:null].filter((part)=>part!==null);return`wezterm ${args.join(" ")} failed (${details.join(", ")||"no output"})`}function weztermExec(args){let result=Bun.spawnSync([resolveWeztermTool("wezterm"),...args],{stdout:"pipe",stderr:"pipe",timeout:1e4});if(result.exitCode!==0)throw Error(formatWeztermFailure(args,result));return result.stdout?.toString("utf-8").trimEnd()??""}function weztermExecSilent(args){try{return Bun.spawnSync([resolveWeztermTool("wezterm"),...args],{stdout:"pipe",stderr:"pipe",timeout:1e4}).exitCode===0}catch{return!1}}function weztermCliExec(args){return weztermExec(["cli",...args])}function weztermCliExecSilent(args){return weztermExecSilent(["cli",...args])}function weztermListPanes(){try{let raw=weztermCliExec(["list","--format","json"]);if(!raw||raw==="[]")return[];return JSON.parse(raw)}catch{return[]}}function nowISO(){return new Date().toISOString()}class WeztermSessionProvider{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("WeztermSessionProvider initialising",{provider:this.name}),await this.ensureDependencies(),await this.ensureMuxServer(),await this.reconcileSessions(),this.log.info("WeztermSessionProvider ready")}async shutdown(context){if(context?.reason==="reload"){this.log.info("Hot-reload: preserving wezterm sessions and ttyd processes"),await this.persistTerminals(),this.ttydPids.clear(),this.ttydPorts.clear();return}this.log.info("WeztermSessionProvider 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("WeztermSessionProvider shutdown complete")}async nuke(){this.log.info("WeztermSessionProvider nuke \u2014 reaping all spawned resources");let reaped=[],stopPromises=[...this.ttydPids.keys()].map((sessionId)=>this.stopTerminal(sessionId)),ttydKilled=(await Promise.allSettled(stopPromises)).filter((r)=>r.status==="fulfilled").length;if(ttydKilled>0)reaped.push(`${ttydKilled} ttyd terminal-server(s)`);let panesKilled=0,workspacesKilled=0;try{let sessions=await this.loadSessions(),ownedWorkspaces=new Set(sessions.map((s)=>this.getWorkspaceName(s))),panes=weztermListPanes();for(let workspace of ownedWorkspaces){let wsPanes=panes.filter((p)=>p.workspace===workspace);if(wsPanes.length===0)continue;let killedAny=!1;for(let pane of wsPanes)if(weztermCliExecSilent(["kill-pane","--pane-id",String(pane.pane_id)]))panesKilled++,killedAny=!0;if(killedAny)workspacesKilled++}}catch(err){this.log.warn("nuke: failed to reap wezterm panes",{error:String(err)})}if(workspacesKilled>0)reaped.push(`${panesKilled} wezterm pane(s) across ${workspacesKilled} workspace(s)`);try{await this.sessionsStore?.delete(),await this.terminalsStore?.delete(),reaped.push("session-wezterm storage")}catch(err){this.log.warn("nuke: failed to clear storage",{error:String(err)})}return this.ttydPids.clear(),this.ttydPorts.clear(),this.log.info("WeztermSessionProvider nuke complete",{reaped}),reaped}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((await this.listSystemSessions()).find((s)=>s.name===target))return this.log.info("create(): attaching to existing wezterm workspace",{target}),this.adopt(target,config.name);throw Error(`No wezterm workspace named "${target}" \u2014 drop externalName to create a new one`)}let id=config.id||generateId(),workspaceName=`vibe-${id.substring(0,8)}`,now=nowISO();this.log.info("Creating wezterm session",{id,name:config.name,workspaceName});let args=["spawn","--new-window","--workspace",workspaceName];if(config.workingDirectory)args.push("--cwd",config.workingDirectory);if(config.shell)args.push("--",config.shell);let paneId=null,createError;try{let paneIdStr=weztermCliExec(args);if(paneId=parseInt(paneIdStr.trim(),10),isNaN(paneId))throw Error(`wezterm cli spawn returned invalid pane_id: ${paneIdStr}`)}catch(err){createError=err instanceof Error?err.message:String(err),this.log.error("Failed to create wezterm session",{id,error:createError}),await sleep(100);let recovered=this.findPaneInWorkspace(workspaceName);if(recovered)paneId=recovered.pane_id,this.log.warn("Recovered wezterm pane after spawn reported failure",{id,workspaceName,paneId,createError})}if(paneId!==null&&config.environment){for(let[key,value]of Object.entries(config.environment)){let assignment=buildEnvAssignmentCommand(key,value,config.shell);weztermCliExecSilent(["send-text","--pane-id",String(paneId),"--no-paste","--",assignment])}await sleep(100)}if(paneId!==null&&config.command)weztermCliExecSilent(["send-text","--pane-id",String(paneId),"--no-paste","--",`${config.command}
7
7
  `]);let pid=paneId!==null?this.getPanePid(paneId):null,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:{...paneId!==null?{weztermPaneId:paneId}:{},weztermWorkspace:workspaceName,shell:config.shell,size:config.size,...paneId===null?{terminalOnly:!0,weztermCreateError:createError??"wezterm pane was not created"}:{}}};if(await this.saveSession(info),paneId===null)this.log.warn("WezTerm pane unavailable; created ttyd-backed session record",{id,workspaceName,createError});else this.log.info("WezTerm session created",{id,workspaceName,paneId,pid});return info}findPaneInWorkspace(workspaceName){return weztermListPanes().filter((p)=>p.workspace===workspaceName)[0]??null}refreshSessionStatus(session){if(session.status==="terminated")return!1;let terminalInfo=this.getRunningTerminalInfo(session.id),exists=this.getPaneId(session)!==null?this.workspaceExists(this.getWorkspaceName(session)):terminalInfo!==null,changed=!1;if(!exists&&session.status==="active")session.status="inactive",session.updatedAt=nowISO(),session.terminal=void 0,changed=!0;else if(exists&&session.status==="inactive")session.status="active",session.updatedAt=nowISO(),changed=!0;if(terminalInfo)session.terminal=terminalInfo;return changed}async terminate(sessionId){let session=await this.getInfo(sessionId);if(!session){this.log.warn("Terminate called for unknown session",{sessionId});return}let workspaceName=this.getWorkspaceName(session);if(this.log.info("Terminating wezterm session",{sessionId,workspaceName}),this.ttydPids.has(sessionId))await this.stopTerminal(sessionId);let panes=weztermListPanes().filter((p)=>p.workspace===workspaceName);for(let pane of panes)weztermCliExecSilent(["kill-pane","--pane-id",String(pane.pane_id)]);if(panes.length===0){let paneId=this.getPaneId(session);if(paneId!==null)weztermCliExecSilent(["kill-pane","--pane-id",String(paneId)])}session.status="terminated",session.updatedAt=nowISO(),session.terminal=void 0,await this.saveSession(session),this.log.info("WezTerm session terminated",{sessionId})}async getInfo(sessionId){let session=(await this.loadSessions()).find((s)=>s.id===sessionId)??null;if(session){if(this.refreshSessionStatus(session))await this.saveSession(session)}return session}async list(){let sessions=await this.loadSessions(),changed=!1;for(let session of sessions)changed=this.refreshSessionStatus(session)||changed;if(changed)await this.saveSessions(sessions);return sessions}async sendCommand(sessionId,command){let session=await this.requireSession(sessionId),paneId=this.requirePaneId(session);this.log.debug("Sending command",{sessionId,command});try{weztermCliExec(["send-text","--pane-id",String(paneId),"--no-paste","--",`${command}
8
- `])}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),paneId=this.requirePaneId(session);this.log.debug("Sending keys",{sessionId,keys});try{weztermCliExec(["send-text","--pane-id",String(paneId),"--no-paste","--",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),paneId=this.requirePaneId(session);this.log.debug("Sending interrupt (Ctrl+C)",{sessionId});try{weztermCliExec(["send-text","--pane-id",String(paneId),"--no-paste","--","\x03"])}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),paneId=this.requirePaneId(session);this.log.debug("Capturing output",{sessionId});try{return weztermCliExec(["get-text","--pane-id",String(paneId)])}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);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 oldWorkspace=this.getWorkspaceName(session),newWorkspace=`vibe-${session.id}`;weztermCliExecSilent(["rename-workspace","--workspace",oldWorkspace,newWorkspace])}async toggleMouse(_sessionId){return this.log.debug("toggleMouse is a no-op for wezterm (always enabled)",{sessionId:_sessionId}),!0}async getTerminationStatus(sessionId){let session=(await this.loadSessions()).find((s)=>s.id===sessionId);if(!session)return{terminated:!0,exists:!1};let exists=this.getPaneId(session)!==null?this.workspaceExists(this.getWorkspaceName(session)):this.getRunningTerminalInfo(sessionId)!==null;return{terminated:session.status==="terminated"||!exists,exists}}async getTerminalInfo(sessionId){return this.getRunningTerminalInfo(sessionId)}async startTerminal(sessionId,port){let session=await this.requireSession(sessionId),paneId=this.getPaneId(session),existing=this.getRunningTerminalInfo(sessionId);if(existing)return this.log.debug("Terminal already running",{sessionId,...existing}),existing;let inUsePorts=[...this.ttydPorts.values()],scanStart=inUsePorts.length?Math.max(TTYD_BASE_PORT,Math.max(...inUsePorts)+1):TTYD_BASE_PORT,assignedPort=port??await findAvailablePort(scanStart,TTYD_PORT_RANGE);this.log.info("Starting ttyd terminal",{sessionId,paneId,port:assignedPort});let cwd=session.workingDirectory||homedir2(),defaultShell=isWindows()?process.env.ComSpec||"cmd.exe":process.env.SHELL||"/bin/bash",shell=session.metadata?.shell||defaultShell,ttydEnv={...process.env};if(paneId!==null)ttydEnv.WEZTERM_PANE=String(paneId),ttydEnv.WEZTERM_UNIX_SOCKET=process.env.WEZTERM_UNIX_SOCKET||"managed-by-vibecontrols";ttydEnv.TERM_PROGRAM="WezTerm",ttydEnv.VIBECONTROLS_PROVIDER="wezterm";let otherProviderVars={tmux:["TMUX","TMUX_PANE","TMUX_PLUGIN_MANAGER_PATH"],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 ttydCmd=await ensureTtydBinary(this.log),child;try{child=Bun.spawn([ttydCmd,"-t","fontSize=14","-t",'theme={"background":"#1e1e1e","foreground":"#cccccc"}',"--writable","--port",String(assignedPort),shell],{stdout:"ignore",stderr:"pipe",stdin:"ignore",cwd,env:ttydEnv})}catch(err){let message=err instanceof Error?err.message:String(err);if(/ENOENT|no such file|not found|uv_spawn/i.test(message))throw Error(`ttyd binary could not be spawned at "${ttydCmd}". ${ttydManualInstallCommand()}. Cause: ${message}`,{cause:err});throw err}if(!child.pid)throw Error("Failed to start ttyd \u2014 no PID returned");let ttydStderr="",stderrStream=child.stderr;if(stderrStream&&typeof stderrStream.getReader==="function")(async()=>{try{let reader=stderrStream.getReader(),dec=new TextDecoder;for(;;){let{done,value}=await reader.read();if(done)break;if(ttydStderr.length<4096)ttydStderr+=dec.decode(value,{stream:!0})}}catch{}})();if(await sleep(500),child.exitCode!==null||!isProcessAlive(child.pid)){let detail=ttydStderr.trim().slice(0,300)||"no stderr captured";throw this.log.error("ttyd exited immediately after launch",{sessionId,port:assignedPort,exitCode:child.exitCode,stderr:detail}),Error(`ttyd exited immediately (code ${child.exitCode??"unknown"}) on port ${assignedPort}: ${detail}`)}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:"127.0.0.1",wsPath:"/ws",subprotocols:["tty"]};return session.status="active",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 panes=weztermListPanes();if(panes.length===0)return[];let workspaceMap=new Map;for(let pane of panes){let ws=pane.workspace||"default";if(!workspaceMap.has(ws))workspaceMap.set(ws,{panes:[],windows:new Set});let entry=workspaceMap.get(ws);entry.panes.push(pane),entry.windows.add(pane.window_id)}let results=[];for(let[workspace,data]of workspaceMap)results.push({id:workspace,name:workspace,windows:data.windows.size,attached:!1});return results}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})}if(!isWindows())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
+ `])}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),paneId=this.requirePaneId(session);this.log.debug("Sending keys",{sessionId,keys});try{weztermCliExec(["send-text","--pane-id",String(paneId),"--no-paste","--",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),paneId=this.requirePaneId(session);this.log.debug("Sending interrupt (Ctrl+C)",{sessionId});try{weztermCliExec(["send-text","--pane-id",String(paneId),"--no-paste","--","\x03"])}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),paneId=this.requirePaneId(session);this.log.debug("Capturing output",{sessionId});try{return weztermCliExec(["get-text","--pane-id",String(paneId)])}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);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 oldWorkspace=this.getWorkspaceName(session),newWorkspace=`vibe-${session.id}`;weztermCliExecSilent(["rename-workspace","--workspace",oldWorkspace,newWorkspace])}async toggleMouse(_sessionId){return this.log.debug("toggleMouse is a no-op for wezterm (always enabled)",{sessionId:_sessionId}),!0}async getTerminationStatus(sessionId){let session=(await this.loadSessions()).find((s)=>s.id===sessionId);if(!session)return{terminated:!0,exists:!1};let exists=this.getPaneId(session)!==null?this.workspaceExists(this.getWorkspaceName(session)):this.getRunningTerminalInfo(sessionId)!==null;return{terminated:session.status==="terminated"||!exists,exists}}async getTerminalInfo(sessionId){return this.getRunningTerminalInfo(sessionId)}async startTerminal(sessionId,port){let session=await this.requireSession(sessionId),paneId=this.getPaneId(session),existing=this.getRunningTerminalInfo(sessionId);if(existing)return this.log.debug("Terminal already running",{sessionId,...existing}),existing;let inUsePorts=[...this.ttydPorts.values()],scanStart=inUsePorts.length?Math.max(TTYD_BASE_PORT,Math.max(...inUsePorts)+1):TTYD_BASE_PORT,assignedPort=port??await findAvailablePort(scanStart,TTYD_PORT_RANGE);this.log.info("Starting ttyd terminal",{sessionId,paneId,port:assignedPort});let cwd=session.workingDirectory||homedir2(),defaultShell=isWindows()?process.env.ComSpec||"cmd.exe":process.env.SHELL||"/bin/bash",shell=session.metadata?.shell||defaultShell,ttydEnv={...process.env};if(paneId!==null)ttydEnv.WEZTERM_PANE=String(paneId),ttydEnv.WEZTERM_UNIX_SOCKET=process.env.WEZTERM_UNIX_SOCKET||"managed-by-vibecontrols";ttydEnv.TERM_PROGRAM="WezTerm",ttydEnv.VIBECONTROLS_PROVIDER="wezterm";let otherProviderVars={tmux:["TMUX","TMUX_PANE","TMUX_PLUGIN_MANAGER_PATH"],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 ttydCmd=await ensureTtydBinary(this.log),child;try{child=Bun.spawn([ttydCmd,"-t","fontSize=14","-t",'theme={"background":"#1e1e1e","foreground":"#cccccc"}',"-t","macOptionClickForcesSelection=true","-t","rightClickSelectsWord=true","-t","scrollback=10000","--writable","--port",String(assignedPort),shell],{stdout:"ignore",stderr:"pipe",stdin:"ignore",cwd,env:ttydEnv})}catch(err){let message=err instanceof Error?err.message:String(err);if(/ENOENT|no such file|not found|uv_spawn/i.test(message))throw Error(`ttyd binary could not be spawned at "${ttydCmd}". ${ttydManualInstallCommand()}. Cause: ${message}`,{cause:err});throw err}if(!child.pid)throw Error("Failed to start ttyd \u2014 no PID returned");let ttydStderr="",stderrStream=child.stderr;if(stderrStream&&typeof stderrStream.getReader==="function")(async()=>{try{let reader=stderrStream.getReader(),dec=new TextDecoder;for(;;){let{done,value}=await reader.read();if(done)break;if(ttydStderr.length<4096)ttydStderr+=dec.decode(value,{stream:!0})}}catch{}})();if(await sleep(500),child.exitCode!==null||!isProcessAlive(child.pid)){let detail=ttydStderr.trim().slice(0,300)||"no stderr captured";throw this.log.error("ttyd exited immediately after launch",{sessionId,port:assignedPort,exitCode:child.exitCode,stderr:detail}),Error(`ttyd exited immediately (code ${child.exitCode??"unknown"}) on port ${assignedPort}: ${detail}`)}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:"127.0.0.1",wsPath:"/ws",subprotocols:["tty"]};return session.status="active",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 panes=weztermListPanes();if(panes.length===0)return[];let workspaceMap=new Map;for(let pane of panes){let ws=pane.workspace||"default";if(!workspaceMap.has(ws))workspaceMap.set(ws,{panes:[],windows:new Set});let entry=workspaceMap.get(ws);entry.panes.push(pane),entry.windows.add(pane.window_id)}let results=[];for(let[workspace,data]of workspaceMap)results.push({id:workspace,name:workspace,windows:data.windows.size,attached:!1});return results}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})}if(!isWindows())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
9
  `)){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 wsName of sessionIds){let panes=weztermListPanes().filter((p)=>p.workspace===wsName);if(panes.length===0)if(weztermCliExecSilent(["kill-pane","--pane-id",wsName]))killed++;else failed++;else{let wsKilled=!1;for(let pane of panes)if(weztermCliExecSilent(["kill-pane","--pane-id",String(pane.pane_id)]))wsKilled=!0;if(wsKilled)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 weztermOk,weztermVersion,sessionCount=0,terminalCount=this.ttydPids.size;try{weztermVersion=weztermExec(["--version"]),weztermOk=!0}catch{return{ok:!1,sessions:0,terminals:terminalCount,message:"wezterm is not available"}}try{sessionCount=(await this.listSystemSessions()).length}catch{}let ttydOk=!1;try{ttydOk=resolveBinary("ttyd")!==null}catch{}let messages=[weztermVersion];if(!ttydOk)messages.push("ttyd not found \u2014 web terminals unavailable");return{ok:weztermOk,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 workspaceName=this.getWorkspaceName(session),exists=this.workspaceExists(workspaceName);if(session.status==="terminated"||!exists){if(this.ttydPids.has(session.id))await this.stopTerminal(session.id);if(exists&&session.status==="terminated"){let panes=weztermListPanes().filter((p)=>p.workspace===workspaceName);for(let pane of panes)weztermCliExecSilent(["kill-pane","--pane-id",String(pane.pane_id)])}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),paneId=this.requirePaneId(session);this.log.warn("resize: WezTerm CLI does not support absolute pane resize. The pane will auto-size based on the window or ttyd terminal.",{sessionId,paneId,requestedCols:cols,requestedRows:rows})}async listSystem(){return this.listSystemSessions()}async killSystem(sessionId){let panes=weztermListPanes().filter((p)=>p.workspace===sessionId);for(let pane of panes)weztermCliExecSilent(["kill-pane","--pane-id",String(pane.pane_id)])}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.getWorkspaceName(s)));return(await this.listSystemSessions()).filter((s)=>s.name.startsWith("vibe-")&&!known.has(s.name)).map((s)=>({externalName:s.name,provider:"wezterm",windows:s.windows,attached:s.attached,createdAt:s.createdAt}))}async adopt(externalName,displayName){if(!externalName.startsWith("vibe-"))throw Error(`Refusing to adopt wezterm workspace "${externalName}" \u2014 only vibe-* workspaces are adoptable`);if(!(await this.listSystemSessions()).find((s)=>s.name===externalName))throw Error(`Wezterm workspace "${externalName}" does not exist`);let id=externalName.slice(5)||externalName,existing=(await this.loadSessions()).find((s)=>s.id===id);if(existing){if(!this.ttydPids.has(id))try{await this.startTerminal(id)}catch{}return await this.get(id)??existing}let now=nowISO(),info={id,name:displayName||externalName,status:"active",provider:this.name,createdAt:now,updatedAt:now,metadata:{weztermWorkspace:externalName,adopted:!0}};await this.saveSession(info);try{info.terminal=await this.startTerminal(id),await this.saveSession(info)}catch{}return info}getCapabilities(){return{provider:"wezterm",features:{mouse:!0,resize:!0,capture:!0,webTerminal:!0,splitPanes:!0,tabs:!0,scrollback:!0,clipboard:!0,search:!0},platform:["linux","macos","windows"]}}async getScrollback(sessionId,lines){let session=await this.requireSession(sessionId),paneId=this.requirePaneId(session);this.log.debug("Getting scrollback",{sessionId,lines});try{return weztermCliExec(["get-text","--pane-id",String(paneId)]).split(`
10
10
  `).slice(-lines).join(`
11
11
  `)}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(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecontrols/vibe-plugin-session-wezterm",
3
- "version": "2026.601.1",
3
+ "version": "2026.731.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "description": "WezTerm + ttyd session provider plugin for VibeControls Agent",
@@ -19,11 +19,11 @@
19
19
  "lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
20
20
  "format": "bunx prettier . --write",
21
21
  "format:check": "bunx prettier . --check",
22
- "type:check": "tsc --noEmit",
22
+ "type:check": "bash scripts/build-limits.sh tsc --noEmit",
23
23
  "clean": "rimraf dist",
24
24
  "prebuild": "bun run clean",
25
25
  "prepublishOnly": "bun run build",
26
- "sanity": "bun run format:check && bun run lint:sanity && bun run type:check && bun run build"
26
+ "sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run format:check && bun run lint:sanity && bun run type:check && bun run build'"
27
27
  },
28
28
  "dependencies": {
29
29
  "@vibecontrols/plugin-sdk": "2026.530.5",