@gonzih/cc-tg 0.3.15 → 0.4.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.
package/README.md CHANGED
@@ -25,6 +25,7 @@ Open your bot in Telegram and start chatting.
25
25
  | `TELEGRAM_BOT_TOKEN` | yes | From @BotFather |
26
26
  | `CLAUDE_CODE_TOKEN` | yes* | Claude Code OAuth token (starts with `sk-ant-oat`) |
27
27
  | `CLAUDE_CODE_OAUTH_TOKEN` | yes* | Alias for `CLAUDE_CODE_TOKEN` |
28
+ | `CLAUDE_CODE_OAUTH_TOKENS` | no | Comma-separated OAuth tokens for rotation — e.g. `token1,token2,token3`. When one account hits its usage limit, automatically switches to the next token instead of sleeping. |
28
29
  | `ANTHROPIC_API_KEY` | yes* | Alternative — API key from console.anthropic.com |
29
30
  | `ALLOWED_USER_IDS` | no | Comma-separated Telegram user IDs. Leave empty to allow anyone |
30
31
  | `CWD` | no | Working directory for Claude Code. Defaults to current directory |
@@ -157,6 +158,9 @@ cc-tg works with the [cc-agent](https://github.com/Gonzih/cc-agent) MCP server t
157
158
  <string>your_token</string>
158
159
  <key>CLAUDE_CODE_TOKEN</key>
159
160
  <string>your_claude_token</string>
161
+ <!-- Optional: comma-separated OAuth tokens for automatic rotation on usage limit -->
162
+ <!-- <key>CLAUDE_CODE_OAUTH_TOKENS</key> -->
163
+ <!-- <string>token1,token2,token3</string> -->
160
164
  <key>ALLOWED_USER_IDS</key>
161
165
  <string>your_telegram_id</string>
162
166
  <key>CWD</key>
package/dist/bot.js CHANGED
@@ -14,6 +14,7 @@ import { transcribeVoice, isVoiceAvailable } from "./voice.js";
14
14
  import { CronManager } from "./cron.js";
15
15
  import { formatForTelegram, splitLongMessage } from "./formatter.js";
16
16
  import { detectUsageLimit } from "./usage-limit.js";
17
+ import { getCurrentToken, rotateToken, getTokenIndex, getTokenCount } from "./tokens.js";
17
18
  const BOT_COMMANDS = [
18
19
  { command: "start", description: "Reset session and start fresh" },
19
20
  { command: "reset", description: "Reset Claude session" },
@@ -409,7 +410,7 @@ export class CcTgBot {
409
410
  return existing;
410
411
  const claude = new ClaudeProcess({
411
412
  cwd: this.opts.cwd,
412
- token: this.opts.claudeToken,
413
+ token: getCurrentToken() || this.opts.claudeToken,
413
414
  });
414
415
  const session = {
415
416
  claude,
@@ -476,6 +477,29 @@ export class CcTgBot {
476
477
  clearTimeout(prevRetry.timer);
477
478
  this.bot.sendMessage(chatId, sig.humanMessage).catch(() => { });
478
479
  this.killSession(chatId);
480
+ // Token rotation: if this is a usage_exhausted signal and we have multiple
481
+ // tokens, rotate to the next one and retry immediately instead of sleeping.
482
+ // Only rotate if we haven't yet cycled through all tokens (attempt <= count-1).
483
+ if (sig.reason === "usage_exhausted" && getTokenCount() > 1 && attempt <= getTokenCount() - 1) {
484
+ const prevIdx = getTokenIndex();
485
+ rotateToken();
486
+ const newIdx = getTokenIndex();
487
+ const total = getTokenCount();
488
+ console.log(`[cc-tg] Token ${prevIdx + 1}/${total} exhausted, rotating to token ${newIdx + 1}/${total}`);
489
+ this.bot.sendMessage(chatId, `🔄 Token ${prevIdx + 1}/${total} exhausted, switching to token ${newIdx + 1}/${total}...`).catch(() => { });
490
+ this.pendingRetries.set(chatId, { text: lastPrompt, attempt, timer: setTimeout(() => { }, 0) });
491
+ try {
492
+ const retrySession = this.getOrCreateSession(chatId);
493
+ retrySession.currentPrompt = lastPrompt;
494
+ retrySession.isRetry = true;
495
+ retrySession.claude.sendPrompt(lastPrompt);
496
+ this.startTyping(chatId, retrySession);
497
+ }
498
+ catch (err) {
499
+ this.bot.sendMessage(chatId, `❌ Failed to retry with rotated token: ${err.message}`).catch(() => { });
500
+ }
501
+ return;
502
+ }
479
503
  if (attempt > 3) {
480
504
  this.bot.sendMessage(chatId, "❌ Claude usage limit persists after 3 retries. Please try again later.").catch(() => { });
481
505
  this.pendingRetries.delete(chatId);
@@ -735,6 +759,11 @@ export class CcTgBot {
735
759
  "This is NOT part of the user's ongoing conversation.",
736
760
  "Be concise. Report results only. No greetings or pleasantries.",
737
761
  "If there is nothing to report, say so in one sentence.",
762
+ "DEDUP RULE: If this task involves resuming or restarting interrupted agents/jobs,",
763
+ " skip any job whose task description already starts with 'RESUMING' (it is already",
764
+ " a resume attempt). Also skip any job that has a non-empty 'resumed_by' field.",
765
+ " Only spawn a resume agent for a job if resume_count < 2 (when that field exists).",
766
+ " This prevents exponential job growth when a cron re-discovers its own spawned agents.",
738
767
  "",
739
768
  `SCHEDULED TASK: ${prompt}`,
740
769
  ].join("\n");
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import os from "os";
21
21
  import { join, dirname } from "path";
22
22
  import { fileURLToPath } from "url";
23
23
  import { CcTgBot } from "./bot.js";
24
+ import { loadTokens } from "./tokens.js";
24
25
  import { Registry, startControlServer } from "@gonzih/agent-ops";
25
26
  import { Redis } from "ioredis";
26
27
  const __filename = fileURLToPath(import.meta.url);
@@ -98,6 +99,11 @@ Set one and run again:
98
99
  `);
99
100
  process.exit(1);
100
101
  }
102
+ // Load OAuth token pool (supports CLAUDE_CODE_OAUTH_TOKENS for multi-account rotation)
103
+ const tokenPool = loadTokens();
104
+ if (tokenPool.length > 1) {
105
+ console.log(`[cc-tg] Token pool loaded: ${tokenPool.length} tokens — will rotate on usage limit`);
106
+ }
101
107
  const allowedUserIds = process.env.ALLOWED_USER_IDS
102
108
  ? process.env.ALLOWED_USER_IDS.split(",").map((s) => parseInt(s.trim(), 10)).filter(Boolean)
103
109
  : [];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * OAuth token pool management.
3
+ *
4
+ * Supports CLAUDE_CODE_OAUTH_TOKENS (comma-separated list of tokens).
5
+ * Falls back to CLAUDE_CODE_OAUTH_TOKEN for single-token / backwards compat.
6
+ */
7
+ /**
8
+ * Load tokens from env vars. Called on startup; also re-callable in tests.
9
+ * Priority: CLAUDE_CODE_OAUTH_TOKENS > CLAUDE_CODE_OAUTH_TOKEN > (empty)
10
+ */
11
+ export declare function loadTokens(): string[];
12
+ /** Returns the current active token, or empty string if none configured. */
13
+ export declare function getCurrentToken(): string;
14
+ /**
15
+ * Advance to the next token (wraps around).
16
+ * Returns the new current token.
17
+ */
18
+ export declare function rotateToken(): string;
19
+ /** Zero-based index of the current token. */
20
+ export declare function getTokenIndex(): number;
21
+ /** Total number of tokens in the pool. */
22
+ export declare function getTokenCount(): number;
package/dist/tokens.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * OAuth token pool management.
3
+ *
4
+ * Supports CLAUDE_CODE_OAUTH_TOKENS (comma-separated list of tokens).
5
+ * Falls back to CLAUDE_CODE_OAUTH_TOKEN for single-token / backwards compat.
6
+ */
7
+ let tokens = [];
8
+ let currentIndex = 0;
9
+ let initialized = false;
10
+ /**
11
+ * Load tokens from env vars. Called on startup; also re-callable in tests.
12
+ * Priority: CLAUDE_CODE_OAUTH_TOKENS > CLAUDE_CODE_OAUTH_TOKEN > (empty)
13
+ */
14
+ export function loadTokens() {
15
+ const multi = process.env.CLAUDE_CODE_OAUTH_TOKENS;
16
+ if (multi) {
17
+ tokens = multi.split(",").map((t) => t.trim()).filter(Boolean);
18
+ }
19
+ else {
20
+ const single = process.env.CLAUDE_CODE_OAUTH_TOKEN;
21
+ tokens = single ? [single] : [];
22
+ }
23
+ currentIndex = 0;
24
+ initialized = true;
25
+ return tokens;
26
+ }
27
+ function ensureInitialized() {
28
+ if (!initialized)
29
+ loadTokens();
30
+ }
31
+ /** Returns the current active token, or empty string if none configured. */
32
+ export function getCurrentToken() {
33
+ ensureInitialized();
34
+ return tokens[currentIndex] ?? "";
35
+ }
36
+ /**
37
+ * Advance to the next token (wraps around).
38
+ * Returns the new current token.
39
+ */
40
+ export function rotateToken() {
41
+ ensureInitialized();
42
+ if (tokens.length === 0)
43
+ return "";
44
+ currentIndex = (currentIndex + 1) % tokens.length;
45
+ return tokens[currentIndex];
46
+ }
47
+ /** Zero-based index of the current token. */
48
+ export function getTokenIndex() {
49
+ ensureInitialized();
50
+ return currentIndex;
51
+ }
52
+ /** Total number of tokens in the pool. */
53
+ export function getTokenCount() {
54
+ ensureInitialized();
55
+ return tokens.length;
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.3.15",
3
+ "version": "0.4.1",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {