@kosinal/claude-code-dashboard 0.0.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 (4) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +89 -0
  3. package/dist/bin.js +1421 -0
  4. package/package.json +41 -0
package/dist/bin.js ADDED
@@ -0,0 +1,1421 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin.ts
4
+ import { exec, spawn } from "child_process";
5
+ import * as http2 from "http";
6
+ import * as net from "net";
7
+
8
+ // src/state.ts
9
+ var EVENT_TO_STATUS = {
10
+ SessionStart: "waiting",
11
+ UserPromptSubmit: "running",
12
+ Stop: "waiting"
13
+ };
14
+ function createStore() {
15
+ const sessions = /* @__PURE__ */ new Map();
16
+ return {
17
+ handleEvent(payload) {
18
+ const { session_id, hook_event_name, cwd } = payload;
19
+ if (hook_event_name === "SessionEnd") {
20
+ sessions.delete(session_id);
21
+ return null;
22
+ }
23
+ const status = EVENT_TO_STATUS[hook_event_name];
24
+ if (!status) {
25
+ const existing2 = sessions.get(session_id);
26
+ if (existing2) return existing2;
27
+ const session2 = {
28
+ sessionId: session_id,
29
+ status: "waiting",
30
+ cwd: cwd ?? "",
31
+ lastEvent: hook_event_name,
32
+ updatedAt: Date.now(),
33
+ startedAt: Date.now()
34
+ };
35
+ sessions.set(session_id, session2);
36
+ return session2;
37
+ }
38
+ const now = Date.now();
39
+ const existing = sessions.get(session_id);
40
+ if (existing) {
41
+ existing.status = status;
42
+ existing.lastEvent = hook_event_name;
43
+ existing.updatedAt = now;
44
+ if (cwd) existing.cwd = cwd;
45
+ return existing;
46
+ }
47
+ const session = {
48
+ sessionId: session_id,
49
+ status,
50
+ cwd: cwd ?? "",
51
+ lastEvent: hook_event_name,
52
+ updatedAt: now,
53
+ startedAt: now
54
+ };
55
+ sessions.set(session_id, session);
56
+ return session;
57
+ },
58
+ getAllSessions() {
59
+ return Array.from(sessions.values());
60
+ },
61
+ getSession(sessionId) {
62
+ return sessions.get(sessionId);
63
+ },
64
+ removeSession(sessionId) {
65
+ return sessions.delete(sessionId);
66
+ },
67
+ cleanIdleSessions(maxIdleMs) {
68
+ const now = Date.now();
69
+ const removed = [];
70
+ for (const [id, session] of sessions) {
71
+ if (now - session.updatedAt > maxIdleMs) {
72
+ sessions.delete(id);
73
+ removed.push(id);
74
+ }
75
+ }
76
+ return removed;
77
+ }
78
+ };
79
+ }
80
+
81
+ // src/server.ts
82
+ import * as http from "http";
83
+
84
+ // src/dashboard.ts
85
+ function getDashboardHtml() {
86
+ return `<!DOCTYPE html>
87
+ <html lang="en">
88
+ <head>
89
+ <meta charset="UTF-8">
90
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
91
+ <title>Claude Code Dashboard</title>
92
+ <style>
93
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
94
+
95
+ body {
96
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, sans-serif;
97
+ background: #0d1117;
98
+ color: #c9d1d9;
99
+ min-height: 100vh;
100
+ padding: 24px;
101
+ }
102
+
103
+ header {
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: space-between;
107
+ margin-bottom: 32px;
108
+ padding-bottom: 16px;
109
+ border-bottom: 1px solid #21262d;
110
+ }
111
+
112
+ h1 {
113
+ font-size: 20px;
114
+ font-weight: 600;
115
+ color: #f0f6fc;
116
+ }
117
+
118
+ .header-right {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 16px;
122
+ }
123
+
124
+ .notification-toggle {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 8px;
128
+ }
129
+
130
+ .toggle-label {
131
+ font-size: 13px;
132
+ color: #8b949e;
133
+ cursor: pointer;
134
+ user-select: none;
135
+ }
136
+
137
+ .toggle-switch {
138
+ position: relative;
139
+ display: inline-block;
140
+ width: 44px;
141
+ height: 22px;
142
+ flex-shrink: 0;
143
+ }
144
+
145
+ .toggle-switch input {
146
+ opacity: 0;
147
+ width: 0;
148
+ height: 0;
149
+ }
150
+
151
+ .toggle-slider {
152
+ position: absolute;
153
+ cursor: pointer;
154
+ top: 0;
155
+ left: 0;
156
+ right: 0;
157
+ bottom: 0;
158
+ background: #21262d;
159
+ border: 1px solid #30363d;
160
+ border-radius: 11px;
161
+ transition: background 0.2s, border-color 0.2s;
162
+ }
163
+
164
+ .toggle-slider::before {
165
+ content: '';
166
+ position: absolute;
167
+ width: 16px;
168
+ height: 16px;
169
+ left: 2px;
170
+ bottom: 2px;
171
+ background: #8b949e;
172
+ border-radius: 50%;
173
+ transition: transform 0.2s, background 0.2s;
174
+ }
175
+
176
+ .toggle-switch input:checked + .toggle-slider {
177
+ background: #238636;
178
+ border-color: #2ea043;
179
+ }
180
+
181
+ .toggle-switch input:checked + .toggle-slider::before {
182
+ transform: translateX(22px);
183
+ background: #f0f6fc;
184
+ }
185
+
186
+ footer {
187
+ display: flex;
188
+ align-items: center;
189
+ justify-content: center;
190
+ gap: 8px;
191
+ margin-top: 32px;
192
+ padding-top: 16px;
193
+ border-top: 1px solid #21262d;
194
+ }
195
+
196
+ footer button {
197
+ background: #21262d;
198
+ color: #c9d1d9;
199
+ border: 1px solid #30363d;
200
+ border-radius: 6px;
201
+ padding: 5px 12px;
202
+ font-size: 13px;
203
+ cursor: pointer;
204
+ transition: background 0.2s, border-color 0.2s;
205
+ }
206
+
207
+ footer button:hover {
208
+ background: #30363d;
209
+ border-color: #484f58;
210
+ }
211
+
212
+ footer button:disabled {
213
+ opacity: 0.5;
214
+ cursor: not-allowed;
215
+ }
216
+
217
+ footer .btn-danger:hover:not(:disabled) {
218
+ background: #da3633;
219
+ border-color: #f85149;
220
+ color: #f0f6fc;
221
+ }
222
+
223
+ .connection-status {
224
+ display: flex;
225
+ align-items: center;
226
+ gap: 8px;
227
+ font-size: 13px;
228
+ color: #8b949e;
229
+ }
230
+
231
+ .connection-dot {
232
+ width: 8px;
233
+ height: 8px;
234
+ border-radius: 50%;
235
+ background: #f85149;
236
+ transition: background 0.3s;
237
+ }
238
+
239
+ .connection-dot.connected { background: #3fb950; }
240
+
241
+ .overlay {
242
+ position: fixed;
243
+ top: 0;
244
+ left: 0;
245
+ right: 0;
246
+ bottom: 0;
247
+ background: rgba(0, 0, 0, 0.6);
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: center;
251
+ z-index: 100;
252
+ }
253
+
254
+ .overlay-card {
255
+ background: #161b22;
256
+ border: 1px solid #30363d;
257
+ border-radius: 12px;
258
+ padding: 24px;
259
+ min-width: 340px;
260
+ max-width: 440px;
261
+ text-align: center;
262
+ }
263
+
264
+ .overlay-card h2 {
265
+ font-size: 18px;
266
+ font-weight: 600;
267
+ color: #f0f6fc;
268
+ margin-bottom: 8px;
269
+ }
270
+
271
+ .overlay-card p {
272
+ font-size: 14px;
273
+ color: #8b949e;
274
+ margin-bottom: 20px;
275
+ line-height: 1.5;
276
+ }
277
+
278
+ .overlay-actions {
279
+ display: flex;
280
+ gap: 8px;
281
+ justify-content: center;
282
+ }
283
+
284
+ .overlay-actions button {
285
+ padding: 8px 20px;
286
+ border-radius: 6px;
287
+ font-size: 14px;
288
+ cursor: pointer;
289
+ border: 1px solid #30363d;
290
+ transition: background 0.2s;
291
+ }
292
+
293
+ .overlay-actions .btn-cancel {
294
+ background: #21262d;
295
+ color: #c9d1d9;
296
+ }
297
+
298
+ .overlay-actions .btn-cancel:hover {
299
+ background: #30363d;
300
+ }
301
+
302
+ .overlay-actions .btn-confirm {
303
+ background: #238636;
304
+ color: #f0f6fc;
305
+ border-color: #2ea043;
306
+ }
307
+
308
+ .overlay-actions .btn-confirm:hover {
309
+ background: #2ea043;
310
+ }
311
+
312
+ .overlay-actions .btn-confirm-danger {
313
+ background: #da3633;
314
+ color: #f0f6fc;
315
+ border-color: #f85149;
316
+ }
317
+
318
+ .overlay-actions .btn-confirm-danger:hover {
319
+ background: #f85149;
320
+ }
321
+
322
+ .empty-state {
323
+ text-align: center;
324
+ padding: 80px 24px;
325
+ color: #8b949e;
326
+ }
327
+
328
+ .empty-state h2 {
329
+ font-size: 18px;
330
+ font-weight: 500;
331
+ color: #c9d1d9;
332
+ margin-bottom: 8px;
333
+ }
334
+
335
+ .empty-state p { font-size: 14px; line-height: 1.6; }
336
+
337
+ .sessions-grid {
338
+ display: grid;
339
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
340
+ gap: 16px;
341
+ }
342
+
343
+ .session-card {
344
+ background: #161b22;
345
+ border: 1px solid #21262d;
346
+ border-radius: 8px;
347
+ padding: 16px;
348
+ transition: border-color 0.2s;
349
+ }
350
+
351
+ .session-card:hover { border-color: #30363d; }
352
+
353
+ .card-header {
354
+ display: flex;
355
+ align-items: center;
356
+ justify-content: space-between;
357
+ margin-bottom: 12px;
358
+ }
359
+
360
+ .project-name {
361
+ font-size: 15px;
362
+ font-weight: 600;
363
+ color: #f0f6fc;
364
+ white-space: nowrap;
365
+ overflow: hidden;
366
+ text-overflow: ellipsis;
367
+ max-width: 200px;
368
+ }
369
+
370
+ .status-badge {
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 6px;
374
+ font-size: 12px;
375
+ font-weight: 500;
376
+ padding: 3px 10px;
377
+ border-radius: 12px;
378
+ white-space: nowrap;
379
+ }
380
+
381
+ .status-dot {
382
+ width: 8px;
383
+ height: 8px;
384
+ border-radius: 50%;
385
+ flex-shrink: 0;
386
+ }
387
+
388
+ .status-running .status-badge { background: rgba(210, 153, 34, 0.15); color: #d29922; }
389
+ .status-running .status-dot {
390
+ background: #d29922;
391
+ animation: pulse 1.5s ease-in-out infinite;
392
+ }
393
+
394
+ .status-waiting .status-badge { background: rgba(56, 139, 253, 0.15); color: #388bfd; }
395
+ .status-waiting .status-dot {
396
+ background: #388bfd;
397
+ animation: breathe 3s ease-in-out infinite;
398
+ }
399
+
400
+ .status-done .status-badge { background: rgba(63, 185, 80, 0.15); color: #3fb950; }
401
+ .status-done .status-dot { background: #3fb950; }
402
+
403
+ @keyframes pulse {
404
+ 0%, 100% { opacity: 1; transform: scale(1); }
405
+ 50% { opacity: 0.5; transform: scale(0.85); }
406
+ }
407
+
408
+ @keyframes breathe {
409
+ 0%, 100% { opacity: 1; }
410
+ 50% { opacity: 0.4; }
411
+ }
412
+
413
+ .card-details {
414
+ display: flex;
415
+ flex-direction: column;
416
+ gap: 6px;
417
+ font-size: 13px;
418
+ color: #8b949e;
419
+ }
420
+
421
+ .detail-row {
422
+ display: flex;
423
+ align-items: center;
424
+ gap: 6px;
425
+ overflow: hidden;
426
+ }
427
+
428
+ .detail-label {
429
+ flex-shrink: 0;
430
+ font-weight: 500;
431
+ color: #6e7681;
432
+ min-width: 55px;
433
+ }
434
+
435
+ .detail-value {
436
+ white-space: nowrap;
437
+ overflow: hidden;
438
+ text-overflow: ellipsis;
439
+ }
440
+
441
+ .session-id-short {
442
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
443
+ font-size: 12px;
444
+ }
445
+ </style>
446
+ </head>
447
+ <body>
448
+ <header>
449
+ <h1>Claude Code Dashboard</h1>
450
+ <div class="header-right">
451
+ <div class="notification-toggle">
452
+ <label class="toggle-switch">
453
+ <input type="checkbox" id="notifToggle">
454
+ <span class="toggle-slider"></span>
455
+ </label>
456
+ <label class="toggle-label" for="notifToggle">Notifications</label>
457
+ </div>
458
+ <div class="connection-status">
459
+ <div class="connection-dot" id="connDot"></div>
460
+ <span id="connLabel">Disconnected</span>
461
+ </div>
462
+ </div>
463
+ </header>
464
+ <div id="overlayContainer"></div>
465
+ <main id="app">
466
+ <div class="empty-state">
467
+ <h2>No sessions yet</h2>
468
+ <p>Start a Claude Code session and it will appear here.<br>
469
+ Sessions already running when the dashboard started won't be tracked.</p>
470
+ </div>
471
+ </main>
472
+ <footer>
473
+ <button id="btnRestart" disabled>Restart</button>
474
+ <button id="btnStop" class="btn-danger" disabled>Stop</button>
475
+ </footer>
476
+ <script>
477
+ (function() {
478
+ var app = document.getElementById('app');
479
+ var connDot = document.getElementById('connDot');
480
+ var connLabel = document.getElementById('connLabel');
481
+ var overlayContainer = document.getElementById('overlayContainer');
482
+ var btnStop = document.getElementById('btnStop');
483
+ var btnRestart = document.getElementById('btnRestart');
484
+ var notifToggle = document.getElementById('notifToggle');
485
+ var notificationsEnabled = localStorage.getItem('notificationsEnabled') !== 'false';
486
+ var sessions = [];
487
+ var previousStatuses = {};
488
+ var initialized = false;
489
+ var es = null;
490
+
491
+ notifToggle.checked = notificationsEnabled;
492
+ notifToggle.addEventListener('change', function() {
493
+ notificationsEnabled = notifToggle.checked;
494
+ localStorage.setItem('notificationsEnabled', notificationsEnabled ? 'true' : 'false');
495
+ if (notificationsEnabled && 'Notification' in window && Notification.permission === 'default') {
496
+ Notification.requestPermission();
497
+ }
498
+ });
499
+
500
+ if (notificationsEnabled && 'Notification' in window && Notification.permission === 'default') {
501
+ Notification.requestPermission();
502
+ }
503
+
504
+ var STATUS_LABELS = { running: 'Running', waiting: 'Waiting for input', done: 'Done' };
505
+ var STATUS_ORDER = { running: 0, waiting: 1, done: 2 };
506
+
507
+ function timeAgo(ts) {
508
+ var diff = Math.floor((Date.now() - ts) / 1000);
509
+ if (diff < 5) return 'just now';
510
+ if (diff < 60) return diff + 's ago';
511
+ if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
512
+ if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
513
+ return Math.floor(diff / 86400) + 'd ago';
514
+ }
515
+
516
+ function folderName(cwd) {
517
+ if (!cwd) return 'Unknown';
518
+ var parts = cwd.replace(/\\\\/g, '/').split('/');
519
+ return parts[parts.length - 1] || parts[parts.length - 2] || cwd;
520
+ }
521
+
522
+ function shortId(id) {
523
+ if (!id) return '';
524
+ return id.length > 12 ? id.slice(0, 8) + '...' : id;
525
+ }
526
+
527
+ function render() {
528
+ if (sessions.length === 0) {
529
+ app.innerHTML = '<div class="empty-state"><h2>No sessions yet</h2>' +
530
+ '<p>Start a Claude Code session and it will appear here.<br>' +
531
+ 'Sessions already running when the dashboard started won\\'t be tracked.</p></div>';
532
+ return;
533
+ }
534
+
535
+ var sorted = sessions.slice().sort(function(a, b) {
536
+ var od = STATUS_ORDER[a.status] - STATUS_ORDER[b.status];
537
+ if (od !== 0) return od;
538
+ return b.updatedAt - a.updatedAt;
539
+ });
540
+
541
+ app.innerHTML = '<div class="sessions-grid">' + sorted.map(function(s) {
542
+ return '<div class="session-card status-' + s.status + '">' +
543
+ '<div class="card-header">' +
544
+ '<span class="project-name" title="' + esc(s.cwd) + '">' + esc(folderName(s.cwd)) + '</span>' +
545
+ '<span class="status-badge"><span class="status-dot"></span>' + STATUS_LABELS[s.status] + '</span>' +
546
+ '</div>' +
547
+ '<div class="card-details">' +
548
+ '<div class="detail-row"><span class="detail-label">Session</span>' +
549
+ '<span class="detail-value session-id-short" title="' + esc(s.sessionId) + '">' + esc(shortId(s.sessionId)) + '</span></div>' +
550
+ '<div class="detail-row"><span class="detail-label">Path</span>' +
551
+ '<span class="detail-value" title="' + esc(s.cwd) + '">' + esc(s.cwd) + '</span></div>' +
552
+ '<div class="detail-row"><span class="detail-label">Event</span>' +
553
+ '<span class="detail-value">' + esc(s.lastEvent) + ' &middot; ' + timeAgo(s.updatedAt) + '</span></div>' +
554
+ '</div>' +
555
+ '</div>';
556
+ }).join('') + '</div>';
557
+ }
558
+
559
+ function esc(str) {
560
+ if (!str) return '';
561
+ return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
562
+ }
563
+
564
+ function checkAndNotify(newSessions) {
565
+ if (!initialized) {
566
+ initialized = true;
567
+ newSessions.forEach(function(s) { previousStatuses[s.sessionId] = s.status; });
568
+ return;
569
+ }
570
+ if (notificationsEnabled && 'Notification' in window && Notification.permission === 'granted') {
571
+ newSessions.forEach(function(s) {
572
+ if (s.status === 'waiting' && previousStatuses[s.sessionId] !== 'waiting') {
573
+ new Notification('Claude Code - Waiting for input', {
574
+ body: folderName(s.cwd),
575
+ tag: 'claude-waiting-' + s.sessionId
576
+ });
577
+ }
578
+ });
579
+ }
580
+ previousStatuses = {};
581
+ newSessions.forEach(function(s) { previousStatuses[s.sessionId] = s.status; });
582
+ }
583
+
584
+ function setButtonsEnabled(enabled) {
585
+ btnStop.disabled = !enabled;
586
+ btnRestart.disabled = !enabled;
587
+ }
588
+
589
+ function clearOverlay() {
590
+ overlayContainer.innerHTML = '';
591
+ }
592
+
593
+ function showOverlay(title, message) {
594
+ overlayContainer.innerHTML =
595
+ '<div class="overlay"><div class="overlay-card">' +
596
+ '<h2>' + esc(title) + '</h2>' +
597
+ '<p>' + esc(message) + '</p>' +
598
+ '</div></div>';
599
+ }
600
+
601
+ function showConfirm(title, message, label, isDanger, onConfirm) {
602
+ var btnClass = isDanger ? 'btn-confirm-danger' : 'btn-confirm';
603
+ overlayContainer.innerHTML =
604
+ '<div class="overlay"><div class="overlay-card">' +
605
+ '<h2>' + esc(title) + '</h2>' +
606
+ '<p>' + esc(message) + '</p>' +
607
+ '<div class="overlay-actions">' +
608
+ '<button class="btn-cancel" id="overlayCancel">Cancel</button>' +
609
+ '<button class="' + btnClass + '" id="overlayConfirm">' + esc(label) + '</button>' +
610
+ '</div>' +
611
+ '</div></div>';
612
+ document.getElementById('overlayCancel').onclick = clearOverlay;
613
+ document.getElementById('overlayConfirm').onclick = function() {
614
+ onConfirm();
615
+ };
616
+ }
617
+
618
+ function attemptReconnect() {
619
+ var attempts = 0;
620
+ var maxAttempts = 30;
621
+ var timer = setInterval(function() {
622
+ attempts++;
623
+ if (attempts > maxAttempts) {
624
+ clearInterval(timer);
625
+ showOverlay('Connection Lost', 'Could not reconnect to the dashboard server.');
626
+ return;
627
+ }
628
+ var req = new XMLHttpRequest();
629
+ req.open('GET', '/api/sessions', true);
630
+ req.timeout = 2000;
631
+ req.onload = function() {
632
+ if (req.status === 200) {
633
+ clearInterval(timer);
634
+ clearOverlay();
635
+ connect();
636
+ }
637
+ };
638
+ req.onerror = function() {};
639
+ req.ontimeout = function() {};
640
+ req.send();
641
+ }, 1000);
642
+ }
643
+
644
+ btnStop.onclick = function() {
645
+ showConfirm('Stop Dashboard', 'Are you sure you want to stop the dashboard server?', 'Stop', true, function() {
646
+ showOverlay('Stopping...', 'Shutting down the dashboard server.');
647
+ setButtonsEnabled(false);
648
+ var req = new XMLHttpRequest();
649
+ req.open('POST', '/api/shutdown', true);
650
+ req.onload = function() {
651
+ showOverlay('Server Stopped', 'The dashboard server has been shut down.');
652
+ };
653
+ req.onerror = function() {
654
+ showOverlay('Server Stopped', 'The dashboard server has been shut down.');
655
+ };
656
+ req.send();
657
+ });
658
+ };
659
+
660
+ btnRestart.onclick = function() {
661
+ showConfirm('Restart Dashboard', 'Are you sure you want to restart the dashboard server?', 'Restart', false, function() {
662
+ showOverlay('Restarting...', 'The dashboard server is restarting. Reconnecting automatically...');
663
+ setButtonsEnabled(false);
664
+ var req = new XMLHttpRequest();
665
+ req.open('POST', '/api/restart', true);
666
+ req.onload = function() {};
667
+ req.onerror = function() {};
668
+ req.send();
669
+ });
670
+ };
671
+
672
+ function connect() {
673
+ if (es) {
674
+ es.close();
675
+ es = null;
676
+ }
677
+
678
+ es = new EventSource('/api/events');
679
+
680
+ es.addEventListener('init', function(e) {
681
+ sessions = JSON.parse(e.data);
682
+ checkAndNotify(sessions);
683
+ render();
684
+ });
685
+
686
+ es.addEventListener('update', function(e) {
687
+ sessions = JSON.parse(e.data);
688
+ checkAndNotify(sessions);
689
+ render();
690
+ });
691
+
692
+ es.addEventListener('shutdown', function() {
693
+ if (es) { es.close(); es = null; }
694
+ setButtonsEnabled(false);
695
+ showOverlay('Server Stopped', 'The dashboard server has been shut down.');
696
+ });
697
+
698
+ es.addEventListener('restart', function() {
699
+ if (es) { es.close(); es = null; }
700
+ setButtonsEnabled(false);
701
+ showOverlay('Restarting...', 'The dashboard server is restarting. Reconnecting automatically...');
702
+ attemptReconnect();
703
+ });
704
+
705
+ es.onopen = function() {
706
+ connDot.classList.add('connected');
707
+ connLabel.textContent = 'Connected';
708
+ setButtonsEnabled(true);
709
+ };
710
+
711
+ es.onerror = function() {
712
+ connDot.classList.remove('connected');
713
+ connLabel.textContent = 'Disconnected';
714
+ setButtonsEnabled(false);
715
+ };
716
+ }
717
+
718
+ // Update time-ago values every 10 seconds
719
+ setInterval(render, 10000);
720
+
721
+ connect();
722
+ })();
723
+ </script>
724
+ </body>
725
+ </html>`;
726
+ }
727
+
728
+ // src/server.ts
729
+ function createServer2(options) {
730
+ const { store, onShutdown, onRestart } = options;
731
+ const idleTimeoutMs = options.idleTimeoutMs ?? 5 * 60 * 1e3;
732
+ const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
733
+ const sseClients = /* @__PURE__ */ new Set();
734
+ function broadcastEvent(event, data) {
735
+ for (const res of sseClients) {
736
+ res.write(`event: ${event}
737
+ data: ${data}
738
+
739
+ `);
740
+ }
741
+ }
742
+ function broadcast() {
743
+ broadcastEvent("update", JSON.stringify(store.getAllSessions()));
744
+ }
745
+ const server = http.createServer((req, res) => {
746
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
747
+ const pathname = url.pathname;
748
+ if (req.method === "GET" && pathname === "/") {
749
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
750
+ res.end(getDashboardHtml());
751
+ return;
752
+ }
753
+ if (req.method === "POST" && pathname === "/api/hook") {
754
+ let body = "";
755
+ req.on("data", (chunk) => {
756
+ body += chunk.toString();
757
+ });
758
+ req.on("end", () => {
759
+ try {
760
+ const payload = JSON.parse(body);
761
+ if (!payload.session_id || !payload.hook_event_name) {
762
+ res.writeHead(400, { "Content-Type": "application/json" });
763
+ res.end(JSON.stringify({ error: "Missing session_id or hook_event_name" }));
764
+ return;
765
+ }
766
+ store.handleEvent(payload);
767
+ broadcast();
768
+ res.writeHead(200, { "Content-Type": "application/json" });
769
+ res.end(JSON.stringify({ ok: true }));
770
+ } catch {
771
+ res.writeHead(400, { "Content-Type": "application/json" });
772
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
773
+ }
774
+ });
775
+ return;
776
+ }
777
+ if (req.method === "GET" && pathname === "/api/events") {
778
+ res.writeHead(200, {
779
+ "Content-Type": "text/event-stream",
780
+ "Cache-Control": "no-cache",
781
+ Connection: "keep-alive"
782
+ });
783
+ const initData = JSON.stringify(store.getAllSessions());
784
+ res.write(`event: init
785
+ data: ${initData}
786
+
787
+ `);
788
+ sseClients.add(res);
789
+ req.on("close", () => {
790
+ sseClients.delete(res);
791
+ });
792
+ return;
793
+ }
794
+ if (req.method === "GET" && pathname === "/api/sessions") {
795
+ res.writeHead(200, { "Content-Type": "application/json" });
796
+ res.end(JSON.stringify(store.getAllSessions()));
797
+ return;
798
+ }
799
+ if (req.method === "POST" && pathname === "/api/shutdown") {
800
+ broadcastEvent("shutdown", JSON.stringify({ ok: true }));
801
+ res.writeHead(200, { "Content-Type": "application/json" });
802
+ res.end(JSON.stringify({ ok: true }));
803
+ if (onShutdown) setImmediate(() => onShutdown());
804
+ return;
805
+ }
806
+ if (req.method === "POST" && pathname === "/api/restart") {
807
+ broadcastEvent("restart", JSON.stringify({ ok: true }));
808
+ res.writeHead(200, { "Content-Type": "application/json" });
809
+ res.end(JSON.stringify({ ok: true }));
810
+ if (onRestart) setImmediate(() => onRestart());
811
+ return;
812
+ }
813
+ res.writeHead(404, { "Content-Type": "application/json" });
814
+ res.end(JSON.stringify({ error: "Not found" }));
815
+ });
816
+ const cleanupTimer = setInterval(() => {
817
+ const removed = store.cleanIdleSessions(idleTimeoutMs);
818
+ if (removed.length > 0) broadcast();
819
+ }, cleanupIntervalMs);
820
+ cleanupTimer.unref();
821
+ return {
822
+ server,
823
+ listen(port, callback) {
824
+ server.listen(port, "127.0.0.1", callback);
825
+ },
826
+ close() {
827
+ clearInterval(cleanupTimer);
828
+ for (const res of sseClients) {
829
+ res.end();
830
+ }
831
+ sseClients.clear();
832
+ return new Promise((resolve, reject) => {
833
+ server.close((err) => err ? reject(err) : resolve());
834
+ });
835
+ }
836
+ };
837
+ }
838
+
839
+ // src/hooks.ts
840
+ import * as fs from "fs";
841
+ import * as path from "path";
842
+ import * as os from "os";
843
+ var MARKER = "__claude_code_dashboard__";
844
+ var HOOK_EVENTS = [
845
+ "SessionStart",
846
+ "UserPromptSubmit",
847
+ "Stop",
848
+ "SessionEnd"
849
+ ];
850
+ function getConfigDir(configDir) {
851
+ return configDir ?? path.join(os.homedir(), ".claude");
852
+ }
853
+ function getSettingsPath(configDir) {
854
+ return path.join(getConfigDir(configDir), "settings.json");
855
+ }
856
+ function readSettings(configDir) {
857
+ const settingsPath = getSettingsPath(configDir);
858
+ try {
859
+ const content = fs.readFileSync(settingsPath, "utf-8");
860
+ const parsed = JSON.parse(content);
861
+ return parsed;
862
+ } catch (err) {
863
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
864
+ return {};
865
+ }
866
+ try {
867
+ fs.copyFileSync(settingsPath, settingsPath + ".bak");
868
+ console.warn(
869
+ `Warning: Invalid settings.json backed up to ${settingsPath}.bak`
870
+ );
871
+ } catch {
872
+ }
873
+ return {};
874
+ }
875
+ }
876
+ function writeSettings(settings, configDir) {
877
+ const settingsPath = getSettingsPath(configDir);
878
+ const dir = path.dirname(settingsPath);
879
+ fs.mkdirSync(dir, { recursive: true });
880
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
881
+ }
882
+ function backupSettings(configDir) {
883
+ const settingsPath = getSettingsPath(configDir);
884
+ const backupPath = settingsPath.replace(/\.json$/, ".pre-dashboard.json");
885
+ try {
886
+ fs.copyFileSync(settingsPath, backupPath);
887
+ } catch {
888
+ }
889
+ }
890
+ function installHooks(port, configDir) {
891
+ const settings = readSettings(configDir);
892
+ backupSettings(configDir);
893
+ removeHooksFromSettings(settings);
894
+ if (!settings.hooks) {
895
+ settings.hooks = {};
896
+ }
897
+ for (const event of HOOK_EVENTS) {
898
+ if (!settings.hooks[event]) {
899
+ settings.hooks[event] = [];
900
+ }
901
+ const command = process.platform === "win32" ? `powershell -NoProfile -Command "$input | Invoke-WebRequest -Uri http://localhost:${port}/api/hook -Method POST -ContentType 'application/json' -ErrorAction SilentlyContinue | Out-Null"` : `curl -s -X POST -H "Content-Type: application/json" -d @- http://localhost:${port}/api/hook > /dev/null 2>&1`;
902
+ settings.hooks[event].push({
903
+ hooks: [{
904
+ type: "command",
905
+ command,
906
+ async: true,
907
+ statusMessage: MARKER
908
+ }]
909
+ });
910
+ }
911
+ writeSettings(settings, configDir);
912
+ }
913
+ function installHooksWithCommand(command, configDir) {
914
+ const settings = readSettings(configDir);
915
+ backupSettings(configDir);
916
+ removeHooksFromSettings(settings);
917
+ if (!settings.hooks) {
918
+ settings.hooks = {};
919
+ }
920
+ for (const event of HOOK_EVENTS) {
921
+ if (!settings.hooks[event]) {
922
+ settings.hooks[event] = [];
923
+ }
924
+ settings.hooks[event].push({
925
+ hooks: [{
926
+ type: "command",
927
+ command,
928
+ async: true,
929
+ statusMessage: MARKER
930
+ }]
931
+ });
932
+ }
933
+ writeSettings(settings, configDir);
934
+ }
935
+ function removeHooksFromSettings(settings) {
936
+ if (!settings.hooks) return;
937
+ for (const event of HOOK_EVENTS) {
938
+ const groups = settings.hooks[event];
939
+ if (!groups) continue;
940
+ const filtered = [];
941
+ for (const group of groups) {
942
+ const kept = group.hooks.filter((h) => h.statusMessage !== MARKER);
943
+ if (kept.length > 0) {
944
+ filtered.push({ ...group, hooks: kept });
945
+ }
946
+ }
947
+ if (filtered.length > 0) {
948
+ settings.hooks[event] = filtered;
949
+ } else {
950
+ delete settings.hooks[event];
951
+ }
952
+ }
953
+ if (Object.keys(settings.hooks).length === 0) {
954
+ delete settings.hooks;
955
+ }
956
+ }
957
+ function removeHooks(configDir) {
958
+ const settingsPath = getSettingsPath(configDir);
959
+ try {
960
+ fs.accessSync(settingsPath);
961
+ } catch {
962
+ return;
963
+ }
964
+ const settings = readSettings(configDir);
965
+ removeHooksFromSettings(settings);
966
+ writeSettings(settings, configDir);
967
+ }
968
+
969
+ // src/installer.ts
970
+ import * as fs2 from "fs";
971
+ import * as path2 from "path";
972
+ import * as os2 from "os";
973
+ var DASHBOARD_DIR = path2.join(os2.homedir(), ".claude", "dashboard");
974
+ var BIN_DIR = path2.join(os2.homedir(), ".claude", "bin");
975
+ var CONFIG_PATH = path2.join(DASHBOARD_DIR, "config.json");
976
+ var SERVER_DIR = path2.join(DASHBOARD_DIR, "server");
977
+ var HOOK_SCRIPT_PATH = path2.join(BIN_DIR, "claude-dashboard-hook.mjs");
978
+ var LOCK_PATH = path2.join(DASHBOARD_DIR, "dashboard.lock");
979
+ function getThisBundle() {
980
+ return process.argv[1] ?? __filename;
981
+ }
982
+ function writeHookScript(port, serverEntry) {
983
+ const script = `#!/usr/bin/env node
984
+ // Auto-generated by claude-code-dashboard install
985
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
986
+ import { spawn } from 'node:child_process';
987
+ import { request } from 'node:http';
988
+ import { openSync } from 'node:fs';
989
+
990
+ const CONFIG_PATH = ${JSON.stringify(CONFIG_PATH)};
991
+ const LOCK_PATH = ${JSON.stringify(LOCK_PATH)};
992
+
993
+ let config;
994
+ try {
995
+ config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
996
+ } catch {
997
+ process.exit(0); // Config missing \u2014 probably uninstalled
998
+ }
999
+
1000
+ const { port, serverEntry } = config;
1001
+ const stdin = readFileSync(0, 'utf-8');
1002
+
1003
+ function postHook(data) {
1004
+ return new Promise((resolve, reject) => {
1005
+ const req = request({
1006
+ hostname: '127.0.0.1',
1007
+ port,
1008
+ path: '/api/hook',
1009
+ method: 'POST',
1010
+ headers: { 'Content-Type': 'application/json' },
1011
+ timeout: 5000,
1012
+ }, (res) => {
1013
+ res.resume();
1014
+ resolve(res.statusCode);
1015
+ });
1016
+ req.on('error', reject);
1017
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
1018
+ req.write(data);
1019
+ req.end();
1020
+ });
1021
+ }
1022
+
1023
+ function isServerRunning() {
1024
+ try {
1025
+ if (!existsSync(LOCK_PATH)) return false;
1026
+ const pid = parseInt(readFileSync(LOCK_PATH, 'utf-8').trim().split(':')[0], 10);
1027
+ if (isNaN(pid)) return false;
1028
+ process.kill(pid, 0); // Check if process exists
1029
+ return true;
1030
+ } catch {
1031
+ return false;
1032
+ }
1033
+ }
1034
+
1035
+ function startServer() {
1036
+ const logPath = ${JSON.stringify(path2.join(DASHBOARD_DIR, "server.log"))};
1037
+ const logFd = openSync(logPath, 'a');
1038
+ const child = spawn(process.execPath, [serverEntry, '--no-hooks', '--no-open', '--port', String(port)], {
1039
+ detached: true,
1040
+ stdio: ['ignore', logFd, logFd],
1041
+ env: { ...process.env },
1042
+ });
1043
+ child.unref();
1044
+ }
1045
+
1046
+ async function waitForServer(maxWaitMs = 10000) {
1047
+ const start = Date.now();
1048
+ let delay = 100;
1049
+ while (Date.now() - start < maxWaitMs) {
1050
+ try {
1051
+ await postHook('{"session_id":"ping","hook_event_name":"Ping"}');
1052
+ return true;
1053
+ } catch {
1054
+ await new Promise(r => setTimeout(r, delay));
1055
+ delay = Math.min(delay * 1.5, 1000);
1056
+ }
1057
+ }
1058
+ return false;
1059
+ }
1060
+
1061
+ async function main() {
1062
+ try {
1063
+ await postHook(stdin);
1064
+ } catch {
1065
+ // Server not running \u2014 try to start it
1066
+ if (!isServerRunning()) {
1067
+ startServer();
1068
+ }
1069
+ const ready = await waitForServer();
1070
+ if (ready) {
1071
+ try { await postHook(stdin); } catch { /* give up */ }
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ main().catch(() => {});
1077
+ `;
1078
+ fs2.mkdirSync(path2.dirname(HOOK_SCRIPT_PATH), { recursive: true });
1079
+ fs2.writeFileSync(HOOK_SCRIPT_PATH, script);
1080
+ }
1081
+ function install(port) {
1082
+ fs2.mkdirSync(SERVER_DIR, { recursive: true });
1083
+ fs2.mkdirSync(BIN_DIR, { recursive: true });
1084
+ const bundleSrc = getThisBundle();
1085
+ const bundleDest = path2.join(SERVER_DIR, "bin.js");
1086
+ fs2.copyFileSync(bundleSrc, bundleDest);
1087
+ fs2.writeFileSync(
1088
+ CONFIG_PATH,
1089
+ JSON.stringify({ port, serverEntry: bundleDest }, null, 2) + "\n"
1090
+ );
1091
+ writeHookScript(port, bundleDest);
1092
+ const command = `node ${JSON.stringify(HOOK_SCRIPT_PATH)}`;
1093
+ installHooksWithCommand(command);
1094
+ console.log("Dashboard installed successfully!");
1095
+ console.log(` Server bundle: ${bundleDest}`);
1096
+ console.log(` Hook script: ${HOOK_SCRIPT_PATH}`);
1097
+ console.log(` Config: ${CONFIG_PATH}`);
1098
+ console.log(` Port: ${port}`);
1099
+ console.log("");
1100
+ console.log(
1101
+ "The dashboard will auto-launch when a Claude Code session starts."
1102
+ );
1103
+ console.log(
1104
+ "To uninstall: npx @kosinal/claude-code-dashboard uninstall"
1105
+ );
1106
+ }
1107
+ function uninstall() {
1108
+ removeHooks();
1109
+ try {
1110
+ fs2.rmSync(DASHBOARD_DIR, { recursive: true, force: true });
1111
+ } catch {
1112
+ }
1113
+ try {
1114
+ fs2.unlinkSync(HOOK_SCRIPT_PATH);
1115
+ } catch {
1116
+ }
1117
+ console.log("Dashboard uninstalled successfully.");
1118
+ }
1119
+ function writeLockFile(port) {
1120
+ fs2.mkdirSync(DASHBOARD_DIR, { recursive: true });
1121
+ fs2.writeFileSync(LOCK_PATH, `${process.pid}:${port}`);
1122
+ }
1123
+ function readLockFile() {
1124
+ try {
1125
+ const content = fs2.readFileSync(LOCK_PATH, "utf-8").trim();
1126
+ const parts = content.split(":");
1127
+ const pid = parseInt(parts[0], 10);
1128
+ const port = parts.length > 1 ? parseInt(parts[1], 10) : NaN;
1129
+ if (isNaN(pid) || isNaN(port)) return null;
1130
+ process.kill(pid, 0);
1131
+ return { pid, port };
1132
+ } catch {
1133
+ try {
1134
+ fs2.unlinkSync(LOCK_PATH);
1135
+ } catch {
1136
+ }
1137
+ return null;
1138
+ }
1139
+ }
1140
+ function removeLockFile() {
1141
+ try {
1142
+ fs2.unlinkSync(LOCK_PATH);
1143
+ } catch {
1144
+ }
1145
+ }
1146
+
1147
+ // src/bin.ts
1148
+ var DEFAULT_PORT = 8377;
1149
+ function parseArgs(argv) {
1150
+ let port = DEFAULT_PORT;
1151
+ let command = null;
1152
+ let noHooks = false;
1153
+ let noOpen = false;
1154
+ for (let i = 2; i < argv.length; i++) {
1155
+ const arg = argv[i];
1156
+ if (arg === "--port" && i + 1 < argv.length) {
1157
+ port = parseInt(argv[++i], 10);
1158
+ if (isNaN(port) || port < 1 || port > 65535) {
1159
+ console.error("Error: Invalid port number");
1160
+ process.exit(1);
1161
+ }
1162
+ } else if (arg === "--no-hooks") {
1163
+ noHooks = true;
1164
+ } else if (arg === "--no-open") {
1165
+ noOpen = true;
1166
+ } else if (arg === "install" || arg === "uninstall" || arg === "stop" || arg === "restart") {
1167
+ command = arg;
1168
+ } else if (arg === "--help" || arg === "-h") {
1169
+ printHelp();
1170
+ process.exit(0);
1171
+ } else {
1172
+ console.error(`Unknown argument: ${arg}`);
1173
+ printHelp();
1174
+ process.exit(1);
1175
+ }
1176
+ }
1177
+ return { port, command, noHooks, noOpen };
1178
+ }
1179
+ function printHelp() {
1180
+ console.log(`
1181
+ claude-code-dashboard - Real-time browser dashboard for Claude Code sessions
1182
+
1183
+ Usage:
1184
+ claude-code-dashboard Start dashboard (quick mode)
1185
+ claude-code-dashboard install Install persistent dashboard with auto-launch
1186
+ claude-code-dashboard uninstall Remove persistent dashboard
1187
+ claude-code-dashboard stop Stop the running dashboard
1188
+ claude-code-dashboard restart Restart the running dashboard
1189
+
1190
+ Options:
1191
+ --port <number> Port to use (default: ${DEFAULT_PORT})
1192
+ --no-hooks Start server without installing hooks
1193
+ --no-open Don't open the browser on start
1194
+ -h, --help Show this help message
1195
+
1196
+ Quick mode (default):
1197
+ Starts the dashboard server and installs temporary hooks.
1198
+ Hooks are removed automatically when you stop the dashboard.
1199
+
1200
+ Install mode:
1201
+ Copies the server to ~/.claude/dashboard/ and installs persistent hooks.
1202
+ The dashboard auto-launches when a Claude Code session starts.
1203
+ `.trim());
1204
+ }
1205
+ function openBrowser(url) {
1206
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
1207
+ exec(cmd, () => {
1208
+ });
1209
+ }
1210
+ function httpPost(port, urlPath) {
1211
+ return new Promise((resolve, reject) => {
1212
+ const req = http2.request(
1213
+ {
1214
+ hostname: "127.0.0.1",
1215
+ port,
1216
+ path: urlPath,
1217
+ method: "POST",
1218
+ timeout: 3e3
1219
+ },
1220
+ (res) => {
1221
+ let data = "";
1222
+ res.on("data", (chunk) => data += chunk);
1223
+ res.on("end", () => resolve({ status: res.statusCode, body: data }));
1224
+ }
1225
+ );
1226
+ req.on("error", reject);
1227
+ req.on("timeout", () => {
1228
+ req.destroy();
1229
+ reject(new Error("timeout"));
1230
+ });
1231
+ req.end();
1232
+ });
1233
+ }
1234
+ function waitForExit(pid, timeoutMs) {
1235
+ return new Promise((resolve) => {
1236
+ const start = Date.now();
1237
+ const check = () => {
1238
+ try {
1239
+ process.kill(pid, 0);
1240
+ if (Date.now() - start > timeoutMs) {
1241
+ resolve(false);
1242
+ } else {
1243
+ setTimeout(check, 100);
1244
+ }
1245
+ } catch {
1246
+ resolve(true);
1247
+ }
1248
+ };
1249
+ check();
1250
+ });
1251
+ }
1252
+ function forceKill(pid) {
1253
+ try {
1254
+ if (process.platform === "win32") {
1255
+ spawn("taskkill", ["/PID", String(pid), "/F"], { stdio: "ignore" });
1256
+ } else {
1257
+ process.kill(pid, "SIGKILL");
1258
+ }
1259
+ } catch {
1260
+ }
1261
+ }
1262
+ function waitForPortFree(port, timeoutMs) {
1263
+ return new Promise((resolve) => {
1264
+ const start = Date.now();
1265
+ const check = () => {
1266
+ const srv = net.createServer();
1267
+ srv.once("error", () => {
1268
+ if (Date.now() - start > timeoutMs) {
1269
+ resolve(false);
1270
+ } else {
1271
+ setTimeout(check, 200);
1272
+ }
1273
+ });
1274
+ srv.listen(port, "127.0.0.1", () => {
1275
+ srv.close(() => resolve(true));
1276
+ });
1277
+ };
1278
+ check();
1279
+ });
1280
+ }
1281
+ async function tryShutdownByPort(port) {
1282
+ try {
1283
+ const { status } = await httpPost(port, "/api/shutdown");
1284
+ return status === 200;
1285
+ } catch {
1286
+ return false;
1287
+ }
1288
+ }
1289
+ async function stopServer() {
1290
+ const lock = readLockFile();
1291
+ if (!lock) {
1292
+ console.log("No running dashboard found.");
1293
+ return false;
1294
+ }
1295
+ try {
1296
+ await httpPost(lock.port, "/api/shutdown");
1297
+ const exited = await waitForExit(lock.pid, 5e3);
1298
+ if (!exited) {
1299
+ forceKill(lock.pid);
1300
+ await waitForExit(lock.pid, 3e3);
1301
+ }
1302
+ } catch {
1303
+ forceKill(lock.pid);
1304
+ await waitForExit(lock.pid, 3e3);
1305
+ }
1306
+ removeLockFile();
1307
+ console.log("Dashboard stopped.");
1308
+ return true;
1309
+ }
1310
+ function main() {
1311
+ const { port, command, noHooks, noOpen } = parseArgs(process.argv);
1312
+ if (command === "install") {
1313
+ install(port);
1314
+ return;
1315
+ }
1316
+ if (command === "uninstall") {
1317
+ uninstall();
1318
+ return;
1319
+ }
1320
+ if (command === "stop") {
1321
+ stopServer();
1322
+ return;
1323
+ }
1324
+ if (command === "restart") {
1325
+ (async () => {
1326
+ const lock = readLockFile();
1327
+ if (lock) {
1328
+ await stopServer();
1329
+ } else {
1330
+ const shutdown = await tryShutdownByPort(port);
1331
+ if (shutdown) {
1332
+ console.log("Shutting down dashboard via port...");
1333
+ } else {
1334
+ console.log("No running dashboard found. Starting fresh...");
1335
+ }
1336
+ }
1337
+ const portFree = await waitForPortFree(port, 5e3);
1338
+ if (!portFree) {
1339
+ console.error(`Error: Port ${port} is still in use after timeout. Try --port <number>`);
1340
+ process.exit(1);
1341
+ }
1342
+ startDashboard(port, noHooks, noOpen);
1343
+ })();
1344
+ return;
1345
+ }
1346
+ startDashboard(port, noHooks, noOpen);
1347
+ }
1348
+ function startDashboard(port, noHooks, noOpen) {
1349
+ const store = createStore();
1350
+ let cleanedUp = false;
1351
+ function cleanup() {
1352
+ if (cleanedUp) return;
1353
+ cleanedUp = true;
1354
+ if (!noHooks) {
1355
+ try {
1356
+ removeHooks();
1357
+ console.log("\nHooks removed from settings.json");
1358
+ } catch (err) {
1359
+ console.error("Warning: Failed to remove hooks:", err);
1360
+ }
1361
+ }
1362
+ removeLockFile();
1363
+ dashboard.close().catch(() => {
1364
+ });
1365
+ }
1366
+ const dashboard = createServer2({
1367
+ store,
1368
+ onShutdown() {
1369
+ cleanup();
1370
+ process.exit(0);
1371
+ },
1372
+ onRestart() {
1373
+ cleanup();
1374
+ const args = process.argv.slice(2).filter((a) => a !== "restart");
1375
+ const child = spawn(process.execPath, [process.argv[1], ...args], {
1376
+ detached: true,
1377
+ stdio: "ignore"
1378
+ });
1379
+ child.unref();
1380
+ process.exit(0);
1381
+ }
1382
+ });
1383
+ process.on("SIGINT", () => {
1384
+ cleanup();
1385
+ process.exit(0);
1386
+ });
1387
+ process.on("SIGTERM", () => {
1388
+ cleanup();
1389
+ process.exit(0);
1390
+ });
1391
+ process.on("uncaughtException", (err) => {
1392
+ console.error("Uncaught exception:", err);
1393
+ cleanup();
1394
+ process.exit(1);
1395
+ });
1396
+ dashboard.server.on("error", (err) => {
1397
+ if (err.code === "EADDRINUSE") {
1398
+ console.error(
1399
+ `Error: Port ${port} is already in use. Try --port <number>`
1400
+ );
1401
+ process.exit(1);
1402
+ }
1403
+ throw err;
1404
+ });
1405
+ if (!noHooks) {
1406
+ installHooks(port);
1407
+ }
1408
+ writeLockFile(port);
1409
+ dashboard.listen(port, () => {
1410
+ const url = `http://localhost:${port}`;
1411
+ console.log(`Dashboard running at ${url}`);
1412
+ if (!noHooks) {
1413
+ console.log("Hooks installed in ~/.claude/settings.json");
1414
+ }
1415
+ console.log("Press Ctrl+C to stop");
1416
+ if (!noOpen) {
1417
+ openBrowser(url);
1418
+ }
1419
+ });
1420
+ }
1421
+ main();