tmux-weblink 2.0.6 → 2.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{createRequire as l}from"node:module";import{hostname as h}from"node:os";import T from"node:path";import{listSessions as E}from"./sessions.js";import{loadSecurityConfig as g}from"./lib/security-config.js";import{TokenStore as _}from"./lib/auth.js";import{RateLimiter as w}from"./lib/rateLimiter.js";import{db as n}from"./lib/db.js";import{loadExtensions as b,spawnExtensionBackend as k}from"./lib/ext-loader.js";import{SchedulerService as A}from"./lib/scheduler.js";import{handleClientMessage as M}from"./lib/ws-message.js";import{loadDotEnv as N}from"./lib/load-env.js";import{readSettings as y}from"./lib/settings.js";import{readActiveTheme as S}from"./lib/theme-store.js";import{buildApp as x}from"./lib/build-app.js";import{acquireControlClient as B}from"./lib/tmux-control.js";import{getSessionPaneTarget as C,capturePaneTail as U,capturePaneHistoryChunk as W,toCrlf as G}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as D}from"./lib/terminal-config.js";import{startAgentClient as H}from"./lib/agent-client.js";N();const O=l(import.meta.url),{version:R}=O("../package.json");function P(e){let r=process.env.TMUX_WEB_AGENT_HUB||"",s=process.env.TMUX_WEB_AGENT_TOKEN||"",a=process.env.TMUX_WEB_AGENT_NAME||h();for(let o=0;o<e.length;o++){const t=e[o];if(t!=="agent"){if((t==="--hub"||t==="-h")&&e[o+1]){r=e[++o];continue}if((t==="--token"||t==="-t")&&e[o+1]){s=e[++o];continue}if((t==="--name"||t==="-n")&&e[o+1]){a=e[++o];continue}t.startsWith("--hub=")&&(r=t.slice(6)),t.startsWith("--token=")&&(s=t.slice(8)),t.startsWith("--name=")&&(a=t.slice(7))}}return r||(console.error("usage: tmux-web agent --hub wss://HOST[:PORT] --token TOKEN [--name NAME]"),console.error(" (or set TMUX_WEB_AGENT_HUB / TMUX_WEB_AGENT_TOKEN)"),process.exit(1)),s||(console.error("agent token required (--token or TMUX_WEB_AGENT_TOKEN)"),process.exit(1)),{hub:r,token:s,name:a.slice(0,64)}}const{hub:X,token:q,name:I}=P(process.argv.slice(2)),c=D(),K=g(),j=new _,F=new w;await n.read(),n.data.sessionAccess??=[],n.data.pinnedViews??=[],n.data.watchedPanes??=[],n.data.triggeredTasks??=[],n.data.quickCommands??=[];const i=await y(),L=await S(),$=i.commandbar===!0,p=z(i.scheduleHistoryDays),f=T.join(process.cwd(),"extensions"),d=await b(f);for(const e of d)e.start&&k(e.dir,e);const m=new A({db:n,historyRetentionMs:p*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await m.restoreFromDb();const v=x({mode:"agent",securityConfig:K,tokenStore:j,rateLimiter:F,scheduler:m,settings:i,commandbarEnabled:$,terminalRenderer:i.terminalRenderer==="ghostty"?"ghostty":"xterm",scheduleHistoryDays:p,extsDir:f,extensions:d,terminalBufferConfig:c,getAgentId:()=>null,state:{activeTheme:L,settingUpPassword:!1}}),V=H({hub:X,token:q,name:I,version:R,app:v,listSessions:E,terminalBufferConfig:c,getSessionPaneTarget:C,capturePaneTail:U,capturePaneHistoryChunk:W,toCrlf:G,handleClientMessage:M,acquireControlClient:B});function u(){m.cleanup(),V.stop(),process.exit(0)}process.on("SIGINT",u),process.on("SIGTERM",u);function z(e){return typeof e!="number"||!Number.isFinite(e)?7:Math.min(365,Math.max(1,Math.round(e)))}
2
+ import{createRequire as l}from"node:module";import{hostname as h}from"node:os";import T from"node:path";import{listSessions as E,resolveTmuxSocketPath as g}from"./sessions.js";import{loadSecurityConfig as _}from"./lib/security-config.js";import{TokenStore as w}from"./lib/auth.js";import{RateLimiter as b}from"./lib/rateLimiter.js";import{db as n}from"./lib/db.js";import{loadExtensions as k,spawnExtensionBackend as A}from"./lib/ext-loader.js";import{SchedulerService as M}from"./lib/scheduler.js";import{handleClientMessage as S}from"./lib/ws-message.js";import{loadDotEnv as x}from"./lib/load-env.js";import{readSettings as N}from"./lib/settings.js";import{readActiveTheme as y}from"./lib/theme-store.js";import{buildApp as B}from"./lib/build-app.js";import{acquireControlClient as C}from"./lib/tmux-control.js";import{getSessionPaneTarget as U,capturePaneTail as W,capturePaneHistoryChunk as G,toCrlf as P}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as D}from"./lib/terminal-config.js";import{startAgentClient as H}from"./lib/agent-client.js";x();const O=l(import.meta.url),{version:R}=O("../package.json");function X(e){let r=process.env.TMUX_WEB_AGENT_HUB||"",s=process.env.TMUX_WEB_AGENT_TOKEN||"",a=process.env.TMUX_WEB_AGENT_NAME||h();for(let o=0;o<e.length;o++){const t=e[o];if(t!=="agent"){if((t==="--hub"||t==="-h")&&e[o+1]){r=e[++o];continue}if((t==="--token"||t==="-t")&&e[o+1]){s=e[++o];continue}if((t==="--name"||t==="-n")&&e[o+1]){a=e[++o];continue}t.startsWith("--hub=")&&(r=t.slice(6)),t.startsWith("--token=")&&(s=t.slice(8)),t.startsWith("--name=")&&(a=t.slice(7))}}return r||(console.error("usage: tmux-web agent --hub wss://HOST[:PORT] --token TOKEN [--name NAME]"),console.error(" (or set TMUX_WEB_AGENT_HUB / TMUX_WEB_AGENT_TOKEN)"),process.exit(1)),s||(console.error("agent token required (--token or TMUX_WEB_AGENT_TOKEN)"),process.exit(1)),{hub:r,token:s,name:a.slice(0,64)}}const{hub:q,token:I,name:K}=X(process.argv.slice(2)),c=D(),j=_(),v=new w,F=new b;await n.read(),n.data.sessionAccess??=[],n.data.pinnedViews??=[],n.data.watchedPanes??=[],n.data.triggeredTasks??=[],n.data.quickCommands??=[];const i=await N(),L=await y(),$=i.commandbar===!0,f=J(i.scheduleHistoryDays),p=T.join(process.cwd(),"extensions"),u=await k(p);for(const e of u)e.start&&A(e.dir,e);const m=new M({db:n,historyRetentionMs:f*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await m.restoreFromDb();const V=B({mode:"agent",securityConfig:j,tokenStore:v,rateLimiter:F,scheduler:m,settings:i,commandbarEnabled:$,terminalRenderer:i.terminalRenderer==="ghostty"?"ghostty":"xterm",scheduleHistoryDays:f,extsDir:p,extensions:u,terminalBufferConfig:c,getAgentId:()=>null,state:{activeTheme:L,settingUpPassword:!1}}),z=H({hub:q,token:I,name:K,version:R,app:V,listSessions:E,terminalBufferConfig:c,getSessionPaneTarget:U,capturePaneTail:W,capturePaneHistoryChunk:G,toCrlf:P,handleClientMessage:S,acquireControlClient:C,tmuxSocketPath:g()??void 0});function d(){m.cleanup(),z.stop(),process.exit(0)}process.on("SIGINT",d),process.on("SIGTERM",d);function J(e){return typeof e!="number"||!Number.isFinite(e)?7:Math.min(365,Math.max(1,Math.round(e)))}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import{chmodSync as he,existsSync as z,readdirSync as ge,statSync as ye}from"node:fs";import{createRequire as V}from"node:module";import{hostname as $}from"node:os";const ke=V(import.meta.url),{version:_e}=ke("../package.json");import I from"node:path";import{fileURLToPath as Te}from"node:url";import{serve as we}from"@hono/node-server";import{WebSocketServer as Ae,WebSocket as D}from"ws";try{const e=I.dirname(V(import.meta.url).resolve("node-pty/package.json")),n=I.join(e,"prebuilds");if(z(n))for(const r of ge(n)){const t=I.join(n,r,"spawn-helper");z(t)&&!(ye(t).mode&73)&&he(t,493)}}catch{}import{loadSecurityConfig as Se}from"./lib/security-config.js";import{TokenStore as xe,hashPassword as ve,verifyPassword as Ce,validatePassword as be}from"./lib/auth.js";import{RateLimiter as Re}from"./lib/rateLimiter.js";import{audit as l}from"./lib/auditLog.js";import{db as b}from"./lib/db.js";import{loadExtensions as Me,spawnExtensionBackend as Ie}from"./lib/ext-loader.js";import{SchedulerService as Ee}from"./lib/scheduler.js";import{handleClientMessage as X}from"./lib/ws-message.js";import{loadDotEnv as Oe}from"./lib/load-env.js";import{listSessions as Le}from"./sessions.js";import{cmdAdd as Pe,cmdRemove as $e,cmdList as Ue,cmdSetup as Ne,cmdTheme as He,printUsage as K,printVersion as Fe}from"./lib/cli.js";import{readSettings as De}from"./lib/settings.js";import{readActiveTheme as je}from"./lib/theme-store.js";import{buildApp as Q}from"./lib/build-app.js";import{attachTerminal as Be,killAllAttachedPtys as We}from"./lib/attach-terminal.js";import{acquireControlClient as Z,killAllControlClients as Je}from"./lib/tmux-control.js";import{getSessionPaneTarget as q,capturePaneTail as ee,capturePaneHistoryChunk as te,toCrlf as ne}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Ge}from"./lib/terminal-config.js";import{AgentRegistry as Ye}from"./lib/agent-registry.js";import{AgentChannel as ze,MAX_RELAY_CONNS_PER_AGENT as Ve}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as Xe,listAgentTokens as Ke}from"./lib/agent-tokens.js";import{saveSecurityConfig as Qe}from"./lib/security-config.js";import{readFederationConfig as Ze,writeFederationConfig as qe}from"./lib/federation-config.js";import{startAgentClient as et}from"./lib/agent-client.js";Oe();const U=Ge(),h=Se(),y=new xe,A=new Re;let j=!1;const tt="tmux-web-token";function B(e){if(h.security.trustProxy){const n=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(n)return n}return e.socket.remoteAddress||"unknown"}function W(e){const n=e.headers.authorization;if(typeof n=="string"&&n.startsWith("Bearer "))return n.slice(7);const r=e.headers.cookie;if(typeof r!="string")return null;const t=r.match(new RegExp(`(?:^|;\\s*)${tt}=([^;]+)`));return t?decodeURIComponent(t[1]):null}function Xt(e){const n=W(e);return n?y.validateToken(n):null}const E=new Map;function nt(e){let n=0;for(const r of E.values())r.ip===e&&n++;return n}function x(e,n,r){try{e.close(n,r)}catch{}}function oe(e,n){e.readyState===D.OPEN&&e.send(JSON.stringify(n))}const N=process.argv.slice(2);{const e=N.filter(n=>n!=="--ghostty"&&n!=="--xterm");if(e.length>0){const[n,r]=e;switch(n){case"add":r||(console.error("usage: tmux-web add <package>"),process.exit(1)),await Pe(r),process.exit(0);case"remove":case"rm":r||(console.error("usage: tmux-web remove <package>"),process.exit(1)),await $e(r),process.exit(0);case"list":case"ls":await Ue(),process.exit(0);case"setup":await Ne(e),process.exit(0);case"theme":await He(e.slice(1)),process.exit(0);case"agent":{const t=I.join(I.dirname(Te(import.meta.url)),"agent.js"),{spawn:u}=await import("node:child_process"),s=u(process.execPath,[t,...N],{stdio:"inherit"});s.on("error",o=>{console.error(o),process.exit(1)}),s.on("exit",o=>process.exit(o??0)),await new Promise(()=>{})}case"agent-token":await yt(e.slice(1)),process.exit(0);case"help":case"--help":case"-h":K(),process.exit(0);case"-V":case"--version":case"-v":Fe(),process.exit(0);default:console.error(`unknown argument: ${n}`),K(),process.exit(1)}}}function R(e,n){e.readyState===D.OPEN&&e.send(JSON.stringify(n))}const re=new Set,se=[],ot=7;function rt(e){return typeof e!="number"||!Number.isFinite(e)?ot:Math.min(365,Math.max(1,Math.round(e)))}await b.read(),b.data.sessionAccess??=[],b.data.pinnedViews??=[],b.data.watchedPanes??=[],b.data.triggeredTasks??=[],b.data.quickCommands??=[];const O=await De();let st=await je();const ae=O.commandbar===!0,J=rt(O.scheduleHistoryDays),G=I.join(process.cwd(),"extensions"),Y=await Me(G);for(const e of Y)e.start&&se.push(Ie(e.dir,e));const H=new Ee({db:b,historyRetentionMs:J*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await H.restoreFromDb();const g=new Ye({onChange:()=>{}}),T=new Map;function ie(e){return g.get(e)?T.get(e)??null:null}const ce={activeTheme:st,settingUpPassword:!1},at=Q({mode:"hub",securityConfig:h,tokenStore:y,rateLimiter:A,scheduler:H,settings:O,commandbarEnabled:ae,terminalRenderer:me(N,O.terminalRenderer),scheduleHistoryDays:J,extsDir:G,extensions:Y,terminalBufferConfig:U,state:ce,federation:{getConfig:()=>C,setConfig:async e=>{C=e,await qe(e),ue(e)},status:ct,hostname:$()},hub:{agents:g,getChannel:ie}}),it=Q({mode:"agent",securityConfig:h,tokenStore:y,rateLimiter:A,scheduler:H,settings:O,commandbarEnabled:ae,terminalRenderer:me(N,O.terminalRenderer),scheduleHistoryDays:J,extsDir:G,extensions:Y,terminalBufferConfig:U,state:ce});let C=await Ze(),L=null;function le(){L&&(L.stop(),L=null)}function ue(e){le(),!(!e.enabled||!e.hub||!e.token)&&(L=et({hub:e.hub,token:e.token,name:e.name?.trim()||$(),version:_e,app:it,listSessions:Le,terminalBufferConfig:U,getSessionPaneTarget:q,capturePaneTail:ee,capturePaneHistoryChunk:te,toCrlf:ne,handleClientMessage:X,acquireControlClient:Z}))}function ct(){return C.enabled?L?L.status():{state:"idle",agentId:null,hub:C.hub??"",name:C.name??$()}:{state:"idle",agentId:null,hub:C.hub??"",name:C.name??$()}}ue(C);const lt=parseInt(process.env.PORT||"21000",10),ut=we({fetch:at.fetch,port:lt,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),P=new Ae({noServer:!0});function M(e,n,r){e.write(`HTTP/1.1 ${n} ${r}\r
2
+ import{chmodSync as he,existsSync as z,readdirSync as ge,statSync as ye}from"node:fs";import{createRequire as V}from"node:module";import{hostname as $}from"node:os";const ke=V(import.meta.url),{version:_e}=ke("../package.json");import I from"node:path";import{fileURLToPath as Te}from"node:url";import{serve as we}from"@hono/node-server";import{WebSocketServer as Se,WebSocket as D}from"ws";try{const e=I.dirname(V(import.meta.url).resolve("node-pty/package.json")),n=I.join(e,"prebuilds");if(z(n))for(const r of ge(n)){const t=I.join(n,r,"spawn-helper");z(t)&&!(ye(t).mode&73)&&he(t,493)}}catch{}import{loadSecurityConfig as xe}from"./lib/security-config.js";import{TokenStore as Ae,hashPassword as ve,verifyPassword as Ce,validatePassword as be}from"./lib/auth.js";import{RateLimiter as Re}from"./lib/rateLimiter.js";import{audit as l}from"./lib/auditLog.js";import{db as b}from"./lib/db.js";import{loadExtensions as Me,spawnExtensionBackend as Ie}from"./lib/ext-loader.js";import{SchedulerService as Ee}from"./lib/scheduler.js";import{handleClientMessage as X}from"./lib/ws-message.js";import{loadDotEnv as Oe}from"./lib/load-env.js";import{listSessions as Pe,resolveTmuxSocketPath as Le}from"./sessions.js";import{cmdAdd as $e,cmdRemove as Ue,cmdList as Ne,cmdSetup as He,cmdTheme as Fe,printUsage as K,printVersion as De}from"./lib/cli.js";import{readSettings as je}from"./lib/settings.js";import{readActiveTheme as Be}from"./lib/theme-store.js";import{buildApp as Q}from"./lib/build-app.js";import{attachTerminal as We,killAllAttachedPtys as Je}from"./lib/attach-terminal.js";import{acquireControlClient as Z,killAllControlClients as Ge}from"./lib/tmux-control.js";import{getSessionPaneTarget as q,capturePaneTail as ee,capturePaneHistoryChunk as te,toCrlf as ne}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Ye}from"./lib/terminal-config.js";import{AgentRegistry as ze}from"./lib/agent-registry.js";import{AgentChannel as Ve,MAX_RELAY_CONNS_PER_AGENT as Xe}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as Ke,listAgentTokens as Qe}from"./lib/agent-tokens.js";import{saveSecurityConfig as Ze}from"./lib/security-config.js";import{readFederationConfig as qe,writeFederationConfig as et}from"./lib/federation-config.js";import{startAgentClient as tt}from"./lib/agent-client.js";Oe();const U=Ye(),h=xe(),y=new Ae,S=new Re;let j=!1;const nt="tmux-web-token";function B(e){if(h.security.trustProxy){const n=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(n)return n}return e.socket.remoteAddress||"unknown"}function W(e){const n=e.headers.authorization;if(typeof n=="string"&&n.startsWith("Bearer "))return n.slice(7);const r=e.headers.cookie;if(typeof r!="string")return null;const t=r.match(new RegExp(`(?:^|;\\s*)${nt}=([^;]+)`));return t?decodeURIComponent(t[1]):null}function Kt(e){const n=W(e);return n?y.validateToken(n):null}const E=new Map;function ot(e){let n=0;for(const r of E.values())r.ip===e&&n++;return n}function A(e,n,r){try{e.close(n,r)}catch{}}function oe(e,n){e.readyState===D.OPEN&&e.send(JSON.stringify(n))}const N=process.argv.slice(2);{const e=N.filter(n=>n!=="--ghostty"&&n!=="--xterm");if(e.length>0){const[n,r]=e;switch(n){case"add":r||(console.error("usage: tmux-web add <package>"),process.exit(1)),await $e(r),process.exit(0);case"remove":case"rm":r||(console.error("usage: tmux-web remove <package>"),process.exit(1)),await Ue(r),process.exit(0);case"list":case"ls":await Ne(),process.exit(0);case"setup":await He(e),process.exit(0);case"theme":await Fe(e.slice(1)),process.exit(0);case"agent":{const t=I.join(I.dirname(Te(import.meta.url)),"agent.js"),{spawn:u}=await import("node:child_process"),s=u(process.execPath,[t,...N],{stdio:"inherit"});s.on("error",o=>{console.error(o),process.exit(1)}),s.on("exit",o=>process.exit(o??0)),await new Promise(()=>{})}case"agent-token":await kt(e.slice(1)),process.exit(0);case"help":case"--help":case"-h":K(),process.exit(0);case"-V":case"--version":case"-v":De(),process.exit(0);default:console.error(`unknown argument: ${n}`),K(),process.exit(1)}}}function R(e,n){e.readyState===D.OPEN&&e.send(JSON.stringify(n))}const re=new Set,se=[],rt=7;function st(e){return typeof e!="number"||!Number.isFinite(e)?rt:Math.min(365,Math.max(1,Math.round(e)))}await b.read(),b.data.sessionAccess??=[],b.data.pinnedViews??=[],b.data.watchedPanes??=[],b.data.triggeredTasks??=[],b.data.quickCommands??=[];const O=await je();let at=await Be();const ae=O.commandbar===!0,J=st(O.scheduleHistoryDays),G=I.join(process.cwd(),"extensions"),Y=await Me(G);for(const e of Y)e.start&&se.push(Ie(e.dir,e));const H=new Ee({db:b,historyRetentionMs:J*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await H.restoreFromDb();const g=new ze({onChange:()=>{}}),T=new Map;function ie(e){return g.get(e)?T.get(e)??null:null}const ce={activeTheme:at,settingUpPassword:!1},it=Q({mode:"hub",securityConfig:h,tokenStore:y,rateLimiter:S,scheduler:H,settings:O,commandbarEnabled:ae,terminalRenderer:me(N,O.terminalRenderer),scheduleHistoryDays:J,extsDir:G,extensions:Y,terminalBufferConfig:U,state:ce,federation:{getConfig:()=>C,setConfig:async e=>{C=e,await et(e),ue(e)},status:lt,hostname:$()},hub:{agents:g,getChannel:ie}}),ct=Q({mode:"agent",securityConfig:h,tokenStore:y,rateLimiter:S,scheduler:H,settings:O,commandbarEnabled:ae,terminalRenderer:me(N,O.terminalRenderer),scheduleHistoryDays:J,extsDir:G,extensions:Y,terminalBufferConfig:U,state:ce});let C=await qe(),P=null;function le(){P&&(P.stop(),P=null)}function ue(e){le(),!(!e.enabled||!e.hub||!e.token)&&(P=tt({hub:e.hub,token:e.token,name:e.name?.trim()||$(),version:_e,app:ct,listSessions:Pe,terminalBufferConfig:U,getSessionPaneTarget:q,capturePaneTail:ee,capturePaneHistoryChunk:te,toCrlf:ne,handleClientMessage:X,acquireControlClient:Z}))}function lt(){return C.enabled?P?P.status():{state:"idle",agentId:null,hub:C.hub??"",name:C.name??$()}:{state:"idle",agentId:null,hub:C.hub??"",name:C.name??$()}}ue(C);const ut=parseInt(process.env.PORT||"21000",10),dt=we({fetch:it.fetch,port:ut,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),L=new Se({noServer:!0});function M(e,n,r){e.write(`HTTP/1.1 ${n} ${r}\r
3
3
  \r
4
- `),e.destroy()}ut.on("upgrade",(e,n,r)=>{const t=new URL(e.url||"/",`http://${e.headers.host}`),u=t.pathname;if(u==="/agent/ws"){pt(e,n,r);return}const s=u.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(s){dt(e,n,r,decodeURIComponent(s[1]),decodeURIComponent(s[2]));return}const o=u.match(/^\/ws\/(.+)$/);if(!o){n.destroy();return}const f=B(e),i=e.headers.origin,d=h.security.allowedOrigins;if(i&&!(i===`http://${e.headers.host}`||i===`https://${e.headers.host}`)&&(d.length===0||!d.includes(i))){l("ws_rejected_origin",{ip:f,origin:i}),M(n,403,"Origin not allowed");return}const k=nt(f);if(k>=h.security.maxConnectionsPerIp){l("ws_rejected_per_ip_cap",{ip:f,liveFromIp:k}),M(n,429,"Too many connections");return}const p=W(e)||t.searchParams.get("token")||"",w=p?y.validateToken(p):null;P.handleUpgrade(e,n,r,a=>{P.emit("connection",a,e,decodeURIComponent(o[1]),f,w)})}),P.on("connection",(e,n,r,t,u)=>{const s={ws:e,ip:t,authenticated:!1,authTimeout:null};E.set(e,s),l("ws_connected",{ip:t});let o=null,f=null;function i(a,c){s.authenticated=!0,s.authTimeout&&(clearTimeout(s.authTimeout),s.authTimeout=null),l("auth_success",{ip:t,method:a,tokenName:c}),R(e,{type:"auth.ok",setupMode:!h.passwordHash}),k()}function d(a,c={}){R(e,{type:"auth.failed",message:a,...c})}function k(){o||(o=Be(r,{send:a=>R(e,a),onMessage:a=>{f=a},onClose:a=>{p=a}},{terminalBufferConfig:U,getSessionPaneTarget:q,capturePaneTail:ee,capturePaneHistoryChunk:te,toCrlf:ne,handleClientMessage:X,acquireControlClient:Z,onPtyExit:(a,c)=>x(e,1e3,"pty exited"),onSpawnError:(a,c)=>x(e,1011,"pty spawn failed")}))}let p=null;async function w(a){let c;try{c=JSON.parse(a)}catch{return!1}if(c.type==="auth.token"&&typeof c.token=="string"){const m=y.validateToken(c.token);if(!m){l("token_auth_failed",{ip:t});const v=A.recordFailure(t);return v.permanentLock&&(y.revokeAll(),l("permanent_lock",{ip:t,failures:v.failures})),d("Invalid or expired token",{permanentLock:v.permanentLock,retryAfterMs:v.retryAfterMs}),!0}return y.touch(m.tokenHash),i("token",m.name),!0}if(c.type==="auth"&&typeof c.password=="string"){const m=A.check(t);if(!m.allowed)return l("rate_limited",{ip:t,retryAfterMs:m.retryAfterMs,permanentLock:m.permanentLock}),d("Too many attempts",{permanentLock:m.permanentLock,retryAfterMs:m.retryAfterMs}),!0;if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!gt(t))return l("setup_rejected_remote",{ip:t}),d("First-run setup must be performed from localhost"),!0;if(j)return d("Password setup in progress"),!0;const S=be(c.password);if(S)return d(S),!0;j=!0;try{h.passwordHash=await ve(c.password),Qe(h),l("password_set",{ip:t})}finally{j=!1}const{stored:F,plaintext:pe}=y.createAccessToken("setup",h.security.tokenTtlDays);return R(e,{type:"auth.ok",setupMode:!0,token:pe}),l("token_created",{ip:t,name:F.name,tokenId:F.id}),l("auth_success",{ip:t,method:"password",tokenName:F.name}),k(),!0}if(!h.passwordHash)return d("Server not configured"),!0;if(!await Ce(c.password,h.passwordHash)){const S=A.recordFailure(t);return l("auth_failed",{ip:t,method:"password",failures:S.failures,permanentLock:S.permanentLock}),S.permanentLock&&(y.revokeAll(),l("permanent_lock",{ip:t,failures:S.failures})),d("Incorrect password",{permanentLock:S.permanentLock,retryAfterMs:S.retryAfterMs}),!0}A.recordSuccess(t);const _=`ws-${t}`,{plaintext:fe}=y.createAccessToken(_,h.security.tokenTtlDays);return i("password",_),R(e,{type:"auth.ok",setupMode:!1,token:fe}),!0}return!1}u?(y.touch(u.tokenHash),i("token",u.name)):(R(e,{type:"auth.required",setupMode:!h.passwordHash}),s.authTimeout=setTimeout(()=>{s.authenticated||(l("auth_timeout",{ip:t}),d("Authentication timeout"),x(e,4e3,"Auth timeout"))},h.security.authTimeoutMs)),e.on("message",async a=>{const c=typeof a=="string"?a:a.toString("utf-8");if(!s.authenticated){if(c.length>1e6){R(e,{type:"auth.failed",message:"Message too large"});return}await w(c)||d("Authentication required");return}if(f)try{f(c)}catch{}}),e.on("close",()=>{s.authTimeout&&clearTimeout(s.authTimeout),E.delete(e),l("ws_disconnected",{ip:t}),p?.(),o&&(o.dispose(),o=null)}),e.on("error",()=>{s.authTimeout&&clearTimeout(s.authTimeout),E.delete(e),p?.(),o&&(o.dispose(),o=null)})});function dt(e,n,r,t,u){const s=B(e),o=new URL(e.url||"/",`http://${e.headers.host}`),f=W(e)||o.searchParams.get("token")||"";if(!(f?y.validateToken(f):null)){l("ws_rejected_remote_auth",{ip:s,agentId:t}),M(n,401,"Unauthorized");return}const d=ie(t);if(!d){l("ws_rejected_remote_offline",{ip:s,agentId:t}),M(n,503,"Agent offline");return}if(d.relayCount>=Ve){M(n,429,"Too many connections to agent");return}const k=e.headers.origin,p=h.security.allowedOrigins;if(k&&!(k===`http://${e.headers.host}`||k===`https://${e.headers.host}`)&&(p.length===0||!p.includes(k))){M(n,403,"Origin not allowed");return}const w=ft();P.handleUpgrade(e,n,r,a=>{if(d.registerRelay(w,u,a),g.bumpRelayConns(t,1),!d.attach(w,u)){a.send(JSON.stringify({type:"data",data:`\r
4
+ `),e.destroy()}dt.on("upgrade",(e,n,r)=>{const t=new URL(e.url||"/",`http://${e.headers.host}`),u=t.pathname;if(u==="/agent/ws"){ht(e,n,r);return}const s=u.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(s){mt(e,n,r,decodeURIComponent(s[1]),decodeURIComponent(s[2]));return}const o=u.match(/^\/ws\/(.+)$/);if(!o){n.destroy();return}const f=B(e),i=e.headers.origin,d=h.security.allowedOrigins;if(i&&!(i===`http://${e.headers.host}`||i===`https://${e.headers.host}`)&&(d.length===0||!d.includes(i))){l("ws_rejected_origin",{ip:f,origin:i}),M(n,403,"Origin not allowed");return}const k=ot(f);if(k>=h.security.maxConnectionsPerIp){l("ws_rejected_per_ip_cap",{ip:f,liveFromIp:k}),M(n,429,"Too many connections");return}const p=W(e)||t.searchParams.get("token")||"",w=p?y.validateToken(p):null;L.handleUpgrade(e,n,r,a=>{L.emit("connection",a,e,decodeURIComponent(o[1]),f,w)})}),L.on("connection",(e,n,r,t,u)=>{const s={ws:e,ip:t,authenticated:!1,authTimeout:null};E.set(e,s),l("ws_connected",{ip:t});let o=null,f=null;function i(a,c){s.authenticated=!0,s.authTimeout&&(clearTimeout(s.authTimeout),s.authTimeout=null),l("auth_success",{ip:t,method:a,tokenName:c}),R(e,{type:"auth.ok",setupMode:!h.passwordHash}),k()}function d(a,c={}){R(e,{type:"auth.failed",message:a,...c})}function k(){o||(o=We(r,{send:a=>R(e,a),onMessage:a=>{f=a},onClose:a=>{p=a}},{terminalBufferConfig:U,getSessionPaneTarget:q,capturePaneTail:ee,capturePaneHistoryChunk:te,toCrlf:ne,handleClientMessage:X,acquireControlClient:Z,tmuxSocketPath:Le()??void 0,onPtyExit:(a,c)=>A(e,1e3,"pty exited"),onSpawnError:(a,c)=>A(e,1011,"pty spawn failed")}))}let p=null;async function w(a){let c;try{c=JSON.parse(a)}catch{return!1}if(c.type==="auth.token"&&typeof c.token=="string"){const m=y.validateToken(c.token);if(!m){l("token_auth_failed",{ip:t});const v=S.recordFailure(t);return v.permanentLock&&(y.revokeAll(),l("permanent_lock",{ip:t,failures:v.failures})),d("Invalid or expired token",{permanentLock:v.permanentLock,retryAfterMs:v.retryAfterMs}),!0}return y.touch(m.tokenHash),i("token",m.name),!0}if(c.type==="auth"&&typeof c.password=="string"){const m=S.check(t);if(!m.allowed)return l("rate_limited",{ip:t,retryAfterMs:m.retryAfterMs,permanentLock:m.permanentLock}),d("Too many attempts",{permanentLock:m.permanentLock,retryAfterMs:m.retryAfterMs}),!0;if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!yt(t))return l("setup_rejected_remote",{ip:t}),d("First-run setup must be performed from localhost"),!0;if(j)return d("Password setup in progress"),!0;const x=be(c.password);if(x)return d(x),!0;j=!0;try{h.passwordHash=await ve(c.password),Ze(h),l("password_set",{ip:t})}finally{j=!1}const{stored:F,plaintext:pe}=y.createAccessToken("setup",h.security.tokenTtlDays);return R(e,{type:"auth.ok",setupMode:!0,token:pe}),l("token_created",{ip:t,name:F.name,tokenId:F.id}),l("auth_success",{ip:t,method:"password",tokenName:F.name}),k(),!0}if(!h.passwordHash)return d("Server not configured"),!0;if(!await Ce(c.password,h.passwordHash)){const x=S.recordFailure(t);return l("auth_failed",{ip:t,method:"password",failures:x.failures,permanentLock:x.permanentLock}),x.permanentLock&&(y.revokeAll(),l("permanent_lock",{ip:t,failures:x.failures})),d("Incorrect password",{permanentLock:x.permanentLock,retryAfterMs:x.retryAfterMs}),!0}S.recordSuccess(t);const _=`ws-${t}`,{plaintext:fe}=y.createAccessToken(_,h.security.tokenTtlDays);return i("password",_),R(e,{type:"auth.ok",setupMode:!1,token:fe}),!0}return!1}u?(y.touch(u.tokenHash),i("token",u.name)):(R(e,{type:"auth.required",setupMode:!h.passwordHash}),s.authTimeout=setTimeout(()=>{s.authenticated||(l("auth_timeout",{ip:t}),d("Authentication timeout"),A(e,4e3,"Auth timeout"))},h.security.authTimeoutMs)),e.on("message",async a=>{const c=typeof a=="string"?a:a.toString("utf-8");if(!s.authenticated){if(c.length>1e6){R(e,{type:"auth.failed",message:"Message too large"});return}await w(c)||d("Authentication required");return}if(f)try{f(c)}catch{}}),e.on("close",()=>{s.authTimeout&&clearTimeout(s.authTimeout),E.delete(e),l("ws_disconnected",{ip:t}),p?.(),o&&(o.dispose(),o=null)}),e.on("error",()=>{s.authTimeout&&clearTimeout(s.authTimeout),E.delete(e),p?.(),o&&(o.dispose(),o=null)})});function mt(e,n,r,t,u){const s=B(e),o=new URL(e.url||"/",`http://${e.headers.host}`),f=W(e)||o.searchParams.get("token")||"";if(!(f?y.validateToken(f):null)){l("ws_rejected_remote_auth",{ip:s,agentId:t}),M(n,401,"Unauthorized");return}const d=ie(t);if(!d){l("ws_rejected_remote_offline",{ip:s,agentId:t}),M(n,503,"Agent offline");return}if(d.relayCount>=Xe){M(n,429,"Too many connections to agent");return}const k=e.headers.origin,p=h.security.allowedOrigins;if(k&&!(k===`http://${e.headers.host}`||k===`https://${e.headers.host}`)&&(p.length===0||!p.includes(k))){M(n,403,"Origin not allowed");return}const w=pt();L.handleUpgrade(e,n,r,a=>{if(d.registerRelay(w,u,a),g.bumpRelayConns(t,1),!d.attach(w,u)){a.send(JSON.stringify({type:"data",data:`\r
5
5
  \x1B[31magent offline\x1B[0m\r
6
- `})),a.close(1011,"agent offline"),d.relayClosed(w),g.bumpRelayConns(t,-1);return}a.on("message",m=>{const v=typeof m=="string"?m:m.toString("utf-8");let _;try{_=JSON.parse(v)}catch{return}(_.type==="input"||_.type==="resize"||_.type==="load_history")&&d.sendToAgent(w,_)});const c=()=>{d.relayClosed(w),g.bumpRelayConns(t,-1)};a.on("close",c),a.on("error",c)})}let mt=1;function ft(){return mt++}function pt(e,n,r){const t=B(e),u=e.headers.origin;if(u){l("agent_rejected_origin",{ip:t,origin:u}),M(n,403,"Origin not allowed");return}P.handleUpgrade(e,n,r,s=>{let o=null,f=setTimeout(()=>{l("agent_hello_timeout",{ip:t}),x(s,4e3,"hello timeout")},1e4);s.on("message",(i,d)=>{if(o){g.touch(o.agentId),d?o.handleData(Array.isArray(i)?Buffer.concat(i):Buffer.from(i)):o.handleMessage(typeof i=="string"?i:Buffer.from(i).toString("utf-8"));return}const k=typeof i=="string"?i:i.toString("utf-8");let p;try{p=JSON.parse(k)}catch{x(s,1008,"bad hello");return}if(p.type!=="hello"||typeof p.token!="string"){x(s,1008,"expected hello");return}if(!A.check(t).allowed){l("agent_rate_limited",{ip:t}),x(s,429,"Too many attempts");return}const a=Xe(p.token);if(!a){A.recordFailure(t),l("agent_bad_token",{ip:t}),oe(s,{type:"hello_err",reason:"invalid token"}),x(s,4003,"invalid token");return}const c=typeof p.name=="string"&&p.name.trim()?p.name.trim().slice(0,64):"agent",m=a.id;if(!g.register(m,c,s,a.tokenHash)){l("agent_cap_reached",{ip:t}),oe(s,{type:"hello_err",reason:"too many agents"}),x(s,4004,"too many agents");return}clearTimeout(f),A.recordSuccess(t),l("agent_connected",{ip:t,agentId:m,name:c}),o=new ze(s,m,{onSessions:_=>g.setSessions(m,_),onClose:_=>{T.get(m)===_&&T.delete(m)}}),T.set(m,o),g.touch(m),s.send(JSON.stringify({type:"hello_ok",agentId:m})),o.sendSessionsReq()}),s.on("close",()=>{o&&(o.dispose(),T.delete(o.agentId),g.unregister(o.agentId),l("agent_disconnected",{ip:t,agentId:o.agentId})),clearTimeout(f)}),s.on("error",()=>{o&&(o.dispose(),T.delete(o.agentId),g.unregister(o.agentId)),clearTimeout(f)})})}const ht=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=g.pruneOffline(ht);for(const r of e){const t=g.peek(r);t&&t.ws.readyState===D.CLOSED&&(g.unregister(r),T.get(r)?.dispose(),T.delete(r))}const n=new Set(Ke().map(r=>r.tokenHash));for(const[r,t]of T){const u=g.peek(r);if(!(!u||n.has(u.tokenHash))){l("agent_token_revoked",{agentId:r}),t.dispose(),T.delete(r),g.unregister(r);try{u.ws.close(4003,"token revoked")}catch{}}}y.purgeExpired()},10*1e3).unref();function de(){H.cleanup(),le(),Je(),We();for(const e of T.values())e.dispose();T.clear();for(const[e,n]of E){try{e.close(1001,"Server shutting down")}catch{}n.authTimeout&&clearTimeout(n.authTimeout)}E.clear();for(const e of re)try{e.kill()}catch{}re.clear();for(const e of se)try{e.kill("SIGTERM")}catch{}A.dispose(),process.exit(0)}process.on("SIGINT",de),process.on("SIGTERM",de);function gt(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function me(e,n){let r="xterm";(n==="ghostty"||n==="xterm")&&(r=n);const t=process.env.TMUX_WEB_TERMINAL_RENDERER?.trim().toLowerCase();(t==="ghostty"||t==="xterm")&&(r=t);for(const u of e)u==="--ghostty"&&(r="ghostty"),u==="--xterm"&&(r="xterm");return r}async function yt(e){const{createAgentToken:n,listAgentTokens:r,removeAgentToken:t}=await import("./lib/agent-tokens.js"),[u,...s]=e;switch(u){case"add":{const o=s.find(d=>!d.startsWith("-"))||"agent",{id:f,token:i}=n(o);console.log("\u2713 agent token created"),console.log(` id: ${f}`),console.log(` name: ${o}`),console.log(` token: ${i}`),console.log(`
6
+ `})),a.close(1011,"agent offline"),d.relayClosed(w),g.bumpRelayConns(t,-1);return}a.on("message",m=>{const v=typeof m=="string"?m:m.toString("utf-8");let _;try{_=JSON.parse(v)}catch{return}(_.type==="input"||_.type==="resize"||_.type==="load_history")&&d.sendToAgent(w,_)});const c=()=>{d.relayClosed(w),g.bumpRelayConns(t,-1)};a.on("close",c),a.on("error",c)})}let ft=1;function pt(){return ft++}function ht(e,n,r){const t=B(e),u=e.headers.origin;if(u){l("agent_rejected_origin",{ip:t,origin:u}),M(n,403,"Origin not allowed");return}L.handleUpgrade(e,n,r,s=>{let o=null,f=setTimeout(()=>{l("agent_hello_timeout",{ip:t}),A(s,4e3,"hello timeout")},1e4);s.on("message",(i,d)=>{if(o){g.touch(o.agentId),d?o.handleData(Array.isArray(i)?Buffer.concat(i):Buffer.from(i)):o.handleMessage(typeof i=="string"?i:Buffer.from(i).toString("utf-8"));return}const k=typeof i=="string"?i:i.toString("utf-8");let p;try{p=JSON.parse(k)}catch{A(s,1008,"bad hello");return}if(p.type!=="hello"||typeof p.token!="string"){A(s,1008,"expected hello");return}if(!S.check(t).allowed){l("agent_rate_limited",{ip:t}),A(s,429,"Too many attempts");return}const a=Ke(p.token);if(!a){S.recordFailure(t),l("agent_bad_token",{ip:t}),oe(s,{type:"hello_err",reason:"invalid token"}),A(s,4003,"invalid token");return}const c=typeof p.name=="string"&&p.name.trim()?p.name.trim().slice(0,64):"agent",m=a.id;if(!g.register(m,c,s,a.tokenHash)){l("agent_cap_reached",{ip:t}),oe(s,{type:"hello_err",reason:"too many agents"}),A(s,4004,"too many agents");return}clearTimeout(f),S.recordSuccess(t),l("agent_connected",{ip:t,agentId:m,name:c}),o=new Ve(s,m,{onSessions:_=>g.setSessions(m,_),onClose:_=>{T.get(m)===_&&T.delete(m)}}),T.set(m,o),g.touch(m),s.send(JSON.stringify({type:"hello_ok",agentId:m})),o.sendSessionsReq()}),s.on("close",()=>{o&&(o.dispose(),T.delete(o.agentId),g.unregister(o.agentId),l("agent_disconnected",{ip:t,agentId:o.agentId})),clearTimeout(f)}),s.on("error",()=>{o&&(o.dispose(),T.delete(o.agentId),g.unregister(o.agentId)),clearTimeout(f)})})}const gt=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=g.pruneOffline(gt);for(const r of e){const t=g.peek(r);t&&t.ws.readyState===D.CLOSED&&(g.unregister(r),T.get(r)?.dispose(),T.delete(r))}const n=new Set(Qe().map(r=>r.tokenHash));for(const[r,t]of T){const u=g.peek(r);if(!(!u||n.has(u.tokenHash))){l("agent_token_revoked",{agentId:r}),t.dispose(),T.delete(r),g.unregister(r);try{u.ws.close(4003,"token revoked")}catch{}}}y.purgeExpired()},10*1e3).unref();function de(){H.cleanup(),le(),Ge(),Je();for(const e of T.values())e.dispose();T.clear();for(const[e,n]of E){try{e.close(1001,"Server shutting down")}catch{}n.authTimeout&&clearTimeout(n.authTimeout)}E.clear();for(const e of re)try{e.kill()}catch{}re.clear();for(const e of se)try{e.kill("SIGTERM")}catch{}S.dispose(),process.exit(0)}process.on("SIGINT",de),process.on("SIGTERM",de);function yt(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function me(e,n){let r="xterm";(n==="ghostty"||n==="xterm")&&(r=n);const t=process.env.TMUX_WEB_TERMINAL_RENDERER?.trim().toLowerCase();(t==="ghostty"||t==="xterm")&&(r=t);for(const u of e)u==="--ghostty"&&(r="ghostty"),u==="--xterm"&&(r="xterm");return r}async function kt(e){const{createAgentToken:n,listAgentTokens:r,removeAgentToken:t}=await import("./lib/agent-tokens.js"),[u,...s]=e;switch(u){case"add":{const o=s.find(d=>!d.startsWith("-"))||"agent",{id:f,token:i}=n(o);console.log("\u2713 agent token created"),console.log(` id: ${f}`),console.log(` name: ${o}`),console.log(` token: ${i}`),console.log(`
7
7
  Run on the remote machine:`),console.log(` tmux-web agent --hub wss://YOUR_HUB --token ${i} --name "${o}"`);return}case"list":{const o=r();if(!o.length){console.log("No agent tokens. Create one with: tmux-web agent-token add --name <name>");return}console.log("Agent tokens:");for(const f of o)console.log(` ${f.id} ${f.name} (created ${new Date(f.createdAt).toISOString()})`);return}case"remove":{const o=s.find(i=>!i.startsWith("-"));o||(console.error("usage: tmux-web agent-token remove <id>"),process.exit(1)),t(o)||(console.error(`no token with id ${o}`),process.exit(1)),console.log(`\u2713 token ${o} removed`);return}default:console.error("usage: tmux-web agent-token <add|list|remove>"),process.exit(1)}}
@@ -1 +1 @@
1
- import{WebSocket as k}from"ws";import{attachTerminal as J}from"./attach-terminal.js";import{encodeBinaryFrame as j,decodeBinaryFrame as x,BIN_KIND_HTTP_BODY as $}from"./agent-channel.js";const K=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),W=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),F=new Set(["content-type","content-disposition","location","cache-control","etag"]);function Q(o){const T=o.log??(e=>console.log(`[agent] ${e}`)),f=e=>console.error(`[agent] ${e}`);let s=null,S=1e3,B=null,m=!1;const d=new Map,h=new Map,p=new Map,b=new Set;function A(e){return b.has(e)?Promise.resolve():new Promise(t=>{p.set(e,t),setTimeout(()=>{p.delete(e)&&t()},3e4)})}let y=null,_=null,E="",w=null;const g={state:"idle",agentId:null,hub:o.hub,name:o.name};function u(e,t){t!==void 0&&(g.lastError=t),g.state=e,g.agentId=w,o.onStatus?.({...g})}function c(e){s&&s.readyState===k.OPEN&&s.send(JSON.stringify(e))}function C(){let e;try{e=o.listSessions()}catch{return}const t=JSON.stringify(e);t!==E&&(E=t,c({type:"sessions",sessions:e}))}function v(){y||(y=setInterval(C,W)),_||(_=setInterval(()=>c({type:"ping",ts:Date.now()}),K))}function H(){y&&(clearInterval(y),y=null),_&&(clearInterval(_),_=null)}async function O(e){let t;if(e.hasBody){await A(e.id),b.delete(e.id);const r=h.get(e.id);h.delete(e.id),t=r&&r.length?Buffer.concat(r):Buffer.alloc(0)}const n=new URL(e.path,"http://agent.invalid");let i;try{const r={method:e.method,headers:e.headers};t&&t.length&&(r.body=t),i=await Promise.race([o.app.request(new Request(n,r)),new Promise((G,D)=>setTimeout(()=>D(new Error("app.request timed out")),3e4))])}catch(r){f(`tunnel request failed: ${e.method} ${e.path} ${r.message}`),c({type:"http_resp",id:e.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const I={};i.headers.forEach((r,l)=>{F.has(l.toLowerCase())&&(I[l]=r)});const a=Buffer.from(await i.arrayBuffer());if(c({type:"http_resp",id:e.id,status:i.status,headers:I,hasBody:a.length>0}),a.length>0){for(let l=0;l<a.length;l+=65536){if(!s||s.readyState!==k.OPEN)return;s.send(j($,e.id,a.subarray(l,l+65536)))}c({type:"http_body_end",id:e.id})}}function L(e){const t=e.session;let n=!1;try{n=o.listSessions().some(a=>a.name===t)}catch{n=!1}if(!n){c({type:"attach_err",connId:e.connId,session:t,message:"no such session"});return}let i=null;const I=J(t,{send:a=>c({type:"ws_to_hub",connId:e.connId,msg:a}),onMessage:a=>{i=a},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,onPtyExit:(a,r)=>{c({type:"ws_close",connId:e.connId,code:1e3,reason:`tmux exited (${r})`})},onSpawnError:(a,r)=>{f(`attach failed for session "${e.session}": ${r}`),c({type:"attach_err",connId:e.connId,session:t,message:r})},onAttached:a=>{c({type:"attach_ok",connId:e.connId,session:t})}});d.set(e.connId,{onMessage:a=>i?.(a),dispose:()=>{d.delete(e.connId),I.dispose()}})}function R(e){let t;try{t=JSON.parse(e)}catch{return}switch(t.type){case"hello_ok":w=t.agentId,S=1e3,T(`connected to hub as "${w}"`),u("connected"),C(),v();break;case"hello_err":if(f(`hub rejected us: ${t.reason}`),u("error",t.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":C();break;case"http_req":O(t);break;case"http_body_end":{b.add(t.id);const n=p.get(t.id);n&&(p.delete(t.id),n());break}case"attach":L(t);break;case"ws_to_agent":{const n=d.get(t.connId);if(n)try{n.onMessage(JSON.stringify(t.msg))}catch{}break}case"detach":{const n=d.get(t.connId);n&&n.dispose();break}case"pong":break;default:break}}function q(e){const t=x(e);if(t&&t.kind===$){const n=h.get(t.id)??[];n.push(t.payload),h.set(t.id,n)}}function M(){H();for(const e of d.values())try{e.dispose()}catch{}d.clear(),h.clear(),p.clear(),b.clear(),E=""}function P(){if(s){try{s.removeAllListeners()}catch{}s=null}if(M(),m)return;u("connecting");const e=S+Math.random()*500;T(`reconnecting in ${Math.round(e)}ms\u2026`),B=setTimeout(N,e),S=Math.min(S*2,6e4)}function N(){if(m||s)return;const e=o.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let t;try{t=new k(e)}catch(n){f(`bad hub URL: ${n.message} (${e})`),u("error",`bad hub URL: ${n.message}`);return}s=t,u("connecting"),t.on("open",()=>{T(`connecting to ${e}\u2026`),c({type:"hello",token:o.token,name:o.name,version:o.version})}),t.on("message",(n,i)=>{i?q(Array.isArray(n)?Buffer.concat(n):Buffer.isBuffer(n)?n:Buffer.from(n)):R(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),t.on("close",()=>{s===t&&P()}),t.on("error",n=>{f(`connection error: ${n.message}`),s===t&&P()})}function U(){if(!m){if(m=!0,B&&clearTimeout(B),M(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}w=null,u("idle")}}return N(),{stop:U,status:()=>({...g})}}export{Q as startAgentClient};
1
+ import{WebSocket as C}from"ws";import{attachTerminal as D}from"./attach-terminal.js";import{encodeBinaryFrame as J,decodeBinaryFrame as j,BIN_KIND_HTTP_BODY as $}from"./agent-channel.js";const K=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),W=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),F=new Set(["content-type","content-disposition","location","cache-control","etag"]);function Q(o){const T=o.log??(e=>console.log(`[agent] ${e}`)),f=e=>console.error(`[agent] ${e}`);let s=null,S=1e3,B=null,m=!1;const d=new Map,h=new Map,y=new Map,b=new Set;function A(e){return b.has(e)?Promise.resolve():new Promise(t=>{y.set(e,t),setTimeout(()=>{y.delete(e)&&t()},3e4)})}let p=null,_=null,E="",w=null;const g={state:"idle",agentId:null,hub:o.hub,name:o.name};function u(e,t){t!==void 0&&(g.lastError=t),g.state=e,g.agentId=w,o.onStatus?.({...g})}function c(e){s&&s.readyState===C.OPEN&&s.send(JSON.stringify(e))}function k(){let e;try{e=o.listSessions()}catch{return}const t=JSON.stringify(e);t!==E&&(E=t,c({type:"sessions",sessions:e}))}function v(){p||(p=setInterval(k,W)),_||(_=setInterval(()=>c({type:"ping",ts:Date.now()}),K))}function H(){p&&(clearInterval(p),p=null),_&&(clearInterval(_),_=null)}async function O(e){let t;if(e.hasBody){await A(e.id),b.delete(e.id);const r=h.get(e.id);h.delete(e.id),t=r&&r.length?Buffer.concat(r):Buffer.alloc(0)}const n=new URL(e.path,"http://agent.invalid");let i;try{const r={method:e.method,headers:e.headers};t&&t.length&&(r.body=t),i=await Promise.race([o.app.request(new Request(n,r)),new Promise((G,x)=>setTimeout(()=>x(new Error("app.request timed out")),3e4))])}catch(r){f(`tunnel request failed: ${e.method} ${e.path} ${r.message}`),c({type:"http_resp",id:e.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const I={};i.headers.forEach((r,l)=>{F.has(l.toLowerCase())&&(I[l]=r)});const a=Buffer.from(await i.arrayBuffer());if(c({type:"http_resp",id:e.id,status:i.status,headers:I,hasBody:a.length>0}),a.length>0){for(let l=0;l<a.length;l+=65536){if(!s||s.readyState!==C.OPEN)return;s.send(J($,e.id,a.subarray(l,l+65536)))}c({type:"http_body_end",id:e.id})}}function L(e){const t=e.session;let n=!1;try{n=o.listSessions().some(a=>a.name===t)}catch{n=!1}if(!n){c({type:"attach_err",connId:e.connId,session:t,message:"no such session"});return}let i=null;const I=D(t,{send:a=>c({type:"ws_to_hub",connId:e.connId,msg:a}),onMessage:a=>{i=a},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,tmuxSocketPath:o.tmuxSocketPath,onPtyExit:(a,r)=>{c({type:"ws_close",connId:e.connId,code:1e3,reason:`tmux exited (${r})`})},onSpawnError:(a,r)=>{f(`attach failed for session "${e.session}": ${r}`),c({type:"attach_err",connId:e.connId,session:t,message:r})},onAttached:a=>{c({type:"attach_ok",connId:e.connId,session:t})}});d.set(e.connId,{onMessage:a=>i?.(a),dispose:()=>{d.delete(e.connId),I.dispose()}})}function R(e){let t;try{t=JSON.parse(e)}catch{return}switch(t.type){case"hello_ok":w=t.agentId,S=1e3,T(`connected to hub as "${w}"`),u("connected"),k(),v();break;case"hello_err":if(f(`hub rejected us: ${t.reason}`),u("error",t.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":k();break;case"http_req":O(t);break;case"http_body_end":{b.add(t.id);const n=y.get(t.id);n&&(y.delete(t.id),n());break}case"attach":L(t);break;case"ws_to_agent":{const n=d.get(t.connId);if(n)try{n.onMessage(JSON.stringify(t.msg))}catch{}break}case"detach":{const n=d.get(t.connId);n&&n.dispose();break}case"pong":break;default:break}}function q(e){const t=j(e);if(t&&t.kind===$){const n=h.get(t.id)??[];n.push(t.payload),h.set(t.id,n)}}function P(){H();for(const e of d.values())try{e.dispose()}catch{}d.clear(),h.clear(),y.clear(),b.clear(),E=""}function M(){if(s){try{s.removeAllListeners()}catch{}s=null}if(P(),m)return;u("connecting");const e=S+Math.random()*500;T(`reconnecting in ${Math.round(e)}ms\u2026`),B=setTimeout(N,e),S=Math.min(S*2,6e4)}function N(){if(m||s)return;const e=o.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let t;try{t=new C(e)}catch(n){f(`bad hub URL: ${n.message} (${e})`),u("error",`bad hub URL: ${n.message}`);return}s=t,u("connecting"),t.on("open",()=>{T(`connecting to ${e}\u2026`),c({type:"hello",token:o.token,name:o.name,version:o.version})}),t.on("message",(n,i)=>{i?q(Array.isArray(n)?Buffer.concat(n):Buffer.isBuffer(n)?n:Buffer.from(n)):R(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),t.on("close",()=>{s===t&&M()}),t.on("error",n=>{f(`connection error: ${n.message}`),s===t&&M()})}function U(){if(!m){if(m=!0,B&&clearTimeout(B),P(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}w=null,u("idle")}}return N(),{stop:U,status:()=>({...g})}}export{Q as startAgentClient};
@@ -1,6 +1,6 @@
1
- import*as O from"node-pty";const f=new Set;function _(){for(const n of[...f])try{n()}catch{}f.clear()}function B(){return f.size}function H(n,r,e){const{initialLines:M,historyChunk:A,syncIdleMs:P,syncMaxMs:w}=e.terminalBufferConfig,E=e.spawn??(c=>O.spawn("tmux",["attach-session","-t",c],{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:process.env}));let a=null,y=n,d=null,g=0,T="",h=!0,p="",i=null,m=null,o=!1;const S=()=>{i&&(clearTimeout(i),i=null),m&&(clearTimeout(m),m=null)},b=()=>{if(!(!h||o)){S(),h=!1;try{y=e.getSessionPaneTarget(n)}catch{y=n}try{const c=e.capturePaneTail(y,M);r.send({type:"snapshot",data:e.toCrlf(c),lines:M})}catch{r.send({type:"data",data:`\r
2
- `})}p&&(r.send({type:"data",data:p}),p="")}},k=()=>{h&&(i&&clearTimeout(i),i=setTimeout(b,P))};function $(){if(o)return;try{a=E(n)}catch(t){const l=t?.message??String(t);r.send({type:"data",data:`\r
3
- \x1B[31mFailed to attach to tmux session "${n}": ${l}\x1B[0m\r
4
- `}),e.onSpawnError?.(n,l);return}let c=-1,s="";d=e.acquireControlClient(n,({activeIndex:t,windows:l})=>{r.send({type:"window_changed",activeIndex:t,windows:l});const x=l.map(D=>D.index).join(","),v=t!==c||x!==s;c=t,s=x}),g=Date.now(),m=setTimeout(b,w),e.onAttached?.(n),r.onMessage(C),a.onData(t=>{if(!o){if(Date.now()-g<w&&T.length<2048&&(T+=t),h){p+=t,k();return}r.send({type:"data",data:t})}}),a.onExit(({exitCode:t})=>{if(S(),a&&(a=null),!o){if(t!==0&&Date.now()-g<w){const l=T.trim();e.onSpawnError?.(n,l?`tmux attach-session failed: ${l}`:`tmux attach-session exited with code ${t}`);return}e.onPtyExit?.(n,t),r.send({type:"data",data:`\r
1
+ import*as O from"node-pty";import{tmuxEnv as _}from"./tmux-env.js";const f=new Set;function H(){for(const e of[...f])try{e()}catch{}f.clear()}function K(){return f.size}function j(e,r,n){const{initialLines:M,historyChunk:C,syncIdleMs:k,syncMaxMs:g}=n.terminalBufferConfig,A=n.spawn??(o=>{const c=n.tmuxSocketPath?["-S",n.tmuxSocketPath]:[];return c.push("attach-session","-t",o),O.spawn("tmux",c,{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:_()})});let a=null,y=e,h=null,w=0,S="",d=!0,m="",u=null,p=null,s=!1;const T=()=>{u&&(clearTimeout(u),u=null),p&&(clearTimeout(p),p=null)},P=()=>{if(!(!d||s)){T(),d=!1;try{y=n.getSessionPaneTarget(e)}catch{y=e}try{const o=n.capturePaneTail(y,M);r.send({type:"snapshot",data:n.toCrlf(o),lines:M})}catch{r.send({type:"data",data:`\r
2
+ `})}m&&(r.send({type:"data",data:m}),m="")}},E=()=>{d&&(u&&clearTimeout(u),u=setTimeout(P,k))};function $(){if(s)return;try{a=A(e)}catch(t){const l=t?.message??String(t);r.send({type:"data",data:`\r
3
+ \x1B[31mFailed to attach to tmux session "${e}": ${l}\x1B[0m\r
4
+ `}),n.onSpawnError?.(e,l);return}let o=-1,c="";h=n.acquireControlClient(e,({activeIndex:t,windows:l})=>{r.send({type:"window_changed",activeIndex:t,windows:l});const x=l.map(D=>D.index).join(","),v=t!==o||x!==c;o=t,c=x}),w=Date.now(),p=setTimeout(P,g),n.onAttached?.(e),r.onMessage(b),a.onData(t=>{if(!s){if(Date.now()-w<g&&S.length<2048&&(S+=t),d){m+=t,E();return}r.send({type:"data",data:t})}}),a.onExit(({exitCode:t})=>{if(T(),a&&(a=null),!s){if(t!==0&&Date.now()-w<g){const l=S.trim();n.onSpawnError?.(e,l?`tmux attach-session failed: ${l}`:`tmux attach-session exited with code ${t}`);return}n.onPtyExit?.(e,t),r.send({type:"data",data:`\r
5
5
  \x1B[2m--- tmux exited (code ${t}) ---\x1B[0m\r
6
- `}),u()}})}function u(){if(!o){if(o=!0,S(),d&&(d(),d=null),a){try{a.kill()}catch{}a=null}f.delete(u)}}r.onClose(u);const C=c=>{if(o||!a)return;try{if(e.handleClientMessage(c,a))return}catch{return}let s;try{s=JSON.parse(c)}catch{return}if(s.type==="load_history"&&typeof s.before=="number"){const t=Math.max(0,Math.floor(s.before));try{const{data:l,lines:x}=e.capturePaneHistoryChunk(y,t,A);r.send({type:"history",data:e.toCrlf(l),before:t,lines:x})}catch{r.send({type:"history",data:"",before:t,lines:0})}}};return $(),f.add(u),{onMessage:C,dispose:u}}export{B as _liveAttachmentCount,H as attachTerminal,_ as killAllAttachedPtys};
6
+ `}),i()}})}function i(){if(!s){if(s=!0,T(),h&&(h(),h=null),a){try{a.kill()}catch{}a=null}f.delete(i)}}r.onClose(i);const b=o=>{if(s||!a)return;try{if(n.handleClientMessage(o,a))return}catch{return}let c;try{c=JSON.parse(o)}catch{return}if(c.type==="load_history"&&typeof c.before=="number"){const t=Math.max(0,Math.floor(c.before));try{const{data:l,lines:x}=n.capturePaneHistoryChunk(y,t,C);r.send({type:"history",data:n.toCrlf(l),before:t,lines:x})}catch{r.send({type:"history",data:"",before:t,lines:0})}}};return $(),f.add(i),{onMessage:b,dispose:i}}export{K as _liveAttachmentCount,j as attachTerminal,H as killAllAttachedPtys};
@@ -1 +1 @@
1
- import{execFileSync as o}from"node:child_process";import{randomUUID as c}from"node:crypto";import{ARM_SCAN_INTERVAL_MS as u,isValidDelayMs as h,MAX_TIMER_MS as n}from"./schedule-delay.js";const T=10080*60*1e3;function m(a,e,s){const t=`${a}:${e}`;o("tmux",["send-keys","-t",t,"-l",s],{timeout:5e3}),o("tmux",["send-keys","-t",t,"Enter"],{timeout:5e3})}function w(a){if(!a||typeof a!="object")return!1;const e=a;return typeof e.delayMs=="number"&&h(e.delayMs)}function y(a){if(!a||typeof a!="object")return!1;const e=a;return typeof e.sessionName=="string"&&e.sessionName.length>0&&typeof e.windowIndex=="number"&&Number.isInteger(e.windowIndex)&&e.windowIndex>=0&&typeof e.text=="string"&&e.text.length>0&&e.text.length<=4096&&typeof e.delayMs=="number"&&h(e.delayMs)}class I{deps;scheduledTasks=new Map;now;setTimer;clearTimer;setIntervalFn;clearIntervalFn;armScanIntervalMs;createId;sendKeys;onError;onMissedTask;historyRetentionMs;scanIntervalHandle=null;constructor(e){this.deps=e,this.now=e.now??Date.now,this.setTimer=e.setTimer??setTimeout,this.clearTimer=e.clearTimer??clearTimeout,this.setIntervalFn=e.setInterval??setInterval,this.clearIntervalFn=e.clearInterval??clearInterval,this.armScanIntervalMs=e.armScanIntervalMs??u,this.createId=e.createId??c,this.sendKeys=e.sendKeys??m,this.onError=e.onError??(s=>console.error(s)),this.onMissedTask=e.onMissedTask??(()=>{}),this.historyRetentionMs=e.historyRetentionMs??T}record(e){this.deps.db.data.triggeredTasks??=[],this.deps.db.data.triggeredTasks.push(e);const s=this.now()-this.historyRetentionMs;this.deps.db.data.triggeredTasks=this.deps.db.data.triggeredTasks.filter(t=>t.triggeredAt>=s)}listTriggered(){this.deps.db.data.triggeredTasks??=[];const e=this.now()-this.historyRetentionMs;return this.deps.db.data.triggeredTasks.filter(s=>s.triggeredAt>=e).sort((s,t)=>t.triggeredAt-s.triggeredAt)}async restoreFromDb(){const e=this.now(),s=[];for(const r of this.deps.db.data.scheduledTasks)r.fireAt<=e?(s.push(r.id),this.onMissedTask(r),this.record({...r,triggeredAt:e,status:"missed"})):this.registerTask(r);this.deps.db.data.triggeredTasks??=[];const t=this.deps.db.data.triggeredTasks.length,i=e-this.historyRetentionMs;this.deps.db.data.triggeredTasks=this.deps.db.data.triggeredTasks.filter(r=>r.triggeredAt>=i);const d=this.deps.db.data.triggeredTasks.length!==t;s.length&&(this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(r=>!s.includes(r.id))),(s.length||d)&&await this.deps.db.write(),this.scanPending(),this.startScanLoop()}list(e){return[...this.scheduledTasks.values()].filter(s=>!e||s.sessionName===e).map(({id:s,sessionName:t,windowIndex:i,text:d,fireAt:r,createdAt:l})=>({id:s,sessionName:t,windowIndex:i,text:d,fireAt:r,createdAt:l,remainingMs:Math.max(0,r-this.now())})).sort((s,t)=>s.fireAt-t.fireAt)}async create(e){const s=this.createId(),t=this.now(),i={id:s,sessionName:e.sessionName,windowIndex:e.windowIndex,text:e.text,fireAt:t+e.delayMs,createdAt:t};return this.registerTask(i),this.deps.db.data.scheduledTasks.push(i),await this.deps.db.write(),this.tryArm(i.id),i}async reschedule(e,s){const t=this.scheduledTasks.get(e);if(!t)return null;this.disarmTask(e);const i={...t,fireAt:this.now()+s};this.registerTask(i);const d=this.deps.db.data.scheduledTasks.findIndex(r=>r.id===e);return d>=0&&(this.deps.db.data.scheduledTasks[d]=i),await this.deps.db.write(),this.tryArm(e),i}async delete(e){return this.scheduledTasks.get(e)?(this.disarmTask(e),this.scheduledTasks.delete(e),this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(t=>t.id!==e),await this.deps.db.write(),!0):!1}cleanup(){if(this.scanIntervalHandle!=null){try{this.clearIntervalFn(this.scanIntervalHandle)}catch{}this.scanIntervalHandle=null}for(const e of this.scheduledTasks.values())if(e.timeoutHandle!=null)try{this.clearTimer(e.timeoutHandle)}catch{}this.scheduledTasks.clear()}scanPending(){for(const e of[...this.scheduledTasks.values()]){const s=e.fireAt-this.now();if(s<=0){e.timeoutHandle!=null&&this.disarmTask(e.id),this.fireTask(e);continue}s<=n&&e.timeoutHandle==null&&this.armTask(e)}}registerTask(e){this.scheduledTasks.set(e.id,{...e,timeoutHandle:null})}disarmTask(e){const s=this.scheduledTasks.get(e);s&&s.timeoutHandle!=null&&(this.clearTimer(s.timeoutHandle),s.timeoutHandle=null)}tryArm(e){const s=this.scheduledTasks.get(e);if(!s)return;const t=s.fireAt-this.now();if(t<=0){this.fireTask(s);return}t<=n&&s.timeoutHandle==null&&this.armTask(s)}armTask(e){const s=e.fireAt-this.now();if(s<=0){this.fireTask(e);return}this.disarmTask(e.id);const t=Math.min(s,n),i=this.setTimer(()=>this.onTimer(e.id),t);this.scheduledTasks.set(e.id,{...e,timeoutHandle:i})}onTimer(e){const s=this.scheduledTasks.get(e);if(s){if(s.timeoutHandle=null,s.fireAt<=this.now()){this.fireTask(s);return}this.armTask(s)}}startScanLoop(){this.scanIntervalHandle==null&&(this.scanIntervalHandle=this.setIntervalFn(()=>this.scanPending(),this.armScanIntervalMs))}fireTask(e){if(!this.scheduledTasks.has(e.id))return;this.disarmTask(e.id);const s=this.now(),t={id:e.id,sessionName:e.sessionName,windowIndex:e.windowIndex,text:e.text,fireAt:e.fireAt,createdAt:e.createdAt};try{this.sendKeys(e.sessionName,e.windowIndex,e.text),this.record({...t,triggeredAt:s,status:"ok"})}catch(i){const d=String(i?.message??i);this.onError(`[scheduler] send-keys to ${e.sessionName}:${e.windowIndex} failed: ${d}`),this.record({...t,triggeredAt:s,status:"error",error:d})}this.scheduledTasks.delete(e.id),this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(i=>i.id!==e.id),this.deps.db.write().catch(this.onError)}}export{I as SchedulerService,w as isValidRescheduleInput,y as isValidScheduleInput,m as sendTmuxKeys};
1
+ import{execFileSync as o}from"node:child_process";import{tmuxEnv as h}from"./tmux-env.js";import{randomUUID as u}from"node:crypto";import{ARM_SCAN_INTERVAL_MS as T,isValidDelayMs as l,MAX_TIMER_MS as d}from"./schedule-delay.js";const m=10080*60*1e3;function f(n,e,s){const t=`${n}:${e}`;o("tmux",["send-keys","-t",t,"-l",s],{timeout:5e3,env:h()}),o("tmux",["send-keys","-t",t,"Enter"],{timeout:5e3,env:h()})}function I(n){if(!n||typeof n!="object")return!1;const e=n;return typeof e.delayMs=="number"&&l(e.delayMs)}function p(n){if(!n||typeof n!="object")return!1;const e=n;return typeof e.sessionName=="string"&&e.sessionName.length>0&&typeof e.windowIndex=="number"&&Number.isInteger(e.windowIndex)&&e.windowIndex>=0&&typeof e.text=="string"&&e.text.length>0&&e.text.length<=4096&&typeof e.delayMs=="number"&&l(e.delayMs)}class b{deps;scheduledTasks=new Map;now;setTimer;clearTimer;setIntervalFn;clearIntervalFn;armScanIntervalMs;createId;sendKeys;onError;onMissedTask;historyRetentionMs;scanIntervalHandle=null;constructor(e){this.deps=e,this.now=e.now??Date.now,this.setTimer=e.setTimer??setTimeout,this.clearTimer=e.clearTimer??clearTimeout,this.setIntervalFn=e.setInterval??setInterval,this.clearIntervalFn=e.clearInterval??clearInterval,this.armScanIntervalMs=e.armScanIntervalMs??T,this.createId=e.createId??u,this.sendKeys=e.sendKeys??f,this.onError=e.onError??(s=>console.error(s)),this.onMissedTask=e.onMissedTask??(()=>{}),this.historyRetentionMs=e.historyRetentionMs??m}record(e){this.deps.db.data.triggeredTasks??=[],this.deps.db.data.triggeredTasks.push(e);const s=this.now()-this.historyRetentionMs;this.deps.db.data.triggeredTasks=this.deps.db.data.triggeredTasks.filter(t=>t.triggeredAt>=s)}listTriggered(){this.deps.db.data.triggeredTasks??=[];const e=this.now()-this.historyRetentionMs;return this.deps.db.data.triggeredTasks.filter(s=>s.triggeredAt>=e).sort((s,t)=>t.triggeredAt-s.triggeredAt)}async restoreFromDb(){const e=this.now(),s=[];for(const r of this.deps.db.data.scheduledTasks)r.fireAt<=e?(s.push(r.id),this.onMissedTask(r),this.record({...r,triggeredAt:e,status:"missed"})):this.registerTask(r);this.deps.db.data.triggeredTasks??=[];const t=this.deps.db.data.triggeredTasks.length,i=e-this.historyRetentionMs;this.deps.db.data.triggeredTasks=this.deps.db.data.triggeredTasks.filter(r=>r.triggeredAt>=i);const a=this.deps.db.data.triggeredTasks.length!==t;s.length&&(this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(r=>!s.includes(r.id))),(s.length||a)&&await this.deps.db.write(),this.scanPending(),this.startScanLoop()}list(e){return[...this.scheduledTasks.values()].filter(s=>!e||s.sessionName===e).map(({id:s,sessionName:t,windowIndex:i,text:a,fireAt:r,createdAt:c})=>({id:s,sessionName:t,windowIndex:i,text:a,fireAt:r,createdAt:c,remainingMs:Math.max(0,r-this.now())})).sort((s,t)=>s.fireAt-t.fireAt)}async create(e){const s=this.createId(),t=this.now(),i={id:s,sessionName:e.sessionName,windowIndex:e.windowIndex,text:e.text,fireAt:t+e.delayMs,createdAt:t};return this.registerTask(i),this.deps.db.data.scheduledTasks.push(i),await this.deps.db.write(),this.tryArm(i.id),i}async reschedule(e,s){const t=this.scheduledTasks.get(e);if(!t)return null;this.disarmTask(e);const i={...t,fireAt:this.now()+s};this.registerTask(i);const a=this.deps.db.data.scheduledTasks.findIndex(r=>r.id===e);return a>=0&&(this.deps.db.data.scheduledTasks[a]=i),await this.deps.db.write(),this.tryArm(e),i}async delete(e){return this.scheduledTasks.get(e)?(this.disarmTask(e),this.scheduledTasks.delete(e),this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(t=>t.id!==e),await this.deps.db.write(),!0):!1}cleanup(){if(this.scanIntervalHandle!=null){try{this.clearIntervalFn(this.scanIntervalHandle)}catch{}this.scanIntervalHandle=null}for(const e of this.scheduledTasks.values())if(e.timeoutHandle!=null)try{this.clearTimer(e.timeoutHandle)}catch{}this.scheduledTasks.clear()}scanPending(){for(const e of[...this.scheduledTasks.values()]){const s=e.fireAt-this.now();if(s<=0){e.timeoutHandle!=null&&this.disarmTask(e.id),this.fireTask(e);continue}s<=d&&e.timeoutHandle==null&&this.armTask(e)}}registerTask(e){this.scheduledTasks.set(e.id,{...e,timeoutHandle:null})}disarmTask(e){const s=this.scheduledTasks.get(e);s&&s.timeoutHandle!=null&&(this.clearTimer(s.timeoutHandle),s.timeoutHandle=null)}tryArm(e){const s=this.scheduledTasks.get(e);if(!s)return;const t=s.fireAt-this.now();if(t<=0){this.fireTask(s);return}t<=d&&s.timeoutHandle==null&&this.armTask(s)}armTask(e){const s=e.fireAt-this.now();if(s<=0){this.fireTask(e);return}this.disarmTask(e.id);const t=Math.min(s,d),i=this.setTimer(()=>this.onTimer(e.id),t);this.scheduledTasks.set(e.id,{...e,timeoutHandle:i})}onTimer(e){const s=this.scheduledTasks.get(e);if(s){if(s.timeoutHandle=null,s.fireAt<=this.now()){this.fireTask(s);return}this.armTask(s)}}startScanLoop(){this.scanIntervalHandle==null&&(this.scanIntervalHandle=this.setIntervalFn(()=>this.scanPending(),this.armScanIntervalMs))}fireTask(e){if(!this.scheduledTasks.has(e.id))return;this.disarmTask(e.id);const s=this.now(),t={id:e.id,sessionName:e.sessionName,windowIndex:e.windowIndex,text:e.text,fireAt:e.fireAt,createdAt:e.createdAt};try{this.sendKeys(e.sessionName,e.windowIndex,e.text),this.record({...t,triggeredAt:s,status:"ok"})}catch(i){const a=String(i?.message??i);this.onError(`[scheduler] send-keys to ${e.sessionName}:${e.windowIndex} failed: ${a}`),this.record({...t,triggeredAt:s,status:"error",error:a})}this.scheduledTasks.delete(e.id),this.deps.db.data.scheduledTasks=this.deps.db.data.scheduledTasks.filter(i=>i.id!==e.id),this.deps.db.write().catch(this.onError)}}export{b as SchedulerService,I as isValidRescheduleInput,p as isValidScheduleInput,f as sendTmuxKeys};
@@ -1,5 +1,5 @@
1
- import{execFileSync as f}from"node:child_process";const l=3e3;function g(n){return n.replace(/\r?\n/g,`\r
2
- `)}function o(n){return f("tmux",n,{encoding:"utf-8",timeout:l}).trimEnd()}function S(n){return o(["display-message","-p","-t",n,"#{session_name}:#{window_index}.#{pane_index}"])}function m(n){const t=o(["display-message","-p","-t",n,"#{history_size}"]),e=parseInt(t,10);return Number.isFinite(e)?e:0}function _(n){return o(["display-message","-p","-t",n,"#{alternate_on}"])==="1"}function c(n,t,e){const i=["capture-pane","-t",n,"-p","-e","-S",String(t)];return e!==void 0&&i.push("-E",String(e)),o(i)}function h(n,t){return c(n,-t)}function y(n,t,e){const i=m(n);if(t>=i)return{data:"",lines:0};const p=-(t+e),a=-(t+1);let r=c(n,p,a);const u=r===""?0:r.split(`
1
+ import{execFileSync as f}from"node:child_process";import{tmuxEnv as l}from"./tmux-env.js";const m=3e3;function _(n){return n.replace(/\r?\n/g,`\r
2
+ `)}function o(n){return f("tmux",n,{encoding:"utf-8",timeout:m,env:l()}).trimEnd()}function h(n){return o(["display-message","-p","-t",n,"#{session_name}:#{window_index}.#{pane_index}"])}function x(n){const t=o(["display-message","-p","-t",n,"#{history_size}"]),e=parseInt(t,10);return Number.isFinite(e)?e:0}function y(n){return o(["display-message","-p","-t",n,"#{alternate_on}"])==="1"}function c(n,t,e){const i=["capture-pane","-t",n,"-p","-e","-S",String(t)];return e!==void 0&&i.push("-E",String(e)),o(i)}function P(n,t){return c(n,-t)}function T(n,t,e){const i=x(n);if(t>=i)return{data:"",lines:0};const p=-(t+e),a=-(t+1);let r=c(n,p,a);const u=r===""?0:r.split(`
3
3
  `).length,s=Math.min(e,u,i-t);return s<u&&s>0&&(r=r.split(`
4
4
  `).slice(-s).join(`
5
- `)),{data:r,lines:s}}export{y as capturePaneHistoryChunk,c as capturePaneLines,h as capturePaneTail,m as getPaneHistorySize,S as getSessionPaneTarget,_ as isAlternateScreen,g as toCrlf};
5
+ `)),{data:r,lines:s}}export{T as capturePaneHistoryChunk,c as capturePaneLines,P as capturePaneTail,x as getPaneHistorySize,h as getSessionPaneTarget,y as isAlternateScreen,_ as toCrlf};
@@ -1,2 +1,2 @@
1
- import*as w from"node-pty";import{captureAndStoreWindows as p}from"./session-windows.js";import{recordWindowVisit as T}from"./window-history.js";const h=["%session-window-changed","%window-add","%window-close","%unlinked-window-close","%window-renamed","%session-renamed"];function x(e){return h.some(r=>e.startsWith(r))}const y=75,C=e=>w.spawn("tmux",["-CC","attach-session","-t",e,"-f","ignore-size,no-output"],{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:process.env});function v(e){const r=p(e);if(!r.length)return null;const o=r.find(i=>i.active);return o&&T(e,o.index,o.name),{activeIndex:o?o.index:r[0].index,windows:r.map(i=>({index:i.index,name:i.name,active:i.active}))}}const c=new Map;function M(e,r,o={}){const i=o.spawn??C,a=o.requery??v,f=o.debounceMs??y;let l=c.get(e);if(!l){let n;try{n=i(e)}catch{return()=>{}}const t={pty:n,refCount:0,subscribers:new Set,buf:"",debounceTimer:null,schedule:()=>{}},m=()=>{t.debounceTimer=null;let u;try{u=a(e)}catch{u=null}if(u)for(const d of t.subscribers)try{d(u)}catch{}};t.schedule=()=>{t.debounceTimer&&clearTimeout(t.debounceTimer),t.debounceTimer=setTimeout(m,f)},n.onData(u=>{t.buf+=u;let d;for(;(d=t.buf.indexOf(`
2
- `))>=0;){const b=t.buf.slice(0,d).replace(/\r$/,"");t.buf=t.buf.slice(d+1),x(b)&&t.schedule()}}),n.onExit(()=>{t.debounceTimer&&clearTimeout(t.debounceTimer),c.delete(e)}),c.set(e,t),l=t}l.subscribers.add(r),l.refCount+=1,l.schedule();let s=!1;return()=>{if(s)return;s=!0;const n=c.get(e);if(n&&(n.subscribers.delete(r),n.refCount-=1,n.refCount<=0)){n.debounceTimer&&clearTimeout(n.debounceTimer);try{n.pty.kill()}catch{}c.delete(e)}}}function R(){for(const e of c.values()){e.debounceTimer&&clearTimeout(e.debounceTimer);try{e.pty.kill()}catch{}}c.clear()}function S(){return c.size}export{S as _controlClientCount,M as acquireControlClient,R as killAllControlClients,x as shouldRequery};
1
+ import*as w from"node-pty";import{tmuxEnv as p}from"./tmux-env.js";import{captureAndStoreWindows as T}from"./session-windows.js";import{recordWindowVisit as x}from"./window-history.js";const h=["%session-window-changed","%window-add","%window-close","%unlinked-window-close","%window-renamed","%session-renamed"];function y(e){return h.some(r=>e.startsWith(r))}const C=75,v=e=>w.spawn("tmux",["-CC","attach-session","-t",e,"-f","ignore-size,no-output"],{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:p()});function E(e){const r=T(e);if(!r.length)return null;const o=r.find(i=>i.active);return o&&x(e,o.index,o.name),{activeIndex:o?o.index:r[0].index,windows:r.map(i=>({index:i.index,name:i.name,active:i.active}))}}const c=new Map;function S(e,r,o={}){const i=o.spawn??v,a=o.requery??E,f=o.debounceMs??C;let l=c.get(e);if(!l){let n;try{n=i(e)}catch{return()=>{}}const t={pty:n,refCount:0,subscribers:new Set,buf:"",debounceTimer:null,schedule:()=>{}},m=()=>{t.debounceTimer=null;let u;try{u=a(e)}catch{u=null}if(u)for(const d of t.subscribers)try{d(u)}catch{}};t.schedule=()=>{t.debounceTimer&&clearTimeout(t.debounceTimer),t.debounceTimer=setTimeout(m,f)},n.onData(u=>{t.buf+=u;let d;for(;(d=t.buf.indexOf(`
2
+ `))>=0;){const b=t.buf.slice(0,d).replace(/\r$/,"");t.buf=t.buf.slice(d+1),y(b)&&t.schedule()}}),n.onExit(()=>{t.debounceTimer&&clearTimeout(t.debounceTimer),c.delete(e)}),c.set(e,t),l=t}l.subscribers.add(r),l.refCount+=1,l.schedule();let s=!1;return()=>{if(s)return;s=!0;const n=c.get(e);if(n&&(n.subscribers.delete(r),n.refCount-=1,n.refCount<=0)){n.debounceTimer&&clearTimeout(n.debounceTimer);try{n.pty.kill()}catch{}c.delete(e)}}}function g(){for(const e of c.values()){e.debounceTimer&&clearTimeout(e.debounceTimer);try{e.pty.kill()}catch{}}c.clear()}function k(){return c.size}export{k as _controlClientCount,S as acquireControlClient,g as killAllControlClients,y as shouldRequery};
@@ -0,0 +1 @@
1
+ function e(){const n={...process.env};return!n.LC_ALL&&!n.LANG&&(n.LC_ALL=process.platform==="darwin"?"en_US.UTF-8":"C.UTF-8"),n}export{e as tmuxEnv};
@@ -1,2 +1,2 @@
1
- import{execFileSync as u}from"node:child_process";const w=3e3,l="#{pane_id} #{pane_index} #{window_index} #{pane_pid} #{pane_current_command} #{pane_active} #{window_active}";function m(t,e){const[n,a,c,r,d,p,s]=e.split(" ");if(!n)return null;const i=parseInt(c,10),o=parseInt(a,10);return{paneId:n,paneIndex:Number.isFinite(o)?o:0,windowIndex:Number.isFinite(i)?i:0,pid:parseInt(r,10)||0,command:d??"",active:p==="1",windowActive:s==="1",target:`${t}:${i}.${o}`}}function x(t){try{return u("tmux",["list-panes","-s","-t",t,"-F",l],{encoding:"utf-8",timeout:w}).trim().split(`
2
- `).filter(Boolean).map(n=>m(t,n)).filter(n=>n!==null)}catch{return[]}}function _(t){return x(t).find(n=>n.active&&n.windowActive)??null}export{_ as getActivePaneInfo,x as listSessionPanes};
1
+ import{execFileSync as u}from"node:child_process";import{tmuxEnv as m}from"./tmux-env.js";const w=3e3,l="#{pane_id} #{pane_index} #{window_index} #{pane_pid} #{pane_current_command} #{pane_active} #{window_active}";function x(t,e){const[n,a,r,c,d,p,s]=e.split(" ");if(!n)return null;const i=parseInt(r,10),o=parseInt(a,10);return{paneId:n,paneIndex:Number.isFinite(o)?o:0,windowIndex:Number.isFinite(i)?i:0,pid:parseInt(c,10)||0,command:d??"",active:p==="1",windowActive:s==="1",target:`${t}:${i}.${o}`}}function I(t){try{return u("tmux",["list-panes","-s","-t",t,"-F",l],{encoding:"utf-8",timeout:w,env:m()}).trim().split(`
2
+ `).filter(Boolean).map(n=>x(t,n)).filter(n=>n!==null)}catch{return[]}}function _(t){return I(t).find(n=>n.active&&n.windowActive)??null}export{_ as getActivePaneInfo,I as listSessionPanes};
@@ -1,4 +1,4 @@
1
- import{execFileSync as r}from"node:child_process";class s extends Error{status;constructor(e,n){super(e),this.status=n,this.name="TmuxWindowsError"}}function u(t){try{return r("tmux",["list-windows","-t",t,"-F","#{window_index} #{window_name} #{window_active}"],{encoding:"utf-8",timeout:3e3}).trim().split(`
2
- `).filter(Boolean).map(n=>{const[o,i,w]=n.split(" ");return{index:parseInt(o,10),name:i,active:w==="1"}})}catch{return[]}}function d(t){try{return r("tmux",["list-windows","-t",t,"-F","#{window_index} #{window_name} #{window_active} #{pane_current_path}"],{encoding:"utf-8",timeout:3e3}).trim().split(`
3
- `).filter(Boolean).map(n=>{const[o,i,w,a]=n.split(" ");return{index:parseInt(o,10),name:i,active:w==="1",path:a??""}})}catch{return[]}}function f(t){if(!t)return!1;try{const e=r("git",["-C",t,"rev-parse","--git-dir","--git-common-dir"],{encoding:"utf-8",timeout:3e3,stdio:["ignore","pipe","ignore"]}),[n,o]=e.trim().split(`
4
- `);return!n||!o?!1:n!==o}catch{return!1}}function c(t){try{return r("tmux",["has-session","-t",t],{timeout:3e3}),!0}catch{return!1}}function x(t){if(!c(t))throw new s("session not found",404);try{r("tmux",["new-window","-t",t],{timeout:3e3})}catch(e){const n=e instanceof Error?e.message:"new-window failed";throw new s(n,500)}}function p(t,e){const n=["new-session","-d","-s",t];e&&n.push("-c",e);try{r("tmux",n,{timeout:5e3})}catch(o){const i=o instanceof Error?o.message:"new-session failed";throw new s(i,500)}}function l(t,e){r("tmux",["rename-session","-t",t,e],{timeout:5e3})}function h(t){r("tmux",["kill-session","-t",t],{timeout:5e3})}function g(t,e){if(!c(t))throw new s("session not found",404);if(!u(t).some(o=>o.index===e))throw new s("window not found",404);try{r("tmux",["select-window","-t",`${t}:${e}`],{timeout:3e3})}catch(o){const i=o instanceof Error?o.message:"select-window failed";throw new s(i,500)}}function y(t,e,n){if(!c(t))throw new s("session not found",404);if(!u(t).some(w=>w.index===e))throw new s("window not found",404);const i=n.trim();if(!i)throw new s("name is required",500);try{r("tmux",["rename-window","-t",`${t}:${e}`,i],{timeout:3e3})}catch(w){const a=w instanceof Error?w.message:"rename-window failed";throw new s(a,500)}}export{s as TmuxWindowsError,d as captureSessionWindowsWithPath,f as isGitWorktree,h as killSession,u as listSessionWindows,x as newSessionWindow,p as newTmuxSession,l as renameSession,y as renameSessionWindow,g as selectSessionWindow};
1
+ import{execFileSync as r}from"node:child_process";import{tmuxEnv as a}from"./tmux-env.js";class s extends Error{status;constructor(e,n){super(e),this.status=n,this.name="TmuxWindowsError"}}function m(t){try{return r("tmux",["list-windows","-t",t,"-F","#{window_index} #{window_name} #{window_active}"],{encoding:"utf-8",timeout:3e3}).trim().split(`
2
+ `).filter(Boolean).map(n=>{const[o,i,w]=n.split(" ");return{index:parseInt(o,10),name:i,active:w==="1"}})}catch{return[]}}function x(t){try{return r("tmux",["list-windows","-t",t,"-F","#{window_index} #{window_name} #{window_active} #{pane_current_path}"],{encoding:"utf-8",timeout:3e3}).trim().split(`
3
+ `).filter(Boolean).map(n=>{const[o,i,w,c]=n.split(" ");return{index:parseInt(o,10),name:i,active:w==="1",path:c??""}})}catch{return[]}}function p(t){if(!t)return!1;try{const e=r("git",["-C",t,"rev-parse","--git-dir","--git-common-dir"],{encoding:"utf-8",timeout:3e3,stdio:["ignore","pipe","ignore"]}),[n,o]=e.trim().split(`
4
+ `);return!n||!o?!1:n!==o}catch{return!1}}function u(t){try{return r("tmux",["has-session","-t",t],{timeout:3e3,env:a()}),!0}catch{return!1}}function l(t){if(!u(t))throw new s("session not found",404);try{r("tmux",["new-window","-t",t],{timeout:3e3,env:a()})}catch(e){const n=e instanceof Error?e.message:"new-window failed";throw new s(n,500)}}function h(t,e){const n=["new-session","-d","-s",t];e&&n.push("-c",e);try{r("tmux",n,{timeout:5e3,env:a()})}catch(o){const i=o instanceof Error?o.message:"new-session failed";throw new s(i,500)}}function g(t,e){r("tmux",["rename-session","-t",t,e],{timeout:5e3,env:a()})}function v(t){r("tmux",["kill-session","-t",t],{timeout:5e3,env:a()})}function y(t,e){if(!u(t))throw new s("session not found",404);if(!m(t).some(o=>o.index===e))throw new s("window not found",404);try{r("tmux",["select-window","-t",`${t}:${e}`],{timeout:3e3})}catch(o){const i=o instanceof Error?o.message:"select-window failed";throw new s(i,500)}}function E(t,e,n){if(!u(t))throw new s("session not found",404);if(!m(t).some(w=>w.index===e))throw new s("window not found",404);const i=n.trim();if(!i)throw new s("name is required",500);try{r("tmux",["rename-window","-t",`${t}:${e}`,i],{timeout:3e3})}catch(w){const c=w instanceof Error?w.message:"rename-window failed";throw new s(c,500)}}export{s as TmuxWindowsError,x as captureSessionWindowsWithPath,p as isGitWorktree,v as killSession,m as listSessionWindows,l as newSessionWindow,h as newTmuxSession,g as renameSession,E as renameSessionWindow,y as selectSessionWindow};
package/dist/sessions.js CHANGED
@@ -1,2 +1,2 @@
1
- import{execFileSync as o}from"node:child_process";function a(){try{return o("tmux",["list-sessions","-F","#{session_name} #{session_windows} #{session_attached}"],{encoding:"utf-8",timeout:3e3}).trim().split(`
2
- `).filter(Boolean).map(t=>{const[s,n,e]=t.split(" ");return{name:s,windows:parseInt(n,10),attached:e!=="0"}})}catch{return[]}}export{a as listSessions};
1
+ import{execFileSync as n}from"node:child_process";import{tmuxEnv as e}from"./lib/tmux-env.js";function a(){try{return n("tmux",["display-message","-p","#{socket_path}"],{encoding:"utf-8",timeout:3e3,env:e()}).trim()||null}catch{return null}}function m(){try{return n("tmux",["list-sessions","-F","#{session_name} #{session_windows} #{session_attached}"],{encoding:"utf-8",timeout:3e3,env:e()}).trim().split(`
2
+ `).filter(Boolean).map(t=>{const[s,i,r]=t.split(" ");return{name:s,windows:parseInt(i,10),attached:r!=="0"}})}catch{return[]}}export{m as listSessions,a as resolveTmuxSocketPath};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {