polygram 0.3.5 → 0.3.6

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.3.5",
4
+ "version": "0.3.6",
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",
@@ -11,6 +11,14 @@
11
11
  "streamReplies": true,
12
12
  "streamMinChars": 30,
13
13
  "streamThrottleMs": 500,
14
+ "_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.",
15
+ "pairedChatDefaults": {
16
+ "agent": "admin",
17
+ "cwd": "/Users/you/admin-agent",
18
+ "model": "sonnet",
19
+ "effort": "medium",
20
+ "timeout": 600
21
+ },
14
22
  "voice": {
15
23
  "enabled": true,
16
24
  "provider": "openai",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
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
@@ -1271,6 +1271,87 @@ function createBot(token) {
1271
1271
  // not another bot's problem.
1272
1272
  const knownChat = (chatId) => !!config.chats[chatId];
1273
1273
 
1274
+ // Claim a pair code from an unconfigured private chat and persist a new
1275
+ // chat entry so subsequent messages go through the normal flow. Replies
1276
+ // to the user on both success and failure. Returns the new chatConfig on
1277
+ // success, null on any failure.
1278
+ //
1279
+ // The new chat inherits cwd/agent from bot-level pairedChatDefaults if
1280
+ // present, otherwise from the first existing chat the bot owns — on the
1281
+ // reasonable assumption that paired DMs should behave like other DMs for
1282
+ // this bot. Operator can override by setting config.bots.<bot>.pairedChatDefaults.
1283
+ async function onboardPairedChat(ctx, code) {
1284
+ const chatId = ctx.chat.id.toString();
1285
+ const userId = ctx.message.from?.id;
1286
+ const send = (text) => bot.api.sendMessage(chatId, text).catch(() => {});
1287
+
1288
+ if (!userId) {
1289
+ await send('No user id on request.');
1290
+ return null;
1291
+ }
1292
+
1293
+ const res = pairings.claimCode({
1294
+ code, claimer_user_id: userId,
1295
+ chat_id: chatId, bot_name: BOT_NAME,
1296
+ });
1297
+ dbWrite(() => db.logEvent('pair-claim-attempt', {
1298
+ bot: BOT_NAME, user_id: userId, chat_id: chatId,
1299
+ ok: res.ok, reason: res.reason, via: 'auto-onboard',
1300
+ }), 'log pair-claim-attempt');
1301
+
1302
+ if (!res.ok) {
1303
+ const reply = res.reason === 'rate-limited'
1304
+ ? 'Too many attempts. Try again later.'
1305
+ : 'Invalid or expired code.';
1306
+ await send(reply);
1307
+ return null;
1308
+ }
1309
+
1310
+ const paired = config.bot?.pairedChatDefaults || {};
1311
+ const globals = config.defaults || {};
1312
+ const firstChat = Object.values(config.chats)[0] || {};
1313
+ const chatName = paired.name
1314
+ || (ctx.chat.username && `@${ctx.chat.username}`)
1315
+ || ctx.chat.first_name
1316
+ || `User ${userId}`;
1317
+
1318
+ const cwd = paired.cwd || firstChat.cwd;
1319
+ if (!cwd) {
1320
+ dbWrite(() => db.logEvent('auto-onboard-failed', {
1321
+ bot: BOT_NAME, chat_id: chatId, user_id: userId,
1322
+ reason: 'no-cwd',
1323
+ }), 'log auto-onboard-failed');
1324
+ await send('Paired, but no working directory is configured. Ask the operator to set pairedChatDefaults.cwd.');
1325
+ return null;
1326
+ }
1327
+
1328
+ const newChat = {
1329
+ name: chatName,
1330
+ bot: BOT_NAME,
1331
+ agent: paired.agent || firstChat.agent,
1332
+ model: paired.model || globals.model || 'sonnet',
1333
+ effort: paired.effort || globals.effort || 'medium',
1334
+ cwd,
1335
+ timeout: paired.timeout || globals.timeout || 600,
1336
+ };
1337
+ if (paired.requireMention != null) newChat.requireMention = paired.requireMention;
1338
+
1339
+ config.chats[chatId] = newChat;
1340
+ try { saveConfig(); }
1341
+ catch (err) {
1342
+ console.error(`[${BOT_NAME}] saveConfig on auto-onboard failed: ${err.message}`);
1343
+ }
1344
+ dbWrite(() => db.logEvent('chat-auto-created', {
1345
+ bot: BOT_NAME, chat_id: chatId, user_id: userId,
1346
+ source: 'pair-claim', model: newChat.model, effort: newChat.effort,
1347
+ }), 'log chat-auto-created');
1348
+
1349
+ const chatLabel = res.chat_id ? `chat ${res.chat_id}` : `every chat ${BOT_NAME} is in`;
1350
+ const suffix = res.note ? `\n(${res.note})` : '';
1351
+ await send(`Paired. You can use me in ${chatLabel}.${suffix}`);
1352
+ return newChat;
1353
+ }
1354
+
1274
1355
  bot.on('message', async (ctx) => {
1275
1356
  if (!isWellFormedMessage(ctx.message)) {
1276
1357
  dbWrite(() => db.logEvent('malformed-update', {
@@ -1281,7 +1362,25 @@ function createBot(token) {
1281
1362
  return;
1282
1363
  }
1283
1364
  const chatId = ctx.chat.id.toString();
1284
- const chatConfig = config.chats[chatId];
1365
+ let chatConfig = config.chats[chatId];
1366
+
1367
+ // Auto-onboarding: /pair <CODE> from an unconfigured private chat.
1368
+ // Without this, the !chatConfig drop below would silently eat pair
1369
+ // claims from DMs the operator hasn't pre-listed — defeating the
1370
+ // whole point of pair codes (which exist to grant access without
1371
+ // pre-configuration). Group chats are not auto-onboarded: they must
1372
+ // still be added to config.json by the operator, because adding a
1373
+ // group can affect multiple users.
1374
+ if (!chatConfig && ctx.chat.type === 'private') {
1375
+ const probe = (ctx.message.text || '').trim();
1376
+ const pairMatch = /^\/pair(?:@\S+)?\s+(\S+)\s*$/.exec(probe);
1377
+ if (pairMatch) {
1378
+ chatConfig = await onboardPairedChat(ctx, pairMatch[1]);
1379
+ if (!chatConfig) return;
1380
+ recordInbound(ctx.message);
1381
+ return;
1382
+ }
1383
+ }
1285
1384
  if (!chatConfig) return;
1286
1385
 
1287
1386
  // Record every inbound msg, even unaddressed ones — needed for reply-to