@delt/claude-alarm 1.3.0 → 1.4.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/dist/cli.js CHANGED
@@ -301,7 +301,7 @@ var init_session_manager = __esm({
301
301
 
302
302
  // src/hub/notifier.ts
303
303
  import notifier from "node-notifier";
304
- import { execFile } from "child_process";
304
+ import { execFile as execFile2 } from "child_process";
305
305
  var Notifier;
306
306
  var init_notifier = __esm({
307
307
  "src/hub/notifier.ts"() {
@@ -362,7 +362,7 @@ var init_notifier = __esm({
362
362
  }
363
363
  checkWindowsNotifications() {
364
364
  return new Promise((resolve) => {
365
- execFile(
365
+ execFile2(
366
366
  "powershell",
367
367
  ["-Command", '(Get-ItemProperty -Path "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\PushNotifications" -Name ToastEnabled -ErrorAction SilentlyContinue).ToastEnabled'],
368
368
  { windowsHide: true },
@@ -383,7 +383,7 @@ var init_notifier = __esm({
383
383
  logger.warn("Windows notifications are disabled. Opening notification settings...");
384
384
  logger.warn("Please enable notifications for this app, then try again.");
385
385
  if (process.platform === "win32") {
386
- execFile("powershell", ["-Command", "Start-Process ms-settings:notifications"], { windowsHide: true });
386
+ execFile2("powershell", ["-Command", "Start-Process ms-settings:notifications"], { windowsHide: true });
387
387
  }
388
388
  setTimeout(() => {
389
389
  this.notificationSettingsOpened = false;
@@ -445,7 +445,7 @@ import { randomUUID as randomUUID2 } from "crypto";
445
445
  function visibleLength(html) {
446
446
  return html.replace(/<[^>]*>/g, "").replace(/&(?:amp|lt|gt);/g, "&").length;
447
447
  }
448
- var TELEGRAM_API, MAX_CHOICE_MESSAGES, MAX_SELECTIONS, MAX_VISIBLE_CHARS, TRUNCATED, MAX_PHOTO_BYTES, TelegramBot;
448
+ var TELEGRAM_API, MAX_CHOICE_MESSAGES, MAX_SELECTIONS, MAX_VISIBLE_CHARS, TRUNCATED, MAX_PHOTO_BYTES, NO_LONGER_CONNECTED, TelegramBot;
449
449
  var init_telegram = __esm({
450
450
  "src/hub/telegram.ts"() {
451
451
  "use strict";
@@ -459,6 +459,7 @@ var init_telegram = __esm({
459
459
  MAX_VISIBLE_CHARS = 4e3;
460
460
  TRUNCATED = "\u2026(truncated)";
461
461
  MAX_PHOTO_BYTES = 10 * 1024 * 1024;
462
+ NO_LONGER_CONNECTED = "the session is no longer connected";
462
463
  TelegramBot = class _TelegramBot {
463
464
  config;
464
465
  offset = 0;
@@ -629,13 +630,7 @@ ${TRUNCATED}`);
629
630
  this.selections.delete(latest);
630
631
  const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
631
632
  if (session) {
632
- let delivered = true;
633
- if (pending.photoFileId) {
634
- delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
635
- } else if (pending.text) {
636
- this.deliverToSession(session.id, pending.text);
637
- }
638
- if (delivered) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
633
+ if (await this.deliverPending(session.id, pending)) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
639
634
  } else {
640
635
  this.sendMessage("Invalid session number.");
641
636
  }
@@ -677,9 +672,13 @@ ${TRUNCATED}`);
677
672
  this.sendMessage("Multiple sessions active. Select one:", void 0, { inline_keyboard: rows });
678
673
  }
679
674
  deliverToSession(sessionId2, content) {
680
- if (this.onMessageToSession) {
681
- this.onMessageToSession(sessionId2, content);
682
- }
675
+ const delivered = this.onMessageToSession?.(sessionId2, content) === true;
676
+ if (!delivered) void this.sendMessage(`Not delivered: ${NO_LONGER_CONNECTED}`);
677
+ return delivered;
678
+ }
679
+ async deliverPending(sessionId2, pending) {
680
+ if (pending.photoFileId) return this.deliverPhotoToSessionByFileId(sessionId2, pending.photoFileId, pending.caption);
681
+ return pending.text ? this.deliverToSession(sessionId2, pending.text) : false;
683
682
  }
684
683
  async deliverPhotoToSession(sessionId2, photos, caption) {
685
684
  const largest = photos[photos.length - 1];
@@ -711,8 +710,19 @@ ${TRUNCATED}`);
711
710
  const filePath = path6.join(UPLOADS_DIR, filename);
712
711
  fs5.writeFileSync(filePath, buffer);
713
712
  logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
714
- if (this.onImageToSession) {
715
- this.onImageToSession(sessionId2, filePath, mimeType, caption);
713
+ let accepted = false;
714
+ try {
715
+ accepted = this.onImageToSession?.(sessionId2, filePath, mimeType, caption) === true;
716
+ } catch (err) {
717
+ logger.warn(`Telegram photo hand-off failed: ${err.message}`);
718
+ }
719
+ if (!accepted) {
720
+ try {
721
+ fs5.unlinkSync(filePath);
722
+ } catch {
723
+ }
724
+ this.photoNotDelivered(NO_LONGER_CONNECTED);
725
+ return false;
716
726
  }
717
727
  setTimeout(() => {
718
728
  try {
@@ -720,7 +730,7 @@ ${TRUNCATED}`);
720
730
  } catch {
721
731
  }
722
732
  }, 5 * 60 * 1e3).unref();
723
- return this.onImageToSession !== void 0;
733
+ return true;
724
734
  } catch (err) {
725
735
  logger.warn(`Telegram photo download failed: ${err.message}`);
726
736
  this.photoNotDelivered("the download failed");
@@ -915,15 +925,10 @@ ${label}`);
915
925
  return;
916
926
  }
917
927
  this.selections.delete(selectionId);
918
- if (pending.photoFileId) {
919
- const delivered = await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
920
- if (!delivered) {
921
- await this.answerCallbackQuery(query.id, "Photo not delivered");
922
- if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
923
- return;
924
- }
925
- } else if (pending.text) {
926
- this.deliverToSession(session.id, pending.text);
928
+ if (!await this.deliverPending(session.id, pending)) {
929
+ await this.answerCallbackQuery(query.id, pending.photoFileId ? "Photo not delivered" : "Not delivered");
930
+ if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
931
+ return;
927
932
  }
928
933
  await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
929
934
  if (query.message) {
@@ -1624,7 +1629,16 @@ var init_server = __esm({
1624
1629
  this.sessions.updateActivity(msg.sessionId);
1625
1630
  const notifySession = this.sessions.get(msg.sessionId);
1626
1631
  const notifyLabel = this.getSessionLabel(notifySession);
1627
- this.notifier.notifyWithSession(msg.sessionId, notifyLabel, `[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
1632
+ const notifyTitle = `[${notifyLabel}] ${msg.title}`;
1633
+ if (msg.to === "api") {
1634
+ logger.info(`${notifyTitle}: ${msg.message}`);
1635
+ break;
1636
+ }
1637
+ if (msg.to === "telegram") {
1638
+ void this.telegramBot?.sendNotification(msg.sessionId, notifyLabel, notifyTitle, msg.message);
1639
+ break;
1640
+ }
1641
+ if (msg.to !== "dashboard") this.notifier.notifyWithSession(msg.sessionId, notifyLabel, notifyTitle, msg.message, msg.level ?? "info");
1628
1642
  this.broadcastToDashboards({
1629
1643
  type: "notification",
1630
1644
  sessionId: msg.sessionId,
@@ -1826,6 +1840,11 @@ var init_server = __esm({
1826
1840
  const channelWs = this.channelSockets.get(msg.sessionId);
1827
1841
  if (channelWs?.readyState === WebSocket2.OPEN) {
1828
1842
  channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
1843
+ } else {
1844
+ const reason = "the session is not connected";
1845
+ logger.warn(`Message rejected for ${msg.sessionId}: ${reason}`);
1846
+ const rejected = { type: "message_rejected", sessionId: msg.sessionId, reason };
1847
+ if (ws.readyState === WebSocket2.OPEN) ws.send(JSON.stringify(rejected));
1829
1848
  }
1830
1849
  } else if (msg.type === "image_upload") {
1831
1850
  this.handleImageUpload(ws, msg);
@@ -1951,19 +1970,25 @@ var init_server = __esm({
1951
1970
  this.telegramBot.getSessions = () => this.sessions.getAll();
1952
1971
  this.telegramBot.onMessageToSession = (sessionId2, content) => {
1953
1972
  const channelWs = this.channelSockets.get(sessionId2);
1954
- if (channelWs?.readyState === WebSocket2.OPEN) {
1955
- const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "telegram" };
1956
- channelWs.send(JSON.stringify(msg));
1957
- logger.info(`Telegram message forwarded to session: ${sessionId2}`);
1973
+ if (channelWs?.readyState !== WebSocket2.OPEN) {
1974
+ logger.warn(`Telegram message not delivered to ${sessionId2}: the session is not connected`);
1975
+ return false;
1958
1976
  }
1977
+ const msg = { type: "message_to_session", sessionId: sessionId2, content, source: "telegram" };
1978
+ channelWs.send(JSON.stringify(msg));
1979
+ logger.info(`Telegram message forwarded to session: ${sessionId2}`);
1980
+ return true;
1959
1981
  };
1960
1982
  this.telegramBot.onImageToSession = (sessionId2, imagePath, mimeType, caption) => {
1961
1983
  const channelWs = this.channelSockets.get(sessionId2);
1962
- if (channelWs?.readyState === WebSocket2.OPEN) {
1963
- const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption, source: "telegram" };
1964
- channelWs.send(JSON.stringify(msg));
1965
- logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
1984
+ if (channelWs?.readyState !== WebSocket2.OPEN) {
1985
+ logger.warn(`Telegram photo not delivered to ${sessionId2}: the session is not connected`);
1986
+ return false;
1966
1987
  }
1988
+ const msg = { type: "image_to_session", sessionId: sessionId2, imagePath, mimeType, content: caption, source: "telegram" };
1989
+ channelWs.send(JSON.stringify(msg));
1990
+ logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
1991
+ return true;
1967
1992
  };
1968
1993
  this.telegramBot.onPermissionVerdict = (sessionId2, requestId, behavior) => {
1969
1994
  if (this.forwardPermissionResponse({ sessionId: sessionId2, requestId, behavior })) {
@@ -2627,7 +2652,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
2627
2652
 
2628
2653
  // src/codex/transport.ts
2629
2654
  init_logger();
2630
- import { spawn } from "child_process";
2655
+ import { execFile, spawn } from "child_process";
2631
2656
  import fs2 from "fs";
2632
2657
  import path3 from "path";
2633
2658
  import { Duplex } from "stream";
@@ -2657,14 +2682,116 @@ function findCodex(command, platform = process.platform, env = process.env) {
2657
2682
  ];
2658
2683
  return candidates.find((file) => !!file && fs2.existsSync(file));
2659
2684
  }
2685
+ var shellSpawns = /* @__PURE__ */ new WeakSet();
2686
+ var spawnedThroughShell = (child) => shellSpawns.has(child);
2660
2687
  var defaultSpawn = (command, args) => {
2661
2688
  const { file, shell } = resolveCommand(command);
2662
- if (shell) return spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
2663
- return spawn(file, args, { stdio: "pipe", windowsHide: true });
2689
+ if (!shell) return spawn(file, args, { stdio: "pipe", windowsHide: true });
2690
+ const child = spawn(`"${file}" ${args.join(" ")}`, { stdio: "pipe", windowsHide: true, shell: true });
2691
+ shellSpawns.add(child);
2692
+ return child;
2664
2693
  };
2694
+ var queryProcesses = () => new Promise((resolve, reject) => {
2695
+ const script = "@(Get-CimInstance Win32_Process | ForEach-Object { [pscustomobject]@{ pid = [int]$_.ProcessId; parentPid = [int]$_.ParentProcessId; creationTime = $_.CreationDate.ToUniversalTime().ToString('o') } }) | ConvertTo-Json -Compress";
2696
+ execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { windowsHide: true, timeout: 5e3, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
2697
+ if (err) {
2698
+ reject(err);
2699
+ return;
2700
+ }
2701
+ try {
2702
+ const rows = JSON.parse(stdout);
2703
+ if (!Array.isArray(rows) || rows.some((row) => !Number.isInteger(row.pid) || !Number.isInteger(row.parentPid) || typeof row.creationTime !== "string")) {
2704
+ throw new Error("invalid process snapshot");
2705
+ }
2706
+ resolve(rows);
2707
+ } catch (error) {
2708
+ reject(error);
2709
+ }
2710
+ });
2711
+ });
2712
+ var runFile = (file, args, options, callback) => {
2713
+ execFile(file, args, options, (err) => callback(err));
2714
+ };
2715
+ function descendantsOf(rootPid, rows) {
2716
+ const root = rows.find((row) => row.pid === rootPid);
2717
+ if (!root) return void 0;
2718
+ const found = [];
2719
+ const seen = /* @__PURE__ */ new Set([root.pid]);
2720
+ let level = [root];
2721
+ for (let depth = 1; level.length; depth++) {
2722
+ const next = [];
2723
+ for (const parent of level) {
2724
+ for (const row of rows) {
2725
+ if (row.parentPid !== parent.pid || seen.has(row.pid) || row.creationTime < parent.creationTime) continue;
2726
+ seen.add(row.pid);
2727
+ next.push(row);
2728
+ found.push({ ...row, depth });
2729
+ }
2730
+ }
2731
+ level = next;
2732
+ }
2733
+ return found;
2734
+ }
2735
+ var exited = (child) => child.exitCode !== null || child.signalCode !== null;
2736
+ function treeKiller(platform = process.platform, run = runFile, query = queryProcesses, throughShell = spawnedThroughShell) {
2737
+ const recorded = /* @__PURE__ */ new WeakMap();
2738
+ const stop = (pid) => new Promise((resolve) => {
2739
+ run("taskkill", ["/PID", String(pid), "/F"], { windowsHide: true }, (err) => {
2740
+ if (err) logger.debug(`taskkill ${pid} failed: ${err.message}`);
2741
+ resolve();
2742
+ });
2743
+ });
2744
+ const stopDeepestFirst = async (descendants) => {
2745
+ for (const descendant of [...descendants].sort((a, b) => b.depth - a.depth)) await stop(descendant.pid);
2746
+ };
2747
+ const snapshot = async () => {
2748
+ try {
2749
+ return await query();
2750
+ } catch (err) {
2751
+ logger.debug(`codex process query failed: ${String(err)}`);
2752
+ return void 0;
2753
+ }
2754
+ };
2755
+ const isShellTree = (child) => platform === "win32" && child.pid !== void 0 && throughShell(child);
2756
+ const killer = async (child) => {
2757
+ if (!isShellTree(child)) {
2758
+ if (!exited(child)) child.kill();
2759
+ return;
2760
+ }
2761
+ if (!exited(child)) {
2762
+ const rows = await snapshot();
2763
+ if (!exited(child)) {
2764
+ const descendants2 = rows && descendantsOf(child.pid, rows);
2765
+ if (descendants2) await stopDeepestFirst(descendants2);
2766
+ child.kill();
2767
+ return;
2768
+ }
2769
+ }
2770
+ const descendants = await recorded.get(child) ?? [];
2771
+ if (!descendants.length) return;
2772
+ const current = await snapshot();
2773
+ if (!current) return;
2774
+ await stopDeepestFirst(descendants.filter((d) => current.some((row) => row.pid === d.pid && row.creationTime === d.creationTime)));
2775
+ };
2776
+ killer.track = async (child) => {
2777
+ if (!isShellTree(child) || exited(child)) return;
2778
+ const earlier = recorded.get(child);
2779
+ const latest = (async () => {
2780
+ const rows = await snapshot();
2781
+ const found = rows && !exited(child) ? descendantsOf(child.pid, rows) ?? [] : [];
2782
+ const before = await earlier ?? [];
2783
+ return [...before, ...found.filter((d) => !before.some((b) => b.pid === d.pid && b.creationTime === d.creationTime))];
2784
+ })();
2785
+ recorded.set(child, latest);
2786
+ await latest;
2787
+ };
2788
+ return killer;
2789
+ }
2790
+ var killTree = treeKiller();
2665
2791
  var HANDSHAKE_TIMEOUT_MS = 1e4;
2666
- function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS) {
2792
+ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIMEOUT_MS, killTreeFn = killTree) {
2667
2793
  const child = spawnFn(command, ["app-server", "proxy"]);
2794
+ void killTreeFn.track?.(child);
2668
2795
  child.stdin?.on("error", () => {
2669
2796
  });
2670
2797
  child.stderr?.on("data", (d) => logger.debug(`codex proxy: ${String(d).trim()}`));
@@ -2691,10 +2818,12 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
2691
2818
  const ws = new WebSocket("ws://localhost/", {
2692
2819
  createConnection: (() => stream)
2693
2820
  });
2694
- const close = () => {
2821
+ let closing;
2822
+ const close = () => closing ??= (async () => {
2695
2823
  ws.terminate();
2696
- child.kill();
2697
- };
2824
+ child.stdin?.end();
2825
+ await killTreeFn(child);
2826
+ })();
2698
2827
  return new Promise((resolve, reject) => {
2699
2828
  let settled = false;
2700
2829
  const timer = setTimeout(() => fail(new Error(`codex daemon did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
@@ -2702,8 +2831,7 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
2702
2831
  if (settled) return;
2703
2832
  settled = true;
2704
2833
  clearTimeout(timer);
2705
- close();
2706
- reject(err);
2834
+ void close().then(() => reject(err), () => reject(err));
2707
2835
  };
2708
2836
  child.on("error", fail);
2709
2837
  child.once("exit", (code) => fail(new Error(`codex proxy exited (code ${code})`)));
@@ -2714,6 +2842,7 @@ function connectProxy(command, spawnFn = defaultSpawn, timeoutMs = HANDSHAKE_TIM
2714
2842
  clearTimeout(timer);
2715
2843
  child.on("error", (err) => logger.warn(`codex proxy error: ${err.message}`));
2716
2844
  ws.on("error", (err) => logger.warn(`codex daemon socket error: ${err.message}`));
2845
+ void killTreeFn.track?.(child);
2717
2846
  resolve({ ws, close });
2718
2847
  });
2719
2848
  });
@@ -3005,7 +3134,7 @@ async function runCodexTask(opts) {
3005
3134
  };
3006
3135
  } finally {
3007
3136
  for (const t of timers) clearTimeout(t);
3008
- conn.close();
3137
+ await conn.close();
3009
3138
  }
3010
3139
  }
3011
3140
  var VALUE_FLAGS = {
@@ -3496,19 +3625,19 @@ async function hubStart(daemon) {
3496
3625
  process.exit(1);
3497
3626
  }
3498
3627
  fs9.writeFileSync(PID_FILE, String(pid), "utf-8");
3499
- let exited = false;
3628
+ let exited2 = false;
3500
3629
  child.once("exit", () => {
3501
- exited = true;
3630
+ exited2 = true;
3502
3631
  });
3503
3632
  child.unref();
3504
3633
  const startup = await waitForHub({
3505
3634
  url: `http://${displayHost}:${port}/api/status`,
3506
3635
  token: config2.hub.token,
3507
3636
  pid,
3508
- isAlive: () => !exited
3637
+ isAlive: () => !exited2
3509
3638
  });
3510
3639
  if (startup !== "ready") {
3511
- if (!exited) child.kill();
3640
+ if (!exited2) child.kill();
3512
3641
  if (fs9.existsSync(PID_FILE)) fs9.unlinkSync(PID_FILE);
3513
3642
  console.error(startup === "exited" ? `Hub exited during startup. See ${LOG_FILE}` : `Hub did not answer at http://${displayHost}:${port} within ${HUB_START_TIMEOUT_MS / 1e3}s and was stopped. See ${LOG_FILE}`);
3514
3643
  process.exit(1);