claude-code-hub 0.41.0 → 0.42.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.
package/lib/cookies.js ADDED
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ // Returns the Cookie header value without the named cookie, or '' when nothing is left.
4
+ function stripCookie(header, name) {
5
+ if (typeof header !== 'string') return '';
6
+ return header
7
+ .split(';')
8
+ .map((part) => part.trim())
9
+ .filter((part) => part && part.split('=', 1)[0].trim() !== name)
10
+ .join('; ');
11
+ }
12
+
13
+ module.exports = { stripCookie };
@@ -0,0 +1,548 @@
1
+ [
2
+ {
3
+ "id": "ember",
4
+ "label": "Ember",
5
+ "dark": {
6
+ "ember": "#e86f33",
7
+ "emberGlow": "#f0a070",
8
+ "emberDim": "rgba(232,111,51,0.25)",
9
+ "field": "#101114",
10
+ "surface": "#16181c",
11
+ "elevated": "#1e2025",
12
+ "hover": "#282a30",
13
+ "border": "#363840",
14
+ "ink1": "#f0f1f3",
15
+ "ink2": "#c2c4c9",
16
+ "ink3": "#9a9da5",
17
+ "inkMuted": "#7d808a"
18
+ },
19
+ "light": {
20
+ "ember": "#e86f33",
21
+ "emberGlow": "#a0501c",
22
+ "emberDim": "rgba(184,90,32,0.18)",
23
+ "field": "#e8e6e3",
24
+ "surface": "#efede9",
25
+ "elevated": "#fbfaf9",
26
+ "hover": "#d2d0cc",
27
+ "border": "#cfcbc4",
28
+ "ink1": "#0a0a0a",
29
+ "ink2": "#2c2c2c",
30
+ "ink3": "#565656",
31
+ "inkMuted": "#767676"
32
+ }
33
+ },
34
+ {
35
+ "id": "gruvbox",
36
+ "label": "Gruvbox",
37
+ "dark": {
38
+ "ember": "#fe8019",
39
+ "emberGlow": "#f9b27c",
40
+ "emberDim": "rgba(254,128,25,0.25)",
41
+ "field": "#1d2021",
42
+ "surface": "#282828",
43
+ "elevated": "#32302f",
44
+ "hover": "#3c3836",
45
+ "border": "#504945",
46
+ "ink1": "#ebdbb2",
47
+ "ink2": "#d5c4a1",
48
+ "ink3": "#bdae93",
49
+ "inkMuted": "#928374"
50
+ },
51
+ "light": {
52
+ "ember": "#d65d0e",
53
+ "emberGlow": "#af3a03",
54
+ "emberDim": "rgba(214,93,14,0.18)",
55
+ "field": "#ede7d5",
56
+ "surface": "#f3eee0",
57
+ "elevated": "#faf7ec",
58
+ "hover": "#e4dcc6",
59
+ "border": "#c9bc9d",
60
+ "ink1": "#3c3836",
61
+ "ink2": "#504945",
62
+ "ink3": "#665c54",
63
+ "inkMuted": "#7c6f64"
64
+ }
65
+ },
66
+ {
67
+ "id": "catppuccin",
68
+ "label": "Catppuccin",
69
+ "dark": {
70
+ "ember": "#cba6f7",
71
+ "emberGlow": "#d8c2fa",
72
+ "emberDim": "rgba(203,166,247,0.25)",
73
+ "field": "#11111b",
74
+ "surface": "#181825",
75
+ "elevated": "#1e1e2e",
76
+ "hover": "#313244",
77
+ "border": "#45475a",
78
+ "ink1": "#cdd6f4",
79
+ "ink2": "#bac2de",
80
+ "ink3": "#a6adc8",
81
+ "inkMuted": "#7f849c"
82
+ },
83
+ "light": {
84
+ "ember": "#8839ef",
85
+ "emberGlow": "#6f2dbd",
86
+ "emberDim": "rgba(136,57,239,0.18)",
87
+ "field": "#e6e9ef",
88
+ "surface": "#eff1f5",
89
+ "elevated": "#ffffff",
90
+ "hover": "#ccd0da",
91
+ "border": "#bcc0cc",
92
+ "ink1": "#4c4f69",
93
+ "ink2": "#5c5f77",
94
+ "ink3": "#6c6f85",
95
+ "inkMuted": "#8c8fa1"
96
+ }
97
+ },
98
+ {
99
+ "id": "tokyo-night",
100
+ "label": "Tokyo Night",
101
+ "dark": {
102
+ "ember": "#7aa2f7",
103
+ "emberGlow": "#9ab8ff",
104
+ "emberDim": "rgba(122,162,247,0.25)",
105
+ "field": "#16161e",
106
+ "surface": "#1a1b26",
107
+ "elevated": "#1f2335",
108
+ "hover": "#292e42",
109
+ "border": "#3b4261",
110
+ "ink1": "#c0caf5",
111
+ "ink2": "#a9b1d6",
112
+ "ink3": "#787c99",
113
+ "inkMuted": "#565f89"
114
+ },
115
+ "light": {
116
+ "ember": "#2e7de9",
117
+ "emberGlow": "#1659c7",
118
+ "emberDim": "rgba(46,125,233,0.18)",
119
+ "field": "#e1e2e7",
120
+ "surface": "#e9eaf0",
121
+ "elevated": "#f7f8fc",
122
+ "hover": "#d0d5e3",
123
+ "border": "#a8aecb",
124
+ "ink1": "#343b58",
125
+ "ink2": "#565a6e",
126
+ "ink3": "#6c6e75",
127
+ "inkMuted": "#848cb5"
128
+ }
129
+ },
130
+ {
131
+ "id": "solarized",
132
+ "label": "Solarized",
133
+ "dark": {
134
+ "ember": "#c96f47",
135
+ "emberGlow": "#d48a6a",
136
+ "emberDim": "rgba(201,111,71,0.25)",
137
+ "field": "#002b36",
138
+ "surface": "#073642",
139
+ "elevated": "#0a4250",
140
+ "hover": "#11505f",
141
+ "border": "#586e75",
142
+ "ink1": "#e2dfd0",
143
+ "ink2": "#b7c0bf",
144
+ "ink3": "#93a1a1",
145
+ "inkMuted": "#839496"
146
+ },
147
+ "light": {
148
+ "ember": "#2280c2",
149
+ "emberGlow": "#1b6aa3",
150
+ "emberDim": "rgba(34,128,194,0.18)",
151
+ "field": "#eee8d5",
152
+ "sidebar": "#e6dfc7",
153
+ "sidebarItem": "#f3ecd6",
154
+ "surface": "#fdf6e3",
155
+ "elevated": "#fffbef",
156
+ "hover": "#efe8d3",
157
+ "border": "#cdc4ab",
158
+ "ink1": "#073642",
159
+ "ink2": "#4f6369",
160
+ "ink3": "#586e75",
161
+ "inkMuted": "#728589"
162
+ }
163
+ },
164
+ {
165
+ "id": "dracula",
166
+ "label": "Dracula",
167
+ "dark": {
168
+ "ember": "#bd93f9",
169
+ "emberGlow": "#d6b5ff",
170
+ "emberDim": "rgba(189,147,249,0.25)",
171
+ "field": "#21222c",
172
+ "surface": "#282a36",
173
+ "elevated": "#343746",
174
+ "hover": "#44475a",
175
+ "border": "#565a75",
176
+ "ink1": "#f8f8f2",
177
+ "ink2": "#d8d8d2",
178
+ "ink3": "#b0b3c5",
179
+ "inkMuted": "#6272a4"
180
+ },
181
+ "light": {
182
+ "ember": "#7c3aed",
183
+ "emberGlow": "#5b21b6",
184
+ "emberDim": "rgba(124,58,237,0.18)",
185
+ "field": "#e8e8ee",
186
+ "surface": "#f0f0f5",
187
+ "elevated": "#fbfbfd",
188
+ "hover": "#dfdfe8",
189
+ "border": "#c9c9d6",
190
+ "ink1": "#16161e",
191
+ "ink2": "#34343f",
192
+ "ink3": "#5a5a68",
193
+ "inkMuted": "#787885"
194
+ }
195
+ },
196
+ {
197
+ "id": "nord",
198
+ "label": "Nord",
199
+ "dark": {
200
+ "ember": "#88c0d0",
201
+ "emberGlow": "#a8d8e8",
202
+ "emberDim": "rgba(136,192,208,0.25)",
203
+ "field": "#272c36",
204
+ "surface": "#2e3440",
205
+ "elevated": "#3b4252",
206
+ "hover": "#434c5e",
207
+ "border": "#4c566a",
208
+ "ink1": "#eceff4",
209
+ "ink2": "#d8dee9",
210
+ "ink3": "#aeb6c5",
211
+ "inkMuted": "#7b88a1"
212
+ },
213
+ "light": {
214
+ "ember": "#5e81ac",
215
+ "emberGlow": "#44688f",
216
+ "emberDim": "rgba(94,129,172,0.18)",
217
+ "field": "#e5e9f0",
218
+ "surface": "#eceff4",
219
+ "elevated": "#f8f9fb",
220
+ "hover": "#d8dee9",
221
+ "border": "#c2c9d6",
222
+ "ink1": "#2e3440",
223
+ "ink2": "#3b4252",
224
+ "ink3": "#4c566a",
225
+ "inkMuted": "#6b7589"
226
+ }
227
+ },
228
+ {
229
+ "id": "rose-pine",
230
+ "label": "Rosé Pine",
231
+ "dark": {
232
+ "ember": "#c4a7e7",
233
+ "emberGlow": "#d9c4f2",
234
+ "emberDim": "rgba(196,167,231,0.25)",
235
+ "field": "#191724",
236
+ "surface": "#1f1d2e",
237
+ "elevated": "#26233a",
238
+ "hover": "#34304e",
239
+ "border": "#403d52",
240
+ "ink1": "#e0def4",
241
+ "ink2": "#c5c2dd",
242
+ "ink3": "#908caa",
243
+ "inkMuted": "#6e6a86"
244
+ },
245
+ "light": {
246
+ "ember": "#907aa9",
247
+ "emberGlow": "#6f598c",
248
+ "emberDim": "rgba(144,122,169,0.18)",
249
+ "field": "#f2e9e1",
250
+ "surface": "#faf4ed",
251
+ "elevated": "#fffaf3",
252
+ "hover": "#ebdfd4",
253
+ "border": "#cecacd",
254
+ "ink1": "#575279",
255
+ "ink2": "#635e87",
256
+ "ink3": "#6e6a88",
257
+ "inkMuted": "#857f96"
258
+ }
259
+ },
260
+ {
261
+ "id": "everforest",
262
+ "label": "Everforest",
263
+ "dark": {
264
+ "ember": "#a7c080",
265
+ "emberGlow": "#c3d6a2",
266
+ "emberDim": "rgba(167,192,128,0.25)",
267
+ "field": "#232a2e",
268
+ "surface": "#2d353b",
269
+ "elevated": "#343f44",
270
+ "hover": "#3d484d",
271
+ "border": "#475258",
272
+ "ink1": "#d3c6aa",
273
+ "ink2": "#bfb6a3",
274
+ "ink3": "#9da9a0",
275
+ "inkMuted": "#859289"
276
+ },
277
+ "light": {
278
+ "ember": "#6f8352",
279
+ "emberGlow": "#56683f",
280
+ "emberDim": "rgba(111,131,82,0.18)",
281
+ "field": "#ebeadb",
282
+ "surface": "#f5f4e8",
283
+ "elevated": "#fbfbf4",
284
+ "hover": "#e5e4d3",
285
+ "border": "#d3d2bf",
286
+ "ink1": "#3d484e",
287
+ "ink2": "#55636b",
288
+ "ink3": "#66705f",
289
+ "inkMuted": "#7a8577"
290
+ }
291
+ },
292
+ {
293
+ "id": "kanagawa",
294
+ "label": "Kanagawa",
295
+ "dark": {
296
+ "ember": "#7e9cd8",
297
+ "emberGlow": "#9fb9ec",
298
+ "emberDim": "rgba(126,156,216,0.25)",
299
+ "field": "#16161d",
300
+ "surface": "#1f1f28",
301
+ "elevated": "#2a2a37",
302
+ "hover": "#363646",
303
+ "border": "#54546d",
304
+ "ink1": "#dcd7ba",
305
+ "ink2": "#c8c093",
306
+ "ink3": "#a6a69c",
307
+ "inkMuted": "#727169"
308
+ },
309
+ "light": {
310
+ "ember": "#4d699b",
311
+ "emberGlow": "#38537f",
312
+ "emberDim": "rgba(77,105,155,0.18)",
313
+ "field": "#e9e5d6",
314
+ "surface": "#f3f0e4",
315
+ "elevated": "#faf8f1",
316
+ "hover": "#e2ddcb",
317
+ "border": "#d2ccb6",
318
+ "ink1": "#545464",
319
+ "ink2": "#66667a",
320
+ "ink3": "#716e61",
321
+ "inkMuted": "#8a8775"
322
+ }
323
+ },
324
+ {
325
+ "id": "one-dark",
326
+ "label": "One Dark",
327
+ "dark": {
328
+ "ember": "#61afef",
329
+ "emberGlow": "#8cc7ff",
330
+ "emberDim": "rgba(97,175,239,0.25)",
331
+ "field": "#21252b",
332
+ "surface": "#282c34",
333
+ "elevated": "#2f343e",
334
+ "hover": "#3e4452",
335
+ "border": "#4b5263",
336
+ "ink1": "#d7dae0",
337
+ "ink2": "#abb2bf",
338
+ "ink3": "#8b919e",
339
+ "inkMuted": "#5c6370"
340
+ },
341
+ "light": {
342
+ "ember": "#4078f2",
343
+ "emberGlow": "#2c5dd4",
344
+ "emberDim": "rgba(64,120,242,0.18)",
345
+ "field": "#eaeaeb",
346
+ "surface": "#fafafa",
347
+ "elevated": "#ffffff",
348
+ "hover": "#e0e0e2",
349
+ "border": "#d4d4d6",
350
+ "ink1": "#383a42",
351
+ "ink2": "#50525a",
352
+ "ink3": "#696c77",
353
+ "inkMuted": "#a0a1a7"
354
+ }
355
+ },
356
+ {
357
+ "id": "night-owl",
358
+ "label": "Night Owl",
359
+ "dark": {
360
+ "ember": "#82aaff",
361
+ "emberGlow": "#a8c4ff",
362
+ "emberDim": "rgba(130,170,255,0.25)",
363
+ "field": "#011627",
364
+ "surface": "#0b2942",
365
+ "elevated": "#13344f",
366
+ "hover": "#1d3b53",
367
+ "border": "#5f7e97",
368
+ "ink1": "#d6deeb",
369
+ "ink2": "#b8c5d6",
370
+ "ink3": "#8ba3b8",
371
+ "inkMuted": "#637777"
372
+ },
373
+ "light": {
374
+ "ember": "#0c969b",
375
+ "emberGlow": "#0a7479",
376
+ "emberDim": "rgba(12,150,155,0.18)",
377
+ "field": "#ededed",
378
+ "surface": "#f6f6f6",
379
+ "elevated": "#fbfbfb",
380
+ "hover": "#e2e2e2",
381
+ "border": "#d0d0d0",
382
+ "ink1": "#403f53",
383
+ "ink2": "#545167",
384
+ "ink3": "#6f6e85",
385
+ "inkMuted": "#989fb1"
386
+ }
387
+ },
388
+ {
389
+ "id": "monokai",
390
+ "label": "Monokai Pro",
391
+ "dark": {
392
+ "ember": "#ffd866",
393
+ "emberGlow": "#ffe6a0",
394
+ "emberDim": "rgba(255,216,102,0.25)",
395
+ "field": "#221f22",
396
+ "surface": "#2d2a2e",
397
+ "elevated": "#403e41",
398
+ "hover": "#4a484b",
399
+ "border": "#5b595c",
400
+ "ink1": "#fcfcfa",
401
+ "ink2": "#d9d8d6",
402
+ "ink3": "#939293",
403
+ "inkMuted": "#727072"
404
+ },
405
+ "light": {
406
+ "ember": "#c08a00",
407
+ "emberGlow": "#856000",
408
+ "emberDim": "rgba(192,138,0,0.18)",
409
+ "field": "#e9e6e4",
410
+ "surface": "#f1efed",
411
+ "elevated": "#fcfbfa",
412
+ "hover": "#e0dcda",
413
+ "border": "#cdc8c5",
414
+ "ink1": "#2c292d",
415
+ "ink2": "#46434a",
416
+ "ink3": "#6b686d",
417
+ "inkMuted": "#8d8a8d"
418
+ }
419
+ },
420
+ {
421
+ "id": "github",
422
+ "label": "GitHub",
423
+ "dark": {
424
+ "ember": "#58a6ff",
425
+ "emberGlow": "#85bdff",
426
+ "emberDim": "rgba(88,166,255,0.25)",
427
+ "field": "#1f2428",
428
+ "surface": "#24292e",
429
+ "elevated": "#2b3138",
430
+ "hover": "#2f363d",
431
+ "border": "#444d56",
432
+ "ink1": "#e1e4e8",
433
+ "ink2": "#d1d5da",
434
+ "ink3": "#959da5",
435
+ "inkMuted": "#6a737d"
436
+ },
437
+ "light": {
438
+ "ember": "#0366d6",
439
+ "emberGlow": "#044289",
440
+ "emberDim": "rgba(3,102,214,0.18)",
441
+ "field": "#f0f2f4",
442
+ "surface": "#f6f8fa",
443
+ "elevated": "#ffffff",
444
+ "hover": "#e8eaed",
445
+ "border": "#d1d5da",
446
+ "ink1": "#24292e",
447
+ "ink2": "#444d56",
448
+ "ink3": "#586069",
449
+ "inkMuted": "#6a737d"
450
+ }
451
+ },
452
+ {
453
+ "id": "ayu",
454
+ "label": "Ayu",
455
+ "dark": {
456
+ "ember": "#e6b450",
457
+ "emberGlow": "#f0cd85",
458
+ "emberDim": "rgba(230,180,80,0.25)",
459
+ "field": "#0d1017",
460
+ "surface": "#10141c",
461
+ "elevated": "#141821",
462
+ "hover": "#1d2330",
463
+ "border": "#2d3343",
464
+ "ink1": "#bfbdb6",
465
+ "ink2": "#a8a6a0",
466
+ "ink3": "#8a9199",
467
+ "inkMuted": "#6c7380"
468
+ },
469
+ "light": {
470
+ "ember": "#c97612",
471
+ "emberGlow": "#a65e0c",
472
+ "emberDim": "rgba(201,118,18,0.18)",
473
+ "field": "#eff1f3",
474
+ "surface": "#f8f9fa",
475
+ "elevated": "#fcfcfc",
476
+ "hover": "#e7eaed",
477
+ "border": "#d8dde2",
478
+ "ink1": "#3d4149",
479
+ "ink2": "#5c6166",
480
+ "ink3": "#787b80",
481
+ "inkMuted": "#8a9199"
482
+ }
483
+ },
484
+ {
485
+ "id": "vitesse",
486
+ "label": "Vitesse",
487
+ "dark": {
488
+ "ember": "#4d9375",
489
+ "emberGlow": "#6fb392",
490
+ "emberDim": "rgba(77,147,117,0.25)",
491
+ "field": "#121212",
492
+ "surface": "#181818",
493
+ "elevated": "#1e1e1e",
494
+ "hover": "#262626",
495
+ "border": "#333333",
496
+ "ink1": "#dbd7ca",
497
+ "ink2": "#bfbaaa",
498
+ "ink3": "#8f8f85",
499
+ "inkMuted": "#758575"
500
+ },
501
+ "light": {
502
+ "ember": "#1c6b48",
503
+ "emberGlow": "#145236",
504
+ "emberDim": "rgba(28,107,72,0.18)",
505
+ "field": "#f0f0f0",
506
+ "surface": "#f7f7f7",
507
+ "elevated": "#ffffff",
508
+ "hover": "#e7e7e7",
509
+ "border": "#d6d6d6",
510
+ "ink1": "#393a34",
511
+ "ink2": "#4e4f47",
512
+ "ink3": "#6b6d63",
513
+ "inkMuted": "#a0ada0"
514
+ }
515
+ },
516
+ {
517
+ "id": "synthwave",
518
+ "label": "Synthwave '84",
519
+ "dark": {
520
+ "ember": "#ff7edb",
521
+ "emberGlow": "#ffa9e7",
522
+ "emberDim": "rgba(255,126,219,0.25)",
523
+ "field": "#241b2f",
524
+ "surface": "#262335",
525
+ "elevated": "#322a47",
526
+ "hover": "#3d3460",
527
+ "border": "#495495",
528
+ "ink1": "#ffffff",
529
+ "ink2": "#d8d6e8",
530
+ "ink3": "#b6b1d8",
531
+ "inkMuted": "#848bbd"
532
+ },
533
+ "light": {
534
+ "ember": "#c936a6",
535
+ "emberGlow": "#9c2381",
536
+ "emberDim": "rgba(201,54,166,0.18)",
537
+ "field": "#e6e2f0",
538
+ "surface": "#efecf6",
539
+ "elevated": "#fbfafd",
540
+ "hover": "#ddd7ec",
541
+ "border": "#c5bcdd",
542
+ "ink1": "#241b2f",
543
+ "ink2": "#3d3455",
544
+ "ink3": "#5c5380",
545
+ "inkMuted": "#7d76a3"
546
+ }
547
+ }
548
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-hub",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "description": "Unified hub for Claude Code tools — Marketplace + Kanban in one PWA",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "scripts": {
10
10
  "start": "node server.js",
11
11
  "dev": "node server.js --open",
12
+ "test": "node --test test/*.test.js",
12
13
  "update": "bash scripts/update-submodules.sh",
13
14
  "update:check": "bash scripts/update-submodules.sh --check",
14
15
  "lint": "npx @biomejs/biome check public/app.js server.js && npm run check:escaping && npm run check:security-lib",
@@ -43,10 +44,10 @@
43
44
  },
