tmux-weblink 0.3.0

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.
Files changed (75) hide show
  1. package/README.md +70 -0
  2. package/dist/assets/addon-fit-D5N335P2.js +16 -0
  3. package/dist/assets/terminal-client.js +4 -0
  4. package/dist/assets/xterm-NI46XXQY.js +51 -0
  5. package/dist/assets/xterm.css +285 -0
  6. package/dist/browser/terminal-client.js +4 -0
  7. package/dist/frontend.js +1 -0
  8. package/dist/index.js +12 -0
  9. package/dist/lib/agent-detect.js +3 -0
  10. package/dist/lib/agents-watch.js +2 -0
  11. package/dist/lib/atomicWrite.js +1 -0
  12. package/dist/lib/auditLog.js +1 -0
  13. package/dist/lib/auth.js +2 -0
  14. package/dist/lib/cli.js +32 -0
  15. package/dist/lib/commandbar.js +555 -0
  16. package/dist/lib/db.js +1 -0
  17. package/dist/lib/drawer-resize.js +166 -0
  18. package/dist/lib/drawer-script.js +8 -0
  19. package/dist/lib/ext-loader.js +1 -0
  20. package/dist/lib/html.js +1 -0
  21. package/dist/lib/icons.js +1 -0
  22. package/dist/lib/image-upload.js +1 -0
  23. package/dist/lib/load-env.js +6 -0
  24. package/dist/lib/mobile-toolbar.js +313 -0
  25. package/dist/lib/notes-db.js +18 -0
  26. package/dist/lib/notes-drawer.js +210 -0
  27. package/dist/lib/notes-utils.js +15 -0
  28. package/dist/lib/pages/agents-index.js +105 -0
  29. package/dist/lib/pages/history-index.js +85 -0
  30. package/dist/lib/pages/landing.js +78 -0
  31. package/dist/lib/pages/login.js +191 -0
  32. package/dist/lib/pages/notes-index.js +65 -0
  33. package/dist/lib/pages/notes-page.js +158 -0
  34. package/dist/lib/pages/quick-commands.js +361 -0
  35. package/dist/lib/pages/schedule-index.js +392 -0
  36. package/dist/lib/pages/settings.js +206 -0
  37. package/dist/lib/pages/terminal.js +364 -0
  38. package/dist/lib/pinned-views.js +1 -0
  39. package/dist/lib/plugins.js +3 -0
  40. package/dist/lib/quick-commands.js +1 -0
  41. package/dist/lib/rateLimiter.js +2 -0
  42. package/dist/lib/schedule-delay.js +9 -0
  43. package/dist/lib/scheduler-drawer.js +379 -0
  44. package/dist/lib/scheduler.js +1 -0
  45. package/dist/lib/security-config.js +2 -0
  46. package/dist/lib/session-access.js +1 -0
  47. package/dist/lib/session-windows.js +1 -0
  48. package/dist/lib/sessions-drawer.js +634 -0
  49. package/dist/lib/sessions-sidebar.js +1 -0
  50. package/dist/lib/settings.js +2 -0
  51. package/dist/lib/setup-features.js +1 -0
  52. package/dist/lib/setup-prompts.js +3 -0
  53. package/dist/lib/shared-layout.js +412 -0
  54. package/dist/lib/state-paths.js +1 -0
  55. package/dist/lib/terminal-config.js +1 -0
  56. package/dist/lib/theme-store.js +2 -0
  57. package/dist/lib/theme.js +28 -0
  58. package/dist/lib/themes/dark-cove.js +1 -0
  59. package/dist/lib/themes/ghostty.js +1 -0
  60. package/dist/lib/themes/index.js +1 -0
  61. package/dist/lib/themes/types.js +0 -0
  62. package/dist/lib/themes/vscode.js +1 -0
  63. package/dist/lib/themes/warm-clay.js +1 -0
  64. package/dist/lib/tmux-capture.js +5 -0
  65. package/dist/lib/tmux-control.js +2 -0
  66. package/dist/lib/tmux-panes.js +2 -0
  67. package/dist/lib/tmux-windows.js +4 -0
  68. package/dist/lib/watched-panes.js +1 -0
  69. package/dist/lib/window-history.js +1 -0
  70. package/dist/lib/window-labels.js +1 -0
  71. package/dist/lib/windows-drawer.js +364 -0
  72. package/dist/lib/ws-message.js +1 -0
  73. package/dist/sessions.js +2 -0
  74. package/package.json +62 -0
  75. package/scripts/fix-pty-perms.mjs +16 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Copyright (c) 2014 The xterm.js authors. All rights reserved.
