@delt/claude-alarm 0.3.12 → 0.4.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/dist/cli.js +23 -0
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +158 -4
- package/dist/hub/server.js +87 -20
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +111 -89
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +158 -4
|
@@ -119,6 +119,22 @@
|
|
|
119
119
|
font-size: 14px;
|
|
120
120
|
font-weight: 500;
|
|
121
121
|
margin-bottom: 4px;
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 6px;
|
|
125
|
+
}
|
|
126
|
+
.unread-badge {
|
|
127
|
+
background: var(--accent);
|
|
128
|
+
color: #fff;
|
|
129
|
+
font-size: 10px;
|
|
130
|
+
font-weight: 600;
|
|
131
|
+
min-width: 18px;
|
|
132
|
+
height: 18px;
|
|
133
|
+
border-radius: 9px;
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
padding: 0 5px;
|
|
122
138
|
}
|
|
123
139
|
.session-status {
|
|
124
140
|
display: inline-block;
|
|
@@ -474,12 +490,61 @@
|
|
|
474
490
|
}
|
|
475
491
|
.token-form button:hover { background: var(--accent-dim); }
|
|
476
492
|
.token-error { color: var(--red); font-size: 12px; margin-top: 8px; display: none; }
|
|
493
|
+
|
|
494
|
+
/* Mobile tabs */
|
|
495
|
+
.mobile-tabs {
|
|
496
|
+
display: none;
|
|
497
|
+
border-bottom: 1px solid var(--border);
|
|
498
|
+
}
|
|
499
|
+
.mobile-tabs button {
|
|
500
|
+
flex: 1;
|
|
501
|
+
background: none;
|
|
502
|
+
border: none;
|
|
503
|
+
border-bottom: 2px solid transparent;
|
|
504
|
+
color: var(--text-dim);
|
|
505
|
+
padding: 10px;
|
|
506
|
+
font-size: 13px;
|
|
507
|
+
cursor: pointer;
|
|
508
|
+
}
|
|
509
|
+
.mobile-tabs button.active {
|
|
510
|
+
color: var(--accent);
|
|
511
|
+
border-bottom-color: var(--accent);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/* Mobile responsive */
|
|
515
|
+
@media (max-width: 768px) {
|
|
516
|
+
.container {
|
|
517
|
+
grid-template-columns: 1fr;
|
|
518
|
+
height: calc(100vh - 97px);
|
|
519
|
+
}
|
|
520
|
+
.sessions-panel, .messages-panel, .notifications-panel {
|
|
521
|
+
border: none;
|
|
522
|
+
display: none;
|
|
523
|
+
}
|
|
524
|
+
.sessions-panel.mobile-active,
|
|
525
|
+
.messages-panel.mobile-active,
|
|
526
|
+
.notifications-panel.mobile-active {
|
|
527
|
+
display: flex;
|
|
528
|
+
flex-direction: column;
|
|
529
|
+
}
|
|
530
|
+
.sessions-panel.mobile-active {
|
|
531
|
+
display: block;
|
|
532
|
+
overflow-y: auto;
|
|
533
|
+
}
|
|
534
|
+
.notifications-panel.mobile-active {
|
|
535
|
+
display: block;
|
|
536
|
+
overflow-y: auto;
|
|
537
|
+
}
|
|
538
|
+
.mobile-tabs { display: flex; }
|
|
539
|
+
.message { max-width: 95%; }
|
|
540
|
+
}
|
|
477
541
|
</style>
|
|
478
542
|
</head>
|
|
479
543
|
<body>
|
|
480
544
|
<header>
|
|
481
545
|
<h1>Claude Alarm</h1>
|
|
482
546
|
<div class="header-right">
|
|
547
|
+
<button class="theme-toggle" id="webhookToggle" title="Webhook settings">⚙</button>
|
|
483
548
|
<button class="theme-toggle" id="themeToggle" title="Toggle theme">☾</button>
|
|
484
549
|
<div class="status-badge">
|
|
485
550
|
<span class="status-dot" id="connDot"></span>
|
|
@@ -498,6 +563,24 @@
|
|
|
498
563
|
</div>
|
|
499
564
|
</div>
|
|
500
565
|
|
|
566
|
+
<div class="token-overlay hidden" id="webhookOverlay">
|
|
567
|
+
<div class="token-form" style="max-width:500px;text-align:left">
|
|
568
|
+
<h2 style="text-align:center">Webhook Settings</h2>
|
|
569
|
+
<p style="text-align:center">Add webhook URLs to receive notifications externally.</p>
|
|
570
|
+
<div id="webhookList"></div>
|
|
571
|
+
<button id="webhookAdd" style="width:100%;margin-top:8px;background:var(--surface);color:var(--text);border:1px dashed var(--border);border-radius:6px;padding:8px;cursor:pointer;font-size:13px">+ Add Webhook</button>
|
|
572
|
+
<div style="display:flex;gap:8px;margin-top:16px">
|
|
573
|
+
<button id="webhookSave" style="flex:1">Save</button>
|
|
574
|
+
<button id="webhookCancel" style="flex:1;background:var(--surface);color:var(--text);border:1px solid var(--border)">Cancel</button>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<div class="mobile-tabs" id="mobileTabs">
|
|
580
|
+
<button class="active" data-tab="sessions">Sessions</button>
|
|
581
|
+
<button data-tab="messages">Messages</button>
|
|
582
|
+
<button data-tab="notifications">Notifications</button>
|
|
583
|
+
</div>
|
|
501
584
|
<div class="container">
|
|
502
585
|
<div class="sessions-panel">
|
|
503
586
|
<h2>Sessions</h2>
|
|
@@ -545,6 +628,7 @@
|
|
|
545
628
|
notifications: [],
|
|
546
629
|
token: null,
|
|
547
630
|
pendingImage: null,
|
|
631
|
+
unread: {},
|
|
548
632
|
};
|
|
549
633
|
|
|
550
634
|
const $ = (sel) => document.querySelector(sel);
|
|
@@ -644,7 +728,8 @@
|
|
|
644
728
|
case 'reply_from_session':
|
|
645
729
|
if (!state.messages[msg.sessionId]) state.messages[msg.sessionId] = [];
|
|
646
730
|
state.messages[msg.sessionId].push({ from: 'session', content: msg.content, time: msg.timestamp });
|
|
647
|
-
if (state.selectedSession === msg.sessionId) renderMessages();
|
|
731
|
+
if (state.selectedSession === msg.sessionId) { renderMessages(); }
|
|
732
|
+
else { state.unread[msg.sessionId] = (state.unread[msg.sessionId] || 0) + 1; renderSessions(); }
|
|
648
733
|
state.notifications.unshift({ sessionId: msg.sessionId, title: 'Reply', message: msg.content.slice(0, 100), level: 'info', time: msg.timestamp });
|
|
649
734
|
renderNotifications();
|
|
650
735
|
break;
|
|
@@ -690,8 +775,9 @@
|
|
|
690
775
|
const s = state.sessions[id];
|
|
691
776
|
const active = state.selectedSession === id ? ' active' : '';
|
|
692
777
|
const cwdDisplay = s.cwd ? s.cwd.replace(/^.*[/\\]/, '') : '';
|
|
778
|
+
const unread = state.unread[id] || 0;
|
|
693
779
|
return `<div class="session-card${active}" data-id="${id}">
|
|
694
|
-
<div class="session-name">${esc(s.name)}</div>
|
|
780
|
+
<div class="session-name">${esc(s.name)}${unread ? `<span class="unread-badge">${unread}</span>` : ''}</div>
|
|
695
781
|
${cwdDisplay ? `<div class="session-cwd" title="${esc(s.cwd)}">${esc(cwdDisplay)}</div>` : ''}
|
|
696
782
|
<span class="session-status ${s.status}">${s.status.replace('_', ' ')}</span>
|
|
697
783
|
</div>`;
|
|
@@ -704,6 +790,7 @@
|
|
|
704
790
|
|
|
705
791
|
function selectSession(id) {
|
|
706
792
|
state.selectedSession = id;
|
|
793
|
+
state.unread[id] = 0;
|
|
707
794
|
if (!state.messages[id]) state.messages[id] = [];
|
|
708
795
|
$('#msgInput').disabled = false;
|
|
709
796
|
$('#sendBtn').disabled = false;
|
|
@@ -742,7 +829,7 @@
|
|
|
742
829
|
|
|
743
830
|
el.innerHTML = msgs.map(m => {
|
|
744
831
|
const cls = m.from === 'session' ? 'from-session' : 'from-dashboard';
|
|
745
|
-
const timeStr =
|
|
832
|
+
const timeStr = relativeTime(m.time);
|
|
746
833
|
let content;
|
|
747
834
|
if (m.imageData) {
|
|
748
835
|
content = `<img src="${m.imageData}" alt="${esc(m.imageName || 'image')}">`;
|
|
@@ -769,7 +856,7 @@
|
|
|
769
856
|
}
|
|
770
857
|
clearBtn.style.display = 'block';
|
|
771
858
|
el.innerHTML = state.notifications.slice(0, 50).map((n, i) => {
|
|
772
|
-
const timeStr =
|
|
859
|
+
const timeStr = relativeTime(n.time);
|
|
773
860
|
const session = state.sessions[n.sessionId];
|
|
774
861
|
const sName = session ? session.name : n.sessionId.slice(0, 8);
|
|
775
862
|
return `<div class="notif-item" data-session="${n.sessionId}" data-index="${i}">
|
|
@@ -911,6 +998,15 @@
|
|
|
911
998
|
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
|
912
999
|
});
|
|
913
1000
|
|
|
1001
|
+
function relativeTime(ts) {
|
|
1002
|
+
const diff = Math.floor((Date.now() - ts) / 1000);
|
|
1003
|
+
if (diff < 5) return 'just now';
|
|
1004
|
+
if (diff < 60) return diff + 's ago';
|
|
1005
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
1006
|
+
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
1007
|
+
return new Date(ts).toLocaleDateString();
|
|
1008
|
+
}
|
|
1009
|
+
|
|
914
1010
|
function esc(s) {
|
|
915
1011
|
const d = document.createElement('div');
|
|
916
1012
|
d.textContent = s;
|
|
@@ -959,6 +1055,64 @@
|
|
|
959
1055
|
msgList.scrollTo({ top: msgList.scrollHeight, behavior: 'smooth' });
|
|
960
1056
|
});
|
|
961
1057
|
|
|
1058
|
+
// --- Webhook settings ---
|
|
1059
|
+
let webhookData = [];
|
|
1060
|
+
$('#webhookToggle').addEventListener('click', async () => {
|
|
1061
|
+
try {
|
|
1062
|
+
const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
|
|
1063
|
+
const res = await fetch(`/api/webhooks${tokenQuery}`);
|
|
1064
|
+
const data = await res.json();
|
|
1065
|
+
webhookData = data.webhooks || [];
|
|
1066
|
+
} catch { webhookData = []; }
|
|
1067
|
+
renderWebhooks();
|
|
1068
|
+
$('#webhookOverlay').classList.remove('hidden');
|
|
1069
|
+
});
|
|
1070
|
+
$('#webhookCancel').addEventListener('click', () => $('#webhookOverlay').classList.add('hidden'));
|
|
1071
|
+
$('#webhookAdd').addEventListener('click', () => {
|
|
1072
|
+
webhookData.push({ url: '' });
|
|
1073
|
+
renderWebhooks();
|
|
1074
|
+
});
|
|
1075
|
+
$('#webhookSave').addEventListener('click', async () => {
|
|
1076
|
+
const valid = webhookData.filter(w => w.url.trim());
|
|
1077
|
+
try {
|
|
1078
|
+
const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
|
|
1079
|
+
await fetch(`/api/webhooks${tokenQuery}`, {
|
|
1080
|
+
method: 'POST',
|
|
1081
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1082
|
+
body: JSON.stringify({ webhooks: valid }),
|
|
1083
|
+
});
|
|
1084
|
+
} catch {}
|
|
1085
|
+
$('#webhookOverlay').classList.add('hidden');
|
|
1086
|
+
});
|
|
1087
|
+
function renderWebhooks() {
|
|
1088
|
+
const el = $('#webhookList');
|
|
1089
|
+
el.innerHTML = webhookData.map((w, i) => `<div style="display:flex;gap:6px;margin-bottom:6px">
|
|
1090
|
+
<input type="text" value="${esc(w.url)}" placeholder="https://hooks.slack.com/..." style="flex:1;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px;color:var(--text);font-size:13px;outline:none" data-idx="${i}">
|
|
1091
|
+
<button onclick="this.parentElement.remove()" style="background:none;border:1px solid var(--border);border-radius:6px;color:var(--red);cursor:pointer;padding:4px 8px">×</button>
|
|
1092
|
+
</div>`).join('');
|
|
1093
|
+
el.querySelectorAll('input').forEach(inp => {
|
|
1094
|
+
inp.addEventListener('input', () => { webhookData[parseInt(inp.dataset.idx)].url = inp.value; });
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Refresh relative times every 30s
|
|
1099
|
+
setInterval(() => { renderMessages(); renderNotifications(); }, 30000);
|
|
1100
|
+
|
|
1101
|
+
// Mobile tabs
|
|
1102
|
+
document.querySelectorAll('#mobileTabs button').forEach(btn => {
|
|
1103
|
+
btn.addEventListener('click', () => {
|
|
1104
|
+
document.querySelectorAll('#mobileTabs button').forEach(b => b.classList.remove('active'));
|
|
1105
|
+
btn.classList.add('active');
|
|
1106
|
+
const tab = btn.dataset.tab;
|
|
1107
|
+
document.querySelectorAll('.sessions-panel, .messages-panel, .notifications-panel').forEach(p => p.classList.remove('mobile-active'));
|
|
1108
|
+
document.querySelector(`.${tab === 'sessions' ? 'sessions' : tab === 'messages' ? 'messages' : 'notifications'}-panel`).classList.add('mobile-active');
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
1111
|
+
// Default mobile tab
|
|
1112
|
+
if (window.innerWidth <= 768) {
|
|
1113
|
+
document.querySelector('.sessions-panel').classList.add('mobile-active');
|
|
1114
|
+
}
|
|
1115
|
+
|
|
962
1116
|
state.token = getToken();
|
|
963
1117
|
connect();
|
|
964
1118
|
})();
|
package/dist/hub/server.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/hub/server.ts
|
|
4
4
|
import http from "http";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import fs2 from "fs";
|
|
6
|
+
import path3 from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { WebSocketServer, WebSocket } from "ws";
|
|
9
9
|
|
|
@@ -26,7 +26,7 @@ var logger = {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
// src/hub/server.ts
|
|
29
|
-
import { randomUUID } from "crypto";
|
|
29
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
30
30
|
|
|
31
31
|
// src/shared/constants.ts
|
|
32
32
|
import path from "path";
|
|
@@ -178,8 +178,53 @@ var Notifier = class {
|
|
|
178
178
|
}
|
|
179
179
|
};
|
|
180
180
|
|
|
181
|
+
// src/shared/config.ts
|
|
182
|
+
import fs from "fs";
|
|
183
|
+
import path2 from "path";
|
|
184
|
+
import { randomUUID } from "crypto";
|
|
185
|
+
var DEFAULT_CONFIG = {
|
|
186
|
+
hub: {
|
|
187
|
+
host: DEFAULT_HUB_HOST,
|
|
188
|
+
port: DEFAULT_HUB_PORT
|
|
189
|
+
},
|
|
190
|
+
notifications: {
|
|
191
|
+
desktop: true,
|
|
192
|
+
sound: true
|
|
193
|
+
},
|
|
194
|
+
webhooks: []
|
|
195
|
+
};
|
|
196
|
+
function ensureConfigDir() {
|
|
197
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
198
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function loadConfig() {
|
|
202
|
+
ensureConfigDir();
|
|
203
|
+
let config;
|
|
204
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
205
|
+
config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
|
|
206
|
+
} else {
|
|
207
|
+
try {
|
|
208
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
209
|
+
const parsed = JSON.parse(raw);
|
|
210
|
+
config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub } };
|
|
211
|
+
} catch {
|
|
212
|
+
config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!config.hub.token) {
|
|
216
|
+
config.hub.token = randomUUID();
|
|
217
|
+
saveConfig(config);
|
|
218
|
+
}
|
|
219
|
+
return config;
|
|
220
|
+
}
|
|
221
|
+
function saveConfig(config) {
|
|
222
|
+
ensureConfigDir();
|
|
223
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
224
|
+
}
|
|
225
|
+
|
|
181
226
|
// src/hub/server.ts
|
|
182
|
-
var __dirname =
|
|
227
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
183
228
|
var HubServer = class {
|
|
184
229
|
httpServer;
|
|
185
230
|
wssChannel;
|
|
@@ -307,29 +352,34 @@ var HubServer = class {
|
|
|
307
352
|
this.handleApiSend(req, res);
|
|
308
353
|
} else if (url.pathname === "/api/notify" && req.method === "POST") {
|
|
309
354
|
this.handleApiNotify(req, res);
|
|
355
|
+
} else if (url.pathname === "/api/webhooks" && req.method === "GET") {
|
|
356
|
+
const config = loadConfig();
|
|
357
|
+
this.jsonResponse(res, 200, { webhooks: config.webhooks || [] });
|
|
358
|
+
} else if (url.pathname === "/api/webhooks" && req.method === "POST") {
|
|
359
|
+
this.handleWebhookSave(req, res);
|
|
310
360
|
} else {
|
|
311
361
|
this.jsonResponse(res, 404, { error: "Not found" });
|
|
312
362
|
}
|
|
313
363
|
}
|
|
314
364
|
serveDashboard(res) {
|
|
315
365
|
const candidates = [
|
|
316
|
-
|
|
366
|
+
path3.join(__dirname, "..", "dashboard", "index.html"),
|
|
317
367
|
// from dist/hub/
|
|
318
|
-
|
|
368
|
+
path3.join(__dirname, "dashboard", "index.html"),
|
|
319
369
|
// from dist/ (bundled index.js)
|
|
320
|
-
|
|
370
|
+
path3.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
|
|
321
371
|
// from dist/hub/ -> src/
|
|
322
|
-
|
|
372
|
+
path3.join(__dirname, "..", "src", "dashboard", "index.html"),
|
|
323
373
|
// from dist/ -> src/
|
|
324
|
-
|
|
374
|
+
path3.join(process.cwd(), "dist", "dashboard", "index.html"),
|
|
325
375
|
// from cwd
|
|
326
|
-
|
|
376
|
+
path3.join(process.cwd(), "src", "dashboard", "index.html")
|
|
327
377
|
// from cwd/src
|
|
328
378
|
];
|
|
329
379
|
logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
|
|
330
380
|
for (const candidate of candidates) {
|
|
331
|
-
if (
|
|
332
|
-
const html =
|
|
381
|
+
if (fs2.existsSync(candidate)) {
|
|
382
|
+
const html = fs2.readFileSync(candidate, "utf-8");
|
|
333
383
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
334
384
|
res.end(html);
|
|
335
385
|
return;
|
|
@@ -507,11 +557,11 @@ var HubServer = class {
|
|
|
507
557
|
logger.warn("Image upload rejected: exceeds 10MB");
|
|
508
558
|
return;
|
|
509
559
|
}
|
|
510
|
-
|
|
560
|
+
fs2.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
511
561
|
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
512
|
-
const filename = `${
|
|
513
|
-
const filePath =
|
|
514
|
-
|
|
562
|
+
const filename = `${randomUUID2()}.${ext}`;
|
|
563
|
+
const filePath = path3.join(UPLOADS_DIR, filename);
|
|
564
|
+
fs2.writeFileSync(filePath, buffer);
|
|
515
565
|
const forwardMsg = {
|
|
516
566
|
type: "image_to_session",
|
|
517
567
|
sessionId,
|
|
@@ -523,18 +573,35 @@ var HubServer = class {
|
|
|
523
573
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
524
574
|
setTimeout(() => {
|
|
525
575
|
try {
|
|
526
|
-
|
|
576
|
+
fs2.unlinkSync(filePath);
|
|
527
577
|
} catch {
|
|
528
578
|
}
|
|
529
579
|
}, 5 * 60 * 1e3);
|
|
530
580
|
}
|
|
581
|
+
async handleWebhookSave(req, res) {
|
|
582
|
+
const body = await this.readBody(req);
|
|
583
|
+
if (!body) {
|
|
584
|
+
this.jsonResponse(res, 400, { error: "Invalid JSON" });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const { webhooks } = body;
|
|
588
|
+
if (!Array.isArray(webhooks)) {
|
|
589
|
+
this.jsonResponse(res, 400, { error: "webhooks must be an array" });
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const config = loadConfig();
|
|
593
|
+
config.webhooks = webhooks;
|
|
594
|
+
saveConfig(config);
|
|
595
|
+
this.notifier.configure({ webhooks });
|
|
596
|
+
this.jsonResponse(res, 200, { ok: true });
|
|
597
|
+
}
|
|
531
598
|
cleanupUploads() {
|
|
532
599
|
try {
|
|
533
|
-
if (!
|
|
534
|
-
const files =
|
|
600
|
+
if (!fs2.existsSync(UPLOADS_DIR)) return;
|
|
601
|
+
const files = fs2.readdirSync(UPLOADS_DIR);
|
|
535
602
|
for (const file of files) {
|
|
536
603
|
try {
|
|
537
|
-
|
|
604
|
+
fs2.unlinkSync(path3.join(UPLOADS_DIR, file));
|
|
538
605
|
} catch {
|
|
539
606
|
}
|
|
540
607
|
}
|
package/dist/hub/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hub/server.ts","../../src/shared/logger.ts","../../src/shared/constants.ts","../../src/hub/session-manager.ts","../../src/hub/notifier.ts"],"sourcesContent":["import http from 'node:http';\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { fileURLToPath } from 'node:url';\r\nimport { WebSocketServer, WebSocket } from 'ws';\r\nimport { logger } from '../shared/logger.js';\r\nimport { randomUUID } from 'node:crypto';\r\nimport {\r\n DEFAULT_HUB_HOST,\r\n DEFAULT_HUB_PORT,\r\n WS_PATH_CHANNEL,\r\n WS_PATH_DASHBOARD,\r\n UPLOADS_DIR,\r\n} from '../shared/constants.js';\r\nimport { SessionManager } from './session-manager.js';\r\nimport { Notifier } from './notifier.js';\r\nimport type { ChannelMessage, AppConfig, SessionInfo } from '../shared/types.js';\r\n\r\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\r\n\r\nexport class HubServer {\r\n private httpServer: http.Server;\r\n private wssChannel: WebSocketServer;\r\n private wssDashboard: WebSocketServer;\r\n private sessions = new SessionManager();\r\n private notifier = new Notifier();\r\n private startTime = Date.now();\r\n\r\n // Map sessionId -> channel WebSocket\r\n private channelSockets = new Map<string, WebSocket>();\r\n // Track which channel connections are local\r\n private localChannels = new Set<string>();\r\n // All connected dashboard WebSockets\r\n private dashboardSockets = new Set<WebSocket>();\r\n\r\n private host: string;\r\n private port: number;\r\n private token?: string;\r\n\r\n constructor(config?: Partial<AppConfig>) {\r\n this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;\r\n this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;\r\n this.token = config?.hub?.token;\r\n\r\n if (config?.notifications) {\r\n this.notifier.configure({\r\n desktop: config.notifications.desktop,\r\n });\r\n }\r\n if (config?.webhooks) {\r\n this.notifier.configure({ webhooks: config.webhooks });\r\n }\r\n const displayHost = this.host === '0.0.0.0' ? '127.0.0.1' : this.host;\r\n this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });\r\n\r\n // HTTP Server\r\n this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));\r\n\r\n // WebSocket for channel servers\r\n this.wssChannel = new WebSocketServer({ noServer: true });\r\n this.wssChannel.on('connection', (ws: WebSocket, req: http.IncomingMessage) => this.handleChannelConnection(ws, req));\r\n\r\n // WebSocket for dashboard\r\n this.wssDashboard = new WebSocketServer({ noServer: true });\r\n this.wssDashboard.on('connection', (ws) => this.handleDashboardConnection(ws));\r\n\r\n // Route WebSocket upgrade requests\r\n this.httpServer.on('upgrade', (req, socket, head) => {\r\n const url = new URL(req.url!, `http://${req.headers.host}`);\r\n const pathname = url.pathname;\r\n\r\n // Token auth for WebSocket connections (skip for local requests)\r\n if (this.token && !this.isLocalRequest(req)) {\r\n const wsToken = url.searchParams.get('token');\r\n if (wsToken !== this.token) {\r\n socket.destroy();\r\n return;\r\n }\r\n }\r\n\r\n if (pathname === WS_PATH_CHANNEL) {\r\n this.wssChannel.handleUpgrade(req, socket, head, (ws) => {\r\n this.wssChannel.emit('connection', ws, req);\r\n });\r\n } else if (pathname === WS_PATH_DASHBOARD) {\r\n this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {\r\n this.wssDashboard.emit('connection', ws, req);\r\n });\r\n } else {\r\n socket.destroy();\r\n }\r\n });\r\n }\r\n\r\n async start(): Promise<void> {\r\n this.cleanupUploads();\r\n return new Promise((resolve, reject) => {\r\n this.httpServer.on('error', reject);\r\n this.httpServer.listen(this.port, this.host, () => {\r\n const displayHost = this.host === '0.0.0.0' ? '127.0.0.1' : this.host;\r\n logger.info(`Hub server listening on http://${displayHost}:${this.port}`);\r\n resolve();\r\n });\r\n });\r\n }\r\n\r\n stop(): Promise<void> {\r\n return new Promise((resolve) => {\r\n // Force-close all WebSocket connections\r\n for (const ws of this.channelSockets.values()) ws.terminate();\r\n for (const ws of this.dashboardSockets) ws.terminate();\r\n this.channelSockets.clear();\r\n this.dashboardSockets.clear();\r\n\r\n this.wssChannel.close();\r\n this.wssDashboard.close();\r\n this.httpServer.close(() => {\r\n logger.info('Hub server stopped');\r\n resolve();\r\n });\r\n\r\n // Force resolve after 3 seconds if server won't close\r\n setTimeout(() => {\r\n logger.warn('Force shutting down');\r\n resolve();\r\n }, 3000);\r\n });\r\n }\r\n\r\n // --- HTTP Handler ---\r\n\r\n private handleHttp(req: http.IncomingMessage, res: http.ServerResponse): void {\r\n const url = new URL(req.url!, `http://${req.headers.host}`);\r\n\r\n // CORS headers - restrict to same origin\r\n const origin = req.headers.origin;\r\n if (origin && (origin.includes('127.0.0.1') || origin.includes('localhost'))) {\r\n res.setHeader('Access-Control-Allow-Origin', origin);\r\n }\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');\r\n\r\n if (req.method === 'OPTIONS') {\r\n res.writeHead(204);\r\n res.end();\r\n return;\r\n }\r\n\r\n // Token auth for API endpoints (skip dashboard HTML serving)\r\n if (url.pathname !== '/' && this.token) {\r\n if (!this.isLocalRequest(req)) {\r\n const authHeader = req.headers['authorization'];\r\n const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;\r\n if (bearerToken !== this.token) {\r\n this.jsonResponse(res, 401, { error: 'Unauthorized' });\r\n return;\r\n }\r\n }\r\n }\r\n\r\n // Route\r\n if (url.pathname === '/' && req.method === 'GET') {\r\n this.serveDashboard(res);\r\n } else if (url.pathname === '/api/sessions' && req.method === 'GET') {\r\n this.jsonResponse(res, 200, { sessions: this.sessions.getAll() });\r\n } else if (url.pathname === '/api/status' && req.method === 'GET') {\r\n this.jsonResponse(res, 200, {\r\n running: true,\r\n pid: process.pid,\r\n port: this.port,\r\n sessions: this.sessions.count(),\r\n uptime: Date.now() - this.startTime,\r\n });\r\n } else if (url.pathname === '/api/send' && req.method === 'POST') {\r\n this.handleApiSend(req, res);\r\n } else if (url.pathname === '/api/notify' && req.method === 'POST') {\r\n this.handleApiNotify(req, res);\r\n } else {\r\n this.jsonResponse(res, 404, { error: 'Not found' });\r\n }\r\n }\r\n\r\n private serveDashboard(res: http.ServerResponse): void {\r\n // Look for dashboard HTML relative to this file (dist) or source\r\n const candidates = [\r\n path.join(__dirname, '..', 'dashboard', 'index.html'), // from dist/hub/\r\n path.join(__dirname, 'dashboard', 'index.html'), // from dist/ (bundled index.js)\r\n path.join(__dirname, '..', '..', 'src', 'dashboard', 'index.html'), // from dist/hub/ -> src/\r\n path.join(__dirname, '..', 'src', 'dashboard', 'index.html'), // from dist/ -> src/\r\n path.join(process.cwd(), 'dist', 'dashboard', 'index.html'), // from cwd\r\n path.join(process.cwd(), 'src', 'dashboard', 'index.html'), // from cwd/src\r\n ];\r\n logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);\r\n\r\n for (const candidate of candidates) {\r\n if (fs.existsSync(candidate)) {\r\n const html = fs.readFileSync(candidate, 'utf-8');\r\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\r\n res.end(html);\r\n return;\r\n }\r\n }\r\n\r\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\r\n res.end('<html><body><h1>claude-alarm</h1><p>Dashboard HTML not found. Reinstall the package.</p></body></html>');\r\n }\r\n\r\n private async handleApiSend(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\r\n const body = await this.readBody(req);\r\n if (!body) { this.jsonResponse(res, 400, { error: 'Invalid JSON' }); return; }\r\n\r\n const { sessionId, content } = body as { sessionId?: string; content?: string };\r\n if (!sessionId || !content) {\r\n this.jsonResponse(res, 400, { error: 'sessionId and content are required' });\r\n return;\r\n }\r\n\r\n const ws = this.channelSockets.get(sessionId);\r\n if (!ws || ws.readyState !== WebSocket.OPEN) {\r\n this.jsonResponse(res, 404, { error: 'Session not connected' });\r\n return;\r\n }\r\n\r\n const msg: ChannelMessage = { type: 'message_to_session', sessionId, content };\r\n ws.send(JSON.stringify(msg));\r\n this.jsonResponse(res, 200, { ok: true });\r\n }\r\n\r\n private async handleApiNotify(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\r\n const body = await this.readBody(req);\r\n if (!body) { this.jsonResponse(res, 400, { error: 'Invalid JSON' }); return; }\r\n\r\n const { title, message, level } = body as { title?: string; message?: string; level?: string };\r\n if (!title || !message) {\r\n this.jsonResponse(res, 400, { error: 'title and message are required' });\r\n return;\r\n }\r\n\r\n await this.notifier.notify(title, message, (level as any) ?? 'info');\r\n this.jsonResponse(res, 200, { ok: true });\r\n }\r\n\r\n // --- Channel WebSocket ---\r\n\r\n private handleChannelConnection(ws: WebSocket, req: http.IncomingMessage): void {\r\n const isLocal = this.isLocalRequest(req);\r\n logger.info(`Channel server connected (local: ${isLocal})`);\r\n\r\n ws.on('message', (data) => {\r\n try {\r\n const msg = JSON.parse(data.toString()) as ChannelMessage;\r\n this.handleChannelMessage(ws, isLocal, msg);\r\n } catch {\r\n logger.warn('Invalid message from channel');\r\n }\r\n });\r\n\r\n ws.on('close', () => {\r\n // Find and remove the session for this socket\r\n for (const [sessionId, sock] of this.channelSockets) {\r\n if (sock === ws) {\r\n const session = this.sessions.unregister(sessionId);\r\n this.channelSockets.delete(sessionId);\r\n this.localChannels.delete(sessionId);\r\n logger.info(`Channel disconnected: ${sessionId}`);\r\n this.broadcastToDashboards({\r\n type: 'session_disconnected',\r\n sessionId,\r\n });\r\n break;\r\n }\r\n }\r\n });\r\n }\r\n\r\n private handleChannelMessage(ws: WebSocket, isLocal: boolean, msg: ChannelMessage): void {\r\n switch (msg.type) {\r\n case 'register': {\r\n const session = msg.session;\r\n session.isLocal = isLocal;\r\n const isReregister = !!this.sessions.get(session.id);\r\n this.sessions.register(session);\r\n this.channelSockets.set(session.id, ws);\r\n if (isLocal) this.localChannels.add(session.id);\r\n logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false})`);\r\n this.broadcastToDashboards({\r\n type: isReregister ? 'session_updated' : 'session_connected',\r\n session,\r\n });\r\n break;\r\n }\r\n\r\n case 'status': {\r\n const updated = this.sessions.updateStatus(msg.sessionId, msg.status);\r\n if (updated) {\r\n this.broadcastToDashboards({ type: 'session_updated', session: updated });\r\n }\r\n break;\r\n }\r\n\r\n case 'notify': {\r\n this.sessions.updateActivity(msg.sessionId);\r\n const notifySession = this.sessions.get(msg.sessionId);\r\n const notifyLabel = this.getSessionLabel(notifySession);\r\n this.notifier.notify(`[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? 'info');\r\n this.broadcastToDashboards({\r\n type: 'notification',\r\n sessionId: msg.sessionId,\r\n title: msg.title,\r\n message: msg.message,\r\n level: msg.level,\r\n timestamp: Date.now(),\r\n });\r\n break;\r\n }\r\n\r\n case 'reply': {\r\n this.sessions.updateActivity(msg.sessionId);\r\n const replySession = this.sessions.get(msg.sessionId);\r\n const replyLabel = this.getSessionLabel(replySession);\r\n this.notifier.notify(`[${replyLabel}] Reply`, msg.content.slice(0, 200), 'info');\r\n this.broadcastToDashboards({\r\n type: 'reply_from_session',\r\n sessionId: msg.sessionId,\r\n content: msg.content,\r\n timestamp: Date.now(),\r\n });\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // --- Dashboard WebSocket ---\r\n\r\n private handleDashboardConnection(ws: WebSocket): void {\r\n this.dashboardSockets.add(ws);\r\n logger.info(`Dashboard connected (total: ${this.dashboardSockets.size})`);\r\n\r\n // Send current session list\r\n const sessionsMsg: ChannelMessage = {\r\n type: 'sessions_list',\r\n sessions: this.sessions.getAll(),\r\n };\r\n ws.send(JSON.stringify(sessionsMsg));\r\n\r\n ws.on('message', (data) => {\r\n try {\r\n const msg = JSON.parse(data.toString()) as ChannelMessage;\r\n if (msg.type === 'message_to_session') {\r\n const channelWs = this.channelSockets.get(msg.sessionId);\r\n if (channelWs?.readyState === WebSocket.OPEN) {\r\n channelWs.send(JSON.stringify(msg));\r\n }\r\n } else if (msg.type === 'image_upload') {\r\n this.handleImageUpload(msg);\r\n }\r\n } catch {\r\n logger.warn('Invalid message from dashboard');\r\n }\r\n });\r\n\r\n ws.on('close', () => {\r\n this.dashboardSockets.delete(ws);\r\n logger.info(`Dashboard disconnected (total: ${this.dashboardSockets.size})`);\r\n });\r\n }\r\n\r\n // --- Helpers ---\r\n\r\n private broadcastToDashboards(msg: ChannelMessage): void {\r\n const payload = JSON.stringify(msg);\r\n for (const ws of this.dashboardSockets) {\r\n if (ws.readyState === WebSocket.OPEN) {\r\n ws.send(payload);\r\n }\r\n }\r\n }\r\n\r\n private handleImageUpload(msg: ChannelMessage & { type: 'image_upload' }): void {\r\n const { sessionId, imageData, mimeType, originalName } = msg;\r\n\r\n // Only allow for local sessions\r\n if (!this.localChannels.has(sessionId)) {\r\n logger.warn(`Image upload rejected: session ${sessionId} is not local`);\r\n return;\r\n }\r\n\r\n const channelWs = this.channelSockets.get(sessionId);\r\n if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;\r\n\r\n // Validate mime type\r\n const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];\r\n if (!allowedTypes.includes(mimeType)) return;\r\n\r\n // Extract base64 data (remove data URL prefix if present)\r\n const base64Data = imageData.replace(/^data:image\\/\\w+;base64,/, '');\r\n const buffer = Buffer.from(base64Data, 'base64');\r\n\r\n // Validate size (10MB max)\r\n if (buffer.length > 10 * 1024 * 1024) {\r\n logger.warn('Image upload rejected: exceeds 10MB');\r\n return;\r\n }\r\n\r\n // Save to uploads dir\r\n fs.mkdirSync(UPLOADS_DIR, { recursive: true });\r\n const ext = mimeType.split('/')[1] === 'jpeg' ? 'jpg' : mimeType.split('/')[1];\r\n const filename = `${randomUUID()}.${ext}`;\r\n const filePath = path.join(UPLOADS_DIR, filename);\r\n fs.writeFileSync(filePath, buffer);\r\n\r\n // Forward file path to channel\r\n const forwardMsg: ChannelMessage = {\r\n type: 'image_to_session',\r\n sessionId,\r\n imagePath: filePath,\r\n mimeType,\r\n originalName,\r\n };\r\n channelWs.send(JSON.stringify(forwardMsg));\r\n logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);\r\n\r\n // Cleanup after 5 minutes\r\n setTimeout(() => {\r\n try { fs.unlinkSync(filePath); } catch {}\r\n }, 5 * 60 * 1000);\r\n }\r\n\r\n private cleanupUploads(): void {\r\n try {\r\n if (!fs.existsSync(UPLOADS_DIR)) return;\r\n const files = fs.readdirSync(UPLOADS_DIR);\r\n for (const file of files) {\r\n try { fs.unlinkSync(path.join(UPLOADS_DIR, file)); } catch {}\r\n }\r\n if (files.length > 0) logger.info(`Cleaned up ${files.length} leftover upload(s)`);\r\n } catch {}\r\n }\r\n\r\n private getSessionLabel(session?: SessionInfo): string {\r\n if (!session) return 'unknown';\r\n return session.cwd?.replace(/^.*[/\\\\]/, '') || session.name;\r\n }\r\n\r\n private jsonResponse(res: http.ServerResponse, status: number, body: unknown): void {\r\n res.writeHead(status, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify(body));\r\n }\r\n\r\n private isLocalRequest(req: http.IncomingMessage): boolean {\r\n const addr = req.socket.remoteAddress;\r\n return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';\r\n }\r\n\r\n private readBody(req: http.IncomingMessage, maxSize = 1024 * 1024): Promise<unknown | null> {\r\n return new Promise((resolve) => {\r\n let data = '';\r\n let size = 0;\r\n req.on('data', (chunk) => {\r\n size += chunk.length;\r\n if (size > maxSize) {\r\n req.destroy();\r\n resolve(null);\r\n return;\r\n }\r\n data += chunk;\r\n });\r\n req.on('end', () => {\r\n try {\r\n resolve(JSON.parse(data));\r\n } catch {\r\n resolve(null);\r\n }\r\n });\r\n });\r\n }\r\n}\r\n\r\n// Direct execution support\r\nif (process.argv[1] && (\r\n process.argv[1].endsWith('hub/server.js') ||\r\n process.argv[1].endsWith('hub/server.ts')\r\n)) {\r\n const hub = new HubServer();\r\n hub.start().catch((err) => {\r\n logger.error('Failed to start hub:', err);\r\n process.exit(1);\r\n });\r\n\r\n const shutdown = () => {\r\n hub.stop().then(() => process.exit(0));\r\n };\r\n process.on('SIGINT', shutdown);\r\n process.on('SIGTERM', shutdown);\r\n}\r\n","/**\r\n * Logger that writes to stderr only.\r\n * CRITICAL: In MCP channel servers, stdout is used for the stdio protocol.\r\n * Any console.log() would corrupt the protocol. Always use this logger.\r\n */\r\nexport const logger = {\r\n info(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm] ${msg}`, ...args);\r\n },\r\n warn(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm WARN] ${msg}`, ...args);\r\n },\r\n error(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm ERROR] ${msg}`, ...args);\r\n },\r\n debug(msg: string, ...args: unknown[]) {\r\n if (process.env.CLAUDE_ALARM_DEBUG) {\r\n console.error(`[claude-alarm DEBUG] ${msg}`, ...args);\r\n }\r\n },\r\n};\r\n","import path from 'node:path';\r\nimport os from 'node:os';\r\n\r\nexport const DEFAULT_HUB_HOST = '127.0.0.1';\r\nexport const DEFAULT_HUB_PORT = 7900;\r\n\r\nexport const CONFIG_DIR = path.join(os.homedir(), '.claude-alarm');\r\nexport const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');\r\nexport const PID_FILE = path.join(CONFIG_DIR, 'hub.pid');\r\nexport const LOG_FILE = path.join(CONFIG_DIR, 'hub.log');\r\nexport const UPLOADS_DIR = path.join(CONFIG_DIR, 'uploads');\r\n\r\nexport const WS_PATH_CHANNEL = '/ws/channel';\r\nexport const WS_PATH_DASHBOARD = '/ws/dashboard';\r\n\r\nexport const CHANNEL_SERVER_NAME = 'claude-alarm';\r\nexport const CHANNEL_SERVER_VERSION = '0.1.0';\r\n","import type { SessionInfo, SessionStatus } from '../shared/types.js';\r\n\r\nexport class SessionManager {\r\n private sessions = new Map<string, SessionInfo>();\r\n\r\n register(session: SessionInfo): void {\r\n this.sessions.set(session.id, { ...session });\r\n }\r\n\r\n unregister(sessionId: string): SessionInfo | undefined {\r\n const session = this.sessions.get(sessionId);\r\n this.sessions.delete(sessionId);\r\n return session;\r\n }\r\n\r\n updateStatus(sessionId: string, status: SessionStatus): SessionInfo | undefined {\r\n const session = this.sessions.get(sessionId);\r\n if (session) {\r\n session.status = status;\r\n session.lastActivity = Date.now();\r\n }\r\n return session;\r\n }\r\n\r\n updateActivity(sessionId: string): void {\r\n const session = this.sessions.get(sessionId);\r\n if (session) {\r\n session.lastActivity = Date.now();\r\n }\r\n }\r\n\r\n get(sessionId: string): SessionInfo | undefined {\r\n return this.sessions.get(sessionId);\r\n }\r\n\r\n getAll(): SessionInfo[] {\r\n return Array.from(this.sessions.values());\r\n }\r\n\r\n count(): number {\r\n return this.sessions.size;\r\n }\r\n}\r\n","import notifier from 'node-notifier';\r\nimport { execFile } from 'node:child_process';\r\nimport { logger } from '../shared/logger.js';\r\nimport type { NotifyLevel, WebhookConfig } from '../shared/types.js';\r\n\r\nexport class Notifier {\r\n private webhooks: WebhookConfig[] = [];\r\n private desktopEnabled = true;\r\n private notificationSettingsOpened = false;\r\n private dashboardUrl?: string;\r\n\r\n configure(options: { desktop?: boolean; webhooks?: WebhookConfig[]; dashboardUrl?: string }): void {\r\n if (options.dashboardUrl) this.dashboardUrl = options.dashboardUrl;\r\n if (options.desktop !== undefined) this.desktopEnabled = options.desktop;\r\n if (options.webhooks) this.webhooks = options.webhooks;\r\n }\r\n\r\n async notify(title: string, message: string, level: NotifyLevel = 'info'): Promise<void> {\r\n const promises: Promise<void>[] = [];\r\n\r\n if (this.desktopEnabled) {\r\n promises.push(this.sendDesktop(title, message, level));\r\n }\r\n\r\n for (const webhook of this.webhooks) {\r\n promises.push(this.sendWebhook(webhook, title, message, level));\r\n }\r\n\r\n await Promise.allSettled(promises);\r\n }\r\n\r\n private async sendDesktop(title: string, message: string, _level: NotifyLevel): Promise<void> {\r\n if (process.platform === 'win32') {\r\n // Check if notifications are enabled by running snoretoast directly\r\n const enabled = await this.checkWindowsNotifications();\r\n if (!enabled) {\r\n this.openNotificationSettings();\r\n return;\r\n }\r\n }\r\n\r\n return new Promise((resolve) => {\r\n const notification = (notifier as any).notify(\r\n {\r\n title: `Claude Alarm: ${title}`,\r\n message,\r\n sound: true,\r\n wait: true,\r\n },\r\n (err: Error | null) => {\r\n if (err) {\r\n logger.warn(`Desktop notification failed: ${err.message}`);\r\n }\r\n resolve();\r\n },\r\n );\r\n\r\n // No click handler - dashboard is already open and notifications\r\n // can be clicked there to focus the relevant session\r\n });\r\n }\r\n\r\n private checkWindowsNotifications(): Promise<boolean> {\r\n return new Promise((resolve) => {\r\n execFile(\r\n 'powershell',\r\n ['-Command', '(Get-ItemProperty -Path \"HKCU:\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\PushNotifications\" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],\r\n (err, stdout) => {\r\n if (err) { resolve(true); return; } // assume enabled on error\r\n const value = stdout.trim();\r\n resolve(value !== '0');\r\n },\r\n );\r\n });\r\n }\r\n\r\n private openNotificationSettings(): void {\r\n if (this.notificationSettingsOpened) return;\r\n this.notificationSettingsOpened = true;\r\n\r\n logger.warn('Windows notifications are disabled. Opening notification settings...');\r\n logger.warn('Please enable notifications for this app, then try again.');\r\n\r\n if (process.platform === 'win32') {\r\n execFile('powershell', ['-Command', 'Start-Process ms-settings:notifications']);\r\n }\r\n\r\n // Allow re-opening after 5 minutes\r\n setTimeout(() => { this.notificationSettingsOpened = false; }, 5 * 60 * 1000);\r\n }\r\n\r\n private async sendWebhook(\r\n webhook: WebhookConfig,\r\n title: string,\r\n message: string,\r\n level: NotifyLevel,\r\n ): Promise<void> {\r\n try {\r\n const response = await fetch(webhook.url, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...webhook.headers,\r\n },\r\n body: JSON.stringify({\r\n title,\r\n message,\r\n level,\r\n timestamp: Date.now(),\r\n source: 'claude-alarm',\r\n }),\r\n });\r\n\r\n if (!response.ok) {\r\n logger.warn(`Webhook ${webhook.url} returned ${response.status}`);\r\n }\r\n } catch (err) {\r\n logger.warn(`Webhook ${webhook.url} failed: ${(err as Error).message}`);\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":["path","path"]}
|
|
1
|
+
{"version":3,"sources":["../../src/hub/server.ts","../../src/shared/logger.ts","../../src/shared/constants.ts","../../src/hub/session-manager.ts","../../src/hub/notifier.ts","../../src/shared/config.ts"],"sourcesContent":["import http from 'node:http';\r\nimport fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { fileURLToPath } from 'node:url';\r\nimport { WebSocketServer, WebSocket } from 'ws';\r\nimport { logger } from '../shared/logger.js';\r\nimport { randomUUID } from 'node:crypto';\r\nimport {\r\n DEFAULT_HUB_HOST,\r\n DEFAULT_HUB_PORT,\r\n WS_PATH_CHANNEL,\r\n WS_PATH_DASHBOARD,\r\n UPLOADS_DIR,\r\n} from '../shared/constants.js';\r\nimport { SessionManager } from './session-manager.js';\r\nimport { Notifier } from './notifier.js';\r\nimport { loadConfig, saveConfig } from '../shared/config.js';\r\nimport type { ChannelMessage, AppConfig, SessionInfo, WebhookConfig } from '../shared/types.js';\r\n\r\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\r\n\r\nexport class HubServer {\r\n private httpServer: http.Server;\r\n private wssChannel: WebSocketServer;\r\n private wssDashboard: WebSocketServer;\r\n private sessions = new SessionManager();\r\n private notifier = new Notifier();\r\n private startTime = Date.now();\r\n\r\n // Map sessionId -> channel WebSocket\r\n private channelSockets = new Map<string, WebSocket>();\r\n // Track which channel connections are local\r\n private localChannels = new Set<string>();\r\n // All connected dashboard WebSockets\r\n private dashboardSockets = new Set<WebSocket>();\r\n\r\n private host: string;\r\n private port: number;\r\n private token?: string;\r\n\r\n constructor(config?: Partial<AppConfig>) {\r\n this.host = config?.hub?.host ?? DEFAULT_HUB_HOST;\r\n this.port = config?.hub?.port ?? DEFAULT_HUB_PORT;\r\n this.token = config?.hub?.token;\r\n\r\n if (config?.notifications) {\r\n this.notifier.configure({\r\n desktop: config.notifications.desktop,\r\n });\r\n }\r\n if (config?.webhooks) {\r\n this.notifier.configure({ webhooks: config.webhooks });\r\n }\r\n const displayHost = this.host === '0.0.0.0' ? '127.0.0.1' : this.host;\r\n this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });\r\n\r\n // HTTP Server\r\n this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));\r\n\r\n // WebSocket for channel servers\r\n this.wssChannel = new WebSocketServer({ noServer: true });\r\n this.wssChannel.on('connection', (ws: WebSocket, req: http.IncomingMessage) => this.handleChannelConnection(ws, req));\r\n\r\n // WebSocket for dashboard\r\n this.wssDashboard = new WebSocketServer({ noServer: true });\r\n this.wssDashboard.on('connection', (ws) => this.handleDashboardConnection(ws));\r\n\r\n // Route WebSocket upgrade requests\r\n this.httpServer.on('upgrade', (req, socket, head) => {\r\n const url = new URL(req.url!, `http://${req.headers.host}`);\r\n const pathname = url.pathname;\r\n\r\n // Token auth for WebSocket connections (skip for local requests)\r\n if (this.token && !this.isLocalRequest(req)) {\r\n const wsToken = url.searchParams.get('token');\r\n if (wsToken !== this.token) {\r\n socket.destroy();\r\n return;\r\n }\r\n }\r\n\r\n if (pathname === WS_PATH_CHANNEL) {\r\n this.wssChannel.handleUpgrade(req, socket, head, (ws) => {\r\n this.wssChannel.emit('connection', ws, req);\r\n });\r\n } else if (pathname === WS_PATH_DASHBOARD) {\r\n this.wssDashboard.handleUpgrade(req, socket, head, (ws) => {\r\n this.wssDashboard.emit('connection', ws, req);\r\n });\r\n } else {\r\n socket.destroy();\r\n }\r\n });\r\n }\r\n\r\n async start(): Promise<void> {\r\n this.cleanupUploads();\r\n return new Promise((resolve, reject) => {\r\n this.httpServer.on('error', reject);\r\n this.httpServer.listen(this.port, this.host, () => {\r\n const displayHost = this.host === '0.0.0.0' ? '127.0.0.1' : this.host;\r\n logger.info(`Hub server listening on http://${displayHost}:${this.port}`);\r\n resolve();\r\n });\r\n });\r\n }\r\n\r\n stop(): Promise<void> {\r\n return new Promise((resolve) => {\r\n // Force-close all WebSocket connections\r\n for (const ws of this.channelSockets.values()) ws.terminate();\r\n for (const ws of this.dashboardSockets) ws.terminate();\r\n this.channelSockets.clear();\r\n this.dashboardSockets.clear();\r\n\r\n this.wssChannel.close();\r\n this.wssDashboard.close();\r\n this.httpServer.close(() => {\r\n logger.info('Hub server stopped');\r\n resolve();\r\n });\r\n\r\n // Force resolve after 3 seconds if server won't close\r\n setTimeout(() => {\r\n logger.warn('Force shutting down');\r\n resolve();\r\n }, 3000);\r\n });\r\n }\r\n\r\n // --- HTTP Handler ---\r\n\r\n private handleHttp(req: http.IncomingMessage, res: http.ServerResponse): void {\r\n const url = new URL(req.url!, `http://${req.headers.host}`);\r\n\r\n // CORS headers - restrict to same origin\r\n const origin = req.headers.origin;\r\n if (origin && (origin.includes('127.0.0.1') || origin.includes('localhost'))) {\r\n res.setHeader('Access-Control-Allow-Origin', origin);\r\n }\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');\r\n\r\n if (req.method === 'OPTIONS') {\r\n res.writeHead(204);\r\n res.end();\r\n return;\r\n }\r\n\r\n // Token auth for API endpoints (skip dashboard HTML serving)\r\n if (url.pathname !== '/' && this.token) {\r\n if (!this.isLocalRequest(req)) {\r\n const authHeader = req.headers['authorization'];\r\n const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;\r\n if (bearerToken !== this.token) {\r\n this.jsonResponse(res, 401, { error: 'Unauthorized' });\r\n return;\r\n }\r\n }\r\n }\r\n\r\n // Route\r\n if (url.pathname === '/' && req.method === 'GET') {\r\n this.serveDashboard(res);\r\n } else if (url.pathname === '/api/sessions' && req.method === 'GET') {\r\n this.jsonResponse(res, 200, { sessions: this.sessions.getAll() });\r\n } else if (url.pathname === '/api/status' && req.method === 'GET') {\r\n this.jsonResponse(res, 200, {\r\n running: true,\r\n pid: process.pid,\r\n port: this.port,\r\n sessions: this.sessions.count(),\r\n uptime: Date.now() - this.startTime,\r\n });\r\n } else if (url.pathname === '/api/send' && req.method === 'POST') {\r\n this.handleApiSend(req, res);\r\n } else if (url.pathname === '/api/notify' && req.method === 'POST') {\r\n this.handleApiNotify(req, res);\r\n } else if (url.pathname === '/api/webhooks' && req.method === 'GET') {\r\n const config = loadConfig();\r\n this.jsonResponse(res, 200, { webhooks: config.webhooks || [] });\r\n } else if (url.pathname === '/api/webhooks' && req.method === 'POST') {\r\n this.handleWebhookSave(req, res);\r\n } else {\r\n this.jsonResponse(res, 404, { error: 'Not found' });\r\n }\r\n }\r\n\r\n private serveDashboard(res: http.ServerResponse): void {\r\n // Look for dashboard HTML relative to this file (dist) or source\r\n const candidates = [\r\n path.join(__dirname, '..', 'dashboard', 'index.html'), // from dist/hub/\r\n path.join(__dirname, 'dashboard', 'index.html'), // from dist/ (bundled index.js)\r\n path.join(__dirname, '..', '..', 'src', 'dashboard', 'index.html'), // from dist/hub/ -> src/\r\n path.join(__dirname, '..', 'src', 'dashboard', 'index.html'), // from dist/ -> src/\r\n path.join(process.cwd(), 'dist', 'dashboard', 'index.html'), // from cwd\r\n path.join(process.cwd(), 'src', 'dashboard', 'index.html'), // from cwd/src\r\n ];\r\n logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);\r\n\r\n for (const candidate of candidates) {\r\n if (fs.existsSync(candidate)) {\r\n const html = fs.readFileSync(candidate, 'utf-8');\r\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\r\n res.end(html);\r\n return;\r\n }\r\n }\r\n\r\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\r\n res.end('<html><body><h1>claude-alarm</h1><p>Dashboard HTML not found. Reinstall the package.</p></body></html>');\r\n }\r\n\r\n private async handleApiSend(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\r\n const body = await this.readBody(req);\r\n if (!body) { this.jsonResponse(res, 400, { error: 'Invalid JSON' }); return; }\r\n\r\n const { sessionId, content } = body as { sessionId?: string; content?: string };\r\n if (!sessionId || !content) {\r\n this.jsonResponse(res, 400, { error: 'sessionId and content are required' });\r\n return;\r\n }\r\n\r\n const ws = this.channelSockets.get(sessionId);\r\n if (!ws || ws.readyState !== WebSocket.OPEN) {\r\n this.jsonResponse(res, 404, { error: 'Session not connected' });\r\n return;\r\n }\r\n\r\n const msg: ChannelMessage = { type: 'message_to_session', sessionId, content };\r\n ws.send(JSON.stringify(msg));\r\n this.jsonResponse(res, 200, { ok: true });\r\n }\r\n\r\n private async handleApiNotify(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\r\n const body = await this.readBody(req);\r\n if (!body) { this.jsonResponse(res, 400, { error: 'Invalid JSON' }); return; }\r\n\r\n const { title, message, level } = body as { title?: string; message?: string; level?: string };\r\n if (!title || !message) {\r\n this.jsonResponse(res, 400, { error: 'title and message are required' });\r\n return;\r\n }\r\n\r\n await this.notifier.notify(title, message, (level as any) ?? 'info');\r\n this.jsonResponse(res, 200, { ok: true });\r\n }\r\n\r\n // --- Channel WebSocket ---\r\n\r\n private handleChannelConnection(ws: WebSocket, req: http.IncomingMessage): void {\r\n const isLocal = this.isLocalRequest(req);\r\n logger.info(`Channel server connected (local: ${isLocal})`);\r\n\r\n ws.on('message', (data) => {\r\n try {\r\n const msg = JSON.parse(data.toString()) as ChannelMessage;\r\n this.handleChannelMessage(ws, isLocal, msg);\r\n } catch {\r\n logger.warn('Invalid message from channel');\r\n }\r\n });\r\n\r\n ws.on('close', () => {\r\n // Find and remove the session for this socket\r\n for (const [sessionId, sock] of this.channelSockets) {\r\n if (sock === ws) {\r\n const session = this.sessions.unregister(sessionId);\r\n this.channelSockets.delete(sessionId);\r\n this.localChannels.delete(sessionId);\r\n logger.info(`Channel disconnected: ${sessionId}`);\r\n this.broadcastToDashboards({\r\n type: 'session_disconnected',\r\n sessionId,\r\n });\r\n break;\r\n }\r\n }\r\n });\r\n }\r\n\r\n private handleChannelMessage(ws: WebSocket, isLocal: boolean, msg: ChannelMessage): void {\r\n switch (msg.type) {\r\n case 'register': {\r\n const session = msg.session;\r\n session.isLocal = isLocal;\r\n const isReregister = !!this.sessions.get(session.id);\r\n this.sessions.register(session);\r\n this.channelSockets.set(session.id, ws);\r\n if (isLocal) this.localChannels.add(session.id);\r\n logger.info(`Session registered: ${session.id} (${session.name}, channel: ${session.channelEnabled ?? false})`);\r\n this.broadcastToDashboards({\r\n type: isReregister ? 'session_updated' : 'session_connected',\r\n session,\r\n });\r\n break;\r\n }\r\n\r\n case 'status': {\r\n const updated = this.sessions.updateStatus(msg.sessionId, msg.status);\r\n if (updated) {\r\n this.broadcastToDashboards({ type: 'session_updated', session: updated });\r\n }\r\n break;\r\n }\r\n\r\n case 'notify': {\r\n this.sessions.updateActivity(msg.sessionId);\r\n const notifySession = this.sessions.get(msg.sessionId);\r\n const notifyLabel = this.getSessionLabel(notifySession);\r\n this.notifier.notify(`[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? 'info');\r\n this.broadcastToDashboards({\r\n type: 'notification',\r\n sessionId: msg.sessionId,\r\n title: msg.title,\r\n message: msg.message,\r\n level: msg.level,\r\n timestamp: Date.now(),\r\n });\r\n break;\r\n }\r\n\r\n case 'reply': {\r\n this.sessions.updateActivity(msg.sessionId);\r\n const replySession = this.sessions.get(msg.sessionId);\r\n const replyLabel = this.getSessionLabel(replySession);\r\n this.notifier.notify(`[${replyLabel}] Reply`, msg.content.slice(0, 200), 'info');\r\n this.broadcastToDashboards({\r\n type: 'reply_from_session',\r\n sessionId: msg.sessionId,\r\n content: msg.content,\r\n timestamp: Date.now(),\r\n });\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // --- Dashboard WebSocket ---\r\n\r\n private handleDashboardConnection(ws: WebSocket): void {\r\n this.dashboardSockets.add(ws);\r\n logger.info(`Dashboard connected (total: ${this.dashboardSockets.size})`);\r\n\r\n // Send current session list\r\n const sessionsMsg: ChannelMessage = {\r\n type: 'sessions_list',\r\n sessions: this.sessions.getAll(),\r\n };\r\n ws.send(JSON.stringify(sessionsMsg));\r\n\r\n ws.on('message', (data) => {\r\n try {\r\n const msg = JSON.parse(data.toString()) as ChannelMessage;\r\n if (msg.type === 'message_to_session') {\r\n const channelWs = this.channelSockets.get(msg.sessionId);\r\n if (channelWs?.readyState === WebSocket.OPEN) {\r\n channelWs.send(JSON.stringify(msg));\r\n }\r\n } else if (msg.type === 'image_upload') {\r\n this.handleImageUpload(msg);\r\n }\r\n } catch {\r\n logger.warn('Invalid message from dashboard');\r\n }\r\n });\r\n\r\n ws.on('close', () => {\r\n this.dashboardSockets.delete(ws);\r\n logger.info(`Dashboard disconnected (total: ${this.dashboardSockets.size})`);\r\n });\r\n }\r\n\r\n // --- Helpers ---\r\n\r\n private broadcastToDashboards(msg: ChannelMessage): void {\r\n const payload = JSON.stringify(msg);\r\n for (const ws of this.dashboardSockets) {\r\n if (ws.readyState === WebSocket.OPEN) {\r\n ws.send(payload);\r\n }\r\n }\r\n }\r\n\r\n private handleImageUpload(msg: ChannelMessage & { type: 'image_upload' }): void {\r\n const { sessionId, imageData, mimeType, originalName } = msg;\r\n\r\n // Only allow for local sessions\r\n if (!this.localChannels.has(sessionId)) {\r\n logger.warn(`Image upload rejected: session ${sessionId} is not local`);\r\n return;\r\n }\r\n\r\n const channelWs = this.channelSockets.get(sessionId);\r\n if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;\r\n\r\n // Validate mime type\r\n const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];\r\n if (!allowedTypes.includes(mimeType)) return;\r\n\r\n // Extract base64 data (remove data URL prefix if present)\r\n const base64Data = imageData.replace(/^data:image\\/\\w+;base64,/, '');\r\n const buffer = Buffer.from(base64Data, 'base64');\r\n\r\n // Validate size (10MB max)\r\n if (buffer.length > 10 * 1024 * 1024) {\r\n logger.warn('Image upload rejected: exceeds 10MB');\r\n return;\r\n }\r\n\r\n // Save to uploads dir\r\n fs.mkdirSync(UPLOADS_DIR, { recursive: true });\r\n const ext = mimeType.split('/')[1] === 'jpeg' ? 'jpg' : mimeType.split('/')[1];\r\n const filename = `${randomUUID()}.${ext}`;\r\n const filePath = path.join(UPLOADS_DIR, filename);\r\n fs.writeFileSync(filePath, buffer);\r\n\r\n // Forward file path to channel\r\n const forwardMsg: ChannelMessage = {\r\n type: 'image_to_session',\r\n sessionId,\r\n imagePath: filePath,\r\n mimeType,\r\n originalName,\r\n };\r\n channelWs.send(JSON.stringify(forwardMsg));\r\n logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);\r\n\r\n // Cleanup after 5 minutes\r\n setTimeout(() => {\r\n try { fs.unlinkSync(filePath); } catch {}\r\n }, 5 * 60 * 1000);\r\n }\r\n\r\n private async handleWebhookSave(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\r\n const body = await this.readBody(req);\r\n if (!body) { this.jsonResponse(res, 400, { error: 'Invalid JSON' }); return; }\r\n const { webhooks } = body as { webhooks?: WebhookConfig[] };\r\n if (!Array.isArray(webhooks)) { this.jsonResponse(res, 400, { error: 'webhooks must be an array' }); return; }\r\n const config = loadConfig();\r\n config.webhooks = webhooks;\r\n saveConfig(config);\r\n this.notifier.configure({ webhooks });\r\n this.jsonResponse(res, 200, { ok: true });\r\n }\r\n\r\n private cleanupUploads(): void {\r\n try {\r\n if (!fs.existsSync(UPLOADS_DIR)) return;\r\n const files = fs.readdirSync(UPLOADS_DIR);\r\n for (const file of files) {\r\n try { fs.unlinkSync(path.join(UPLOADS_DIR, file)); } catch {}\r\n }\r\n if (files.length > 0) logger.info(`Cleaned up ${files.length} leftover upload(s)`);\r\n } catch {}\r\n }\r\n\r\n private getSessionLabel(session?: SessionInfo): string {\r\n if (!session) return 'unknown';\r\n return session.cwd?.replace(/^.*[/\\\\]/, '') || session.name;\r\n }\r\n\r\n private jsonResponse(res: http.ServerResponse, status: number, body: unknown): void {\r\n res.writeHead(status, { 'Content-Type': 'application/json' });\r\n res.end(JSON.stringify(body));\r\n }\r\n\r\n private isLocalRequest(req: http.IncomingMessage): boolean {\r\n const addr = req.socket.remoteAddress;\r\n return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';\r\n }\r\n\r\n private readBody(req: http.IncomingMessage, maxSize = 1024 * 1024): Promise<unknown | null> {\r\n return new Promise((resolve) => {\r\n let data = '';\r\n let size = 0;\r\n req.on('data', (chunk) => {\r\n size += chunk.length;\r\n if (size > maxSize) {\r\n req.destroy();\r\n resolve(null);\r\n return;\r\n }\r\n data += chunk;\r\n });\r\n req.on('end', () => {\r\n try {\r\n resolve(JSON.parse(data));\r\n } catch {\r\n resolve(null);\r\n }\r\n });\r\n });\r\n }\r\n}\r\n\r\n// Direct execution support\r\nif (process.argv[1] && (\r\n process.argv[1].endsWith('hub/server.js') ||\r\n process.argv[1].endsWith('hub/server.ts')\r\n)) {\r\n const hub = new HubServer();\r\n hub.start().catch((err) => {\r\n logger.error('Failed to start hub:', err);\r\n process.exit(1);\r\n });\r\n\r\n const shutdown = () => {\r\n hub.stop().then(() => process.exit(0));\r\n };\r\n process.on('SIGINT', shutdown);\r\n process.on('SIGTERM', shutdown);\r\n}\r\n","/**\r\n * Logger that writes to stderr only.\r\n * CRITICAL: In MCP channel servers, stdout is used for the stdio protocol.\r\n * Any console.log() would corrupt the protocol. Always use this logger.\r\n */\r\nexport const logger = {\r\n info(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm] ${msg}`, ...args);\r\n },\r\n warn(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm WARN] ${msg}`, ...args);\r\n },\r\n error(msg: string, ...args: unknown[]) {\r\n console.error(`[claude-alarm ERROR] ${msg}`, ...args);\r\n },\r\n debug(msg: string, ...args: unknown[]) {\r\n if (process.env.CLAUDE_ALARM_DEBUG) {\r\n console.error(`[claude-alarm DEBUG] ${msg}`, ...args);\r\n }\r\n },\r\n};\r\n","import path from 'node:path';\r\nimport os from 'node:os';\r\n\r\nexport const DEFAULT_HUB_HOST = '127.0.0.1';\r\nexport const DEFAULT_HUB_PORT = 7900;\r\n\r\nexport const CONFIG_DIR = path.join(os.homedir(), '.claude-alarm');\r\nexport const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');\r\nexport const PID_FILE = path.join(CONFIG_DIR, 'hub.pid');\r\nexport const LOG_FILE = path.join(CONFIG_DIR, 'hub.log');\r\nexport const UPLOADS_DIR = path.join(CONFIG_DIR, 'uploads');\r\n\r\nexport const WS_PATH_CHANNEL = '/ws/channel';\r\nexport const WS_PATH_DASHBOARD = '/ws/dashboard';\r\n\r\nexport const CHANNEL_SERVER_NAME = 'claude-alarm';\r\nexport const CHANNEL_SERVER_VERSION = '0.1.0';\r\n","import type { SessionInfo, SessionStatus } from '../shared/types.js';\r\n\r\nexport class SessionManager {\r\n private sessions = new Map<string, SessionInfo>();\r\n\r\n register(session: SessionInfo): void {\r\n this.sessions.set(session.id, { ...session });\r\n }\r\n\r\n unregister(sessionId: string): SessionInfo | undefined {\r\n const session = this.sessions.get(sessionId);\r\n this.sessions.delete(sessionId);\r\n return session;\r\n }\r\n\r\n updateStatus(sessionId: string, status: SessionStatus): SessionInfo | undefined {\r\n const session = this.sessions.get(sessionId);\r\n if (session) {\r\n session.status = status;\r\n session.lastActivity = Date.now();\r\n }\r\n return session;\r\n }\r\n\r\n updateActivity(sessionId: string): void {\r\n const session = this.sessions.get(sessionId);\r\n if (session) {\r\n session.lastActivity = Date.now();\r\n }\r\n }\r\n\r\n get(sessionId: string): SessionInfo | undefined {\r\n return this.sessions.get(sessionId);\r\n }\r\n\r\n getAll(): SessionInfo[] {\r\n return Array.from(this.sessions.values());\r\n }\r\n\r\n count(): number {\r\n return this.sessions.size;\r\n }\r\n}\r\n","import notifier from 'node-notifier';\r\nimport { execFile } from 'node:child_process';\r\nimport { logger } from '../shared/logger.js';\r\nimport type { NotifyLevel, WebhookConfig } from '../shared/types.js';\r\n\r\nexport class Notifier {\r\n private webhooks: WebhookConfig[] = [];\r\n private desktopEnabled = true;\r\n private notificationSettingsOpened = false;\r\n private dashboardUrl?: string;\r\n\r\n configure(options: { desktop?: boolean; webhooks?: WebhookConfig[]; dashboardUrl?: string }): void {\r\n if (options.dashboardUrl) this.dashboardUrl = options.dashboardUrl;\r\n if (options.desktop !== undefined) this.desktopEnabled = options.desktop;\r\n if (options.webhooks) this.webhooks = options.webhooks;\r\n }\r\n\r\n async notify(title: string, message: string, level: NotifyLevel = 'info'): Promise<void> {\r\n const promises: Promise<void>[] = [];\r\n\r\n if (this.desktopEnabled) {\r\n promises.push(this.sendDesktop(title, message, level));\r\n }\r\n\r\n for (const webhook of this.webhooks) {\r\n promises.push(this.sendWebhook(webhook, title, message, level));\r\n }\r\n\r\n await Promise.allSettled(promises);\r\n }\r\n\r\n private async sendDesktop(title: string, message: string, _level: NotifyLevel): Promise<void> {\r\n if (process.platform === 'win32') {\r\n // Check if notifications are enabled by running snoretoast directly\r\n const enabled = await this.checkWindowsNotifications();\r\n if (!enabled) {\r\n this.openNotificationSettings();\r\n return;\r\n }\r\n }\r\n\r\n return new Promise((resolve) => {\r\n const notification = (notifier as any).notify(\r\n {\r\n title: `Claude Alarm: ${title}`,\r\n message,\r\n sound: true,\r\n wait: true,\r\n },\r\n (err: Error | null) => {\r\n if (err) {\r\n logger.warn(`Desktop notification failed: ${err.message}`);\r\n }\r\n resolve();\r\n },\r\n );\r\n\r\n // No click handler - dashboard is already open and notifications\r\n // can be clicked there to focus the relevant session\r\n });\r\n }\r\n\r\n private checkWindowsNotifications(): Promise<boolean> {\r\n return new Promise((resolve) => {\r\n execFile(\r\n 'powershell',\r\n ['-Command', '(Get-ItemProperty -Path \"HKCU:\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\PushNotifications\" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],\r\n (err, stdout) => {\r\n if (err) { resolve(true); return; } // assume enabled on error\r\n const value = stdout.trim();\r\n resolve(value !== '0');\r\n },\r\n );\r\n });\r\n }\r\n\r\n private openNotificationSettings(): void {\r\n if (this.notificationSettingsOpened) return;\r\n this.notificationSettingsOpened = true;\r\n\r\n logger.warn('Windows notifications are disabled. Opening notification settings...');\r\n logger.warn('Please enable notifications for this app, then try again.');\r\n\r\n if (process.platform === 'win32') {\r\n execFile('powershell', ['-Command', 'Start-Process ms-settings:notifications']);\r\n }\r\n\r\n // Allow re-opening after 5 minutes\r\n setTimeout(() => { this.notificationSettingsOpened = false; }, 5 * 60 * 1000);\r\n }\r\n\r\n private async sendWebhook(\r\n webhook: WebhookConfig,\r\n title: string,\r\n message: string,\r\n level: NotifyLevel,\r\n ): Promise<void> {\r\n try {\r\n const response = await fetch(webhook.url, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n ...webhook.headers,\r\n },\r\n body: JSON.stringify({\r\n title,\r\n message,\r\n level,\r\n timestamp: Date.now(),\r\n source: 'claude-alarm',\r\n }),\r\n });\r\n\r\n if (!response.ok) {\r\n logger.warn(`Webhook ${webhook.url} returned ${response.status}`);\r\n }\r\n } catch (err) {\r\n logger.warn(`Webhook ${webhook.url} failed: ${(err as Error).message}`);\r\n }\r\n }\r\n}\r\n","import fs from 'node:fs';\r\nimport path from 'node:path';\r\nimport { randomUUID } from 'node:crypto';\r\nimport { CONFIG_DIR, CONFIG_FILE, DEFAULT_HUB_HOST, DEFAULT_HUB_PORT } from './constants.js';\r\nimport type { AppConfig } from './types.js';\r\n\r\nconst DEFAULT_CONFIG: AppConfig = {\r\n hub: {\r\n host: DEFAULT_HUB_HOST,\r\n port: DEFAULT_HUB_PORT,\r\n },\r\n notifications: {\r\n desktop: true,\r\n sound: true,\r\n },\r\n webhooks: [],\r\n};\r\n\r\nexport function ensureConfigDir(): void {\r\n if (!fs.existsSync(CONFIG_DIR)) {\r\n fs.mkdirSync(CONFIG_DIR, { recursive: true });\r\n }\r\n}\r\n\r\nexport function loadConfig(): AppConfig {\r\n ensureConfigDir();\r\n let config: AppConfig;\r\n if (!fs.existsSync(CONFIG_FILE)) {\r\n config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };\r\n } else {\r\n try {\r\n const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');\r\n const parsed = JSON.parse(raw);\r\n config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub } };\r\n } catch {\r\n config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };\r\n }\r\n }\r\n\r\n // Auto-generate token if missing\r\n if (!config.hub.token) {\r\n config.hub.token = randomUUID();\r\n saveConfig(config);\r\n }\r\n\r\n return config;\r\n}\r\n\r\n/** Get the current token, generating one if needed */\r\nexport function getOrCreateToken(): string {\r\n const config = loadConfig();\r\n return config.hub.token!;\r\n}\r\n\r\nexport function saveConfig(config: AppConfig): void {\r\n ensureConfigDir();\r\n fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });\r\n}\r\n\r\n/**\r\n * Add claude-alarm as an MCP channel server to .mcp.json\r\n */\r\nexport function setupMcpConfig(targetDir?: string): string {\r\n const dir = targetDir ?? process.cwd();\r\n const mcpPath = path.join(dir, '.mcp.json');\r\n\r\n let mcpConfig: Record<string, any> = {};\r\n if (fs.existsSync(mcpPath)) {\r\n try {\r\n mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));\r\n } catch {\r\n mcpConfig = {};\r\n }\r\n }\r\n\r\n if (!mcpConfig.mcpServers) {\r\n mcpConfig.mcpServers = {};\r\n }\r\n\r\n mcpConfig.mcpServers['claude-alarm'] = {\r\n command: 'npx',\r\n args: ['-y', '@delt/claude-alarm', 'serve'],\r\n env: {\r\n CLAUDE_ALARM_SESSION_NAME: path.basename(dir),\r\n },\r\n };\r\n\r\n fs.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');\r\n return mcpPath;\r\n}\r\n"],"mappings":"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","names":["fs","path","randomUUID","path","path","fs","randomUUID"]}
|
package/dist/index.d.ts
CHANGED