claude-mux 0.7.0 → 0.7.1

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 (115) hide show
  1. package/README.md +4 -2
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/utils/version.d.ts +1 -1
  5. package/dist/utils/version.js +1 -1
  6. package/dist/web/client/_app/immutable/chunks/{DU91Ml7U.js → BGcEgn7w.js} +1 -1
  7. package/{web/.svelte-kit/output/client/_app/immutable/chunks/DmdO6ygw.js → dist/web/client/_app/immutable/chunks/By6CYjmE.js} +1 -1
  8. package/dist/web/client/_app/immutable/chunks/{HKNo9LID.js → CR5jMWGV.js} +1 -1
  9. package/{web/.svelte-kit/output/client/_app/immutable/entry/app.CGIBnoln.js → dist/web/client/_app/immutable/entry/app.DmtnygN7.js} +2 -2
  10. package/dist/web/client/_app/immutable/entry/start.fYmelGlC.js +1 -0
  11. package/dist/web/client/_app/immutable/nodes/{0.CqlJ9a31.js → 0.DGDAdwT5.js} +1 -1
  12. package/dist/web/client/_app/immutable/nodes/{1.BQUZh2-w.js → 1.Cg8dWgUN.js} +1 -1
  13. package/dist/web/client/_app/immutable/nodes/{2.CCV1YdgF.js → 2.DItUEo3e.js} +1 -1
  14. package/dist/web/client/_app/immutable/nodes/{3.D9tDCdq8.js → 3.dHui0twF.js} +1 -1
  15. package/dist/web/client/_app/immutable/nodes/{4.BqPyNkFA.js → 4.CiEHP0cr.js} +1 -1
  16. package/dist/web/client/_app/version.json +1 -1
  17. package/dist/web/server/chunks/{0-BHWsmCJv.js → 0-BmRg-l2z.js} +5 -5
  18. package/dist/web/server/chunks/{0-BHWsmCJv.js.map → 0-BmRg-l2z.js.map} +1 -1
  19. package/dist/web/server/chunks/{1-YRx6A8Tm.js → 1-CPgO8U7d.js} +3 -3
  20. package/dist/web/server/chunks/{1-YRx6A8Tm.js.map → 1-CPgO8U7d.js.map} +1 -1
  21. package/dist/web/server/chunks/{2-eC6JuGAo.js → 2--ZO5fZp_.js} +5 -5
  22. package/dist/web/server/chunks/{2-eC6JuGAo.js.map → 2--ZO5fZp_.js.map} +1 -1
  23. package/dist/web/server/chunks/{3-Bk-wV20p.js → 3-7HqSep9c.js} +3 -3
  24. package/dist/web/server/chunks/{3-Bk-wV20p.js.map → 3-7HqSep9c.js.map} +1 -1
  25. package/dist/web/server/chunks/{4-nteBgDrW.js → 4-C1PCdmY0.js} +4 -4
  26. package/dist/web/server/chunks/{4-nteBgDrW.js.map → 4-C1PCdmY0.js.map} +1 -1
  27. package/dist/web/server/index.js +1 -1
  28. package/dist/web/server/index.js.map +1 -1
  29. package/dist/web/server/manifest.js +10 -10
  30. package/dist/web/server/manifest.js.map +1 -1
  31. package/docs/release-checklist.md +8 -34
  32. package/docs/removing-hooks.md +14 -14
  33. package/package.json +1 -1
  34. package/src/cli.ts +1 -1
  35. package/src/utils/version.ts +1 -1
  36. package/web/.svelte-kit/adapter-bun/chunks/internal.js +1 -1
  37. package/web/.svelte-kit/adapter-bun/manifest-full.js +1 -1
  38. package/web/.svelte-kit/adapter-bun/manifest.js +1 -1
  39. package/web/.svelte-kit/adapter-bun/nodes/0.js +1 -1
  40. package/web/.svelte-kit/adapter-bun/nodes/1.js +1 -1
  41. package/web/.svelte-kit/adapter-bun/nodes/2.js +1 -1
  42. package/web/.svelte-kit/adapter-bun/nodes/3.js +1 -1
  43. package/web/.svelte-kit/adapter-bun/nodes/4.js +1 -1
  44. package/web/.svelte-kit/adapter-node/.vite/manifest.json +200 -15
  45. package/web/.svelte-kit/adapter-node/_app/immutable/assets/AllSessionsPanel.BKhqOrbV.css +1 -0
  46. package/web/.svelte-kit/adapter-node/_app/immutable/assets/_layout.WptSHSUl.css +1 -0
  47. package/web/.svelte-kit/adapter-node/_app/immutable/assets/_page.DldLgTc-.css +1 -0
  48. package/web/.svelte-kit/adapter-node/_app/immutable/assets/_page.DoNWy7tW.css +1 -0
  49. package/web/.svelte-kit/adapter-node/chunks/AllSessionsPanel.svelte_svelte_type_style_lang.js +49 -0
  50. package/web/.svelte-kit/adapter-node/chunks/alert-dialog-description.js +2670 -0
  51. package/web/.svelte-kit/adapter-node/chunks/auth.js +59 -0
  52. package/web/.svelte-kit/adapter-node/chunks/button.js +82 -0
  53. package/web/.svelte-kit/adapter-node/chunks/client.js +29 -0
  54. package/web/.svelte-kit/adapter-node/chunks/context.js +28 -16
  55. package/web/.svelte-kit/adapter-node/chunks/events.js +121 -0
  56. package/web/.svelte-kit/adapter-node/chunks/index.js +1 -1
  57. package/web/.svelte-kit/adapter-node/chunks/index2.js +186 -68
  58. package/web/.svelte-kit/adapter-node/chunks/internal.js +5 -90
  59. package/web/.svelte-kit/adapter-node/chunks/pane.js +82 -0
  60. package/web/.svelte-kit/adapter-node/chunks/sessions-json.js +16 -1
  61. package/web/.svelte-kit/adapter-node/chunks/sessions.svelte.js +174 -12
  62. package/web/.svelte-kit/adapter-node/chunks/ws-handlers.js +782 -0
  63. package/web/.svelte-kit/adapter-node/entries/endpoints/api/auth/login/_server.ts.js +22 -0
  64. package/web/.svelte-kit/adapter-node/entries/endpoints/api/auth/logout/_server.ts.js +9 -0
  65. package/web/.svelte-kit/adapter-node/entries/endpoints/api/beads/_server.ts.js +22 -0
  66. package/web/.svelte-kit/adapter-node/entries/endpoints/api/chrome/_server.ts.js +30 -0
  67. package/web/.svelte-kit/adapter-node/entries/endpoints/api/files/image/_server.ts.js +53 -0
  68. package/web/.svelte-kit/adapter-node/entries/endpoints/api/sessions/_id_/kill/_server.ts.js +12 -6
  69. package/web/.svelte-kit/adapter-node/entries/endpoints/api/sessions/_id_/restart/_server.ts.js +40 -0
  70. package/web/.svelte-kit/adapter-node/entries/endpoints/api/sessions/_id_/screenshots/_server.ts.js +28 -0
  71. package/web/.svelte-kit/adapter-node/entries/endpoints/api/sessions/_id_/send/_server.ts.js +11 -4
  72. package/web/.svelte-kit/adapter-node/entries/endpoints/api/sessions/_server.ts.js +1 -78
  73. package/web/.svelte-kit/adapter-node/entries/endpoints/api/sessions/_target_/output/_server.ts.js +2 -2
  74. package/web/.svelte-kit/adapter-node/entries/endpoints/api/tmux/panes/_server.ts.js +21 -0
  75. package/web/.svelte-kit/adapter-node/entries/fallbacks/error.svelte.js +1 -18
  76. package/web/.svelte-kit/adapter-node/entries/hooks.server.js +105 -0
  77. package/web/.svelte-kit/adapter-node/entries/pages/_layout.svelte.js +493 -6
  78. package/web/.svelte-kit/adapter-node/entries/pages/_page.svelte.js +3024 -54
  79. package/web/.svelte-kit/adapter-node/entries/pages/login/_page.server.ts.js +15 -0
  80. package/web/.svelte-kit/adapter-node/entries/pages/login/_page.svelte.js +37 -0
  81. package/web/.svelte-kit/adapter-node/entries/pages/session/_target_/_page.svelte.js +623 -46
  82. package/web/.svelte-kit/adapter-node/manifest-full.js +67 -3
  83. package/web/.svelte-kit/adapter-node/manifest.js +67 -3
  84. package/web/.svelte-kit/adapter-node/nodes/0.js +2 -2
  85. package/web/.svelte-kit/adapter-node/nodes/1.js +1 -1
  86. package/web/.svelte-kit/adapter-node/nodes/2.js +2 -2
  87. package/web/.svelte-kit/adapter-node/nodes/3.js +6 -4
  88. package/web/.svelte-kit/adapter-node/nodes/4.js +8 -0
  89. package/web/.svelte-kit/ambient.d.ts +28 -2
  90. package/web/.svelte-kit/generated/server/internal.js +1 -1
  91. package/web/.svelte-kit/output/client/.vite/manifest.json +44 -44
  92. package/web/.svelte-kit/output/client/_app/immutable/chunks/{DU91Ml7U.js → BGcEgn7w.js} +1 -1
  93. package/{dist/web/client/_app/immutable/chunks/DmdO6ygw.js → web/.svelte-kit/output/client/_app/immutable/chunks/By6CYjmE.js} +1 -1
  94. package/web/.svelte-kit/output/client/_app/immutable/chunks/{HKNo9LID.js → CR5jMWGV.js} +1 -1
  95. package/{dist/web/client/_app/immutable/entry/app.CGIBnoln.js → web/.svelte-kit/output/client/_app/immutable/entry/app.DmtnygN7.js} +2 -2
  96. package/web/.svelte-kit/output/client/_app/immutable/entry/start.fYmelGlC.js +1 -0
  97. package/web/.svelte-kit/output/client/_app/immutable/nodes/{0.CqlJ9a31.js → 0.DGDAdwT5.js} +1 -1
  98. package/web/.svelte-kit/output/client/_app/immutable/nodes/{1.BQUZh2-w.js → 1.Cg8dWgUN.js} +1 -1
  99. package/web/.svelte-kit/output/client/_app/immutable/nodes/{2.CCV1YdgF.js → 2.DItUEo3e.js} +1 -1
  100. package/web/.svelte-kit/output/client/_app/immutable/nodes/{3.D9tDCdq8.js → 3.dHui0twF.js} +1 -1
  101. package/web/.svelte-kit/output/client/_app/immutable/nodes/{4.BqPyNkFA.js → 4.CiEHP0cr.js} +1 -1
  102. package/web/.svelte-kit/output/client/_app/version.json +1 -1
  103. package/web/.svelte-kit/output/server/chunks/internal.js +1 -1
  104. package/web/.svelte-kit/output/server/manifest-full.js +1 -1
  105. package/web/.svelte-kit/output/server/manifest.js +1 -1
  106. package/web/.svelte-kit/output/server/nodes/0.js +1 -1
  107. package/web/.svelte-kit/output/server/nodes/1.js +1 -1
  108. package/web/.svelte-kit/output/server/nodes/2.js +1 -1
  109. package/web/.svelte-kit/output/server/nodes/3.js +1 -1
  110. package/web/.svelte-kit/output/server/nodes/4.js +1 -1
  111. package/dist/web/client/_app/immutable/entry/start.CJk8zB1j.js +0 -1
  112. package/web/.svelte-kit/adapter-node/_app/immutable/assets/_layout.4NiX29PU.css +0 -1
  113. package/web/.svelte-kit/adapter-node/_app/immutable/assets/_page.BEMzYUGV.css +0 -1
  114. package/web/.svelte-kit/adapter-node/_app/immutable/assets/_page.DOJn7TG7.css +0 -1
  115. package/web/.svelte-kit/output/client/_app/immutable/entry/start.CJk8zB1j.js +0 -1
