polygram 0.8.0-rc.1 → 0.8.0-rc.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/lib/attachments.js +7 -9
- package/package.json +1 -1
- package/polygram.js +51 -17
|
@@ -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.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/lib/attachments.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Attachment filter — caps
|
|
2
|
+
* Attachment filter — caps total size + per-file size + MIME allowlist.
|
|
3
3
|
* Rejected items return a human-readable reason that we surface to the
|
|
4
4
|
* user and log to the events table.
|
|
5
|
+
*
|
|
6
|
+
* No count cap: per-file (10 MB) and total-size (20 MB) bound resource
|
|
7
|
+
* usage already; an additional count limit just produces "skipped: max
|
|
8
|
+
* count" surprises on Telegram albums (up to 10 photos in one send).
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
|
-
const MAX_COUNT = 5;
|
|
8
11
|
const MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|
9
12
|
const MAX_TOTAL_BYTES = 20 * 1024 * 1024;
|
|
10
13
|
const MIME_ALLOW = [
|
|
@@ -16,7 +19,6 @@ const MIME_ALLOW = [
|
|
|
16
19
|
];
|
|
17
20
|
|
|
18
21
|
function filterAttachments(attachments, opts = {}) {
|
|
19
|
-
const maxCount = opts.maxCount ?? MAX_COUNT;
|
|
20
22
|
const maxFileBytes = opts.maxFileBytes ?? MAX_FILE_BYTES;
|
|
21
23
|
const maxTotalBytes = opts.maxTotalBytes ?? MAX_TOTAL_BYTES;
|
|
22
24
|
const mimeAllow = opts.mimeAllow ?? MIME_ALLOW;
|
|
@@ -26,10 +28,6 @@ function filterAttachments(attachments, opts = {}) {
|
|
|
26
28
|
let totalBytes = 0;
|
|
27
29
|
|
|
28
30
|
for (const a of attachments || []) {
|
|
29
|
-
if (accepted.length >= maxCount) {
|
|
30
|
-
rejected.push({ att: a, reason: `exceeds max count (${maxCount})` });
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
31
|
const mime = a.mime_type || '';
|
|
34
32
|
if (!mimeAllow.some((re) => re.test(mime))) {
|
|
35
33
|
rejected.push({ att: a, reason: `mime not allowed (${mime || 'unknown'})` });
|
|
@@ -38,7 +36,7 @@ function filterAttachments(attachments, opts = {}) {
|
|
|
38
36
|
const reported = a.size || 0;
|
|
39
37
|
// Telegram sometimes reports file_size=0 or omits it. Pre-0.6.14
|
|
40
38
|
// those bypassed the cumulative cap entirely (totalBytes + 0 always
|
|
41
|
-
// ≤ maxTotalBytes), so
|
|
39
|
+
// ≤ maxTotalBytes), so unsized attachments could blow through the
|
|
42
40
|
// 20 MB total cap. Treat unknown sizes as worst-case (= per-file
|
|
43
41
|
// cap) for budgeting; the per-file cap is still enforced live by
|
|
44
42
|
// the streaming download in polygram.js.
|
|
@@ -57,4 +55,4 @@ function filterAttachments(attachments, opts = {}) {
|
|
|
57
55
|
return { accepted, rejected, totalBytes };
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
module.exports = { filterAttachments,
|
|
58
|
+
module.exports = { filterAttachments, MAX_FILE_BYTES, MAX_TOTAL_BYTES, MIME_ALLOW };
|
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.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
|
@@ -1709,16 +1709,34 @@ async function handleConfigCallback(ctx) {
|
|
|
1709
1709
|
user: cmdUser, user_id: cmdUserId, source: 'inline-button',
|
|
1710
1710
|
}), `log ${setting} change`);
|
|
1711
1711
|
|
|
1712
|
-
// Graceful
|
|
1712
|
+
// Graceful application of the change to the topic's session. With
|
|
1713
1713
|
// isolateTopics=false sessionKey is the chat (one shared session). With
|
|
1714
1714
|
// isolateTopics=true sessionKey carries the topic, so other topics'
|
|
1715
1715
|
// in-flight turns are not disturbed and the card update + button toast
|
|
1716
|
-
// only affect the user's own context.
|
|
1717
|
-
//
|
|
1716
|
+
// only affect the user's own context.
|
|
1717
|
+
//
|
|
1718
|
+
// CLI pm: requestRespawn drains pending turns then kills the process;
|
|
1719
|
+
// the next user message spawns fresh with the updated chatConfig.
|
|
1720
|
+
// SDK pm: applies live to the running Query via setModel /
|
|
1721
|
+
// applyFlagSettings — no respawn needed, change takes effect for the
|
|
1722
|
+
// rest of the in-flight turn AND all future ones. Falls back to
|
|
1723
|
+
// {killed: false} if neither method is available, leaving the new
|
|
1724
|
+
// chatConfig value to be picked up by the next cold spawn.
|
|
1718
1725
|
const callbackThreadId = ctx.callbackQuery.message?.message_thread_id?.toString() || null;
|
|
1719
1726
|
const callbackSessionKey = getSessionKey(chatId, callbackThreadId, chatConfig);
|
|
1720
1727
|
const reason = setting === 'model' ? 'model-change' : 'effort-change';
|
|
1721
|
-
|
|
1728
|
+
let respawn;
|
|
1729
|
+
if (typeof pm.requestRespawn === 'function') {
|
|
1730
|
+
respawn = pm.requestRespawn(callbackSessionKey, reason);
|
|
1731
|
+
} else if (setting === 'effort' && typeof pm.applyFlagSettings === 'function') {
|
|
1732
|
+
const ok = await pm.applyFlagSettings(callbackSessionKey, { effortLevel: value });
|
|
1733
|
+
respawn = { killed: ok };
|
|
1734
|
+
} else if (setting === 'model' && typeof pm.setModel === 'function') {
|
|
1735
|
+
const ok = await pm.setModel(callbackSessionKey, value);
|
|
1736
|
+
respawn = { killed: ok };
|
|
1737
|
+
} else {
|
|
1738
|
+
respawn = { killed: false };
|
|
1739
|
+
}
|
|
1722
1740
|
const anyActive = !respawn.killed;
|
|
1723
1741
|
|
|
1724
1742
|
// Re-render the card with updated ✓ + the same help text shown initially.
|
|
@@ -1969,17 +1987,33 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1969
1987
|
}
|
|
1970
1988
|
return;
|
|
1971
1989
|
}
|
|
1972
|
-
// Graceful
|
|
1973
|
-
// isolateTopics=false the sessionKey is just the
|
|
1974
|
-
// session for the whole chat — every topic
|
|
1975
|
-
// With isolateTopics=true each topic is a
|
|
1976
|
-
// /model in topic A should NOT disturb
|
|
1977
|
-
// post a phantom "✓ Using sonnet now"
|
|
1978
|
-
//
|
|
1979
|
-
//
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1990
|
+
// Graceful application of a model/effort change to the user's CURRENT
|
|
1991
|
+
// session only. With isolateTopics=false the sessionKey is just the
|
|
1992
|
+
// chat (one shared session for the whole chat — every topic
|
|
1993
|
+
// respawns implicitly). With isolateTopics=true each topic is a
|
|
1994
|
+
// separate session, and a /model in topic A should NOT disturb
|
|
1995
|
+
// topic B's in-flight turn or post a phantom "✓ Using sonnet now"
|
|
1996
|
+
// in a topic that didn't ask.
|
|
1997
|
+
//
|
|
1998
|
+
// CLI pm: requestRespawn drains pending turns then kills the process;
|
|
1999
|
+
// the next user message spawns fresh with the updated chatConfig.
|
|
2000
|
+
// SDK pm: applies live to the running Query via setModel /
|
|
2001
|
+
// applyFlagSettings — no respawn needed, change takes effect for
|
|
2002
|
+
// the rest of the in-flight turn AND all future ones.
|
|
2003
|
+
const applyConfigChange = async (reason, setting, value) => {
|
|
2004
|
+
if (typeof pm.requestRespawn === 'function') {
|
|
2005
|
+
const res = pm.requestRespawn(sessionKey, reason);
|
|
2006
|
+
return { queued: res.queued, anyActive: !res.killed };
|
|
2007
|
+
}
|
|
2008
|
+
if (setting === 'effort' && typeof pm.applyFlagSettings === 'function') {
|
|
2009
|
+
const ok = await pm.applyFlagSettings(sessionKey, { effortLevel: value });
|
|
2010
|
+
return { queued: 0, anyActive: !ok };
|
|
2011
|
+
}
|
|
2012
|
+
if (setting === 'model' && typeof pm.setModel === 'function') {
|
|
2013
|
+
const ok = await pm.setModel(sessionKey, value);
|
|
2014
|
+
return { queued: 0, anyActive: !ok };
|
|
2015
|
+
}
|
|
2016
|
+
return { queued: 0, anyActive: false };
|
|
1983
2017
|
};
|
|
1984
2018
|
|
|
1985
2019
|
if (botAllowsCommands && text.startsWith('/model ')) {
|
|
@@ -1993,7 +2027,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1993
2027
|
old_value: oldModel, new_value: newModel,
|
|
1994
2028
|
user: cmdUser, user_id: cmdUserId, source: 'command',
|
|
1995
2029
|
}), 'log model change');
|
|
1996
|
-
const { anyActive } =
|
|
2030
|
+
const { anyActive } = await applyConfigChange('model-change', 'model', newModel);
|
|
1997
2031
|
const ver = MODEL_VERSIONS[newModel] || newModel;
|
|
1998
2032
|
const suffix = anyActive ? ` — I'll switch when I finish` : '';
|
|
1999
2033
|
await sendReply(`Model → ${newModel} (${ver})${suffix}`);
|
|
@@ -2013,7 +2047,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2013
2047
|
old_value: oldEffort, new_value: newEffort,
|
|
2014
2048
|
user: cmdUser, user_id: cmdUserId, source: 'command',
|
|
2015
2049
|
}), 'log effort change');
|
|
2016
|
-
const { anyActive } =
|
|
2050
|
+
const { anyActive } = await applyConfigChange('effort-change', 'effort', newEffort);
|
|
2017
2051
|
const suffix = anyActive ? ` — I'll switch when I finish` : '';
|
|
2018
2052
|
await sendReply(`Effort → ${newEffort}${suffix}`);
|
|
2019
2053
|
} else {
|