polygram 0.5.0 → 0.5.1

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.5.0",
4
+ "version": "0.5.1",
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.0",
3
+ "version": "0.5.1",
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,29 @@ 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
+
740
763
  function approvalCardText(row, opts = {}) {
741
764
  // No parse_mode is used on this card — tool_name/turn_id/tool_input
742
765
  // originate from the Claude subprocess and could contain Markdown special
@@ -927,6 +950,81 @@ async function handleApprovalCallback(ctx) {
927
950
  resolveApprovalWaiter(id, status);
928
951
  }
929
952
 
953
+ // Handles taps on the /model and /effort inline keyboard buttons. Same
954
+ // outcome as the text-typed `/model sonnet` flow: mutate chatConfig,
955
+ // trigger graceful respawn, log config change, edit the message to show
956
+ // the new ✓ marker.
957
+ async function handleConfigCallback(ctx) {
958
+ const data = ctx.callbackQuery?.data || '';
959
+ const m = String(data).match(/^cfg:(model|effort):(\S+)$/);
960
+ if (!m) return;
961
+ const setting = m[1];
962
+ const value = m[2];
963
+
964
+ const chatId = String(ctx.callbackQuery.message?.chat?.id || '');
965
+ const chatConfig = config.chats[chatId];
966
+ if (!chatConfig) {
967
+ await ctx.answerCallbackQuery({ text: 'Chat not configured', show_alert: true }).catch(() => {});
968
+ return;
969
+ }
970
+ if (!config.bot?.allowConfigCommands) {
971
+ await ctx.answerCallbackQuery({ text: 'Config commands disabled', show_alert: true }).catch(() => {});
972
+ return;
973
+ }
974
+
975
+ const validValues = setting === 'model' ? MODEL_OPTIONS : EFFORT_OPTIONS;
976
+ if (!validValues.includes(value)) {
977
+ await ctx.answerCallbackQuery({ text: `Invalid ${setting}` }).catch(() => {});
978
+ return;
979
+ }
980
+
981
+ const oldValue = chatConfig[setting];
982
+ if (oldValue === value) {
983
+ await ctx.answerCallbackQuery({ text: `Already ${value}` }).catch(() => {});
984
+ return;
985
+ }
986
+
987
+ chatConfig[setting] = value;
988
+ const cmdUserId = ctx.callbackQuery.from?.id || null;
989
+ const cmdUser = ctx.callbackQuery.from?.first_name || ctx.callbackQuery.from?.username || null;
990
+ dbWrite(() => db.logConfigChange({
991
+ chat_id: chatId, thread_id: null, field: setting,
992
+ old_value: oldValue, new_value: value,
993
+ user: cmdUser, user_id: cmdUserId, source: 'inline-button',
994
+ }), `log ${setting} change`);
995
+
996
+ // Graceful respawn across all sessionKeys for this chat (matches the
997
+ // text-command flow in handleMessage).
998
+ const reason = setting === 'model' ? 'model-change' : 'effort-change';
999
+ const prefix = chatId;
1000
+ let anyActive = false;
1001
+ for (const key of pm.keys()) {
1002
+ if (key === prefix || key.startsWith(prefix + ':')) {
1003
+ const res = pm.requestRespawn(key, reason);
1004
+ if (!res.killed) anyActive = true;
1005
+ }
1006
+ }
1007
+
1008
+ // Re-render the card with updated ✓.
1009
+ const ver = MODEL_VERSIONS[chatConfig.model] || chatConfig.model;
1010
+ const newInfo = `Model: ${chatConfig.model} (${ver})\nEffort: ${chatConfig.effort}\nAgent: ${chatConfig.agent}`;
1011
+ const showRow = setting; // /model card → only model row, /effort → only effort, /config → both.
1012
+ // Detect original card type from existing reply_markup row count.
1013
+ const existingRows = ctx.callbackQuery.message?.reply_markup?.inline_keyboard?.length || 0;
1014
+ const newKeyboard = buildConfigKeyboard(chatConfig, existingRows >= 2 ? 'all' : showRow);
1015
+ try {
1016
+ await ctx.editMessageText(newInfo, { reply_markup: newKeyboard });
1017
+ } catch (err) {
1018
+ // Edit may fail if message is too old or unchanged — not fatal.
1019
+ console.error(`[${BOT_NAME}] config-card edit failed: ${err.message}`);
1020
+ }
1021
+
1022
+ const ackText = anyActive
1023
+ ? `${setting} → ${value} — switching when finished`
1024
+ : `${setting} → ${value}`;
1025
+ await ctx.answerCallbackQuery({ text: ackText }).catch(() => {});
1026
+ }
1027
+
930
1028
  function startApprovalSweeper(intervalMs = 30_000) {
931
1029
  return setInterval(() => {
932
1030
  let rows;
@@ -1006,15 +1104,24 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
1006
1104
  const cmdUser = msg.from?.first_name || msg.from?.username || null;
1007
1105
  const cmdUserId = msg.from?.id || null;
1008
1106
 
1009
- const sendReply = (replyText, meta = {}) => tg(bot, 'sendMessage', {
1010
- chat_id: chatId, text: replyText, ...replyOpts(threadId),
1011
- }, { source: 'command-reply', botName: BOT_NAME, model: chatConfig.model, effort: chatConfig.effort, ...meta });
1107
+ // sendReply accepts (text, meta?) with optional extra Telegram params
1108
+ // pulled out via meta.params (kept separate so meta stays for DB tags).
1109
+ const sendReply = (replyText, meta = {}) => {
1110
+ const { params: extraParams = {}, ...metaTags } = meta;
1111
+ return tg(bot, 'sendMessage', {
1112
+ chat_id: chatId, text: replyText, ...replyOpts(threadId), ...extraParams,
1113
+ }, { source: 'command-reply', botName: BOT_NAME, model: chatConfig.model, effort: chatConfig.effort, ...metaTags });
1114
+ };
1012
1115
 
1013
1116
  if (botAllowsCommands && (text === '/model' || text === '/config' || text === '/effort')) {
1014
1117
  const alive = pm.has(sessionKey) && !pm.get(sessionKey).closed;
1015
1118
  const ver = MODEL_VERSIONS[chatConfig.model] || chatConfig.model;
1016
1119
  const info = `Model: ${chatConfig.model} (${ver})\nEffort: ${chatConfig.effort}\nAgent: ${chatConfig.agent}\nProcess: ${alive ? 'warm' : 'cold'}\nSession: ${getClaudeSessionId(db, sessionKey)?.slice(0, 8) || 'new'}`;
1017
- await sendReply(info);
1120
+ // Inline keyboard so users tap to switch instead of typing exact
1121
+ // names (avoids "sonet" typo problem).
1122
+ const show = text === '/effort' ? 'effort' : text === '/model' ? 'model' : 'all';
1123
+ const reply_markup = buildConfigKeyboard(chatConfig, show);
1124
+ await sendReply(info, { params: { reply_markup } });
1018
1125
  return;
1019
1126
  }
1020
1127
  // Helper: request respawn across ALL sessionKeys owned by this chat (one
@@ -1665,7 +1772,12 @@ function createBot(token) {
1665
1772
 
1666
1773
  bot.on('callback_query:data', async (ctx) => {
1667
1774
  try {
1668
- await handleApprovalCallback(ctx);
1775
+ const data = ctx.callbackQuery?.data || '';
1776
+ if (data.startsWith('cfg:')) {
1777
+ await handleConfigCallback(ctx);
1778
+ } else {
1779
+ await handleApprovalCallback(ctx);
1780
+ }
1669
1781
  } catch (err) {
1670
1782
  console.error(`[${BOT_NAME}] callback_query error: ${err.message}`);
1671
1783
  }