44
45
  "homepage": "https://github.com/NikiforovAll/claude-code-hub#readme",
45
46
  "dependencies": {
46
- "claude-code-cost": "^0.25.0",
47
- "claude-code-kanban": "^4.30.0",
48
- "claude-code-marketplace": "^0.21.0",
49
- "claude-code-memory-explorer": "^0.16.0",
47
+ "claude-code-cost": "^0.26.0",
48
+ "claude-code-kanban": "^4.31.1",
49
+ "claude-code-marketplace": "^0.22.0",
50
+ "claude-code-memory-explorer": "^0.17.0",
50
51
  "express": "^4.21.0",
51
52
  "open": "^10.1.0"
52
53
  },
package/public/app.js CHANGED
@@ -3,7 +3,6 @@ let activeApp = null;
3
3
  const iframes = {};
4
4
  const loadedApps = new Set();
5
5
  const guardedApps = new Set();
6
- let allowedOrigins = new Set();
7
6
  // {theme: 'dark'|'light', colorTheme: '<id>', colorThemes: {<configDir>: '<id>'}} — survives hub
8
7
  // reloads so late-loading iframes and fresh sessions get the last chosen theme. Light/dark is
9
8
  // global; the color theme is per config dir, and colorTheme is the last one picked anywhere, which
