@delt/claude-alarm 0.4.8 → 0.5.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/README.md +215 -182
- package/dist/channel/server.js +1 -1
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +274 -3
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +130 -15
- package/dist/hub/server.js +266 -3
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +266 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +1339 -1224
|
@@ -227,6 +227,8 @@
|
|
|
227
227
|
.messages-panel {
|
|
228
228
|
display: flex;
|
|
229
229
|
flex-direction: column;
|
|
230
|
+
overflow: hidden;
|
|
231
|
+
min-height: 0;
|
|
230
232
|
}
|
|
231
233
|
.messages-header {
|
|
232
234
|
padding: 12px 20px;
|
|
@@ -237,6 +239,7 @@
|
|
|
237
239
|
.messages-list {
|
|
238
240
|
flex: 1;
|
|
239
241
|
overflow-y: auto;
|
|
242
|
+
min-height: 0;
|
|
240
243
|
padding: 16px 20px;
|
|
241
244
|
}
|
|
242
245
|
.message {
|
|
@@ -611,7 +614,7 @@
|
|
|
611
614
|
<header>
|
|
612
615
|
<h1>Claude Alarm</h1>
|
|
613
616
|
<div class="header-right">
|
|
614
|
-
<button class="theme-toggle" id="
|
|
617
|
+
<button class="theme-toggle" id="settingsToggle" title="Notification settings">⚙</button>
|
|
615
618
|
<button class="theme-toggle" id="themeToggle" title="Toggle theme">☾</button>
|
|
616
619
|
<div class="status-badge">
|
|
617
620
|
<span class="status-dot" id="connDot"></span>
|
|
@@ -630,15 +633,42 @@
|
|
|
630
633
|
</div>
|
|
631
634
|
</div>
|
|
632
635
|
|
|
633
|
-
<div class="token-overlay hidden" id="
|
|
634
|
-
<div class="token-form" style="max-width:
|
|
635
|
-
<h2 style="text-align:center;margin-bottom:
|
|
636
|
-
<
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
<
|
|
636
|
+
<div class="token-overlay hidden" id="settingsOverlay">
|
|
637
|
+
<div class="token-form" style="max-width:520px;text-align:left">
|
|
638
|
+
<h2 style="text-align:center;margin-bottom:12px">Notification Settings</h2>
|
|
639
|
+
<div style="display:flex;border-bottom:1px solid var(--border);margin-bottom:16px">
|
|
640
|
+
<button class="settings-tab active" data-settings-tab="webhook" style="flex:1;padding:10px;background:none;border:none;border-bottom:2px solid var(--accent);color:var(--text);cursor:pointer;font-size:13px;font-weight:600;transition:all 0.15s">Webhook</button>
|
|
641
|
+
<button class="settings-tab" data-settings-tab="telegram" style="flex:1;padding:10px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-dim);cursor:pointer;font-size:13px;font-weight:500;transition:all 0.15s">Telegram</button>
|
|
642
|
+
</div>
|
|
643
|
+
<div id="settingsWebhook">
|
|
644
|
+
<p style="margin-bottom:12px;font-size:12px;color:var(--text-dim)">Send notifications to Slack, Discord, or any webhook endpoint.</p>
|
|
645
|
+
<div id="webhookList" style="max-height:200px;overflow-y:auto"></div>
|
|
646
|
+
<button id="webhookAdd" style="width:100%;margin-top:10px;background:none;color:var(--text-dim);border:1px dashed var(--border);border-radius:6px;padding:10px;cursor:pointer;font-size:13px;transition:border-color 0.15s">+ Add Webhook</button>
|
|
647
|
+
<div style="display:flex;gap:8px;margin-top:16px">
|
|
648
|
+
<button id="webhookSave" style="flex:1;padding:10px">Save</button>
|
|
649
|
+
<button id="settingsCancel1" style="flex:1;padding:10px;background:none;color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:14px;font-weight:500">Cancel</button>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
<div id="settingsTelegram" style="display:none">
|
|
653
|
+
<p style="margin-bottom:12px;font-size:12px;color:var(--text-dim)">Connect a Telegram bot to receive notifications and send messages.</p>
|
|
654
|
+
<div style="margin-bottom:12px">
|
|
655
|
+
<label style="display:block;font-size:12px;color:var(--text-dim);margin-bottom:4px">Bot Token</label>
|
|
656
|
+
<input type="text" id="tgBotToken" placeholder="123456:ABC-DEF..." autocomplete="off" style="width:100%;box-sizing:border-box;padding:10px;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:monospace">
|
|
657
|
+
</div>
|
|
658
|
+
<div style="margin-bottom:12px">
|
|
659
|
+
<label style="display:block;font-size:12px;color:var(--text-dim);margin-bottom:4px">Chat ID</label>
|
|
660
|
+
<input type="text" id="tgChatId" placeholder="-1001234567890" autocomplete="off" style="width:100%;box-sizing:border-box;padding:10px;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;font-family:monospace">
|
|
661
|
+
</div>
|
|
662
|
+
<div style="margin-bottom:16px;display:flex;align-items:center;gap:8px">
|
|
663
|
+
<input type="checkbox" id="tgEnabled" style="width:16px;height:16px;accent-color:var(--accent)">
|
|
664
|
+
<label for="tgEnabled" style="font-size:13px;color:var(--text)">Enable Telegram notifications</label>
|
|
665
|
+
</div>
|
|
666
|
+
<div id="tgStatus" style="display:none;margin-bottom:12px;padding:8px 12px;border-radius:6px;font-size:12px"></div>
|
|
667
|
+
<div style="display:flex;gap:8px">
|
|
668
|
+
<button id="tgTest" style="flex:1;padding:10px;background:none;color:var(--accent);border:1px solid var(--accent);border-radius:6px;cursor:pointer;font-size:13px;font-weight:500">Test</button>
|
|
669
|
+
<button id="tgSave" style="flex:1;padding:10px">Save</button>
|
|
670
|
+
<button id="settingsCancel2" style="flex:1;padding:10px;background:none;color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:14px;font-weight:500">Cancel</button>
|
|
671
|
+
</div>
|
|
642
672
|
</div>
|
|
643
673
|
</div>
|
|
644
674
|
</div>
|
|
@@ -1144,9 +1174,32 @@
|
|
|
1144
1174
|
});
|
|
1145
1175
|
});
|
|
1146
1176
|
|
|
1147
|
-
// --- Webhook
|
|
1177
|
+
// --- Settings modal (Webhook + Telegram) ---
|
|
1148
1178
|
let webhookData = [];
|
|
1149
|
-
$('#
|
|
1179
|
+
const closeSettings = () => $('#settingsOverlay').classList.add('hidden');
|
|
1180
|
+
|
|
1181
|
+
// Tab switching
|
|
1182
|
+
document.querySelectorAll('.settings-tab').forEach(tab => {
|
|
1183
|
+
tab.addEventListener('click', () => {
|
|
1184
|
+
document.querySelectorAll('.settings-tab').forEach(t => {
|
|
1185
|
+
t.classList.remove('active');
|
|
1186
|
+
t.style.borderBottomColor = 'transparent';
|
|
1187
|
+
t.style.color = 'var(--text-dim)';
|
|
1188
|
+
t.style.fontWeight = '500';
|
|
1189
|
+
});
|
|
1190
|
+
tab.classList.add('active');
|
|
1191
|
+
tab.style.borderBottomColor = 'var(--accent)';
|
|
1192
|
+
tab.style.color = 'var(--text)';
|
|
1193
|
+
tab.style.fontWeight = '600';
|
|
1194
|
+
const target = tab.dataset.settingsTab;
|
|
1195
|
+
$('#settingsWebhook').style.display = target === 'webhook' ? 'block' : 'none';
|
|
1196
|
+
$('#settingsTelegram').style.display = target === 'telegram' ? 'block' : 'none';
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// Open settings
|
|
1201
|
+
$('#settingsToggle').addEventListener('click', async () => {
|
|
1202
|
+
// Load webhook data
|
|
1150
1203
|
try {
|
|
1151
1204
|
const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
|
|
1152
1205
|
const res = await fetch(`/api/webhooks${tokenQuery}`);
|
|
@@ -1154,9 +1207,27 @@
|
|
|
1154
1207
|
webhookData = data.webhooks || [];
|
|
1155
1208
|
} catch { webhookData = []; }
|
|
1156
1209
|
renderWebhooks();
|
|
1157
|
-
|
|
1210
|
+
// Load telegram data
|
|
1211
|
+
try {
|
|
1212
|
+
const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
|
|
1213
|
+
const res = await fetch(`/api/telegram${tokenQuery}`);
|
|
1214
|
+
const data = await res.json();
|
|
1215
|
+
const tg = data.telegram || {};
|
|
1216
|
+
if (!$('#tgBotToken').value || $('#tgBotToken').value.includes('...')) {
|
|
1217
|
+
$('#tgBotToken').value = tg.botToken || '';
|
|
1218
|
+
}
|
|
1219
|
+
$('#tgChatId').value = tg.chatId || '';
|
|
1220
|
+
$('#tgEnabled').checked = !!tg.enabled;
|
|
1221
|
+
} catch {}
|
|
1222
|
+
$('#tgStatus').style.display = 'none';
|
|
1223
|
+
$('#settingsOverlay').classList.remove('hidden');
|
|
1158
1224
|
});
|
|
1159
|
-
|
|
1225
|
+
|
|
1226
|
+
// Cancel buttons
|
|
1227
|
+
$('#settingsCancel1').addEventListener('click', closeSettings);
|
|
1228
|
+
$('#settingsCancel2').addEventListener('click', closeSettings);
|
|
1229
|
+
|
|
1230
|
+
// Webhook
|
|
1160
1231
|
$('#webhookAdd').addEventListener('click', () => {
|
|
1161
1232
|
webhookData.push({ url: '' });
|
|
1162
1233
|
renderWebhooks();
|
|
@@ -1171,7 +1242,7 @@
|
|
|
1171
1242
|
body: JSON.stringify({ webhooks: valid }),
|
|
1172
1243
|
});
|
|
1173
1244
|
} catch {}
|
|
1174
|
-
|
|
1245
|
+
closeSettings();
|
|
1175
1246
|
});
|
|
1176
1247
|
function renderWebhooks() {
|
|
1177
1248
|
const el = $('#webhookList');
|
|
@@ -1198,6 +1269,50 @@
|
|
|
1198
1269
|
});
|
|
1199
1270
|
}
|
|
1200
1271
|
|
|
1272
|
+
// Telegram
|
|
1273
|
+
function showTgStatus(msg, ok) {
|
|
1274
|
+
const el = $('#tgStatus');
|
|
1275
|
+
el.textContent = msg;
|
|
1276
|
+
el.style.display = 'block';
|
|
1277
|
+
el.style.background = ok ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)';
|
|
1278
|
+
el.style.color = ok ? 'var(--green)' : 'var(--red)';
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
$('#tgTest').addEventListener('click', async () => {
|
|
1282
|
+
const botToken = $('#tgBotToken').value.trim();
|
|
1283
|
+
const chatId = $('#tgChatId').value.trim();
|
|
1284
|
+
if (!botToken || !chatId) { showTgStatus('Bot Token and Chat ID are required.', false); return; }
|
|
1285
|
+
if (botToken.includes('...')) { showTgStatus('Please enter full Bot Token (not masked).', false); return; }
|
|
1286
|
+
try {
|
|
1287
|
+
const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
|
|
1288
|
+
const res = await fetch(`/api/telegram/test${tokenQuery}`, {
|
|
1289
|
+
method: 'POST',
|
|
1290
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1291
|
+
body: JSON.stringify({ botToken, chatId }),
|
|
1292
|
+
});
|
|
1293
|
+
const data = await res.json();
|
|
1294
|
+
if (data.ok) { showTgStatus('Test message sent! Check your Telegram.', true); }
|
|
1295
|
+
else { showTgStatus(data.error || 'Test failed.', false); }
|
|
1296
|
+
} catch (e) { showTgStatus('Connection error.', false); }
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
$('#tgSave').addEventListener('click', async () => {
|
|
1300
|
+
const botToken = $('#tgBotToken').value.trim();
|
|
1301
|
+
const chatId = $('#tgChatId').value.trim();
|
|
1302
|
+
const enabled = $('#tgEnabled').checked;
|
|
1303
|
+
if (enabled && botToken.includes('...')) { showTgStatus('Please enter full Bot Token to enable.', false); return; }
|
|
1304
|
+
try {
|
|
1305
|
+
const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
|
|
1306
|
+
await fetch(`/api/telegram${tokenQuery}`, {
|
|
1307
|
+
method: 'POST',
|
|
1308
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1309
|
+
body: JSON.stringify({ telegram: { botToken, chatId, enabled } }),
|
|
1310
|
+
});
|
|
1311
|
+
showTgStatus('Saved!', true);
|
|
1312
|
+
setTimeout(closeSettings, 800);
|
|
1313
|
+
} catch { showTgStatus('Save failed.', false); }
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1201
1316
|
// Refresh relative times every 30s
|
|
1202
1317
|
setInterval(() => { renderMessages(); renderNotifications(); }, 30000);
|
|
1203
1318
|
|
package/dist/hub/server.js
CHANGED
|
@@ -85,12 +85,17 @@ var Notifier = class {
|
|
|
85
85
|
desktopEnabled = true;
|
|
86
86
|
notificationSettingsOpened = false;
|
|
87
87
|
dashboardUrl;
|
|
88
|
+
telegramBot;
|
|
88
89
|
configure(options) {
|
|
89
90
|
if (options.dashboardUrl) this.dashboardUrl = options.dashboardUrl;
|
|
90
91
|
if (options.desktop !== void 0) this.desktopEnabled = options.desktop;
|
|
91
92
|
if (options.webhooks) this.webhooks = options.webhooks;
|
|
93
|
+
if (options.telegramBot) this.telegramBot = options.telegramBot;
|
|
92
94
|
}
|
|
93
95
|
async notify(title, message, level = "info") {
|
|
96
|
+
await this.notifyWithSession(void 0, void 0, title, message, level);
|
|
97
|
+
}
|
|
98
|
+
async notifyWithSession(sessionId, sessionLabel, title, message, level = "info") {
|
|
94
99
|
const promises = [];
|
|
95
100
|
if (this.desktopEnabled) {
|
|
96
101
|
promises.push(this.sendDesktop(title, message, level));
|
|
@@ -98,6 +103,9 @@ var Notifier = class {
|
|
|
98
103
|
for (const webhook of this.webhooks) {
|
|
99
104
|
promises.push(this.sendWebhook(webhook, title, message, level));
|
|
100
105
|
}
|
|
106
|
+
if (this.telegramBot && sessionId && sessionLabel) {
|
|
107
|
+
promises.push(this.telegramBot.sendNotification(sessionId, sessionLabel, title, message));
|
|
108
|
+
}
|
|
101
109
|
await Promise.allSettled(promises);
|
|
102
110
|
}
|
|
103
111
|
async sendDesktop(title, message, _level) {
|
|
@@ -178,6 +186,176 @@ var Notifier = class {
|
|
|
178
186
|
}
|
|
179
187
|
};
|
|
180
188
|
|
|
189
|
+
// src/hub/telegram.ts
|
|
190
|
+
var TELEGRAM_API = "https://api.telegram.org/bot";
|
|
191
|
+
var TelegramBot = class {
|
|
192
|
+
config;
|
|
193
|
+
offset = 0;
|
|
194
|
+
polling = false;
|
|
195
|
+
pollTimer = null;
|
|
196
|
+
// message_id -> sessionId mapping for reply-based routing
|
|
197
|
+
messageSessionMap = /* @__PURE__ */ new Map();
|
|
198
|
+
// Callback: when a message arrives from Telegram for a session
|
|
199
|
+
onMessageToSession;
|
|
200
|
+
// Callback: get current sessions list
|
|
201
|
+
getSessions;
|
|
202
|
+
// Callback: when user needs to select a session (sends inline keyboard)
|
|
203
|
+
pendingMessages = /* @__PURE__ */ new Map();
|
|
204
|
+
// chatId -> pending message text
|
|
205
|
+
constructor(config) {
|
|
206
|
+
this.config = config;
|
|
207
|
+
}
|
|
208
|
+
get apiUrl() {
|
|
209
|
+
return `${TELEGRAM_API}${this.config.botToken}`;
|
|
210
|
+
}
|
|
211
|
+
/** Send a notification message to Telegram */
|
|
212
|
+
async sendNotification(sessionId, sessionLabel, title, message) {
|
|
213
|
+
const text = `[${sessionLabel}] ${title}
|
|
214
|
+
${message}`;
|
|
215
|
+
const result = await this.sendMessage(text);
|
|
216
|
+
if (result?.message_id) {
|
|
217
|
+
this.messageSessionMap.set(result.message_id, sessionId);
|
|
218
|
+
if (this.messageSessionMap.size > 200) {
|
|
219
|
+
const keys = [...this.messageSessionMap.keys()];
|
|
220
|
+
for (let i = 0; i < keys.length - 200; i++) {
|
|
221
|
+
this.messageSessionMap.delete(keys[i]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** Send a text message to the configured chat */
|
|
227
|
+
async sendMessage(text, replyToMessageId, replyMarkup) {
|
|
228
|
+
try {
|
|
229
|
+
const body = {
|
|
230
|
+
chat_id: this.config.chatId,
|
|
231
|
+
text,
|
|
232
|
+
parse_mode: "HTML"
|
|
233
|
+
};
|
|
234
|
+
if (replyToMessageId) body.reply_to_message_id = replyToMessageId;
|
|
235
|
+
if (replyMarkup) body.reply_markup = replyMarkup;
|
|
236
|
+
const res = await fetch(`${this.apiUrl}/sendMessage`, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "Content-Type": "application/json" },
|
|
239
|
+
body: JSON.stringify(body)
|
|
240
|
+
});
|
|
241
|
+
if (!res.ok) {
|
|
242
|
+
const err = await res.text();
|
|
243
|
+
logger.warn(`Telegram sendMessage failed: ${res.status} ${err}`);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const data = await res.json();
|
|
247
|
+
return data.ok ? data.result : null;
|
|
248
|
+
} catch (err) {
|
|
249
|
+
logger.warn(`Telegram sendMessage error: ${err.message}`);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Start long polling for incoming messages */
|
|
254
|
+
startPolling() {
|
|
255
|
+
if (this.polling) return;
|
|
256
|
+
this.polling = true;
|
|
257
|
+
logger.info("Telegram bot polling started");
|
|
258
|
+
this.poll();
|
|
259
|
+
}
|
|
260
|
+
/** Stop polling */
|
|
261
|
+
stopPolling() {
|
|
262
|
+
this.polling = false;
|
|
263
|
+
if (this.pollTimer) {
|
|
264
|
+
clearTimeout(this.pollTimer);
|
|
265
|
+
this.pollTimer = null;
|
|
266
|
+
}
|
|
267
|
+
logger.info("Telegram bot polling stopped");
|
|
268
|
+
}
|
|
269
|
+
async poll() {
|
|
270
|
+
if (!this.polling) return;
|
|
271
|
+
try {
|
|
272
|
+
const res = await fetch(`${this.apiUrl}/getUpdates?offset=${this.offset}&timeout=30`, {
|
|
273
|
+
signal: AbortSignal.timeout(35e3)
|
|
274
|
+
});
|
|
275
|
+
if (!res.ok) {
|
|
276
|
+
logger.warn(`Telegram getUpdates failed: ${res.status}`);
|
|
277
|
+
this.scheduleNextPoll(5e3);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const data = await res.json();
|
|
281
|
+
if (data.ok && data.result.length > 0) {
|
|
282
|
+
for (const update of data.result) {
|
|
283
|
+
this.offset = update.update_id + 1;
|
|
284
|
+
if (update.message) {
|
|
285
|
+
this.handleIncomingMessage(update.message);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (err.name !== "AbortError") {
|
|
291
|
+
logger.warn(`Telegram poll error: ${err.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
this.scheduleNextPoll(1e3);
|
|
295
|
+
}
|
|
296
|
+
scheduleNextPoll(delay) {
|
|
297
|
+
if (!this.polling) return;
|
|
298
|
+
this.pollTimer = setTimeout(() => this.poll(), delay);
|
|
299
|
+
}
|
|
300
|
+
handleIncomingMessage(msg) {
|
|
301
|
+
if (!msg.text) return;
|
|
302
|
+
if (String(msg.chat.id) !== String(this.config.chatId)) return;
|
|
303
|
+
const text = msg.text.trim();
|
|
304
|
+
if (msg.reply_to_message) {
|
|
305
|
+
const sessionId = this.messageSessionMap.get(msg.reply_to_message.message_id);
|
|
306
|
+
if (sessionId) {
|
|
307
|
+
this.deliverToSession(sessionId, text);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const selectMatch = text.match(/^\/s_(\d+)$/);
|
|
312
|
+
if (selectMatch) {
|
|
313
|
+
const pendingText = this.pendingMessages.get(msg.chat.id);
|
|
314
|
+
if (pendingText) {
|
|
315
|
+
this.pendingMessages.delete(msg.chat.id);
|
|
316
|
+
const sessions2 = this.getSessions?.() ?? [];
|
|
317
|
+
const idx = parseInt(selectMatch[1], 10) - 1;
|
|
318
|
+
if (idx >= 0 && idx < sessions2.length) {
|
|
319
|
+
this.deliverToSession(sessions2[idx].id, pendingText);
|
|
320
|
+
this.sendMessage(`Sent to [${this.getLabel(sessions2[idx])}]`);
|
|
321
|
+
} else {
|
|
322
|
+
this.sendMessage("Invalid session number.");
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const sessions = this.getSessions?.() ?? [];
|
|
328
|
+
if (sessions.length === 0) {
|
|
329
|
+
this.sendMessage("No active sessions connected.");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (sessions.length === 1) {
|
|
333
|
+
this.deliverToSession(sessions[0].id, text);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
this.pendingMessages.set(msg.chat.id, text);
|
|
337
|
+
const sessionList = sessions.map((s, i) => `/s_${i + 1} - ${this.getLabel(s)}`).join("\n");
|
|
338
|
+
this.sendMessage(`Multiple sessions active. Reply with a command to select:
|
|
339
|
+
|
|
340
|
+
${sessionList}`);
|
|
341
|
+
}
|
|
342
|
+
deliverToSession(sessionId, content) {
|
|
343
|
+
if (this.onMessageToSession) {
|
|
344
|
+
this.onMessageToSession(sessionId, content);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
getLabel(session) {
|
|
348
|
+
return session.cwd?.replace(/^.*[/\\]/, "") || session.name;
|
|
349
|
+
}
|
|
350
|
+
/** Update config (e.g., from dashboard settings) */
|
|
351
|
+
updateConfig(config) {
|
|
352
|
+
const wasPolling = this.polling;
|
|
353
|
+
if (wasPolling) this.stopPolling();
|
|
354
|
+
this.config = config;
|
|
355
|
+
if (wasPolling && config.enabled) this.startPolling();
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
181
359
|
// src/shared/config.ts
|
|
182
360
|
import fs from "fs";
|
|
183
361
|
import path2 from "path";
|
|
@@ -207,7 +385,7 @@ function loadConfig() {
|
|
|
207
385
|
try {
|
|
208
386
|
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
209
387
|
const parsed = JSON.parse(raw);
|
|
210
|
-
config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub } };
|
|
388
|
+
config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...parsed.telegram ? { telegram: parsed.telegram } : {} };
|
|
211
389
|
} catch {
|
|
212
390
|
config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
|
|
213
391
|
}
|
|
@@ -238,6 +416,7 @@ var HubServer = class {
|
|
|
238
416
|
localChannels = /* @__PURE__ */ new Set();
|
|
239
417
|
// All connected dashboard WebSockets
|
|
240
418
|
dashboardSockets = /* @__PURE__ */ new Set();
|
|
419
|
+
telegramBot;
|
|
241
420
|
host;
|
|
242
421
|
port;
|
|
243
422
|
token;
|
|
@@ -255,6 +434,10 @@ var HubServer = class {
|
|
|
255
434
|
}
|
|
256
435
|
const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
|
|
257
436
|
this.notifier.configure({ dashboardUrl: `http://${displayHost}:${this.port}` });
|
|
437
|
+
const fullConfig = loadConfig();
|
|
438
|
+
if (fullConfig.telegram?.enabled && fullConfig.telegram.botToken && fullConfig.telegram.chatId) {
|
|
439
|
+
this.initTelegram(fullConfig.telegram);
|
|
440
|
+
}
|
|
258
441
|
this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
|
|
259
442
|
this.wssChannel = new WebSocketServer({ noServer: true });
|
|
260
443
|
this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
|
|
@@ -296,6 +479,7 @@ var HubServer = class {
|
|
|
296
479
|
}
|
|
297
480
|
stop() {
|
|
298
481
|
return new Promise((resolve) => {
|
|
482
|
+
if (this.telegramBot) this.telegramBot.stopPolling();
|
|
299
483
|
for (const ws of this.channelSockets.values()) ws.terminate();
|
|
300
484
|
for (const ws of this.dashboardSockets) ws.terminate();
|
|
301
485
|
this.channelSockets.clear();
|
|
@@ -357,6 +541,16 @@ var HubServer = class {
|
|
|
357
541
|
this.jsonResponse(res, 200, { webhooks: config.webhooks || [] });
|
|
358
542
|
} else if (url.pathname === "/api/webhooks" && req.method === "POST") {
|
|
359
543
|
this.handleWebhookSave(req, res);
|
|
544
|
+
} else if (url.pathname === "/api/telegram" && req.method === "GET") {
|
|
545
|
+
const cfg = loadConfig();
|
|
546
|
+
const tg = cfg.telegram ?? { botToken: "", chatId: "", enabled: false };
|
|
547
|
+
this.jsonResponse(res, 200, {
|
|
548
|
+
telegram: { ...tg, botToken: tg.botToken ? `${tg.botToken.slice(0, 8)}...` : "" }
|
|
549
|
+
});
|
|
550
|
+
} else if (url.pathname === "/api/telegram" && req.method === "POST") {
|
|
551
|
+
this.handleTelegramSave(req, res);
|
|
552
|
+
} else if (url.pathname === "/api/telegram/test" && req.method === "POST") {
|
|
553
|
+
this.handleTelegramTest(req, res);
|
|
360
554
|
} else {
|
|
361
555
|
this.jsonResponse(res, 404, { error: "Not found" });
|
|
362
556
|
}
|
|
@@ -477,7 +671,7 @@ var HubServer = class {
|
|
|
477
671
|
this.sessions.updateActivity(msg.sessionId);
|
|
478
672
|
const notifySession = this.sessions.get(msg.sessionId);
|
|
479
673
|
const notifyLabel = this.getSessionLabel(notifySession);
|
|
480
|
-
this.notifier.
|
|
674
|
+
this.notifier.notifyWithSession(msg.sessionId, notifyLabel, `[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
|
|
481
675
|
this.broadcastToDashboards({
|
|
482
676
|
type: "notification",
|
|
483
677
|
sessionId: msg.sessionId,
|
|
@@ -492,7 +686,7 @@ var HubServer = class {
|
|
|
492
686
|
this.sessions.updateActivity(msg.sessionId);
|
|
493
687
|
const replySession = this.sessions.get(msg.sessionId);
|
|
494
688
|
const replyLabel = this.getSessionLabel(replySession);
|
|
495
|
-
this.notifier.
|
|
689
|
+
this.notifier.notifyWithSession(msg.sessionId, replyLabel, `[${replyLabel}] Reply`, msg.content.slice(0, 200), "info");
|
|
496
690
|
this.broadcastToDashboards({
|
|
497
691
|
type: "reply_from_session",
|
|
498
692
|
sessionId: msg.sessionId,
|
|
@@ -595,6 +789,75 @@ var HubServer = class {
|
|
|
595
789
|
this.notifier.configure({ webhooks });
|
|
596
790
|
this.jsonResponse(res, 200, { ok: true });
|
|
597
791
|
}
|
|
792
|
+
initTelegram(config) {
|
|
793
|
+
this.telegramBot = new TelegramBot(config);
|
|
794
|
+
this.telegramBot.getSessions = () => this.sessions.getAll();
|
|
795
|
+
this.telegramBot.onMessageToSession = (sessionId, content) => {
|
|
796
|
+
const channelWs = this.channelSockets.get(sessionId);
|
|
797
|
+
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
798
|
+
const msg = { type: "message_to_session", sessionId, content };
|
|
799
|
+
channelWs.send(JSON.stringify(msg));
|
|
800
|
+
logger.info(`Telegram message forwarded to session: ${sessionId}`);
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
this.notifier.configure({ telegramBot: this.telegramBot });
|
|
804
|
+
this.telegramBot.startPolling();
|
|
805
|
+
logger.info("Telegram bot initialized");
|
|
806
|
+
}
|
|
807
|
+
async handleTelegramSave(req, res) {
|
|
808
|
+
const body = await this.readBody(req);
|
|
809
|
+
if (!body) {
|
|
810
|
+
this.jsonResponse(res, 400, { error: "Invalid JSON" });
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const { telegram } = body;
|
|
814
|
+
if (!telegram) {
|
|
815
|
+
this.jsonResponse(res, 400, { error: "telegram config required" });
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const config = loadConfig();
|
|
819
|
+
config.telegram = telegram;
|
|
820
|
+
saveConfig(config);
|
|
821
|
+
if (this.telegramBot) {
|
|
822
|
+
this.telegramBot.stopPolling();
|
|
823
|
+
this.telegramBot = void 0;
|
|
824
|
+
this.notifier.configure({ telegramBot: void 0 });
|
|
825
|
+
}
|
|
826
|
+
if (telegram.enabled && telegram.botToken && telegram.chatId) {
|
|
827
|
+
this.initTelegram(telegram);
|
|
828
|
+
}
|
|
829
|
+
this.jsonResponse(res, 200, { ok: true });
|
|
830
|
+
}
|
|
831
|
+
async handleTelegramTest(req, res) {
|
|
832
|
+
const body = await this.readBody(req);
|
|
833
|
+
if (!body) {
|
|
834
|
+
this.jsonResponse(res, 400, { error: "Invalid JSON" });
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const { botToken, chatId } = body;
|
|
838
|
+
if (!botToken || !chatId) {
|
|
839
|
+
this.jsonResponse(res, 400, { error: "botToken and chatId required" });
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
const testRes = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
844
|
+
method: "POST",
|
|
845
|
+
headers: { "Content-Type": "application/json" },
|
|
846
|
+
body: JSON.stringify({
|
|
847
|
+
chat_id: chatId,
|
|
848
|
+
text: "Claude Alarm test message! Connection successful."
|
|
849
|
+
})
|
|
850
|
+
});
|
|
851
|
+
if (testRes.ok) {
|
|
852
|
+
this.jsonResponse(res, 200, { ok: true });
|
|
853
|
+
} else {
|
|
854
|
+
const err = await testRes.json();
|
|
855
|
+
this.jsonResponse(res, 400, { error: err.description || "Telegram API error" });
|
|
856
|
+
}
|
|
857
|
+
} catch (err) {
|
|
858
|
+
this.jsonResponse(res, 500, { error: err.message });
|
|
859
|
+
}
|
|
860
|
+
}
|
|
598
861
|
cleanupUploads() {
|
|
599
862
|
try {
|
|
600
863
|
if (!fs2.existsSync(UPLOADS_DIR)) return;
|