polygram 0.8.0-rc.19 → 0.8.0-rc.20
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/.claude-plugin/plugin.json +1 -1
- package/lib/approval-ui.js +135 -0
- package/package.json +1 -1
- package/polygram.js +10 -67
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.8.0-rc.
|
|
4
|
+
"version": "0.8.0-rc.20",
|
|
5
5
|
"description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Multi-bot, multi-chat, per-topic isolation; SQLite transcripts; inline-keyboard approvals. Bundles /polygram:status|logs|pair-code|approvals admin commands and a history skill.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"telegram",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure UI builders for the approval flow's Telegram surface.
|
|
3
|
+
*
|
|
4
|
+
* - 2-button keyboard (CLI pm IPC approval-hook flow)
|
|
5
|
+
* - 4-button keyboard (rc.6 SDK pm canUseTool flow with persisted
|
|
6
|
+
* "Always allow / Always deny" via chat_tool_decisions)
|
|
7
|
+
* - Card text with friendly heading + clipped tool_input body
|
|
8
|
+
*
|
|
9
|
+
* No runtime dependencies — these are pure transforms suitable for
|
|
10
|
+
* unit-testing in isolation. The polygram.js side wires them to
|
|
11
|
+
* `tg(bot, 'sendMessage', ...)` / `editMessageText`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 2-button keyboard for the legacy IPC approval flow.
|
|
18
|
+
* @param {number|string} approvalId
|
|
19
|
+
* @param {string} token
|
|
20
|
+
*/
|
|
21
|
+
function buildApprovalKeyboard(approvalId, token) {
|
|
22
|
+
return {
|
|
23
|
+
inline_keyboard: [[
|
|
24
|
+
{ text: '✅ Approve', callback_data: `approve:${approvalId}:${token}` },
|
|
25
|
+
{ text: '❌ Deny', callback_data: `deny:${approvalId}:${token}` },
|
|
26
|
+
]],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 4-button keyboard for the SDK canUseTool flow (rc.6 Phase 2 step 6).
|
|
32
|
+
* "Always allow" / "Always deny" rows persist the decision into
|
|
33
|
+
* `chat_tool_decisions` so subsequent invocations of the same tool
|
|
34
|
+
* with the same input short-circuit.
|
|
35
|
+
*
|
|
36
|
+
* Callback_data conventions:
|
|
37
|
+
* approve:<id>:<token> — one-time allow
|
|
38
|
+
* deny:<id>:<token> — one-time deny
|
|
39
|
+
* approve-always:<id>:<token> — allow + persist
|
|
40
|
+
* deny-always:<id>:<token> — deny + persist
|
|
41
|
+
*
|
|
42
|
+
* @param {number|string} approvalId
|
|
43
|
+
* @param {string} token
|
|
44
|
+
*/
|
|
45
|
+
function buildApprovalKeyboardWithAlways(approvalId, token) {
|
|
46
|
+
return {
|
|
47
|
+
inline_keyboard: [
|
|
48
|
+
[
|
|
49
|
+
{ text: '✅ Approve', callback_data: `approve:${approvalId}:${token}` },
|
|
50
|
+
{ text: '❌ Deny', callback_data: `deny:${approvalId}:${token}` },
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
{ text: '🔁 Always allow', callback_data: `approve-always:${approvalId}:${token}` },
|
|
54
|
+
{ text: '🚫 Always deny', callback_data: `deny-always:${approvalId}:${token}` },
|
|
55
|
+
],
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Format a tool_input value for the inline-keyboard card body.
|
|
62
|
+
* Clips aggressively so the whole card stays under Telegram's
|
|
63
|
+
* 4096-char limit (approval card has surrounding metadata too).
|
|
64
|
+
*
|
|
65
|
+
* @param {unknown} input — string OR any JSON-able object
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function formatToolInputForCard(input) {
|
|
69
|
+
let s;
|
|
70
|
+
try {
|
|
71
|
+
s = typeof input === 'string' ? input : JSON.stringify(input, null, 2);
|
|
72
|
+
} catch {
|
|
73
|
+
s = String(input);
|
|
74
|
+
}
|
|
75
|
+
// JSON.stringify(undefined) returns undefined, and objects with
|
|
76
|
+
// a circular toJSON could surface odd values too. Fall back to
|
|
77
|
+
// String() so we always operate on a real string.
|
|
78
|
+
if (typeof s !== 'string') s = String(input);
|
|
79
|
+
if (s.length <= 1200) return s;
|
|
80
|
+
return s.slice(0, 900) + '\n…[clipped]…\n' + s.slice(-200);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Approval card text. Plain-text only (NO parse_mode) — tool_input
|
|
85
|
+
* originates from Claude and could contain Markdown specials or
|
|
86
|
+
* tg:// links crafted for phishing.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} row — approval row from the approvals store
|
|
89
|
+
* @param {string} row.tool_name
|
|
90
|
+
* @param {number|string|null} row.turn_id
|
|
91
|
+
* @param {string} row.requester_chat_id
|
|
92
|
+
* @param {object|string|null} [row.tool_input_json]
|
|
93
|
+
* @param {object|string|null} [row.tool_input] — alias for tool_input_json
|
|
94
|
+
* @param {number} row.timeout_ts — unix ms when the row expires
|
|
95
|
+
* @param {object} [opts]
|
|
96
|
+
* @param {string} [opts.resolvedBy] — heading override for resolved cards
|
|
97
|
+
* (e.g. "✓ Approved by ivan").
|
|
98
|
+
* When set, footer is dropped.
|
|
99
|
+
* When unset, heading is "Approval needed — <tool>"
|
|
100
|
+
* and footer shows seconds-to-expire.
|
|
101
|
+
* @param {() => number} [opts.now] — clock injection for tests
|
|
102
|
+
*
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
function approvalCardText(row, opts = {}) {
|
|
106
|
+
const now = (typeof opts.now === 'function' ? opts.now : Date.now)();
|
|
107
|
+
const heading = opts.resolvedBy
|
|
108
|
+
? opts.resolvedBy
|
|
109
|
+
: `Approval needed — ${row.tool_name}`;
|
|
110
|
+
// tool_input may arrive as a parsed object OR a JSON string under
|
|
111
|
+
// either key name depending on the call site.
|
|
112
|
+
const inputSource = row.tool_input_json !== undefined
|
|
113
|
+
? row.tool_input_json
|
|
114
|
+
: row.tool_input;
|
|
115
|
+
const parsed = typeof inputSource === 'string'
|
|
116
|
+
? safeParse(inputSource)
|
|
117
|
+
: inputSource;
|
|
118
|
+
const body = formatToolInputForCard(parsed);
|
|
119
|
+
const ttl = Math.max(0, Math.round((row.timeout_ts - now) / 1000));
|
|
120
|
+
const footer = opts.resolvedBy ? '' : `\n\n⏱ expires in ${ttl}s`;
|
|
121
|
+
return `${heading}\nChat: ${row.requester_chat_id}\nTurn: ${row.turn_id || '-'}\n\n${body}${footer}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function safeParse(s) {
|
|
125
|
+
try { return JSON.parse(s); } catch { return s; }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
buildApprovalKeyboard,
|
|
130
|
+
buildApprovalKeyboardWithAlways,
|
|
131
|
+
formatToolInputForCard,
|
|
132
|
+
approvalCardText,
|
|
133
|
+
// Internals exposed for tests
|
|
134
|
+
_safeParse: safeParse,
|
|
135
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.8.0-rc.
|
|
3
|
+
"version": "0.8.0-rc.20",
|
|
4
4
|
"description": "Telegram daemon for Claude Code that preserves the OpenClaw per-chat session model. Migration path for OpenClaw users moving to Claude Code.",
|
|
5
5
|
"main": "lib/ipc-client.js",
|
|
6
6
|
"bin": {
|
package/polygram.js
CHANGED
|
@@ -34,6 +34,12 @@ const { ProcessManagerSdk } = require('./lib/process-manager-sdk');
|
|
|
34
34
|
const { createAutosteerBuffer, makePostToolBatchHook } = require('./lib/autosteer-buffer');
|
|
35
35
|
const { makeRouterPolicy, createPmRouter } = require('./lib/pm-router');
|
|
36
36
|
const { canonicalizeToolInput } = require('./lib/canonical-json');
|
|
37
|
+
const {
|
|
38
|
+
buildApprovalKeyboard,
|
|
39
|
+
buildApprovalKeyboardWithAlways,
|
|
40
|
+
formatToolInputForCard,
|
|
41
|
+
approvalCardText,
|
|
42
|
+
} = require('./lib/approval-ui');
|
|
37
43
|
const agentLoader = require('./lib/agent-loader');
|
|
38
44
|
const USE_SDK = process.env.POLYGRAM_USE_SDK === '1';
|
|
39
45
|
const { createSender } = require('./lib/telegram');
|
|
@@ -1232,51 +1238,9 @@ async function handleSendOverIpc(req) {
|
|
|
1232
1238
|
}
|
|
1233
1239
|
|
|
1234
1240
|
// ─── Approvals ─────────────────────────────────────────────────────
|
|
1235
|
-
|
|
1236
|
-
//
|
|
1237
|
-
//
|
|
1238
|
-
function formatToolInputForCard(input) {
|
|
1239
|
-
let s;
|
|
1240
|
-
try { s = typeof input === 'string' ? input : JSON.stringify(input, null, 2); }
|
|
1241
|
-
catch { s = String(input); }
|
|
1242
|
-
if (s.length <= 1200) return s;
|
|
1243
|
-
return s.slice(0, 900) + '\n…[clipped]…\n' + s.slice(-200);
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
function buildApprovalKeyboard(approvalId, token) {
|
|
1247
|
-
return {
|
|
1248
|
-
inline_keyboard: [[
|
|
1249
|
-
{ text: '✅ Approve', callback_data: `approve:${approvalId}:${token}` },
|
|
1250
|
-
{ text: '❌ Deny', callback_data: `deny:${approvalId}:${token}` },
|
|
1251
|
-
]],
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
// 0.8.0 Phase 2 step 6: 4-button approval keyboard for SDK canUseTool
|
|
1256
|
-
// flow. Adds "Always allow" and "Always deny" rows that persist the
|
|
1257
|
-
// decision into chat_tool_decisions (via callback_query handler),
|
|
1258
|
-
// so subsequent invocations of the same tool with the same input
|
|
1259
|
-
// short-circuit without prompting.
|
|
1260
|
-
//
|
|
1261
|
-
// Callback_data conventions:
|
|
1262
|
-
// approve:<id>:<token> — one-time allow
|
|
1263
|
-
// deny:<id>:<token> — one-time deny
|
|
1264
|
-
// approve-always:<id>:<token> — allow + persist
|
|
1265
|
-
// deny-always:<id>:<token> — deny + persist
|
|
1266
|
-
function buildApprovalKeyboardWithAlways(approvalId, token) {
|
|
1267
|
-
return {
|
|
1268
|
-
inline_keyboard: [
|
|
1269
|
-
[
|
|
1270
|
-
{ text: '✅ Approve', callback_data: `approve:${approvalId}:${token}` },
|
|
1271
|
-
{ text: '❌ Deny', callback_data: `deny:${approvalId}:${token}` },
|
|
1272
|
-
],
|
|
1273
|
-
[
|
|
1274
|
-
{ text: '🔁 Always allow', callback_data: `approve-always:${approvalId}:${token}` },
|
|
1275
|
-
{ text: '🚫 Always deny', callback_data: `deny-always:${approvalId}:${token}` },
|
|
1276
|
-
],
|
|
1277
|
-
],
|
|
1278
|
-
};
|
|
1279
|
-
}
|
|
1241
|
+
// rc.20: pure UI builders moved to lib/approval-ui.js for testability.
|
|
1242
|
+
// Imported above (buildApprovalKeyboard, buildApprovalKeyboardWithAlways,
|
|
1243
|
+
// approvalCardText, formatToolInputForCard).
|
|
1280
1244
|
|
|
1281
1245
|
// /model and /effort inline keyboard. `show` controls which row(s) appear:
|
|
1282
1246
|
// 'model', 'effort', or 'all'. The current value gets a ✓ marker so the
|
|
@@ -1345,28 +1309,7 @@ function formatConfigInfoText(chatConfig, show, sessionKey) {
|
|
|
1345
1309
|
return body;
|
|
1346
1310
|
}
|
|
1347
1311
|
|
|
1348
|
-
|
|
1349
|
-
// No parse_mode is used on this card — tool_name/turn_id/tool_input
|
|
1350
|
-
// originate from the Claude subprocess and could contain Markdown special
|
|
1351
|
-
// chars or tg:// links crafted for phishing. Plain text renders as-is.
|
|
1352
|
-
const heading = opts.resolvedBy
|
|
1353
|
-
? opts.resolvedBy
|
|
1354
|
-
: `Approval needed — ${row.tool_name}`;
|
|
1355
|
-
const body = formatToolInputForCard(
|
|
1356
|
-
typeof row.tool_input_json === 'string'
|
|
1357
|
-
? safeParse(row.tool_input_json)
|
|
1358
|
-
: row.tool_input_json,
|
|
1359
|
-
);
|
|
1360
|
-
const ttl = Math.max(0, Math.round((row.timeout_ts - Date.now()) / 1000));
|
|
1361
|
-
const footer = opts.resolvedBy
|
|
1362
|
-
? ''
|
|
1363
|
-
: `\n\n⏱ expires in ${ttl}s`;
|
|
1364
|
-
return `${heading}\nChat: ${row.requester_chat_id}\nTurn: ${row.turn_id || '-'}\n\n${body}${footer}`;
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
function safeParse(s) {
|
|
1368
|
-
try { return JSON.parse(s); } catch { return s; }
|
|
1369
|
-
}
|
|
1312
|
+
// rc.20: approvalCardText + safeParse moved to lib/approval-ui.js.
|
|
1370
1313
|
|
|
1371
1314
|
// 0.8.0-rc.18+: canonicalizeToolInput moved to lib/canonical-json.js
|
|
1372
1315
|
// for testability. Same function, no behavior change.
|