3
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
4
+ * https://github.com/chjj/term.js
5
+ * @license MIT
6
+ *
7
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ * of this software and associated documentation files (the "Software"), to deal
9
+ * in the Software without restriction, including without limitation the rights
10
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ * copies of the Software, and to permit persons to whom the Software is
12
+ * furnished to do so, subject to the following conditions:
13
+ *
14
+ * The above copyright notice and this permission notice shall be included in
15
+ * all copies or substantial portions of the Software.
16
+ *
17
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ * THE SOFTWARE.
24
+ *
25
+ * Originally forked from (with the author's permission):
26
+ * Fabrice Bellard's javascript vt100 for jslinux:
27
+ * http://bellard.org/jslinux/
28
+ * Copyright (c) 2011 Fabrice Bellard
29
+ * The original design remains. The terminal itself
30
+ * has been extended to include xterm CSI codes, among
31
+ * other features.
32
+ */
33
+
34
+ /**
35
+ * Default styles for xterm.js
36
+ */
37
+
38
+ .xterm {
39
+ cursor: text;
40
+ position: relative;
41
+ user-select: none;
42
+ -ms-user-select: none;
43
+ -webkit-user-select: none;
44
+ }
45
+
46
+ .xterm.focus,
47
+ .xterm:focus {
48
+ outline: none;
49
+ }
50
+
51
+ .xterm .xterm-helpers {
52
+ position: absolute;
53
+ top: 0;
54
+ /**
55
+ * The z-index of the helpers must be higher than the canvases in order for
56
+ * IMEs to appear on top.
57
+ */
58
+ z-index: 5;
59
+ }
60
+
61
+ .xterm .xterm-helper-textarea {
62
+ padding: 0;
63
+ border: 0;
64
+ margin: 0;
65
+ /* Move textarea out of the screen to the far left, so that the cursor is not visible */
66
+ position: absolute;
67
+ opacity: 0;
68
+ left: -9999em;
69
+ top: 0;
70
+ width: 0;
71
+ height: 0;
72
+ z-index: -5;
73
+ /** Prevent wrapping so the IME appears against the textarea at the correct position */
74
+ white-space: nowrap;
75
+ overflow: hidden;
76
+ resize: none;
77
+ }
78
+
79
+ .xterm .composition-view {
80
+ /* TODO: Composition position got messed up somewhere */
81
+ background: #000;
82
+ color: #FFF;
83
+ display: none;
84
+ position: absolute;
85
+ white-space: nowrap;
86
+ z-index: 1;
87
+ }
88
+
89
+ .xterm .composition-view.active {
90
+ display: block;
91
+ }
92
+
93
+ .xterm .xterm-viewport {
94
+ /* On OS X this is required in order for the scroll bar to appear fully opaque */
95
+ background-color: #000;
96
+ overflow-y: scroll;
97
+ cursor: default;
98
+ position: absolute;
99
+ right: 0;
100
+ left: 0;
101
+ top: 0;
102
+ bottom: 0;
103
+ }
104
+
105
+ .xterm .xterm-screen {
106
+ position: relative;
107
+ }
108
+
109
+ .xterm .xterm-screen canvas {
110
+ position: absolute;
111
+ left: 0;
112
+ top: 0;
113
+ }
114
+
115
+ .xterm-char-measure-element {
116
+ display: inline-block;
117
+ visibility: hidden;
118
+ position: absolute;
119
+ top: 0;
120
+ left: -9999em;
121
+ line-height: normal;
122
+ }
123
+
124
+ .xterm.enable-mouse-events {
125
+ /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
126
+ cursor: default;
127
+ }
128
+
129
+ .xterm.xterm-cursor-pointer,
130
+ .xterm .xterm-cursor-pointer {
131
+ cursor: pointer;
132
+ }
133
+
134
+ .xterm.column-select.focus {
135
+ /* Column selection mode */
136
+ cursor: crosshair;
137
+ }
138
+
139
+ .xterm .xterm-accessibility:not(.debug),
140
+ .xterm .xterm-message {
141
+ position: absolute;
142
+ left: 0;
143
+ top: 0;
144
+ bottom: 0;
145
+ right: 0;
146
+ z-index: 10;
147
+ color: transparent;
148
+ pointer-events: none;
149
+ }
150
+
151
+ .xterm .xterm-accessibility-tree:not(.debug) *::selection {
152
+ color: transparent;
153
+ }
154
+
155
+ .xterm .xterm-accessibility-tree {
156
+ font-family: monospace;
157
+ user-select: text;
158
+ white-space: pre;
159
+ }
160
+
161
+ .xterm .xterm-accessibility-tree > div {
162
+ transform-origin: left;
163
+ width: fit-content;
164
+ }
165
+
166
+ .xterm .live-region {
167
+ position: absolute;
168
+ left: -9999px;
169
+ width: 1px;
170
+ height: 1px;
171
+ overflow: hidden;
172
+ }
173
+
174
+ .xterm-dim {
175
+ /* Dim should not apply to background, so the opacity of the foreground color is applied
176
+ * explicitly in the generated class and reset to 1 here */
177
+ opacity: 1 !important;
178
+ }
179
+
180
+ .xterm-underline-1 { text-decoration: underline; }
181
+ .xterm-underline-2 { text-decoration: double underline; }
182
+ .xterm-underline-3 { text-decoration: wavy underline; }
183
+ .xterm-underline-4 { text-decoration: dotted underline; }
184
+ .xterm-underline-5 { text-decoration: dashed underline; }
185
+
186
+ .xterm-overline {
187
+ text-decoration: overline;
188
+ }
189
+
190
+ .xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
191
+ .xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
192
+ .xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
193
+ .xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
194
+ .xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
195
+
196
+ .xterm-strikethrough {
197
+ text-decoration: line-through;
198
+ }
199
+
200
+ .xterm-screen .xterm-decoration-container .xterm-decoration {
201
+ z-index: 6;
202
+ position: absolute;
203
+ }
204
+
205
+ .xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
206
+ z-index: 7;
207
+ }
208
+
209
+ .xterm-decoration-overview-ruler {
210
+ z-index: 8;
211
+ position: absolute;
212
+ top: 0;
213
+ right: 0;
214
+ pointer-events: none;
215
+ }
216
+
217
+ .xterm-decoration-top {
218
+ z-index: 2;
219
+ position: relative;
220
+ }
221
+
222
+
223
+
224
+ /* Derived from vs/base/browser/ui/scrollbar/media/scrollbar.css */
225
+
226
+ /* xterm.js customization: Override xterm's cursor style */
227
+ .xterm .xterm-scrollable-element > .scrollbar {
228
+ cursor: default;
229
+ }
230
+
231
+ /* Arrows */
232
+ .xterm .xterm-scrollable-element > .scrollbar > .scra {
233
+ cursor: pointer;
234
+ font-size: 11px !important;
235
+ }
236
+
237
+ .xterm .xterm-scrollable-element > .visible {
238
+ opacity: 1;
239
+
240
+ /* Background rule added for IE9 - to allow clicks on dom node */
241
+ background:rgba(0,0,0,0);
242
+
243
+ transition: opacity 100ms linear;
244
+ /* In front of peek view */
245
+ z-index: 11;
246
+ }
247
+ .xterm .xterm-scrollable-element > .invisible {
248
+ opacity: 0;
249
+ pointer-events: none;
250
+ }
251
+ .xterm .xterm-scrollable-element > .invisible.fade {
252
+ transition: opacity 800ms linear;
253
+ }
254
+
255
+ /* Scrollable Content Inset Shadow */
256
+ .xterm .xterm-scrollable-element > .shadow {
257
+ position: absolute;
258
+ display: none;
259
+ }
260
+ .xterm .xterm-scrollable-element > .shadow.top {
261
+ display: block;
262
+ top: 0;
263
+ left: 3px;
264
+ height: 3px;
265
+ width: 100%;
266
+ box-shadow: var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset;
267
+ }
268
+ .xterm .xterm-scrollable-element > .shadow.left {
269
+ display: block;
270
+ top: 3px;
271
+ left: 0;
272
+ height: 100%;
273
+ width: 3px;
274
+ box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
275
+ }
276
+ .xterm .xterm-scrollable-element > .shadow.top-left-corner {
277
+ display: block;
278
+ top: 0;
279
+ left: 0;
280
+ height: 3px;
281
+ width: 3px;
282
+ }
283
+ .xterm .xterm-scrollable-element > .shadow.top.left {
284
+ box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
285
+ }
@@ -0,0 +1,4 @@
1
+ const O="https://esm.sh/ghostty-web@0.4.0";let m;function q(){return m||(document.querySelector("link[data-tmux-web-xterm-css]")?m=Promise.resolve():(m=new Promise(t=>{const n=document.createElement("link");n.rel="stylesheet",n.href="/assets/xterm.css",n.setAttribute("data-tmux-web-xterm-css",""),n.addEventListener("load",()=>t(),{once:!0}),n.addEventListener("error",()=>t(),{once:!0}),document.head.appendChild(n)}),m))}class B{terminal;fitAddon;container;constructor(t,n,r){this.container=t,this.terminal=n,this.fitAddon=r,this.terminal.loadAddon(this.fitAddon),this.terminal.open(t)}static async create(t,n,r){const[{Terminal:a},{FitAddon:f}]=await Promise.all([import("@xterm/xterm"),import("@xterm/addon-fit"),q()]),b=new a({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,theme:r});return new B(t,b,new f)}get cols(){return this.terminal.cols}get rows(){return this.terminal.rows}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom()}scrollToLine(t){this.terminal.scrollToLine(Math.max(0,t))}isNearScrollbackTop(){return this.terminal.buffer.active.viewportY<=1}viewportY(){return this.terminal.buffer.active.viewportY}baseY(){return this.terminal.buffer.active.baseY}fit(){const t=R(this.container);return t.width<=0||t.height<=0?!1:(this.fitAddon.fit(),!0)}focus(){this.terminal.focus()}pasteText(t){this.terminal.paste(t)}input(t){this.terminal.input(t,!1)}onData(t){this.terminal.onData(t),this.terminal.onBinary(t)}onResize(t){this.terminal.onResize(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){const t=document.activeElement;return!!t&&(t===this.container||this.container.contains(t))}}class F{terminal;container;colsValue=80;rowsValue=24;charW=0;charH=0;resizeCallbacks=[];constructor(t,n){this.container=t,this.terminal=n,this.terminal.open(t)}static async create(t,n,r){const a=await import(O);await a.init();const f=new a.Terminal({fontSize:14,fontFamily:"'JetBrains Mono', 'SF Mono', 'Menlo', monospace",cursorBlink:!0,cursorStyle:"bar",scrollback:n,convertEol:!1,theme:r});return new F(t,f)}get cols(){return this.colsValue}get rows(){return this.rowsValue}write(t){return new Promise(n=>this.terminal.write(t,n))}reset(){this.terminal.reset()}scrollToBottom(){this.terminal.scrollToBottom?.()}scrollToLine(t){this.terminal.scrollToLine?.(Math.max(0,t))}isNearScrollbackTop(){const t=this.terminal.buffer?.active;return t?t.viewportY>=t.baseY:!1}viewportY(){return this.terminal.buffer?.active?.viewportY??0}baseY(){return this.terminal.buffer?.active?.baseY??0}fit(){const t=R(this.container);if(t.width<=0||t.height<=0)return!1;this.updateCellMetrics(!this.charW||!this.charH);const n=Math.floor(t.width/this.charW),r=Math.floor(t.height/this.charH);if(n<10||r<5)return!1;if(n!==this.colsValue||r!==this.rowsValue){this.colsValue=n,this.rowsValue=r,this.terminal.resize(n,r);for(const a of this.resizeCallbacks)a({cols:n,rows:r})}return!0}focus(){(this.terminal.textarea||this.container.querySelector("textarea")||this.container)?.focus?.()}pasteText(t){const n=this.terminal.getMode?.(2004)??!1;this.input(n?"\x1B[200~"+t+"\x1B[201~":t)}input(t){this.terminal.input?.(t,!1)}onData(t){this.terminal.onData(n=>t(n))}onResize(t){this.resizeCallbacks.push(t)}onScroll(t){this.terminal.onScroll(t)}attachCustomKeyEventHandler(t){this.terminal.attachCustomKeyEventHandler(t)}isFocused(){const t=document.activeElement;return!!t&&(t===this.container||t===this.terminal.textarea||this.container.contains(t))}updateCellMetrics(t=!1){const n=this.container.querySelector("canvas");if(n instanceof HTMLElement&&n.offsetWidth>0&&n.offsetHeight>0&&this.colsValue>0&&this.rowsValue>0){const r=n.offsetWidth/this.colsValue,a=n.offsetHeight/this.rowsValue;(t||!this.charW||!this.charH)&&(this.charW=r,this.charH=a)}(!this.charW||!this.charH)&&(this.charW=9,this.charH=18)}}const A=window.__TMUX_WEB_TERMINAL__;if(!A)throw new Error("missing tmux-web terminal config");const u=A,H=document.getElementById("terminal-container");if(!H)throw new Error("missing terminal container");const o=H;function J(){const e=document.createElement("div");e.textContent="ghostty-web unavailable \u2014 using xterm.js",e.style.cssText='position:fixed;bottom:12px;right:12px;z-index:2000;background:rgba(0,0,0,0.85);color:#fbbf24;font:12px/1.4 "JetBrains Mono",monospace;padding:8px 12px;border:1px solid rgba(251,191,36,0.4);border-radius:6px;',document.body.appendChild(e),setTimeout(()=>e.remove(),6e3)}async function G(){if(u.renderer==="ghostty")try{return await F.create(o,u.scrollback,u.theme)}catch(e){console.error("[tmux-web] ghostty-web failed to load; falling back to xterm.js:",e),J()}return B.create(o,u.scrollback,u.theme)}const i=await G();let c,v=0,C,s=null,P=0,d="connecting",x=0,T=!1,E=[],h="";function Q(){return E.join("")+h}async function Z(e,t=0){const n=e?i.viewportY():0,r=e?i.baseY():0,a=Q();if(i.reset(),!a){e||i.scrollToBottom();return}await i.write(a),e?i.scrollToLine(r+n+t):i.scrollToBottom()}function $(e){let t;try{t=JSON.parse(e)}catch{d==="live"&&(h+=e,i.write(e));return}if(t.type==="auth.required"){const n=localStorage.getItem("tmux-web-token");n?g({type:"auth.token",token:n}):location.href="/login?returnTo="+encodeURIComponent(location.pathname+location.search);return}if(t.type==="auth.ok"){t.token&&localStorage.setItem("tmux-web-token",t.token),S(!0),w=1e3,p(),g({type:"resize",cols:i.cols,rows:i.rows}),l();return}if(t.type==="auth.failed"){if(S(!1),t.permanentLock||t.retryAfterMs&&t.retryAfterMs>6e4){localStorage.removeItem("tmux-web-token"),location.href="/login?error="+encodeURIComponent(t.message);return}const n=document.createElement("div");n.textContent="Auth failed: "+t.message,n.style.cssText='position:fixed;top:12px;right:12px;z-index:2000;background:rgba(0,0,0,0.85);color:#f87171;font:12px/1.4 "JetBrains Mono",monospace;padding:8px 12px;border:1px solid rgba(248,113,113,0.4);border-radius:6px;',document.body.appendChild(n),setTimeout(()=>n.remove(),4e3);return}if(t.type==="snapshot"&&typeof t.data=="string"){E=[t.data],h="",x=typeof t.lines=="number"?t.lines:u.terminal.initialLines,d="live",i.reset(),i.write(t.data).then(()=>i.scrollToBottom());return}if(t.type==="window_changed"&&typeof t.activeIndex=="number"){const n=new URL(location.href);n.searchParams.get("window")!==String(t.activeIndex)&&(n.searchParams.set("window",String(t.activeIndex)),history.replaceState(history.state,"",n)),window.dispatchEvent(new CustomEvent("tmux:windows",{detail:t.windows}));return}if(t.type==="history"&&typeof t.data=="string"){T=!1,t.lines>0&&t.data&&(E.unshift(t.data),x+=t.lines,Z(!0,t.lines));return}if(t.type==="data"&&typeof t.data=="string"){if(d==="connecting"){d="live",h=t.data,i.write(t.data);return}d==="live"&&(h+=t.data,i.write(t.data))}}function R(e){const t=e.getBoundingClientRect(),n=window.visualViewport;return n?{width:Math.min(t.width,n.width),height:Math.max(0,Math.min(t.height,n.height-Math.max(0,t.top)))}:t}function p(){return i.fit()}function Y(){o.classList.remove("terminal-pending")}function l(){v&&cancelAnimationFrame(v),C&&clearTimeout(C),p(),v=requestAnimationFrame(()=>{v=0,p()&&Y()}),C=setTimeout(()=>{p()&&Y()},120)}function V(){l(),setTimeout(l,50),setTimeout(l,150),setTimeout(l,300)}const N=document.getElementById("status-dot"),k=document.getElementById("status-text");function S(e){N&&(N.className="dot"+(e?" connected":"")),k&&(k.textContent=e?"connected":"reconnecting")}const tt=location.protocol==="https:"?"wss:":"ws:",et=tt+"//"+location.host+"/ws/"+encodeURIComponent(u.sessionName),nt="/api/session/"+encodeURIComponent(u.sessionName)+"/upload";let w=1e3;const rt=/^((?!chrome|android|crios|fxios|edgios).)*safari/i.test(navigator.userAgent);function g(e){c?.readyState===WebSocket.OPEN&&c.send(JSON.stringify(e))}function K(){let e=et;const t=localStorage.getItem("tmux-web-token");t&&(e+=(e.includes("?")?"&":"?")+"token="+encodeURIComponent(t)),c=new WebSocket(e),c.onopen=()=>{d="connecting",x=0,T=!1,E=[],h="",i.reset(),w=1e3},c.onmessage=n=>{typeof n.data=="string"&&$(n.data)},c.onclose=()=>{S(!1),d="connecting",setTimeout(()=>{w=Math.min(w*2,1e4),K()},w)},c.onerror=()=>c?.close()}function L(e){g({type:"input",data:e})}function it(e){L(e)}function U(e){return e.split(`\r
2
+ `).join(`
3
+ `).split(`
4
+ `).join("\r")}function X(e){return typeof e=="string"&&e.startsWith("image/")}function _(e){return e?.files?.length?e.files[0]??null:null}function ot(e){if(!e?.items)return null;for(let t=0;t<e.items.length;t++){const n=e.items[t];if(n?.kind==="file"&&X(n.type))return n.getAsFile()}return null}function D(e){e&&k?k.textContent=e:S(c?.readyState===WebSocket.OPEN)}async function at(e){const t=new FormData,n=e instanceof File&&e.name?e.name:"upload";t.append("file",e,n);const r=localStorage.getItem("tmux-web-token"),a={};r&&(a.Authorization="Bearer "+r);const f=await fetch(nt,{method:"POST",body:t,headers:a});if(!f.ok){let W="upload failed";try{const z=await f.json();z.error&&(W=z.error)}catch{}throw new Error(W)}const b=await f.json();if(!b.path)throw new Error("no path in response");return b.path}async function I(e){try{D("uploading...");const t=await at(e);it(t),D(null)}catch(t){console.warn("image upload:",t),D("upload failed"),setTimeout(()=>D(null),2e3)}}i.onData(e=>L(e)),i.onResize(({cols:e,rows:t})=>g({type:"resize",cols:e,rows:t})),window.tmuxWeb={sendInput:L,focusTerminal:j},i.onScroll(()=>{d!=="live"||T||!i.isNearScrollbackTop()||(T=!0,g({type:"load_history",before:x}))}),i.attachCustomKeyEventHandler(e=>e.type!=="keydown"?!0:(e.ctrlKey||e.metaKey)&&e.code==="KeyV"?(e.preventDefault(),(async()=>{try{if(navigator.clipboard?.read){const t=await navigator.clipboard.read();for(const n of t)for(const r of n.types)if(X(r)){const a=await n.getType(r);await I(a);return}}}catch{}try{const t=await navigator.clipboard?.readText();t&&i.pasteText(U(t))}catch{}})(),!1):!0);let M=0;async function st(e){const t=ot(e.clipboardData);if(t){e.preventDefault(),e.stopPropagation();const a=Date.now();if(a-M<50)return;M=a,await I(t);return}const n=e.clipboardData?.getData("text/plain");if(!n)return;e.preventDefault(),e.stopPropagation();const r=Date.now();r-M<50||(M=r,i.pasteText(U(n)))}o.addEventListener("paste",st);let y=0;o.addEventListener("dragenter",e=>{e.preventDefault(),y++,_(e.dataTransfer)&&o.classList.add("terminal-drag-over")}),o.addEventListener("dragover",e=>{e.preventDefault(),e.dataTransfer&&(e.dataTransfer.dropEffect="copy")}),o.addEventListener("dragleave",e=>{e.preventDefault(),y--,y<=0&&(y=0,o.classList.remove("terminal-drag-over"))}),o.addEventListener("drop",async e=>{e.preventDefault(),y=0,o.classList.remove("terminal-drag-over");const t=_(e.dataTransfer);t&&await I(t)}),document.addEventListener("keydown",e=>{!rt||e.key!=="Escape"||e.defaultPrevented||e.repeat||e.metaKey||e.ctrlKey||e.altKey||i.isFocused()&&(e.preventDefault(),e.stopPropagation(),L("\x1B"))},!0);function lt(e,t,n){(o.querySelector(".xterm-screen")??o).dispatchEvent(new WheelEvent("wheel",{deltaY:e,deltaMode:WheelEvent.DOM_DELTA_PIXEL,clientX:t,clientY:n,bubbles:!0,cancelable:!0}))}function j(){i.focus(),V()}o.addEventListener("touchstart",e=>{if(e.touches.length!==1){s=null;return}const t=e.touches[0];t&&(s={startX:t.clientX,startY:t.clientY,lastX:t.clientX,lastY:t.clientY,scrolling:!1},e.stopPropagation())},{passive:!0,capture:!0}),o.addEventListener("touchmove",e=>{if(!s||e.touches.length!==1)return;const t=e.touches[0];if(!t)return;const n=t.clientY-s.startY,r=t.clientX-s.startX;if(!s.scrolling){if(Math.abs(n)<8||Math.abs(n)<Math.abs(r))return;s.scrolling=!0,P=Date.now()+500}e.preventDefault(),e.stopPropagation(),lt(-(t.clientY-s.lastY),t.clientX,t.clientY),s.lastX=t.clientX,s.lastY=t.clientY},{passive:!1,capture:!0}),o.addEventListener("touchend",e=>{if(!s)return;const t=s.scrolling;s=null,e.stopPropagation(),t?(P=Date.now()+500,e.preventDefault()):j()},{passive:!1,capture:!0}),o.addEventListener("touchcancel",e=>{s=null,e.stopPropagation()},{passive:!0,capture:!0}),o.addEventListener("pointerup",e=>{e.pointerType==="touch"&&Date.now()<P&&(e.preventDefault(),e.stopPropagation())},!0),window.addEventListener("resize",l),window.visualViewport?.addEventListener("resize",l),window.visualViewport?.addEventListener("scroll",l),new ResizeObserver(l).observe(o),document.fonts?.ready.then(()=>{l(),setTimeout(l,50),setTimeout(l,200)}),o.addEventListener("focusin",V,!0),requestAnimationFrame(()=>{p()&&Y(),K(),l()});
@@ -0,0 +1 @@
1
+ import{renderTerminal as o}from"./lib/pages/terminal.js";import{renderLoginPage as d}from"./lib/pages/login.js";import{renderLanding as m}from"./lib/pages/landing.js";import{renderNotesIndex as f}from"./lib/pages/notes-index.js";import{renderNotesPage as g}from"./lib/pages/notes-page.js";import{renderSettings as s,renderThemeSettings as a}from"./lib/pages/settings.js";import{renderScheduleIndex as P}from"./lib/pages/schedule-index.js";import{renderAgentsIndex as c}from"./lib/pages/agents-index.js";import{renderHistoryIndex as l}from"./lib/pages/history-index.js";import{renderQuickCommandsPage as L}from"./lib/pages/quick-commands.js";export{c as renderAgentsIndex,l as renderHistoryIndex,m as renderLanding,d as renderLoginPage,f as renderNotesIndex,g as renderNotesPage,L as renderQuickCommandsPage,P as renderScheduleIndex,s as renderSettings,o as renderTerminal,a as renderThemeSettings};
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import{chmodSync as Oe,existsSync as Y,readdirSync as le,statSync as Z}from"node:fs";import{readFile as Ne}from"node:fs/promises";import{createRequire as $e}from"node:module";import v from"node:path";import{fileURLToPath as Be}from"node:url";import{Hono as Fe}from"hono";import{serve as Ve}from"@hono/node-server";import{WebSocketServer as Ge,WebSocket as me}from"ws";import*as ze from"node-pty";try{const e=v.dirname($e(import.meta.url).resolve("node-pty/package.json")),t=v.join(e,"prebuilds");if(Y(t))for(const r of le(t)){const n=v.join(t,r,"spawn-helper");Y(n)&&!(Z(n).mode&73)&&Oe(n,493)}}catch{}import{listSessions as b}from"./sessions.js";import{renderLanding as Ke,renderTerminal as Qe,renderLoginPage as Je,renderNotesIndex as Xe,renderNotesPage as Ye,renderSettings as Ze,renderThemeSettings as et,renderScheduleIndex as tt,renderAgentsIndex as rt,renderHistoryIndex as nt,renderQuickCommandsPage as ot}from"./frontend.js";import{loadSecurityConfig as st,saveSecurityConfig as pe}from"./lib/security-config.js";import{hashPassword as fe,verifyPassword as he,validatePassword as ye,TokenStore as at}from"./lib/auth.js";import{RateLimiter as it}from"./lib/rateLimiter.js";import{audit as u}from"./lib/auditLog.js";import{db as k}from"./lib/db.js";import{recordSessionAccess as dt,getSessionAccessMap as I}from"./lib/session-access.js";import{listWindowHistory as ct,clearWindowHistory as ut}from"./lib/window-history.js";import{loadExtensions as lt,spawnExtensionBackend as mt,registerExtensionRoutes as pt}from"./lib/ext-loader.js";import{SchedulerService as ft,isValidScheduleInput as ht,isValidRescheduleInput as yt}from"./lib/scheduler.js";import{getScheduleDelayError as we}from"./lib/schedule-delay.js";import{handleClientMessage as wt}from"./lib/ws-message.js";import{loadDotEnv as gt}from"./lib/load-env.js";import{cmdAdd as kt,cmdRemove as jt,cmdList as vt,cmdSetup as xt,cmdTheme as bt,printUsage as ge,printVersion as St}from"./lib/cli.js";import{readSettings as ee,writeSettings as Tt}from"./lib/settings.js";import{readActiveTheme as It,setActiveThemeTemplate as ke}from"./lib/theme-store.js";import{isThemeTemplateId as je,THEME_TEMPLATE_IDS as At}from"./lib/themes/index.js";import{installPlugin as qt,uninstallPlugin as Ct}from"./lib/plugins.js";import{buildCommandbarSessions as M}from"./lib/commandbar.js";import{pinView as Mt,unpinView as Rt,listPinnedViews as _t}from"./lib/pinned-views.js";import{listWindowLabels as Et,setWindowLabel as Pt}from"./lib/window-labels.js";import{captureAndStoreWindows as te,getStoredWindows as ve}from"./lib/session-windows.js";import{acquireControlClient as Lt,killAllControlClients as Dt}from"./lib/tmux-control.js";import{buildSidebarSessions as Ut}from"./lib/sessions-sidebar.js";import{createQuickCommand as Wt,deleteQuickCommand as Ht,listQuickCommands as re,updateQuickCommand as Ot}from"./lib/quick-commands.js";import{getSessionPaneTarget as Nt,capturePaneTail as $t,capturePaneHistoryChunk as Bt,isAlternateScreen as Ft,toCrlf as xe}from"./lib/tmux-capture.js";import{readTerminalBufferConfig as Vt}from"./lib/terminal-config.js";import{ImageUploadError as Gt,saveUploadedImage as zt}from"./lib/image-upload.js";import{listSessionWindows as Kt,selectSessionWindow as Qt,newSessionWindow as Jt,renameSessionWindow as Xt,newTmuxSession as Yt,TmuxWindowsError as V}from"./lib/tmux-windows.js";import{getActivePaneInfo as Zt}from"./lib/tmux-panes.js";import{recordWatchedPane as er}from"./lib/watched-panes.js";import{probeWatchedPanes as tr,startBackgroundWatch as rr,getCachedAgentStatuses as nr,requestProbe as or}from"./lib/agents-watch.js";gt();const be=Vt(),l=st(),p=new at,R=new it;let U=!1;const G="tmux-web-token",sr=365;function Se(e){return e==="127.0.0.1"||e==="::1"||e==="::ffff:127.0.0.1"}function z(e){if(l.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 ar(e){if(l.security.trustProxy){const t=(e.headers["x-forwarded-for"]||"").split(",")[0].trim();if(t)return t}return e.socket.remoteAddress||"unknown"}function ne(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*)${G}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function Te(e){const t=e.headers.authorization;if(typeof t=="string"&&t.startsWith("Bearer "))return t.slice(7);const r=e.headers.cookie;if(typeof r!="string")return null;const n=r.match(new RegExp(`(?:^|;\\s*)${G}=([^;]+)`));return n?decodeURIComponent(n[1]):null}function sn(e){const t=ne(e);return t?p.validateToken(t):null}function an(e){const t=Te(e);return t?p.validateToken(t):null}function Ie(e,t){const r=e.req.header("x-forwarded-proto")==="https"||e.req.url.startsWith("https:");e.header("Set-Cookie",`${G}=${encodeURIComponent(t)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${sr*86400}${r?"; Secure":""}`)}function ir(e){e.header("Set-Cookie",`${G}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`)}function d(){return async(e,t)=>{const r=ne(e);return!r||!p.validateToken(r)?(u("http_unauthorized",{ip:z(e)}),e.json({error:"unauthorized"},401)):t()}}function dr(e){const t=encodeURIComponent(e.req.url);return e.redirect(`/login?returnTo=${t}`,302)}function A(){return async(e,t)=>{const r=ne(e);return!r||!p.validateToken(r)?dr(e):t()}}const W=new Map;function cr(e){let t=0;for(const r of W.values())r.ip===e&&t++;return t}function oe(e,t,r){try{e.close(t,r)}catch{}}function dn(e,t){e.readyState===me.OPEN&&e.send(JSON.stringify(t))}function ur(e,t){let r="xterm";(t==="ghostty"||t==="xterm")&&(r=t);const n=process.env.TMUX_WEB_TERMINAL_RENDERER?.trim().toLowerCase();(n==="ghostty"||n==="xterm")&&(r=n);for(const s of e)s==="--ghostty"&&(r="ghostty"),s==="--xterm"&&(r="xterm");return r}const lr=7;function Ae(e){return typeof e!="number"||!Number.isFinite(e)?lr:Math.min(365,Math.max(1,Math.round(e)))}const qe=process.argv.slice(2);{const e=qe.filter(t=>t!=="--ghostty"&&t!=="--xterm");if(e.length>0){const[t,r]=e;switch(t){case"add":r||(console.error("usage: tmux-web add <package>"),process.exit(1)),await kt(r),process.exit(0);case"remove":case"rm":r||(console.error("usage: tmux-web remove <package>"),process.exit(1)),await jt(r),process.exit(0);case"list":case"ls":await vt(),process.exit(0);case"setup":await xt(e),process.exit(0);case"theme":await bt(e.slice(1)),process.exit(0);case"help":case"--help":case"-h":ge(),process.exit(0);case"-V":case"--version":case"-v":St(),process.exit(0);default:console.error(`unknown argument: ${t}`),ge(),process.exit(1)}}}function x(e,t){e.readyState===me.OPEN&&e.send(JSON.stringify(t))}const H=new Set,Ce=[];await k.read(),k.data.sessionAccess??=[],k.data.pinnedViews??=[],k.data.watchedPanes??=[],k.data.triggeredTasks??=[],k.data.quickCommands??=[];const O=await ee();let y=await It();const h=O.commandbar===!0,q=O.agents===!0,Me=q&&O.agentsBackgroundWatch===!0,se=ur(qe,O.terminalRenderer),Re=Ae(O.scheduleHistoryDays),_e=v.join(process.cwd(),"extensions"),ae=await lt(_e);for(const e of ae)e.start&&Ce.push(mt(e.dir,e));const _=new ft({db:k,historyRetentionMs:Re*864e5,onMissedTask:e=>console.warn(`[scheduler] dropped missed task ${e.id} (was due ${new Date(e.fireAt).toISOString()})`)});await _.restoreFromDb();function K(e){if(q)try{const t=Zt(e);if(!t)return;er({paneId:t.paneId,sessionName:e,windowIndex:t.windowIndex,paneIndex:t.paneIndex}).catch(()=>{})}catch{}}const a=new Fe;a.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'","connect-src 'self' ws: wss: http: https:","img-src 'self' data:","font-src 'self'","worker-src 'self' blob:"].join("; ")),e.header("Referrer-Policy","strict-origin-when-cross-origin")}),a.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()}),pt(a,_e,ae);const mr=v.dirname(Be(import.meta.url)),pr=[v.join(mr,"assets"),v.join(process.cwd(),"dist","assets")];a.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 pr){const n=v.join(r,t);if(!Y(n))continue;const s=await Ne(n),o=v.extname(t),i={".css":"text/css; charset=utf-8",".js":"application/javascript; charset=utf-8",".map":"application/json; charset=utf-8"};return e.body(s,200,{"Content-Type":i[o]??"application/octet-stream"})}return e.notFound()});const fr=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
3
+ <rect x="2.5" y="5.5" width="27" height="21" rx="4" fill="#0d1117" stroke="#7dd3fc" stroke-width="2"/>
4
+ <path d="M8 13l4.2 3.1L8 19.2" fill="none" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
5
+ <line x1="15.5" y1="19.6" x2="22.5" y2="19.6" stroke="#7dd3fc" stroke-width="2.2" stroke-linecap="round"/>
6
+ </svg>`,Ee=e=>e.body(fr,200,{"Content-Type":"image/svg+xml; charset=utf-8","Cache-Control":"public, max-age=86400"});a.get("/favicon.svg",Ee),a.get("/favicon.ico",Ee),a.get("/login",e=>{const r=e.req.query("setup")==="1"?!0:!l.passwordHash,n=e.req.query("error");return e.html(Je({setupMode:r,error:n?decodeURIComponent(n):void 0,theme:y}))}),a.get("/",A(),e=>{const t=e.req.query("view"),r=t==="recent"?"recent":t==="default"?"default":O.defaultView??"default",n=b(),s=I(),o=h?M(n,s):[];return e.html(Ke(n,{view:r,accessMap:s,commandbarEnabled:h,commandbarSessions:o,agentsEnabled:q,theme:y}))}),a.get("/s/:session",A(),async e=>{const t=decodeURIComponent(e.req.param("session"));await dt(t),te(t);const r=b(),n=I(),s=h?M(r,n):[];return e.html(Qe(t,ae,{commandbarEnabled:h,commandbarSessions:s,quickCommands:h?re():[],agentsEnabled:q,terminal:be,theme:y,renderer:se}))}),a.get("/notes",A(),e=>{const t=h?M(b(),I()):[];return e.html(Xe(k.data.notes,y,h,t,q))}),a.get("/notes/:session",A(),e=>{const t=decodeURIComponent(e.req.param("session")),r=h?M(b(),I()):[];return e.html(Ye(t,y,h,r))}),a.get("/schedule",A(),e=>{const t=h?M(b(),I()):[];return e.html(tt(_.list(),_.listTriggered(),y,Re,h,t,q))}),a.get("/agents",A(),e=>{if(!q)return e.redirect("/settings",303);const t=h?M(b(),I()):[];return e.html(rt(y,h,t))}),a.get("/history",A(),e=>{const t=b(),r=h?M(t,I()):[],n=new Set(t.map(s=>s.name));return e.html(nt(ct(),y,h,r,q,n))}),a.get("/quick-commands",A(),e=>{const t=h?M(b(),I()):[];return e.html(ot(re(),y,h,t,q))}),a.post("/api/history/clear",d(),async e=>(await ut(),e.json({ok:!0}))),a.get("/api/quick-commands",d(),e=>e.json(re())),a.post("/api/quick-commands",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Wt(t);return"error"in r?e.json({error:r.error},400):e.json(r,201)}),a.patch("/api/quick-commands/:id",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=await Ot(e.req.param("id"),t);return"error"in r?e.json({error:r.error},r.status):e.json(r)}),a.delete("/api/quick-commands/:id",d(),async e=>await Ht(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),a.get("/api/agents",d(),async e=>{if(!q)return e.json({error:"agents page disabled"},403);const t=Me?nr():await tr();return e.json(t)}),a.post("/api/auth/password",async e=>{const t=z(e),r=R.check(t);if(!r.allowed)return u("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 s=typeof n.password=="string"?n.password:"";if(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Se(t))return u("setup_rejected_remote",{ip:t}),e.json({error:"First-run setup must be performed from localhost"},403);if(U)return e.json({error:"Password setup in progress"},409);const f=ye(s);if(f)return e.json({error:f},400);U=!0;try{l.passwordHash=await fe(s),pe(l),u("password_set",{ip:t})}finally{U=!1}const{plaintext:T}=p.createAccessToken("setup",l.security.tokenTtlDays);return Ie(e,T),e.json({ok:!0,token:T,setupMode:!0})}if(!await he(s,l.passwordHash)){const f=R.recordFailure(t);return u("auth_failed",{ip:t,method:"password",failures:f.failures,permanentLock:f.permanentLock}),f.permanentLock?(p.revokeAll(),u("permanent_lock",{ip:t,failures:f.failures}),e.json({error:"Server locked after too many failed attempts",permanentLock:!0},403)):f.allowed?e.json({error:"Incorrect password"},401):e.json({error:"Too many attempts",retryAfterMs:f.retryAfterMs},429)}R.recordSuccess(t);const i=`browser-${t}`,{plaintext:w}=p.createAccessToken(i,l.security.tokenTtlDays);return Ie(e,w),u("auth_success",{ip:t,method:"password",tokenName:i}),e.json({ok:!0,token:w})}),a.post("/api/auth/token",d(),async e=>{const t=z(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}`,s=typeof r.ttlDays=="number"?r.ttlDays:l.security.tokenTtlDays,{stored:o,plaintext:i}=p.createAccessToken(n,s);return u("token_created",{ip:t,name:o.name,tokenId:o.id}),e.json({id:o.id,name:o.name,token:i,expiresAt:o.expiresAt})}),a.get("/api/auth/tokens",d(),e=>e.json(p.list().map(t=>({id:t.id,name:t.name,createdAt:t.createdAt,lastUsedAt:t.lastUsedAt,expiresAt:t.expiresAt})))),a.delete("/api/auth/tokens/:id",d(),e=>p.revoke(e.req.param("id"))?(u("token_revoked",{ip:z(e),tokenId:e.req.param("id")}),e.json({ok:!0})):e.json({error:"not found"},404)),a.post("/api/auth/logout",d(),e=>(ir(e),e.json({ok:!0}))),a.get("/settings",A(),async e=>{const t=await ee(),r=t.terminalRenderer??"xterm";return e.html(Ze({settings:t,renderer:se,rendererOverridden:se!==r,theme:y,plugins:t.plugins??[],saved:e.req.query("saved")==="1",error:e.req.query("error")?decodeURIComponent(e.req.query("error")):void 0}))}),a.post("/settings",d(),async e=>{let t;try{t=await e.req.parseBody()}catch{return e.redirect("/settings?error="+encodeURIComponent("invalid form body"),303)}const r=await ee(),n=t.terminalRenderer==="ghostty"?"ghostty":"xterm",s=t.defaultView==="recent"?"recent":"default",o=Ae(typeof t.scheduleHistoryDays=="string"?Number(t.scheduleHistoryDays):void 0);return await Tt({...r,commandbar:t.commandbar!==void 0,agents:t.agents!==void 0,agentsBackgroundWatch:t.agentsBackgroundWatch!==void 0,terminalRenderer:n,defaultView:s,scheduleHistoryDays:o}),e.redirect("/settings?saved=1",303)}),a.post("/settings/plugins",d(),async e=>{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 s=r==="remove"?await Ct(n):r==="add"?await qt(n):{ok:!1,output:"unknown action"};return s.ok?e.redirect("/settings?saved=1",303):e.redirect("/settings?error="+encodeURIComponent(s.output.slice(0,800)),303)}),a.get("/settings/theme",A(),e=>e.html(et({theme:y,saved:e.req.query("saved")==="1"}))),a.post("/settings/theme",d(),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"||!je(r)?e.redirect("/settings/theme",303):(y=await ke(r),e.redirect("/settings/theme?saved=1",303))});const hr={vscode:"VS Code",ghostty:"Ghostty","warm-clay":"Warm Clay","dark-cove":"Dark Cove"};a.get("/api/theme",d(),e=>e.json({active:y.template,templates:At.map(t=>({id:t,name:hr[t]}))})),a.post("/api/theme",d(),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"||!je(r)?e.json({error:"invalid theme template"},400):(y=await ke(r),e.json({ok:!0,active:y.template}))}),a.get("/api/sessions",d(),e=>h?e.json(M(b(),I())):e.json({error:"commandbar disabled"},404)),a.post("/api/sessions/new",d(),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(b().some(o=>o.name===r))return e.json({error:"session already exists"},409);try{return Yt(r,n),e.json({ok:!0})}catch(o){const i=o instanceof V?o.message:"failed to create session";return e.json({error:i},500)}}),a.get("/api/fs/list",d(),e=>{const t=process.env.HOME??"/";let r=e.req.query("path")??t;r.startsWith("~")&&(r=t+r.slice(1)),r.startsWith("/")||(r=v.join(t,r));let n=r,s="",o=r.endsWith("/");if(!o)try{o=Z(r).isDirectory()}catch{}o||(n=v.dirname(r),s=v.basename(r).toLowerCase());try{const i=le(n),w=[];for(const f of i)if(!f.startsWith(".")&&!(s&&!f.toLowerCase().startsWith(s))){try{const T=v.join(n,f);Z(T).isDirectory()&&w.push(T)}catch{}if(w.length>=50)break}return e.json({dirs:w})}catch{return e.json({dirs:[]})}});function ie(e){return{...Ut(b(),I(),_t()),currentSession:e??null}}function Pe(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}}a.get("/api/sidebar/sessions",d(),e=>{const t=e.req.query("currentSession");return e.json(ie(typeof t=="string"&&t?t:void 0))}),a.get("/api/sidebar/session-windows/:session",d(),e=>{const t=decodeURIComponent(e.req.param("session"));return e.json(ve(t))}),a.post("/api/pinned-views",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Pe(t);return"error"in r?e.json({error:r.error},400):(await Mt(r.sessionName,r.windowIndex),e.json(ie()))}),a.delete("/api/pinned-views",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=Pe(t);return"error"in r?e.json({error:r.error},400):(await Rt(r.sessionName,r.windowIndex),e.json(ie()))}),a.get("/api/notes",d(),e=>{const t=[...k.data.notes].sort((r,n)=>(n.updatedAt??0)-(r.updatedAt??0));return e.json(t)}),a.get("/api/notes/:scope",d(),e=>{const t=decodeURIComponent(e.req.param("scope")),r=k.data.notes.find(n=>n.scope===t);return r?e.json(r):e.json(null,404)}),a.put("/api/notes/:scope",d(),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()},s=k.data.notes.findIndex(o=>o.scope===t);return s>=0?k.data.notes[s]=n:k.data.notes.push(n),await k.write(),e.json({ok:!0})}),a.post("/api/session/:session/upload",d(),async e=>{const t=decodeURIComponent(e.req.param("session"));if(!b().some(o=>o.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 s=n.file;if(!(s instanceof File))return e.json({error:"missing file field"},400);try{const o=await s.arrayBuffer(),i=Buffer.from(o),{path:w}=await zt(i,s.type||void 0,s.name||void 0);return e.json({path:w})}catch(o){return o instanceof Gt?e.json({error:o.message},o.status):(console.error("[upload]",o),e.json({error:"upload failed"},500))}}),a.get("/api/session/:session/windows",d(),e=>{const t=decodeURIComponent(e.req.param("session")),r=new Map(Et(t).map(o=>[o.windowIndex,o.label])),n=new Map(ve(t).map(o=>[o.index,o])),s=Kt(t).map(o=>({...o,label:r.get(o.index)??null,worktree:n.get(o.index)?.worktree??!1}));return e.json(s)}),a.post("/api/session/:session/window-label",d(),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 s=typeof r.label=="string"?r.label:"",o=await Pt(t,n,s);return e.json(o)}),a.post("/api/session/:session/select-window",d(),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 Qt(t,n),K(t),e.json({ok:!0})}catch(s){return s instanceof V?e.json({error:s.message},s.status):(console.error("[select-window]",s),e.json({error:"select-window failed"},500))}}),a.post("/api/session/:session/rename-window",d(),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 Xt(t,n,r.name),te(t),e.json({ok:!0})}catch(s){return s instanceof V?e.json({error:s.message},s.status):(console.error("[rename-window]",s),e.json({error:"rename-window failed"},500))}}),a.post("/api/session/:session/new-window",d(),e=>{const t=decodeURIComponent(e.req.param("session"));try{return Jt(t),K(t),te(t),e.json({ok:!0})}catch(r){return r instanceof V?e.json({error:r.message},r.status):(console.error("[new-window]",r),e.json({error:"new-window failed"},500))}}),a.get("/api/schedule",d(),e=>e.json(_.list(e.req.query("session")))),a.post("/api/schedule",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=we(t);if(r)return e.json({error:r},400);if(!ht(t))return e.json({error:"invalid body"},400);const n=await _.create(t);return e.json({id:n.id,fireAt:n.fireAt})}),a.delete("/api/schedule/:id",d(),async e=>await _.delete(e.req.param("id"))?e.json({ok:!0}):e.json({error:"not found"},404)),a.patch("/api/schedule/:id",d(),async e=>{let t;try{t=await e.req.json()}catch{return e.json({error:"invalid json"},400)}const r=we(t);if(r)return e.json({error:r},400);if(!yt(t))return e.json({error:"invalid body"},400);const n=await _.reschedule(e.req.param("id"),t.delayMs);return n?e.json({id:n.id,fireAt:n.fireAt}):e.json({error:"not found"},404)});const yr=parseInt(process.env.PORT||"21000",10),wr=Ve({fetch:a.fetch,port:yr},e=>{console.log(`tmux-web running at http://localhost:${e.port}`)});Me&&rr();const de=new Ge({noServer:!0});function Le(e,t,r){e.write(`HTTP/1.1 ${t} ${r}\r
7
+ \r
8
+ `),e.destroy()}wr.on("upgrade",(e,t,r)=>{const n=new URL(e.url||"/",`http://${e.headers.host}`),s=n.pathname.match(/^\/ws\/(.+)$/);if(!s){t.destroy();return}const o=ar(e),i=e.headers.origin,w=l.security.allowedOrigins;if(i&&!(i===`http://${e.headers.host}`||i===`https://${e.headers.host}`)&&(w.length===0||!w.includes(i))){u("ws_rejected_origin",{ip:o,origin:i}),Le(t,403,"Origin not allowed");return}const f=cr(o);if(f>=l.security.maxConnectionsPerIp){u("ws_rejected_per_ip_cap",{ip:o,liveFromIp:f}),Le(t,429,"Too many connections");return}const T=Te(e)||n.searchParams.get("token")||"",Q=T?p.validateToken(T):null;de.handleUpgrade(e,t,r,E=>{de.emit("connection",E,e,decodeURIComponent(s[1]),o,Q)})}),de.on("connection",(e,t,r,n,s)=>{const o={ws:e,ip:n,authenticated:!1,authTimeout:null};W.set(e,o),u("ws_connected",{ip:n});let i=null;const{initialLines:w,historyChunk:f,syncIdleMs:T,syncMaxMs:Q}=be;let E=!0,D=null,B=null,N=r,P=null;const F=()=>{D&&(clearTimeout(D),D=null),B&&(clearTimeout(B),B=null)},ce=()=>{if(E){F(),E=!1;try{N=Nt(r)}catch{N=r}if(K(r),!Ft(N))try{const g=$t(N,w);x(e,{type:"snapshot",data:xe(g),lines:w})}catch{}}},Ue=()=>{E&&(D&&clearTimeout(D),D=setTimeout(ce,T))};function ue(){try{i=ze.spawn("tmux",["attach-session","-t",r],{name:"xterm-256color",cols:80,rows:24,cwd:process.env.HOME||"/",env:process.env})}catch(c){x(e,{type:"data",data:`\r
9
+ \x1B[31mFailed to attach to tmux session "${r}": ${c.message}\x1B[0m\r
10
+ `}),oe(e,1011,"pty spawn failed");return}H.add(i);let g=-1,m="";P=Lt(r,({activeIndex:c,windows:j})=>{x(e,{type:"window_changed",activeIndex:c,windows:j});const L=j.map(S=>S.index).join(","),$=c!==g||L!==m;g=c,m=L,$&&(K(r),or())}),B=setTimeout(ce,Q),i.onData(c=>{if(E){Ue();return}x(e,{type:"data",data:c})}),i.onExit(({exitCode:c})=>{F(),i&&H.delete(i),i=null,x(e,{type:"data",data:`\r
11
+ \x1B[2m--- tmux exited (code ${c}) ---\x1B[0m\r
12
+ `}),oe(e,1e3,"pty exited")})}function J(g,m){o.authenticated=!0,o.authTimeout&&(clearTimeout(o.authTimeout),o.authTimeout=null),u("auth_success",{ip:n,method:g,tokenName:m}),x(e,{type:"auth.ok",setupMode:!l.passwordHash}),ue()}function C(g,m={}){x(e,{type:"auth.failed",message:g,...m})}async function We(g){let m;try{m=JSON.parse(g)}catch{return!1}if(m.type==="auth.token"&&typeof m.token=="string"){const c=p.validateToken(m.token);if(!c){u("token_auth_failed",{ip:n});const j=R.recordFailure(n);return j.permanentLock&&(p.revokeAll(),u("permanent_lock",{ip:n,failures:j.failures})),C("Invalid or expired token",{permanentLock:j.permanentLock,retryAfterMs:j.retryAfterMs}),!0}return p.touch(c.tokenHash),J("token",c.name),!0}if(m.type==="auth"&&typeof m.password=="string"){const c=R.check(n);if(!c.allowed)return u("rate_limited",{ip:n,retryAfterMs:c.retryAfterMs,permanentLock:c.permanentLock}),C("Too many attempts",{permanentLock:c.permanentLock,retryAfterMs:c.retryAfterMs}),!0;if(!l.passwordHash){if(!l.security.allowRemoteSetup&&!Se(n))return u("setup_rejected_remote",{ip:n}),C("First-run setup must be performed from localhost"),!0;if(U)return C("Password setup in progress"),!0;const S=ye(m.password);if(S)return C(S),!0;U=!0;try{l.passwordHash=await fe(m.password),pe(l),u("password_set",{ip:n})}finally{U=!1}const{stored:X,plaintext:He}=p.createAccessToken("setup",l.security.tokenTtlDays);return x(e,{type:"auth.ok",setupMode:!0,token:He}),u("token_created",{ip:n,name:X.name,tokenId:X.id}),u("auth_success",{ip:n,method:"password",tokenName:X.name}),ue(),!0}if(!l.passwordHash)return C("Server not configured"),!0;if(!await he(m.password,l.passwordHash)){const S=R.recordFailure(n);return u("auth_failed",{ip:n,method:"password",failures:S.failures,permanentLock:S.permanentLock}),S.permanentLock&&(p.revokeAll(),u("permanent_lock",{ip:n,failures:S.failures})),C("Incorrect password",{permanentLock:S.permanentLock,retryAfterMs:S.retryAfterMs}),!0}R.recordSuccess(n);const L=`ws-${n}`,{plaintext:$}=p.createAccessToken(L,l.security.tokenTtlDays);return J("password",L),x(e,{type:"auth.ok",setupMode:!1,token:$}),!0}return!1}s?(p.touch(s.tokenHash),J("token",s.name)):(x(e,{type:"auth.required",setupMode:!l.passwordHash}),o.authTimeout=setTimeout(()=>{o.authenticated||(u("auth_timeout",{ip:n}),C("Authentication timeout"),oe(e,4e3,"Auth timeout"))},l.security.authTimeoutMs)),e.on("message",async g=>{const m=typeof g=="string"?g:g.toString("utf-8");if(!o.authenticated){if(m.length>1e6){x(e,{type:"auth.failed",message:"Message too large"});return}await We(m)||C("Authentication required");return}if(!i)return;try{if(wt(m,i))return}catch{return}let c;try{c=JSON.parse(m)}catch{return}if(c.type==="load_history"&&typeof c.before=="number"){const j=Math.max(0,Math.floor(c.before));try{const{data:L,lines:$}=Bt(N,j,f);x(e,{type:"history",data:xe(L),before:j,lines:$})}catch{x(e,{type:"history",data:"",before:j,lines:0})}}}),e.on("close",()=>{o.authTimeout&&clearTimeout(o.authTimeout),W.delete(e),u("ws_disconnected",{ip:n}),F(),P&&(P(),P=null),i&&(H.delete(i),i.kill(),i=null)}),e.on("error",()=>{o.authTimeout&&clearTimeout(o.authTimeout),W.delete(e),F(),P&&(P(),P=null),i&&(H.delete(i),i.kill(),i=null)})});function De(){_.cleanup(),Dt();for(const[e,t]of W){try{e.close(1001,"Server shutting down")}catch{}t.authTimeout&&clearTimeout(t.authTimeout)}W.clear();for(const e of H)try{e.kill()}catch{}H.clear();for(const e of Ce)try{e.kill("SIGTERM")}catch{}R.dispose(),process.exit(0)}process.on("SIGINT",De),process.on("SIGTERM",De),setInterval(()=>{p.purgeExpired()},60*60*1e3).unref();
@@ -0,0 +1,3 @@
1
+ const E={claude:"Claude Code",codex:"Codex",opencode:"OpenCode",cursor:"Cursor Agent",droid:"Droid",copilot:"GitHub Copilot"},f=new Set(["node","nodejs","bun","deno","python","python3","ruby","sh","bash","zsh","fish","env","npx","bunx","tmux"]);function s(t){let n=t.trim();if(!n)return"";const e=n.lastIndexOf("/");return e>=0&&(n=n.slice(e+1)),n=n.toLowerCase(),n.startsWith(".")&&(n=n.slice(1)),n=n.replace(/-wrapped$/,""),n=n.replace(/\.(c|m)?js$/,"").replace(/\.py$/,""),n}function d(t){switch(t){case"claude":case"claude-code":return"claude";case"codex":case"codex-cli":return"codex";case"opencode":case"open-code":return"opencode";case"cursor":case"cursor-agent":case"agent":return"cursor";case"droid":return"droid";case"copilot":case"github-copilot":case"gh-copilot":return"copilot";default:return null}}function I(t,n=[]){const e=d(s(t));if(e)return e;if(!f.has(s(t))&&n.length===0)return null;for(const a of n)for(const l of a.split(/\s+/)){const i=d(s(l));if(i)return i}return null}const p=20,h="\u2B21\u2B22\u25D0\u25D1\u25D2\u25D3\u25F4\u25F5\u25F6\u25F7";function m(t){const n=t.codePointAt(0)??0;return n>=10240&&n<=10495}function y(t){return t.split(`
2
+ `).map(e=>e.replace(/\s+$/,"")).filter(e=>e.trim()!=="").slice(-p)}function r(t){return t.includes("esc to interrupt")||t.includes("ctrl+c to interrupt")||t.includes("ctrl-c to interrupt")}function o(t){return t.some(n=>/[❯>›]\s*\d+\.\s+\S/.test(n))}function u(t){for(const n of t){const e=n.trimStart();if(e.length<2)continue;const c=e[0];if(m(c)||h.includes(c))return!0}return!1}function k(t){return t.some(n=>/^[·✢✳✶✻✽*] \S.*…/.test(n))}function g(t,n){return o(t)||n.includes("waiting for permission")||n.includes("do you want to proceed")||n.includes("do you want to make this edit")||n.includes("do you want to create")?"blocked":r(n)||k(t)?"working":"idle"}function x(t,n){return o(t)||n.includes("press enter to confirm or esc to cancel")||n.includes("enter to submit answer")||n.includes("allow command?")||n.includes("[y/n]")?"blocked":r(n)||n.includes("\u2022 working (")?"working":"idle"}function b(t,n){return o(t)||n.includes("[y/n]")||n.includes("(y/n)")||n.includes("allow this")?"blocked":r(n)||u(t)?"working":"idle"}function C(t,n){return o(t)||n.includes("run this command?")||n.includes("[y/n]")||n.includes("(y) (enter)")?"blocked":r(n)||u(t)?"working":"idle"}function S(t,n){return o(t)||n.includes("[y/n]")||n.includes("(y/n)")||n.includes("allow this")||n.includes("do you want")?"blocked":r(n)||u(t)?"working":"idle"}function A(t,n){return o(t)||n.includes("[y/n]")||n.includes("(y/n)")||n.includes("allow command")||n.includes("do you want")?"blocked":r(n)||u(t)?"working":"idle"}function N(t,n){const e=y(n),c=e.join(`
3
+ `).toLowerCase();switch(t){case"claude":return g(e,c);case"codex":return x(e,c);case"opencode":return b(e,c);case"cursor":return C(e,c);case"droid":return S(e,c);case"copilot":return A(e,c);default:return"unknown"}}export{E as AGENT_LABELS,N as detectState,I as identifyAgent};
@@ -0,0 +1,2 @@
1
+ import{execFileSync as y}from"node:child_process";import{capturePaneLines as N}from"./tmux-capture.js";import{listSessionPanes as b}from"./tmux-panes.js";import{getWatchedPanes as k,pruneWatchedPanes as W,recordWatchedPane as B}from"./watched-panes.js";import{identifyAgent as S,detectState as M}from"./agent-detect.js";function T(){const n=new Map;try{const o=y("ps",["-axo","pid=,ppid=,command="],{encoding:"utf-8",timeout:3e3,maxBuffer:8388608});for(const a of o.split(`
2
+ `)){const c=a.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);c&&n.set(parseInt(c[1],10),{ppid:parseInt(c[2],10),command:c[3]})}}catch{}return n}function v(n,o){if(o.size===0||n<=0)return[];const a=new Map;for(const[e,t]of o){const s=a.get(t.ppid);s?s.push(e):a.set(t.ppid,[e])}const c=[],f=[...a.get(n)??[]],p=o.get(n);p&&c.push(p.command);let l=0;for(;f.length&&l<200;){l++;const e=f.shift(),t=o.get(e);t&&c.push(t.command);const s=a.get(e);s&&f.push(...s)}return c}const m=new Map;function D(n){let o=5381;for(let a=0;a<n.length;a++)o=(o<<5)+o+n.charCodeAt(a)>>>0;return o}async function L(){const n=k();if(n.length===0)return[];const o=new Map;for(const e of n)o.has(e.sessionName)||o.set(e.sessionName,b(e.sessionName));const a=new Set;for(const e of o.values())for(const t of e)a.add(t.paneId);await W(a);for(const e of m.keys())a.has(e)||m.delete(e);const c=T(),f=new Set(n.map(e=>e.paneId)),p=[...n];for(const[e,t]of o){const s=t.find(d=>d.active&&d.windowActive);if(!s||f.has(s.paneId)||!S(s.command,v(s.pid,c)))continue;const u={paneId:s.paneId,sessionName:e,windowIndex:s.windowIndex,paneIndex:s.paneIndex};await B(u),f.add(s.paneId),p.unshift({...u,watchedAt:Date.now()})}const l=[];for(const e of p){const t=o.get(e.sessionName)?.find(h=>h.paneId===e.paneId);if(!t)continue;const s=v(t.pid,c),u=S(t.command,s);if(!u)continue;let d="unknown";try{const h=N(t.target,0);d=M(u,h);const g=D(h),x=m.get(t.paneId);m.set(t.paneId,g),d==="idle"&&x!==void 0&&x!==g&&(d="working")}catch{}l.push({paneId:t.paneId,sessionName:e.sessionName,windowIndex:t.windowIndex,paneIndex:t.paneIndex,agent:u,state:d,watchedAt:e.watchedAt})}return l}let i=null,I=[],A=0,r=null;const P=1500;function w(){A=Date.now(),L().then(n=>{I=n}).catch(()=>{})}function O(){return I}function z(){return i!==null}function F(){if(!i||r)return;const n=Date.now()-A;if(n>=P){w();return}r=setTimeout(()=>{r=null,w()},P-n),typeof r.unref=="function"&&r.unref()}function H(n=3500){i||(w(),i=setInterval(w,n),typeof i.unref=="function"&&i.unref())}function V(){i&&(clearInterval(i),i=null),r&&(clearTimeout(r),r=null),I=[]}export{O as getCachedAgentStatuses,z as isBackgroundWatchRunning,L as probeWatchedPanes,F as requestProbe,H as startBackgroundWatch,V as stopBackgroundWatch};
@@ -0,0 +1 @@
1
+ import{writeFileSync as i,renameSync as m,mkdirSync as a,existsSync as d}from"node:fs";import y from"node:path";function S(t,c,n){const e=y.dirname(t);d(e)||a(e,{recursive:!0});const r=`${t}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;try{i(r,c,n!==void 0?{mode:n}:void 0),m(r,t)}catch(o){try{i(r,"")}catch{}throw o}}export{S as atomicWriteFileSync};
@@ -0,0 +1 @@
1
+ const a=new Set(["auth_failed","token_auth_failed","token_expired","auth_timeout","http_unauthorized"]),u=new Set(["rate_limited","permanent_lock"]);function l(t){return u.has(t)?"error":a.has(t)?"warn":"info"}function c(t,i={}){const e=l(t),r={level:e,component:t.startsWith("tmux")||t.startsWith("schedule")||t.startsWith("note")?"app":"auth",event:t,timestamp:new Date().toISOString()};for(const[s,o]of Object.entries(i))o!=null&&(r[s]=o);const n=JSON.stringify(r);e==="error"?console.error(n):e==="warn"?console.warn(n):console.log(n)}export{c as audit};
@@ -0,0 +1,2 @@
1
+ import*as a from"node:crypto";import{promisify as d}from"node:util";import*as c from"node:fs";import*as k from"node:path";import{atomicWriteFileSync as w}from"./atomicWrite.js";import{getDataRoot as g}from"./state-paths.js";const m=d(a.scrypt),h={N:131072,r:8,p:1,keylen:32},S=16384,p=256*1024*1024;async function O(n){const t=a.randomBytes(16).toString("hex"),e=await m(n,t,h.keylen,{N:h.N,r:h.r,p:h.p,maxmem:p});return{salt:t,hash:e.toString("hex"),algorithm:"scrypt",params:{...h}}}async function E(n,t){if(t.params.N<S||t.params.r<1||t.params.p<1||t.params.keylen<16)return!1;const e=await m(n,t.salt,t.params.keylen,{N:t.params.N,r:t.params.r,p:t.params.p,maxmem:p}),s=Buffer.from(t.hash,"hex");return e.length!==s.length?!1:a.timingSafeEqual(e,s)}const y=new Set(["password","password1","12345678","123456789","1234567890","qwerty123","abcdefgh","abcd1234","letmein01","iloveyou","trustno1","sunshine1","princess1","football1","charlie1","passw0rd","admin123","welcome1","p@ssw0rd","changeme"]);function P(n){if(n.length<8)return"Password must be at least 8 characters";if(n.length>128)return"Password must be 128 characters or fewer";if(y.has(n.toLowerCase()))return"This password is too common. Please choose a stronger password";const t=/[a-zA-Z]/.test(n),e=/[^a-zA-Z]/.test(n);return!t||!e?"Password must contain at least one letter and one number or special character":null}function x(){const n=a.randomBytes(32),t=n.toString("base64url"),e=a.createHash("sha256").update(n).digest("hex");return{plaintext:t,hash:e}}function A(n){const t=Buffer.from(n,"base64url");return a.createHash("sha256").update(t).digest("hex")}const l=g(),i=k.join(l,"tokens.json"),v=5*6e4;function f(){c.existsSync(l)||c.mkdirSync(l,{recursive:!0,mode:448})}class ${tokens=[];constructor(){this.load()}load(){let t;try{t=c.readFileSync(i,"utf-8")}catch(r){if(r.code==="ENOENT"){this.tokens=[];return}throw new Error(`Failed to read tokens at ${i}: ${r.message}`)}let e;try{e=JSON.parse(t)}catch(r){const o=`${i}.corrupt-${Date.now()}`;try{c.renameSync(i,o)}catch{}throw new Error(`tokens.json was corrupt (saved aside as ${o}): ${r.message}`)}if(e&&typeof e=="object"&&Array.isArray(e.tokens)){this.tokens=e.tokens;return}const s=`${i}.malformed-${Date.now()}`;try{c.renameSync(i,s)}catch{}throw new Error(`tokens.json had unexpected shape (saved aside as ${s})`)}save(){f(),w(i,JSON.stringify({tokens:this.tokens},null,2)+`
2
+ `,384)}validateToken(t){const e=A(t);for(const s of this.tokens)if(s.tokenHash===e)return s.expiresAt&&new Date(s.expiresAt)<new Date?null:s;return null}touch(t){const e=this.tokens.find(o=>o.tokenHash===t);if(!e)return;const s=Date.now(),r=new Date(e.lastUsedAt).getTime();s-r<v||(e.lastUsedAt=new Date(s).toISOString(),this.save())}createAccessToken(t,e){f();const{plaintext:s,hash:r}=x(),o=new Date,u={id:a.randomUUID(),name:t.slice(0,100)||`Token (${Date.now()})`,tokenHash:r,createdAt:o.toISOString(),lastUsedAt:o.toISOString(),expiresAt:e?new Date(o.getTime()+e*24*60*60*1e3).toISOString():null};return this.tokens.push(u),this.save(),{stored:u,plaintext:s}}list(){return this.tokens.map(t=>({...t}))}revoke(t){const e=this.tokens.length;return this.tokens=this.tokens.filter(s=>s.id!==t),this.tokens.length!==e?(this.save(),!0):!1}revokeAll(){this.tokens.length!==0&&(this.tokens=[],this.save())}purgeExpired(){const t=new Date,e=this.tokens.length;this.tokens=this.tokens.filter(s=>!s.expiresAt||new Date(s.expiresAt)>=t),this.tokens.length!==e&&this.save()}}export{$ as TokenStore,x as generateToken,O as hashPassword,A as hashToken,P as validatePassword,E as verifyPassword};
@@ -0,0 +1,32 @@
1
+ import{existsSync as d}from"node:fs";import b from"node:path";import{createRequire as w}from"node:module";import{getDataRoot as x,getSettingsPath as y,getThemePath as l}from"./state-paths.js";import{readSettings as c,writeSettings as v}from"./settings.js";import{readActiveTheme as T,setActiveThemeTemplate as E}from"./theme-store.js";import{isThemeTemplateId as S,THEME_TEMPLATE_IDS as $}from"./themes/index.js";import{cmdAdd as R,cmdRemove as A,getPluginDir as k}from"./plugins.js";import{getEnvFilePath as m}from"./load-env.js";import{SETUP_FEATURES as p}from"./setup-features.js";import{promptYesNo as D,promptChoice as I,requireTty as P}from"./setup-prompts.js";const _=x(),U=k(),f=y(),L=w(import.meta.url),{version:M}=L("../../package.json");function N(s){const t={};for(const e of s)switch(e){case"--yes":case"-y":t.yes=!0;break;case"--commandbar":t.commandbar=!0;break;case"--no-commandbar":t.commandbar=!1;break;case"--agents":t.agents=!0;break;case"--no-agents":t.agents=!1;break}return t}function j(s){switch(s){case"agents":return"agents";default:return"commandbar"}}async function X(s){const t=s[0]==="setup"?s.slice(1):s,e=N(t),n=e.yes||e.commandbar!==void 0||e.agents!==void 0;n||P();const u=await c();console.log(`tmux-weblink setup
2
+ `),console.log(`Configure optional features.
3
+ `);const g=new Map;for(const o of p){const i=j(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 D(`${o.label} (${o.description})`,h)}g.set(o.id,r)}let a;if(!n){const o=u.terminalRenderer==="ghostty"?"ghostty":"xterm";a=await I("Terminal renderer (xterm.js is the default; ghostty-web is experimental)",["xterm","ghostty"],o)}for(const o of p)g.get(o.id)?await o.enable():await o.disable();if(a){const o=await c();await v({...o,terminalRenderer:a}),console.log(`\u2713 terminal renderer set to ${a}`)}console.log(`
4
+ Done. Settings: ${f}`),d(m())&&console.log(`Env file: ${m()}`),console.log("Restart tmux-web to apply UI changes.")}async function z(s){const[t,e]=s;switch(t){case"list":{console.log("Available themes:");for(const n of $)console.log(` ${n}`);return}case"set":{e||(console.error("usage: tmux-web theme set <vscode|ghostty>"),process.exit(1)),S(e)||(console.error(`unknown theme: ${e}`),console.error("Run: tmux-web theme list"),process.exit(1)),await E(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)}}async function J(){const t=(await c()).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 n=d(b.join(U,"node_modules",e));console.log(` ${n?"\u2713":"\u2717"} ${e}${n?"":" (not installed \u2014 run: tmux-web add "+e+")"}`)}}function Q(){console.log(M)}function Z(){const s=_,t=m();console.log(`tmux-web \u2014 terminal-in-the-browser for tmux
6
+
7
+ Usage:
8
+ tmux-web Start the server (PORT env var, default 3000)
9
+ tmux-web --ghostty Start with ghostty-web instead of xterm.js
10
+ tmux-web --xterm Start with xterm.js explicitly
11
+ tmux-web -V, --version Print version and exit
12
+ tmux-web -h, --help Show this help
13
+ tmux-web setup Interactive feature setup
14
+ 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
+ tmux-web theme list List available themes
19
+ tmux-web theme set <name> Set active theme (vscode, ghostty)
20
+ tmux-web theme show Show active theme
21
+
22
+ Files:
23
+ ${f} settings (plugins, commandbar)
24
+ ${l()} active theme (shell + terminal colors)
25
+ ${t} secrets (loaded automatically)
26
+ ${s}/ plugin installs + runtime state
27
+
28
+ Most of these are also editable from the browser at /settings and /settings/theme.
29
+
30
+ Env:
31
+ TMUX_WEB_TERMINAL_RENDERER=xterm|ghostty (also persistable via /settings)
32
+ `)}export{R as cmdAdd,J as cmdList,A as cmdRemove,X as cmdSetup,z as cmdTheme,Z as printUsage,Q as printVersion};