tmux-weblink 2.0.14 → 2.0.15

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # tmux-weblink
2
2
 
3
- Access your tmux sessions from the browser. A lightweight web server that lists running tmux sessions and lets you attach through a full terminal in your browser — with built-in notes, a scheduler, and sidebar extensions.
3
+ Access your tmux sessions from the browser. A lightweight web server that lists running tmux sessions and lets you attach through a full terminal in your browser — with built-in notes and a sidebar.
4
4
 
5
5
  ## Install
6
6
 
@@ -17,7 +17,7 @@ npx tmux-weblink
17
17
  ## Usage
18
18
 
19
19
  ```bash
20
- # Interactive setup (command bar, agents page)
20
+ # Interactive setup (agents page)
21
21
  tmux-weblink setup
22
22
 
23
23
  # Start on default port 21000
@@ -54,11 +54,9 @@ Then open `http://localhost:21000` in your browser. You'll see a list of active
54
54
 
55
55
  - [Documentation hub](docs/index.md)
56
56
  - [Notes](docs/notes.md) — per-session Markdown scratchpad
57
- - [Scheduler](docs/scheduler.md) — delayed `send-keys` tasks
58
57
  - **Windows drawer** — switch tmux windows from the terminal header (mobile-friendly tab picker)
59
- - [Extensions](docs/extensions.md) — install, configure, and build sidebar plugins
60
58
  - [Federation / Agents](docs/agents.md) — join NAT'd machines to a public hub
61
- - [Architecture](docs/architecture.md) — how the server, terminal, and extensions connect
59
+ - [Architecture](docs/architecture.md) — how the server, terminal, and sidebar connect
62
60
 
63
61
  ## Prerequisites