@@ -55,7 +54,7 @@ function themeMessage() {
55
54
  }
56
55
 
57
56
  // {<themeId>: {dark, light}}, each a map of CSS variables, from /api/config, which derives it from
58
- // scripts/themes.json — the same registry that generates each sub-app's themes.css. Empty until
57
+ // lib/themes.json — the same registry that generates each sub-app's themes.css. Empty until
59
58
  // config arrives, and empty if the registry is unreadable; then the hub keeps index.html's colors.
60
59
  let themePalettes = {};
61
60
  // The last applied variables, so the loading screen paints in the theme before /api/config lands.
@@ -151,7 +150,7 @@ async function init() {
151
150
  showLoading();
152
151
  const res = await fetch('/api/config');
153
152
  const config = await res.json();
154
- setApps(config.apps);
153
+ apps = config.apps;
155
154
  defaultConfigDir = config.defaultConfigDir ?? null;
156
155
  activeConfigDir = config.activeConfigDir ?? null;
157
156
  applyTitle(config.activeConfigDir);
@@ -172,11 +171,6 @@ function applyTitle(dir) {
172
171
  document.title = dir && dir !== defaultConfigDir ? basename(dir) : 'Claude Code Hub';
173
172
  }
174
173
 
175
- function setApps(next) {
176
- apps = next;
177
- allowedOrigins = new Set(Object.values(apps).map((a) => new URL(a.url).origin));
178
- }
179
-
180
174
  // After a config-dir switch every child is a new process, possibly on a new port. Reloading each
181
175
  // iframe from scratch also drops the sub-apps' in-memory project state, which belonged to the old
182
176
  // dir; the hub's own projectState is cleared for the same reason.
@@ -312,7 +306,8 @@ function listenMessages() {
312
306
  if (guardedApps.size) e.preventDefault();
313
307
  });
