@delt/claude-alarm 0.5.2 → 0.5.4

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.
@@ -650,24 +650,64 @@
650
650
  </div>
651
651
  </div>
652
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">
653
+ <!-- Step 1: Bot Token -->
654
+ <div id="tgStep1">
655
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
656
+ <span style="background:var(--accent);color:#fff;border-radius:50%;width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0">1</span>
657
+ <span style="font-size:13px;font-weight:600;color:var(--text)">Create a bot &amp; paste the token</span>
658
+ </div>
659
+ <p style="margin:0 0 10px 30px;font-size:12px;color:var(--text-dim)">Open <strong>@BotFather</strong> on Telegram → <code>/newbot</code> → copy the token</p>
660
+ <div style="margin-left:30px;margin-bottom:12px">
661
+ <input type="text" id="tgBotToken" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v..." 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">
662
+ </div>
663
+ <div style="margin-left:30px">
664
+ <button id="tgNextStep2" style="padding:8px 20px;font-size:13px">Next →</button>
665
+ </div>
657
666
  </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>
667
+
668
+ <!-- Step 2: Detect Chat ID -->
669
+ <div id="tgStep2" style="display:none">
670
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
671
+ <span style="background:var(--accent);color:#fff;border-radius:50%;width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0">2</span>
672
+ <span style="font-size:13px;font-weight:600;color:var(--text)">Find your Chat ID</span>
673
+ </div>
674
+ <p style="margin:0 0 10px 30px;font-size:12px;color:var(--text-dim)">Send any message to your bot on Telegram, then click Detect.</p>
675
+ <div style="margin-left:30px;margin-bottom:10px;display:flex;gap:8px">
676
+ <button id="tgDetect" style="padding:8px 20px;font-size:13px;background:none;color:var(--accent);border:1px solid var(--accent);border-radius:6px;cursor:pointer;font-weight:500">Detect Chat ID</button>
677
+ <span id="tgDetectStatus" style="font-size:12px;color:var(--text-dim);display:flex;align-items:center"></span>
678
+ </div>
679
+ <div id="tgChatList" style="margin-left:30px;margin-bottom:10px"></div>
680
+ <div style="margin-left:30px;margin-bottom:12px">
681
+ <label style="display:block;font-size:11px;color:var(--text-dim);margin-bottom:4px">Or enter manually:</label>
682
+ <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">
683
+ </div>
684
+ <div style="margin-left:30px;display:flex;gap:8px">
685
+ <button id="tgBackStep1" style="padding:8px 20px;font-size:13px;background:none;color:var(--text-dim);border:1px solid var(--border);border-radius:6px;cursor:pointer">← Back</button>
686
+ <button id="tgNextStep3" style="padding:8px 20px;font-size:13px">Next →</button>
687
+ </div>
665
688
  </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>
689
+
690
+ <!-- Step 3: Test & Save -->
691
+ <div id="tgStep3" style="display:none">
692
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
693
+ <span style="background:var(--accent);color:#fff;border-radius:50%;width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0">3</span>
694
+ <span style="font-size:13px;font-weight:600;color:var(--text)">Test &amp; Enable</span>
695
+ </div>
696
+ <div style="margin-left:30px;margin-bottom:12px;padding:10px;background:var(--input-bg);border-radius:6px;font-size:12px;color:var(--text-dim)">
697
+ <div>Bot Token: <span id="tgSummaryToken" style="color:var(--text);font-family:monospace"></span></div>
698
+ <div style="margin-top:4px">Chat ID: <span id="tgSummaryChatId" style="color:var(--text);font-family:monospace"></span></div>
699
+ </div>
700
+ <div style="margin-left:30px;margin-bottom:12px;display:flex;align-items:center;gap:8px">
701
+ <input type="checkbox" id="tgEnabled" checked style="width:16px;height:16px;accent-color:var(--accent)">
702
+ <label for="tgEnabled" style="font-size:13px;color:var(--text)">Enable Telegram notifications</label>
703
+ </div>
704
+ <div id="tgStatus" style="display:none;margin:0 0 12px 30px;padding:8px 12px;border-radius:6px;font-size:12px"></div>
705
+ <div style="margin-left:30px;display:flex;gap:8px">
706
+ <button id="tgBackStep2" style="padding:8px 20px;font-size:13px;background:none;color:var(--text-dim);border:1px solid var(--border);border-radius:6px;cursor:pointer">← Back</button>
707
+ <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">Send Test</button>
708
+ <button id="tgSave" style="flex:1;padding:10px">Save</button>
709
+ <button id="settingsCancel2" style="padding:10px 16px;background:none;color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:13px">Cancel</button>
710
+ </div>
671
711
  </div>