64
62
 
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 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)))}
2
+ import{createRequire as f}from"node:module";import{hostname as p}from"node:os";import{listSessions as l,resolveTmuxSocketPath as u}from"./sessions.js";import{loadSecurityConfig as T}from"./lib/security-config.js";import{TokenStore as d}from"./lib/auth.js";import{RateLimiter as h}from"./lib/rateLimiter.js";import{db as n}from"./lib/db.js";import{handleClientMessage as E}from"./lib/ws-message.js";import{loadDotEnv as _}from"./lib/load-env.js";import{readSettings as g}from"./lib/settings.js";import{readActiveTheme as k}from"./lib/theme-store.js";import{buildApp as A}from"./lib/build-app.js";import{acquireControlClient as N}from"./lib/tmux-control.js";import{getSessionPaneTarget as b,capturePaneTail as C,capturePaneHistoryChunk as S,toCrlf as w}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as B}from"./lib/terminal-config.js";import{startAgentClient as M}from"./lib/agent-client.js";_();const U=f(import.meta.url),{version:W}=U("../package.json");function G(o){let r=process.env.TMUX_WEB_AGENT_HUB||"",i=process.env.TMUX_WEB_AGENT_TOKEN||"",s=process.env.TMUX_WEB_AGENT_NAME||p();for(let t=0;t<o.length;t++){const e=o[t];if(e!=="agent"){if((e==="--hub"||e==="-h")&&o[t+1]){r=o[++t];continue}if((e==="--token"||e==="-t")&&o[t+1]){i=o[++t];continue}if((e==="--name"||e==="-n")&&o[t+1]){s=o[++t];continue}e.startsWith("--hub=")&&(r=e.slice(6)),e.startsWith("--token=")&&(i=e.slice(8)),e.startsWith("--name=")&&(s=e.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)),i||(console.error("agent token required (--token or TMUX_WEB_AGENT_TOKEN)"),process.exit(1)),{hub:r,token:i,name:s.slice(0,64)}}const{hub:P,token:x,name:O}=G(process.argv.slice(2)),a=B(),R=T(),X=new d,q=new h;await n.read(),n.data.sessionAccess??=[],n.data.pinnedViews??=[],n.data.watchedPanes??=[],n.data.quickCommands??=[];const m=await g(),y=await k(),H=A({mode:"agent",securityConfig:R,tokenStore:X,rateLimiter:q,settings:m,terminalRenderer:m.terminalRenderer==="ghostty"?"ghostty":"xterm",terminalBufferConfig:a,getAgentId:()=>null,state:{activeTheme:y,settingUpPassword:!1}}),I=M({hub:P,token:x,name:O,version:W,app:H,listSessions:l,terminalBufferConfig:a,getSessionPaneTarget:b,capturePaneTail:C,capturePaneHistoryChunk:S,toCrlf:w,handleClientMessage:E,acquireControlClient:N,tmuxSocketPath:u()??void 0});function c(){I.stop(),process.exit(0)}process.on("SIGINT",c),process.on("SIGTERM",c);
package/dist/frontend.js CHANGED
@@ -1 +1 @@
1
- import{renderLoginPage as o}from"./lib/pages/login.js";import{renderNotesIndex as d}from"./lib/pages/notes-index.js";import{renderNotesPage as x}from"./lib/pages/notes-page.js";import{renderSettings as f,renderThemeSettings as p}from"./lib/pages/settings.js";import{renderScheduleIndex as g}from"./lib/pages/schedule-index.js";import{renderHistoryIndex as a}from"./lib/pages/history-index.js";import{renderQuickCommandsPage as I}from"./lib/pages/quick-commands.js";import{renderFilesIndex as h}from"./lib/pages/files-index.js";import{renderShell as c}from"./lib/pages/shell.js";export{h as renderFilesIndex,a as renderHistoryIndex,o as renderLoginPage,d as renderNotesIndex,x as renderNotesPage,I as renderQuickCommandsPage,g as renderScheduleIndex,f as renderSettings,c as renderShell,p as renderThemeSettings};
1
+ import{renderLoginPage as o}from"./lib/pages/login.js";import{renderNotesIndex as t}from"./lib/pages/notes-index.js";import{renderNotesPage as m}from"./lib/pages/notes-page.js";import{renderSettings as f,renderThemeSettings as p}from"./lib/pages/settings.js";import{renderHistoryIndex as g}from"./lib/pages/history-index.js";import{renderQuickCommandsPage as a}from"./lib/pages/quick-commands.js";import{renderFilesIndex as I}from"./lib/pages/files-index.js";import{renderShell as S}from"./lib/pages/shell.js";export{I as renderFilesIndex,g as renderHistoryIndex,o as renderLoginPage,t as renderNotesIndex,m as renderNotesPage,a as renderQuickCommandsPage,f as renderSettings,S as renderShell,p as renderThemeSettings};
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
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
2
+ import{chmodSync as le,existsSync as D,readdirSync as ue,statSync as de}from"node:fs";import{createRequire as J}from"node:module";import{hostname as L}from"node:os";const me=J(import.meta.url),{version:fe}=me("../package.json");import O from"node:path";import{fileURLToPath as pe}from"node:url";import{serve as he}from"@hono/node-server";import{WebSocketServer as ge,WebSocket as H}from"ws";try{const e=O.dirname(J(import.meta.url).resolve("node-pty/package.json")),n=O.join(e,"prebuilds");if(D(n))for(const r of ue(n)){const t=O.join(n,r,"spawn-helper");D(t)&&!(de(t).mode&73)&&le(t,493)}}catch{}import{loadSecurityConfig as ye}from"./lib/security-config.js";import{TokenStore as ke,hashPassword as _e,verifyPassword as Te,validatePassword as ve}from"./lib/auth.js";import{RateLimiter as Ae}from"./lib/rateLimiter.js";import{audit as l}from"./lib/auditLog.js";import{db as P}from"./lib/db.js";import{handleClientMessage as G}from"./lib/ws-message.js";import{loadDotEnv as Ce}from"./lib/load-env.js";import{listSessions as Se,resolveTmuxSocketPath as we}from"./sessions.js";import{cmdSetup as xe,cmdTheme as Re,printUsage as z,printVersion as be}from"./lib/cli.js";import{readSettings as Ie}from"./lib/settings.js";import{readActiveTheme as Me}from"./lib/theme-store.js";import{buildApp as V}from"./lib/build-app.js";import{attachTerminal as Oe,killAllAttachedPtys as Pe}from"./lib/attach-terminal.js";import{acquireControlClient as X,killAllControlClients as Ee}from"./lib/tmux-control.js";import{getSessionPaneTarget as Y,capturePaneTail as K,capturePaneHistoryChunk as Q,toCrlf as Z}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Le}from"./lib/terminal-config.js";import{AgentRegistry as $e}from"./lib/agent-registry.js";import{AgentChannel as Ne,MAX_RELAY_CONNS_PER_AGENT as Ue}from"./lib/agent-relay.js";import{findAgentTokenByPlaintext as Fe,listAgentTokens as He}from"./lib/agent-tokens.js";import{saveSecurityConfig as je}from"./lib/security-config.js";import{readFederationConfig as Be,writeFederationConfig as We}from"./lib/federation-config.js";import{startAgentClient as De}from"./lib/agent-client.js";Ce();const $=Le(),h=ye(),y=new ke,A=new Ae;let j=!1;const Je="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*)${Je}=([^;]+)`));return t?decodeURIComponent(t[1]):null}function Lt(e){const n=W(e);return n?y.validateToken(n):null}const I=new Map;function Ge(e){let n=0;for(const r of I.values())r.ip===e&&n++;return n}function S(e,n,r){try{e.close(n,r)}catch{}}function q(e,n){e.readyState===H.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"setup":await xe(e),process.exit(0);case"theme":await Re(e.slice(1)),process.exit(0);case"agent":{const t=O.join(O.dirname(pe(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 rt(e.slice(1)),process.exit(0);case"help":case"--help":case"-h":z(),process.exit(0);case"-V":case"--version":case"-v":be(),process.exit(0);default:console.error(`unknown argument: ${n}`),z(),process.exit(1)}}}function R(e,n){e.readyState===H.OPEN&&e.send(JSON.stringify(n))}const ee=new Set;await P.read(),P.data.sessionAccess??=[],P.data.pinnedViews??=[],P.data.watchedPanes??=[],P.data.quickCommands??=[];const U=await Ie();let ze=await Me();const g=new $e({onChange:()=>{}}),T=new Map;function te(e){return g.get(e)?T.get(e)??null:null}const ne={activeTheme:ze,settingUpPassword:!1},Ve=V({mode:"hub",securityConfig:h,tokenStore:y,rateLimiter:A,settings:U,terminalRenderer:ae(N,U.terminalRenderer),terminalBufferConfig:$,state:ne,federation:{getConfig:()=>x,setConfig:async e=>{x=e,await We(e),re(e)},status:Ye,hostname:L()},hub:{agents:g,getChannel:te}}),Xe=V({mode:"agent",securityConfig:h,tokenStore:y,rateLimiter:A,settings:U,terminalRenderer:ae(N,U.terminalRenderer),terminalBufferConfig:$,state:ne});let x=await Be(),M=null;function oe(){M&&(M.stop(),M=null)}function re(e){oe(),!(!e.enabled||!e.hub||!e.token)&&(M=De({hub:e.hub,token:e.token,name:e.name?.trim()||L(),version:fe,app:Xe,listSessions:Se,terminalBufferConfig:$,getSessionPaneTarget:Y,capturePaneTail:K,capturePaneHistoryChunk:Q,toCrlf:Z,handleClientMessage:G,acquireControlClient:X}))}function Ye(){return x.enabled?M?M.status():{state:"idle",agentId:null,hub:x.hub??"",name:x.name??L()}:{state:"idle",agentId:null,hub:x.hub??"",name:x.name??L()}}re(x);const Ke=parseInt(process.env.PORT||"21000",10),Qe=he({fetch:Ve.fetch,port:Ke,hostname:"0.0.0.0"},e=>{console.log(`tmux-web running at http://${e.port}`)}),E=new ge({noServer:!0});function b(e,n,r){e.write(`HTTP/1.1 ${n} ${r}\r
3
3
  \r