314
308
  window.addEventListener('message', (e) => {
315
- if (!allowedOrigins.has(e.origin)) return;
309
+ const appId = appIdOf(e.source);
310
+ if (!appId || e.origin !== originOf(appId)) return;
316
311
  const data = e.data ?? {};
317
312
  if (data.type === 'hub:navigate') {
318
313
  if (!apps[data.app]) return;
@@ -324,14 +319,10 @@ function listenMessages() {
324
319
  } else if (data.type === 'hub:keydown') {
325
320
  handleForwardedKey(data);
326
321
  } else if (data.type === 'hub:closeGuard') {
327
- const appId = appIdOf(e.source);
328
- if (!appId) return;
329
322
  if (data.on === true) guardedApps.add(appId);
330
323
  else guardedApps.delete(appId);
331
324
  } else if (data.type === 'hub:terminalToken') {
332
325
  // This hub page may have outlived the restart too, so the cached token is not trusted.
333
- const appId = appIdOf(e.source);
334
- if (!appId) return;
335
326
  sendJson('GET', '/api/config')
336
327
  .then((cfg) => {
337
328
  const token = cfg.apps?.[appId]?.terminalToken;
@@ -785,7 +776,7 @@ async function commitConfigDir(row) {
785
776
  if (target === palette.configDirs.active) return;
786
777
  showLoading();
787
778
  try {
788
- setApps((await sendJson('POST', '/api/config-dirs/activate', { path: target })).apps);
779
+ apps = (await sendJson('POST', '/api/config-dirs/activate', { path: target })).apps;
789
780
  // The palette renders from this cache before its refetch lands, and initialSel keys off it.
790
781
  palette.configDirs.active = target;
791
782
  activeConfigDir = target;
package/server.js CHANGED
@@ -9,6 +9,7 @@ const tcp = require('net');
9
9
  const os = require('os');
10
10
  const path = require('path');
11
11
  const { selectApps } = require('./lib/apps');
12
+ const { stripCookie } = require('./lib/cookies');
12
13
  const { createNetGuard } = require('./lib/net-guard');
13
14
 
14
15
  function getArg(name) {
@@ -20,6 +21,7 @@ function getArg(name) {
20
21
  }
21
22
 
22
23
  const HUB_PORT = parseInt(getArg('port') || process.env.PORT || '3540', 10);
24
+ let hubPort = HUB_PORT;
23
25
 
24
26
  const POOL_SIZE = Math.max(1, parseInt(getArg('pool-size') || '3', 10));
25
27
  const RESTART_DELAY_MS = 500;
@@ -191,7 +193,7 @@ function childEnv(pool, name) {
191
193
  PORT: '0',
192
194
  CLAUDE_CONFIG_DIR: pool.dir,
193
195
  CLAUDE_HUB: '1',
194
- HUB_URL: `http://localhost:${HUB_PORT}`,
196
+ HUB_URL: `http://localhost:${hubPort}`,
195
197
  HOST: net.BIND_HOST,
196
198
  ALLOWED_HOSTS: net.ALLOWED_HOSTS,
197
199
  };
@@ -530,10 +532,14 @@ function forward(name, req, res, childPort, headers, replayable, onUpstream) {
530
532
  // Host is forwarded untouched so the child's hostGuard still sees what the browser sent. Origin is
531
533
  // the one header the child cannot judge on its own: its guard compares the port to its own
532
534
  // ephemeral one, so the public-port spelling is mapped to the child's; anything else passes
533
- // through as-is and the child rejects it.
535
+ // through as-is and the child rejects it. Cookies on localhost are shared across ports, so the
536
+ // browser sends the hub's own token cookie to every app; it never reaches a child.
534
537
  function proxyHandler(name) {
535
538
  return async (req, res) => {
536
539
  const headers = { ...req.headers };
540
+ const cookie = stripCookie(headers.cookie, TOKEN_COOKIE);
541
+ if (cookie) headers.cookie = cookie;
542
+ else delete headers.cookie;
537
543
  // req is consumed by the first attempt, so only a request with no body can be re-sent. Waiting
538
544
  // for a port is not a replay — nothing has been sent yet — so every method gets the same wait.
539
545
  const replayable = req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS';
@@ -562,6 +568,9 @@ function proxyHandler(name) {
562
568
  sendUnavailable(req, res, name, 502, err.message);
563
569
  return;
564
570
  }
571
+ console.warn(
572
+ `[${name}] ${req.method} ${req.url.split('?')[0]} to 127.0.0.1:${childPort} failed (${err.code}), retrying`,
573
+ );
565
574
  await delay(ATTEMPT_DELAY_MS);
566
575
  }
567
576
  };
@@ -580,24 +589,46 @@ function proxyUpgrade(name) {
580
589
  }
581
590
  const lines = [`${req.method} ${req.url} HTTP/${req.httpVersion}`];
582
591
  for (let i = 0; i < req.rawHeaders.length; i += 2) {
592
+ const header = req.rawHeaders[i].toLowerCase();
583
593
  let value = req.rawHeaders[i + 1];
584
- if (req.rawHeaders[i].toLowerCase() === 'origin') value = rewriteOrigin(value, publicPorts[name], childPort);
594
+ if (header === 'origin') value = rewriteOrigin(value, publicPorts[name], childPort);
595
+ if (header === 'cookie' && !(value = stripCookie(value, TOKEN_COOKIE))) continue;
585
596
  lines.push(`${req.rawHeaders[i]}: ${value}`);
586
597
  }
587
- const upstream = tcp.connect(childPort, '127.0.0.1', () => {
588
- clearTimeout(connectTimer);
589
- upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
590
- if (head?.length) upstream.write(head);
591
- upstream.pipe(socket);
592
- socket.pipe(upstream);
593
- });
594
- const connectTimer = setTimeout(() => upstream.destroy(), CONNECT_TIMEOUT_MS);
595
- upstream.on('error', () => socket.destroy());
596
- socket.on('close', () => upstream.destroy());
597
- upstream.on('close', () => socket.destroy());
598
+ tunnel(name, childPort, `${lines.join('\r\n')}\r\n\r\n`, head, socket, 1);
598
599
  };
599
600
  }
600
601
 
602
+ // A fresh loopback connect to a child sometimes fails at once with ETIMEDOUT on Windows (cause
603
+ // unknown, seen only on a just-started hub), so a connect that never opened is tried once more.
604
+ function tunnel(name, childPort, request, head, socket, retries) {
605
+ let connected = false;
606
+ const upstream = tcp.connect(childPort, '127.0.0.1', () => {
607
+ connected = true;
608
+ clearTimeout(connectTimer);
609
+ upstream.write(request);
610
+ if (head?.length) upstream.write(head);
611
+ upstream.pipe(socket);
612
+ socket.pipe(upstream);
613
+ });
614
+ const connectTimer = setTimeout(() => upstream.destroy(), CONNECT_TIMEOUT_MS);
615
+ const onSocketClose = () => upstream.destroy();
616
+ socket.on('close', onSocketClose);
617
+ upstream.on('error', (err) => {
618
+ if (connected || socket.destroyed || retries < 1) {
619
+ if (!connected) console.warn(`[${name}] upgrade connect to 127.0.0.1:${childPort} failed (${err.code})`);
620
+ socket.destroy();
621
+ return;
622
+ }
623
+ console.warn(`[${name}] upgrade connect to 127.0.0.1:${childPort} failed (${err.code}), retrying`);
624
+ clearTimeout(connectTimer);
625
+ socket.off('close', onSocketClose);
626
+ upstream.removeAllListeners('close');
627
+ tunnel(name, childPort, request, head, socket, retries - 1);
628
+ });
629
+ upstream.on('close', () => socket.destroy());
630
+ }
631
+
601
632
  function listenWithFallback(handler, port, onReady, label, onUpgrade) {
602
633
  const opts = { maxHeaderSize: HDR_BYTES, onUpgrade };
603
634
  const server = net.listenLoopback(handler, port, onReady, opts);
@@ -621,8 +652,6 @@ for (const [name, port] of Object.entries(publicPorts)) {
621
652
  );
622
653
  }
623
654
 
624
- ensurePool(hubConfig.activeConfigDir);
625
-
626
655
  const app = express();
627
656
 
628
657
  // Mounted before the routes below, which are registered ahead of the first
@@ -649,7 +678,7 @@ const hubVars = (p) => ({
649
678
  });
650
679
  const themePalettes = (() => {
651
680
  try {
652
- const themes = JSON.parse(fs.readFileSync(path.join(__dirname, 'scripts/themes.json'), 'utf8'));
681
+ const themes = JSON.parse(fs.readFileSync(path.join(__dirname, 'lib/themes.json'), 'utf8'));
653
682
  return Object.fromEntries(themes.map((t) => [t.id, { dark: hubVars(t.dark), light: hubVars(t.light) }]));
654
683
  } catch {
655
684
  // Palette falls back to the --accent in index.html; not worth failing startup over.
@@ -770,7 +799,11 @@ app.get('/api/resolve-path', (req, res) => {
770
799
 
771
800
  app.use(express.static(path.join(__dirname, 'public')));
772
801
 
802
+ // Children get HUB_URL at spawn and trust only that origin, so the first pool waits for the port
803
+ // the hub actually bound: HUB_PORT may be busy and fall back to a random one.
773
804
  const onReady = (actual) => {
805
+ hubPort = actual;
806
+ ensurePool(hubConfig.activeConfigDir);
774
807
  printBanner(actual);
775
808
  if (process.argv.includes('--open')) {
776
809
  import('open').then((m) => m.default(accessUrl(actual)));