672
712
  </div>
673
713
  </div>
@@ -1217,7 +1257,17 @@
1217
1257
  }
1218
1258
  $('#tgChatId').value = tg.chatId || '';
1219
1259
  $('#tgEnabled').checked = !!tg.enabled;
1220
- } catch {}
1260
+ // Jump to appropriate step
1261
+ if (tg.chatId && tg.botToken) {
1262
+ $('#tgSummaryToken').textContent = (tg.botToken || '').slice(0, 8) + '...';
1263
+ $('#tgSummaryChatId').textContent = tg.chatId;
1264
+ showTgStep(3);
1265
+ } else if (tg.botToken) {
1266
+ showTgStep(2);
1267
+ } else {
1268
+ showTgStep(1);
1269
+ }
1270
+ } catch { showTgStep(1); }
1221
1271
  $('#tgStatus').style.display = 'none';
1222
1272
  $('#settingsOverlay').classList.remove('hidden');
1223
1273
  });
@@ -1268,7 +1318,13 @@
1268
1318
  });
1269
1319
  }
1270
1320
 
1271
- // Telegram
1321
+ // Telegram wizard
1322
+ function showTgStep(step) {
1323
+ $('#tgStep1').style.display = step === 1 ? 'block' : 'none';
1324
+ $('#tgStep2').style.display = step === 2 ? 'block' : 'none';
1325
+ $('#tgStep3').style.display = step === 3 ? 'block' : 'none';
1326
+ }
1327
+
1272
1328
  function showTgStatus(msg, ok) {
1273
1329
  const el = $('#tgStatus');
1274
1330
  el.textContent = msg;
@@ -1277,11 +1333,70 @@
1277
1333
  el.style.color = ok ? 'var(--green)' : 'var(--red)';
1278
1334
  }
1279
1335
 
