polygram 0.39.0 → 0.39.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.39.0",
4
+ "version": "0.39.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 plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
6
6
  "keywords": [
7
7
  "telegram",
@@ -9,6 +9,8 @@
9
9
  "_comment_adminChatId": "Required when allowConfigCommands is true for pairing commands (/pair-code, /pairings, /unpair) to work. These grant cross-chat trust and are gated to the admin chat only.",
10
10
  "adminChatId": "123456789",
11
11
  "needsToken": false,
12
+ "_comment_claudeCliAuthSource": "native (default) checks the host Claude login before CLI turns. Set external only when the configured session launcher supplies proxy credentials. SDK retains native checks; Codex is unaffected. Requires a verified pinned CLI and an absolute session launcher. Restart after changes; restore native when disabling proxy routing.",
13
+ "claudeCliAuthSource": "native",
12
14
  "_comment_stream": "Streaming is always on. `streamMinChars` sets the initial-send debounce (default 30) — short responses below that stay idle until the final result. `streamThrottleMs` sets the edit cadence (default 1000ms, min 250).",
13
15
  "streamMinChars": 30,
14
16
  "streamThrottleMs": 1000,
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { resolveRuntimeDescriptor } = require('../runtime-config');
5
+
6
+ function validateClaudeCliAuthSource(botConfig = {}) {
7
+ const source = botConfig.claudeCliAuthSource;
8
+ if (source !== undefined && source !== 'native' && source !== 'external') {
9
+ throw new TypeError('claudeCliAuthSource must be native or external');
10
+ }
11
+ }
12
+
13
+ function needsNativeClaudeAuth(botConfig, backend) {
14
+ validateClaudeCliAuthSource(botConfig);
15
+ if (backend === 'codex') return false;
16
+ if (backend === 'sdk') return true;
17
+ if (backend !== 'cli') throw new TypeError('Unknown Claude auth backend');
18
+ return botConfig?.claudeCliAuthSource !== 'external';
19
+ }
20
+
21
+ function hasNativeClaudeRuntime(config) {
22
+ validateClaudeCliAuthSource(config.bot);
23
+ for (const [chatId, chat] of Object.entries(config.chats || {})) {
24
+ for (const threadId of [null, ...Object.keys(chat.topics || {})]) {
25
+ const { backend } = resolveRuntimeDescriptor({
26
+ config, chatId, threadId, defaultPm: 'sdk',
27
+ });
28
+ if (needsNativeClaudeAuth(config.bot, backend)) return true;
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+
34
+ function assertExternalClaudeCliReady(botConfig, { sessionLauncher, pinnedClaudeBin }) {
35
+ validateClaudeCliAuthSource(botConfig);
36
+ if (botConfig?.claudeCliAuthSource !== 'external') return;
37
+ if (typeof sessionLauncher !== 'string' || !path.isAbsolute(sessionLauncher)) {
38
+ throw new Error('External Claude CLI auth requires an absolute session launcher');
39
+ }
40
+ // Without a pinned binary the process factory falls back to SDK, which does
41
+ // not use the CLI launcher's external credentials.
42
+ if (!pinnedClaudeBin) {
43
+ throw new Error('External Claude CLI auth requires a verified pinned Claude binary');
44
+ }
45
+ }
46
+
47
+ module.exports = {
48
+ validateClaudeCliAuthSource,
49
+ needsNativeClaudeAuth,
50
+ hasNativeClaudeRuntime,
51
+ assertExternalClaudeCliReady,
52
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.39.0",
3
+ "version": "0.39.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
@@ -61,6 +61,12 @@ const { filterAttachments, resolveFileCaps, resolveMaxFileOverride, MAX_TOTAL_BY
61
61
  const { ProcessManager } = require('@shumkov/orchestra');
62
62
  const { createProcessFactory, pickBackend } = require('@shumkov/orchestra');
63
63
  const { checkClaudeAuthHealth } = require('@shumkov/orchestra').claudeBin;
64
+ const {
65
+ validateClaudeCliAuthSource,
66
+ needsNativeClaudeAuth,
67
+ hasNativeClaudeRuntime,
68
+ assertExternalClaudeCliReady,
69
+ } = require('./lib/ops/claude-auth-health');
64
70
  const { extractAssistantText } = require('@shumkov/orchestra');
65
71
  // 0.11.0: channels backend tool dispatcher — adapts CliProcess's reply
66
72
  // tool callback into polygram's existing chunkText + deliverReplies primitives.
@@ -1411,7 +1417,7 @@ async function handleMessage(sessionKey, chatId, msg, bot, dispatchContext = nul
1411
1417
  cmdUser, cmdUserId, label, sendReply,
1412
1418
  })) return;
1413
1419
 
1414
- // Claude-auth gate: if the CLI login has expired, every claude turn wedges
1420
+ // Claude-auth gate: if the native CLI login has expired, its turns wedge
1415
1421
  // INVISIBLY — the 401 fires inside the subprocess, never reaches the
1416
1422
  // classifier, and the turn degrades to a silent "⏱ went quiet" across every
1417
1423
  // topic. Refuse up-front with a clear message + a loud log instead of
@@ -1424,7 +1430,9 @@ async function handleMessage(sessionKey, chatId, msg, bot, dispatchContext = nul
1424
1430
  // read, and a token can expire between an original dispatch and its later
1425
1431
  // redelivery, so skipping it there would silently re-wedge exactly the
1426
1432
  // messages most likely to hit an expired-auth window (post-restart backlog).
1427
- if (selectedInboundProvider === 'claude') {
1433
+ // An externally authenticated CLI uses its launcher's credentials instead;
1434
+ // the host login says nothing about that route. SDK retains its native check.
1435
+ if (needsNativeClaudeAuth(config.bot, selectedBackend)) {
1428
1436
  let auth;
1429
1437
  try {
1430
1438
  auth = checkClaudeAuthHealth();
@@ -3634,6 +3642,7 @@ async function main() {
3634
3642
  // `= config.bots[BOT_NAME]` silently dropped top-level shared fields and
3635
3643
  // orphaned apiRoot (both bots ran on cloud, not the 2GB local server).
3636
3644
  config.bot = activeBotConfig(config, BOT_NAME);
3645
+ validateClaudeCliAuthSource(config.bot);
3637
3646
  } catch (err) {
3638
3647
  console.error(`[fatal] ${err.message}`);
3639
3648
  process.exit(2);
@@ -3929,18 +3938,19 @@ async function main() {
3929
3938
 
3930
3939
  // Claude-auth health monitor — a FREE credentials-file check (no API call, no
3931
3940
  // model spawn). An expired refresh token makes the CLI 401 silently and wedges
3932
- // every channels turn; the handleMessage gate refuses turns + tells the user,
3941
+ // native CLI turns; the handleMessage gate refuses turns + tells the user,
3933
3942
  // and this surfaces the same state loudly in the operator logs at boot + every
3934
3943
  // 30 min, warning a few days ahead so re-login happens before it breaks.
3935
3944
  const runAuthCheck = (trigger) => {
3936
3945
  try {
3946
+ if (!hasNativeClaudeRuntime(config)) return;
3937
3947
  const auth = checkClaudeAuthHealth();
3938
3948
  const expIso = auth.refreshTokenExpiresAt ? new Date(auth.refreshTokenExpiresAt).toISOString() : 'n/a';
3939
3949
  if (auth.state === 'expired') {
3940
- console.error(`[auth] (${trigger}) Claude login EXPIRED (refresh token ${expIso}) — turns are being refused; re-login on the host.`);
3950
+ console.error(`[auth] (${trigger}) Native Claude login EXPIRED (refresh token ${expIso}) — native-auth turns are being refused; re-login on the host.`);
3941
3951
  logEvent('auth-expired', { source: 'monitor', trigger, refresh_token_expires_at: auth.refreshTokenExpiresAt });
3942
3952
  } else if (auth.state === 'expiring') {
3943
- console.warn(`[auth] (${trigger}) Claude login expires in ${auth.daysLeft}d (${expIso}) — re-login soon.`);
3953
+ console.warn(`[auth] (${trigger}) Native Claude login expires in ${auth.daysLeft}d (${expIso}) — re-login soon.`);
3944
3954
  logEvent('auth-expiring', { source: 'monitor', trigger, days_left: auth.daysLeft, refresh_token_expires_at: auth.refreshTokenExpiresAt });
3945
3955
  } else if (auth.state === 'unknown') {
3946
3956
  console.warn(`[auth] (${trigger}) Claude credentials check inconclusive: ${auth.reason}`);
@@ -4320,6 +4330,7 @@ async function main() {
4320
4330
  logger: console,
4321
4331
  });
4322
4332
  const channelsClaudeBin = pinnedClaudeBin;
4333
+ assertExternalClaudeCliReady(config.bot, { sessionLauncher, pinnedClaudeBin });
4323
4334
 
4324
4335
  const orchestraProcessFactory = createProcessFactory({
4325
4336
  config,