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.
@@ -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.38",
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",
@@ -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
- * Match shape: optional whitespace, `[sticker:`, NAME (alnum/_/-), `]`,
23
- * optional whitespace. NAME must resolve in the supplied stickerMap;
24
- * unknown NAMEs fall through to the text path so a genuine
25
- * "[sticker:foo]" message (e.g. someone joking, or a stale name from an
26
- * older deploy) still reaches the user verbatim.
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 { text: '', sticker: fileId, stickerLabel: name, reaction: null };
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 { text: '', sticker: emojiToSticker[trimmed], stickerLabel: trimmed, reaction: null };
69
+ return {
70
+ text: '',
71
+ sticker: emojiToSticker[trimmed],
72
+ stickerLabel: trimmed,
73
+ reaction: null,
74
+ stickers: [],
75
+ };
49
76
  }
50
- return { text: '', sticker: null, stickerLabel: null, reaction: trimmed };
77
+ return {
78
+ text: '',
79
+ sticker: null,
80
+ stickerLabel: null,
81
+ reaction: trimmed,
82
+ stickers: [],
83
+ };
51
84
  }
52
85
 
53
- return { text: trimmed, sticker: null, stickerLabel: null, reaction: null };
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.38",
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) {