telegram-approval-buttons 5.0.0 → 5.1.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/CHANGELOG.md +19 -17
- package/README.md +11 -24
- package/index.ts +175 -9
- package/lib/approval-parser.ts +30 -7
- package/lib/approval-store.ts +1 -1
- package/lib/diagnostics.ts +2 -2
- package/lib/message-formatter.ts +1 -1
- package/lib/slack-api.ts +2 -2
- package/lib/slack-formatter.ts +1 -1
- package/lib/telegram-api.ts +1 -1
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/types.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,27 +5,30 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [5.
|
|
8
|
+
## [5.1.0] - 2026-03-31
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **OpenClaw 2026 approval format support** — parser now handles native `Approval required`, `Full id:`, tool-result `(id ..., full ...)`, and `/approve ...` guidance formats (#4)
|
|
12
|
+
- **Async completion delivery** — captures post-approval exec output when the agent emits `NO_REPLY` and delivers results to Telegram users instead of silently dropping them
|
|
13
|
+
- **Duplicate approval suppression** — detects and cancels assistant fallback `/approve` messages when a native approval prompt already exists, preventing double prompts
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Broadened duplicate fallback suppression to cover all actions (`allow-once|allow-always|deny`)
|
|
17
|
+
- `NO_REPLY` delivery paths now reuse shared `escapeHtml` helper instead of manual `.replace()` chains
|
|
18
|
+
- Added null-result handling + warning logs for failed Telegram `NO_REPLY` sends
|
|
19
|
+
- Normalized `NO_REPLY` user-facing strings to English for consistency
|
|
9
20
|
|
|
10
|
-
###
|
|
21
|
+
### Contributors
|
|
11
22
|
|
|
12
|
-
|
|
23
|
+
Thanks to [@BlazzzPlay](https://github.com/BlazzzPlay) for contributing OpenClaw 2026 compatibility and async completion delivery in this release 🙏
|
|
13
24
|
|
|
14
|
-
|
|
25
|
+
## [5.0.1] - 2026-03-01
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- "telegram-approval-buttons": {
|
|
20
|
-
+ "approval-buttons": {
|
|
21
|
-
"enabled": true,
|
|
22
|
-
...
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
+
### Fixed
|
|
28
|
+
- **Reverted plugin ID to `telegram-approval-buttons`** — v5.0.0 renamed it to `approval-buttons`, causing a config warning on every startup. The plugin ID now matches the npm package name again. No config changes needed for users upgrading from v4.x.
|
|
29
|
+
- Normalized all file headers to use consistent naming
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
## [5.0.0] - 2026-02-24
|
|
29
32
|
|
|
30
33
|
### Added
|
|
31
34
|
- **Slack inline button support** — approval buttons now work in Slack via Block Kit interactive messages
|
|
@@ -36,7 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
36
39
|
|
|
37
40
|
### Changed
|
|
38
41
|
- **Cleaner approval messages** — removed verbose internal fields (Security, Ask, Host) from both Telegram and Slack formats. Messages now focus on: command, CWD, agent, and expiry
|
|
39
|
-
- Plugin ID and TAG shortened to `approval-buttons`
|
|
40
42
|
|
|
41
43
|
### Contributors
|
|
42
44
|
|
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
|
|
2
|
+
--Closed #30695.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
1
7
|
# 🔐 Approval Buttons for OpenClaw
|
|
2
8
|
|
|
3
9
|
> One-tap `exec` approvals in **Telegram** and **Slack** — no more typing `/approve <uuid> allow-once`.
|
|
@@ -75,7 +81,7 @@ Open your `~/.openclaw/openclaw.json` and add two things:
|
|
|
75
81
|
},
|
|
76
82
|
"plugins": {
|
|
77
83
|
"entries": {
|
|
78
|
-
"approval-buttons": {
|
|
84
|
+
"telegram-approval-buttons": {
|
|
79
85
|
"enabled": true,
|
|
80
86
|
"config": {
|
|
81
87
|
"botToken": "<your_bot_token>",
|
|
@@ -110,7 +116,7 @@ Pending: 0 · Processed: 0
|
|
|
110
116
|
Uptime: 1m
|
|
111
117
|
```
|
|
112
118
|
|
|
113
|
-
> ⚠️ **If you see `DISABLED — missing config`**, the plugin can't find your bot token or chat ID. Double-check that `botToken` and `chatId` are set in `plugins.entries.approval-buttons.config` in your `~/.openclaw/openclaw.json`.
|
|
119
|
+
> ⚠️ **If you see `DISABLED — missing config`**, the plugin can't find your bot token or chat ID. Double-check that `botToken` and `chatId` are set in `plugins.entries.telegram-approval-buttons.config` in your `~/.openclaw/openclaw.json`.
|
|
114
120
|
|
|
115
121
|
**That's it!** Next time the AI triggers an `exec` approval, you'll get inline buttons instead of text.
|
|
116
122
|
|
|
@@ -162,7 +168,7 @@ The plugin **auto-detects** `botToken` and `chatId` from your Telegram channel c
|
|
|
162
168
|
{
|
|
163
169
|
"plugins": {
|
|
164
170
|
"entries": {
|
|
165
|
-
"approval-buttons": {
|
|
171
|
+
"telegram-approval-buttons": {
|
|
166
172
|
"enabled": true,
|
|
167
173
|
"config": {
|
|
168
174
|
"chatId": "123456789", // Telegram chat ID
|
|
@@ -202,36 +208,17 @@ A: Yes, but the bot needs to be an admin or it needs permission to edit its own
|
|
|
202
208
|
|
|
203
209
|
| Problem | Fix |
|
|
204
210
|
|---------|-----|
|
|
205
|
-
| `DISABLED — missing config` in logs | Add `botToken` and `chatId` to `plugins.entries.approval-buttons.config` in your `~/.openclaw/openclaw.json`. See Step 2. |
|
|
206
|
-
| `plugin not found: telegram-approval-buttons` | You upgraded from v4.x. Rename the key in your config from `telegram-approval-buttons` to `approval-buttons`. See [Upgrading from v4.x](#upgrading-from-v4x). |
|
|
211
|
+
| `DISABLED — missing config` in logs | Add `botToken` and `chatId` to `plugins.entries.telegram-approval-buttons.config` in your `~/.openclaw/openclaw.json`. See Step 2. |
|
|
207
212
|
| Still getting old text approvals | Your `approvals.exec` config must target Telegram. See Step 2. |
|
|
208
213
|
| `/approvalstatus` says "unknown command" | Plugin didn't load. Run `openclaw plugins install telegram-approval-buttons` and restart the gateway. |
|
|
209
214
|
| No buttons appear | Check `tools.exec.ask` is not `"off"`. Run `/approvalstatus` to check config. |
|
|
210
215
|
| Buttons show but nothing happens | Bot needs message editing permission. Use a private chat or make bot admin. |
|
|
211
216
|
| Buttons say "expired" | Approval timed out before you tapped. Adjust `staleMins` if needed. |
|
|
212
217
|
|
|
213
|
-
## Upgrading from v4.x
|
|
214
|
-
|
|
215
|
-
v5.0.0 renamed the plugin ID from `telegram-approval-buttons` to `approval-buttons`. Update your `~/.openclaw/openclaw.json`:
|
|
216
|
-
|
|
217
|
-
```diff
|
|
218
|
-
"plugins": {
|
|
219
|
-
"entries": {
|
|
220
|
-
- "telegram-approval-buttons": {
|
|
221
|
-
+ "approval-buttons": {
|
|
222
|
-
"enabled": true,
|
|
223
|
-
...
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Then restart: `openclaw gateway restart`
|
|
230
|
-
|
|
231
218
|
## Architecture
|
|
232
219
|
|
|
233
220
|
```
|
|
234
|
-
approval-buttons/
|
|
221
|
+
telegram-approval-buttons/
|
|
235
222
|
├── index.ts # Entry point — orchestration only
|
|
236
223
|
├── types.ts # Shared TypeScript interfaces
|
|
237
224
|
├── lib/
|
package/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// approval-buttons · index.ts (v5.
|
|
2
|
+
// telegram-approval-buttons · index.ts (v5.1.0)
|
|
3
3
|
// Plugin entry point — orchestration only, all logic lives in lib/
|
|
4
4
|
//
|
|
5
5
|
// Adds inline keyboard/button approval messages to Telegram and Slack.
|
|
@@ -16,6 +16,7 @@ import { SlackApi } from "./lib/slack-api.js";
|
|
|
16
16
|
import { ApprovalStore } from "./lib/approval-store.js";
|
|
17
17
|
import { parseApprovalText, detectApprovalResult } from "./lib/approval-parser.js";
|
|
18
18
|
import {
|
|
19
|
+
escapeHtml,
|
|
19
20
|
formatApprovalRequest,
|
|
20
21
|
formatApprovalResolved,
|
|
21
22
|
formatApprovalExpired,
|
|
@@ -37,8 +38,31 @@ import {
|
|
|
37
38
|
|
|
38
39
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
39
40
|
|
|
40
|
-
const PLUGIN_VERSION = "5.
|
|
41
|
-
const TAG = "approval-buttons";
|
|
41
|
+
const PLUGIN_VERSION = "5.1.0";
|
|
42
|
+
const TAG = "telegram-approval-buttons";
|
|
43
|
+
|
|
44
|
+
const RE_RICH_APPROVAL_HEADER = /^\s*🔐\s+<b>Exec Approval<\/b>/i;
|
|
45
|
+
const RE_ASSISTANT_APPROVE_FALLBACK = /\/approve\s+[a-f0-9-]{8,}\s+(allow-once|allow-always|deny)\b/i;
|
|
46
|
+
|
|
47
|
+
function approvalIdVariants(id: string): string[] {
|
|
48
|
+
const clean = id.trim();
|
|
49
|
+
const short = clean.slice(0, 8);
|
|
50
|
+
if (!short || short === clean) return [clean];
|
|
51
|
+
return [clean, short];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type CompletionSnapshot = { output: string; capturedAt: number };
|
|
55
|
+
type ResolvedSnapshot = { id: string; command: string; resolvedAt: number };
|
|
56
|
+
|
|
57
|
+
const RE_ASYNC_COMPLETION = /An async command the user already approved has completed\./i;
|
|
58
|
+
const RE_COMPLETION_OUTPUT = /Exact completion details:\r?\nExec finished[^\r\n]*\r?\n([\s\S]+?)\r?\n\r?\nReply to the user/i;
|
|
59
|
+
|
|
60
|
+
function parseAsyncCompletionOutput(text: string): string | null {
|
|
61
|
+
if (!RE_ASYNC_COMPLETION.test(text)) return null;
|
|
62
|
+
const match = text.match(RE_COMPLETION_OUTPUT);
|
|
63
|
+
const output = match?.[1]?.trim();
|
|
64
|
+
return output || null;
|
|
65
|
+
}
|
|
42
66
|
|
|
43
67
|
// ── Plugin registration ─────────────────────────────────────────────────────
|
|
44
68
|
|
|
@@ -103,25 +127,28 @@ function register(api: any): void {
|
|
|
103
127
|
config.telegram.chatId,
|
|
104
128
|
entry.messageId,
|
|
105
129
|
formatApprovalExpired(entry.info),
|
|
106
|
-
).catch(() => {});
|
|
130
|
+
).catch(() => { });
|
|
107
131
|
} else if (entry.channel === "slack" && slack && config.slack) {
|
|
108
132
|
slack.updateMessage(
|
|
109
133
|
config.slack.channelId,
|
|
110
134
|
entry.slackTs,
|
|
111
135
|
"Exec Approval Expired",
|
|
112
136
|
formatSlackApprovalExpired(entry.info),
|
|
113
|
-
).catch(() => {});
|
|
137
|
+
).catch(() => { });
|
|
114
138
|
}
|
|
115
139
|
},
|
|
116
140
|
);
|
|
117
141
|
|
|
142
|
+
let lastCompletionGlobal: CompletionSnapshot | null = null;
|
|
143
|
+
let lastResolvedGlobal: ResolvedSnapshot | null = null;
|
|
144
|
+
|
|
118
145
|
// ─── 4. Register background service (cleanup timer) ──────────────────
|
|
119
146
|
|
|
120
147
|
api.registerService({
|
|
121
148
|
id: `${TAG}-cleanup`,
|
|
122
149
|
start: () => {
|
|
123
150
|
store.start();
|
|
124
|
-
runStartupChecks(tg, slack, log).catch(() => {});
|
|
151
|
+
runStartupChecks(tg, slack, log).catch(() => { });
|
|
125
152
|
},
|
|
126
153
|
stop: () => store.stop(),
|
|
127
154
|
});
|
|
@@ -141,6 +168,24 @@ function register(api: any): void {
|
|
|
141
168
|
|
|
142
169
|
// ─── 6. Register message_sending hook ────────────────────────────────
|
|
143
170
|
|
|
171
|
+
api.on(
|
|
172
|
+
"message_received",
|
|
173
|
+
async (
|
|
174
|
+
event: { content: string },
|
|
175
|
+
_ctx: { channelId: string },
|
|
176
|
+
) => {
|
|
177
|
+
const output = parseAsyncCompletionOutput(event.content);
|
|
178
|
+
if (!output) return;
|
|
179
|
+
lastCompletionGlobal = {
|
|
180
|
+
output,
|
|
181
|
+
capturedAt: Date.now(),
|
|
182
|
+
};
|
|
183
|
+
if (config.verbose) {
|
|
184
|
+
log.info(`[${TAG}] captured async completion output (${output.length} chars)`);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
|
|
144
189
|
api.on(
|
|
145
190
|
"message_sending",
|
|
146
191
|
async (
|
|
@@ -149,7 +194,17 @@ function register(api: any): void {
|
|
|
149
194
|
) => {
|
|
150
195
|
// ── Telegram ──────────────────────────────────────────────────
|
|
151
196
|
if (ctx.channelId === "telegram" && tg && config.telegram) {
|
|
152
|
-
return handleTelegram(
|
|
197
|
+
return handleTelegram(
|
|
198
|
+
event,
|
|
199
|
+
config.telegram.chatId,
|
|
200
|
+
tg,
|
|
201
|
+
store,
|
|
202
|
+
log,
|
|
203
|
+
() => lastCompletionGlobal,
|
|
204
|
+
() => { lastCompletionGlobal = null; },
|
|
205
|
+
() => lastResolvedGlobal,
|
|
206
|
+
(next) => { lastResolvedGlobal = next; },
|
|
207
|
+
);
|
|
153
208
|
}
|
|
154
209
|
|
|
155
210
|
// ── Slack ─────────────────────────────────────────────────────
|
|
@@ -159,6 +214,44 @@ function register(api: any): void {
|
|
|
159
214
|
},
|
|
160
215
|
);
|
|
161
216
|
|
|
217
|
+
// Some OpenClaw builds deliver approval prompts before plugins can cancel.
|
|
218
|
+
// In that case, rewrite the already-sent native approval message in-place
|
|
219
|
+
// to avoid a second "button message".
|
|
220
|
+
api.on(
|
|
221
|
+
"message_sent",
|
|
222
|
+
async (
|
|
223
|
+
event: { content: string; messageId?: string; metadata?: Record<string, unknown> },
|
|
224
|
+
ctx: { channelId: string },
|
|
225
|
+
) => {
|
|
226
|
+
if (ctx.channelId !== "telegram" || !tg || !config.telegram) return;
|
|
227
|
+
if (RE_RICH_APPROVAL_HEADER.test(event.content)) return;
|
|
228
|
+
|
|
229
|
+
const info = parseApprovalText(event.content);
|
|
230
|
+
if (!info) return;
|
|
231
|
+
|
|
232
|
+
const rawMsgId =
|
|
233
|
+
event.messageId
|
|
234
|
+
?? (typeof event.metadata?.message_id === "string" ? event.metadata.message_id : undefined)
|
|
235
|
+
?? (typeof event.metadata?.messageId === "string" ? event.metadata.messageId : undefined);
|
|
236
|
+
|
|
237
|
+
const messageId = rawMsgId ? Number(rawMsgId) : NaN;
|
|
238
|
+
if (!Number.isFinite(messageId)) return;
|
|
239
|
+
|
|
240
|
+
const edited = await tg.editMessageText(
|
|
241
|
+
config.telegram.chatId,
|
|
242
|
+
messageId,
|
|
243
|
+
formatApprovalRequest(info),
|
|
244
|
+
buildApprovalKeyboard(info.id),
|
|
245
|
+
);
|
|
246
|
+
if (!edited) return;
|
|
247
|
+
|
|
248
|
+
if (!store.has(info.id)) {
|
|
249
|
+
store.add(info.id, "telegram", { messageId }, info);
|
|
250
|
+
}
|
|
251
|
+
log.info(`[${TAG}] telegram upgraded native approval ${info.id.slice(0, 8)}… (msg=${messageId})`);
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
|
|
162
255
|
// ─── Done ─────────────────────────────────────────────────────────────
|
|
163
256
|
|
|
164
257
|
const channels = [config.telegram && "Telegram", config.slack && "Slack"]
|
|
@@ -175,13 +268,65 @@ async function handleTelegram(
|
|
|
175
268
|
tg: TelegramApi,
|
|
176
269
|
store: ApprovalStore,
|
|
177
270
|
log: any,
|
|
271
|
+
getLastCompletion: () => CompletionSnapshot | null,
|
|
272
|
+
clearLastCompletion: () => void,
|
|
273
|
+
getLastResolved: () => ResolvedSnapshot | null,
|
|
274
|
+
setLastResolved: (next: ResolvedSnapshot | null) => void,
|
|
178
275
|
): Promise<{ cancel: true } | void> {
|
|
276
|
+
// If the agent emits NO_REPLY right after an async exec completion event,
|
|
277
|
+
// convert it into a short delivery so Telegram users still get the result.
|
|
278
|
+
if (event.content.trim() === "NO_REPLY") {
|
|
279
|
+
const completion = getLastCompletion();
|
|
280
|
+
if (completion && Date.now() - completion.capturedAt <= 120_000) {
|
|
281
|
+
const sentId = await tg.sendMessage(
|
|
282
|
+
chatId,
|
|
283
|
+
[
|
|
284
|
+
"✅ <b>Command completed</b>",
|
|
285
|
+
"",
|
|
286
|
+
`<pre>${escapeHtml(completion.output)}</pre>`,
|
|
287
|
+
].join("\n"),
|
|
288
|
+
);
|
|
289
|
+
if (sentId === null) {
|
|
290
|
+
log.warn(`[${TAG}] telegram failed to deliver NO_REPLY completion output`);
|
|
291
|
+
}
|
|
292
|
+
clearLastCompletion();
|
|
293
|
+
setLastResolved(null);
|
|
294
|
+
return { cancel: true };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const resolved = getLastResolved();
|
|
298
|
+
if (resolved && Date.now() - resolved.resolvedAt <= 120_000) {
|
|
299
|
+
const sentId = await tg.sendMessage(
|
|
300
|
+
chatId,
|
|
301
|
+
[
|
|
302
|
+
"✅ <b>Command completed</b>",
|
|
303
|
+
"",
|
|
304
|
+
`<code>${escapeHtml(resolved.command)}</code>`,
|
|
305
|
+
"",
|
|
306
|
+
"Output was not forwarded by the agent (NO_REPLY).",
|
|
307
|
+
].join("\n"),
|
|
308
|
+
);
|
|
309
|
+
if (sentId === null) {
|
|
310
|
+
log.warn(`[${TAG}] telegram failed to deliver NO_REPLY fallback notice`);
|
|
311
|
+
}
|
|
312
|
+
setLastResolved(null);
|
|
313
|
+
return { cancel: true };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (log?.info) log.info(`[${TAG}] NO_REPLY without completion snapshot`);
|
|
317
|
+
}
|
|
318
|
+
|
|
179
319
|
// Check for approval resolution
|
|
180
320
|
const resolution = detectApprovalResult(event.content, store.entries());
|
|
181
321
|
if (resolution) {
|
|
182
322
|
const entry = store.resolve(resolution.id);
|
|
183
323
|
if (entry && entry.channel === "telegram") {
|
|
184
324
|
log.info(`[${TAG}] telegram resolved ${resolution.id.slice(0, 8)}… → ${resolution.action}`);
|
|
325
|
+
setLastResolved({
|
|
326
|
+
id: resolution.id,
|
|
327
|
+
command: entry.info.command,
|
|
328
|
+
resolvedAt: Date.now(),
|
|
329
|
+
});
|
|
185
330
|
await tg.editMessageText(
|
|
186
331
|
chatId,
|
|
187
332
|
entry.messageId,
|
|
@@ -195,6 +340,27 @@ async function handleTelegram(
|
|
|
195
340
|
const info = parseApprovalText(event.content);
|
|
196
341
|
if (!info) return;
|
|
197
342
|
|
|
343
|
+
const looksLikeAssistantFallback =
|
|
344
|
+
RE_ASSISTANT_APPROVE_FALLBACK.test(event.content)
|
|
345
|
+
&& !/Exec approval required/i.test(event.content)
|
|
346
|
+
&& !/\bApproval required\b/i.test(event.content);
|
|
347
|
+
|
|
348
|
+
// Newer OpenClaw builds already send a native approval card/message.
|
|
349
|
+
// Suppress the assistant's plain `/approve ...` fallback so users don't
|
|
350
|
+
// receive a second approval prompt.
|
|
351
|
+
if (looksLikeAssistantFallback) return { cancel: true };
|
|
352
|
+
|
|
353
|
+
// If this is the model's fallback "/approve ..." guidance and we already
|
|
354
|
+
// converted the native approval prompt, suppress this duplicate text.
|
|
355
|
+
for (const variant of approvalIdVariants(info.id)) {
|
|
356
|
+
if (store.has(variant)) return { cancel: true };
|
|
357
|
+
for (const pendingId of store.entries().keys()) {
|
|
358
|
+
if (pendingId.startsWith(variant) || variant.startsWith(pendingId)) {
|
|
359
|
+
return { cancel: true };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
198
364
|
if (store.has(info.id)) return { cancel: true };
|
|
199
365
|
|
|
200
366
|
log.info(`[${TAG}] telegram intercepting ${info.id.slice(0, 8)}…`);
|
|
@@ -265,8 +431,8 @@ async function handleSlack(
|
|
|
265
431
|
// ─── Plugin export ──────────────────────────────────────────────────────────
|
|
266
432
|
|
|
267
433
|
export default {
|
|
268
|
-
id: "approval-buttons",
|
|
269
|
-
name: "Approval Buttons",
|
|
434
|
+
id: "telegram-approval-buttons",
|
|
435
|
+
name: "Telegram Approval Buttons",
|
|
270
436
|
description:
|
|
271
437
|
"Adds inline buttons to exec approval messages in Telegram and Slack. " +
|
|
272
438
|
"Tap to approve/deny without typing commands.",
|
package/lib/approval-parser.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// approval-buttons · lib/approval-parser.ts
|
|
2
|
+
// telegram-approval-buttons · lib/approval-parser.ts
|
|
3
3
|
// Parse OpenClaw's plain-text exec approval format into structured data
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
|
|
@@ -8,11 +8,15 @@ import type { ApprovalAction, ApprovalInfo, ApprovalResolution, SentApproval } f
|
|
|
8
8
|
// ─── Regex patterns (compiled once) ─────────────────────────────────────────
|
|
9
9
|
|
|
10
10
|
const RE_APPROVAL_MARKER = /Exec approval required/i;
|
|
11
|
+
const RE_NATIVE_APPROVAL_MARKER = /\bApproval required\b/i;
|
|
12
|
+
const RE_TOOLRESULT_APPROVAL = /Approval required\s*\(id\s+([a-f0-9-]{8,})\s*,\s*full\s+([a-f0-9-]{8,})\)/i;
|
|
11
13
|
const RE_ID = /ID:\s*([a-f0-9-]+)/i;
|
|
14
|
+
const RE_FULL_ID = /Full id:\s*([a-f0-9-]{8,})/i;
|
|
15
|
+
const RE_APPROVE_CMD = /\/approve\s+([a-f0-9-]{8,})\s+(allow-once|allow-always|deny)\b/i;
|
|
12
16
|
const RE_UUID = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i;
|
|
13
17
|
const RE_SHORT_HEX = /\b([a-f0-9]{8,})\b/i;
|
|
14
18
|
const RE_GATEWAY_DENIAL = /Exec denied.*approval-timeout/i;
|
|
15
|
-
const RE_COMMAND_BLOCK = /Command:\s
|
|
19
|
+
const RE_COMMAND_BLOCK = /Command:\s*```(?:[a-z0-9_+-]+)?\n([\s\S]+?)\n```/i;
|
|
16
20
|
const RE_COMMAND_INLINE = /Command:\s*(.+)/i;
|
|
17
21
|
const RE_CWD = /CWD:\s*(.+)/i;
|
|
18
22
|
const RE_HOST = /Host:\s*(.+)/i;
|
|
@@ -20,6 +24,7 @@ const RE_AGENT = /Agent:\s*(.+)/i;
|
|
|
20
24
|
const RE_SECURITY = /Security:\s*(.+)/i;
|
|
21
25
|
const RE_ASK = /Ask:\s*(.+)/i;
|
|
22
26
|
const RE_EXPIRES = /Expires in:\s*(.+)/i;
|
|
27
|
+
const RE_PENDING_COMMAND = /Pending command:\s*`{0,3}\n?(.+?)\n?(?:`{0,3})(?:\n|$)/is;
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Parse OpenClaw's plain-text approval message into an ApprovalInfo object.
|
|
@@ -29,14 +34,32 @@ const RE_EXPIRES = /Expires in:\s*(.+)/i;
|
|
|
29
34
|
* falls back to sensible defaults for missing fields.
|
|
30
35
|
*/
|
|
31
36
|
export function parseApprovalText(text: string): ApprovalInfo | null {
|
|
32
|
-
|
|
37
|
+
const hasLegacyMarker = RE_APPROVAL_MARKER.test(text);
|
|
38
|
+
const hasNativeMarker = RE_NATIVE_APPROVAL_MARKER.test(text);
|
|
39
|
+
const toolResultMatch = text.match(RE_TOOLRESULT_APPROVAL);
|
|
40
|
+
|
|
41
|
+
let id = hasLegacyMarker ? text.match(RE_ID)?.[1]?.trim() : undefined;
|
|
42
|
+
|
|
43
|
+
if (!id && toolResultMatch) {
|
|
44
|
+
id = toolResultMatch[2]?.trim() || toolResultMatch[1]?.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!id && hasNativeMarker) {
|
|
48
|
+
id = text.match(RE_FULL_ID)?.[1]?.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// OpenClaw 2026 format often delivers plain guidance text like:
|
|
52
|
+
// `/approve <id> allow-once` (with or without markdown backticks).
|
|
53
|
+
if (!id) {
|
|
54
|
+
id = text.match(RE_APPROVE_CMD)?.[1]?.trim();
|
|
55
|
+
}
|
|
33
56
|
|
|
34
|
-
const id = text.match(RE_ID)?.[1]?.trim();
|
|
35
57
|
if (!id) return null;
|
|
36
58
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
const command = text.match(RE_COMMAND_BLOCK)?.[1]?.trim()
|
|
60
|
+
?? text.match(RE_PENDING_COMMAND)?.[1]?.trim()
|
|
61
|
+
?? text.match(RE_COMMAND_INLINE)?.[1]?.trim()
|
|
62
|
+
?? "approval-pending";
|
|
40
63
|
|
|
41
64
|
return {
|
|
42
65
|
id,
|
package/lib/approval-store.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// approval-buttons · lib/approval-store.ts
|
|
2
|
+
// telegram-approval-buttons · lib/approval-store.ts
|
|
3
3
|
// In-memory store for pending approvals with TTL-based cleanup
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
|
package/lib/diagnostics.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// approval-buttons · lib/diagnostics.ts
|
|
2
|
+
// telegram-approval-buttons · lib/diagnostics.ts
|
|
3
3
|
// Self-diagnostics: config validation, connectivity check, auto-repair
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
|
|
@@ -201,7 +201,7 @@ export function logStartupDiagnostics(
|
|
|
201
201
|
|
|
202
202
|
log.info(
|
|
203
203
|
`[diagnostics] Config OK → ${channels.join(", ")}, ` +
|
|
204
|
-
|
|
204
|
+
`staleMins=${config.staleMins}, verbose=${config.verbose}`,
|
|
205
205
|
);
|
|
206
206
|
}
|
|
207
207
|
|
package/lib/message-formatter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// approval-buttons · lib/message-formatter.ts
|
|
2
|
+
// telegram-approval-buttons · lib/message-formatter.ts
|
|
3
3
|
// HTML message formatting for Telegram (approval requests & resolutions)
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
|
package/lib/slack-api.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// approval-buttons · lib/slack-api.ts
|
|
2
|
+
// telegram-approval-buttons · lib/slack-api.ts
|
|
3
3
|
// Isolated Slack Web API wrapper — only depends on fetch (Node built-in)
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
|
|
@@ -63,7 +63,7 @@ export class SlackApi {
|
|
|
63
63
|
constructor(
|
|
64
64
|
private readonly token: string,
|
|
65
65
|
private readonly log?: Logger,
|
|
66
|
-
) {}
|
|
66
|
+
) { }
|
|
67
67
|
|
|
68
68
|
// ── Connectivity ────────────────────────────────────────────────────────
|
|
69
69
|
|
package/lib/slack-formatter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// approval-buttons · lib/slack-formatter.ts
|
|
2
|
+
// telegram-approval-buttons · lib/slack-formatter.ts
|
|
3
3
|
// Block Kit message formatting for Slack (approval requests & resolutions)
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
|
package/lib/telegram-api.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// approval-buttons · lib/telegram-api.ts
|
|
2
|
+
// telegram-approval-buttons · lib/telegram-api.ts
|
|
3
3
|
// Isolated Telegram Bot API wrapper — only depends on fetch (Node built-in)
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "approval-buttons",
|
|
3
|
-
"name": "Approval Buttons",
|
|
2
|
+
"id": "telegram-approval-buttons",
|
|
3
|
+
"name": "Telegram Approval Buttons",
|
|
4
4
|
"description": "Adds inline buttons to exec approval messages in Telegram and Slack. Tap to approve/deny without typing commands.",
|
|
5
|
-
"version": "5.
|
|
5
|
+
"version": "5.1.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
@@ -69,4 +69,4 @@
|
|
|
69
69
|
"advanced": true
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
}
|
|
72
|
+
}
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
-
// approval-buttons · types.ts
|
|
2
|
+
// telegram-approval-buttons · types.ts
|
|
3
3
|
// Shared TypeScript interfaces for the plugin
|
|
4
4
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
5
|
|