4
- `),e.destroy()}ut.on("upgrade",(e,n,r)=>{const t=new URL(e.url||"/",`http://${e.headers.host}`),u=t.pathname;if(u==="/agent/ws"){pt(e,n,r);return}const s=u.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(s){dt(e,n,r,decodeURIComponent(s[1]),decodeURIComponent(s[2]));return}const o=u.match(/^\/ws\/(.+)$/);if(!o){n.destroy();return}const f=B(e),i=e.headers.origin,d=h.security.allowedOrigins;if(i&&!(i===`http://${e.headers.host}`||i===`https://${e.headers.host}`)&&(d.length===0||!d.includes(i))){l("ws_rejected_origin",{ip:f,origin:i}),M(n,403,"Origin not allowed");return}const k=nt(f);if(k>=h.security.maxConnectionsPerIp){l("ws_rejected_per_ip_cap",{ip:f,liveFromIp:k}),M(n,429,"Too many connections");return}const p=W(e)||t.searchParams.get("token")||"",w=p?y.validateToken(p):null;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
4
+ `),e.destroy()}Qe.on("upgrade",(e,n,r)=>{const t=new URL(e.url||"/",`http://${e.headers.host}`),u=t.pathname;if(u==="/agent/ws"){tt(e,n,r);return}const s=u.match(/^\/ws\/a\/([^/]+)\/(.+)$/);if(s){Ze(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}),b(n,403,"Origin not allowed");return}const k=Ge(f);if(k>=h.security.maxConnectionsPerIp){l("ws_rejected_per_ip_cap",{ip:f,liveFromIp:k}),b(n,429,"Too many connections");return}const p=W(e)||t.searchParams.get("token")||"",v=p?y.validateToken(p):null;E.handleUpgrade(e,n,r,a=>{E.emit("connection",a,e,decodeURIComponent(o[1]),f,v)})}),E.on("connection",(e,n,r,t,u)=>{const s={ws:e,ip:t,authenticated:!1,authTimeout:null};I.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=Oe(r,{send:a=>R(e,a),onMessage:a=>{f=a},onClose:a=>{p=a}},{terminalBufferConfig:$,getSessionPaneTarget:Y,capturePaneTail:K,capturePaneHistoryChunk:Q,toCrlf:Z,handleClientMessage:G,acquireControlClient:X,tmuxSocketPath:we()??void 0,onPtyExit:(a,c)=>S(e,1e3,"pty exited"),onSpawnError:(a,c)=>S(e,1011,"pty spawn failed")}))}let p=null;async function v(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 w=A.recordFailure(t);return w.permanentLock&&(y.revokeAll(),l("permanent_lock",{ip:t,failures:w.failures})),d("Invalid or expired token",{permanentLock:w.permanentLock,retryAfterMs:w.retryAfterMs}),!0}return y.touch(m.tokenHash),i("token",m.name),!0}if(c.type==="auth"&&typeof c.password=="string"){const m=A.check(t);if(!m.allowed)return l("rate_limited",{ip:t,retryAfterMs:m.retryAfterMs,permanentLock:m.permanentLock}),d("Too many attempts",{permanentLock:m.permanentLock,retryAfterMs:m.retryAfterMs}),!0;if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!ot(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 C=ve(c.password);if(C)return d(C),!0;j=!0;try{h.passwordHash=await _e(c.password),je(h),l("password_set",{ip:t})}finally{j=!1}const{stored:F,plaintext:ce}=y.createAccessToken("setup",h.security.tokenTtlDays);return R(e,{type:"auth.ok",setupMode:!0,token:ce}),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 Te(c.password,h.passwordHash)){const C=A.recordFailure(t);return l("auth_failed",{ip:t,method:"password",failures:C.failures,permanentLock:C.permanentLock}),C.permanentLock&&(y.revokeAll(),l("permanent_lock",{ip:t,failures:C.failures})),d("Incorrect password",{permanentLock:C.permanentLock,retryAfterMs:C.retryAfterMs}),!0}A.recordSuccess(t);const _=`ws-${t}`,{plaintext:ie}=y.createAccessToken(_,h.security.tokenTtlDays);return i("password",_),R(e,{type:"auth.ok",setupMode:!1,token:ie}),!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"),S(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 v(c)||d("Authentication required");return}if(f)try{f(c)}catch{}}),e.on("close",()=>{s.authTimeout&&clearTimeout(s.authTimeout),I.delete(e),l("ws_disconnected",{ip:t}),p?.(),o&&(o.dispose(),o=null)}),e.on("error",()=>{s.authTimeout&&clearTimeout(s.authTimeout),I.delete(e),p?.(),o&&(o.dispose(),o=null)})});function Ze(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}),b(n,401,"Unauthorized");return}const d=te(t);if(!d){l("ws_rejected_remote_offline",{ip:s,agentId:t}),b(n,503,"Agent offline");return}if(d.relayCount>=Ue){b(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))){b(n,403,"Origin not allowed");return}const v=et();E.handleUpgrade(e,n,r,a=>{if(d.registerRelay(v,u,a),g.bumpRelayConns(t,1),!d.attach(v,u)){a.send(JSON.stringify({type:"data",data:`\r
5
5
  \x1B[31magent offline\x1B[0m\r
6
- `})),a.close(1011,"agent offline"),d.relayClosed(w),g.bumpRelayConns(t,-1);return}a.on("message",m=>{const v=typeof m=="string"?m:m.toString("utf-8");let _;try{_=JSON.parse(v)}catch{return}(_.type==="input"||_.type==="resize"||_.type==="load_history")&&d.sendToAgent(w,_)});const c=()=>{d.relayClosed(w),g.bumpRelayConns(t,-1)};a.on("close",c),a.on("error",c)})}let mt=1;function ft(){return mt++}function pt(e,n,r){const t=B(e),u=e.headers.origin;if(u){l("agent_rejected_origin",{ip:t,origin:u}),M(n,403,"Origin not allowed");return}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(`
6
+ `})),a.close(1011,"agent offline"),d.relayClosed(v),g.bumpRelayConns(t,-1);return}a.on("message",m=>{const w=typeof m=="string"?m:m.toString("utf-8");let _;try{_=JSON.parse(w)}catch{return}(_.type==="input"||_.type==="resize"||_.type==="load_history")&&d.sendToAgent(v,_)});const c=()=>{d.relayClosed(v),g.bumpRelayConns(t,-1)};a.on("close",c),a.on("error",c)})}let qe=1;function et(){return qe++}function tt(e,n,r){const t=B(e),u=e.headers.origin;if(u){l("agent_rejected_origin",{ip:t,origin:u}),b(n,403,"Origin not allowed");return}E.handleUpgrade(e,n,r,s=>{let o=null,f=setTimeout(()=>{l("agent_hello_timeout",{ip:t}),S(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{S(s,1008,"bad hello");return}if(p.type!=="hello"||typeof p.token!="string"){S(s,1008,"expected hello");return}if(!A.check(t).allowed){l("agent_rate_limited",{ip:t}),S(s,429,"Too many attempts");return}const a=Fe(p.token);if(!a){A.recordFailure(t),l("agent_bad_token",{ip:t}),q(s,{type:"hello_err",reason:"invalid token"}),S(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}),q(s,{type:"hello_err",reason:"too many agents"}),S(s,4004,"too many agents");return}clearTimeout(f),A.recordSuccess(t),l("agent_connected",{ip:t,agentId:m,name:c}),o=new Ne(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 nt=parseInt(process.env.TMUX_WEB_AGENT_OFFLINE_MS||"45000",10);setInterval(()=>{const e=g.pruneOffline(nt);for(const r of e){const t=g.peek(r);t&&t.ws.readyState===H.CLOSED&&(g.unregister(r),T.get(r)?.dispose(),T.delete(r))}const n=new Set(He().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 se(){oe(),Ee(),Pe();for(const e of T.values())e.dispose();T.clear();for(const[e,n]of I){try{e.close(1001,"Server shutting down")}catch{}n.authTimeout&&clearTimeout(n.authTimeout)}I.clear();for(const e of ee)try{e.kill()}catch{}ee.clear(),A.dispose(),process.exit(0)}process.on("SIGINT",se),process.on("SIGTERM",se);function ot(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function ae(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 rt(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 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">
1
+ import{existsSync as L,readdirSync as ae,statSync as A,unlinkSync as de,writeFileSync as ue,mkdirSync as ce,readFileSync as G}from"node:fs";import{readFile as le}from"node:fs/promises";import{execSync as _,execFileSync as z}from"node:child_process";import p from"node:path";import{fileURLToPath as me}from"node:url";import{Hono as fe}from"hono";import{listSessions as D}from"../sessions.js";import{renderLoginPage as pe,renderNotesIndex as he,renderNotesPage as ge,renderSettings as ye,renderThemeSettings as je,renderHistoryIndex as we,renderQuickCommandsPage as ke,renderFilesIndex as ve,renderShell as qe}from"../frontend.js";import{renderFederationPage as Se}from"./pages/federation.js";import{createAgentToken as Te,removeAgentToken as Ae,listAgentTokens as B}from"./agent-tokens.js";import{hashPassword as be,verifyPassword as xe,validatePassword as _e}from"./auth.js";import{atomicWriteFileSync as Ce}from"./atomicWrite.js";import{resolveFsPath as b,resolveFsRoots as V,MAX_FILE_BYTES as Q,walkRecursive as Oe}from"./fs-access.js";import{captureSessionWindowsWithPath as Ee}from"./tmux-windows.js";import{audit as j}from"./auditLog.js";import{db as S}from"./db.js";import{getSessionAccessMap as Ie}from"./session-access.js";import{listWindowHistory as Re,clearWindowHistory as Ne}from"./window-history.js";import{saveSecurityConfig as Pe}from"./security-config.js";import{readSettings as K,writeSettings as Le}from"./settings.js";import{setActiveThemeTemplate as X}from"./theme-store.js";import{isThemeTemplateId as Y,THEME_TEMPLATE_IDS as De}from"./themes/index.js";import{pinView as Fe,unpinView as He,listPinnedViews as Ue}from"./pinned-views.js";import{getStoredWindows as Me}from"./session-windows.js";import{buildSidebarSessions as We}from"./sessions-sidebar.js";import{createQuickCommand as $e,deleteQuickCommand as Ge,listQuickCommands as Z,updateQuickCommand as ze}from"./quick-commands.js";import{getSystemStatus as Be,getTopProcesses as Ve,killProcess as Qe}from"./system-monitor.js";import{newTmuxSession as Ke,renameSession as Xe,killSession as Ye,TmuxWindowsError as Ze}from"./tmux-windows.js";import{registerAgentTunnel as Je}from"./agent-tunnel.js";import{injectScopeScript as et}from"./scope-script.js";const F="tmux-web-token",tt=365;function rt(x){return x==="127.0.0.1"||x==="::1"||x==="::ffff:127.0.0.1"}function Pt(x){const{mode:J,securityConfig:h,tokenStore:g,rateLimiter:O,settings:nt,terminalRenderer:E,terminalBufferConfig:I,getAgentId:H,state:u,federation:w,hub:q}=x,k=J==="agent";function C(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 R(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*)${F}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function st(e){const t=R(e);return t?g.validateToken(t):null}function U(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${F}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${tt*86400}${r?"; Secure":""}`)}function ee(e){e.header("Set-Cookie",`${F}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}const o=()=>k?async(e,t)=>t():async(e,t)=>{const r=R(e);return!r||!g.validateToken(r)?(j("http_unauthorized",{ip:C(e)}),e.json({error:"unauthorized"},401)):t()},y=()=>k?async(e,t)=>t():async(e,t)=>{const r=R(e);if(!r||!g.validateToken(r)){const n=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${n}`,302)}return t()};function N(e){const t=We(D(),Ie(),Ue()),r=[];if(q)for(const n of q.agents.list()){const i=q.agents.get(n.agentId);r.push({agentId:n.agentId,agentName:n.name,online:n.online,sessions:i?i.sessions:[]})}return{...t,currentSession:e??null,agents:r}}function M(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 fe;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(et(n,{prefix:"",agentId:null}),e.res)});const W=p.dirname(me(import.meta.url)),te=[p.join(W,"..","assets"),p.join(W,"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 te){const n=p.join(r,t);if(!L(n))continue;const i=await le(n),a=p.extname(t),c={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(i,200,{"Content-Type":c[a]??"application/octet-stream","Cache-Control":a===".js"||a===".css"?"no-cache":"public, max-age=3600"})}return e.notFound()});const re=`<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>`,$=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
5
+ </svg>`,$=e=>e.body(re,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});s.get("/favicon.svg",$),s.get("/favicon.ico",$);const ne={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(ne,200,{"Cache-Control":"public, max-age=3600"}));const se=`// 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(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};
16
+ `;s.get("/sw.js",e=>e.body(se,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(pe({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:u.activeTheme}))}),s.get("/",y(),e=>{const t=V();return e.html(qe({theme:u.activeTheme,fsRoots:t,terminalCfg:I,renderer:E,scrollback:I.initialLines+2*I.historyChunk,wsBase:k&&H?.()?`/ws/a/${H()}`:void 0}))}),s.get("/notes",y(),e=>e.html(he(S.data.notes,u.activeTheme))),s.get("/notes/:session",y(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.html(ge(t,u.activeTheme))}),s.get("/history",y(),e=>{const t=D(),r=new Set(t.map(n=>n.name));return e.html(we(Re(),u.activeTheme,r))}),s.get("/quick-commands",y(),e=>e.html(ke(Z(),u.activeTheme))),s.get("/files",y(),e=>{const t=V();return e.html(ve(u.activeTheme,t))}),s.post("/api/history/clear",o(),async e=>(await Ne(),e.json({ok:!0}))),s.get("/api/quick-commands",o(),e=>e.json(Z())),s.post("/api/quick-commands",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await $e(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),s.patch("/api/quick-commands/:id",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await ze(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",o(),e=>Ge(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=C(e),r=O.check(t);if(!r.allowed)return j("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 i=typeof n.password=="string"?n.password:"";if(!h.passwordHash){if(!h.security.allowRemoteSetup&&!rt(t))return j("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(u.settingUpPassword)return e.json({error:"Password setup in progress"},409);const l=_e(i);if(l)return e.json({error:l},400);u.settingUpPassword=!0;try{h.passwordHash=await be(i),Pe(h),j("password_set",{ip:t})}finally{u.settingUpPassword=!1}const{plaintext:d}=g.createAccessToken("setup",h.security.tokenTtlDays);return U(e,d),e.json({ok:!0,token:d,setupMode:!0})}if(!await xe(i,h.passwordHash)){const l=O.recordFailure(t);return j("auth_failed",{ip:t,method:"password",failures:l.failures,permanentLock:l.permanentLock}),l.permanentLock?(g.revokeAll(),j("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 c=`browser-${t}`,{plaintext:v}=g.createAccessToken(c,h.security.tokenTtlDays);return U(e,v),j("auth_success",{ip:t,method:"password",tokenName:c}),e.json({ok:!0,token:v})}),s.post("/api/auth/token",o(),async e=>{if(k)return e.json({error:"hub only"},403);const t=C(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}`,i=typeof r.ttlDays=="number"?r.ttlDays:h.security.tokenTtlDays,{stored:a,plaintext:c}=g.createAccessToken(n,i);return j("token_created",{ip:t,name:a.name,tokenId:a.id}),e.json({id:a.id,name:a.name,token:c,expiresAt:a.expiresAt})}),s.get("/api/auth/tokens",o(),e=>k?e.json({error:"hub only"},403):e.json(g.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),s.delete("/api/auth/tokens/:id",o(),e=>k?e.json({error:"hub only"},403):g.revoke(e.req.param("id"))?(j("token_revoked",{ip:C(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),s.post("/api/auth/logout",o(),e=>(ee(e),e.json({ok:!0}))),s.get("/settings",y(),async e=>{const t=await K(),r=t.terminalRenderer??"xterm";return e.html(ye({settings:t,renderer:E,rendererOverridden:E!==r,theme:u.activeTheme,saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),s.post("/settings",o(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await K(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",i=t.defaultView==="recent"?"recent":"default";return await Le({...r,terminalRenderer:n,defaultView:i}),e.redirect("/settings?saved=1",303)}),s.get("/settings/theme",y(),e=>e.html(je({theme:u.activeTheme,saved:e.req.query("saved")==="1"}))),s.post("/settings/theme",o(),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"||!Y(r)?e.redirect("/settings/theme",303):(u.activeTheme=await X(r),e.redirect("/settings/theme?saved=1",303))});const oe={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};s.get("/api/theme",o(),e=>e.json({active:u.activeTheme.template,templates:De.map(t=>({id:t,name:oe[t]}))})),s.post("/api/theme",o(),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"||!Y(r)?e.json({error:"invalid theme template"},400):(u.activeTheme=await X(r),e.json({ok:!0,active:u.activeTheme.template}))}),s.get("/api/system/status",o(),e=>e.json(Be())),s.get("/api/system/processes",o(),e=>e.json(Ve())),w&&(s.get("/settings/federation",y(),async e=>e.html(Se({config:w.getConfig(),status:w.status(),tokens:B().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})),hostname:w.hostname},u.activeTheme))),s.get("/api/federation",o(),e=>e.json({config:w.getConfig(),status:w.status()})),s.post("/api/federation",o(),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():"",i=typeof t.name=="string"?t.name.trim().slice(0,64):"",a=t.enabled===!0;if(a){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 c={hub:r||void 0,token:n||void 0,name:i||void 0,enabled:a};return await w.setConfig(c),e.json({ok:!0,status:w.status()})}),s.get("/api/agent-tokens",o(),e=>e.json(B().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt})))),s.post("/api/agent-tokens",o(),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:i}=Te(r);return e.json({ok:!0,id:n,name:r,token:i})}),s.delete("/api/agent-tokens/:id",o(),e=>{const t=e.req.param("id");return Ae(t)?e.json({ok:!0}):e.json({error:"no such token"},404)})),s.post("/api/system/kill",o(),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=Qe(t);return r.ok?e.json({ok:!0}):e.json({error:r.error},500)}),s.get("/api/agents",o(),e=>q?e.json(q.agents.list()):e.json([])),s.post("/api/sessions/new",o(),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(D().some(a=>a.name===r))return e.json({error:"session already exists"},409);try{return Ke(r,n),e.json({ok:!0})}catch(a){const c=a instanceof Ze?a.message:"failed to create session";return e.json({error:c},500)}}),s.post("/api/sessions/rename",o(),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)?(Xe(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",o(),async e=>{try{const{name:t}=await e.req.json();return t?(Ye(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",o(),e=>{const t=e.req.query("session");if(!t)return e.json({error:"session is required"},400);try{const r=Ee(t),i=r.find(a=>a.active)?.path??r[0]?.path??process.env.HOME??"/";return e.json({path:i})}catch{return e.json({path:process.env.HOME??"/"})}}),s.get("/api/fs/list",o(),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 i=r,a="",c=r.endsWith("/");if(!c)try{c=A(r).isDirectory()}catch{}c||(i=p.dirname(r),a=p.basename(r).toLowerCase());try{const v=ae(i),l=[],d=[];for(const m of v)if(!m.startsWith(".")&&!(a&&!m.toLowerCase().startsWith(a))){try{const f=p.join(i,m);A(f).isDirectory()?(l.push(f),n&&Oe(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 ie(e){const t={repoRoot:null,branch:null,files:[],linesAdded:0,linesRemoved:0};try{const r=_("git rev-parse --show-toplevel 2>/dev/null",{cwd:e,encoding:"utf-8",timeout:3e3}).trim();if(!r)return t;let n;try{n=_("git branch --show-current 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}catch{n=_("git rev-parse --short HEAD 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3}).trim()}n||(n="HEAD");let i="";try{i=_("git status --porcelain 2>/dev/null",{cwd:r,encoding:"utf-8",timeout:3e3})}catch{}const a=[];for(const d of i.split(`
17
+ `)){if(!d.trim())continue;const m=d[0],f=d[1],T=d.substring(3).trim(),P=f!==" "?f:m;a.push({path:T,status:P,staged:m,unstaged:f,additions:0,deletions:0})}const c=new Map;try{const d=_("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 T=parseInt(f[0],10)||0,P=parseInt(f[1],10)||0;c.set(f[2],{added:T,deleted:P})}}}catch{}for(const d of a){const m=c.get(d.path);m&&(d.additions=m.added,d.deletions=m.deleted)}let v=0,l=0;for(const d of a)v+=d.additions,l+=d.deletions;for(const d of a)if(d.status==="?"&&d.additions===0)try{const m=p.join(r,d.path),T=G(m,"utf-8").split(`
19
+ `).length;d.additions=T,v+=T}catch{}return{repoRoot:r,branch:n,files:a,linesAdded:v,linesRemoved:l}}catch{return t}}return s.get("/api/git/status",o(),e=>{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);try{const r=b(t),n=ie(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",o(),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=b(t);if(!L(n)||!A(n).isDirectory())return e.json({error:"invalid repository path: "+n},400);try{const i=z("/usr/bin/git",["diff","HEAD","--",r],{cwd:n,encoding:"utf-8",timeout:5e3}),a=z("/usr/bin/git",["diff","--cached","--",r],{cwd:n,encoding:"utf-8",timeout:5e3});return e.json({diff:i,stagedDiff:a})}catch(i){throw console.error("[git/diff] execFileSync failed:",i?.code,i.message),i}}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",o(),e=>{try{const t=e.req.query("path");if(!t)return e.json({error:"path is required"},400);const r=b(t);if(!A(r).isFile())return e.json({error:"not a file"},400);const n=A(r).size;if(n>Q)return e.json({error:"file too large",size:n,maxBytes:Q},413);const i=G(r,"utf-8");return e.json({path:r,content:i,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",o(),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=b(t.path);return Ce(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",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=b(t.path);return A(r).isFile()?(de(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",o(),async e=>{try{const t=await e.req.json();if(!t.path)return e.json({error:"path is required"},400);const r=b(t.path);return L(r)?e.json({error:"file already exists"},409):(ce(p.dirname(r),{recursive:!0}),ue(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",o(),e=>{const t=e.req.query("currentSession");return e.json(N(typeof t=="string"&&t?t:void 0))}),s.get("/api/sidebar/session-windows/:session",o(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(Me(t))}),s.post("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=M(t);return"error"in r?e.json({error:r.error},400):(await Fe(r.sessionName,r.windowIndex),e.json(N()))}),s.delete("/api/pinned-views",o(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=M(t);return"error"in r?e.json({error:r.error},400):(await He(r.sessionName,r.windowIndex),e.json(N()))}),s.get("/api/notes",o(),e=>{const t=[...S.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),s.get("/api/notes/:scope",o(),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",o(),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()},i=S.data.notes.findIndex(a=>a.scope===t);return i>=0?S.data.notes[i]=n:S.data.notes.push(n),await S.write(),e.json({ok:!0})}),q&&(s.use("/a/:agentId/*",o()),Je(s,{getChannel:q.getChannel})),s}export{Pt as buildApp};
package/dist/lib/cli.js CHANGED
@@ -1,8 +1,7 @@
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
1
+ import{existsSync as w}from"node:fs";import{createRequire as b}from"node:module";import{getDataRoot as d,getSettingsPath as x,getThemePath as l}from"./state-paths.js";import{readSettings as g,writeSettings as E}from"./settings.js";import{readActiveTheme as T,setActiveThemeTemplate as y}from"./theme-store.js";import{isThemeTemplateId as _,THEME_TEMPLATE_IDS as v}from"./themes/index.js";import{getEnvFilePath as m}from"./load-env.js";import{SETUP_FEATURES as h}from"./setup-features.js";import{promptYesNo as A,promptChoice as S,requireTty as k}from"./setup-prompts.js";const M=d(),f=x(),R=b(import.meta.url),{version:N}=R("../../package.json");function $(o){const s={};for(const e of o)switch(e){case"--yes":case"-y":s.yes=!0;break;case"--agents":s.agents=!0;break;case"--no-agents":s.agents=!1;break}return s}function U(o){switch(o){case"agents":return"agents";default:return"agents"}}async function F(o){const s=o[0]==="setup"?o.slice(1):o,e=$(s),n=e.yes||e.agents!==void 0;n||k();const c=await g();console.log(`tmux-weblink setup
2
2
  `),console.log(`Configure optional features.
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
3
+ `);const u=new Map;for(const t of h){const i=U(t.id);let a;if(e.yes)a=!0;else if(e[i]!==void 0)a=e[i];else{const p=t.isEnabled(c);a=await A(`${t.label} (${t.description})`,p)}u.set(t.id,a)}let r;if(!n){const t=c.terminalRenderer==="ghostty"?"ghostty":"xterm";r=await S("Terminal renderer (xterm.js is the default; ghostty-web is experimental)",["xterm","ghostty"],t)}for(const t of h)u.get(t.id)?await t.enable():await t.disable();if(r){const t=await g();await E({...t,terminalRenderer:r}),console.log(`\u2713 terminal renderer set to ${r}`)}console.log(`
4
+ Done. Settings: ${f}`),w(m())&&console.log(`Env file: ${m()}`),console.log("Restart tmux-web to apply UI changes.")}async function L(o){const[s,e]=o;switch(s){case"list":{console.log("Available themes:");for(const n of v)console.log(` ${n}`);return}case"set":{e||(console.error("usage: tmux-web theme set <vscode|ghostty>"),process.exit(1)),_(e)||(console.error(`unknown theme: ${e}`),console.error("Run: tmux-web theme list"),process.exit(1)),await y(e),console.log(`\u2713 theme set to ${e}`),console.log(` ${l()}`),console.log("Restart tmux-web to apply.");return}case"show":{const n=await T();console.log(`Active theme: ${n.template}`),console.log(`Config file: ${l()}`);return}default:console.error("usage: tmux-web theme <list|set|show>"),process.exit(1)}}function j(){console.log(N)}function q(){const o=M,s=m();console.log(`tmux-web \u2014 terminal-in-the-browser for tmux
6
5
 
7
6
  Usage:
8
7
  tmux-web Start the server (PORT env var, default 3000)
@@ -12,9 +11,6 @@ Usage:
12
11
  tmux-web -h, --help Show this help
13
12
  tmux-web setup Interactive feature setup
14
13
  tmux-web setup --yes Enable all features without prompts
15
- tmux-web add <package> Install a plugin and enable it
16
- tmux-web remove <package> Uninstall a plugin and disable it
17
- tmux-web list Show enabled plugins
18
14
  tmux-web theme list List available themes
19
15
  tmux-web theme set <name> Set active theme (vscode, ghostty)
20
16
  tmux-web theme show Show active theme
@@ -27,10 +23,10 @@ Usage:
27
23
  /settings/federation to join a hub or to create/revoke agent tokens.
28
24
 
29
25
  Files:
30
- ${f} settings (plugins, agents)
26
+ ${f} settings (agents)
31
27
  ${l()} active theme (shell + terminal colors)
32
- ${t} secrets (loaded automatically)
33
- ${n}/ plugin installs + runtime state
28
+ ${s} secrets (loaded automatically)
29
+ ${o}/ runtime state
34
30
 
35
31
  Most of these are also editable from the browser at /settings and /settings/theme.
36
32
 
@@ -38,4 +34,4 @@ Env:
38
34
  TMUX_WEB_TERMINAL_RENDERER=xterm|ghostty (also persistable via /settings)
39
35
  TMUX_WEB_AGENT_HUB / TMUX_WEB_AGENT_TOKEN / TMUX_WEB_AGENT_NAME (agent mode)
40
36
  TMUX_WEB_MAX_AGENTS / TMUX_WEB_MAX_AGENT_CONNS / TMUX_WEB_AGENT_OFFLINE_MS
41
- `)}export{S as cmdAdd,J as cmdList,k as cmdRemove,Y as cmdSetup,z as cmdTheme,Z as printUsage,Q as printVersion};
37
+ `)}export{F as cmdSetup,L as cmdTheme,q as printUsage,j as printVersion};
package/dist/lib/db.js CHANGED
@@ -1 +1 @@
1
- import{Low as s}from"lowdb";import{JSONFile as e}from"lowdb/node";import{mkdirSync as i}from"node:fs";import{join as r}from"node:path";import{getDataRoot as n}from"./state-paths.js";const o=n();i(o,{recursive:!0});const a=new s(new e(r(o,"db.json")),{notes:[],scheduledTasks:[],triggeredTasks:[],sessionAccess:[],pinnedViews:[],watchedPanes:[],windowLabels:[],sessionWindows:[],windowHistory:[],quickCommands:[]});export{a as db};
1
+ import{Low as i}from"lowdb";import{JSONFile as s}from"lowdb/node";import{mkdirSync as e}from"node:fs";import{join as n}from"node:path";import{getDataRoot as r}from"./state-paths.js";const o=r();e(o,{recursive:!0});const p=new i(new s(n(o,"db.json")),{notes:[],sessionAccess:[],pinnedViews:[],watchedPanes:[],windowLabels:[],sessionWindows:[],windowHistory:[],quickCommands:[]});export{p as db};
@@ -91,7 +91,7 @@ import{cssVarsStyle as c}from"../theme.js";function e(o){return o.replace(/&/g,"
91
91
  <div class="section">
92
92
  <h2>Join a hub (run as agent)</h2>
93
93
  <p class="desc">This machine keeps serving its own UI and also connects <em>outbound</em> to a hub,
94
- so the hub's browser can reach its tmux sessions, files, notes, scheduler and monitor.
94
+ so the hub's browser can reach its tmux sessions, files, notes and monitor.
95
95
  No ports need to be opened. Get a token on the hub with <code>tmux-web agent-token add --name ${e(s)}</code>.</p>
96
96
 
97
97
  <div class="status-line">
@@ -1,12 +1,12 @@
1
- import{cssVarsStyle as u}from"../theme.js";import{getThemeTemplates as h,THEME_TEMPLATE_IDS as b}from"../themes/index.js";function n(a){return a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function m(a,e){return`<!DOCTYPE html>
1
+ import{cssVarsStyle as m}from"../theme.js";import{getThemeTemplates as g,THEME_TEMPLATE_IDS as v}from"../themes/index.js";function s(a){return a.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function p(a,e){return`<!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
- <title>${n(a)} - tmux-web</title>
7
+ <title>${s(a)} - tmux-web</title>
8
8
  <style>
9
- ${u(e.shell)}
9
+ ${m(e.shell)}
10
10
  html, body { background: var(--page-bg); color: var(--page-fg); min-height: 100%; font-family: var(--font-sans); }
11
11
  .container { max-width: 680px; margin: 80px auto; padding: 0 24px; }
12
12
  .page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; }
@@ -47,16 +47,6 @@ import{cssVarsStyle as u}from"../theme.js";import{getThemeTemplates as h,THEME_T
47
47
  .btn:hover { border-color: var(--panel-accent); color: var(--panel-accent); }
48
48
  .btn.primary { border-color: var(--panel-success); color: var(--panel-success); }
49
49
  .btn.danger:hover { border-color: #e06c75; color: #e06c75; }
50
- .plugin-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 8px 0; font-size: var(--text-sm); border-top: 1px solid var(--panel-border); }
51
- .plugin-row:first-of-type { border-top: none; }
52
- .plugin-row .pkg { word-break: break-all; }
53
- .plugin-add { display: flex; gap: 10px; margin-top: 14px; }
54
- .plugin-add input[type=text] {
55
- flex: 1; font-size: var(--text-sm); font-family: inherit; color: var(--page-fg);
56
- background: var(--page-bg); border: 1px solid var(--panel-border);
57
- border-radius: 12px; padding: 9px 12px; outline: none; transition: border-color 0.15s, box-shadow 0.15s;
58
- }
59
- .plugin-add input[type=text]:focus { border-color: var(--panel-accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--panel-accent) 8%, transparent); }
60
50
  .num-input {
61
51
  width: 100px; font-size: var(--text-sm); font-family: inherit; color: var(--page-fg);
62
52
  background: var(--page-bg); border: 1px solid var(--panel-border);
@@ -87,15 +77,7 @@ import{cssVarsStyle as u}from"../theme.js";import{getThemeTemplates as h,THEME_T
87
77
  .num-input { width: 100%; }
88
78
  }
89
79
  </style>
90
- </head>`}function x(a,e){let r="";return a&&(r+='<div class="saved-flash">\u2713 Saved. Restart tmux-web to apply.</div>'),e&&(r+=`<div class="error-flash">${n(e)}</div>`),r}function $(a){const{settings:e,renderer:r,rendererOverridden:l,theme:d,plugins:t,saved:c=!1,error:s}=a,i=e.terminalRenderer??"xterm",o=e.defaultView??"default",g=e.scheduleHistoryDays??7,v=t.length?t.map(p=>`<div class="plugin-row">
91
- <span class="pkg">${n(p)}</span>
92
- <form method="POST" action="/settings/plugins" onsubmit="return confirm('Remove ${n(p)}?');">
93
- <input type="hidden" name="action" value="remove" />
94
- <input type="hidden" name="pkg" value="${n(p)}" />
95
- <button type="submit" class="btn danger">Remove</button>
96
- </form>
97
- </div>`).join(`
98
- `):'<p class="desc" style="margin:0">No plugins enabled.</p>';return`${m("Settings",d)}
80
+ </head>`}function h(a,e){let t="";return a&&(t+='<div class="saved-flash">\u2713 Saved. Restart tmux-web to apply.</div>'),e&&(t+=`<div class="error-flash">${s(e)}</div>`),t}function y(a){const{settings:e,renderer:t,rendererOverridden:i,theme:l,saved:r=!1,error:c}=a,n=e.terminalRenderer??"xterm",o=e.defaultView??"default";return`${p("Settings",l)}
99
81
  <body>
100
82
  <div class="container">
101
83
  <div class="page-header">
@@ -108,17 +90,17 @@ import{cssVarsStyle as u}from"../theme.js";import{getThemeTemplates as h,THEME_T
108
90
  <strong>restart the tmux-web process</strong> after making changes for them to take effect.
109
91
  </div>
110
92
 
111
- ${x(c,s)}
93
+ ${h(r,c)}
112
94
 
113
95
  <form method="POST" action="/settings">
114
96
  <div class="section">
115
97
  <h2>Terminal library</h2>
116
98
  <p class="desc">Rendering engine for the terminal view.</p>
117
99
  <div class="radios">
118
- <label class="row"><input type="radio" name="terminalRenderer" value="xterm" ${i==="xterm"?"checked":""} /> xterm.js (default)</label>
119
- <label class="row"><input type="radio" name="terminalRenderer" value="ghostty" ${i==="ghostty"?"checked":""} /> ghostty-web</label>
100
+ <label class="row"><input type="radio" name="terminalRenderer" value="xterm" ${n==="xterm"?"checked":""} /> xterm.js (default)</label>
101
+ <label class="row"><input type="radio" name="terminalRenderer" value="ghostty" ${n==="ghostty"?"checked":""} /> ghostty-web</label>
120
102
  </div>
121
- ${l?`<p class="override-note">\u26A0 A CLI flag or <code>TMUX_WEB_TERMINAL_RENDERER</code> env var is currently forcing <strong>${r}</strong>, overriding this setting for the running process.</p>`:""}
103
+ ${i?`<p class="override-note">\u26A0 A CLI flag or <code>TMUX_WEB_TERMINAL_RENDERER</code> env var is currently forcing <strong>${t}</strong>, overriding this setting for the running process.</p>`:""}
122
104
  </div>
123
105
 
124
106
  <div class="section">
@@ -130,28 +112,11 @@ import{cssVarsStyle as u}from"../theme.js";import{getThemeTemplates as h,THEME_T
130
112
  </div>
131
113
  </div>
132
114
 
133
- <div class="section">
134
- <h2>Schedule history</h2>
135
- <p class="desc">Days to keep the <code>/schedule</code> "Recently Triggered" history (fired &amp; missed tasks). 1\u2013365, default 7.</p>
136
- <label class="row"><input type="number" class="num-input" name="scheduleHistoryDays" min="1" max="365" value="${g}" /> days</label>
137
- </div>
138
-
139
115
  <div class="form-actions">
140
116
  <button type="submit" class="btn primary">Save settings</button>
141
117
  </div>
142
118
  </form>
143
119
 
144
- <div class="section" style="margin-top:16px">
145
- <h2>Plugins</h2>
146
- <p class="desc">Installed via npm into the tmux-web data dir. Add/remove runs npm and may take a few seconds.</p>
147
- ${v}
148
- <form method="POST" action="/settings/plugins" class="plugin-add">
149
- <input type="hidden" name="action" value="add" />
150
- <input type="text" id="pkg-input" name="pkg" placeholder="npm package name" autocomplete="off" />
151
- <button type="submit" class="btn">Add</button>
152
- </form>
153
- </div>
154
-
155
120
  <div class="section">
156
121
  <h2>Theme</h2>
157
122
  <p class="desc">Shell chrome + terminal colors.</p>
@@ -165,14 +130,14 @@ import{cssVarsStyle as u}from"../theme.js";import{getThemeTemplates as h,THEME_T
165
130
  </div>
166
131
  </div>
167
132
  </body>
168
- </html>`}const f={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"},y=["background","foreground","red","green","yellow","blue","magenta","cyan"];function T(a){const{theme:e,saved:r=!1}=a,l=h(),d=b.map(t=>{const c=l[t],s=e.template===t,i=y.map(o=>`<span class="swatch" style="background:${n(c.terminal[o])}" title="${o}"></span>`).join("");return`<label class="theme-card${s?" active":""}">
133
+ </html>`}const x={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"},b=["background","foreground","red","green","yellow","blue","magenta","cyan"];function w(a){const{theme:e,saved:t=!1}=a,i=g(),l=v.map(r=>{const c=i[r],n=e.template===r,o=b.map(d=>`<span class="swatch" style="background:${s(c.terminal[d])}" title="${d}"></span>`).join("");return`<label class="theme-card${n?" active":""}">
169
134
  <div class="tname">
170
- <input type="radio" name="template" value="${t}" ${s?"checked":""} style="accent-color:var(--panel-success)" />
171
- ${n(f[t]??t)}
135
+ <input type="radio" name="template" value="${r}" ${n?"checked":""} style="accent-color:var(--panel-success)" />
136
+ ${s(x[r]??r)}
172
137
  </div>
173
- <div class="swatches">${i}</div>
138
+ <div class="swatches">${o}</div>
174
139
  </label>`}).join(`
175
- `);return`${m("Theme",e)}
140
+ `);return`${p("Theme",e)}
176
141
  <body>
177
142
  <div class="container">
178
143
  <div class="page-header">
@@ -184,11 +149,11 @@ import{cssVarsStyle as u}from"../theme.js";import{getThemeTemplates as h,THEME_T
184
149
  <strong>Note:</strong> the active theme is applied immediately to new page renders.
185
150
  </div>
186
151
 
187
- ${r?'<div class="saved-flash">\u2713 Theme saved.</div>':""}
152
+ ${t?'<div class="saved-flash">\u2713 Theme saved.</div>':""}
188
153
 
189
154
  <form method="POST" action="/settings/theme">
190
155
  <div class="theme-grid">
191
- ${d}
156
+ ${l}
192
157
  </div>
193
158
  <div class="form-actions" style="margin-top:16px">
194
159
  <button type="submit" class="btn primary">Save theme</button>
@@ -196,4 +161,4 @@ import{cssVarsStyle as u}from"../theme.js";import{getThemeTemplates as h,THEME_T
196
161
  </form>
197
162
  </div>
198
163
  </body>
199
- </html>`}export{$ as renderSettings,T as renderThemeSettings};
164
+ </html>`}export{y as renderSettings,w as renderThemeSettings};
@@ -712,8 +712,8 @@ import{cssVarsStyle as c}from"../theme.js";import{newSessionModalCSS as f,newSes
712
712
  <button class="theme-option" data-theme="dark-cove" title="Dark Cove"><span class="theme-dot" style="background:#7aa2f7"></span>Dark Cove${e.template==="dark-cove"?'<span class="theme-check">\u2713</span>':""}</button>
713
713
  </div>
714
714
  <hr class="settings-divider" />
715
- <a href="/settings" class="settings-item" id="set-plugins-link" style="cursor:pointer;color:inherit;text-decoration:none">
716
- <span>Plugins & Settings</span>
715
+ <a href="/settings" class="settings-item" id="set-settings-link" style="cursor:pointer;color:inherit;text-decoration:none">
716
+ <span>Settings</span>
717
717
  <span style="font-size:var(--text-xs);color:var(--panel-muted)">\u2192</span>
718
718
  </a>
719
719
  </div>