@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/cli.js CHANGED
@@ -134,6 +134,62 @@ var init_logger = __esm({
134
134
  }
135
135
  });
136
136
 
137
+ // src/shared/crash-guard.ts
138
+ import fs2 from "fs";
139
+ function appendToLog(text) {
140
+ try {
141
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
142
+ fs2.appendFileSync(LOG_FILE, text, "utf-8");
143
+ } catch {
144
+ }
145
+ }
146
+ function describe(err) {
147
+ if (err instanceof Error) return err.stack ?? `${err.name}: ${err.message}`;
148
+ try {
149
+ return JSON.stringify(err);
150
+ } catch {
151
+ return String(err);
152
+ }
153
+ }
154
+ function installCrashGuard(context) {
155
+ let recent = [];
156
+ const handle = (kind, err) => {
157
+ const stack = describe(err);
158
+ appendToLog(`
159
+ [${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} (${context})
160
+ ${stack}
161
+ `);
162
+ logger.error(`${kind} caught by crash guard: ${stack}`);
163
+ const now = Date.now();
164
+ recent = recent.filter((t) => now - t < BURST_WINDOW_MS);
165
+ recent.push(now);
166
+ if (recent.length > BURST_LIMIT) {
167
+ const msg = `${recent.length} unhandled errors within ${BURST_WINDOW_MS / 1e3}s \u2014 exiting`;
168
+ appendToLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
169
+ `);
170
+ logger.error(msg);
171
+ process.exit(1);
172
+ }
173
+ };
174
+ process.on("uncaughtException", (err) => handle("uncaughtException", err));
175
+ process.on("unhandledRejection", (reason) => handle("unhandledRejection", reason));
176
+ }
177
+ function logStartup(context) {
178
+ appendToLog(`
179
+ [${(/* @__PURE__ */ new Date()).toISOString()}] ${context} started (pid ${process.pid})
180
+ `);
181
+ }
182
+ var BURST_WINDOW_MS, BURST_LIMIT;
183
+ var init_crash_guard = __esm({
184
+ "src/shared/crash-guard.ts"() {
185
+ "use strict";
186
+ init_constants();
187
+ init_logger();
188
+ BURST_WINDOW_MS = 6e4;
189
+ BURST_LIMIT = 10;
190
+ }
191
+ });
192
+
137
193
  // src/hub/session-manager.ts
138
194
  var SessionManager;
139
195
  var init_session_manager = __esm({
@@ -312,7 +368,7 @@ var init_notifier = __esm({
312
368
  });
313
369
 
314
370
  // src/hub/telegram.ts
315
- import fs2 from "fs";
371
+ import fs3 from "fs";
316
372
  import path3 from "path";
317
373
  import { randomUUID as randomUUID2 } from "crypto";
318
374
  var TELEGRAM_API, TelegramBot;
@@ -322,7 +378,7 @@ var init_telegram = __esm({
322
378
  init_logger();
323
379
  init_constants();
324
380
  TELEGRAM_API = "https://api.telegram.org/bot";
325
- TelegramBot = class {
381
+ TelegramBot = class _TelegramBot {
326
382
  config;
327
383
  offset = 0;
328
384
  polling = false;
@@ -531,17 +587,17 @@ ${this.mdToHtml(message)}`;
531
587
  const buffer = Buffer.from(await imgRes.arrayBuffer());
532
588
  const ext = fileData.result.file_path.split(".").pop() || "jpg";
533
589
  const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
534
- fs2.mkdirSync(UPLOADS_DIR, { recursive: true });
590
+ fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
535
591
  const filename = `${randomUUID2()}.${ext}`;
536
592
  const filePath = path3.join(UPLOADS_DIR, filename);
537
- fs2.writeFileSync(filePath, buffer);
593
+ fs3.writeFileSync(filePath, buffer);
538
594
  logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
539
595
  if (this.onImageToSession) {
540
596
  this.onImageToSession(sessionId2, filePath, mimeType, caption);
541
597
  }
542
598
  setTimeout(() => {
543
599
  try {
544
- fs2.unlinkSync(filePath);
600
+ fs3.unlinkSync(filePath);
545
601
  } catch {
546
602
  }
547
603
  }, 5 * 60 * 1e3);
@@ -554,33 +610,45 @@ ${this.mdToHtml(message)}`;
554
610
  }
555
611
  /** Send a permission request with inline buttons */
556
612
  async sendPermissionRequest(sessionId2, sessionLabel, requestId, toolName, description, inputPreview) {
613
+ const isNotify = toolName.endsWith("__notify") || toolName === "notify";
557
614
  let preview = inputPreview;
615
+ let kind = "code";
616
+ let lang = "";
558
617
  let truncated = false;
559
618
  try {
560
619
  const p = JSON.parse(inputPreview);
561
- if (p.command) preview = `$ ${p.command}`;
562
- else if (p.title && p.message) preview = p.message;
563
- else if (p.file_path) {
620
+ if (p.command) {
621
+ preview = `$ ${p.command}`;
622
+ lang = "bash";
623
+ } else if (p.title && p.message) {
624
+ preview = p.message;
625
+ kind = "prose";
626
+ } else if (p.file_path) {
564
627
  preview = p.file_path;
628
+ lang = this.langOf(p.file_path);
565
629
  if (p.content) {
566
630
  preview += "\n" + p.content.slice(0, 3e3);
567
631
  if (p.content.length > 500) truncated = true;
568
632
  }
569
633
  } else if (p.content && typeof p.content === "string") {
570
634
  preview = p.content.slice(0, 3e3);
635
+ if (isNotify) kind = "prose";
571
636
  if (p.content.length > 500) truncated = true;
572
637
  }
573
638
  } catch {
574
639
  truncated = true;
575
640
  const cmdMatch = inputPreview.match(/"command"\s*:\s*"((?:[^"\\]|\\.)*)"/);
576
641
  const contentMatch = inputPreview.match(/"content"\s*:\s*"((?:[^"\\]|\\.)*)"/);
577
- if (cmdMatch) preview = `$ ${cmdMatch[1]}`;
578
- else if (contentMatch) preview = contentMatch[1].slice(0, 3e3);
642
+ if (cmdMatch) {
643
+ preview = `$ ${cmdMatch[1]}`;
644
+ lang = "bash";
645
+ } else if (contentMatch) preview = contentMatch[1].slice(0, 3e3);
579
646
  }
580
647
  const truncNote = truncated ? "\n\n<i>...truncated</i>" : "";
581
648
  const previewSlice = preview.slice(0, 3e3);
582
649
  const isShort = !previewSlice.includes("\n") && previewSlice.length < 100;
583
- const previewHtml = isShort ? `<code>${this.escHtml(previewSlice)}</code>` : this.escHtml(previewSlice);
650
+ const langAttr = lang ? ` class="language-${lang}"` : "";
651
+ const previewHtml = kind === "prose" ? this.mdToHtml(previewSlice) : isShort ? `<code>${this.escHtml(previewSlice)}</code>` : `<pre><code${langAttr}>${this.escHtml(previewSlice)}</code></pre>`;
584
652
  let displayTool = toolName;
585
653
  if ((toolName.endsWith("__notify") || toolName === "notify") && inputPreview) {
586
654
  try {
@@ -680,6 +748,40 @@ ${label}`);
680
748
  escHtml(s) {
681
749
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
682
750
  }
751
+ static LANG_BY_EXT = {
752
+ js: "javascript",
753
+ mjs: "javascript",
754
+ cjs: "javascript",
755
+ jsx: "javascript",
756
+ ts: "typescript",
757
+ tsx: "typescript",
758
+ py: "python",
759
+ rb: "ruby",
760
+ go: "go",
761
+ java: "java",
762
+ kt: "kotlin",
763
+ rs: "rust",
764
+ php: "php",
765
+ cs: "csharp",
766
+ sh: "bash",
767
+ bash: "bash",
768
+ zsh: "bash",
769
+ ps1: "powershell",
770
+ json: "json",
771
+ yml: "yaml",
772
+ yaml: "yaml",
773
+ xml: "xml",
774
+ sql: "sql",
775
+ md: "markdown",
776
+ html: "html",
777
+ vue: "html",
778
+ css: "css",
779
+ scss: "scss"
780
+ };
781
+ langOf(path7) {
782
+ const m = /\.([a-z0-9]+)$/i.exec(path7 || "");
783
+ return m ? _TelegramBot.LANG_BY_EXT[m[1].toLowerCase()] ?? "" : "";
784
+ }
683
785
  /** Convert markdown to Telegram HTML (escape first, then apply formatting) */
684
786
  mdToHtml(s) {
685
787
  let text = s.replace(
@@ -728,7 +830,7 @@ __export(server_exports, {
728
830
  HubServer: () => HubServer
729
831
  });
730
832
  import http from "http";
731
- import fs3 from "fs";
833
+ import fs4 from "fs";
732
834
  import path4 from "path";
733
835
  import { fileURLToPath } from "url";
734
836
  import { WebSocketServer, WebSocket } from "ws";
@@ -743,6 +845,7 @@ var init_server = __esm({
743
845
  init_notifier();
744
846
  init_telegram();
745
847
  init_config();
848
+ init_crash_guard();
746
849
  __dirname = path4.dirname(fileURLToPath(import.meta.url));
747
850
  HubServer = class {
748
851
  httpServer;
@@ -785,9 +888,12 @@ var init_server = __esm({
785
888
  this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
786
889
  this.wssChannel = new WebSocketServer({ noServer: true });
787
890
  this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
891
+ this.wssChannel.on("error", (err) => logger.warn(`Channel WebSocket server error: ${err.message}`));
788
892
  this.wssDashboard = new WebSocketServer({ noServer: true });
789
893
  this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
894
+ this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
790
895
  this.httpServer.on("upgrade", (req, socket, head) => {
896
+ socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
791
897
  const url = new URL(req.url, `http://${req.headers.host}`);
792
898
  const pathname = url.pathname;
793
899
  if (this.token && !this.isLocalRequest(req)) {
@@ -814,8 +920,11 @@ var init_server = __esm({
814
920
  this.cleanupUploads();
815
921
  this.startHeartbeat();
816
922
  return new Promise((resolve, reject) => {
817
- this.httpServer.on("error", reject);
923
+ const onStartupError = (err) => reject(err);
924
+ this.httpServer.once("error", onStartupError);
818
925
  this.httpServer.listen(this.port, this.host, () => {
926
+ this.httpServer.removeListener("error", onStartupError);
927
+ this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
819
928
  const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
820
929
  logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
821
930
  resolve();
@@ -936,8 +1045,8 @@ var init_server = __esm({
936
1045
  ];
937
1046
  logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
938
1047
  for (const candidate of candidates) {
939
- if (fs3.existsSync(candidate)) {
940
- const html = fs3.readFileSync(candidate, "utf-8");
1048
+ if (fs4.existsSync(candidate)) {
1049
+ const html = fs4.readFileSync(candidate, "utf-8");
941
1050
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
942
1051
  res.end(html);
943
1052
  return;
@@ -984,6 +1093,10 @@ var init_server = __esm({
984
1093
  handleChannelConnection(ws, req) {
985
1094
  const isLocal = this.isLocalRequest(req);
986
1095
  logger.info(`Channel server connected (local: ${isLocal})`);
1096
+ ws.on("error", (err) => {
1097
+ logger.warn(`Channel WebSocket error: ${err.message}`);
1098
+ ws.terminate();
1099
+ });
987
1100
  ws.on("pong", () => {
988
1101
  for (const [sessionId2, sock] of this.channelSockets) {
989
1102
  if (sock === ws) {
@@ -1093,6 +1206,10 @@ var init_server = __esm({
1093
1206
  handleDashboardConnection(ws) {
1094
1207
  this.dashboardSockets.add(ws);
1095
1208
  logger.info(`Dashboard connected (total: ${this.dashboardSockets.size})`);
1209
+ ws.on("error", (err) => {
1210
+ logger.warn(`Dashboard WebSocket error: ${err.message}`);
1211
+ ws.terminate();
1212
+ });
1096
1213
  const sessionsMsg = {
1097
1214
  type: "sessions_list",
1098
1215
  sessions: this.sessions.getAll()
@@ -1149,11 +1266,11 @@ var init_server = __esm({
1149
1266
  logger.warn("Image upload rejected: exceeds 10MB");
1150
1267
  return;
1151
1268
  }
1152
- fs3.mkdirSync(UPLOADS_DIR, { recursive: true });
1269
+ fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
1153
1270
  const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
1154
1271
  const filename = `${randomUUID3()}.${ext}`;
1155
1272
  const filePath = path4.join(UPLOADS_DIR, filename);
1156
- fs3.writeFileSync(filePath, buffer);
1273
+ fs4.writeFileSync(filePath, buffer);
1157
1274
  const forwardMsg = {
1158
1275
  type: "image_to_session",
1159
1276
  sessionId: sessionId2,
@@ -1166,7 +1283,7 @@ var init_server = __esm({
1166
1283
  logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
1167
1284
  setTimeout(() => {
1168
1285
  try {
1169
- fs3.unlinkSync(filePath);
1286
+ fs4.unlinkSync(filePath);
1170
1287
  } catch {
1171
1288
  }
1172
1289
  }, 5 * 60 * 1e3);
@@ -1336,11 +1453,11 @@ var init_server = __esm({
1336
1453
  }
1337
1454
  cleanupUploads() {
1338
1455
  try {
1339
- if (!fs3.existsSync(UPLOADS_DIR)) return;
1340
- const files = fs3.readdirSync(UPLOADS_DIR);
1456
+ if (!fs4.existsSync(UPLOADS_DIR)) return;
1457
+ const files = fs4.readdirSync(UPLOADS_DIR);
1341
1458
  for (const file of files) {
1342
1459
  try {
1343
- fs3.unlinkSync(path4.join(UPLOADS_DIR, file));
1460
+ fs4.unlinkSync(path4.join(UPLOADS_DIR, file));
1344
1461
  } catch {
1345
1462
  }
1346
1463
  }
@@ -1384,6 +1501,8 @@ var init_server = __esm({
1384
1501
  }
1385
1502
  };
1386
1503
  if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.argv[1].endsWith("hub/server.ts"))) {
1504
+ installCrashGuard("hub daemon");
1505
+ logStartup("Hub daemon");
1387
1506
  const config2 = loadConfig();
1388
1507
  const hub = new HubServer(config2);
1389
1508
  hub.start().catch((err) => {
@@ -1749,8 +1868,9 @@ NOTIFICATIONS:
1749
1868
  init_config();
1750
1869
  init_constants();
1751
1870
  init_logger();
1871
+ init_crash_guard();
1752
1872
  import { spawn } from "child_process";
1753
- import fs4 from "fs";
1873
+ import fs5 from "fs";
1754
1874
  import path6 from "path";
1755
1875
  import readline from "readline";
1756
1876
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -1776,7 +1896,7 @@ Quick start:
1776
1896
  async function checkForUpdates() {
1777
1897
  try {
1778
1898
  const pkgPath = path6.join(__dirname2, "..", "package.json");
1779
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
1899
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
1780
1900
  const currentVersion = pkg.version;
1781
1901
  const res = await fetch("https://registry.npmjs.org/@delt/claude-alarm/latest", {
1782
1902
  signal: AbortSignal.timeout(3e3)
@@ -1799,17 +1919,17 @@ async function hubStart(daemon) {
1799
1919
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
1800
1920
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
1801
1921
  checkForUpdates();
1802
- if (fs4.existsSync(PID_FILE)) {
1803
- const pid = parseInt(fs4.readFileSync(PID_FILE, "utf-8").trim(), 10);
1922
+ if (fs5.existsSync(PID_FILE)) {
1923
+ const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
1804
1924
  if (isProcessRunning(pid)) {
1805
1925
  console.log(`Hub is already running (PID: ${pid}) on http://${displayHost}:${port}`);
1806
1926
  return;
1807
1927
  }
1808
- fs4.unlinkSync(PID_FILE);
1928
+ fs5.unlinkSync(PID_FILE);
1809
1929
  }
1810
1930
  if (daemon) {
1811
1931
  ensureConfigDir();
1812
- const logFd = fs4.openSync(LOG_FILE, "a");
1932
+ const logFd = fs5.openSync(LOG_FILE, "a");
1813
1933
  const hubScript = path6.join(__dirname2, "hub", "server.js");
1814
1934
  const child = spawn(process.execPath, [hubScript], {
1815
1935
  detached: true,
@@ -1817,7 +1937,7 @@ async function hubStart(daemon) {
1817
1937
  env: { ...process.env }
1818
1938
  });
1819
1939
  if (child.pid) {
1820
- fs4.writeFileSync(PID_FILE, String(child.pid), "utf-8");
1940
+ fs5.writeFileSync(PID_FILE, String(child.pid), "utf-8");
1821
1941
  child.unref();
1822
1942
  console.log(`Hub started as daemon (PID: ${child.pid})`);
1823
1943
  console.log(`Dashboard: http://${displayHost}:${port}`);
@@ -1830,15 +1950,18 @@ async function hubStart(daemon) {
1830
1950
  } else {
1831
1951
  console.log(`Starting hub on http://${displayHost}:${port} (press Ctrl+C to stop)`);
1832
1952
  console.log(`Token: ${config2.hub.token}`);
1953
+ console.log(`Logs: ${LOG_FILE}`);
1954
+ installCrashGuard("hub foreground");
1955
+ logStartup("Hub foreground");
1833
1956
  const { HubServer: HubServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
1834
1957
  const hub = new HubServer2(config2);
1835
1958
  await hub.start();
1836
1959
  ensureConfigDir();
1837
- fs4.writeFileSync(PID_FILE, String(process.pid), "utf-8");
1960
+ fs5.writeFileSync(PID_FILE, String(process.pid), "utf-8");
1838
1961
  const shutdown = async () => {
1839
1962
  console.log("\nShutting down...");
1840
1963
  await hub.stop();
1841
- if (fs4.existsSync(PID_FILE)) fs4.unlinkSync(PID_FILE);
1964
+ if (fs5.existsSync(PID_FILE)) fs5.unlinkSync(PID_FILE);
1842
1965
  process.exit(0);
1843
1966
  };
1844
1967
  process.on("SIGINT", shutdown);
@@ -1846,18 +1969,18 @@ async function hubStart(daemon) {
1846
1969
  }
1847
1970
  }
1848
1971
  function hubStop() {
1849
- if (!fs4.existsSync(PID_FILE)) {
1972
+ if (!fs5.existsSync(PID_FILE)) {
1850
1973
  console.log("Hub is not running (no PID file found)");
1851
1974
  return;
1852
1975
  }
1853
- const pid = parseInt(fs4.readFileSync(PID_FILE, "utf-8").trim(), 10);
1976
+ const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
1854
1977
  try {
1855
1978
  process.kill(pid, "SIGTERM");
1856
1979
  console.log(`Hub stopped (PID: ${pid})`);
1857
1980
  } catch {
1858
1981
  console.log("Hub process not found (may have already stopped)");
1859
1982
  }
1860
- fs4.unlinkSync(PID_FILE);
1983
+ fs5.unlinkSync(PID_FILE);
1861
1984
  }
1862
1985
  async function hubStatus() {
1863
1986
  const config2 = loadConfig();
@@ -1865,8 +1988,8 @@ async function hubStatus() {
1865
1988
  const port = config2.hub.port ?? DEFAULT_HUB_PORT;
1866
1989
  const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
1867
1990
  let pidInfo = "not running";
1868
- if (fs4.existsSync(PID_FILE)) {
1869
- const pid = parseInt(fs4.readFileSync(PID_FILE, "utf-8").trim(), 10);
1991
+ if (fs5.existsSync(PID_FILE)) {
1992
+ const pid = parseInt(fs5.readFileSync(PID_FILE, "utf-8").trim(), 10);
1870
1993
  if (isProcessRunning(pid)) {
1871
1994
  pidInfo = `running (PID: ${pid})`;
1872
1995
  } else {
@@ -1960,9 +2083,9 @@ claude-alarm init for "${projectName}"
1960
2083
  }
1961
2084
  const mcpPath = path6.join(dir, ".mcp.json");
1962
2085
  let mcpConfig = {};
1963
- if (fs4.existsSync(mcpPath)) {
2086
+ if (fs5.existsSync(mcpPath)) {
1964
2087
  try {
1965
- mcpConfig = JSON.parse(fs4.readFileSync(mcpPath, "utf-8"));
2088
+ mcpConfig = JSON.parse(fs5.readFileSync(mcpPath, "utf-8"));
1966
2089
  } catch {
1967
2090
  mcpConfig = {};
1968
2091
  }
@@ -1973,7 +2096,7 @@ claude-alarm init for "${projectName}"
1973
2096
  args: ["-y", "@delt/claude-alarm", "serve"],
1974
2097
  env
1975
2098
  };
1976
- fs4.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
2099
+ fs5.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
1977
2100
  console.log(`
1978
2101
  \u2713 Created ${mcpPath}`);
1979
2102
  if (remote.toLowerCase() !== "y") {