tmux-weblink 2.2.9 → 2.2.11

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.
@@ -1 +1 @@
1
- const i=1,f=2;function u(t,e,n){const s=typeof n=="string"?Buffer.from(n,"utf-8"):Buffer.isBuffer(n)?n:Buffer.from(n),r=Buffer.allocUnsafe(5+s.length);return r[0]=t,r.writeUInt32BE(e>>>0,1),s.copy(r,5),r}function c(t){return t.length<5?null:{kind:t[0],id:t.readUInt32BE(1),payload:t.subarray(5)}}function l(t){if(typeof t!="object"||t===null)return!1;const e=t.type;return typeof e=="string"&&o.has(e)}function a(t){if(typeof t!="object"||t===null)return!1;const e=t.type;return typeof e=="string"&&_.has(e)}const o=new Set(["hello","sessions","http_resp","http_body_end","attach_ok","attach_err","ws_to_hub","ws_close","activity","rebuild_session_result","ping"]),_=new Set(["hello_ok","hello_err","sessions_req","http_req","http_body_end","attach","ws_to_agent","rebuild_session","detach","pong"]);export{f as BIN_KIND_HTTP_BODY,i as BIN_KIND_TERM,c as decodeBinaryFrame,u as encodeBinaryFrame,l as isAgentToHubMessage,a as isHubToAgentMessage};
1
+ const i=1,f=2;function u(t,e,n){const s=typeof n=="string"?Buffer.from(n,"utf-8"):Buffer.isBuffer(n)?n:Buffer.from(n),r=Buffer.allocUnsafe(5+s.length);return r[0]=t,r.writeUInt32BE(e>>>0,1),s.copy(r,5),r}function c(t){return t.length<5?null:{kind:t[0],id:t.readUInt32BE(1),payload:t.subarray(5)}}function l(t){if(typeof t!="object"||t===null)return!1;const e=t.type;return typeof e=="string"&&o.has(e)}function p(t){if(typeof t!="object"||t===null)return!1;const e=t.type;return typeof e=="string"&&_.has(e)}const o=new Set(["hello","sessions","http_resp","http_body_end","attach_ok","attach_err","ws_to_hub","ws_close","activity","rebuild_session_result","ping"]),_=new Set(["hello_ok","hello_err","sessions_req","http_req","http_body_end","attach","ws_to_agent","rebuild_session","ping","detach","pong"]);export{f as BIN_KIND_HTTP_BODY,i as BIN_KIND_TERM,c as decodeBinaryFrame,u as encodeBinaryFrame,l as isAgentToHubMessage,p as isHubToAgentMessage};
@@ -1 +1 @@
1
- import{WebSocket as w}from"ws";import{attachTerminal as F}from"./attach-terminal.js";import{encodeBinaryFrame as G,decodeBinaryFrame as X,BIN_KIND_HTTP_BODY as O}from"./agent-channel.js";import{ActivityProbe as Y}from"./activity-probe.js";import{sessionWorkingPath as z}from"./sessions-sidebar.js";import{newTmuxSession as Q}from"./tmux-windows.js";const V=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),Z=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),ee=new Set(["content-type","content-disposition","location","cache-control","etag"]);function ce(o){const B=o.log??(e=>console.log(`[agent] ${e}`)),h=e=>console.error(`[agent] ${e}`);let s=null,I=1e3,v=null,T=!1;const u=new Map,y=new Map,p=new Map,E=new Set;function H(e){return E.has(e)?Promise.resolve():new Promise(t=>{p.set(e,t),setTimeout(()=>{p.delete(e)&&t()},3e4)})}let _=null,m=null,S=null,g=null,M=new Map,C="",P=null;const b={state:"idle",agentId:null,hub:o.hub,name:o.name};function f(e,t){t!==void 0&&(b.lastError=t),b.state=e,b.agentId=P,o.onStatus?.({...b})}function c(e){s&&s.readyState===w.OPEN&&s.send(JSON.stringify(e))}function k(){let e;try{e=o.listSessions()}catch{return}const t=e.map(r=>({...r,path:z(r.name)})),n=JSON.stringify(t);n!==C&&(C=n,c({type:"sessions",sessions:t}))}function R(){_||(_=setInterval(k,Z)),m||(m=setInterval(()=>c({type:"ping",ts:Date.now()}),V)),S||(g=new Y,S=setInterval(()=>void q(),g.scanMs))}function L(){_&&(clearInterval(_),_=null),m&&(clearInterval(m),m=null),S&&(clearInterval(S),S=null),g=null}async function q(){if(!(!s||s.readyState!==w.OPEN||!g))try{const e=await g.scan(),t=[];for(const[n,r]of e)M.get(n)!==r&&t.push({session:n,state:r});M=e,t.length&&c({type:"activity",activities:t})}catch{}}async function U(e){let t;if(e.hasBody){await H(e.id),E.delete(e.id);const a=y.get(e.id);y.delete(e.id),t=a&&a.length?Buffer.concat(a):Buffer.alloc(0)}const n=new URL(e.path,"http://agent.invalid");let r;try{const a={method:e.method,headers:e.headers};t&&t.length&&(a.body=t),r=await Promise.race([o.app.request(new Request(n,a)),new Promise((te,K)=>setTimeout(()=>K(new Error("app.request timed out")),3e4))])}catch(a){h(`tunnel request failed: ${e.method} ${e.path} ${a.message}`),c({type:"http_resp",id:e.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const d={};r.headers.forEach((a,l)=>{ee.has(l.toLowerCase())&&(d[l]=a)});const i=Buffer.from(await r.arrayBuffer());if(c({type:"http_resp",id:e.id,status:r.status,headers:d,hasBody:i.length>0}),i.length>0){for(let l=0;l<i.length;l+=65536){if(!s||s.readyState!==w.OPEN)return;s.send(G(O,e.id,i.subarray(l,l+65536)))}c({type:"http_body_end",id:e.id})}}function x(e,t){let n=!1,r="";try{Q(e,t),n=!0}catch(d){r=d instanceof Error?d.message:"rebuild failed"}s&&s.readyState===w.OPEN&&s.send(JSON.stringify({type:"rebuild_session_result",session:e,ok:n,message:r})),k()}function J(e){const t=e.session;let n=!1;try{n=o.listSessions().some(i=>i.name===t)}catch{n=!1}if(!n){c({type:"attach_err",connId:e.connId,session:t,message:"no such session"});return}let r=null;const d=F(t,{send:i=>c({type:"ws_to_hub",connId:e.connId,msg:i}),onMessage:i=>{r=i},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,initialCols:e.cols,initialRows:e.rows,tmuxSocketPath:o.tmuxSocketPath,onPtyExit:(i,a)=>{c({type:"ws_close",connId:e.connId,code:1e3,reason:`tmux exited (${a})`})},onSpawnError:(i,a)=>{h(`attach failed for session "${e.session}": ${a}`),c({type:"attach_err",connId:e.connId,session:t,message:a})},onAttached:i=>{c({type:"attach_ok",connId:e.connId,session:t})}});u.set(e.connId,{onMessage:i=>r?.(i),dispose:()=>{u.delete(e.connId),d.dispose()}})}function D(e){let t;try{t=JSON.parse(e)}catch{return}switch(t.type){case"hello_ok":P=t.agentId,I=1e3,B(`connected to hub as "${P}"`),f("connected"),k(),R();break;case"hello_err":if(h(`hub rejected us: ${t.reason}`),f("error",t.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":k();break;case"http_req":U(t);break;case"http_body_end":{E.add(t.id);const n=p.get(t.id);n&&(p.delete(t.id),n());break}case"attach":J(t);break;case"rebuild_session":x(t.session,t.dir);break;case"ws_to_agent":{const n=u.get(t.connId);if(n)try{n.onMessage(JSON.stringify(t.msg))}catch{}break}case"detach":{const n=u.get(t.connId);n&&n.dispose();break}case"pong":break;default:break}}function W(e){const t=X(e);if(t&&t.kind===O){const n=y.get(t.id)??[];n.push(t.payload),y.set(t.id,n)}}function N(){L();for(const e of u.values())try{e.dispose()}catch{}u.clear(),y.clear(),p.clear(),E.clear(),C=""}function A(){if(s){try{s.removeAllListeners()}catch{}s=null}if(N(),T)return;f("connecting");const e=I+Math.random()*500;B(`reconnecting in ${Math.round(e)}ms\u2026`),v=setTimeout($,e),I=Math.min(I*2,6e4)}function $(){if(T||s)return;const e=o.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let t;try{t=new w(e)}catch(n){h(`bad hub URL: ${n.message} (${e})`),f("error",`bad hub URL: ${n.message}`);return}s=t,f("connecting"),t.on("open",()=>{B(`connecting to ${e}\u2026`),c({type:"hello",token:o.token,name:o.name,version:o.version})}),t.on("message",(n,r)=>{r?W(Array.isArray(n)?Buffer.concat(n):Buffer.isBuffer(n)?n:Buffer.from(n)):D(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),t.on("close",()=>{s===t&&A()}),t.on("error",n=>{h(`connection error: ${n.message}`),s===t&&A()})}function j(){if(!T){if(T=!0,v&&clearTimeout(v),N(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}P=null,f("idle")}}return $(),{stop:j,status:()=>({...b})}}export{ce as startAgentClient};
1
+ import{WebSocket as h}from"ws";import{attachTerminal as X}from"./attach-terminal.js";import{encodeBinaryFrame as Y,decodeBinaryFrame as z,BIN_KIND_HTTP_BODY as R}from"./agent-channel.js";import{ActivityProbe as Q}from"./activity-probe.js";import{sessionWorkingPath as V}from"./sessions-sidebar.js";import{newTmuxSession as Z}from"./tmux-windows.js";const ee=parseInt(process.env.TMUX_WEB_AGENT_HEARTBEAT_MS||"15000",10),te=parseInt(process.env.TMUX_WEB_AGENT_SESSIONS_POLL_MS||"10000",10),ne=parseInt(process.env.TMUX_WEB_AGENT_PONG_TIMEOUT_MS||"45000",10),H=5,se=6e4,oe=new Set(["content-type","content-disposition","location","cache-control","etag"]);function fe(o){const E=o.log??(t=>console.log(`[agent] ${t}`)),p=t=>console.error(`[agent] ${t}`);let s=null,I=0,y=0,B=null,P=!1;const d=new Map,_=new Map,S=new Map,M=new Set;function U(t){return M.has(t)?Promise.resolve():new Promise(e=>{S.set(t,e),setTimeout(()=>{S.delete(t)&&e()},3e4)})}let m=null,g=null,b=null,w=null,v=new Map,C="",N=null;const T={state:"idle",agentId:null,hub:o.hub,name:o.name};function f(t,e){e!==void 0&&(T.lastError=e),T.state=t,T.agentId=N,o.onStatus?.({...T})}function c(t){s&&s.readyState===h.OPEN&&s.send(JSON.stringify(t))}function k(){let t;try{t=o.listSessions()}catch{return}const e=t.map(r=>({...r,path:V(r.name)})),n=JSON.stringify(e);n!==C&&(C=n,c({type:"sessions",sessions:e}))}function L(){m||(m=setInterval(k,te)),g||(g=setInterval(()=>{if(s&&s.readyState===h.OPEN&&y>0&&Date.now()-y>ne){E(`hub heartbeat timeout (${Date.now()-y}ms without pong), reconnecting`);try{s.close(1001,"hub heartbeat timeout")}catch{}return}c({type:"ping",ts:Date.now()})},ee)),b||(w=new Q,b=setInterval(()=>void D(),w.scanMs))}function q(){m&&(clearInterval(m),m=null),g&&(clearInterval(g),g=null),b&&(clearInterval(b),b=null),w=null}async function D(){if(!(!s||s.readyState!==h.OPEN||!w))try{const t=await w.scan(),e=[];for(const[n,r]of t)v.get(n)!==r&&e.push({session:n,state:r});v=t,e.length&&c({type:"activity",activities:e})}catch{}}async function x(t){let e;if(t.hasBody){await U(t.id),M.delete(t.id);const a=_.get(t.id);_.delete(t.id),e=a&&a.length?Buffer.concat(a):Buffer.alloc(0)}const n=new URL(t.path,"http://agent.invalid");let r;try{const a={method:t.method,headers:t.headers};e&&e.length&&(a.body=e),r=await Promise.race([o.app.request(new Request(n,a)),new Promise((re,K)=>setTimeout(()=>K(new Error("app.request timed out")),3e4))])}catch(a){p(`tunnel request failed: ${t.method} ${t.path} ${a.message}`),c({type:"http_resp",id:t.id,status:500,headers:{"content-type":"application/json"},hasBody:!1});return}const u={};r.headers.forEach((a,l)=>{oe.has(l.toLowerCase())&&(u[l]=a)});const i=Buffer.from(await r.arrayBuffer());if(c({type:"http_resp",id:t.id,status:r.status,headers:u,hasBody:i.length>0}),i.length>0){for(let l=0;l<i.length;l+=65536){if(!s||s.readyState!==h.OPEN)return;s.send(Y(R,t.id,i.subarray(l,l+65536)))}c({type:"http_body_end",id:t.id})}}function G(t,e){let n=!1,r="";try{Z(t,e),n=!0}catch(u){r=u instanceof Error?u.message:"rebuild failed"}s&&s.readyState===h.OPEN&&s.send(JSON.stringify({type:"rebuild_session_result",session:t,ok:n,message:r})),k()}function J(t){const e=t.session;let n=!1;try{n=o.listSessions().some(i=>i.name===e)}catch{n=!1}if(!n){c({type:"attach_err",connId:t.connId,session:e,message:"no such session"});return}let r=null;const u=X(e,{send:i=>c({type:"ws_to_hub",connId:t.connId,msg:i}),onMessage:i=>{r=i},onClose:()=>{}},{terminalBufferConfig:o.terminalBufferConfig,getSessionPaneTarget:o.getSessionPaneTarget,capturePaneTail:o.capturePaneTail,capturePaneHistoryChunk:o.capturePaneHistoryChunk,toCrlf:o.toCrlf,handleClientMessage:o.handleClientMessage,acquireControlClient:o.acquireControlClient,initialCols:t.cols,initialRows:t.rows,tmuxSocketPath:o.tmuxSocketPath,onPtyExit:(i,a)=>{c({type:"ws_close",connId:t.connId,code:1e3,reason:`tmux exited (${a})`})},onSpawnError:(i,a)=>{p(`attach failed for session "${t.session}": ${a}`),c({type:"attach_err",connId:t.connId,session:e,message:a})},onAttached:i=>{c({type:"attach_ok",connId:t.connId,session:e})}});d.set(t.connId,{onMessage:i=>r?.(i),dispose:()=>{d.delete(t.connId),u.dispose()}})}function W(t){let e;try{e=JSON.parse(t)}catch{return}switch(e.type){case"hello_ok":N=e.agentId,I=0,y=Date.now(),E(`connected to hub as "${N}"`),f("connected"),k(),L();break;case"hello_err":if(p(`hub rejected us: ${e.reason}`),f("error",e.reason),s)try{s.close(4003,"hello rejected")}catch{}break;case"sessions_req":k();break;case"http_req":x(e);break;case"http_body_end":{M.add(e.id);const n=S.get(e.id);n&&(S.delete(e.id),n());break}case"attach":J(e);break;case"rebuild_session":G(e.session,e.dir);break;case"ws_to_agent":{const n=d.get(e.connId);if(n)try{n.onMessage(JSON.stringify(e.msg))}catch{}break}case"detach":{const n=d.get(e.connId);n&&n.dispose();break}case"pong":y=Date.now();break;case"ping":c({type:"pong",ts:e.ts});break;default:break}}function j(t){const e=z(t);if(e&&e.kind===R){const n=_.get(e.id)??[];n.push(e.payload),_.set(e.id,n)}}function A(){q();for(const t of d.values())try{t.dispose()}catch{}d.clear(),_.clear(),S.clear(),M.clear(),C=""}function O(){if(s){try{s.removeAllListeners()}catch{}s=null}if(A(),P)return;f("connecting"),I+=1;const e=(I<=H?1e3:Math.min(1e3*Math.pow(2,I-H),se))+Math.random()*500;E(`reconnecting in ${Math.round(e)}ms\u2026`),B=setTimeout($,e)}function $(){if(P||s)return;const t=o.hub.replace(/\/+$/,"").replace(/^http:/,"ws:").replace(/^https:/,"wss:")+"/agent/ws";let e;try{e=new h(t)}catch(n){p(`bad hub URL: ${n.message} (${t})`),f("error",`bad hub URL: ${n.message}`);return}s=e,f("connecting"),e.on("open",()=>{E(`connecting to ${t}\u2026`),c({type:"hello",token:o.token,name:o.name,version:o.version})}),e.on("message",(n,r)=>{r?j(Array.isArray(n)?Buffer.concat(n):Buffer.isBuffer(n)?n:Buffer.from(n)):W(typeof n=="string"?n:Buffer.from(n).toString("utf-8"))}),e.on("close",()=>{s===e&&O()}),e.on("error",n=>{p(`connection error: ${n.message}`),s===e&&O()})}function F(){if(!P){if(P=!0,B&&clearTimeout(B),A(),s){try{s.removeAllListeners(),s.close(1e3,"shutdown")}catch{}s=null}N=null,f("idle")}}return $(),{stop:F,status:()=>({...T})}}export{fe as startAgentClient};
@@ -1,3 +1,3 @@
1
- import{WebSocket as r}from"ws";import{decodeBinaryFrame as u,encodeBinaryFrame as f,BIN_KIND_TERM as g,BIN_KIND_HTTP_BODY as h}from"./agent-channel.js";const S=parseInt(process.env.TMUX_WEB_MAX_AGENT_CONNS||"64",10),_=parseInt(process.env.TMUX_WEB_TUNNEL_TIMEOUT_MS||"30000",10);let E=1,w=1;class k{ws;agentId;pending=new Map;relays=new Map;onSessions;onActivity;onRebuildResult;onClose;disposed=!1;constructor(t,e,s){this.ws=t,this.agentId=e,this.onSessions=s.onSessions,this.onActivity=s.onActivity,this.onRebuildResult=s.onRebuildResult,this.onClose=s.onClose}send(t){return this.disposed||this.ws.readyState!==r.OPEN?!1:(this.ws.send(JSON.stringify(t)),!0)}attach(t,e,s){return this.send({type:"attach",connId:t,session:e,cols:s?.cols,rows:s?.rows})}detach(t){this.send({type:"detach",connId:t})}sendToAgent(t,e){this.send({type:"ws_to_agent",connId:t,msg:e})}sendSessionsReq(){this.send({type:"sessions_req"})}rebuildSession(t,e){return this.send({type:"rebuild_session",session:t,dir:e})}async httpRequest(t,e,s,n){const i=w++,o=!!n,c=new Promise((d,a)=>{const l=setTimeout(()=>{this.pending.delete(i),a(new Error("tunnel request timed out"))},_);this.pending.set(i,{id:i,resolve:d,reject:a,status:null,headers:null,body:[],hasBody:o,timer:l})});if(!this.send({type:"http_req",id:i,method:t,path:e,headers:s,hasBody:o}))return this.pending.delete(i),Promise.reject(new Error("agent offline"));if(n){const d=n.getReader();(async()=>{for(;;){const{done:l,value:p}=await d.read();if(l)break;const y=Buffer.from(p);this.ws.send(f(h,i,y))}this.send({type:"http_body_end",id:i})})().catch(()=>{this.send({type:"http_body_end",id:i})})}return c}registerRelay(t,e,s){this.relays.set(t,{connId:t,session:e,ws:s,closed:!1})}relayClosed(t){const e=this.relays.get(t);e&&!e.closed&&(e.closed=!0,this.relays.delete(t),this.detach(t))}get relayCount(){return this.relays.size}handleData(t){const e=Buffer.isBuffer(t)?t:Buffer.from(t),s=u(e);if(s){if(s.kind===g){const n=this.relays.get(s.id);n&&!n.closed&&n.ws.readyState===r.OPEN&&n.ws.send(JSON.stringify({type:"data",data:s.payload.toString("utf-8")}));return}if(s.kind===h){const n=this.pending.get(s.id);n&&n.body.push(s.payload)}}}handleMessage(t){let e;try{e=JSON.parse(t)}catch{return}switch(e.type){case"sessions":this.onSessions?.(e.sessions);break;case"activity":this.onActivity?.(e.activities);break;case"rebuild_session_result":this.onRebuildResult?.({session:e.session,ok:e.ok,message:e.message});break;case"http_resp":{const s=this.pending.get(e.id);if(!s)break;s.status=e.status,s.headers=e.headers,e.hasBody||this.finalizeHttp(s.id);break}case"http_body_end":this.finalizeHttp(e.id);break;case"attach_ok":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify({type:"auth.ok",setupMode:!1}));break}case"attach_err":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify({type:"attach_failed",message:`Agent "${this.agentId}": cannot attach to session "${e.session}": ${e.message}`}));break}case"ws_to_hub":{const s=this.relays.get(e.connId);s&&!s.closed&&s.ws.readyState===r.OPEN&&s.ws.send(JSON.stringify(e.msg));break}case"ws_close":{const s=this.relays.get(e.connId);if(s&&!s.closed){s.closed=!0;try{s.ws.close(e.code??1e3,e.reason??"agent closed")}catch{}this.relays.delete(e.connId)}break}case"ping":this.send({type:"pong",ts:e.ts});break;default:break}}finalizeHttp(t){const e=this.pending.get(t);if(!e)return;this.pending.delete(t),clearTimeout(e.timer);const s=e.status??500,n=e.headers??{},i=Buffer.concat(e.body),o={};for(const[c,d]of Object.entries(n)){const a=c.toLowerCase();(a==="content-type"||a==="content-disposition"||a==="cache-control"||a==="etag"||a==="location")&&(o[c]=d)}e.resolve(new Response(i,{status:s,headers:o}))}failAll(t){for(const e of this.pending.values())clearTimeout(e.timer),e.reject(new Error(t));this.pending.clear();for(const e of this.relays.values()){if(!e.closed&&e.ws.readyState===r.OPEN){e.ws.send(JSON.stringify({type:"data",data:`\r
2
- \x1B[31m--- agent disconnected (${t}) ---\x1B[0m\r
3
- `}));try{e.ws.close(1011,"agent disconnected")}catch{}}e.closed=!0}this.relays.clear()}dispose(){this.disposed||(this.disposed=!0,this.failAll("agent disconnected"),this.onClose?.(this))}}export{k as AgentChannel,S as MAX_RELAY_CONNS_PER_AGENT,_ as TUNNEL_REQUEST_TIMEOUT_MS};
1
+ import{WebSocket as r}from"ws";import{decodeBinaryFrame as u,encodeBinaryFrame as f,BIN_KIND_TERM as g,BIN_KIND_HTTP_BODY as h}from"./agent-channel.js";const _=parseInt(process.env.TMUX_WEB_HUB_HEARTBEAT_MS||"15000",10),w=parseInt(process.env.TMUX_WEB_HUB_PONG_TIMEOUT_MS||"90000",10),N=parseInt(process.env.TMUX_WEB_MAX_AGENT_CONNS||"64",10),b=parseInt(process.env.TMUX_WEB_TUNNEL_TIMEOUT_MS||"30000",10);let O=1,m=1;class B{ws;agentId;pending=new Map;relays=new Map;onSessions;onActivity;onRebuildResult;onClose;lastPongAt=Date.now();heartbeatTimer=null;disposed=!1;constructor(s,e,t){this.ws=s,this.agentId=e,this.onSessions=t.onSessions,this.onActivity=t.onActivity,this.onRebuildResult=t.onRebuildResult,this.onClose=t.onClose,this.heartbeatTimer=setInterval(()=>{if(!(this.disposed||this.ws.readyState!==r.OPEN)){if(Date.now()-this.lastPongAt>w){try{this.ws.close(1001,"agent heartbeat timeout")}catch{}return}this.send({type:"ping",ts:Date.now()})}},_)}send(s){return this.disposed||this.ws.readyState!==r.OPEN?!1:(this.ws.send(JSON.stringify(s)),!0)}attach(s,e,t){return this.send({type:"attach",connId:s,session:e,cols:t?.cols,rows:t?.rows})}detach(s){this.send({type:"detach",connId:s})}sendToAgent(s,e){this.send({type:"ws_to_agent",connId:s,msg:e})}sendSessionsReq(){this.send({type:"sessions_req"})}rebuildSession(s,e){return this.send({type:"rebuild_session",session:s,dir:e})}async httpRequest(s,e,t,n){const i=m++,o=!!n,c=new Promise((d,a)=>{const l=setTimeout(()=>{this.pending.delete(i),a(new Error("tunnel request timed out"))},b);this.pending.set(i,{id:i,resolve:d,reject:a,status:null,headers:null,body:[],hasBody:o,timer:l})});if(!this.send({type:"http_req",id:i,method:s,path:e,headers:t,hasBody:o}))return this.pending.delete(i),Promise.reject(new Error("agent offline"));if(n){const d=n.getReader();(async()=>{for(;;){const{done:l,value:p}=await d.read();if(l)break;const y=Buffer.from(p);this.ws.send(f(h,i,y))}this.send({type:"http_body_end",id:i})})().catch(()=>{this.send({type:"http_body_end",id:i})})}return c}registerRelay(s,e,t){this.relays.set(s,{connId:s,session:e,ws:t,closed:!1})}relayClosed(s){const e=this.relays.get(s);e&&!e.closed&&(e.closed=!0,this.relays.delete(s),this.detach(s))}get relayCount(){return this.relays.size}handleData(s){const e=Buffer.isBuffer(s)?s:Buffer.from(s),t=u(e);if(t){if(t.kind===g){const n=this.relays.get(t.id);n&&!n.closed&&n.ws.readyState===r.OPEN&&n.ws.send(JSON.stringify({type:"data",data:t.payload.toString("utf-8")}));return}if(t.kind===h){const n=this.pending.get(t.id);n&&n.body.push(t.payload)}}}handleMessage(s){let e;try{e=JSON.parse(s)}catch{return}switch(e.type){case"sessions":this.onSessions?.(e.sessions);break;case"activity":this.onActivity?.(e.activities);break;case"rebuild_session_result":this.onRebuildResult?.({session:e.session,ok:e.ok,message:e.message});break;case"http_resp":{const t=this.pending.get(e.id);if(!t)break;t.status=e.status,t.headers=e.headers,e.hasBody||this.finalizeHttp(t.id);break}case"http_body_end":this.finalizeHttp(e.id);break;case"attach_ok":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify({type:"auth.ok",setupMode:!1}));break}case"attach_err":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify({type:"attach_failed",message:`Agent "${this.agentId}": cannot attach to session "${e.session}": ${e.message}`}));break}case"ws_to_hub":{const t=this.relays.get(e.connId);t&&!t.closed&&t.ws.readyState===r.OPEN&&t.ws.send(JSON.stringify(e.msg));break}case"ws_close":{const t=this.relays.get(e.connId);if(t&&!t.closed){t.closed=!0;try{t.ws.close(e.code??1e3,e.reason??"agent closed")}catch{}this.relays.delete(e.connId)}break}case"ping":this.lastPongAt=Date.now(),this.send({type:"pong",ts:e.ts});break;default:break}}finalizeHttp(s){const e=this.pending.get(s);if(!e)return;this.pending.delete(s),clearTimeout(e.timer);const t=e.status??500,n=e.headers??{},i=Buffer.concat(e.body),o={};for(const[c,d]of Object.entries(n)){const a=c.toLowerCase();(a==="content-type"||a==="content-disposition"||a==="cache-control"||a==="etag"||a==="location")&&(o[c]=d)}e.resolve(new Response(i,{status:t,headers:o}))}failAll(s){for(const e of this.pending.values())clearTimeout(e.timer),e.reject(new Error(s));this.pending.clear();for(const e of this.relays.values()){if(!e.closed&&e.ws.readyState===r.OPEN){e.ws.send(JSON.stringify({type:"data",data:`\r
2
+ \x1B[31m--- agent disconnected (${s}) ---\x1B[0m\r
3
+ `}));try{e.ws.close(1011,"agent disconnected")}catch{}}e.closed=!0}this.relays.clear()}dispose(){this.disposed||(this.disposed=!0,this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null),this.failAll("agent disconnected"),this.onClose?.(this))}}export{B as AgentChannel,N as MAX_RELAY_CONNS_PER_AGENT,b as TUNNEL_REQUEST_TIMEOUT_MS};
@@ -1 +1 @@
1
- import e from"node:path";import{homedir as n}from"node:os";function u(t){if(!t)return!1;const o=t.trim().toLowerCase();return o==="1"||o==="true"||o==="yes"}function i(){const t=process.env.TMUX_WEB_MODE?.trim().toLowerCase();return t==="development"||t==="dev"||u(process.env.TMUX_WEB_DEV)?!0:process.env.NODE_ENV==="development"||process.env.npm_lifecycle_event==="dev"}function s(){return i()?e.join(n(),".dev",".tmux-web"):e.join(n(),".tmux-web")}function r(){return i()?e.join(n(),".dev",".config"):e.join(n(),".config")}function f(){return e.join(r(),"tmux-web","settings.json")}function m(){return e.join(r(),"tmux-web","theme.json")}function a(){return e.join(r(),"tmux-web","federation.json")}function d(){return e.join(s(),"uploads")}export{r as getConfigRoot,s as getDataRoot,a as getFederationPath,f as getSettingsPath,m as getThemePath,d as getUploadsRoot};
1
+ import e from"node:path";import{homedir as n}from"node:os";function s(t){if(!t)return!1;const o=t.trim().toLowerCase();return o==="1"||o==="true"||o==="yes"}function i(){const t=process.env.TMUX_WEB_MODE?.trim().toLowerCase();return t==="development"||t==="dev"||s(process.env.TMUX_WEB_DEV)?!0:process.env.NODE_ENV==="development"||process.env.npm_lifecycle_event==="dev"}function u(){return process.env.TMUX_WEB_DATA_DIR?process.env.TMUX_WEB_DATA_DIR:i()?e.join(n(),".dev",".tmux-web"):e.join(n(),".tmux-web")}function r(){return i()?e.join(n(),".dev",".config"):e.join(n(),".config")}function f(){return e.join(r(),"tmux-web","settings.json")}function m(){return e.join(r(),"tmux-web","theme.json")}function a(){return e.join(r(),"tmux-web","federation.json")}function d(){return e.join(u(),"uploads")}export{r as getConfigRoot,u as getDataRoot,a as getFederationPath,f as getSettingsPath,m as getThemePath,d as getUploadsRoot};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-weblink",
3
- "version": "2.2.9",
3
+ "version": "2.2.11",
4
4
  "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
5
  "type": "module",
6
6
  "bin": {