polygram 0.8.0-rc.38 → 0.8.0-rc.39
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/parse-response.js +76 -11
- package/package.json +1 -1
- package/polygram.js +25 -0
|
@@ -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.39",
|
|
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",
|
package/lib/parse-response.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* - sticker (single emoji that maps to a sticker, OR literal
|
|
4
4
|
* `[sticker:NAME]` mimic — see below)
|
|
5
5
|
* - reaction (single emoji not mapped to a sticker)
|
|
6
|
-
* - text (everything else
|
|
6
|
+
* - text (everything else, with inline `[sticker:NAME]` markers
|
|
7
|
+
* extracted into a parallel `stickers[]` array)
|
|
7
8
|
*
|
|
8
9
|
* Why this lives in lib/: polygram.js is a top-level script (calls main()
|
|
9
10
|
* at bottom) and can't be require()'d from a test without starting a bot.
|
|
@@ -19,38 +20,102 @@
|
|
|
19
20
|
* the placeholder ended up rendered in the user's chat instead of an
|
|
20
21
|
* actual sticker.
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* 0.8.0-rc.39 (item: inline sticker regression):
|
|
24
|
+
* Claude evolved to use `[sticker:NAME]` INLINE within longer replies
|
|
25
|
+
* (e.g. "Done! [sticker:pumped]\n\nStripe Mar 2026 created ✅\n…") —
|
|
26
|
+
* not as a solo response. The 0.7.5 fix only handled the solo case
|
|
27
|
+
* (full text = tag), so inline tags leaked through the text path
|
|
28
|
+
* verbatim. Now we extract every recognised inline tag, strip it
|
|
29
|
+
* from the text, and surface them in a `stickers[]` array on the
|
|
30
|
+
* result. polygram.js sends the cleaned text first, then each
|
|
31
|
+
* sticker in order. Unknown sticker names still pass through as
|
|
32
|
+
* literal text (someone may genuinely write that string).
|
|
33
|
+
*
|
|
34
|
+
* Match shape: `[sticker:` NAME `]` where NAME is `[A-Za-z0-9_-]+`.
|
|
35
|
+
* The solo-form regex anchors with `^\s*…\s*$`; the inline-form
|
|
36
|
+
* regex is unanchored and global. Both share the same NAME charset.
|
|
27
37
|
*/
|
|
28
38
|
|
|
39
|
+
'use strict';
|
|
40
|
+
|
|
29
41
|
const STICKER_TAG_RE = /^\s*\[sticker:([A-Za-z0-9_-]+)\]\s*$/;
|
|
42
|
+
const STICKER_TAG_INLINE_RE = /\[sticker:([A-Za-z0-9_-]+)\]/g;
|
|
30
43
|
|
|
31
44
|
function parseResponse(text, { stickerMap = {}, emojiToSticker = {} } = {}) {
|
|
32
45
|
const trimmed = (text || '').trim();
|
|
33
46
|
|
|
47
|
+
// Solo-sticker path: entire response is just the tag.
|
|
34
48
|
const tagMatch = trimmed.match(STICKER_TAG_RE);
|
|
35
49
|
if (tagMatch) {
|
|
36
50
|
const name = tagMatch[1];
|
|
37
51
|
const fileId = stickerMap[name];
|
|
38
52
|
if (fileId) {
|
|
39
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
text: '',
|
|
55
|
+
sticker: fileId,
|
|
56
|
+
stickerLabel: name,
|
|
57
|
+
reaction: null,
|
|
58
|
+
stickers: [],
|
|
59
|
+
};
|
|
40
60
|
}
|
|
41
61
|
}
|
|
42
62
|
|
|
63
|
+
// Solo-emoji shortcuts (single emoji → sticker if mapped, else reaction).
|
|
43
64
|
const emojiOnly = /^\p{Emoji_Presentation}$/u.test(trimmed)
|
|
44
65
|
|| /^\p{Emoji}️?$/u.test(trimmed);
|
|
45
66
|
|
|
46
67
|
if (emojiOnly && trimmed) {
|
|
47
68
|
if (emojiToSticker[trimmed]) {
|
|
48
|
-
return {
|
|
69
|
+
return {
|
|
70
|
+
text: '',
|
|
71
|
+
sticker: emojiToSticker[trimmed],
|
|
72
|
+
stickerLabel: trimmed,
|
|
73
|
+
reaction: null,
|
|
74
|
+
stickers: [],
|
|
75
|
+
};
|
|
49
76
|
}
|
|
50
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
text: '',
|
|
79
|
+
sticker: null,
|
|
80
|
+
stickerLabel: null,
|
|
81
|
+
reaction: trimmed,
|
|
82
|
+
stickers: [],
|
|
83
|
+
};
|
|
51
84
|
}
|
|
52
85
|
|
|
53
|
-
|
|
86
|
+
// Inline sticker extraction. Walk every `[sticker:NAME]` in the text;
|
|
87
|
+
// for each NAME present in stickerMap, push to `stickers[]` and remove
|
|
88
|
+
// it from the cleaned text. Unknown NAMEs stay verbatim (someone may
|
|
89
|
+
// genuinely write that string in a message).
|
|
90
|
+
//
|
|
91
|
+
// Whitespace handling: replacing a tag with the empty string can leave
|
|
92
|
+
// a trailing space on its line ("Done! [sticker:x]" → "Done! ") or
|
|
93
|
+
// stack newlines if the tag stood alone on a line. We strip trailing
|
|
94
|
+
// whitespace per-line and collapse runs of 3+ blank lines to 2. We do
|
|
95
|
+
// NOT touch intra-line spacing or code-block indentation.
|
|
96
|
+
const stickers = [];
|
|
97
|
+
const cleaned = trimmed.replace(STICKER_TAG_INLINE_RE, (match, name) => {
|
|
98
|
+
const fileId = stickerMap[name];
|
|
99
|
+
if (fileId) {
|
|
100
|
+
stickers.push({ fileId, name });
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
return match;
|
|
104
|
+
});
|
|
105
|
+
const tidied = cleaned
|
|
106
|
+
.split('\n')
|
|
107
|
+
.map((line) => line.replace(/[ \t]+$/g, ''))
|
|
108
|
+
.join('\n')
|
|
109
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
110
|
+
.trim();
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
text: tidied,
|
|
114
|
+
sticker: null,
|
|
115
|
+
stickerLabel: null,
|
|
116
|
+
reaction: null,
|
|
117
|
+
stickers,
|
|
118
|
+
};
|
|
54
119
|
}
|
|
55
120
|
|
|
56
|
-
module.exports = { parseResponse, STICKER_TAG_RE };
|
|
121
|
+
module.exports = { parseResponse, STICKER_TAG_RE, STICKER_TAG_INLINE_RE };
|
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.39",
|
|
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
|
@@ -2737,6 +2737,28 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2737
2737
|
const parsed = parseResponse(result.text);
|
|
2738
2738
|
const outMeta = { ...outMetaBase, sessionId: result.sessionId, costUsd: result.cost };
|
|
2739
2739
|
|
|
2740
|
+
// 0.8.0-rc.39: send any inline stickers Claude embedded with
|
|
2741
|
+
// `[sticker:NAME]` markers (parseResponse stripped them from
|
|
2742
|
+
// parsed.text and surfaced them in parsed.stickers[]). Send AFTER
|
|
2743
|
+
// the text reply lands so the sticker reads as punctuation on the
|
|
2744
|
+
// message, not as a leading icon. Failures are logged but never
|
|
2745
|
+
// block the rest of the reply — a missing sticker is a soft UX
|
|
2746
|
+
// miss, not a turn failure.
|
|
2747
|
+
const sendInlineStickers = async () => {
|
|
2748
|
+
if (!parsed.stickers || parsed.stickers.length === 0) return;
|
|
2749
|
+
for (const s of parsed.stickers) {
|
|
2750
|
+
try {
|
|
2751
|
+
await tg(bot, 'sendSticker', {
|
|
2752
|
+
chat_id: chatId,
|
|
2753
|
+
sticker: s.fileId,
|
|
2754
|
+
...(threadId && { message_thread_id: threadId }),
|
|
2755
|
+
}, { ...outMeta, stickerName: s.name, source: 'inline-sticker' });
|
|
2756
|
+
} catch (err) {
|
|
2757
|
+
console.error(`[${label}] inline sendSticker(${s.name}) failed: ${err.message}`);
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
};
|
|
2761
|
+
|
|
2740
2762
|
// OpenClaw's preview-becomes-final flow:
|
|
2741
2763
|
//
|
|
2742
2764
|
// 1. flushDraft() — drain any pending throttled edit so the
|
|
@@ -2757,6 +2779,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2757
2779
|
if (fin.finalEditOk) {
|
|
2758
2780
|
// Preview was successfully edited to the final text.
|
|
2759
2781
|
// No follow-up messages needed.
|
|
2782
|
+
await sendInlineStickers();
|
|
2760
2783
|
await cleanupArchivedBubbles();
|
|
2761
2784
|
console.log(`[${label}] ${elapsed}s | ${result.text.length} chars | streamed | ${chatConfig.model}/${chatConfig.effort} | $${result.cost?.toFixed(4) || '?'}`);
|
|
2762
2785
|
markReplied();
|
|
@@ -2802,6 +2825,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2802
2825
|
console.error(`[${label}] partial-delivery warning failed: ${warnErr.message}`);
|
|
2803
2826
|
}
|
|
2804
2827
|
}
|
|
2828
|
+
await sendInlineStickers();
|
|
2805
2829
|
await cleanupArchivedBubbles();
|
|
2806
2830
|
console.log(`[${label}] ${elapsed}s | ${result.text.length} chars | streamed-redeliver(${reason}, ${chunks.length} chunks${r.failed.length ? `, ${r.failed.length} failed` : ''}) | ${chatConfig.model}/${chatConfig.effort} | $${result.cost?.toFixed(4) || '?'}`);
|
|
2807
2831
|
markReplied();
|
|
@@ -2844,6 +2868,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2844
2868
|
});
|
|
2845
2869
|
}
|
|
2846
2870
|
|
|
2871
|
+
await sendInlineStickers();
|
|
2847
2872
|
console.log(`[${label}] ${elapsed}s | ${result.text.length} chars | ${chatConfig.model}/${chatConfig.effort} | $${result.cost?.toFixed(4) || '?'}`);
|
|
2848
2873
|
markReplied();
|
|
2849
2874
|
} catch (err) {
|