tmux-weblink 2.0.6 → 2.0.7
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 +1 -1
- package/dist/index.js +3 -3
- package/dist/lib/agent-client.js +1 -1
- package/dist/lib/attach-terminal.js +4 -4
- package/dist/sessions.js +2 -2
- package/package.json +1 -1
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
|
|
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
|
|
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()}
|
|
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
|
|
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)}}
|
package/dist/lib/agent-client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocket as
|
|
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:
|
|
2
|
-
`})}
|
|
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:C,syncIdleMs:k,syncMaxMs:g}=e.terminalBufferConfig,A=e.spawn??(o=>{const c=e.tmuxSocketPath?["-S",e.tmuxSocketPath]:[];return c.push("attach-session","-t",o),O.spawn("tmux",c,{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:process.env})});let a=null,y=n,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=e.getSessionPaneTarget(n)}catch{y=n}try{const o=e.capturePaneTail(y,M);r.send({type:"snapshot",data:e.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(n)}catch(t){const l=t?.message??String(t);r.send({type:"data",data:`\r
|
|
3
3
|
\x1B[31mFailed to attach to tmux session "${n}": ${l}\x1B[0m\r
|
|
4
|
-
`}),e.onSpawnError?.(n,l);return}let
|
|
4
|
+
`}),e.onSpawnError?.(n,l);return}let o=-1,c="";h=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!==o||x!==c;o=t,c=x}),w=Date.now(),p=setTimeout(P,g),e.onAttached?.(n),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();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
|
|
5
5
|
\x1B[2m--- tmux exited (code ${t}) ---\x1B[0m\r
|
|
6
|
-
`}),
|
|
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(e.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}=e.capturePaneHistoryChunk(y,t,C);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(i),{onMessage:b,dispose:i}}export{B as _liveAttachmentCount,H as attachTerminal,_ as killAllAttachedPtys};
|
package/dist/sessions.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{execFileSync as
|
|
2
|
-
`).filter(Boolean).map(t=>{const[s,
|
|
1
|
+
import{execFileSync as e}from"node:child_process";function r(){try{return e("tmux",["display-message","-p","#{socket_path}"],{encoding:"utf-8",timeout:3e3}).trim()||null}catch{return null}}function c(){try{return e("tmux",["list-sessions","-F","#{session_name} #{session_windows} #{session_attached}"],{encoding:"utf-8",timeout:3e3}).trim().split(`
|
|
2
|
+
`).filter(Boolean).map(t=>{const[s,o,i]=t.split(" ");return{name:s,windows:parseInt(o,10),attached:i!=="0"}})}catch{return[]}}export{c as listSessions,r as resolveTmuxSocketPath};
|