@delt/claude-alarm 0.6.0 → 0.6.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/README.md CHANGED
@@ -19,6 +19,7 @@ Monitor and interact with multiple Claude Code sessions from a web dashboard. Ge
19
19
  - **Webhook Support** — Slack, Discord, or custom webhook endpoints
20
20
  - **Token Auth** — Auto-generated secure access
21
21
  - **Dark / Light Mode** — Theme toggle with persistence
22
+ - **Permission Relay** — Approve/deny tool calls from dashboard or phone
22
23
  - **Multi-Machine** — Remote hub access support
23
24
 
24
25
  ## Quick Start
@@ -167,6 +168,17 @@ Two-way messaging with Claude sessions via Telegram — text and images.
167
168
  }
168
169
  ```
169
170
 
171
+ ## Permission Relay
172
+
173
+ Approve or deny Claude's tool calls remotely — from the dashboard or your phone — without `--dangerously-skip-permissions`.
174
+
175
+ When Claude wants to run a tool (Bash, Write, Edit, etc.), a permission request appears on the dashboard with **Allow / Deny** buttons. Keyboard shortcuts: **Enter** = Allow, **Esc** = Deny.
176
+
177
+ - Works with Claude Code **v2.1.81+**
178
+ - No extra setup needed — automatically enabled
179
+ - Local terminal prompt stays open; whichever answer arrives first (local or dashboard) is applied
180
+ - Parsed previews: Bash commands show `$ command`, file operations show file paths
181
+
170
182
  ## Remote Access
171
183
 
172
184
  <p align="center">
package/dist/cli.js CHANGED
@@ -314,6 +314,8 @@ var init_telegram = __esm({
314
314
  onMessageToSession;
315
315
  // Callback: when an image arrives from Telegram for a session
316
316
  onImageToSession;
317
+ // Callback: when a permission verdict arrives from Telegram
318
+ onPermissionVerdict;
317
319
  // Callback: get current sessions list
318
320
  getSessions;
319
321
  // Pending messages for session selection
@@ -398,7 +400,9 @@ ${message}`;
398
400
  if (data.ok && data.result.length > 0) {
399
401
  for (const update of data.result) {
400
402
  this.offset = update.update_id + 1;
401
- if (update.message) {
403
+ if (update.callback_query) {
404
+ this.handleCallbackQuery(update.callback_query);
405
+ } else if (update.message) {
402
406
  this.handleIncomingMessage(update.message);
403
407
  }
404
408
  }
@@ -524,6 +528,74 @@ ${sessionList}`);
524
528
  getLabel(session) {
525
529
  return session.cwd?.replace(/^.*[/\\]/, "") || session.name;
526
530
  }
531
+ /** Send a permission request with inline buttons */
532
+ async sendPermissionRequest(sessionId2, sessionLabel, requestId, toolName, description, inputPreview) {
533
+ let preview = inputPreview;
534
+ try {
535
+ const p = JSON.parse(inputPreview);
536
+ if (p.command) preview = `$ ${p.command}`;
537
+ else if (p.file_path) preview = p.file_path + (p.content ? "\n" + p.content.slice(0, 150) : "");
538
+ } catch {
539
+ const cmdMatch = inputPreview.match(/"command"\s*:\s*"((?:[^"\\]|\\.)*)"/);
540
+ if (cmdMatch) preview = `$ ${cmdMatch[1]}`;
541
+ }
542
+ const text = `\u26A0\uFE0F <b>Permission Request</b>
543
+
544
+ \u{1F4C2} <b>${this.escHtml(sessionLabel)}</b>
545
+ \u{1F527} <code>${this.escHtml(toolName)}</code> \u2014 ${this.escHtml(description)}
546
+
547
+ <pre>${this.escHtml(preview.slice(0, 300))}</pre>`;
548
+ const replyMarkup = {
549
+ inline_keyboard: [[
550
+ { text: "\u2705 Allow", callback_data: `perm:allow:${sessionId2}:${requestId}` },
551
+ { text: "\u274C Deny", callback_data: `perm:deny:${sessionId2}:${requestId}` }
552
+ ]]
553
+ };
554
+ await this.sendMessage(text, void 0, replyMarkup);
555
+ }
556
+ async handleCallbackQuery(query) {
557
+ if (!query.data?.startsWith("perm:")) return;
558
+ const parts = query.data.split(":");
559
+ if (parts.length < 4) return;
560
+ const [, action, sessionId2, requestId] = parts;
561
+ const behavior = action === "allow" ? "allow" : "deny";
562
+ if (this.onPermissionVerdict) {
563
+ this.onPermissionVerdict(sessionId2, requestId, behavior);
564
+ }
565
+ await this.answerCallbackQuery(query.id, behavior === "allow" ? "\u2705 Allowed" : "\u274C Denied");
566
+ if (query.message) {
567
+ const label = behavior === "allow" ? "\u2705 <b>Allowed</b>" : "\u274C <b>Denied</b>";
568
+ const original = query.message.text || "";
569
+ await this.editMessageText(query.message.chat.id, query.message.message_id, original + `
570
+
571
+ ${label}`);
572
+ }
573
+ }
574
+ async answerCallbackQuery(callbackQueryId, text) {
575
+ try {
576
+ await fetch(`${this.apiUrl}/answerCallbackQuery`, {
577
+ method: "POST",
578
+ headers: { "Content-Type": "application/json" },
579
+ body: JSON.stringify({ callback_query_id: callbackQueryId, text })
580
+ });
581
+ } catch (err) {
582
+ logger.warn(`Telegram answerCallbackQuery error: ${err.message}`);
583
+ }
584
+ }
585
+ async editMessageText(chatId, messageId, text) {
586
+ try {
587
+ await fetch(`${this.apiUrl}/editMessageText`, {
588
+ method: "POST",
589
+ headers: { "Content-Type": "application/json" },
590
+ body: JSON.stringify({ chat_id: chatId, message_id: messageId, text, parse_mode: "HTML" })
591
+ });
592
+ } catch (err) {
593
+ logger.warn(`Telegram editMessageText error: ${err.message}`);
594
+ }
595
+ }
596
+ escHtml(s) {
597
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
598
+ }
527
599
  /** Update config (e.g., from dashboard settings) */
528
600
  updateConfig(config2) {
529
601
  const wasPolling = this.polling;
@@ -863,6 +935,11 @@ var init_server = __esm({
863
935
  inputPreview: msg.inputPreview,
864
936
  timestamp: msg.timestamp
865
937
  });
938
+ if (this.telegramBot) {
939
+ const session = this.sessions.get(msg.sessionId);
940
+ const label = this.getSessionLabel(session);
941
+ this.telegramBot.sendPermissionRequest(msg.sessionId, label, msg.requestId, msg.toolName, msg.description, msg.inputPreview);
942
+ }
866
943
  break;
867
944
  }
868
945
  }
@@ -985,6 +1062,14 @@ var init_server = __esm({
985
1062
  logger.info(`Telegram photo forwarded to session: ${sessionId2}`);
986
1063
  }
987
1064
  };
1065
+ this.telegramBot.onPermissionVerdict = (sessionId2, requestId, behavior) => {
1066
+ const channelWs = this.channelSockets.get(sessionId2);
1067
+ if (channelWs?.readyState === WebSocket.OPEN) {
1068
+ const msg = { type: "permission_response", sessionId: sessionId2, requestId, behavior };
1069
+ channelWs.send(JSON.stringify(msg));
1070
+ logger.info(`Telegram permission verdict [${requestId}]: ${behavior} -> session ${sessionId2}`);
1071
+ }
1072
+ };
988
1073
  this.notifier.configure({ telegramBot: this.telegramBot });
989
1074
  this.telegramBot.startPolling();
990
1075
  logger.info("Telegram bot initialized");