polygram 0.5.0 → 0.5.2
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/package.json +1 -1
- package/polygram.js +161 -8
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.2",
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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
|
@@ -737,6 +737,62 @@ function buildApprovalKeyboard(approvalId, token) {
|
|
|
737
737
|
};
|
|
738
738
|
}
|
|
739
739
|
|
|
740
|
+
// /model and /effort inline keyboard. `show` controls which row(s) appear:
|
|
741
|
+
// 'model', 'effort', or 'all'. The current value gets a ✓ marker so the
|
|
742
|
+
// user can see at a glance what's selected.
|
|
743
|
+
const MODEL_OPTIONS = ['opus', 'sonnet', 'haiku'];
|
|
744
|
+
const EFFORT_OPTIONS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
745
|
+
|
|
746
|
+
function buildConfigKeyboard(chatConfig, show = 'all') {
|
|
747
|
+
const rows = [];
|
|
748
|
+
if (show === 'model' || show === 'all') {
|
|
749
|
+
rows.push(MODEL_OPTIONS.map((m) => ({
|
|
750
|
+
text: m === chatConfig.model ? `✓ ${m}` : m,
|
|
751
|
+
callback_data: `cfg:model:${m}`,
|
|
752
|
+
})));
|
|
753
|
+
}
|
|
754
|
+
if (show === 'effort' || show === 'all') {
|
|
755
|
+
rows.push(EFFORT_OPTIONS.map((e) => ({
|
|
756
|
+
text: e === chatConfig.effort ? `✓ ${e}` : e,
|
|
757
|
+
callback_data: `cfg:effort:${e}`,
|
|
758
|
+
})));
|
|
759
|
+
}
|
|
760
|
+
return { inline_keyboard: rows };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Card text shown above the inline keyboard. Includes plain-language
|
|
764
|
+
// guidance on when to pick which model / effort, since most users
|
|
765
|
+
// (especially in shared groups) don't know which option to tap.
|
|
766
|
+
const MODEL_VERSIONS_DESC = { opus: 'claude-opus-4-6', sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5' };
|
|
767
|
+
|
|
768
|
+
function formatConfigInfoText(chatConfig, show, sessionKey) {
|
|
769
|
+
const alive = pm.has(sessionKey) && !pm.get(sessionKey).closed;
|
|
770
|
+
const ver = MODEL_VERSIONS_DESC[chatConfig.model] || chatConfig.model;
|
|
771
|
+
const head = `Model: ${chatConfig.model} (${ver})\nEffort: ${chatConfig.effort}\nAgent: ${chatConfig.agent}\nProcess: ${alive ? 'warm' : 'cold'}\nSession: ${getClaudeSessionId(db, sessionKey)?.slice(0, 8) || 'new'}`;
|
|
772
|
+
|
|
773
|
+
const modelHelp = [
|
|
774
|
+
'',
|
|
775
|
+
'**Models**',
|
|
776
|
+
'🧠 **opus** — глубокий анализ, code refactor, сверка из 3+ источников. ~5× стоимость sonnet.',
|
|
777
|
+
'🤖 **sonnet** — дефолт. Большинство ops, code review, document summary.',
|
|
778
|
+
'⚡ **haiku** — простые быстрые задачи, классификация, lookup.',
|
|
779
|
+
].join('\n');
|
|
780
|
+
|
|
781
|
+
const effortHelp = [
|
|
782
|
+
'',
|
|
783
|
+
'**Effort** — потолок «сколько Claude может думать». На простых вопросах он сам отвечает быстро, на сложных тратит больше токенов. Можно безопасно ставить выше — он не разгонится без нужды.',
|
|
784
|
+
'• **low** — fast replies, минимум reasoning. Casual chat, simple lookups.',
|
|
785
|
+
'• **medium** — balanced default. Подходит почти всем.',
|
|
786
|
+
'• **high** — сложные многошаговые задачи. Audit, debug, multi-source.',
|
|
787
|
+
'• **xhigh** / **max** — самые тяжёлые. Hard reasoning, edge cases.',
|
|
788
|
+
].join('\n');
|
|
789
|
+
|
|
790
|
+
let body = head;
|
|
791
|
+
if (show === 'model' || show === 'all') body += '\n' + modelHelp;
|
|
792
|
+
if (show === 'effort' || show === 'all') body += '\n' + effortHelp;
|
|
793
|
+
return body;
|
|
794
|
+
}
|
|
795
|
+
|
|
740
796
|
function approvalCardText(row, opts = {}) {
|
|
741
797
|
// No parse_mode is used on this card — tool_name/turn_id/tool_input
|
|
742
798
|
// originate from the Claude subprocess and could contain Markdown special
|
|
@@ -927,6 +983,93 @@ async function handleApprovalCallback(ctx) {
|
|
|
927
983
|
resolveApprovalWaiter(id, status);
|
|
928
984
|
}
|
|
929
985
|
|
|
986
|
+
// Handles taps on the /model and /effort inline keyboard buttons. Same
|
|
987
|
+
// outcome as the text-typed `/model sonnet` flow: mutate chatConfig,
|
|
988
|
+
// trigger graceful respawn, log config change, edit the message to show
|
|
989
|
+
// the new ✓ marker.
|
|
990
|
+
async function handleConfigCallback(ctx) {
|
|
991
|
+
const data = ctx.callbackQuery?.data || '';
|
|
992
|
+
const m = String(data).match(/^cfg:(model|effort):(\S+)$/);
|
|
993
|
+
if (!m) return;
|
|
994
|
+
const setting = m[1];
|
|
995
|
+
const value = m[2];
|
|
996
|
+
|
|
997
|
+
const chatId = String(ctx.callbackQuery.message?.chat?.id || '');
|
|
998
|
+
const chatConfig = config.chats[chatId];
|
|
999
|
+
if (!chatConfig) {
|
|
1000
|
+
await ctx.answerCallbackQuery({ text: 'Chat not configured', show_alert: true }).catch(() => {});
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (!config.bot?.allowConfigCommands) {
|
|
1004
|
+
await ctx.answerCallbackQuery({ text: 'Config commands disabled', show_alert: true }).catch(() => {});
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const validValues = setting === 'model' ? MODEL_OPTIONS : EFFORT_OPTIONS;
|
|
1009
|
+
if (!validValues.includes(value)) {
|
|
1010
|
+
await ctx.answerCallbackQuery({ text: `Invalid ${setting}` }).catch(() => {});
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const oldValue = chatConfig[setting];
|
|
1015
|
+
if (oldValue === value) {
|
|
1016
|
+
await ctx.answerCallbackQuery({ text: `Already ${value}` }).catch(() => {});
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
chatConfig[setting] = value;
|
|
1021
|
+
const cmdUserId = ctx.callbackQuery.from?.id || null;
|
|
1022
|
+
const cmdUser = ctx.callbackQuery.from?.first_name || ctx.callbackQuery.from?.username || null;
|
|
1023
|
+
dbWrite(() => db.logConfigChange({
|
|
1024
|
+
chat_id: chatId, thread_id: null, field: setting,
|
|
1025
|
+
old_value: oldValue, new_value: value,
|
|
1026
|
+
user: cmdUser, user_id: cmdUserId, source: 'inline-button',
|
|
1027
|
+
}), `log ${setting} change`);
|
|
1028
|
+
|
|
1029
|
+
// Graceful respawn across all sessionKeys for this chat (matches the
|
|
1030
|
+
// text-command flow in handleMessage).
|
|
1031
|
+
const reason = setting === 'model' ? 'model-change' : 'effort-change';
|
|
1032
|
+
const prefix = chatId;
|
|
1033
|
+
let anyActive = false;
|
|
1034
|
+
for (const key of pm.keys()) {
|
|
1035
|
+
if (key === prefix || key.startsWith(prefix + ':')) {
|
|
1036
|
+
const res = pm.requestRespawn(key, reason);
|
|
1037
|
+
if (!res.killed) anyActive = true;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Re-render the card with updated ✓ + the same help text shown initially.
|
|
1042
|
+
// Detect original card type (model-only / effort-only / both) by counting
|
|
1043
|
+
// rows in the existing reply_markup so the user sees the same layout
|
|
1044
|
+
// they tapped into.
|
|
1045
|
+
const existingRows = ctx.callbackQuery.message?.reply_markup?.inline_keyboard?.length || 0;
|
|
1046
|
+
const showRow = existingRows >= 2 ? 'all' : setting;
|
|
1047
|
+
// chatId works as a session-key proxy here for the warm-process check
|
|
1048
|
+
// (isolateTopics chats might have multiple keys but for this card we
|
|
1049
|
+
// just want a representative state).
|
|
1050
|
+
const newInfo = formatConfigInfoText(chatConfig, showRow, chatId);
|
|
1051
|
+
const newKeyboard = buildConfigKeyboard(chatConfig, showRow);
|
|
1052
|
+
try {
|
|
1053
|
+
// Pre-format the markdown→HTML ourselves so editMessageText can be
|
|
1054
|
+
// called with the right parse_mode (the bot.api.editMessageText path
|
|
1055
|
+
// bypasses tg() / applyFormatting in the chat-action approval card,
|
|
1056
|
+
// but here we DO want HTML).
|
|
1057
|
+
const { toTelegramHtml } = require('./lib/telegram-format');
|
|
1058
|
+
const { text: html, parseMode } = toTelegramHtml(newInfo);
|
|
1059
|
+
await ctx.editMessageText(html, {
|
|
1060
|
+
reply_markup: newKeyboard,
|
|
1061
|
+
...(parseMode && { parse_mode: parseMode }),
|
|
1062
|
+
});
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
console.error(`[${BOT_NAME}] config-card edit failed: ${err.message}`);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const ackText = anyActive
|
|
1068
|
+
? `${setting} → ${value} — switching when finished`
|
|
1069
|
+
: `${setting} → ${value}`;
|
|
1070
|
+
await ctx.answerCallbackQuery({ text: ackText }).catch(() => {});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
930
1073
|
function startApprovalSweeper(intervalMs = 30_000) {
|
|
931
1074
|
return setInterval(() => {
|
|
932
1075
|
let rows;
|
|
@@ -1006,15 +1149,20 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1006
1149
|
const cmdUser = msg.from?.first_name || msg.from?.username || null;
|
|
1007
1150
|
const cmdUserId = msg.from?.id || null;
|
|
1008
1151
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1152
|
+
// sendReply accepts (text, meta?) with optional extra Telegram params
|
|
1153
|
+
// pulled out via meta.params (kept separate so meta stays for DB tags).
|
|
1154
|
+
const sendReply = (replyText, meta = {}) => {
|
|
1155
|
+
const { params: extraParams = {}, ...metaTags } = meta;
|
|
1156
|
+
return tg(bot, 'sendMessage', {
|
|
1157
|
+
chat_id: chatId, text: replyText, ...replyOpts(threadId), ...extraParams,
|
|
1158
|
+
}, { source: 'command-reply', botName: BOT_NAME, model: chatConfig.model, effort: chatConfig.effort, ...metaTags });
|
|
1159
|
+
};
|
|
1012
1160
|
|
|
1013
1161
|
if (botAllowsCommands && (text === '/model' || text === '/config' || text === '/effort')) {
|
|
1014
|
-
const
|
|
1015
|
-
const
|
|
1016
|
-
const
|
|
1017
|
-
await sendReply(info);
|
|
1162
|
+
const show = text === '/effort' ? 'effort' : text === '/model' ? 'model' : 'all';
|
|
1163
|
+
const info = formatConfigInfoText(chatConfig, show, sessionKey);
|
|
1164
|
+
const reply_markup = buildConfigKeyboard(chatConfig, show);
|
|
1165
|
+
await sendReply(info, { params: { reply_markup } });
|
|
1018
1166
|
return;
|
|
1019
1167
|
}
|
|
1020
1168
|
// Helper: request respawn across ALL sessionKeys owned by this chat (one
|
|
@@ -1665,7 +1813,12 @@ function createBot(token) {
|
|
|
1665
1813
|
|
|
1666
1814
|
bot.on('callback_query:data', async (ctx) => {
|
|
1667
1815
|
try {
|
|
1668
|
-
|
|
1816
|
+
const data = ctx.callbackQuery?.data || '';
|
|
1817
|
+
if (data.startsWith('cfg:')) {
|
|
1818
|
+
await handleConfigCallback(ctx);
|
|
1819
|
+
} else {
|
|
1820
|
+
await handleApprovalCallback(ctx);
|
|
1821
|
+
}
|
|
1669
1822
|
} catch (err) {
|
|
1670
1823
|
console.error(`[${BOT_NAME}] callback_query error: ${err.message}`);
|
|
1671
1824
|
}
|