@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 +12 -0
- package/dist/cli.js +86 -1
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +34 -30
- package/dist/hub/server.js +86 -1
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +86 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +34 -30
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.
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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");
|