@@ -0,0 +1,782 @@
1
+ import { execFileSync } from "child_process";
2
+ import { g as getAllSessions, a as updateSession } from "./sessions-json.js";
3
+ import { c as checkForInterruption, g as getPaneTitle } from "./pane.js";
4
+ import { readdirSync, statSync, existsSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+ function resizeTmuxWindow(target, cols, rows) {
8
+ try {
9
+ const safeCols = Math.max(20, Math.min(500, Math.floor(cols)));
10
+ const safeRows = Math.max(5, Math.min(200, Math.floor(rows)));
11
+ const windowTarget = target.replace(/\.\d+$/, "");
12
+ execFileSync(
13
+ "tmux",
14
+ ["resize-window", "-t", windowTarget, "-x", String(safeCols), "-y", String(safeRows)],
15
+ {
16
+ stdio: ["pipe", "pipe", "pipe"],
17
+ timeout: 2e3
18
+ }
19
+ );
20
+ } catch {
21
+ }
22
+ }
23
+ const CLAUDE_WATCH_DIR = join(homedir(), ".claude-watch");
24
+ const SESSIONS_DIR = join(CLAUDE_WATCH_DIR, "sessions");
25
+ join(CLAUDE_WATCH_DIR, "config.json");
26
+ const CLAUDE_DIR = join(homedir(), ".claude");
27
+ join(CLAUDE_DIR, "settings.json");
28
+ class SessionWatcher {
29
+ subscribers = /* @__PURE__ */ new Set();
30
+ pollTimer = null;
31
+ lastState = /* @__PURE__ */ new Map();
32
+ // filename -> mtime
33
+ pollInterval = 500;
34
+ start() {
35
+ if (this.pollTimer) return;
36
+ console.log("[watcher] Started polling", SESSIONS_DIR, `(${this.pollInterval}ms)`);
37
+ this.updateState();
38
+ this.pollTimer = setInterval(() => {
39
+ if (this.checkForChanges()) {
40
+ this.notifySubscribers();
41
+ }
42
+ }, this.pollInterval);
43
+ }
44
+ updateState() {
45
+ this.lastState.clear();
46
+ try {
47
+ const files = readdirSync(SESSIONS_DIR);
48
+ for (const file of files) {
49
+ if (!file.endsWith(".json")) continue;
50
+ try {
51
+ const stat = statSync(join(SESSIONS_DIR, file));
52
+ this.lastState.set(file, stat.mtimeMs);
53
+ } catch {
54
+ }
55
+ }
56
+ } catch {
57
+ }
58
+ }
59
+ checkForChanges() {
60
+ const newState = /* @__PURE__ */ new Map();
61
+ let changed = false;
62
+ try {
63
+ const files = readdirSync(SESSIONS_DIR);
64
+ for (const file of files) {
65
+ if (!file.endsWith(".json")) continue;
66
+ try {
67
+ const stat = statSync(join(SESSIONS_DIR, file));
68
+ newState.set(file, stat.mtimeMs);
69
+ const oldMtime = this.lastState.get(file);
70
+ if (oldMtime === void 0) {
71
+ console.log("[watcher] File added:", file);
72
+ changed = true;
73
+ } else if (oldMtime !== stat.mtimeMs) {
74
+ console.log("[watcher] File changed:", file);
75
+ changed = true;
76
+ }
77
+ } catch {
78
+ }
79
+ }
80
+ for (const file of this.lastState.keys()) {
81
+ if (!newState.has(file)) {
82
+ console.log("[watcher] File removed:", file);
83
+ changed = true;
84
+ }
85
+ }
86
+ } catch {
87
+ }
88
+ this.lastState = newState;
89
+ return changed;
90
+ }
91
+ notifySubscribers() {
92
+ console.log("[watcher] Notifying", this.subscribers.size, "subscribers");
93
+ for (const callback of this.subscribers) {
94
+ try {
95
+ callback();
96
+ } catch (error) {
97
+ console.error("[watcher] Subscriber error:", error);
98
+ }
99
+ }
100
+ }
101
+ subscribe(callback) {
102
+ this.subscribers.add(callback);
103
+ if (this.subscribers.size === 1) {
104
+ this.start();
105
+ }
106
+ return () => {
107
+ this.subscribers.delete(callback);
108
+ if (this.subscribers.size === 0) {
109
+ this.stop();
110
+ }
111
+ };
112
+ }
113
+ stop() {
114
+ if (this.pollTimer) {
115
+ clearInterval(this.pollTimer);
116
+ this.pollTimer = null;
117
+ }
118
+ this.lastState.clear();
119
+ console.log("[watcher] Stopped");
120
+ }
121
+ }
122
+ const sessionWatcher = new SessionWatcher();
123
+ const DEFAULT_CONFIG = {
124
+ maxSessionsClients: 50,
125
+ maxTerminalClientsPerTarget: 10,
126
+ maxTerminalClientsTotal: 100,
127
+ maxQueuedMessages: 100,
128
+ debug: false
129
+ };
130
+ function log(entry, config) {
131
+ if (entry.level === "debug" && !config.debug) return;
132
+ const prefix = `[ws:${entry.component}]`;
133
+ const msg = entry.data ? `${prefix} ${entry.message} ${JSON.stringify(entry.data)}` : `${prefix} ${entry.message}`;
134
+ switch (entry.level) {
135
+ case "debug":
136
+ console.debug(msg);
137
+ break;
138
+ case "info":
139
+ console.info(msg);
140
+ break;
141
+ case "warn":
142
+ console.warn(msg);
143
+ break;
144
+ case "error":
145
+ console.error(msg);
146
+ break;
147
+ }
148
+ }
149
+ function syncSessionStates() {
150
+ const sessions = getAllSessions().filter((s) => s.tmux_target);
151
+ for (const session of sessions) {
152
+ if (!session.tmux_target) continue;
153
+ const update = checkForInterruption(session.tmux_target);
154
+ if (update && session.state !== "idle") {
155
+ updateSession(session.id, update);
156
+ }
157
+ }
158
+ }
159
+ function deduplicateByTmuxTarget(sessions) {
160
+ const byTarget = /* @__PURE__ */ new Map();
161
+ const noTarget = [];
162
+ for (const session of sessions) {
163
+ if (!session.tmux_target) {
164
+ noTarget.push(session);
165
+ continue;
166
+ }
167
+ const existing = byTarget.get(session.tmux_target);
168
+ if (!existing || session.last_update > existing.last_update) {
169
+ byTarget.set(session.tmux_target, session);
170
+ }
171
+ }
172
+ return [...byTarget.values(), ...noTarget];
173
+ }
174
+ function getEnrichedSessions() {
175
+ syncSessionStates();
176
+ const sessions = getAllSessions();
177
+ const enrichedSessions = sessions.map((s) => ({
178
+ ...s,
179
+ pane_title: s.tmux_target ? getPaneTitle(s.tmux_target) : null
180
+ }));
181
+ return deduplicateByTmuxTarget(enrichedSessions);
182
+ }
183
+ function capturePaneOutput(target) {
184
+ try {
185
+ return execFileSync("tmux", ["capture-pane", "-t", target, "-p", "-S", "-100"], {
186
+ encoding: "utf-8",
187
+ stdio: ["pipe", "pipe", "pipe"],
188
+ timeout: 2e3
189
+ });
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+ function resizePane(target, cols, rows) {
195
+ resizeTmuxWindow(target, cols, rows);
196
+ }
197
+ class SessionsWsManager {
198
+ clients = /* @__PURE__ */ new Set();
199
+ unsubscribe = null;
200
+ interruptCheckTimer = null;
201
+ lastHash = "";
202
+ config;
203
+ droppedClients = 0;
204
+ constructor(config) {
205
+ this.config = { ...DEFAULT_CONFIG, ...config };
206
+ }
207
+ /** Get current stats */
208
+ getStats() {
209
+ return {
210
+ clients: this.clients.size,
211
+ droppedClients: this.droppedClients
212
+ };
213
+ }
214
+ /**
215
+ * Add a client. Returns false if max clients reached.
216
+ */
217
+ addClient(client) {
218
+ if (this.clients.size >= this.config.maxSessionsClients) {
219
+ log(
220
+ {
221
+ level: "warn",
222
+ component: "sessions",
223
+ message: "Max clients reached, rejecting connection",
224
+ data: { current: this.clients.size, max: this.config.maxSessionsClients }
225
+ },
226
+ this.config
227
+ );
228
+ return false;
229
+ }
230
+ this.clients.add(client);
231
+ log(
232
+ {
233
+ level: "debug",
234
+ component: "sessions",
235
+ message: "Client connected",
236
+ data: { total: this.clients.size }
237
+ },
238
+ this.config
239
+ );
240
+ if (this.clients.size === 1 && !this.unsubscribe) {
241
+ console.log("[ws:sessions] First client, subscribing to watcher");
242
+ this.unsubscribe = sessionWatcher.subscribe(() => this.broadcastIfChanged());
243
+ this.interruptCheckTimer = setInterval(() => {
244
+ syncSessionStates();
245
+ this.broadcastIfChanged();
246
+ }, 500);
247
+ } else {
248
+ console.log("[ws:sessions] addClient: clients=", this.clients.size, "hasUnsubscribe=", !!this.unsubscribe);
249
+ }
250
+ this.sendToClient(client, this.createMessage("connected"));
251
+ return true;
252
+ }
253
+ removeClient(client) {
254
+ const had = this.clients.delete(client);
255
+ if (had) {
256
+ log(
257
+ {
258
+ level: "debug",
259
+ component: "sessions",
260
+ message: "Client disconnected",
261
+ data: { total: this.clients.size }
262
+ },
263
+ this.config
264
+ );
265
+ }
266
+ if (this.clients.size === 0 && this.unsubscribe) {
267
+ this.unsubscribe();
268
+ this.unsubscribe = null;
269
+ this.lastHash = "";
270
+ if (this.interruptCheckTimer) {
271
+ clearInterval(this.interruptCheckTimer);
272
+ this.interruptCheckTimer = null;
273
+ }
274
+ }
275
+ }
276
+ createMessage(type) {
277
+ const sessions = getEnrichedSessions();
278
+ return { type, sessions, count: sessions.length, timestamp: Date.now() };
279
+ }
280
+ broadcastIfChanged() {
281
+ console.log("[ws:sessions] broadcastIfChanged called, clients:", this.clients.size);
282
+ const message = this.createMessage("sessions");
283
+ const hash = JSON.stringify(message.sessions);
284
+ if (hash === this.lastHash) {
285
+ console.log("[ws:sessions] Hash unchanged, skipping broadcast");
286
+ return;
287
+ }
288
+ console.log("[ws:sessions] Broadcasting to", this.clients.size, "clients");
289
+ this.lastHash = hash;
290
+ const data = JSON.stringify(message);
291
+ for (const client of this.clients) {
292
+ if (!this.sendToClient(client, message, data)) {
293
+ this.clients.delete(client);
294
+ this.droppedClients++;
295
+ log(
296
+ {
297
+ level: "warn",
298
+ component: "sessions",
299
+ message: "Dropped slow client",
300
+ data: { total: this.clients.size }
301
+ },
302
+ this.config
303
+ );
304
+ }
305
+ }
306
+ }
307
+ sendToClient(client, _message, data) {
308
+ try {
309
+ if (!client.isOpen()) return false;
310
+ if (client.getBufferedAmount) {
311
+ const buffered = client.getBufferedAmount();
312
+ if (buffered > 64 * 1024) {
313
+ log(
314
+ {
315
+ level: "warn",
316
+ component: "sessions",
317
+ message: "Client backpressure detected",
318
+ data: { buffered }
319
+ },
320
+ this.config
321
+ );
322
+ return false;
323
+ }
324
+ }
325
+ client.send(data ?? JSON.stringify(_message));
326
+ return true;
327
+ } catch (err) {
328
+ log(
329
+ {
330
+ level: "error",
331
+ component: "sessions",
332
+ message: "Failed to send to client",
333
+ data: { error: String(err) }
334
+ },
335
+ this.config
336
+ );
337
+ return false;
338
+ }
339
+ }
340
+ }
341
+ class TerminalWsManager {
342
+ clients = /* @__PURE__ */ new Map();
343
+ pollTimers = /* @__PURE__ */ new Map();
344
+ lastOutput = /* @__PURE__ */ new Map();
345
+ config;
346
+ totalClients = 0;
347
+ droppedClients = 0;
348
+ constructor(config) {
349
+ this.config = { ...DEFAULT_CONFIG, ...config };
350
+ }
351
+ /** Get current stats */
352
+ getStats() {
353
+ return {
354
+ totalClients: this.totalClients,
355
+ targets: this.clients.size,
356
+ droppedClients: this.droppedClients
357
+ };
358
+ }
359
+ /**
360
+ * Add a client for a target. Returns false if max clients reached.
361
+ */
362
+ addClient(client, target) {
363
+ if (this.totalClients >= this.config.maxTerminalClientsTotal) {
364
+ log(
365
+ {
366
+ level: "warn",
367
+ component: "terminal",
368
+ message: "Max total clients reached",
369
+ data: { current: this.totalClients, max: this.config.maxTerminalClientsTotal }
370
+ },
371
+ this.config
372
+ );
373
+ return false;
374
+ }
375
+ const targetClients = this.clients.get(target);
376
+ if (targetClients && targetClients.size >= this.config.maxTerminalClientsPerTarget) {
377
+ log(
378
+ {
379
+ level: "warn",
380
+ component: "terminal",
381
+ message: "Max clients per target reached",
382
+ data: { target, current: targetClients.size, max: this.config.maxTerminalClientsPerTarget }
383
+ },
384
+ this.config
385
+ );
386
+ return false;
387
+ }
388
+ if (!this.clients.has(target)) {
389
+ this.clients.set(target, /* @__PURE__ */ new Set());
390
+ }
391
+ this.clients.get(target).add(client);
392
+ this.totalClients++;
393
+ log(
394
+ {
395
+ level: "debug",
396
+ component: "terminal",
397
+ message: "Client connected",
398
+ data: { target, targetClients: this.clients.get(target).size, total: this.totalClients }
399
+ },
400
+ this.config
401
+ );
402
+ if (this.clients.get(target).size === 1) {
403
+ this.startPolling(target);
404
+ }
405
+ const output = capturePaneOutput(target) ?? "";
406
+ this.sendToClient(client, { type: "output", output, timestamp: Date.now() });
407
+ return true;
408
+ }
409
+ removeClient(client, target) {
410
+ if (target) {
411
+ const targetClients = this.clients.get(target);
412
+ if (targetClients && targetClients.delete(client)) {
413
+ this.totalClients--;
414
+ log(
415
+ {
416
+ level: "debug",
417
+ component: "terminal",
418
+ message: "Client disconnected",
419
+ data: { target, targetClients: targetClients.size, total: this.totalClients }
420
+ },
421
+ this.config
422
+ );
423
+ if (targetClients.size === 0) {
424
+ this.stopPolling(target);
425
+ this.clients.delete(target);
426
+ this.lastOutput.delete(target);
427
+ }
428
+ }
429
+ } else {
430
+ for (const [t, clients] of this.clients) {
431
+ if (clients.delete(client)) {
432
+ this.totalClients--;
433
+ if (clients.size === 0) {
434
+ this.stopPolling(t);
435
+ this.clients.delete(t);
436
+ this.lastOutput.delete(t);
437
+ }
438
+ break;
439
+ }
440
+ }
441
+ }
442
+ }
443
+ startPolling(target) {
444
+ if (this.pollTimers.has(target)) return;
445
+ const timer = setInterval(() => this.pollAndBroadcast(target), 200);
446
+ this.pollTimers.set(target, timer);
447
+ }
448
+ stopPolling(target) {
449
+ const timer = this.pollTimers.get(target);
450
+ if (timer) {
451
+ clearInterval(timer);
452
+ this.pollTimers.delete(target);
453
+ }
454
+ }
455
+ pollAndBroadcast(target) {
456
+ const output = capturePaneOutput(target) ?? "";
457
+ const lastOutput = this.lastOutput.get(target) ?? "";
458
+ if (output === lastOutput) return;
459
+ this.lastOutput.set(target, output);
460
+ const message = { type: "output", output, timestamp: Date.now() };
461
+ const data = JSON.stringify(message);
462
+ const clients = this.clients.get(target);
463
+ if (clients) {
464
+ for (const client of clients) {
465
+ if (!this.sendToClient(client, message, data)) {
466
+ clients.delete(client);
467
+ this.totalClients--;
468
+ this.droppedClients++;
469
+ log(
470
+ {
471
+ level: "warn",
472
+ component: "terminal",
473
+ message: "Dropped slow client",
474
+ data: { target, total: this.totalClients }
475
+ },
476
+ this.config
477
+ );
478
+ }
479
+ }
480
+ if (clients.size === 0) {
481
+ this.stopPolling(target);
482
+ this.clients.delete(target);
483
+ this.lastOutput.delete(target);
484
+ }
485
+ }
486
+ }
487
+ sendToClient(client, _message, data) {
488
+ try {
489
+ if (!client.isOpen()) return false;
490
+ if (client.getBufferedAmount) {
491
+ const buffered = client.getBufferedAmount();
492
+ if (buffered > 64 * 1024) {
493
+ log(
494
+ {
495
+ level: "warn",
496
+ component: "terminal",
497
+ message: "Client backpressure detected",
498
+ data: { buffered }
499
+ },
500
+ this.config
501
+ );
502
+ return false;
503
+ }
504
+ }
505
+ client.send(data ?? JSON.stringify(_message));
506
+ return true;
507
+ } catch (err) {
508
+ log(
509
+ {
510
+ level: "error",
511
+ component: "terminal",
512
+ message: "Failed to send to client",
513
+ data: { error: String(err) }
514
+ },
515
+ this.config
516
+ );
517
+ return false;
518
+ }
519
+ }
520
+ }
521
+ function handleWsMessage(msgStr, onResize) {
522
+ if (msgStr === "ping") {
523
+ return "pong";
524
+ }
525
+ if (onResize) {
526
+ try {
527
+ const msg = JSON.parse(msgStr);
528
+ if (msg.type === "resize" && typeof msg.cols === "number" && typeof msg.rows === "number") {
529
+ onResize(msg.cols, msg.rows);
530
+ }
531
+ } catch {
532
+ }
533
+ }
534
+ return null;
535
+ }
536
+ function getBeadsIssues(projectPath) {
537
+ try {
538
+ const result = execFileSync("bd", ["list", "--json"], {
539
+ encoding: "utf-8",
540
+ cwd: projectPath,
541
+ stdio: ["pipe", "pipe", "pipe"],
542
+ timeout: 5e3
543
+ });
544
+ return JSON.parse(result);
545
+ } catch {
546
+ return [];
547
+ }
548
+ }
549
+ class BeadsProjectWatcher {
550
+ subscribers = /* @__PURE__ */ new Map();
551
+ pollTimers = /* @__PURE__ */ new Map();
552
+ lastMtime = /* @__PURE__ */ new Map();
553
+ pollInterval = 1e3;
554
+ /**
555
+ * Subscribe to changes in a project's beads issues
556
+ */
557
+ subscribe(projectPath, callback) {
558
+ if (!this.subscribers.has(projectPath)) {
559
+ this.subscribers.set(projectPath, /* @__PURE__ */ new Set());
560
+ }
561
+ this.subscribers.get(projectPath).add(callback);
562
+ if (this.subscribers.get(projectPath).size === 1) {
563
+ this.startWatching(projectPath);
564
+ }
565
+ return () => {
566
+ const subs = this.subscribers.get(projectPath);
567
+ if (subs) {
568
+ subs.delete(callback);
569
+ if (subs.size === 0) {
570
+ this.stopWatching(projectPath);
571
+ this.subscribers.delete(projectPath);
572
+ }
573
+ }
574
+ };
575
+ }
576
+ startWatching(projectPath) {
577
+ const issuesFile = join(projectPath, ".beads", "issues.jsonl");
578
+ try {
579
+ if (existsSync(issuesFile)) {
580
+ this.lastMtime.set(projectPath, statSync(issuesFile).mtimeMs);
581
+ }
582
+ } catch {
583
+ }
584
+ console.log("[beads-watcher] Started watching", projectPath);
585
+ const timer = setInterval(() => {
586
+ this.checkForChanges(projectPath, issuesFile);
587
+ }, this.pollInterval);
588
+ this.pollTimers.set(projectPath, timer);
589
+ }
590
+ stopWatching(projectPath) {
591
+ const timer = this.pollTimers.get(projectPath);
592
+ if (timer) {
593
+ clearInterval(timer);
594
+ this.pollTimers.delete(projectPath);
595
+ }
596
+ this.lastMtime.delete(projectPath);
597
+ console.log("[beads-watcher] Stopped watching", projectPath);
598
+ }
599
+ checkForChanges(projectPath, issuesFile) {
600
+ try {
601
+ if (!existsSync(issuesFile)) {
602
+ if (this.lastMtime.has(projectPath)) {
603
+ this.lastMtime.delete(projectPath);
604
+ this.notifySubscribers(projectPath);
605
+ }
606
+ return;
607
+ }
608
+ const currentMtime = statSync(issuesFile).mtimeMs;
609
+ const lastMtime = this.lastMtime.get(projectPath);
610
+ if (lastMtime === void 0 || currentMtime !== lastMtime) {
611
+ this.lastMtime.set(projectPath, currentMtime);
612
+ if (lastMtime !== void 0) {
613
+ console.log("[beads-watcher] Issues changed in", projectPath);
614
+ this.notifySubscribers(projectPath);
615
+ }
616
+ }
617
+ } catch {
618
+ }
619
+ }
620
+ notifySubscribers(projectPath) {
621
+ const subs = this.subscribers.get(projectPath);
622
+ if (subs) {
623
+ for (const callback of subs) {
624
+ try {
625
+ callback();
626
+ } catch (error) {
627
+ console.error("[beads-watcher] Subscriber error:", error);
628
+ }
629
+ }
630
+ }
631
+ }
632
+ }
633
+ const beadsWatcher = new BeadsProjectWatcher();
634
+ class BeadsWsManager {
635
+ clients = /* @__PURE__ */ new Map();
636
+ unsubscribes = /* @__PURE__ */ new Map();
637
+ lastHash = /* @__PURE__ */ new Map();
638
+ config;
639
+ totalClients = 0;
640
+ constructor(config) {
641
+ this.config = { ...DEFAULT_CONFIG, ...config };
642
+ }
643
+ getStats() {
644
+ return {
645
+ totalClients: this.totalClients,
646
+ projects: this.clients.size
647
+ };
648
+ }
649
+ addClient(client, projectPath) {
650
+ if (!projectPath) return false;
651
+ if (!this.clients.has(projectPath)) {
652
+ this.clients.set(projectPath, /* @__PURE__ */ new Set());
653
+ }
654
+ this.clients.get(projectPath).add(client);
655
+ this.totalClients++;
656
+ log(
657
+ {
658
+ level: "debug",
659
+ component: "beads",
660
+ message: "Client connected",
661
+ data: { project: projectPath, total: this.totalClients }
662
+ },
663
+ this.config
664
+ );
665
+ if (this.clients.get(projectPath).size === 1 && !this.unsubscribes.has(projectPath)) {
666
+ console.log("[ws:beads] First client for project, subscribing to watcher");
667
+ const unsub = beadsWatcher.subscribe(
668
+ projectPath,
669
+ () => this.broadcastIfChanged(projectPath)
670
+ );
671
+ this.unsubscribes.set(projectPath, unsub);
672
+ }
673
+ this.sendToClient(client, this.createMessage(projectPath, "connected"));
674
+ return true;
675
+ }
676
+ removeClient(client, projectPath) {
677
+ if (projectPath) {
678
+ const projectClients = this.clients.get(projectPath);
679
+ if (projectClients && projectClients.delete(client)) {
680
+ this.totalClients--;
681
+ if (projectClients.size === 0) {
682
+ this.clients.delete(projectPath);
683
+ this.lastHash.delete(projectPath);
684
+ const unsub = this.unsubscribes.get(projectPath);
685
+ if (unsub) {
686
+ unsub();
687
+ this.unsubscribes.delete(projectPath);
688
+ }
689
+ }
690
+ }
691
+ } else {
692
+ for (const [project, clients] of this.clients) {
693
+ if (clients.delete(client)) {
694
+ this.totalClients--;
695
+ if (clients.size === 0) {
696
+ this.clients.delete(project);
697
+ this.lastHash.delete(project);
698
+ const unsub = this.unsubscribes.get(project);
699
+ if (unsub) {
700
+ unsub();
701
+ this.unsubscribes.delete(project);
702
+ }
703
+ }
704
+ break;
705
+ }
706
+ }
707
+ }
708
+ }
709
+ createMessage(projectPath, type) {
710
+ const issues = getBeadsIssues(projectPath);
711
+ return { type, issues, project: projectPath, timestamp: Date.now() };
712
+ }
713
+ broadcastIfChanged(projectPath) {
714
+ const message = this.createMessage(projectPath, "issues");
715
+ const hash = JSON.stringify(message.issues);
716
+ if (hash === this.lastHash.get(projectPath)) {
717
+ return;
718
+ }
719
+ this.lastHash.set(projectPath, hash);
720
+ const data = JSON.stringify(message);
721
+ const clients = this.clients.get(projectPath);
722
+ if (clients) {
723
+ console.log("[ws:beads] Broadcasting to", clients.size, "clients for", projectPath);
724
+ for (const client of clients) {
725
+ if (!this.sendToClient(client, message, data)) {
726
+ clients.delete(client);
727
+ this.totalClients--;
728
+ }
729
+ }
730
+ if (clients.size === 0) {
731
+ this.clients.delete(projectPath);
732
+ this.lastHash.delete(projectPath);
733
+ const unsub = this.unsubscribes.get(projectPath);
734
+ if (unsub) {
735
+ unsub();
736
+ this.unsubscribes.delete(projectPath);
737
+ }
738
+ }
739
+ }
740
+ }
741
+ sendToClient(client, _message, data) {
742
+ try {
743
+ if (!client.isOpen()) return false;
744
+ if (client.getBufferedAmount) {
745
+ const buffered = client.getBufferedAmount();
746
+ if (buffered > 64 * 1024) {
747
+ return false;
748
+ }
749
+ }
750
+ client.send(data ?? JSON.stringify(_message));
751
+ return true;
752
+ } catch {
753
+ return false;
754
+ }
755
+ }
756
+ }
757
+ function parseWsPath(pathname, searchParams) {
758
+ if (pathname === "/api/sessions/stream") {
759
+ return { type: "sessions" };
760
+ }
761
+ if (pathname === "/api/beads/stream") {
762
+ const project = searchParams?.get("project");
763
+ if (project) {
764
+ return { type: "beads", project: decodeURIComponent(project) };
765
+ }
766
+ return null;
767
+ }
768
+ const termMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/stream$/);
769
+ if (termMatch) {
770
+ return { type: "terminal", target: decodeURIComponent(termMatch[1]) };
771
+ }
772
+ return null;
773
+ }
774
+ export {
775
+ BeadsWsManager as B,
776
+ SessionsWsManager as S,
777
+ TerminalWsManager as T,
778
+ getBeadsIssues as g,
779
+ handleWsMessage as h,
780
+ parseWsPath as p,
781
+ resizePane as r
782
+ };