@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.
- package/dist/cli.js +189 -69
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +136 -20
- package/dist/hub/server.js +170 -51
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +175 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +136 -20
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/hub/server.ts
|
|
2
2
|
import http from "http";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import fs3 from "fs";
|
|
4
|
+
import path4 from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { WebSocketServer, WebSocket } from "ws";
|
|
7
7
|
|
|
@@ -24,7 +24,7 @@ var logger = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
// src/hub/server.ts
|
|
27
|
-
import { randomUUID as
|
|
27
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
28
28
|
|
|
29
29
|
// src/shared/constants.ts
|
|
30
30
|
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
|
-
//
|
|
207
|
+
// Pending messages for session selection
|
|
203
208
|
pendingMessages = /* @__PURE__ */ new Map();
|
|
204
|
-
// chatId -> pending
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
361
|
-
import
|
|
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 (!
|
|
376
|
-
|
|
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 (!
|
|
447
|
+
if (!fs2.existsSync(CONFIG_FILE)) {
|
|
383
448
|
config = { ...DEFAULT_CONFIG, hub: { ...DEFAULT_CONFIG.hub } };
|
|
384
449
|
} else {
|
|
385
450
|
try {
|
|
386
|
-
const raw =
|
|
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,22 +456,22 @@ function loadConfig() {
|
|
|
391
456
|
}
|
|
392
457
|
}
|
|
393
458
|
if (!config.hub.token) {
|
|
394
|
-
config.hub.token =
|
|
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
|
-
|
|
466
|
+
fs2.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
402
467
|
}
|
|
403
468
|
function setupMcpConfig(targetDir) {
|
|
404
469
|
const dir = targetDir ?? process.cwd();
|
|
405
|
-
const mcpPath =
|
|
470
|
+
const mcpPath = path3.join(dir, ".mcp.json");
|
|
406
471
|
let mcpConfig = {};
|
|
407
|
-
if (
|
|
472
|
+
if (fs2.existsSync(mcpPath)) {
|
|
408
473
|
try {
|
|
409
|
-
mcpConfig = JSON.parse(
|
|
474
|
+
mcpConfig = JSON.parse(fs2.readFileSync(mcpPath, "utf-8"));
|
|
410
475
|
} catch {
|
|
411
476
|
mcpConfig = {};
|
|
412
477
|
}
|
|
@@ -418,15 +483,15 @@ function setupMcpConfig(targetDir) {
|
|
|
418
483
|
command: "npx",
|
|
419
484
|
args: ["-y", "@delt/claude-alarm", "serve"],
|
|
420
485
|
env: {
|
|
421
|
-
CLAUDE_ALARM_SESSION_NAME:
|
|
486
|
+
CLAUDE_ALARM_SESSION_NAME: path3.basename(dir)
|
|
422
487
|
}
|
|
423
488
|
};
|
|
424
|
-
|
|
489
|
+
fs2.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
|
425
490
|
return mcpPath;
|
|
426
491
|
}
|
|
427
492
|
|
|
428
493
|
// src/hub/server.ts
|
|
429
|
-
var __dirname =
|
|
494
|
+
var __dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
430
495
|
var HubServer = class {
|
|
431
496
|
httpServer;
|
|
432
497
|
wssChannel;
|
|
@@ -575,29 +640,31 @@ var HubServer = class {
|
|
|
575
640
|
this.handleTelegramSave(req, res);
|
|
576
641
|
} else if (url.pathname === "/api/telegram/test" && req.method === "POST") {
|
|
577
642
|
this.handleTelegramTest(req, res);
|
|
643
|
+
} else if (url.pathname === "/api/telegram/detect" && req.method === "POST") {
|
|
644
|
+
this.handleTelegramDetect(req, res);
|
|
578
645
|
} else {
|
|
579
646
|
this.jsonResponse(res, 404, { error: "Not found" });
|
|
580
647
|
}
|
|
581
648
|
}
|
|
582
649
|
serveDashboard(res) {
|
|
583
650
|
const candidates = [
|
|
584
|
-
|
|
651
|
+
path4.join(__dirname, "..", "dashboard", "index.html"),
|
|
585
652
|
// from dist/hub/
|
|
586
|
-
|
|
653
|
+
path4.join(__dirname, "dashboard", "index.html"),
|
|
587
654
|
// from dist/ (bundled index.js)
|
|
588
|
-
|
|
655
|
+
path4.join(__dirname, "..", "..", "src", "dashboard", "index.html"),
|
|
589
656
|
// from dist/hub/ -> src/
|
|
590
|
-
|
|
657
|
+
path4.join(__dirname, "..", "src", "dashboard", "index.html"),
|
|
591
658
|
// from dist/ -> src/
|
|
592
|
-
|
|
659
|
+
path4.join(process.cwd(), "dist", "dashboard", "index.html"),
|
|
593
660
|
// from cwd
|
|
594
|
-
|
|
661
|
+
path4.join(process.cwd(), "src", "dashboard", "index.html")
|
|
595
662
|
// from cwd/src
|
|
596
663
|
];
|
|
597
664
|
logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
|
|
598
665
|
for (const candidate of candidates) {
|
|
599
|
-
if (
|
|
600
|
-
const html =
|
|
666
|
+
if (fs3.existsSync(candidate)) {
|
|
667
|
+
const html = fs3.readFileSync(candidate, "utf-8");
|
|
601
668
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
602
669
|
res.end(html);
|
|
603
670
|
return;
|
|
@@ -775,11 +842,11 @@ var HubServer = class {
|
|
|
775
842
|
logger.warn("Image upload rejected: exceeds 10MB");
|
|
776
843
|
return;
|
|
777
844
|
}
|
|
778
|
-
|
|
845
|
+
fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
779
846
|
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
780
|
-
const filename = `${
|
|
781
|
-
const filePath =
|
|
782
|
-
|
|
847
|
+
const filename = `${randomUUID3()}.${ext}`;
|
|
848
|
+
const filePath = path4.join(UPLOADS_DIR, filename);
|
|
849
|
+
fs3.writeFileSync(filePath, buffer);
|
|
783
850
|
const forwardMsg = {
|
|
784
851
|
type: "image_to_session",
|
|
785
852
|
sessionId,
|
|
@@ -792,7 +859,7 @@ var HubServer = class {
|
|
|
792
859
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
793
860
|
setTimeout(() => {
|
|
794
861
|
try {
|
|
795
|
-
|
|
862
|
+
fs3.unlinkSync(filePath);
|
|
796
863
|
} catch {
|
|
797
864
|
}
|
|
798
865
|
}, 5 * 60 * 1e3);
|
|
@@ -825,6 +892,14 @@ var HubServer = class {
|
|
|
825
892
|
logger.info(`Telegram message forwarded to session: ${sessionId}`);
|
|
826
893
|
}
|
|
827
894
|
};
|
|
895
|
+
this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
|
|
896
|
+
const channelWs = this.channelSockets.get(sessionId);
|
|
897
|
+
if (channelWs?.readyState === WebSocket.OPEN) {
|
|
898
|
+
const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption };
|
|
899
|
+
channelWs.send(JSON.stringify(msg));
|
|
900
|
+
logger.info(`Telegram photo forwarded to session: ${sessionId}`);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
828
903
|
this.notifier.configure({ telegramBot: this.telegramBot });
|
|
829
904
|
this.telegramBot.startPolling();
|
|
830
905
|
logger.info("Telegram bot initialized");
|
|
@@ -883,13 +958,57 @@ var HubServer = class {
|
|
|
883
958
|
this.jsonResponse(res, 500, { error: err.message });
|
|
884
959
|
}
|
|
885
960
|
}
|
|
961
|
+
async handleTelegramDetect(req, res) {
|
|
962
|
+
const body = await this.readBody(req);
|
|
963
|
+
if (!body) {
|
|
964
|
+
this.jsonResponse(res, 400, { error: "Invalid JSON" });
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const { botToken } = body;
|
|
968
|
+
if (!botToken) {
|
|
969
|
+
this.jsonResponse(res, 400, { error: "botToken required" });
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
const detectRes = await fetch(`https://api.telegram.org/bot${botToken}/getUpdates?timeout=0&limit=10`, {
|
|
974
|
+
signal: AbortSignal.timeout(1e4)
|
|
975
|
+
});
|
|
976
|
+
if (!detectRes.ok) {
|
|
977
|
+
const err = await detectRes.json();
|
|
978
|
+
this.jsonResponse(res, 400, { error: err.description || "Invalid bot token" });
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const data = await detectRes.json();
|
|
982
|
+
if (!data.ok || !data.result.length) {
|
|
983
|
+
this.jsonResponse(res, 200, { ok: false, chats: [] });
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
const chatMap = /* @__PURE__ */ new Map();
|
|
987
|
+
for (const update of data.result) {
|
|
988
|
+
if (update.message?.chat) {
|
|
989
|
+
const chat = update.message.chat;
|
|
990
|
+
const id = String(chat.id);
|
|
991
|
+
if (!chatMap.has(id)) {
|
|
992
|
+
chatMap.set(id, {
|
|
993
|
+
id,
|
|
994
|
+
name: chat.title || chat.first_name || id,
|
|
995
|
+
type: chat.type
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
this.jsonResponse(res, 200, { ok: true, chats: [...chatMap.values()] });
|
|
1001
|
+
} catch (err) {
|
|
1002
|
+
this.jsonResponse(res, 500, { error: err.message });
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
886
1005
|
cleanupUploads() {
|
|
887
1006
|
try {
|
|
888
|
-
if (!
|
|
889
|
-
const files =
|
|
1007
|
+
if (!fs3.existsSync(UPLOADS_DIR)) return;
|
|
1008
|
+
const files = fs3.readdirSync(UPLOADS_DIR);
|
|
890
1009
|
for (const file of files) {
|
|
891
1010
|
try {
|
|
892
|
-
|
|
1011
|
+
fs3.unlinkSync(path4.join(UPLOADS_DIR, file));
|
|
893
1012
|
} catch {
|
|
894
1013
|
}
|
|
895
1014
|
}
|