@delt/claude-alarm 0.8.0 → 0.8.2

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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/hub/server.ts
2
2
  import http from "http";
3
- import fs3 from "fs";
3
+ import fs4 from "fs";
4
4
  import path4 from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { WebSocketServer, WebSocket } from "ws";
@@ -210,7 +210,7 @@ import fs from "fs";
210
210
  import path2 from "path";
211
211
  import { randomUUID } from "crypto";
212
212
  var TELEGRAM_API = "https://api.telegram.org/bot";
213
- var TelegramBot = class {
213
+ var TelegramBot = class _TelegramBot {
214
214
  config;
215
215
  offset = 0;
216
216
  polling = false;
@@ -442,33 +442,45 @@ ${this.mdToHtml(message)}`;
442
442
  }
443
443
  /** Send a permission request with inline buttons */
444
444
  async sendPermissionRequest(sessionId, sessionLabel, requestId, toolName, description, inputPreview) {
445
+ const isNotify = toolName.endsWith("__notify") || toolName === "notify";
445
446
  let preview = inputPreview;
447
+ let kind = "code";
448
+ let lang = "";
446
449
  let truncated = false;
447
450
  try {
448
451
  const p = JSON.parse(inputPreview);
449
- if (p.command) preview = `$ ${p.command}`;
450
- else if (p.title && p.message) preview = p.message;
451
- else if (p.file_path) {
452
+ if (p.command) {
453
+ preview = `$ ${p.command}`;
454
+ lang = "bash";
455
+ } else if (p.title && p.message) {
456
+ preview = p.message;
457
+ kind = "prose";
458
+ } else if (p.file_path) {
452
459
  preview = p.file_path;
460
+ lang = this.langOf(p.file_path);
453
461
  if (p.content) {
454
462
  preview += "\n" + p.content.slice(0, 3e3);
455
463
  if (p.content.length > 500) truncated = true;
456
464
  }
457
465
  } else if (p.content && typeof p.content === "string") {
458
466
  preview = p.content.slice(0, 3e3);
467
+ if (isNotify) kind = "prose";
459
468
  if (p.content.length > 500) truncated = true;
460
469
  }
461
470
  } catch {
462
471
  truncated = true;
463
472
  const cmdMatch = inputPreview.match(/"command"\s*:\s*"((?:[^"\\]|\\.)*)"/);
464
473
  const contentMatch = inputPreview.match(/"content"\s*:\s*"((?:[^"\\]|\\.)*)"/);
465
- if (cmdMatch) preview = `$ ${cmdMatch[1]}`;
466
- else if (contentMatch) preview = contentMatch[1].slice(0, 3e3);
474
+ if (cmdMatch) {
475
+ preview = `$ ${cmdMatch[1]}`;
476
+ lang = "bash";
477
+ } else if (contentMatch) preview = contentMatch[1].slice(0, 3e3);
467
478
  }
468
479
  const truncNote = truncated ? "\n\n<i>...truncated</i>" : "";
469
480
  const previewSlice = preview.slice(0, 3e3);
470
481
  const isShort = !previewSlice.includes("\n") && previewSlice.length < 100;
471
- const previewHtml = isShort ? `<code>${this.escHtml(previewSlice)}</code>` : this.escHtml(previewSlice);
482
+ const langAttr = lang ? ` class="language-${lang}"` : "";
483
+ const previewHtml = kind === "prose" ? this.mdToHtml(previewSlice) : isShort ? `<code>${this.escHtml(previewSlice)}</code>` : `<pre><code${langAttr}>${this.escHtml(previewSlice)}</code></pre>`;
472
484
  let displayTool = toolName;
473
485
  if ((toolName.endsWith("__notify") || toolName === "notify") && inputPreview) {
474
486
  try {
@@ -568,6 +580,40 @@ ${label}`);
568
580
  escHtml(s) {
569
581
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
570
582
  }
583
+ static LANG_BY_EXT = {
584
+ js: "javascript",
585
+ mjs: "javascript",
586
+ cjs: "javascript",
587
+ jsx: "javascript",
588
+ ts: "typescript",
589
+ tsx: "typescript",
590
+ py: "python",
591
+ rb: "ruby",
592
+ go: "go",
593
+ java: "java",
594
+ kt: "kotlin",
595
+ rs: "rust",
596
+ php: "php",
597
+ cs: "csharp",
598
+ sh: "bash",
599
+ bash: "bash",
600
+ zsh: "bash",
601
+ ps1: "powershell",
602
+ json: "json",
603
+ yml: "yaml",
604
+ yaml: "yaml",
605
+ xml: "xml",
606
+ sql: "sql",
607
+ md: "markdown",
608
+ html: "html",
609
+ vue: "html",
610
+ css: "css",
611
+ scss: "scss"
612
+ };
613
+ langOf(path5) {
614
+ const m = /\.([a-z0-9]+)$/i.exec(path5 || "");
615
+ return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
616
+ }
571
617
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
572
618
  mdToHtml(s) {
573
619
  let text = s.replace(
@@ -677,6 +723,54 @@ function setupMcpConfig(targetDir) {
677
723
  return mcpPath;
678
724
  }
679
725
 
726
+ // src/shared/crash-guard.ts
727
+ import fs3 from "fs";
728
+ var BURST_WINDOW_MS = 6e4;
729
+ var BURST_LIMIT = 10;
730
+ function appendToLog(text) {
731
+ try {
732
+ fs3.mkdirSync(CONFIG_DIR, { recursive: true });
733
+ fs3.appendFileSync(LOG_FILE, text, "utf-8");
734
+ } catch {
735
+ }
736
+ }
737
+ function describe(err) {
738
+ if (err instanceof Error) return err.stack ?? `${err.name}: ${err.message}`;
739
+ try {
740
+ return JSON.stringify(err);
741
+ } catch {
742
+ return String(err);
743
+ }
744
+ }
745
+ function installCrashGuard(context) {
746
+ let recent = [];
747
+ const handle = (kind, err) => {
748
+ const stack = describe(err);
749
+ appendToLog(`
750
+ [${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} (${context})
751
+ ${stack}
752
+ `);
753
+ logger.error(`${kind} caught by crash guard: ${stack}`);
754
+ const now = Date.now();
755
+ recent = recent.filter((t) => now - t < BURST_WINDOW_MS);
756
+ recent.push(now);
757
+ if (recent.length > BURST_LIMIT) {
758
+ const msg = `${recent.length} unhandled errors within ${BURST_WINDOW_MS / 1e3}s \u2014 exiting`;
759
+ appendToLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
760
+ `);
761
+ logger.error(msg);
762
+ process.exit(1);
763
+ }
764
+ };
765
+ process.on("uncaughtException", (err) => handle("uncaughtException", err));
766
+ process.on("unhandledRejection", (reason) => handle("unhandledRejection", reason));
767
+ }
768
+ function logStartup(context) {
769
+ appendToLog(`
770
+ [${(/* @__PURE__ */ new Date()).toISOString()}] ${context} started (pid ${process.pid})
771
+ `);
772
+ }
773
+
680
774
  // src/hub/server.ts
681
775
  var __dirname = path4.dirname(fileURLToPath(import.meta.url));
682
776
  var HubServer = class {
@@ -720,9 +814,12 @@ var HubServer = class {
720
814
  this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
721
815
  this.wssChannel = new WebSocketServer({ noServer: true });
722
816
  this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
817
+ this.wssChannel.on("error", (err) => logger.warn(`Channel WebSocket server error: ${err.message}`));
723
818
  this.wssDashboard = new WebSocketServer({ noServer: true });
724
819
  this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
820
+ this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
725
821
  this.httpServer.on("upgrade", (req, socket, head) => {
822
+ socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
726
823
  const url = new URL(req.url, `http://${req.headers.host}`);
727
824
  const pathname = url.pathname;
728
825
  if (this.token && !this.isLocalRequest(req)) {
@@ -749,8 +846,11 @@ var HubServer = class {
749
846
  this.cleanupUploads();
750
847
  this.startHeartbeat();
751
848
  return new Promise((resolve, reject) => {
752
- this.httpServer.on("error", reject);
849
+ const onStartupError = (err) => reject(err);
850
+ this.httpServer.once("error", onStartupError);
753
851
  this.httpServer.listen(this.port, this.host, () => {
852
+ this.httpServer.removeListener("error", onStartupError);
853
+ this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
754
854
  const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
755
855
  logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
756
856
  resolve();
@@ -871,8 +971,8 @@ var HubServer = class {
871
971
  ];
872
972
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
873
973
  for (const candidate of candidates) {
874
- if (fs3.existsSync(candidate)) {
875
- const html = fs3.readFileSync(candidate, "utf-8");
974
+ if (fs4.existsSync(candidate)) {
975
+ const html = fs4.readFileSync(candidate, "utf-8");
876
976
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
877
977
  res.end(html);
878
978
  return;
@@ -919,6 +1019,10 @@ var HubServer = class {
919
1019
  handleChannelConnection(ws, req) {
920
1020
  const isLocal = this.isLocalRequest(req);
921
1021
  logger.info(`Channel server connected (local: ${isLocal})`);
1022
+ ws.on("error", (err) => {
1023
+ logger.warn(`Channel WebSocket error: ${err.message}`);
1024
+ ws.terminate();
1025
+ });
922
1026
  ws.on("pong", () => {
923
1027
  for (const [sessionId, sock] of this.channelSockets) {
924
1028
  if (sock === ws) {
@@ -1028,6 +1132,10 @@ var HubServer = class {
1028
1132
  handleDashboardConnection(ws) {
1029
1133
  this.dashboardSockets.add(ws);
1030
1134
  logger.info(`Dashboard connected (total: ${this.dashboardSockets.size})`);
1135
+ ws.on("error", (err) => {
1136
+ logger.warn(`Dashboard WebSocket error: ${err.message}`);
1137
+ ws.terminate();
1138
+ });
1031
1139
  const sessionsMsg = {
1032
1140
  type: "sessions_list",
1033
1141
  sessions: this.sessions.getAll()
@@ -1084,11 +1192,11 @@ var HubServer = class {
1084
1192
  logger.warn("Image upload rejected: exceeds 10MB");
1085
1193
  return;
1086
1194
  }
1087
- fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
1195
+ fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
1088
1196
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
1089
1197
  const filename = `${randomUUID3()}.${ext}`;
1090
1198
  const filePath = path4.join(UPLOADS_DIR, filename);
1091
- fs3.writeFileSync(filePath, buffer);
1199
+ fs4.writeFileSync(filePath, buffer);
1092
1200
  const forwardMsg = {
1093
1201
  type: "image_to_session",
1094
1202
  sessionId,
@@ -1101,7 +1209,7 @@ var HubServer = class {
1101
1209
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
1102
1210
  setTimeout(() => {
1103
1211
  try {
1104
- fs3.unlinkSync(filePath);
1212
+ fs4.unlinkSync(filePath);
1105
1213
  } catch {
1106
1214
  }
1107
1215
  }, 5 * 60 * 1e3);
@@ -1271,11 +1379,11 @@ var HubServer = class {
1271
1379
  }
1272
1380
  cleanupUploads() {
1273
1381
  try {
1274
- if (!fs3.existsSync(UPLOADS_DIR)) return;
1275
- const files = fs3.readdirSync(UPLOADS_DIR);
1382
+ if (!fs4.existsSync(UPLOADS_DIR)) return;
1383
+ const files = fs4.readdirSync(UPLOADS_DIR);
1276
1384
  for (const file of files) {
1277
1385
  try {
1278
- fs3.unlinkSync(path4.join(UPLOADS_DIR, file));
1386
+ fs4.unlinkSync(path4.join(UPLOADS_DIR, file));
1279
1387
  } catch {
1280
1388
  }
1281
1389
  }
@@ -1319,6 +1427,8 @@ var HubServer = class {
1319
1427
  }
1320
1428
  };
1321
1429
  if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.argv[1].endsWith("hub/server.ts"))) {
1430
+ installCrashGuard("hub daemon");
1431
+ logStartup("Hub daemon");
1322
1432
  const config = loadConfig();
1323
1433
  const hub = new HubServer(config);
1324
1434
  hub.start().catch((err) => {