tmux-weblink 2.0.13 → 2.0.14

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,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)))}
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 k}from"./lib/rateLimiter.js";import{db as n}from"./lib/db.js";import{loadExtensions as A,spawnExtensionBackend as M}from"./lib/ext-loader.js";import{SchedulerService as S}from"./lib/scheduler.js";import{handleClientMessage as b}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||"",i=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]){i=e[++o];continue}t.startsWith("--hub=")&&(r=t.slice(6)),t.startsWith("--token=")&&(s=t.slice(8)),t.startsWith("--name=")&&(i=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:i.slice(0,64)}}const{hub:q,token:I,name:K}=X(process.argv.slice(2)),m=D(),j=_(),v=new w,F=new k;await n.read(),n.data.sessionAccess??=[],n.data.pinnedViews??=[],n.data.watchedPanes??=[],n.data.triggeredTasks??=[],n.data.quickCommands??=[];const a=await N(),L=await y(),f=z(a.scheduleHistoryDays),p=T.join(process.cwd(),"extensions"),u=await A(p);for(const e of u)e.start&&M(e.dir,e);const c=new S({db:n,historyRetentionMs:f*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await c.restoreFromDb();const $=B({mode:"agent",securityConfig:j,tokenStore:v,rateLimiter:F,scheduler:c,settings:a,terminalRenderer:a.terminalRenderer==="ghostty"?"ghostty":"xterm",scheduleHistoryDays:f,extsDir:p,extensions:u,terminalBufferConfig:m,getAgentId:()=>null,state:{activeTheme:L,settingUpPassword:!1}}),V=H({hub:q,token:I,name:K,version:R,app:$,listSessions:E,terminalBufferConfig:m,getSessionPaneTarget:U,capturePaneTail:W,capturePaneHistoryChunk:G,toCrlf:P,handleClientMessage:b,acquireControlClient:C,tmuxSocketPath:g()??void 0});function d(){c.cleanup(),V.stop(),process.exit(0)}process.on("SIGINT",d),process.on("SIGTERM",d);function z(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 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
2
+ import{chmodSync as pe,existsSync as z,readdirSync as he,statSync as ge}from"node:fs";import{createRequire as V}from"node:module";import{hostname as $}from"node:os";const ye=V(import.meta.url),{version:ke}=ye("../package.json");import I from"node:path";import{fileURLToPath as _e}from"node:url";import{serve as Te}from"@hono/node-server";import{WebSocketServer as we,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 he(n)){const t=I.join(n,r,"spawn-helper");z(t)&&!(ge(t).mode&73)&&pe(t,493)}}catch{}import{loadSecurityConfig as Se}from"./lib/security-config.js";import{TokenStore as xe,hashPassword as Ae,verifyPassword as ve,validatePassword as Ce}from"./lib/auth.js";import{RateLimiter as be}from"./lib/rateLimiter.js";import{audit as l}from"./lib/auditLog.js";import{db as b}from"./lib/db.js";import{loadExtensions as Re,spawnExtensionBackend as Me}from"./lib/ext-loader.js";import{SchedulerService as Ie}from"./lib/scheduler.js";import{handleClientMessage as X}from"./lib/ws-message.js";import{loadDotEnv as Ee}from"./lib/load-env.js";import{listSessions as Oe,resolveTmuxSocketPath as Pe}from"./sessions.js";import{cmdAdd as Le,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";Ee();const U=Ge(),h=Se(),y=new xe,S=new be;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 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 Le(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(_e(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 P=await De();let st=await je();const J=rt(P.scheduleHistoryDays),G=I.join(process.cwd(),"extensions"),Y=await Re(G);for(const e of Y)e.start&&se.push(Me(e.dir,e));const H=new Ie({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 ae(e){return g.get(e)?T.get(e)??null:null}const ie={activeTheme:st,settingUpPassword:!1},at=Q({mode:"hub",securityConfig:h,tokenStore:y,rateLimiter:S,scheduler:H,settings:P,terminalRenderer:de(N,P.terminalRenderer),scheduleHistoryDays:J,extsDir:G,extensions:Y,terminalBufferConfig:U,state:ie,federation:{getConfig:()=>C,setConfig:async e=>{C=e,await qe(e),le(e)},status:ct,hostname:$()},hub:{agents:g,getChannel:ae}}),it=Q({mode:"agent",securityConfig:h,tokenStore:y,rateLimiter:S,scheduler:H,settings:P,terminalRenderer:de(N,P.terminalRenderer),scheduleHistoryDays:J,extsDir:G,extensions:Y,terminalBufferConfig:U,state:ie});let C=await Ze(),O=null;function ce(){O&&(O.stop(),O=null)}function le(e){ce(),!(!e.enabled||!e.hub||!e.token)&&(O=et({hub:e.hub,token:e.token,name:e.name?.trim()||$(),version:ke,app:it,listSessions:Oe,terminalBufferConfig:U,getSessionPaneTarget:q,capturePaneTail:ee,capturePaneHistoryChunk:te,toCrlf:ne,handleClientMessage:X,acquireControlClient:Z}))}function ct(){return C.enabled?O?O.status():{state:"idle",agentId:null,hub:C.hub??"",name:C.name??$()}:{state:"idle",agentId:null,hub:C.hub??"",name:C.name??$()}}le(C);const lt=parseInt(process.env.PORT||"21000",10),ut=Te({fetch:at.fetch,port:lt,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),L=new we({noServer:!0});function M(e,n,r){e.write(`HTTP/1.1 ${n} ${r}\r
3
3
  \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
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;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=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,tmuxSocketPath:Pe()??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&&!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 x=Ce(c.password);if(x)return d(x),!0;j=!0;try{h.passwordHash=await Ae(c.password),Qe(h),l("password_set",{ip:t})}finally{j=!1}const{stored:F,plaintext:fe}=y.createAccessToken("setup",h.security.tokenTtlDays);return R(e,{type:"auth.ok",setupMode:!0,token:fe}),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 ve(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:me}=y.createAccessToken(_,h.security.tokenTtlDays);return i("password",_),R(e,{type:"auth.ok",setupMode:!1,token:me}),!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 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=ae(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();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 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(`
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}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=Xe(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 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 ue(){H.cleanup(),ce(),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{}S.dispose(),process.exit(0)}process.on("SIGINT",ue),process.on("SIGTERM",ue);function gt(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function de(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(`
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,8 +1,8 @@
1
- import{existsSync as W,readdirSync as je,statSync as C,unlinkSync as ke,writeFileSync as ve,mkdirSync as be,readFileSync as Y}from"node:fs";import{readFile as qe}from"node:fs/promises";import{execSync as E,execFileSync as K}from"node:child_process";import h from"node:path";import{fileURLToPath as Se}from"node:url";import{Hono as Te}from"hono";import{listSessions as R}from"../sessions.js";import{renderLoginPage as Ae,renderNotesIndex as xe,renderNotesPage as Ie,renderSettings as Ce,renderThemeSettings as _e,renderScheduleIndex as Ee,renderHistoryIndex as Re,renderQuickCommandsPage as Oe,renderFilesIndex as Ne,renderShell as De}from"../frontend.js";import{renderFederationPage as Le}from"./pages/federation.js";import{createAgentToken as Pe,removeAgentToken as Ue,listAgentTokens as X}from"./agent-tokens.js";import{hashPassword as Fe,verifyPassword as He,validatePassword as We}from"./auth.js";import{atomicWriteFileSync as Me}from"./atomicWrite.js";import{resolveFsPath as _,resolveFsRoots as Z,MAX_FILE_BYTES as J,walkRecursive as Be}from"./fs-access.js";import{captureSessionWindowsWithPath as Ve}from"./tmux-windows.js";import{audit as q}from"./auditLog.js";import{db as A}from"./db.js";import{getSessionAccessMap as M}from"./session-access.js";import{listWindowHistory as $e,clearWindowHistory as Ge}from"./window-history.js";import{registerExtensionRoutes as ze}from"./ext-loader.js";import{saveSecurityConfig as Qe}from"./security-config.js";import{isValidScheduleInput as Ye,isValidRescheduleInput as Ke}from"./scheduler.js";import{ImageUploadError as Xe,saveUploadedImage as Ze}from"./image-upload.js";import{getScheduleDelayError as ee}from"./schedule-delay.js";import{readSettings as te,writeSettings as Je}from"./settings.js";import{setActiveThemeTemplate as re}from"./theme-store.js";import{isThemeTemplateId as ne,THEME_TEMPLATE_IDS as et}from"./themes/index.js";import{installPlugin as tt,uninstallPlugin as rt}from"./plugins.js";import{buildCommandbarSessions as se}from"./commandbar.js";import{pinView as nt,unpinView as st,listPinnedViews as ot}from"./pinned-views.js";import{listWindowLabels as it,setWindowLabel as at}from"./window-labels.js";import{captureAndStoreWindows as oe,getStoredWindows as ie}from"./session-windows.js";import{buildSidebarSessions as dt}from"./sessions-sidebar.js";import{createQuickCommand as ut,deleteQuickCommand as ct,listQuickCommands as ae,updateQuickCommand as lt}from"./quick-commands.js";import{getSystemStatus as mt,getTopProcesses as ft,killProcess as pt}from"./system-monitor.js";import{listSessionWindows as ht,selectSessionWindow as gt,newSessionWindow as yt,renameSessionWindow as wt,newTmuxSession as jt,renameSession as kt,killSession as vt,TmuxWindowsError as N}from"./tmux-windows.js";import{registerAgentTunnel as bt}from"./agent-tunnel.js";import{injectScopeScript as qt}from"./scope-script.js";const B="tmux-web-token",St=365;function Tt(k){return k==="127.0.0.1"||k==="::1"||k==="::ffff:127.0.0.1"}function fr(k){const{mode:de,securityConfig:g,tokenStore:v,rateLimiter:D,scheduler:x,settings:It,commandbarEnabled:m,terminalRenderer:L,scheduleHistoryDays:ue,extsDir:ce,extensions:le,terminalBufferConfig:P,getAgentId:V,state:c,federation:S,hub:y}=k,b=de==="agent";function O(e){if(g.security.trustProxy){const t=(e.req.header("x-forwarded-for")||"").split(",")[0].trim();if(t)return t}return e.env?.incoming?.socket?.remoteAddress||"unknown"}function U(e){const t=e.req.header("authorization");if(t?.startsWith("Bearer "))return t.slice(7);const r=e.req.header("cookie");if(!r)return null;const n=r.match(new RegExp(`(?:^|;\\s*)${B}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function Ct(e){const t=U(e);return t?v.validateToken(t):null}function $(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${B}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${St*86400}${r?"; Secure":""}`)}function me(e){e.header("Set-Cookie",`${B}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const a=()=>b?async(e,t)=>t():async(e,t)=>{const r=U(e);return!r||!v.validateToken(r)?(q("http_unauthorized",{ip:O(e)}),e.json({error:"unauthorized"},401)):t()},w=()=>b?async(e,t)=>t():async(e,t)=>{const r=U(e);if(!r||!v.validateToken(r)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return t()};function T(){const e=se(R(),M());if(!y)return e;for(const t of y.agents.list()){if(!t.online)continue;const r=y.agents.get(t.agentId);if(r)for(const n of r.sessions)e.push({...n,agentId:t.agentId,agentName:t.name,remote:!0})}return e}function F(e){const t=dt(R(),M(),ot()),r=[];if(y)for(const n of y.agents.list()){const o=y.agents.get(n.agentId);r.push({agentId:n.agentId,agentName:n.name,online:n.online,sessions:o?o.sessions:[]})}return{...t,currentSession:e??null,agents:r}}function G(e){const t=typeof e.sessionName=="string"?e.sessionName.trim():"";if(!t)return{error:"sessionName is required"};if(e.windowIndex===void 0)return{sessionName:t};const r=e.windowIndex;return typeof r!="number"||!Number.isInteger(r)||r<0?{error:"windowIndex must be a non-negative integer"}:{sessionName:t,windowIndex:r}}const s=new Te;s.use("*",async(e,t)=>{await t(),e.header("X-Content-Type-Options","nosniff"),e.header("X-Frame-Options","DENY"),e.header("Content-Security-Policy",["default-src 'self'","script-src 'self' 'unsafe-inline'","style-src 'self' 'unsafe-inline' https://fonts.googleapis.com","connect-src 'self' ws: wss: http: https:","img-src 'self' data:","font-src 'self' https://fonts.gstatic.com","worker-src 'self' blob:"].join("; ")),e.header("Referrer-Policy","strict-origin-when-cross-origin")}),s.use("*",async(e,t)=>{const r=e.req.method;if(r==="GET"||r==="HEAD"||r==="OPTIONS")return t();const n=e.req.header("sec-fetch-site");return n&&n!=="same-origin"&&n!=="none"?e.text("cross-site request blocked",403):t()}),b||s.use("*",async(e,t)=>{if(await t(),e.req.method!=="GET"||e.req.path==="/login"||!(e.res.headers.get("content-type")??"").includes("text/html"))return;const n=await e.res.text();e.res=new Response(qt(n,{prefix:"",agentId:null}),e.res)}),ze(s,ce,le);const z=h.dirname(Se(import.meta.url)),fe=[h.join(z,"..","assets"),h.join(z,"assets"),h.join(process.cwd(),"dist","assets")];s.get("/assets/:file",async e=>{const t=e.req.param("file");if(!/^[a-zA-Z0-9._-]+$/.test(t))return e.notFound();for(const r of fe){const n=h.join(r,t);if(!W(n))continue;const o=await qe(n),i=h.extname(t),u={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(o,200,{"Content-Type":u[i]??"application/octet-stream","Cache-Control":i===".js"||i===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const pe=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1
+ import{existsSync as F,readdirSync as he,statSync as x,unlinkSync as ye,writeFileSync as ge,mkdirSync as we,readFileSync as G}from"node:fs";import{readFile as je}from"node:fs/promises";import{execSync as C,execFileSync as z}from"node:child_process";import p from"node:path";import{fileURLToPath as ke}from"node:url";import{Hono as ve}from"hono";import{listSessions as E}from"../sessions.js";import{renderLoginPage as qe,renderNotesIndex as be,renderNotesPage as Se,renderSettings as Te,renderThemeSettings as Ae,renderScheduleIndex as xe,renderHistoryIndex as Ie,renderQuickCommandsPage as Ce,renderFilesIndex as _e,renderShell as Ee}from"../frontend.js";import{renderFederationPage as Re}from"./pages/federation.js";import{createAgentToken as Oe,removeAgentToken as Ne,listAgentTokens as Q}from"./agent-tokens.js";import{hashPassword as De,verifyPassword as Le,validatePassword as Pe}from"./auth.js";import{atomicWriteFileSync as Ue}from"./atomicWrite.js";import{resolveFsPath as I,resolveFsRoots as Y,MAX_FILE_BYTES as K,walkRecursive as Fe}from"./fs-access.js";import{captureSessionWindowsWithPath as He}from"./tmux-windows.js";import{audit as v}from"./auditLog.js";import{db as S}from"./db.js";import{getSessionAccessMap as We}from"./session-access.js";import{listWindowHistory as Me,clearWindowHistory as Be}from"./window-history.js";import{registerExtensionRoutes as Ve}from"./ext-loader.js";import{saveSecurityConfig as $e}from"./security-config.js";import{isValidScheduleInput as Ge,isValidRescheduleInput as ze}from"./scheduler.js";import{ImageUploadError as Qe,saveUploadedImage as Ye}from"./image-upload.js";import{getScheduleDelayError as X}from"./schedule-delay.js";import{readSettings as Z,writeSettings as Ke}from"./settings.js";import{setActiveThemeTemplate as J}from"./theme-store.js";import{isThemeTemplateId as ee,THEME_TEMPLATE_IDS as Xe}from"./themes/index.js";import{installPlugin as Ze,uninstallPlugin as Je}from"./plugins.js";import{pinView as et,unpinView as tt,listPinnedViews as rt}from"./pinned-views.js";import{listWindowLabels as nt,setWindowLabel as st}from"./window-labels.js";import{captureAndStoreWindows as te,getStoredWindows as re}from"./session-windows.js";import{buildSidebarSessions as ot}from"./sessions-sidebar.js";import{createQuickCommand as it,deleteQuickCommand as at,listQuickCommands as ne,updateQuickCommand as dt}from"./quick-commands.js";import{getSystemStatus as ut,getTopProcesses as ct,killProcess as lt}from"./system-monitor.js";import{listSessionWindows as mt,selectSessionWindow as ft,newSessionWindow as pt,renameSessionWindow as ht,newTmuxSession as yt,renameSession as gt,killSession as wt,TmuxWindowsError as R}from"./tmux-windows.js";import{registerAgentTunnel as jt}from"./agent-tunnel.js";import{injectScopeScript as kt}from"./scope-script.js";const H="tmux-web-token",vt=365;function qt(w){return w==="127.0.0.1"||w==="::1"||w==="::ffff:127.0.0.1"}function ur(w){const{mode:se,securityConfig:h,tokenStore:j,rateLimiter:O,scheduler:T,settings:Tt,terminalRenderer:N,scheduleHistoryDays:oe,extsDir:ie,extensions:ae,terminalBufferConfig:D,getAgentId:W,state:c,federation:q,hub:b}=w,k=se==="agent";function _(e){if(h.security.trustProxy){const t=(e.req.header("x-forwarded-for")||"").split(",")[0].trim();if(t)return t}return e.env?.incoming?.socket?.remoteAddress||"unknown"}function L(e){const t=e.req.header("authorization");if(t?.startsWith("Bearer "))return t.slice(7);const r=e.req.header("cookie");if(!r)return null;const n=r.match(new RegExp(`(?:^|;\\s*)${H}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function At(e){const t=L(e);return t?j.validateToken(t):null}function M(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${H}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${vt*86400}${r?"; Secure":""}`)}function de(e){e.header("Set-Cookie",`${H}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const a=()=>k?async(e,t)=>t():async(e,t)=>{const r=L(e);return!r||!j.validateToken(r)?(v("http_unauthorized",{ip:_(e)}),e.json({error:"unauthorized"},401)):t()},y=()=>k?async(e,t)=>t():async(e,t)=>{const r=L(e);if(!r||!j.validateToken(r)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return t()};function P(e){const t=ot(E(),We(),rt()),r=[];if(b)for(const n of b.agents.list()){const o=b.agents.get(n.agentId);r.push({agentId:n.agentId,agentName:n.name,online:n.online,sessions:o?o.sessions:[]})}return{...t,currentSession:e??null,agents:r}}function B(e){const t=typeof e.sessionName=="string"?e.sessionName.trim():"";if(!t)return{error:"sessionName is required"};if(e.windowIndex===void 0)return{sessionName:t};const r=e.windowIndex;return typeof r!="number"||!Number.isInteger(r)||r<0?{error:"windowIndex must be a non-negative integer"}:{sessionName:t,windowIndex:r}}const s=new ve;s.use("*",async(e,t)=>{await t(),e.header("X-Content-Type-Options","nosniff"),e.header("X-Frame-Options","DENY"),e.header("Content-Security-Policy",["default-src 'self'","script-src 'self' 'unsafe-inline'","style-src 'self' 'unsafe-inline' https://fonts.googleapis.com","connect-src 'self' ws: wss: http: https:","img-src 'self' data:","font-src 'self' https://fonts.gstatic.com","worker-src 'self' blob:"].join("; ")),e.header("Referrer-Policy","strict-origin-when-cross-origin")}),s.use("*",async(e,t)=>{const r=e.req.method;if(r==="GET"||r==="HEAD"||r==="OPTIONS")return t();const n=e.req.header("sec-fetch-site");return n&&n!=="same-origin"&&n!=="none"?e.text("cross-site request blocked",403):t()}),k||s.use("*",async(e,t)=>{if(await t(),e.req.method!=="GET"||e.req.path==="/login"||!(e.res.headers.get("content-type")??"").includes("text/html"))return;const n=await e.res.text();e.res=new Response(kt(n,{prefix:"",agentId:null}),e.res)}),Ve(s,ie,ae);const V=p.dirname(ke(import.meta.url)),ue=[p.join(V,"..","assets"),p.join(V,"assets"),p.join(process.cwd(),"dist","assets")];s.get("/assets/:file",async e=>{const t=e.req.param("file");if(!/^[a-zA-Z0-9._-]+$/.test(t))return e.notFound();for(const r of ue){const n=p.join(r,t);if(!F(n))continue;const o=await je(n),i=p.extname(t),u={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(o,200,{"Content-Type":u[i]??"application/octet-stream","Cache-Control":i===".js"||i===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const ce=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
2
  <rect x="2.5" y="5.5" width="27" height="21" rx="4" fill="#0d1117" stroke="#7dd3fc" stroke-width="2"/>
3
3
  <path d="M8 13l4.2 3.1L8 19.2" fill="none" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
4
4
  <line x1="15.5" y1="19.6" x2="22.5" y2="19.6" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round"/>
5
- </svg>`,Q=e=>e.body(pe,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",Q),s.get("/favicon.ico",Q);const he={name:"tmux-weblink",short_name:"tmux-web",description:"Access your tmux sessions from the browser",start_url:"/",display:"standalone",background_color:"#0d1117",theme_color:"#0d1117",icons:[{src:"/assets/icon-192.png",sizes:"192x192",type:"image/png"},{src:"/assets/icon-512.png",sizes:"512x512",type:"image/png"}]};s.get("/manifest.json",e=>e.json(he,200,{"Cache-Control":"public, max-age=3600"}));const ge=`// tmux-weblink Service Worker
5
+ </svg>`,$=e=>e.body(ce,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",$),s.get("/favicon.ico",$);const le={name:"tmux-weblink",short_name:"tmux-web",description:"Access your tmux sessions from the browser",start_url:"/",display:"standalone",background_color:"#0d1117",theme_color:"#0d1117",icons:[{src:"/assets/icon-192.png",sizes:"192x192",type:"image/png"},{src:"/assets/icon-512.png",sizes:"512x512",type:"image/png"}]};s.get("/manifest.json",e=>e.json(le,200,{"Cache-Control":"public, max-age=3600"}));const me=`// tmux-weblink Service Worker
6
6
  const CACHE = "tmux-weblink-v1";
7
7
  const ASSETS = ["/", "/favicon.svg"];
8
8
  self.addEventListener("install", (e) => {
@@ -13,7 +13,7 @@ self.addEventListener("activate", (e) => { e.waitUntil(clients.claim()); });
13
13
  self.addEventListener("fetch", (e) => {
14
14
  e.respondWith(fetch(e.request).catch(() => caches.match(e.request)));
15
15
  });
16
- `;s.get("/sw.js",e=>e.body(ge,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!g.passwordHash,n=e.req.query("error");return e.html(Ae({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:c.activeTheme}))}),s.get("/",w(),e=>{const t=m?T():[],r=Z();return e.html(De({theme:c.activeTheme,commandbarEnabled:m,commandbarSessions:t,fsRoots:r,terminalCfg:P,renderer:L,scrollback:P.initialLines+2*P.historyChunk,wsBase:b&&V?.()?`/ws/a/${V()}`:void 0}))}),s.get("/notes",w(),e=>{const t=m?T():[];return e.html(xe(A.data.notes,c.activeTheme,m,t))}),s.get("/notes/:session",w(),e=>{const t=decodeURIComponent(e.req.param("session")),r=m?T():[];return e.html(Ie(t,c.activeTheme,m,r))}),s.get("/schedule",w(),e=>{const t=m?T():[];return e.html(Ee(x.list(),x.listTriggered(),c.activeTheme,ue,m,t))}),s.get("/history",w(),e=>{const t=R(),r=m?se(t,M()):[],n=new Set(t.map(o=>o.name));return e.html(Re($e(),c.activeTheme,m,r,n))}),s.get("/quick-commands",w(),e=>{const t=m?T():[];return e.html(Oe(ae(),c.activeTheme,m,t))}),s.get("/files",w(),e=>{const t=m?T():[],r=Z();return e.html(Ne(c.activeTheme,m,t,r))}),s.post("/api/history/clear",a(),async e=>(await Ge(),e.json({ok:!0}))),s.get("/api/quick-commands",a(),e=>e.json(ae())),s.post("/api/quick-commands",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await ut(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await lt(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",a(),e=>ct(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{if(b)return e.json({error:"hub only"},403);const t=O(e),r=D.check(t);if(!r.allowed)return q("rate_limited",{ip:t,retryAfterMs:r.retryAfterMs,permanentLock:r.permanentLock}),r.permanentLock?e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403):e.json({error:"Too many attempts",retryAfterMs:r.retryAfterMs},429);let n;try{n=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const o=typeof n.password=="string"?n.password:"";if(!g.passwordHash){if(!g.security.allowRemoteSetup&&!Tt(t))return q("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(c.settingUpPassword)return e.json({error:"Password setup in progress"},409);const l=We(o);if(l)return e.json({error:l},400);c.settingUpPassword=!0;try{g.passwordHash=await Fe(o),Qe(g),q("password_set",{ip:t})}finally{c.settingUpPassword=!1}const{plaintext:d}=v.createAccessToken("setup",g.security.tokenTtlDays);return $(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await He(o,g.passwordHash)){const l=D.recordFailure(t);return q("auth_failed",{ip:t,method:"password",failures:l.failures,permanentLock:l.permanentLock}),l.permanentLock?(v.revokeAll(),q("permanent_lock",{ip:t,failures:l.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):l.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:l.retryAfterMs},429)}D.recordSuccess(t);const u=`browser-${t}`,{plaintext:j}=v.createAccessToken(u,g.security.tokenTtlDays);return $(e,j),q("auth_success",{ip:t,method:"password",tokenName:u}),e.json({ok:!0,token:j})}),s.post("/api/auth/token",a(),async e=>{if(b)return e.json({error:"hub only"},403);const t=O(e);let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof r.name=="string"?r.name:`api-${t}`,o=typeof r.ttlDays=="number"?r.ttlDays:g.security.tokenTtlDays,{stored:i,plaintext:u}=v.createAccessToken(n,o);return q("token_created",{ip:t,name:i.name,tokenId:i.id}),e.json({id:i.id,name:i.name,token:u,expiresAt:i.expiresAt})}),s.get("/api/auth/tokens",a(),e=>b?e.json({error:"hub only"},403):e.json(v.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",a(),e=>b?e.json({error:"hub only"},403):v.revoke(e.req.param("id"))?(q("token_revoked",{ip:O(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",a(),e=>(me(e),e.json({ok:!0}))),s.get("/settings",w(),async e=>{const t=await te(),r=t.terminalRenderer??"xterm";return e.html(Ce({settings:t,renderer:L,rendererOverridden:L!==r,theme:c.activeTheme,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await te(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",o=t.defaultView==="recent"?"recent":"default",i=xt(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Je({...r,commandbar:t.commandbar!==void 0,terminalRenderer:n,defaultView:o,scheduleHistoryDays:i}),e.redirect("/settings?saved=1",303)}),s.post("/settings/plugins",a(),async e=>{if(b)return e.json({error:"plugin management is hub-only"},403);let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=t.action,n=typeof t.pkg=="string"?t.pkg.trim():"";if(!n)return e.redirect("/settings?error="+encodeURIComponent("missing package name"),303);const o=r==="remove"?await rt(n):r==="add"?await tt(n):{ok:!1,output:"unknown action"};return o.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(o.output.slice(0,800)),303)}),s.get("/settings/theme",w(),e=>e.html(_e({theme:c.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!ne(r)?e.redirect("/settings/theme",303):(c.activeTheme=await re(r),e.redirect("/settings/theme?saved=1",303))});const ye={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",a(),e=>e.json({active:c.activeTheme.template,templates:et.map(t=>({id:t,name:ye[t]}))})),s.post("/api/theme",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!ne(r)?e.json({error:"invalid theme template"},400):(c.activeTheme=await re(r),e.json({ok:!0,active:c.activeTheme.template}))}),s.get("/api/system/status",a(),e=>e.json(mt())),s.get("/api/system/processes",a(),e=>e.json(ft())),S&&(s.get("/settings/federation",w(),async e=>e.html(Le({config:S.getConfig(),status:S.status(),tokens:X().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:S.hostname},c.activeTheme))),s.get("/api/federation",a(),e=>e.json({config:S.getConfig(),status:S.status()})),s.post("/api/federation",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.hub=="string"?t.hub.trim():"",n=typeof t.token=="string"?t.token.trim():"",o=typeof t.name=="string"?t.name.trim().slice(0,64):"",i=t.enabled===!0;if(i){if(!r)return e.json({error:"hub URL is required"},400);if(!/^wss?:\/\//i.test(r))return e.json({error:"hub URL must start with ws:// or wss://"},400);if(!n)return e.json({error:"registration token is required"},400)}const u={hub:r||void 0,token:n||void 0,name:o||void 0,enabled:i};return await S.setConfig(u),e.json({ok:!0,status:S.status()})}),s.get("/api/agent-tokens",a(),e=>e.json(X().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})))),s.post("/api/agent-tokens",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim().slice(0,64):"";if(!r)return e.json({error:"name is required"},400);const{id:n,token:o}=Pe(r);return e.json({ok:!0,id:n,name:r,token:o})}),s.delete("/api/agent-tokens/:id",a(),e=>{const t=e.req.param("id");return Ue(t)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",a(),async e=>{const{pid:t}=await e.req.json();if(typeof t!="number"||!Number.isInteger(t)||t<=0)return e.json({error:"invalid pid"},400);const r=pt(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/agents",a(),e=>y?e.json(y.agents.list()):e.json([])),s.get("/api/sessions",a(),e=>m?e.json(T()):e.json({error:"commandbar disabled"},404)),s.post("/api/sessions/new",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(R().some(i=>i.name===r))return e.json({error:"session already exists"},409);try{return jt(r,n),e.json({ok:!0})}catch(i){const u=i instanceof N?i.message:"failed to create session";return e.json({error:u},500)}}),s.post("/api/sessions/rename",a(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(kt(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",a(),async e=>{try{const{name:t}=await e.req.json();return t?(vt(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.get("/api/fs/session-path",a(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=Ve(t),o=r.find(i=>i.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:o})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",a(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=h.join(t,r));const n=e.req.query("recursive")==="true";let o=r,i="",u=r.endsWith("/");if(!u)try{u=C(r).isDirectory()}catch{}u||(o=h.dirname(r),i=h.basename(r).toLowerCase());try{const j=je(o),l=[],d=[];for(const f of j)if(!f.startsWith(".")&&!(i&&!f.toLowerCase().startsWith(i))){try{const p=h.join(o,f);C(p).isDirectory()?(l.push(p),n&&Be(p,l,d,0)):d.push(p)}catch{}if(l.length+d.length>=5e3)break}return e.json({dirs:l,files:d})}catch{return e.json({dirs:[],files:[]})}});function we(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=E("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=E("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=E("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let o="";try{o=E("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const i=[];for(const d of o.split(`
17
- `)){if(!d.trim())continue;const f=d[0],p=d[1],I=d.substring(3).trim(),H=p!==" "?p:f;i.push({path:I,status:H,staged:f,unstaged:p,additions:0,deletions:0})}const u=new Map;try{const d=E("git diff --numstat HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3});for(const f of d.split(`
18
- `)){if(!f.trim())continue;const p=f.split(" ");if(p.length>=3){const I=parseInt(p[0],10)||0,H=parseInt(p[1],10)||0;u.set(p[2],{added:I,deleted:H})}}}catch{}for(const d of i){const f=u.get(d.path);f&&(d.additions=f.added,d.deletions=f.deleted)}let j=0,l=0;for(const d of i)j+=d.additions,l+=d.deletions;for(const d of i)if(d.status==="?"&&d.additions===0)try{const f=h.join(r,d.path),I=Y(f,"utf-8").split(`
19
- `).length;d.additions=I,j+=I}catch{}return{repoRoot:r,branch:n,files:i,linesAdded:j,linesRemoved:l}}catch{return t}}return s.get("/api/git/status",a(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=_(t),n=we(r);return e.json(n)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),s.get("/api/git/diff",a(),e=>{const t=e.req.query("path"),r=e.req.query("file");if(!t||!r)return e.json({error:"path and file are required"},400);try{const n=_(t);if(!W(n)||!C(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const o=K("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),i=K("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:o,stagedDiff:i})}catch(o){throw console.error("[git/diff] execFileSync failed:",o?.code,o.message),o}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",a(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=_(t);if(!C(r).isFile())return e.json({error:"not a file"},400);const n=C(r).size;if(n>J)return e.json({error:"file too large",size:n,maxBytes:J},413);const o=Y(r,"utf-8");return e.json({path:r,content:o,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",a(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=_(t.path);return Me(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=_(t.path);return C(r).isFile()?(ke(r),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=_(t.path);return W(r)?e.json({error:"file already exists"},409):(be(h.dirname(r),{recursive:!0}),ve(r,"","utf-8"),e.json({ok:!0,path:r}))}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}}),s.get("/api/sidebar/sessions",a(),e=>{const t=e.req.query("currentSession");return e.json(F(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",a(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(ie(t))}),s.post("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=G(t);return"error"in r?e.json({error:r.error},400):(await nt(r.sessionName,r.windowIndex),e.json(F()))}),s.delete("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=G(t);return"error"in r?e.json({error:r.error},400):(await st(r.sessionName,r.windowIndex),e.json(F()))}),s.get("/api/notes",a(),e=>{const t=[...A.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",a(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=A.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",a(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},o=A.data.notes.findIndex(i=>i.scope===t);return o>=0?A.data.notes[o]=n:A.data.notes.push(n),await A.write(),e.json({ok:!0})}),s.post("/api/session/:session/upload",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!R().some(i=>i.name===t))return e.json({error:"session not found"},404);let n;try{n=await e.req.parseBody()}catch{return e.json({error:"invalid multipart body"},400)}const o=n.file;if(!(o instanceof File))return e.json({error:"missing file field"},400);try{const i=await o.arrayBuffer(),u=Buffer.from(i),{path:j}=await Ze(u,o.type||void 0,o.name||void 0);return e.json({path:j})}catch(i){return i instanceof Xe?e.json({error:i.message},i.status):(console.error("[upload]",i),e.json({error:"upload failed"},500))}}),s.get("/api/session/:session/windows",a(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map(it(t).map(i=>[i.windowIndex,i.label])),n=new Map(ie(t).map(i=>[i.index,i])),o=ht(t).map(i=>({...i,label:r.get(i.index)??null,worktree:n.get(i.index)?.worktree??!1}));return e.json(o)}),s.post("/api/session/:session/window-label",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);const o=typeof r.label=="string"?r.label:"",i=await at(t,n,o);return e.json(i)}),s.post("/api/session/:session/select-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);try{return gt(t,n),e.json({ok:!0})}catch(o){return o instanceof N?e.json({error:o.message},o.status):(console.error("[select-window]",o),e.json({error:"select-window failed"},500))}}),s.post("/api/session/:session/rename-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);if(typeof r.name!="string"||!r.name.trim())return e.json({error:"name is required"},400);try{return wt(t,n,r.name),oe(t),e.json({ok:!0})}catch(o){return o instanceof N?e.json({error:o.message},o.status):(console.error("[rename-window]",o),e.json({error:"rename-window failed"},500))}}),s.post("/api/session/:session/new-window",a(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return yt(t),oe(t),e.json({ok:!0})}catch(r){return r instanceof N?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),s.get("/api/schedule",a(),e=>e.json(x.list(e.req.query("session")))),s.post("/api/schedule",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ee(t);if(r)return e.json({error:r},400);if(!Ye(t))return e.json({error:"invalid body"},400);const n=await x.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),s.delete("/api/schedule/:id",a(),async e=>await x.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.patch("/api/schedule/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=ee(t);if(r)return e.json({error:r},400);if(!Ke(t))return e.json({error:"invalid body"},400);const n=await x.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)}),y&&(s.use("/a/:agentId/*",a()),bt(s,{getChannel:y.getChannel})),s}const At=7;function xt(k){return typeof k!="number"||!Number.isFinite(k)?At:Math.min(365,Math.max(1,Math.round(k)))}export{fr as buildApp};
16
+ `;s.get("/sw.js",e=>e.body(me,200,{"Content-Type":"application/javascript; charset=utf-8"})),s.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!h.passwordHash,n=e.req.query("error");return e.html(qe({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:c.activeTheme}))}),s.get("/",y(),e=>{const t=Y();return e.html(Ee({theme:c.activeTheme,fsRoots:t,terminalCfg:D,renderer:N,scrollback:D.initialLines+2*D.historyChunk,wsBase:k&&W?.()?`/ws/a/${W()}`:void 0}))}),s.get("/notes",y(),e=>e.html(be(S.data.notes,c.activeTheme))),s.get("/notes/:session",y(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.html(Se(t,c.activeTheme))}),s.get("/schedule",y(),e=>e.html(xe(T.list(),T.listTriggered(),c.activeTheme,oe))),s.get("/history",y(),e=>{const t=E(),r=new Set(t.map(n=>n.name));return e.html(Ie(Me(),c.activeTheme,r))}),s.get("/quick-commands",y(),e=>e.html(Ce(ne(),c.activeTheme))),s.get("/files",y(),e=>{const t=Y();return e.html(_e(c.activeTheme,t))}),s.post("/api/history/clear",a(),async e=>(await Be(),e.json({ok:!0}))),s.get("/api/quick-commands",a(),e=>e.json(ne())),s.post("/api/quick-commands",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await it(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await dt(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),s.delete("/api/quick-commands/:id",a(),e=>at(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.post("/api/auth/password",async e=>{if(k)return e.json({error:"hub only"},403);const t=_(e),r=O.check(t);if(!r.allowed)return v("rate_limited",{ip:t,retryAfterMs:r.retryAfterMs,permanentLock:r.permanentLock}),r.permanentLock?e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403):e.json({error:"Too many attempts",retryAfterMs:r.retryAfterMs},429);let n;try{n=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const o=typeof n.password=="string"?n.password:"";if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!qt(t))return v("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(c.settingUpPassword)return e.json({error:"Password setup in progress"},409);const l=Pe(o);if(l)return e.json({error:l},400);c.settingUpPassword=!0;try{h.passwordHash=await De(o),$e(h),v("password_set",{ip:t})}finally{c.settingUpPassword=!1}const{plaintext:d}=j.createAccessToken("setup",h.security.tokenTtlDays);return M(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await Le(o,h.passwordHash)){const l=O.recordFailure(t);return v("auth_failed",{ip:t,method:"password",failures:l.failures,permanentLock:l.permanentLock}),l.permanentLock?(j.revokeAll(),v("permanent_lock",{ip:t,failures:l.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):l.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:l.retryAfterMs},429)}O.recordSuccess(t);const u=`browser-${t}`,{plaintext:g}=j.createAccessToken(u,h.security.tokenTtlDays);return M(e,g),v("auth_success",{ip:t,method:"password",tokenName:u}),e.json({ok:!0,token:g})}),s.post("/api/auth/token",a(),async e=>{if(k)return e.json({error:"hub only"},403);const t=_(e);let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const n=typeof r.name=="string"?r.name:`api-${t}`,o=typeof r.ttlDays=="number"?r.ttlDays:h.security.tokenTtlDays,{stored:i,plaintext:u}=j.createAccessToken(n,o);return v("token_created",{ip:t,name:i.name,tokenId:i.id}),e.json({id:i.id,name:i.name,token:u,expiresAt:i.expiresAt})}),s.get("/api/auth/tokens",a(),e=>k?e.json({error:"hub only"},403):e.json(j.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",a(),e=>k?e.json({error:"hub only"},403):j.revoke(e.req.param("id"))?(v("token_revoked",{ip:_(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",a(),e=>(de(e),e.json({ok:!0}))),s.get("/settings",y(),async e=>{const t=await Z(),r=t.terminalRenderer??"xterm";return e.html(Te({settings:t,renderer:N,rendererOverridden:N!==r,theme:c.activeTheme,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await Z(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",o=t.defaultView==="recent"?"recent":"default",i=St(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Ke({...r,terminalRenderer:n,defaultView:o,scheduleHistoryDays:i}),e.redirect("/settings?saved=1",303)}),s.post("/settings/plugins",a(),async e=>{if(k)return e.json({error:"plugin management is hub-only"},403);let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=t.action,n=typeof t.pkg=="string"?t.pkg.trim():"";if(!n)return e.redirect("/settings?error="+encodeURIComponent("missing package name"),303);const o=r==="remove"?await Je(n):r==="add"?await Ze(n):{ok:!1,output:"unknown action"};return o.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(o.output.slice(0,800)),303)}),s.get("/settings/theme",y(),e=>e.html(Ae({theme:c.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",a(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings/theme?error=1",303)}const r=t.template;return typeof r!="string"||!ee(r)?e.redirect("/settings/theme",303):(c.activeTheme=await J(r),e.redirect("/settings/theme?saved=1",303))});const fe={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",a(),e=>e.json({active:c.activeTheme.template,templates:Xe.map(t=>({id:t,name:fe[t]}))})),s.post("/api/theme",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=t.template;return typeof r!="string"||!ee(r)?e.json({error:"invalid theme template"},400):(c.activeTheme=await J(r),e.json({ok:!0,active:c.activeTheme.template}))}),s.get("/api/system/status",a(),e=>e.json(ut())),s.get("/api/system/processes",a(),e=>e.json(ct())),q&&(s.get("/settings/federation",y(),async e=>e.html(Re({config:q.getConfig(),status:q.status(),tokens:Q().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:q.hostname},c.activeTheme))),s.get("/api/federation",a(),e=>e.json({config:q.getConfig(),status:q.status()})),s.post("/api/federation",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.hub=="string"?t.hub.trim():"",n=typeof t.token=="string"?t.token.trim():"",o=typeof t.name=="string"?t.name.trim().slice(0,64):"",i=t.enabled===!0;if(i){if(!r)return e.json({error:"hub URL is required"},400);if(!/^wss?:\/\//i.test(r))return e.json({error:"hub URL must start with ws:// or wss://"},400);if(!n)return e.json({error:"registration token is required"},400)}const u={hub:r||void 0,token:n||void 0,name:o||void 0,enabled:i};return await q.setConfig(u),e.json({ok:!0,status:q.status()})}),s.get("/api/agent-tokens",a(),e=>e.json(Q().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})))),s.post("/api/agent-tokens",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim().slice(0,64):"";if(!r)return e.json({error:"name is required"},400);const{id:n,token:o}=Oe(r);return e.json({ok:!0,id:n,name:r,token:o})}),s.delete("/api/agent-tokens/:id",a(),e=>{const t=e.req.param("id");return Ne(t)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",a(),async e=>{const{pid:t}=await e.req.json();if(typeof t!="number"||!Number.isInteger(t)||t<=0)return e.json({error:"invalid pid"},400);const r=lt(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/agents",a(),e=>b?e.json(b.agents.list()):e.json([])),s.post("/api/sessions/new",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=typeof t.name=="string"?t.name.trim():"";if(!r)return e.json({error:"name is required"},400);if(!/^[a-zA-Z0-9_\-. ]+$/.test(r))return e.json({error:"name contains invalid characters"},400);const n=typeof t.dir=="string"&&t.dir.trim()?t.dir.trim():void 0;if(E().some(i=>i.name===r))return e.json({error:"session already exists"},409);try{return yt(r,n),e.json({ok:!0})}catch(i){const u=i instanceof R?i.message:"failed to create session";return e.json({error:u},500)}}),s.post("/api/sessions/rename",a(),async e=>{try{const{oldName:t,newName:r}=await e.req.json();return!t||!r?e.json({error:"oldName and newName required"},400):/^[a-zA-Z0-9_\-. ]+$/.test(r)?(gt(t,r),e.json({ok:!0})):e.json({error:"invalid characters in name"},400)}catch{return e.json({error:"rename failed"},500)}}),s.post("/api/sessions/kill",a(),async e=>{try{const{name:t}=await e.req.json();return t?(wt(t),e.json({ok:!0})):e.json({error:"name required"},400)}catch{return e.json({error:"kill failed"},500)}}),s.get("/api/fs/session-path",a(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=He(t),o=r.find(i=>i.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:o})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",a(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=p.join(t,r));const n=e.req.query("recursive")==="true";let o=r,i="",u=r.endsWith("/");if(!u)try{u=x(r).isDirectory()}catch{}u||(o=p.dirname(r),i=p.basename(r).toLowerCase());try{const g=he(o),l=[],d=[];for(const m of g)if(!m.startsWith(".")&&!(i&&!m.toLowerCase().startsWith(i))){try{const f=p.join(o,m);x(f).isDirectory()?(l.push(f),n&&Fe(f,l,d,0)):d.push(f)}catch{}if(l.length+d.length>=5e3)break}return e.json({dirs:l,files:d})}catch{return e.json({dirs:[],files:[]})}});function pe(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=C("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=C("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=C("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let o="";try{o=C("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const i=[];for(const d of o.split(`
17
+ `)){if(!d.trim())continue;const m=d[0],f=d[1],A=d.substring(3).trim(),U=f!==" "?f:m;i.push({path:A,status:U,staged:m,unstaged:f,additions:0,deletions:0})}const u=new Map;try{const d=C("git diff --numstat HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3});for(const m of d.split(`
18
+ `)){if(!m.trim())continue;const f=m.split(" ");if(f.length>=3){const A=parseInt(f[0],10)||0,U=parseInt(f[1],10)||0;u.set(f[2],{added:A,deleted:U})}}}catch{}for(const d of i){const m=u.get(d.path);m&&(d.additions=m.added,d.deletions=m.deleted)}let g=0,l=0;for(const d of i)g+=d.additions,l+=d.deletions;for(const d of i)if(d.status==="?"&&d.additions===0)try{const m=p.join(r,d.path),A=G(m,"utf-8").split(`
19
+ `).length;d.additions=A,g+=A}catch{}return{repoRoot:r,branch:n,files:i,linesAdded:g,linesRemoved:l}}catch{return t}}return s.get("/api/git/status",a(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=I(t),n=pe(r);return e.json(n)}catch(r){return r.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):r.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0})}}),s.get("/api/git/diff",a(),e=>{const t=e.req.query("path"),r=e.req.query("file");if(!t||!r)return e.json({error:"path and file are required"},400);try{const n=I(t);if(!F(n)||!x(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const o=z("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),i=z("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:o,stagedDiff:i})}catch(o){throw console.error("[git/diff] execFileSync failed:",o?.code,o.message),o}}catch(n){return n.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):n.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):(console.error("[git/diff] exception:",n),e.json({error:String(n)},500))}}),s.get("/api/file",a(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=I(t);if(!x(r).isFile())return e.json({error:"not a file"},400);const n=x(r).size;if(n>K)return e.json({error:"file too large",size:n,maxBytes:K},413);const o=G(r,"utf-8");return e.json({path:r,content:o,size:n})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):t.code==="ENOENT"?e.json({error:"not found"},404):e.json({error:"internal error"},500)}}),s.put("/api/file",a(),async e=>{try{const t=await e.req.json();if(!t.path||typeof t.content!="string")return e.json({error:"path and content are required"},400);const r=I(t.path);return Ue(r,t.content),e.json({ok:!0})}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"write failed"},500)}}),s.post("/api/file/delete",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=I(t.path);return x(r).isFile()?(ye(r),e.json({ok:!0})):e.json({error:"not a file"},400)}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"delete failed"},500)}}),s.post("/api/file/touch",a(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=I(t.path);return F(r)?e.json({error:"file already exists"},409):(we(p.dirname(r),{recursive:!0}),ge(r,"","utf-8"),e.json({ok:!0,path:r}))}catch(t){return t.message==="FS_ROOTS_NOT_CONFIGURED"?e.json({error:"file access not configured"},403):t.message==="PATH_NOT_ALLOWED"?e.json({error:"path not allowed"},403):e.json({error:"touch failed"},500)}}),s.get("/api/sidebar/sessions",a(),e=>{const t=e.req.query("currentSession");return e.json(P(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",a(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(re(t))}),s.post("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=B(t);return"error"in r?e.json({error:r.error},400):(await et(r.sessionName,r.windowIndex),e.json(P()))}),s.delete("/api/pinned-views",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=B(t);return"error"in r?e.json({error:r.error},400):(await tt(r.sessionName,r.windowIndex),e.json(P()))}),s.get("/api/notes",a(),e=>{const t=[...S.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",a(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=S.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),s.put("/api/notes/:scope",a(),async e=>{const t=decodeURIComponent(e.req.param("scope"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}if(typeof r.content!="string")return e.json({error:"content must be string"},400);const n={scope:t,content:r.content,updatedAt:Date.now()},o=S.data.notes.findIndex(i=>i.scope===t);return o>=0?S.data.notes[o]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),s.post("/api/session/:session/upload",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!E().some(i=>i.name===t))return e.json({error:"session not found"},404);let n;try{n=await e.req.parseBody()}catch{return e.json({error:"invalid multipart body"},400)}const o=n.file;if(!(o instanceof File))return e.json({error:"missing file field"},400);try{const i=await o.arrayBuffer(),u=Buffer.from(i),{path:g}=await Ye(u,o.type||void 0,o.name||void 0);return e.json({path:g})}catch(i){return i instanceof Qe?e.json({error:i.message},i.status):(console.error("[upload]",i),e.json({error:"upload failed"},500))}}),s.get("/api/session/:session/windows",a(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map(nt(t).map(i=>[i.windowIndex,i.label])),n=new Map(re(t).map(i=>[i.index,i])),o=mt(t).map(i=>({...i,label:r.get(i.index)??null,worktree:n.get(i.index)?.worktree??!1}));return e.json(o)}),s.post("/api/session/:session/window-label",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);const o=typeof r.label=="string"?r.label:"",i=await st(t,n,o);return e.json(i)}),s.post("/api/session/:session/select-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);try{return ft(t,n),e.json({ok:!0})}catch(o){return o instanceof R?e.json({error:o.message},o.status):(console.error("[select-window]",o),e.json({error:"select-window failed"},500))}}),s.post("/api/session/:session/rename-window",a(),async e=>{const t=decodeURIComponent(e.req.param("session"));let r;try{r=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const{windowIndex:n}=r;if(typeof n!="number"||!Number.isInteger(n)||n<0)return e.json({error:"windowIndex must be a non-negative integer"},400);if(typeof r.name!="string"||!r.name.trim())return e.json({error:"name is required"},400);try{return ht(t,n,r.name),te(t),e.json({ok:!0})}catch(o){return o instanceof R?e.json({error:o.message},o.status):(console.error("[rename-window]",o),e.json({error:"rename-window failed"},500))}}),s.post("/api/session/:session/new-window",a(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return pt(t),te(t),e.json({ok:!0})}catch(r){return r instanceof R?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),s.get("/api/schedule",a(),e=>e.json(T.list(e.req.query("session")))),s.post("/api/schedule",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=X(t);if(r)return e.json({error:r},400);if(!Ge(t))return e.json({error:"invalid body"},400);const n=await T.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),s.delete("/api/schedule/:id",a(),async e=>await T.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),s.patch("/api/schedule/:id",a(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=X(t);if(r)return e.json({error:r},400);if(!ze(t))return e.json({error:"invalid body"},400);const n=await T.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)}),b&&(s.use("/a/:agentId/*",a()),jt(s,{getChannel:b.getChannel})),s}const bt=7;function St(w){return typeof w!="number"||!Number.isFinite(w)?bt:Math.min(365,Math.max(1,Math.round(w)))}export{ur as buildApp};
package/dist/lib/cli.js CHANGED
@@ -1,8 +1,8 @@
1
- import{existsSync as d}from"node:fs";import b from"node:path";import{createRequire as w}from"node:module";import{getDataRoot as x,getSettingsPath as E,getThemePath as l}from"./state-paths.js";import{readSettings as m,writeSettings as T}from"./settings.js";import{readActiveTheme as y,setActiveThemeTemplate as _}from"./theme-store.js";import{isThemeTemplateId as v,THEME_TEMPLATE_IDS as k}from"./themes/index.js";import{cmdAdd as A,cmdRemove as S,getPluginDir as R}from"./plugins.js";import{getEnvFilePath as c}from"./load-env.js";import{SETUP_FEATURES as p}from"./setup-features.js";import{promptYesNo as $,promptChoice as N,requireTty as M}from"./setup-prompts.js";const U=x(),I=R(),f=E(),D=w(import.meta.url),{version:P}=D("../../package.json");function X(n){const t={};for(const e of n)switch(e){case"--yes":case"-y":t.yes=!0;break;case"--commandbar":t.commandbar=!0;break;case"--no-commandbar":t.commandbar=!1;break;case"--agents":t.agents=!0;break;case"--no-agents":t.agents=!1;break}return t}function B(n){switch(n){case"agents":return"agents";default:return"commandbar"}}async function Y(n){const t=n[0]==="setup"?n.slice(1):n,e=X(t),s=e.yes||e.commandbar!==void 0||e.agents!==void 0;s||M();const u=await m();console.log(`tmux-weblink setup
1
+ import{existsSync as p}from"node:fs";import w from"node:path";import{createRequire as b}from"node:module";import{getDataRoot as x,getSettingsPath as E,getThemePath as l}from"./state-paths.js";import{readSettings as m,writeSettings as T}from"./settings.js";import{readActiveTheme as y,setActiveThemeTemplate as _}from"./theme-store.js";import{isThemeTemplateId as v,THEME_TEMPLATE_IDS as A}from"./themes/index.js";import{cmdAdd as S,cmdRemove as k,getPluginDir as R}from"./plugins.js";import{getEnvFilePath as c}from"./load-env.js";import{SETUP_FEATURES as d}from"./setup-features.js";import{promptYesNo as $,promptChoice as N,requireTty as M}from"./setup-prompts.js";const U=x(),I=R(),f=E(),D=b(import.meta.url),{version:P}=D("../../package.json");function X(n){const t={};for(const e of n)switch(e){case"--yes":case"-y":t.yes=!0;break;case"--agents":t.agents=!0;break;case"--no-agents":t.agents=!1;break}return t}function B(n){switch(n){case"agents":return"agents";default:return"agents"}}async function Y(n){const t=n[0]==="setup"?n.slice(1):n,e=X(t),s=e.yes||e.agents!==void 0;s||M();const u=await m();console.log(`tmux-weblink setup
2
2
  `),console.log(`Configure optional features.
3
- `);const g=new Map;for(const o of p){const i=B(o.id);let r;if(e.yes)r=!0;else if(e[i]!==void 0)r=e[i];else{const h=o.isEnabled(u);r=await $(`${o.label} (${o.description})`,h)}g.set(o.id,r)}let a;if(!s){const o=u.terminalRenderer==="ghostty"?"ghostty":"xterm";a=await N("Terminal renderer (xterm.js is the default; ghostty-web is experimental)",["xterm","ghostty"],o)}for(const o of p)g.get(o.id)?await o.enable():await o.disable();if(a){const o=await m();await T({...o,terminalRenderer:a}),console.log(`\u2713 terminal renderer set to ${a}`)}console.log(`
4
- Done. Settings: ${f}`),d(c())&&console.log(`Env file: ${c()}`),console.log("Restart tmux-web to apply UI changes.")}async function z(n){const[t,e]=n;switch(t){case"list":{console.log("Available themes:");for(const s of k)console.log(` ${s}`);return}case"set":{e||(console.error("usage: tmux-web theme set <vscode|ghostty>"),process.exit(1)),v(e)||(console.error(`unknown theme: ${e}`),console.error("Run: tmux-web theme list"),process.exit(1)),await _(e),console.log(`\u2713 theme set to ${e}`),console.log(` ${l()}`),console.log("Restart tmux-web to apply.");return}case"show":{const s=await y();console.log(`Active theme: ${s.template}`),console.log(`Config file: ${l()}`);return}default:console.error("usage: tmux-web theme <list|set|show>"),process.exit(1)}}async function J(){const t=(await m()).plugins??[];if(t.length===0){console.log(`No plugins enabled. Add one with:
5
- tmux-web add <package>`);return}console.log("Enabled plugins:");for(const e of t){const s=d(b.join(I,"node_modules",e));console.log(` ${s?"\u2713":"\u2717"} ${e}${s?"":" (not installed \u2014 run: tmux-web add "+e+")"}`)}}function Q(){console.log(P)}function Z(){const n=U,t=c();console.log(`tmux-web \u2014 terminal-in-the-browser for tmux
3
+ `);const g=new Map;for(const o of d){const i=B(o.id);let r;if(e.yes)r=!0;else if(e[i]!==void 0)r=e[i];else{const h=o.isEnabled(u);r=await $(`${o.label} (${o.description})`,h)}g.set(o.id,r)}let a;if(!s){const o=u.terminalRenderer==="ghostty"?"ghostty":"xterm";a=await N("Terminal renderer (xterm.js is the default; ghostty-web is experimental)",["xterm","ghostty"],o)}for(const o of d)g.get(o.id)?await o.enable():await o.disable();if(a){const o=await m();await T({...o,terminalRenderer:a}),console.log(`\u2713 terminal renderer set to ${a}`)}console.log(`
4
+ Done. Settings: ${f}`),p(c())&&console.log(`Env file: ${c()}`),console.log("Restart tmux-web to apply UI changes.")}async function z(n){const[t,e]=n;switch(t){case"list":{console.log("Available themes:");for(const s of A)console.log(` ${s}`);return}case"set":{e||(console.error("usage: tmux-web theme set <vscode|ghostty>"),process.exit(1)),v(e)||(console.error(`unknown theme: ${e}`),console.error("Run: tmux-web theme list"),process.exit(1)),await _(e),console.log(`\u2713 theme set to ${e}`),console.log(` ${l()}`),console.log("Restart tmux-web to apply.");return}case"show":{const s=await y();console.log(`Active theme: ${s.template}`),console.log(`Config file: ${l()}`);return}default:console.error("usage: tmux-web theme <list|set|show>"),process.exit(1)}}async function J(){const t=(await m()).plugins??[];if(t.length===0){console.log(`No plugins enabled. Add one with:
5
+ tmux-web add <package>`);return}console.log("Enabled plugins:");for(const e of t){const s=p(w.join(I,"node_modules",e));console.log(` ${s?"\u2713":"\u2717"} ${e}${s?"":" (not installed \u2014 run: tmux-web add "+e+")"}`)}}function Q(){console.log(P)}function Z(){const n=U,t=c();console.log(`tmux-web \u2014 terminal-in-the-browser for tmux
6
6
 
7
7
  Usage:
8
8
  tmux-web Start the server (PORT env var, default 3000)
@@ -27,7 +27,7 @@ Usage:
27
27
  /settings/federation to join a hub or to create/revoke agent tokens.
28
28
 
29
29
  Files:
30
- ${f} settings (plugins, commandbar)
30
+ ${f} settings (plugins, agents)
31
31
  ${l()} active theme (shell + terminal colors)
32
32
  ${t} secrets (loaded automatically)
33
33
  ${n}/ plugin installs + runtime state
@@ -38,4 +38,4 @@ Env:
38
38
  TMUX_WEB_TERMINAL_RENDERER=xterm|ghostty (also persistable via /settings)
39
39
  TMUX_WEB_AGENT_HUB / TMUX_WEB_AGENT_TOKEN / TMUX_WEB_AGENT_NAME (agent mode)
40
40
  TMUX_WEB_MAX_AGENTS / TMUX_WEB_MAX_AGENT_CONNS / TMUX_WEB_AGENT_OFFLINE_MS
41
- `)}export{A as cmdAdd,J as cmdList,S as cmdRemove,Y as cmdSetup,z as cmdTheme,Z as printUsage,Q as printVersion};
41
+ `)}export{S as cmdAdd,J as cmdList,k as cmdRemove,Y as cmdSetup,z as cmdTheme,Z as printUsage,Q as printVersion};
@@ -1,5 +1,14 @@
1
- import{cssVarsStyle as l}from"../theme.js";import{escapeHtml as d}from"../html.js";import{commandbarCSS as c,commandbarHTML as f,commandbarScript as p}from"../commandbar.js";import{sharedLayoutCSS as m,sharedHeader as u,sharedSidebar as v,newSessionModalHTML as b,newSessionModalScript as y}from"../shared-layout.js";function S(a,e=!1,r=[],t){const n=t.length>0,o=t.map(i=>`<li>${d(i)}</li>`).join(`
2
- `),s=`
1
+ import{cssVarsStyle as o}from"../theme.js";import{escapeHtml as s}from"../html.js";import{sharedLayoutCSS as i,sharedHeader as l,sharedSidebar as d,newSessionModalHTML as c,newSessionModalScript as f}from"../shared-layout.js";function b(t,e){const a=e.length>0,n=e.map(r=>`<li>${s(r)}</li>`).join(`
2
+ `);return`<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
8
+ <title>Files - tmux-web</title>
9
+ <style>
10
+ ${o(t.shell)}
11
+ ${i(`
3
12
  .files-empty { font-size: var(--text-sm); color: var(--panel-muted); line-height: 1.7; margin-top: 12px; }
4
13
  .files-empty code { background: color-mix(in srgb, var(--panel-accent) 8%, transparent); padding: 3px 7px; border-radius: 6px; font-size: var(--text-xs); }
5
14
  .files-roots { list-style: none; padding: 0; margin: 0; }
@@ -15,31 +24,22 @@ import{cssVarsStyle as l}from"../theme.js";import{escapeHtml as d}from"../html.j
15
24
  .files-roots li:hover { border-color: var(--panel-accent); transform: translateY(-1px); box-shadow: 0 4px 20px color-mix(in srgb, var(--panel-accent) 8%, transparent); }
16
25
  .files-roots li svg { width: 20px; height: 20px; flex-shrink: 0; color: var(--panel-accent); }
17
26
  .files-info { font-size: var(--text-sm); color: var(--panel-muted); margin-bottom: 16px; line-height: 1.6; }
18
- ${e?c():""}`;return`<!DOCTYPE html>
19
- <html lang="en">
20
- <head>
21
- <meta charset="UTF-8" />
22
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
23
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
24
- <title>Files - tmux-web</title>
25
- <style>
26
- ${l(a.shell)}
27
- ${m(s)}
27
+ `)}
28
28
  </style>
29
29
  </head>
30
30
  <body>
31
31
 
32
- ${u({commandbarEnabled:e,title:"Files",themeTemplate:a.template})}
32
+ ${l({title:"Files",themeTemplate:t.template})}
33
33
 
34
34
  <div class="page-wrap">
35
35
  <div class="page-layout">
36
- ${v({activePage:"files",refreshHref:"/files"})}
36
+ ${d({activePage:"files",refreshHref:"/files"})}
37
37
  <main class="main-panel">
38
38
 
39
- ${n?`
39
+ ${a?`
40
40
  <p class="files-info">Browse and edit files from the configured directories below.</p>
41
41
  <div class="files-roots">
42
- ${o}
42
+ ${n}
43
43
  </div>
44
44
  <div id="file-browser" style="display:none">
45
45
  <div id="fb-path" style="font-size:var(--text-sm);font-family:var(--font-mono);color:var(--panel-muted);margin-bottom:12px;word-break:break-all"></div>
@@ -66,11 +66,10 @@ ${u({commandbarEnabled:e,title:"Files",themeTemplate:a.template})}
66
66
  </div>
67
67
  </div>
68
68
 
69
- ${b()}
70
- ${e?f():""}
69
+ ${c()}
71
70
 
72
71
  <script type="module">
73
- ${n?`const ROOTS = ${JSON.stringify(t)};
72
+ ${a?`const ROOTS = ${JSON.stringify(e)};
74
73
 
75
74
  const tree = document.getElementById('fb-tree');
76
75
  const editor = document.getElementById('fb-editor');
@@ -256,8 +255,7 @@ document.querySelectorAll('.files-roots li').forEach(el => {
256
255
  el.addEventListener('click', () => loadDir(el.textContent));
257
256
  });
258
257
  `:""}
259
- ${e?p(r,[]):""}
260
- ${y()}
258
+ ${f()}
261
259
  </script>
262
260
  </body>
263
- </html>`}export{S as renderFilesIndex};
261
+ </html>`}export{b as renderFilesIndex};
@@ -1,10 +1,19 @@
1
- import{cssVarsStyle as u}from"../theme.js";import{commandbarCSS as v,commandbarHTML as w,commandbarScript as b}from"../commandbar.js";import{sharedLayoutCSS as y,sharedHeader as S,sharedSidebar as $,newSessionModalHTML as M,newSessionModalScript as H}from"../shared-layout.js";function l(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function k(t,s){const e=Math.max(0,s-t),i=Math.floor(e/1e3);if(i<45)return"just now";const o=Math.floor(i/60);if(o<60)return o+"m ago";const r=Math.floor(o/60);if(r<24)return r+"h ago";const a=Math.floor(r/24);return a<30?a+"d ago":new Date(t).toLocaleDateString()}function L(t,s,e=!1,i=[],o=new Set){const r=Date.now(),a=t.map(n=>{const f=l(n.windowName||"(unnamed)"),x=l(n.sessionName),g=l(k(n.visitedAt,r)),c=o.has(n.sessionName),d=`
1
+ import{cssVarsStyle as f}from"../theme.js";import{sharedLayoutCSS as g,sharedHeader as x,sharedSidebar as u,newSessionModalHTML as v,newSessionModalScript as b}from"../shared-layout.js";function l(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function w(e,r){const n=Math.max(0,r-e),s=Math.floor(n/1e3);if(s<45)return"just now";const t=Math.floor(s/60);if(t<60)return t+"m ago";const o=Math.floor(t/60);if(o<24)return o+"h ago";const i=Math.floor(o/24);return i<30?i+"d ago":new Date(e).toLocaleDateString()}function k(e,r,n=new Set){const s=Date.now(),t=e.map(a=>{const p=l(a.windowName||"(unnamed)"),h=l(a.sessionName),m=l(w(a.visitedAt,s)),c=n.has(a.sessionName),d=`
2
2
  <div class="hist-main">
3
- <span class="hist-window">${f}</span>
4
- <span class="hist-session">${x}${c?"":' <span class="hist-gone">gone</span>'}</span>
3
+ <span class="hist-window">${p}</span>
4
+ <span class="hist-session">${h}${c?"":' <span class="hist-gone">gone</span>'}</span>
5
5
  </div>
6
- <span class="hist-time">${g}</span>`;return c?`<a class="hist-row" href="/">${d}</a>`:`<div class="hist-row dead" title="Session no longer exists">${d}</div>`}).join(`
7
- `),p=t.length?'<div class="hist-toolbar"><button id="hist-clear" type="button">Clear history</button></div>':"",h=t.length?a:'<p class="empty">No history yet. Sessions and windows you visit will show up here.</p>',m=`
6
+ <span class="hist-time">${m}</span>`;return c?`<a class="hist-row" href="/">${d}</a>`:`<div class="hist-row dead" title="Session no longer exists">${d}</div>`}).join(`
7
+ `),o=e.length?'<div class="hist-toolbar"><button id="hist-clear" type="button">Clear history</button></div>':"",i=e.length?t:'<p class="empty">No history yet. Sessions and windows you visit will show up here.</p>';return`<!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="UTF-8" />
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
12
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
13
+ <title>History - tmux-web</title>
14
+ <style>
15
+ ${f(r.shell)}
16
+ ${g(`
8
17
  .hist-toolbar { display: flex; justify-content: flex-end; margin-bottom: 12px; }
9
18
  .hist-toolbar button {
10
19
  background: none; border: 1px solid var(--panel-border); border-radius: 6px;
@@ -38,34 +47,24 @@ import{cssVarsStyle as u}from"../theme.js";import{commandbarCSS as v,commandbarH
38
47
  .hist-row { flex-direction: column; align-items: flex-start; gap: 8px; }
39
48
  .hist-time { align-self: flex-end; }
40
49
  }
41
- ${e?v():""}`;return`<!DOCTYPE html>
42
- <html lang="en">
43
- <head>
44
- <meta charset="UTF-8" />
45
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
46
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
47
- <title>History - tmux-web</title>
48
- <style>
49
- ${u(s.shell)}
50
- ${y(m)}
50
+ `)}
51
51
  </style>
52
52
  </head>
53
53
  <body>
54
54
 
55
- ${S({commandbarEnabled:e,title:"History",themeTemplate:s.template})}
55
+ ${x({title:"History",themeTemplate:r.template})}
56
56
 
57
57
  <div class="page-wrap">
58
58
  <div class="page-layout">
59
- ${$({activePage:"history",refreshHref:"/history"})}
59
+ ${u({activePage:"history",refreshHref:"/history"})}
60
60
  <main class="main-panel">
61
- ${p}
62
- <div id="hist-list">${h}</div>
61
+ ${o}
62
+ <div id="hist-list">${i}</div>
63
63
  </main>
64
64
  </div>
65
65
  </div>
66
66
 
67
- ${M()}
68
- ${e?w():""}
67
+ ${v()}
69
68
 
70
69
  <script type="module">
71
70
  const clearBtn = document.getElementById('hist-clear');
@@ -78,8 +77,7 @@ if (clearBtn) {
78
77
  } catch { clearBtn.disabled = false; }
79
78
  });
80
79
  }
81
- ${e?b(i,[]):""}
82
- ${H()}
80
+ ${b()}
83
81
  </script>
84
82
  </body>
85
- </html>`}export{L as renderHistoryIndex};
83
+ </html>`}export{k as renderHistoryIndex};
@@ -1,9 +1,9 @@
1
- import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as x,commandbarHTML as f,commandbarScript as h}from"../commandbar.js";import{notesDrawerCSS as u,notesDrawerHTML as w,notesDrawerScript as S}from"../notes-drawer.js";import{sharedLayoutCSS as $,sharedHeader as y,sharedSidebar as _,newSessionModalHTML as k,newSessionModalScript as D}from"../shared-layout.js";function s(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function T(t){const o=new Date(t);return o.toLocaleDateString()+" "+o.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function M(t,o,r=!1,i=[]){const n=[...t].sort((e,a)=>(a.updatedAt??0)-(e.updatedAt??0)),l=n.map(e=>{const a=e.scope==="__global__",d=a?"Global":e.scope.replace(/^session:/,""),m=a?"/notes/__global__":"/notes/"+encodeURIComponent(e.scope.replace(/^session:/,"")),v=s((e.content||"").slice(0,200).trim()||"No content"),g=s(T(e.updatedAt));return`<a class="note-card" href="${m}">
2
- <div class="label ${a?"global":""}">${s(d)}</div>
3
- <div class="preview">${v}</div>
4
- <div class="meta"><span>${g}</span></div>
1
+ import{cssVarsStyle as b}from"../theme.js";import{notesDrawerCSS as v,notesDrawerHTML as g,notesDrawerScript as x}from"../notes-drawer.js";import{sharedLayoutCSS as h,sharedHeader as f,sharedSidebar as u,newSessionModalHTML as w,newSessionModalScript as y}from"../shared-layout.js";function r(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function S(t){const a=new Date(t);return a.toLocaleDateString()+" "+a.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function D(t,a){const n=[...t].sort((e,o)=>(o.updatedAt??0)-(e.updatedAt??0)),s=n.map(e=>{const o=e.scope==="__global__",c=o?"Global":e.scope.replace(/^session:/,""),p=o?"/notes/__global__":"/notes/"+encodeURIComponent(e.scope.replace(/^session:/,"")),d=r((e.content||"").slice(0,200).trim()||"No content"),m=r(S(e.updatedAt));return`<a class="note-card" href="${p}">
2
+ <div class="label ${o?"global":""}">${r(c)}</div>
3
+ <div class="preview">${d}</div>
4
+ <div class="meta"><span>${m}</span></div>
5
5
  </a>`}).join(`
6
- `),c=n.length?l:'<p class="empty">No notes yet.</p>',p=`
6
+ `),l=n.length?s:'<p class="empty">No notes yet.</p>',i=`
7
7
  .note-card {
8
8
  display: block; padding: 16px 18px; border: 1px solid var(--panel-border); border-radius: 12px;
9
9
  margin-bottom: 10px; text-decoration: none; color: var(--page-fg);
@@ -26,8 +26,8 @@ import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as x,commandbarH
26
26
  display: flex; justify-content: space-between;
27
27
  }
28
28
  .empty { font-size: var(--text-sm); color: var(--panel-muted); line-height: 1.6; margin-top: 20px; }
29
- ${r?x():""}
30
- ${u()}`;return`<!DOCTYPE html>
29
+
30
+ ${v()}`;return`<!DOCTYPE html>
31
31
  <html lang="en">
32
32
  <head>
33
33
  <meta charset="UTF-8" />
@@ -35,31 +35,29 @@ import{cssVarsStyle as b}from"../theme.js";import{commandbarCSS as x,commandbarH
35
35
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
36
36
  <title>Notes - tmux-web</title>
37
37
  <style>
38
- ${b(o.shell)}
39
- ${$(p)}
38
+ ${b(a.shell)}
39
+ ${h(i)}
40
40
  </style>
41
41
  </head>
42
42
  <body>
43
43
 
44
- ${y({commandbarEnabled:r,title:"All Notes",themeTemplate:o.template})}
44
+ ${f({title:"All Notes",themeTemplate:a.template})}
45
45
 
46
46
  <div class="page-wrap">
47
47
  <div class="page-layout">
48
- ${_({activePage:"notes",refreshHref:"/notes"})}
48
+ ${u({activePage:"notes",refreshHref:"/notes"})}
49
49
  <main class="main-panel">
50
- <div id="notes-list">${c}</div>
50
+ <div id="notes-list">${l}</div>
51
51
  </main>
52
52
  </div>
53
53
  </div>
54
54
 
55
- ${k()}
56
- ${r?f():""}
57
- ${w("Notes - Global")}
55
+ ${w()}
56
+ ${g("Notes - Global")}
58
57
 
59
58
  <script type="module">
60
- ${S("__global__")}
61
- ${r?h(i,[]):""}
62
- ${D()}
59
+ ${x("__global__")}
60
+ ${y()}
63
61
  </script>
64
62
  </body>
65
- </html>`}export{M as renderNotesIndex};
63
+ </html>`}export{D as renderNotesIndex};