polygram 0.38.9 → 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.38.9",
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,
@@ -111,6 +111,7 @@ const MIME_ALLOW = [
111
111
  /^text\/csv$/,
112
112
  // rc.68: archives + markup formats Claude reads natively.
113
113
  /^application\/zip$/, /^application\/x-zip-compressed$/,
114
+ /^application\/gzip$/, /^application\/x-gzip$/, /^application\/x-tar$/,
114
115
  /^text\/markdown$/,
115
116
  /^text\/html$/,
116
117
  /^text\/yaml$/, /^application\/yaml$/, /^application\/x-yaml$/,
@@ -122,8 +123,10 @@ const MIME_ALLOW = [
122
123
  // the fallback is "trust the filename when MIME is unhelpful" — not "any
123
124
  // extension goes." A file named foo.exe with empty MIME stays rejected.
124
125
  const EXTENSION_ALLOW = new Set([
125
- // archives
126
- 'zip',
126
+ // archives — extensionOf() only sees the last dot segment, so
127
+ // "foo.tar.gz" resolves to 'gz' (not 'tar.gz'); listing 'gz'/'tgz'/'tar'
128
+ // separately covers the common compound and short forms.
129
+ 'zip', 'gz', 'tgz', 'tar',
127
130
  // text / structured data
128
131
  'txt', 'md', 'csv', 'json', 'yaml', 'yml', 'xml', 'html',
129
132
  // documents
@@ -220,7 +220,7 @@ function resolveDisplayRichText(chatConfig, topicConfig, effectiveRichText) {
220
220
  // polygram passes the alias (opus / sonnet / haiku) and lets claude
221
221
  // resolve. Bump on Claude release.
222
222
  const MODEL_VERSIONS_DESC = {
223
- opus: 'claude-opus-5',
223
+ opus: 'claude-opus-5-5',
224
224
  sonnet: 'claude-sonnet-4-6',
225
225
  haiku: 'claude-haiku-4-5',
226
226
  };
@@ -17,8 +17,8 @@
17
17
  'use strict';
18
18
 
19
19
  // Per-million-token rates ($ USD). Cache-read and cache-creation
20
- // follow Anthropic's prompt-caching pricing — read is ~10% of normal
21
- // input; 1-hour creation is ~125% of normal input.
20
+ // follow Anthropic's prompt-caching pricing — read is usually ~10% of
21
+ // normal input (Opus 5.5: 5%); 5-minute creation is 125% of normal input.
22
22
  const MODEL_COSTS = {
23
23
  // Claude Sonnet 4.6
24
24
  'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.30, cacheCreation: 3.75 },
@@ -31,8 +31,11 @@ const MODEL_COSTS = {
31
31
  // Claude Opus 4.8 — kept for turns recorded before the CLI pin moved to
32
32
  // 2.1.220; without an entry those fall back to the Sonnet `default`.
33
33
  'claude-opus-4-8': { input: 15, output: 75, cacheRead: 1.50, cacheCreation: 18.75 },
34
- // Claude Opus 5 — what `--model opus` resolves to on the pinned CLI.
34
+ // Claude Opus 5 — kept for turns recorded before the CLI pin moved to
35
+ // 2.1.283; without an entry those fall back to the Sonnet `default`.
35
36
  'claude-opus-5': { input: 5, output: 25, cacheRead: 0.50, cacheCreation: 6.25 },
37
+ // Claude Opus 5.5 — what `--model opus` resolves to on the pinned CLI.
38
+ 'claude-opus-5-5': { input: 4, output: 20, cacheRead: 0.20, cacheCreation: 5 },
36
39
  // Default fallback — Sonnet rates (safest mid-tier estimate).
37
40
  default: { input: 3, output: 15, cacheRead: 0.30, cacheCreation: 3.75 },
38
41
  };
@@ -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
+ };
@@ -36,6 +36,19 @@ const CHILD_ENV_ALLOWLIST = new Set([
36
36
  ]);
37
37
  const CHILD_ENV_PREFIXES = ['LC_', 'NODE_', 'CLAUDE_', 'ANTHROPIC_'];
38
38
 
39
+ // Claude Code records a conversation's system prompt once and replays that
40
+ // record on every resume until compaction. The appended display hint depends
41
+ // on richText and changes between releases, so opt out of the recording to
42
+ // let changes reach resumed sessions. Bare string / string[] prompts can only
43
+ // opt out in the custom-object form.
44
+ function withoutPromptSnapshot(systemPrompt) {
45
+ if (systemPrompt == null) return systemPrompt;
46
+ if (typeof systemPrompt === 'string' || Array.isArray(systemPrompt)) {
47
+ return { type: 'custom', prompt: systemPrompt, snapshot: false };
48
+ }
49
+ return { ...systemPrompt, snapshot: false };
50
+ }
51
+
39
52
  function filterEnv(src) {
40
53
  const out = {};
41
54
  for (const [k, v] of Object.entries(src)) {
@@ -173,13 +186,13 @@ function createBuildSdkOptions({
173
186
  // inlineMedia: replies on this path are delivered by the streamer, which
174
187
  // resolves and uploads local media into the rich payload. The reply-tool
175
188
  // path cannot, so it asks for the hint without those paragraphs.
176
- composed.systemPrompt = appendDisplayHint(
189
+ composed.systemPrompt = withoutPromptSnapshot(appendDisplayHint(
177
190
  composed.systemPrompt,
178
191
  buildPolygramDisplayHint(
179
192
  resolveRichTextEnabled(config, chatId, threadId),
180
193
  { inlineMedia: true },
181
194
  ),
182
- );
195
+ ));
183
196
  return composed;
184
197
  };
185
198
  }
@@ -189,6 +202,7 @@ module.exports = {
189
202
  // Exposed for tests + adjacent extractions that need the same env
190
203
  // discipline (e.g. lib/sdk/callbacks.js when it ships).
191
204
  filterEnv,
205
+ withoutPromptSnapshot,
192
206
  CHILD_ENV_ALLOWLIST,
193
207
  CHILD_ENV_PREFIXES,
194
208
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.38.9",
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": {
@@ -66,9 +66,9 @@
66
66
  "homepage": "https://github.com/shumkov/polygram#readme",
67
67
  "type": "commonjs",
68
68
  "dependencies": {
69
- "@anthropic-ai/claude-agent-sdk": "0.2.123",
69
+ "@anthropic-ai/claude-agent-sdk": "0.3.283",
70
70
  "@modelcontextprotocol/sdk": "^1.29.0",
71
- "@shumkov/orchestra": "0.10.21",
71
+ "@shumkov/orchestra": "0.11.0",
72
72
  "acorn": "8.18.0",
73
73
  "better-sqlite3": "^12.9.0",
74
74
  "grammy": "^1.42.0",
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}`);
@@ -4184,6 +4194,11 @@ async function main() {
4184
4194
  + `${binCheck.vendored ? ' (vendored)' : ''}`,
4185
4195
  );
4186
4196
  pinnedClaudeBin = binCheck.path;
4197
+ // CliProcess checks `claude --help` before passing newer flags; the
4198
+ // probe is synchronous and takes up to a few seconds, so run it here
4199
+ // while boot already blocks instead of on the first chat's spawn.
4200
+ const { supportsClaudeFlag } = require('@shumkov/orchestra').claudeBin;
4201
+ supportsClaudeFlag(pinnedClaudeBin, '--system-prompt-snapshot', { logger: console });
4187
4202
  } else {
4188
4203
  console.warn(`[polygram] WARNING: ${binCheck.reason}`);
4189
4204
  }
@@ -4315,6 +4330,7 @@ async function main() {
4315
4330
  logger: console,
4316
4331
  });
4317
4332
  const channelsClaudeBin = pinnedClaudeBin;
4333
+ assertExternalClaudeCliReady(config.bot, { sessionLauncher, pinnedClaudeBin });
4318
4334
 
4319
4335
  const orchestraProcessFactory = createProcessFactory({
4320
4336
  config,