1336
+ // Step navigation
1337
+ $('#tgNextStep2').addEventListener('click', () => {
1338
+ const token = $('#tgBotToken').value.trim();
1339
+ if (!token) { $('#tgBotToken').style.borderColor = 'var(--red)'; return; }
1340
+ $('#tgBotToken').style.borderColor = 'var(--border)';
1341
+ showTgStep(2);
1342
+ });
1343
+ $('#tgBackStep1').addEventListener('click', () => showTgStep(1));
1344
+ $('#tgNextStep3').addEventListener('click', () => {
1345
+ const chatId = $('#tgChatId').value.trim();
1346
+ if (!chatId) { $('#tgChatId').style.borderColor = 'var(--red)'; return; }
1347
+ $('#tgChatId').style.borderColor = 'var(--border)';
1348
+ const token = $('#tgBotToken').value.trim();
1349
+ $('#tgSummaryToken').textContent = token.slice(0, 8) + '...';
1350
+ $('#tgSummaryChatId').textContent = chatId;
1351
+ $('#tgStatus').style.display = 'none';
1352
+ showTgStep(3);
1353
+ });
1354
+ $('#tgBackStep2').addEventListener('click', () => showTgStep(2));
1355
+
1356
+ // Detect Chat ID
1357
+ $('#tgDetect').addEventListener('click', async () => {
1358
+ const botToken = $('#tgBotToken').value.trim();
1359
+ if (!botToken || botToken.includes('...')) { $('#tgDetectStatus').textContent = 'Enter a valid Bot Token first.'; return; }
1360
+ $('#tgDetectStatus').textContent = 'Detecting...';
1361
+ $('#tgChatList').innerHTML = '';
1362
+ try {
1363
+ const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1364
+ const res = await fetch(`/api/telegram/detect${tokenQuery}`, {
1365
+ method: 'POST',
1366
+ headers: { 'Content-Type': 'application/json' },
1367
+ body: JSON.stringify({ botToken }),
1368
+ });
1369
+ const data = await res.json();
1370
+ if (data.error) { $('#tgDetectStatus').textContent = data.error; return; }
1371
+ if (!data.ok || !data.chats.length) {
1372
+ $('#tgDetectStatus').textContent = 'No messages found. Send a message to your bot first!';
1373
+ return;
1374
+ }
1375
+ $('#tgDetectStatus').textContent = 'Found ' + data.chats.length + ' chat(s):';
1376
+ $('#tgChatList').innerHTML = data.chats.map(c =>
1377
+ `<button class="tg-chat-pick" data-id="${esc(c.id)}" style="display:block;width:100%;text-align:left;padding:8px 12px;margin-bottom:4px;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;color:var(--text);cursor:pointer;font-size:12px;transition:border-color 0.15s">
1378
+ <strong>${esc(c.name)}</strong> <span style="color:var(--text-dim)">(${esc(c.type)}, ID: ${esc(c.id)})</span>
1379
+ </button>`
1380
+ ).join('');
1381
+ $('#tgChatList').querySelectorAll('.tg-chat-pick').forEach(btn => {
1382
+ btn.addEventListener('click', () => {
1383
+ $('#tgChatId').value = btn.dataset.id;
1384
+ $('#tgChatList').querySelectorAll('.tg-chat-pick').forEach(b => b.style.borderColor = 'var(--border)');
1385
+ btn.style.borderColor = 'var(--accent)';
1386
+ });
1387
+ btn.addEventListener('mouseenter', () => { btn.style.borderColor = 'var(--accent)'; });
1388
+ btn.addEventListener('mouseleave', () => {
1389
+ if ($('#tgChatId').value !== btn.dataset.id) btn.style.borderColor = 'var(--border)';
1390
+ });
1391
+ });
1392
+ } catch { $('#tgDetectStatus').textContent = 'Connection error.'; }
1393
+ });
1394
+
1395
+ // Test
1280
1396
  $('#tgTest').addEventListener('click', async () => {
1281
1397
  const botToken = $('#tgBotToken').value.trim();
1282
1398
  const chatId = $('#tgChatId').value.trim();
1283
1399
  if (!botToken || !chatId) { showTgStatus('Bot Token and Chat ID are required.', false); return; }
1284
- if (botToken.includes('...')) { showTgStatus('Please enter full Bot Token (not masked).', false); return; }
1285
1400
  try {
1286
1401
  const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1287
1402
  const res = await fetch(`/api/telegram/test${tokenQuery}`, {
@@ -1295,11 +1410,12 @@
1295
1410
  } catch (e) { showTgStatus('Connection error.', false); }
1296
1411
  });
1297
1412
 
1413
+ // Save
1298
1414
  $('#tgSave').addEventListener('click', async () => {
1299
1415
  const botToken = $('#tgBotToken').value.trim();
1300
1416
  const chatId = $('#tgChatId').value.trim();
1301
1417
  const enabled = $('#tgEnabled').checked;
1302
- if (enabled && botToken.includes('...')) { showTgStatus('Please enter full Bot Token to enable.', false); return; }
1418
+ if (!botToken || !chatId) { showTgStatus('Bot Token and Chat ID are required.', false); return; }
1303
1419
  try {
1304
1420
  const tokenQuery = state.token ? `?token=${encodeURIComponent(state.token)}` : '';
1305
1421
  await fetch(`/api/telegram${tokenQuery}`, {
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/hub/server.ts
4
4
  import http from "http";
5
- import fs2 from "fs";
6
- import path3 from "path";
5
+ import fs3 from "fs";
6
+ import path4 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 as randomUUID2 } from "crypto";
29
+ import { randomUUID as randomUUID3 } from "crypto";
30
30
 
31
31
  // src/shared/constants.ts
32
32
  import path from "path";
@@ -187,6 +187,9 @@ var Notifier = class {
187
187
  };
188
188
 
189
189
  // src/hub/telegram.ts
190
+ import fs from "fs";
191
+ import path2 from "path";
192
+ import { randomUUID } from "crypto";
190
193
  var TELEGRAM_API = "https://api.telegram.org/bot";
191
194
  var TelegramBot = class {
192
195
  config;
@@ -195,13 +198,15 @@ var TelegramBot = class {
195
198
  pollTimer = null;
196
199
  // message_id -> sessionId mapping for reply-based routing
197
200
  messageSessionMap = /* @__PURE__ */ new Map();
198
- // Callback: when a message arrives from Telegram for a session
201
+ // Callback: when a text message arrives from Telegram for a session
199
202
  onMessageToSession;
203
+ // Callback: when an image arrives from Telegram for a session
204
+ onImageToSession;
200
205
  // Callback: get current sessions list
201
206
  getSessions;
202
- // Callback: when user needs to select a session (sends inline keyboard)
207
+ // Pending messages for session selection
203
208
  pendingMessages = /* @__PURE__ */ new Map();
204
- // chatId -> pending message text
209
+ // chatId -> pending
205
210
  constructor(config) {
206
211
  this.config = config;
207
212
  }
@@ -297,31 +302,42 @@ ${message}`;
297
302
  if (!this.polling) return;
298
303
  this.pollTimer = setTimeout(() => this.poll(), delay);
299
304
  }
300
- handleIncomingMessage(msg) {
301
- if (!msg.text) return;
305
+ async handleIncomingMessage(msg) {
302
306
  if (String(msg.chat.id) !== String(this.config.chatId)) return;
303
- const text = msg.text.trim();
307
+ const hasPhoto = msg.photo && msg.photo.length > 0;
308
+ const text = (msg.text || msg.caption || "").trim();
309
+ if (!text && !hasPhoto) return;
304
310
  if (msg.reply_to_message) {
305
311
  const sessionId = this.messageSessionMap.get(msg.reply_to_message.message_id);
306
312
  if (sessionId) {
307
- this.deliverToSession(sessionId, text);
313
+ if (hasPhoto) {
314
+ await this.deliverPhotoToSession(sessionId, msg.photo, text);
315
+ } else {
316
+ this.deliverToSession(sessionId, text);
317
+ }
308
318
  return;
309
319
  }
310
320
  }
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.");
321
+ if (text) {
322
+ const selectMatch = text.match(/^\/s_(\d+)$/);
323
+ if (selectMatch) {
324
+ const pending = this.pendingMessages.get(msg.chat.id);
325
+ if (pending) {
326
+ this.pendingMessages.delete(msg.chat.id);
327
+ const sessions2 = this.getSessions?.() ?? [];
328
+ const idx = parseInt(selectMatch[1], 10) - 1;
329
+ if (idx >= 0 && idx < sessions2.length) {
330
+ if (pending.photoFileId) {
331
+ await this.deliverPhotoToSessionByFileId(sessions2[idx].id, pending.photoFileId, pending.caption);
332
+ } else if (pending.text) {
333
+ this.deliverToSession(sessions2[idx].id, pending.text);
334
+ }
335
+ this.sendMessage(`Sent to [${this.getLabel(sessions2[idx])}]`);
336
+ } else {
337
+ this.sendMessage("Invalid session number.");
338
+ }
339
+ return;
323
340
  }
324
- return;
325
341
  }
326
342
  }
327
343
  const sessions = this.getSessions?.() ?? [];
@@ -330,10 +346,19 @@ ${message}`;
330
346
  return;
331
347
  }
332
348
  if (sessions.length === 1) {
333
- this.deliverToSession(sessions[0].id, text);
349
+ if (hasPhoto) {
350
+ await this.deliverPhotoToSession(sessions[0].id, msg.photo, text);
351
+ } else {
352
+ this.deliverToSession(sessions[0].id, text);
353
+ }
334
354
  return;
335
355
  }
336
- this.pendingMessages.set(msg.chat.id, text);
356
+ if (hasPhoto) {
357
+ const largest = msg.photo[msg.photo.length - 1];
358
+ this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text });
359
+ } else {
360
+ this.pendingMessages.set(msg.chat.id, { text });
361
+ }
337
362
  const sessionList = sessions.map((s, i) => `/s_${i + 1} - ${this.getLabel(s)}`).join("\n");
338
363
  this.sendMessage(`Multiple sessions active. Reply with a command to select:
339
364
 
@@ -344,6 +369,46 @@ ${sessionList}`);
344
369
  this.onMessageToSession(sessionId, content);
345
370
  }
346
371
  }
372
+ async deliverPhotoToSession(sessionId, photos, caption) {
373
+ const largest = photos[photos.length - 1];
374
+ await this.deliverPhotoToSessionByFileId(sessionId, largest.file_id, caption);
375
+ }
376
+ async deliverPhotoToSessionByFileId(sessionId, fileId, caption) {
377
+ try {
378
+ const fileRes = await fetch(`${this.apiUrl}/getFile?file_id=${fileId}`);
379
+ if (!fileRes.ok) {
380
+ logger.warn("Failed to get Telegram file info");
381
+ return;
382
+ }
383
+ const fileData = await fileRes.json();
384
+ if (!fileData.ok) return;
385
+ const downloadUrl = `https://api.telegram.org/file/bot${this.config.botToken}/${fileData.result.file_path}`;
386
+ const imgRes = await fetch(downloadUrl);
387
+ if (!imgRes.ok) {
388
+ logger.warn("Failed to download Telegram photo");
389
+ return;
390
+ }
391
+ const buffer = Buffer.from(await imgRes.arrayBuffer());
392
+ const ext = fileData.result.file_path.split(".").pop() || "jpg";
393
+ const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
394
+ fs.mkdirSync(UPLOADS_DIR, { recursive: true });
395
+ const filename = `${randomUUID()}.${ext}`;
396
+ const filePath = path2.join(UPLOADS_DIR, filename);
397
+ fs.writeFileSync(filePath, buffer);
398
+ logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
399
+ if (this.onImageToSession) {
400
+ this.onImageToSession(sessionId, filePath, mimeType, caption);
401
+ }
402
+ setTimeout(() => {
403
+ try {
404
+ fs.unlinkSync(filePath);
405
+ } catch {
406
+ }
407
+ }, 5 * 60 * 1e3);
408
+ } catch (err) {
409
+ logger.warn(`Telegram photo download failed: ${err.message}`);
410
+ }
411
+ }
347
412
  getLabel(session) {
348
413
  return session.cwd?.replace(/^.*[/\\]/, "") || session.name;
349
414
  }
@@ -357,9 +422,9 @@ ${sessionList}`);
357
422
  };
358
423
 
359
424
  // src/shared/config.ts
360
- import fs from "fs";
361
- import path2 from "path";
362
- import { randomUUID } from "crypto";
425
+ import fs2 from "fs";
426
+ import path3 from "path";
427
+ import { randomUUID as randomUUID2 } from "crypto";
363
428
  var DEFAULT_CONFIG = {
364
429
  hub: {
365
430
  host: DEFAULT_HUB_HOST,
@@ -372,18 +437,18 @@ var DEFAULT_CONFIG = {
372
437
  webhooks: []
373
438
  };
374
439
  function ensureConfigDir() {
375
- if (!fs.existsSync(CONFIG_DIR)) {
376
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
440
+ if (!fs2.existsSync(CONFIG_DIR)) {
441
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
377
442
  }
378
443
  }
379
444
  function loadConfig() {
380
445
  ensureConfigDir();
381
446
  let config;
382
- if (!fs.existsSync(CONFIG_FILE)) {
447
+ if (!fs2.existsSync(CONFIG_FILE)) {
383
448
  config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
384
449
  } else {
385
450
  try {
386
- const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
451
+ const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
387
452
  const parsed = JSON.parse(raw);
388
453
  config = { ...DEFAULT_CONFIG, ...parsed, hub: { ...DEFAULT_CONFIG.hub, ...parsed.hub }, ...parsed.telegram ? { telegram: parsed.telegram } : {} };
389
454
  } catch {
@@ -391,18 +456,18 @@ function loadConfig() {
391
456
  }
392
457
  }
393
458
  if (!config.hub.token) {
394
- config.hub.token = randomUUID();
459
+ config.hub.token = randomUUID2();
395
460
  saveConfig(config);
396
461
  }
397
462
  return config;
398
463
  }
399
464
  function saveConfig(config) {
400
465
  ensureConfigDir();
401
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
466
+ fs2.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
402
467
  }
403
468
 
404
469
  // src/hub/server.ts
405
- var __dirname = path3.dirname(fileURLToPath(import.meta.url));
470
+ var __dirname = path4.dirname(fileURLToPath(import.meta.url));
406
471
  var HubServer = class {
407
472
  httpServer;
408
473
  wssChannel;
@@ -551,29 +616,31 @@ var HubServer = class {
551
616
  this.handleTelegramSave(req, res);
552
617
  } else if (url.pathname === "/api/telegram/test" && req.method === "POST") {
553
618
  this.handleTelegramTest(req, res);
619
+ } else if (url.pathname === "/api/telegram/detect" && req.method === "POST") {
620
+ this.handleTelegramDetect(req, res);
554
621
  } else {
555
622
  this.jsonResponse(res, 404, { error: "Not found" });
556
623
  }
557
624
  }
558
625
  serveDashboard(res) {
559
626
  const candidates = [
560
- path3.join(__dirname, "..", "dashboard", "index.html"),
627
+ path4.join(__dirname, "..", "dashboard", "index.html"),
561
628
  // from dist/hub/
562
- path3.join(__dirname, "dashboard", "index.html"),
629
+ path4.join(__dirname, "dashboard", "index.html"),
563
630
  // from dist/ (bundled index.js)
564
- path3.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
631
+ path4.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
565
632
  // from dist/hub/ -> src/
566
- path3.join(__dirname, "..", "src", "dashboard", "index.html"),
633
+ path4.join(__dirname, "..", "src", "dashboard", "index.html"),
567
634
  // from dist/ -> src/
568
- path3.join(process.cwd(), "dist", "dashboard", "index.html"),
635
+ path4.join(process.cwd(), "dist", "dashboard", "index.html"),
569
636
  // from cwd
570
- path3.join(process.cwd(), "src", "dashboard", "index.html")
637
+ path4.join(process.cwd(), "src", "dashboard", "index.html")
571
638
  // from cwd/src
572
639
  ];
573
640
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
574
641
  for (const candidate of candidates) {
575
- if (fs2.existsSync(candidate)) {
576
- const html = fs2.readFileSync(candidate, "utf-8");
642
+ if (fs3.existsSync(candidate)) {
643
+ const html = fs3.readFileSync(candidate, "utf-8");
577
644
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
578
645
  res.end(html);
579
646
  return;
@@ -751,11 +818,11 @@ var HubServer = class {
751
818
  logger.warn("Image upload rejected: exceeds 10MB");
752
819
  return;
753
820
  }
754
- fs2.mkdirSync(UPLOADS_DIR, { recursive: true });
821
+ fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
755
822
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
756
- const filename = `${randomUUID2()}.${ext}`;
757
- const filePath = path3.join(UPLOADS_DIR, filename);
758
- fs2.writeFileSync(filePath, buffer);
823
+ const filename = `${randomUUID3()}.${ext}`;
824
+ const filePath = path4.join(UPLOADS_DIR, filename);
825
+ fs3.writeFileSync(filePath, buffer);
759
826
  const forwardMsg = {
760
827
  type: "image_to_session",
761
828
  sessionId,
@@ -768,7 +835,7 @@ var HubServer = class {
768
835
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
769
836
  setTimeout(() => {
770
837
  try {
771
- fs2.unlinkSync(filePath);
838
+ fs3.unlinkSync(filePath);
772
839
  } catch {
773
840
  }
774
841
  }, 5 * 60 * 1e3);
@@ -801,6 +868,14 @@ var HubServer = class {
801
868
  logger.info(`Telegram message forwarded to session: ${sessionId}`);
802
869
  }
803
870
  };
871
+ this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
872
+ const channelWs = this.channelSockets.get(sessionId);
873
+ if (channelWs?.readyState === WebSocket.OPEN) {
874
+ const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption };
875
+ channelWs.send(JSON.stringify(msg));
876
+ logger.info(`Telegram photo forwarded to session: ${sessionId}`);
877
+ }
878
+ };
804
879
  this.notifier.configure({ telegramBot: this.telegramBot });
805
880
  this.telegramBot.startPolling();
806
881
  logger.info("Telegram bot initialized");
@@ -859,13 +934,57 @@ var HubServer = class {
859
934
  this.jsonResponse(res, 500, { error: err.message });
860
935
  }
861
936
  }
937
+ async handleTelegramDetect(req, res) {
938
+ const body = await this.readBody(req);
939
+ if (!body) {
940
+ this.jsonResponse(res, 400, { error: "Invalid JSON" });
941
+ return;
942
+ }
943
+ const { botToken } = body;
944
+ if (!botToken) {
945
+ this.jsonResponse(res, 400, { error: "botToken required" });
946
+ return;
947
+ }
948
+ try {
949
+ const detectRes = await fetch(`https://api.telegram.org/bot${botToken}/getUpdates?timeout=0&limit=10`, {
950
+ signal: AbortSignal.timeout(1e4)
951
+ });
952
+ if (!detectRes.ok) {
953
+ const err = await detectRes.json();
954
+ this.jsonResponse(res, 400, { error: err.description || "Invalid bot token" });
955
+ return;
956
+ }
957
+ const data = await detectRes.json();
958
+ if (!data.ok || !data.result.length) {
959
+ this.jsonResponse(res, 200, { ok: false, chats: [] });
960
+ return;
961
+ }
962
+ const chatMap = /* @__PURE__ */ new Map();
963
+ for (const update of data.result) {
964
+ if (update.message?.chat) {
965
+ const chat = update.message.chat;
966
+ const id = String(chat.id);
967
+ if (!chatMap.has(id)) {
968
+ chatMap.set(id, {
969
+ id,
970
+ name: chat.title || chat.first_name || id,
971
+ type: chat.type
972
+ });
973
+ }
974
+ }
975
+ }
976
+ this.jsonResponse(res, 200, { ok: true, chats: [...chatMap.values()] });
977
+ } catch (err) {
978
+ this.jsonResponse(res, 500, { error: err.message });
979
+ }
980
+ }
862
981
  cleanupUploads() {
863
982
  try {
864
- if (!fs2.existsSync(UPLOADS_DIR)) return;
865
- const files = fs2.readdirSync(UPLOADS_DIR);
983
+ if (!fs3.existsSync(UPLOADS_DIR)) return;
984
+ const files = fs3.readdirSync(UPLOADS_DIR);
866
985
  for (const file of files) {
867
986
  try {
868
- fs2.unlinkSync(path3.join(UPLOADS_DIR, file));
987
+ fs3.unlinkSync(path4.join(UPLOADS_DIR, file));
869
988
  } catch {
870
989
  }
871
990
  }