polygram 0.7.1 → 0.7.3
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/README.md +1 -1
- package/config.example.json +2 -0
- package/lib/stream-reply.js +18 -0
- package/package.json +1 -1
- package/polygram.js +67 -11
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.3",
|
|
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/README.md
CHANGED
|
@@ -364,7 +364,7 @@ foreign-chat clicks are rejected. Default-deny on IPC error.
|
|
|
364
364
|
## Development
|
|
365
365
|
|
|
366
366
|
```bash
|
|
367
|
-
npm test #
|
|
367
|
+
npm test # 643 tests, 158 suites, node:test, no external services
|
|
368
368
|
npm run coverage # native test coverage (Node 22+, no devDeps)
|
|
369
369
|
npm start -- --bot my-bot
|
|
370
370
|
npm run split-db -- --config config.json --dry-run
|
package/config.example.json
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"attachmentConcurrency": 6,
|
|
16
16
|
"queueWarnThreshold": 20,
|
|
17
17
|
"replayWindowMs": 180000,
|
|
18
|
+
"_comment_chrome": "Opt-in to Claude Code's Chrome-extension browser-automation integration. Default false. Requires the 'Claude in Chrome' extension installed in the daemon-user's Chrome browser AND a live GUI session (Chrome must be running). See https://code.claude.com/docs/en/chrome. Per-chat override via `config.chats.<id>.chrome`.",
|
|
19
|
+
"chrome": false,
|
|
18
20
|
"_comment_pairedChatDefaults": "When a user sends /pair <CODE> from a private chat that isn't in config.chats yet, polygram auto-creates a chat entry using these defaults (merged over top-level `defaults`). Leave out `cwd` at your peril — without it, auto-onboarded DMs have no working directory and pairing will fail.",
|
|
19
21
|
"pairedChatDefaults": {
|
|
20
22
|
"agent": "admin",
|
package/lib/stream-reply.js
CHANGED
|
@@ -57,6 +57,13 @@ function createStreamer({
|
|
|
57
57
|
let lastEditTs = 0;
|
|
58
58
|
let pendingEdit = null; // timer id
|
|
59
59
|
let flushPromise = null; // ongoing edit promise (for back-pressure)
|
|
60
|
+
// 0.7.2: msg_ids of bubbles that have been superseded by
|
|
61
|
+
// forceNewMessage(). The caller (polygram.js handleMessage at
|
|
62
|
+
// end-of-turn) reads getArchived() and issues deleteMessage on
|
|
63
|
+
// each — matches OpenClaw's archivedAnswerPreviews cleanup so
|
|
64
|
+
// the user sees only the final answer's bubble, not every
|
|
65
|
+
// "thinking out loud" intermediate from a tool-heavy turn.
|
|
66
|
+
const archived = [];
|
|
60
67
|
|
|
61
68
|
// LIVE-EDIT truncation only — used during streaming when latestText
|
|
62
69
|
// overshoots maxLen. The trailing "..." signals to the user that more
|
|
@@ -148,6 +155,11 @@ function createStreamer({
|
|
|
148
155
|
if (pendingEdit) { cancel(pendingEdit); pendingEdit = null; }
|
|
149
156
|
// Don't await flushPromise — the caller has decided to start a new
|
|
150
157
|
// message; whatever the old bubble shows is "done".
|
|
158
|
+
// 0.7.2: track the previous bubble's msgId for end-of-turn cleanup.
|
|
159
|
+
// Without this, every intermediate "thinking out loud" assistant
|
|
160
|
+
// message in a tool-heavy turn leaves a permanent bubble in the
|
|
161
|
+
// chat — the user wants only the final answer's bubble visible.
|
|
162
|
+
if (msgId != null) archived.push(msgId);
|
|
151
163
|
msgId = null;
|
|
152
164
|
currentText = '';
|
|
153
165
|
latestText = '';
|
|
@@ -239,6 +251,11 @@ function createStreamer({
|
|
|
239
251
|
}
|
|
240
252
|
}
|
|
241
253
|
|
|
254
|
+
// 0.7.2: snapshot of bubble msgIds that forceNewMessage() superseded.
|
|
255
|
+
// Returns a copy so callers can't mutate internal state. polygram.js
|
|
256
|
+
// reads this at end-of-turn and issues deleteMessage on each.
|
|
257
|
+
function getArchived() { return archived.slice(); }
|
|
258
|
+
|
|
242
259
|
return {
|
|
243
260
|
onChunk,
|
|
244
261
|
finalize,
|
|
@@ -246,6 +263,7 @@ function createStreamer({
|
|
|
246
263
|
forceNewMessage,
|
|
247
264
|
discard,
|
|
248
265
|
archive,
|
|
266
|
+
getArchived,
|
|
249
267
|
// Introspection for tests:
|
|
250
268
|
get state() { return state; },
|
|
251
269
|
get msgId() { return msgId; },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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
|
@@ -640,6 +640,19 @@ let pm = null; // ProcessManager, created in main()
|
|
|
640
640
|
|
|
641
641
|
function spawnClaude(sessionKey, ctx) {
|
|
642
642
|
const { chatConfig, existingSessionId, label, chatId } = ctx;
|
|
643
|
+
// 0.7.3: Claude Code's Chrome-extension integration (browser
|
|
644
|
+
// automation via the "Claude in Chrome" extension) is OPT-IN and
|
|
645
|
+
// NOT enabled by default in `claude`. Polygram lets chats turn it
|
|
646
|
+
// on via `config.chats.<id>.chrome: true` (chat-level wins) or
|
|
647
|
+
// `config.bot.chrome: true` (per-bot default). When opting in, the
|
|
648
|
+
// extension must be installed in the daemon-user's Chrome and the
|
|
649
|
+
// user must have a live Aqua session (so Chrome is running). Falls
|
|
650
|
+
// back to --no-chrome for chats that don't opt in (matches our
|
|
651
|
+
// pre-0.7.3 default — defensive against any "enabled by default"
|
|
652
|
+
// that might have been set in claude's persistent state).
|
|
653
|
+
const wantChrome = chatConfig.chrome != null
|
|
654
|
+
? chatConfig.chrome === true
|
|
655
|
+
: config.bot?.chrome === true;
|
|
643
656
|
const args = [
|
|
644
657
|
'-p',
|
|
645
658
|
'--input-format', 'stream-json',
|
|
@@ -648,7 +661,7 @@ function spawnClaude(sessionKey, ctx) {
|
|
|
648
661
|
'--model', chatConfig.model || config.defaults.model,
|
|
649
662
|
'--effort', chatConfig.effort || config.defaults.effort,
|
|
650
663
|
'--permission-mode', 'bypassPermissions',
|
|
651
|
-
'--no-chrome',
|
|
664
|
+
wantChrome ? '--chrome' : '--no-chrome',
|
|
652
665
|
];
|
|
653
666
|
if (chatConfig.agent) args.push('--agent', chatConfig.agent);
|
|
654
667
|
if (existingSessionId) args.push('--resume', existingSessionId);
|
|
@@ -1664,21 +1677,34 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1664
1677
|
...(linkPreview === false ? { linkPreview: false } : {}),
|
|
1665
1678
|
};
|
|
1666
1679
|
|
|
1680
|
+
// 0.7.2: only the FIRST bubble in a turn quotes the user's message
|
|
1681
|
+
// via reply_parameters. When a tool-heavy turn produces multiple
|
|
1682
|
+
// assistant messages (each spawning its own bubble via
|
|
1683
|
+
// forceNewMessage), subsequent bubbles shouldn't re-quote the user
|
|
1684
|
+
// — the chat would show N copies of the same quoted message stacked
|
|
1685
|
+
// vertically. After the first send, the flag flips and subsequent
|
|
1686
|
+
// initial-sends omit reply_parameters.
|
|
1687
|
+
let firstBubbleSent = false;
|
|
1667
1688
|
// Streaming is unconditional as of 0.4.0 — matches OpenClaw's model and
|
|
1668
1689
|
// eliminates the "stuck at 15min typing" complaint from the non-streaming
|
|
1669
1690
|
// code path. For short responses the streamer stays idle and we fall
|
|
1670
1691
|
// through to the normal send path via finalize() returning streamed=false.
|
|
1671
1692
|
const streamer = createStreamer({
|
|
1672
|
-
send: async (text) =>
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1693
|
+
send: async (text) => {
|
|
1694
|
+
const params = {
|
|
1695
|
+
chat_id: chatId, text,
|
|
1696
|
+
...(threadId && { message_thread_id: threadId }),
|
|
1697
|
+
};
|
|
1698
|
+
if (!firstBubbleSent) {
|
|
1699
|
+
// allow_sending_without_reply: long-running turns give the user
|
|
1700
|
+
// plenty of time to delete their original message. Without this
|
|
1701
|
+
// flag, Telegram rejects the reply with MESSAGE_NOT_FOUND and the
|
|
1702
|
+
// whole streamed answer is lost.
|
|
1703
|
+
params.reply_parameters = { message_id: msg.message_id, allow_sending_without_reply: true };
|
|
1704
|
+
firstBubbleSent = true;
|
|
1705
|
+
}
|
|
1706
|
+
return tg(bot, 'sendMessage', params, outMetaBase);
|
|
1707
|
+
},
|
|
1682
1708
|
edit: async (messageId, text) => {
|
|
1683
1709
|
try {
|
|
1684
1710
|
// Route edits through tg() so applyFormatting runs (MarkdownV2
|
|
@@ -1725,6 +1751,34 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1725
1751
|
});
|
|
1726
1752
|
// streamer is registered with this turn via pm.send's context (below)
|
|
1727
1753
|
|
|
1754
|
+
// 0.7.2: clean up bubbles superseded by forceNewMessage() — the
|
|
1755
|
+
// intermediate "thinking out loud" assistant messages that fired in
|
|
1756
|
+
// a tool-heavy turn. Without this, every tool-result cycle leaves a
|
|
1757
|
+
// permanent bubble in the chat (see the screenshot from the post-
|
|
1758
|
+
// 0.7.1 deploy where six bubbles appeared for one logical turn).
|
|
1759
|
+
// Matches OpenClaw's archivedAnswerPreviews end-of-turn cleanup.
|
|
1760
|
+
// Call AFTER finalize/discard decisions so we never delete the
|
|
1761
|
+
// bubble that's the final reply.
|
|
1762
|
+
async function cleanupArchivedBubbles() {
|
|
1763
|
+
const archived = streamer.getArchived?.() || [];
|
|
1764
|
+
if (archived.length === 0) return;
|
|
1765
|
+
for (const messageId of archived) {
|
|
1766
|
+
try {
|
|
1767
|
+
await tg(bot, 'deleteMessage', {
|
|
1768
|
+
chat_id: chatId, message_id: messageId,
|
|
1769
|
+
}, { source: 'bot-reply-archived-cleanup', botName: BOT_NAME });
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
// Non-fatal — message may be >48h old or already gone.
|
|
1772
|
+
// Operator-visible only via the events table.
|
|
1773
|
+
console.error(`[${label}] archived-cleanup ${messageId}: ${err.message}`);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
logEvent('telegram-archived-cleanup', {
|
|
1777
|
+
chat_id: chatId, msg_id: msg.message_id, count: archived.length,
|
|
1778
|
+
bot: BOT_NAME,
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1728
1782
|
// Status reactions on the user's message: 👀 queued → 🤔 thinking →
|
|
1729
1783
|
// 👨💻 coding / ⚡ web / 🔥 tool → 👍 done / 🤯 error. Silent (no
|
|
1730
1784
|
// notifications), updates in place, one emoji per message. Uses
|
|
@@ -1853,6 +1907,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1853
1907
|
if (fin.finalEditOk) {
|
|
1854
1908
|
// Preview was successfully edited to the final text.
|
|
1855
1909
|
// No follow-up messages needed.
|
|
1910
|
+
await cleanupArchivedBubbles();
|
|
1856
1911
|
console.log(`[${label}] ${elapsed}s | ${result.text.length} chars | streamed | ${chatConfig.model}/${chatConfig.effort} | $${result.cost?.toFixed(4) || '?'}`);
|
|
1857
1912
|
markReplied();
|
|
1858
1913
|
return;
|
|
@@ -1897,6 +1952,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1897
1952
|
console.error(`[${label}] partial-delivery warning failed: ${warnErr.message}`);
|
|
1898
1953
|
}
|
|
1899
1954
|
}
|
|
1955
|
+
await cleanupArchivedBubbles();
|
|
1900
1956
|
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) || '?'}`);
|
|
1901
1957
|
markReplied();
|
|
1902
1958
|
return;
|