cc-claw 0.20.14 → 0.20.16

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.
Files changed (2) hide show
  1. package/dist/cli.js +649 -201
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -33,7 +33,7 @@ var VERSION;
33
33
  var init_version = __esm({
34
34
  "src/version.ts"() {
35
35
  "use strict";
36
- VERSION = true ? "0.20.14" : (() => {
36
+ VERSION = true ? "0.20.16" : (() => {
37
37
  try {
38
38
  return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
39
39
  } catch {
@@ -1333,7 +1333,8 @@ function initSchema(db3) {
1333
1333
  last_run_at TEXT,
1334
1334
  next_run_at TEXT,
1335
1335
  consecutive_failures INTEGER NOT NULL DEFAULT 0,
1336
- allow_paid_slots INTEGER NOT NULL DEFAULT 0
1336
+ allow_paid_slots INTEGER NOT NULL DEFAULT 0,
1337
+ credential_slot_id INTEGER
1337
1338
  );
1338
1339
  `);
1339
1340
  try {
@@ -1493,6 +1494,10 @@ function initSchema(db3) {
1493
1494
  db3.exec("ALTER TABLE jobs ADD COLUMN allow_paid_slots INTEGER NOT NULL DEFAULT 0");
1494
1495
  } catch {
1495
1496
  }
1497
+ try {
1498
+ db3.exec("ALTER TABLE jobs ADD COLUMN credential_slot_id INTEGER");
1499
+ } catch {
1500
+ }
1496
1501
  try {
1497
1502
  db3.exec("UPDATE jobs SET job_type = 'reflection' WHERE description LIKE '%reflection analysis%' AND job_type = 'normal'");
1498
1503
  } catch {
@@ -3317,8 +3322,8 @@ function insertJob(params) {
3317
3322
  const db3 = getDb();
3318
3323
  const result = db3.prepare(`
3319
3324
  INSERT INTO jobs (schedule_type, cron, at_time, every_ms, title, description, chat_id,
3320
- backend, model, thinking, timeout, fallbacks, session_type, channel, target, delivery_mode, timezone, job_type, allow_paid_slots)
3321
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3325
+ backend, model, thinking, timeout, fallbacks, session_type, channel, target, delivery_mode, timezone, job_type, allow_paid_slots, credential_slot_id)
3326
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3322
3327
  `).run(
3323
3328
  params.scheduleType,
3324
3329
  params.cron ?? null,
@@ -3338,7 +3343,8 @@ function insertJob(params) {
3338
3343
  params.deliveryMode ?? "announce",
3339
3344
  params.timezone ?? "UTC",
3340
3345
  params.jobType ?? "normal",
3341
- params.allowPaidSlots ? 1 : 0
3346
+ params.allowPaidSlots ? 1 : 0,
3347
+ params.credentialSlotId ?? null
3342
3348
  );
3343
3349
  return getJobById(Number(result.lastInsertRowid));
3344
3350
  }
@@ -3346,6 +3352,7 @@ function mapJobRow(row) {
3346
3352
  if (!row) return void 0;
3347
3353
  row.fallbacks = row.fallbacks ? JSON.parse(row.fallbacks) : [];
3348
3354
  row.allowPaidSlots = !!row.allowPaidSlots;
3355
+ row.credentialSlotId = row.credentialSlotId ?? null;
3349
3356
  return row;
3350
3357
  }
3351
3358
  function getJobById(id) {
@@ -3400,7 +3407,8 @@ function updateJob(id, fields) {
3400
3407
  deliveryMode: "delivery_mode",
3401
3408
  timezone: "timezone",
3402
3409
  jobType: "job_type",
3403
- allowPaidSlots: "allow_paid_slots"
3410
+ allowPaidSlots: "allow_paid_slots",
3411
+ credentialSlotId: "credential_slot_id"
3404
3412
  };
3405
3413
  for (const [key, val] of Object.entries(fields)) {
3406
3414
  const col = fieldMap[key];
@@ -3473,7 +3481,7 @@ var init_jobs = __esm({
3473
3481
  session_type as sessionType, channel, target, delivery_mode as deliveryMode,
3474
3482
  timezone, job_type as jobType, enabled, active, created_at as createdAt, last_run_at as lastRunAt,
3475
3483
  next_run_at as nextRunAt, consecutive_failures as consecutiveFailures,
3476
- allow_paid_slots as allowPaidSlots
3484
+ allow_paid_slots as allowPaidSlots, credential_slot_id as credentialSlotId
3477
3485
  FROM jobs
3478
3486
  `;
3479
3487
  }
@@ -4141,6 +4149,29 @@ var init_store5 = __esm({
4141
4149
  }
4142
4150
  });
4143
4151
 
4152
+ // src/backends/types.ts
4153
+ var types_exports = {};
4154
+ __export(types_exports, {
4155
+ BACKEND: () => BACKEND,
4156
+ isBackendId: () => isBackendId
4157
+ });
4158
+ function isBackendId(value) {
4159
+ return Object.values(BACKEND).includes(value);
4160
+ }
4161
+ var BACKEND;
4162
+ var init_types = __esm({
4163
+ "src/backends/types.ts"() {
4164
+ "use strict";
4165
+ BACKEND = {
4166
+ CLAUDE: "claude",
4167
+ GEMINI: "gemini",
4168
+ CODEX: "codex",
4169
+ CURSOR: "cursor",
4170
+ OLLAMA: "ollama"
4171
+ };
4172
+ }
4173
+ });
4174
+
4144
4175
  // src/env.ts
4145
4176
  import { homedir as homedir2 } from "os";
4146
4177
  function stripProxyVars(env) {
@@ -4223,6 +4254,11 @@ var init_strip_thinking = __esm({
4223
4254
  });
4224
4255
 
4225
4256
  // src/backends/resolve-executable.ts
4257
+ var resolve_executable_exports = {};
4258
+ __export(resolve_executable_exports, {
4259
+ clearExecutableCache: () => clearExecutableCache,
4260
+ resolveExecutable: () => resolveExecutable
4261
+ });
4226
4262
  import { existsSync as existsSync2 } from "fs";
4227
4263
  import { spawnSync } from "child_process";
4228
4264
  function resolveExecutable(config2) {
@@ -4259,6 +4295,9 @@ function resolveExecutable(config2) {
4259
4295
  cache.set(config2.binaryName, fallback);
4260
4296
  return fallback;
4261
4297
  }
4298
+ function clearExecutableCache() {
4299
+ cache.clear();
4300
+ }
4262
4301
  var cache;
4263
4302
  var init_resolve_executable = __esm({
4264
4303
  "src/backends/resolve-executable.ts"() {
@@ -6494,24 +6533,6 @@ var init_ollama2 = __esm({
6494
6533
  }
6495
6534
  });
6496
6535
 
6497
- // src/backends/types.ts
6498
- function isBackendId(value) {
6499
- return Object.values(BACKEND).includes(value);
6500
- }
6501
- var BACKEND;
6502
- var init_types = __esm({
6503
- "src/backends/types.ts"() {
6504
- "use strict";
6505
- BACKEND = {
6506
- CLAUDE: "claude",
6507
- GEMINI: "gemini",
6508
- CODEX: "codex",
6509
- CURSOR: "cursor",
6510
- OLLAMA: "ollama"
6511
- };
6512
- }
6513
- });
6514
-
6515
6536
  // src/backends/index.ts
6516
6537
  var backends_exports = {};
6517
6538
  __export(backends_exports, {
@@ -14978,8 +14999,8 @@ var init_telegram_throttle = __esm({
14978
14999
  "src/channels/telegram-throttle.ts"() {
14979
15000
  "use strict";
14980
15001
  init_log();
14981
- PER_CHAT_INTERVAL_MS = 350;
14982
- GLOBAL_INTERVAL_MS = 50;
15002
+ PER_CHAT_INTERVAL_MS = 1e3;
15003
+ GLOBAL_INTERVAL_MS = 100;
14983
15004
  MAX_RETRIES2 = 2;
14984
15005
  RETRY_DELAY_MS = 1e3;
14985
15006
  MAX_QUEUE_SIZE = 100;
@@ -15007,6 +15028,9 @@ var init_telegram_throttle = __esm({
15007
15028
  }
15008
15029
  /** Enqueue a Telegram API call with automatic pacing and 429 handling. */
15009
15030
  async send(chatId, label2, fn) {
15031
+ if (this.isPaused() && label2.startsWith("editText")) {
15032
+ throw new Error("Throttle paused (rate limit active) \u2014 edit skipped");
15033
+ }
15010
15034
  return new Promise((resolve, reject) => {
15011
15035
  if (this.queue.length >= MAX_QUEUE_SIZE) {
15012
15036
  const dropped = this.queue.shift();
@@ -15019,6 +15043,31 @@ var init_telegram_throttle = __esm({
15019
15043
  this.drain();
15020
15044
  });
15021
15045
  }
15046
+ /**
15047
+ * Best-effort send — drops silently if throttle is paused or queue is pressured.
15048
+ * Used for cosmetic calls (typing indicators, reactions) that should count toward
15049
+ * rate limits but must never queue up or amplify 429 spirals.
15050
+ */
15051
+ async tryBestEffort(chatId, label2, fn) {
15052
+ if (this.isPaused()) return void 0;
15053
+ if (this.queue.length > 10) return void 0;
15054
+ const lastChat = this.lastSendPerChat.get(chatId) ?? 0;
15055
+ if (Date.now() - lastChat < PER_CHAT_INTERVAL_MS) return void 0;
15056
+ if (Date.now() - this.lastGlobalSend < GLOBAL_INTERVAL_MS) return void 0;
15057
+ try {
15058
+ const result = await fn();
15059
+ this.recordSend(chatId);
15060
+ return result;
15061
+ } catch (err) {
15062
+ if (is429(err)) {
15063
+ const retrySec = err.parameters?.retry_after ?? 10;
15064
+ this.pausedUntil = Date.now() + retrySec * 1e3;
15065
+ if (this.pauseStartedAt === 0) this.pauseStartedAt = Date.now();
15066
+ warn(`[throttle] Best-effort ${label2} hit 429, pausing for ${retrySec}s`);
15067
+ }
15068
+ return void 0;
15069
+ }
15070
+ }
15022
15071
  /** Check whether the throttle is currently paused (rate-limited). */
15023
15072
  isPaused() {
15024
15073
  return Date.now() < this.pausedUntil;
@@ -15101,12 +15150,13 @@ var init_telegram_throttle = __esm({
15101
15150
  // ── Pause management ────────────────────────────────────────────────
15102
15151
  enterPause(retrySec, failedItem) {
15103
15152
  this.queue.unshift(failedItem);
15104
- this.pausedUntil = Date.now() + retrySec * 1e3;
15153
+ const bufferedSec = Math.ceil(retrySec * 1.5);
15154
+ this.pausedUntil = Date.now() + bufferedSec * 1e3;
15105
15155
  if (this.pauseStartedAt === 0) this.pauseStartedAt = Date.now();
15106
15156
  for (const qi of this.queue) {
15107
15157
  this.chatsPendingNotification.add(qi.chatId);
15108
15158
  }
15109
- warn(`[throttle] 429 \u2014 pausing ALL sends for ${retrySec}s (${this.queue.length} items queued)`);
15159
+ warn(`[throttle] 429 \u2014 pausing ALL sends for ${bufferedSec}s (retry_after=${retrySec}s + 50% buffer, ${this.queue.length} items queued)`);
15110
15160
  }
15111
15161
  async sendResumeNotifications() {
15112
15162
  const chats2 = new Set(this.chatsPendingNotification);
@@ -15739,6 +15789,7 @@ var init_profile = __esm({
15739
15789
  var classify_exports = {};
15740
15790
  __export(classify_exports, {
15741
15791
  classifyIntent: () => classifyIntent,
15792
+ classifyIntentAsync: () => classifyIntentAsync,
15742
15793
  getIntentStats: () => getIntentStats,
15743
15794
  resetIntentStats: () => resetIntentStats
15744
15795
  });
@@ -15749,12 +15800,17 @@ function resetIntentStats() {
15749
15800
  intentCounts.chat = 0;
15750
15801
  intentCounts.agentic = 0;
15751
15802
  }
15752
- function classifyIntent(text, chatId) {
15803
+ function classifyIntentFast(text, chatId) {
15753
15804
  const trimmed = text.trim();
15754
15805
  if (trimmed.startsWith(">>")) return "agentic";
15755
15806
  if (trimmed.startsWith("/")) return "agentic";
15756
15807
  const lower = trimmed.toLowerCase();
15757
15808
  const normalized = trimmed.replace(/^["'\u201C\u201D\u2018\u2019`\s]+|["'\u201C\u201D\u2018\u2019`\s]+$/g, "");
15809
+ const lowerNoPunct = lower.replace(/[?!.,…]+$/, "").trim();
15810
+ if (CHAT_EXACT.has(lowerNoPunct) || CHAT_EXACT.has(lower)) {
15811
+ log(`[intent] "${lower}" -> chat (exact match)`);
15812
+ return "chat";
15813
+ }
15758
15814
  const sessionId = getSessionId(chatId);
15759
15815
  if (sessionId) {
15760
15816
  const lastTs = getLastMessageTimestamp(chatId);
@@ -15762,47 +15818,148 @@ function classifyIntent(text, chatId) {
15762
15818
  const elapsed = Date.now() - lastTs;
15763
15819
  if (elapsed < 12e4 && trimmed.length < 30) {
15764
15820
  log(`[intent] "${trimmed.slice(0, 30)}" -> agentic (active session, ${(elapsed / 1e3).toFixed(0)}s ago)`);
15765
- intentCounts.agentic++;
15766
15821
  return "agentic";
15767
15822
  }
15768
15823
  }
15769
15824
  }
15770
- if (CHAT_EXACT.has(lower)) {
15771
- log(`[intent] "${lower}" -> chat (exact match)`);
15772
- intentCounts.chat++;
15773
- return "chat";
15774
- }
15775
15825
  if (trimmed.length <= 4 && /^[\p{Emoji}\s]+$/u.test(trimmed)) {
15776
15826
  log(`[intent] "${trimmed}" -> chat (emoji-only)`);
15777
- intentCounts.chat++;
15778
15827
  return "chat";
15779
15828
  }
15780
15829
  for (const pattern of STRUCTURAL_PATTERNS) {
15781
15830
  if (pattern.test(normalized)) {
15782
15831
  log(`[intent] "${trimmed.slice(0, 40)}..." -> agentic (structural: ${pattern})`);
15783
- intentCounts.agentic++;
15784
15832
  return "agentic";
15785
15833
  }
15786
15834
  }
15787
15835
  for (const pattern of MUTATION_PATTERNS) {
15788
15836
  if (pattern.test(normalized)) {
15789
15837
  log(`[intent] "${trimmed.slice(0, 40)}..." -> agentic (mutation: ${pattern})`);
15790
- intentCounts.agentic++;
15791
15838
  return "agentic";
15792
15839
  }
15793
15840
  }
15794
15841
  for (const pattern of CHAT_QUESTION_PATTERNS) {
15795
15842
  if (pattern.test(normalized)) {
15796
15843
  log(`[intent] "${trimmed.slice(0, 40)}..." -> chat (question: ${pattern})`);
15797
- intentCounts.chat++;
15798
15844
  return "chat";
15799
15845
  }
15800
15846
  }
15801
- log(`[intent] "${trimmed.slice(0, 40)}..." -> agentic (default)`);
15847
+ return null;
15848
+ }
15849
+ async function classifyWithLlm(text) {
15850
+ try {
15851
+ const ollamaResult = await classifyWithOllama(text);
15852
+ if (ollamaResult) return ollamaResult;
15853
+ } catch {
15854
+ }
15855
+ try {
15856
+ const cliResult = await classifyWithSummarizerCli(text);
15857
+ if (cliResult) return cliResult;
15858
+ } catch {
15859
+ }
15860
+ return null;
15861
+ }
15862
+ async function classifyWithOllama(text) {
15863
+ const ollamaService = await Promise.resolve().then(() => (init_service(), service_exports));
15864
+ const ollamaClient = await Promise.resolve().then(() => (init_client(), client_exports));
15865
+ const servers = ollamaService.listServers();
15866
+ const onlineServer = servers.find((s) => s.status === "online");
15867
+ if (!onlineServer) return null;
15868
+ const models = ollamaService.listModels(onlineServer.name);
15869
+ if (models.length === 0) return null;
15870
+ const sorted = [...models].sort(
15871
+ (a, b) => (a.sizeBytes ?? Infinity) - (b.sizeBytes ?? Infinity)
15872
+ );
15873
+ const model2 = sorted[0].name;
15874
+ const result = await ollamaClient.chat(
15875
+ onlineServer.baseUrl,
15876
+ model2,
15877
+ [{ role: "user", content: LLM_CLASSIFY_PROMPT + text.slice(0, 500) }],
15878
+ { timeoutMs: LLM_CLASSIFY_TIMEOUT_MS, maxTokens: 5, temperature: 0 }
15879
+ );
15880
+ return parseClassifyResponse(result.text);
15881
+ }
15882
+ async function classifyWithSummarizerCli(text) {
15883
+ const { getSummarizer: getSummarizer3 } = await Promise.resolve().then(() => (init_chat_settings(), chat_settings_exports));
15884
+ const { getAdapter: getAdapter4, getAllAdapters: getAllAdapters5 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
15885
+ const config2 = getSummarizer3("__global__");
15886
+ let backendId = config2.backend;
15887
+ let modelName = config2.model;
15888
+ if (!backendId) {
15889
+ const adapters2 = getAllAdapters5();
15890
+ const cheapAdapter = adapters2.find((a) => a.summarizerModel);
15891
+ if (!cheapAdapter) return null;
15892
+ backendId = cheapAdapter.id;
15893
+ modelName = cheapAdapter.summarizerModel;
15894
+ }
15895
+ const adapter = getAdapter4(backendId);
15896
+ if (!adapter) return null;
15897
+ const model2 = modelName ?? adapter.summarizerModel;
15898
+ const { spawn: spawn8 } = await import("child_process");
15899
+ const { resolveExecutable: resolveExecutable4 } = await Promise.resolve().then(() => (init_resolve_executable(), resolve_executable_exports));
15900
+ const exe = resolveExecutable4(adapter.id);
15901
+ if (!exe) return null;
15902
+ return new Promise((resolve) => {
15903
+ const timeout = setTimeout(() => {
15904
+ proc.kill("SIGKILL");
15905
+ resolve(null);
15906
+ }, LLM_CLASSIFY_TIMEOUT_MS);
15907
+ const args = adapter.id === "claude" ? ["-p", LLM_CLASSIFY_PROMPT + text.slice(0, 500), "--model", model2, "--no-input"] : ["-p", LLM_CLASSIFY_PROMPT + text.slice(0, 500), "--model", model2];
15908
+ const proc = spawn8(exe, args, {
15909
+ stdio: ["ignore", "pipe", "ignore"],
15910
+ timeout: LLM_CLASSIFY_TIMEOUT_MS + 1e3
15911
+ });
15912
+ let output2 = "";
15913
+ proc.stdout?.on("data", (chunk) => {
15914
+ output2 += chunk.toString();
15915
+ });
15916
+ proc.on("close", () => {
15917
+ clearTimeout(timeout);
15918
+ resolve(parseClassifyResponse(output2));
15919
+ });
15920
+ proc.on("error", () => {
15921
+ clearTimeout(timeout);
15922
+ resolve(null);
15923
+ });
15924
+ });
15925
+ }
15926
+ function parseClassifyResponse(text) {
15927
+ const lower = text.trim().toLowerCase();
15928
+ if (lower.includes("chat")) return "chat";
15929
+ if (lower.includes("task")) return "agentic";
15930
+ return null;
15931
+ }
15932
+ function classifyIntent(text, chatId) {
15933
+ const fast = classifyIntentFast(text, chatId);
15934
+ if (fast) {
15935
+ intentCounts[fast]++;
15936
+ return fast;
15937
+ }
15938
+ log(`[intent] "${text.slice(0, 40)}..." -> agentic (default)`);
15939
+ intentCounts.agentic++;
15940
+ return "agentic";
15941
+ }
15942
+ async function classifyIntentAsync(text, chatId) {
15943
+ const fast = classifyIntentFast(text, chatId);
15944
+ if (fast) {
15945
+ intentCounts[fast]++;
15946
+ return fast;
15947
+ }
15948
+ try {
15949
+ const llmResult = await classifyWithLlm(text);
15950
+ if (llmResult) {
15951
+ log(`[intent] "${text.slice(0, 40)}..." -> ${llmResult} (LLM)`);
15952
+ intentCounts[llmResult]++;
15953
+ return llmResult;
15954
+ }
15955
+ } catch (err) {
15956
+ warn(`[intent] LLM classification failed: ${err instanceof Error ? err.message : err}`);
15957
+ }
15958
+ log(`[intent] "${text.slice(0, 40)}..." -> agentic (default, LLM unavailable)`);
15802
15959
  intentCounts.agentic++;
15803
15960
  return "agentic";
15804
15961
  }
15805
- var intentCounts, CHAT_EXACT, MUTATION_PATTERNS, CHAT_QUESTION_PATTERNS, STRUCTURAL_PATTERNS;
15962
+ var intentCounts, CHAT_EXACT, MUTATION_PATTERNS, CHAT_QUESTION_PATTERNS, STRUCTURAL_PATTERNS, LLM_CLASSIFY_PROMPT, LLM_CLASSIFY_TIMEOUT_MS;
15806
15963
  var init_classify = __esm({
15807
15964
  "src/intent/classify.ts"() {
15808
15965
  "use strict";
@@ -15898,6 +16055,13 @@ var init_classify = __esm({
15898
16055
  /\b(error|bug|crash|fail|broken|issue|problem|exception|stack\s?trace)\b/i,
15899
16056
  /\b(function|class|const|let|var|import|export|return|async|await)\b/i
15900
16057
  ];
16058
+ LLM_CLASSIFY_PROMPT = `Classify this message as either "chat" or "task".
16059
+ - "chat" = greeting, small talk, acknowledgment, question, opinion, or anything conversational
16060
+ - "task" = request to DO something (research, draft, analyze, code, create, fix, etc.)
16061
+ Reply with ONLY the word "chat" or "task", nothing else.
16062
+
16063
+ Message: `;
16064
+ LLM_CLASSIFY_TIMEOUT_MS = 3e3;
15901
16065
  }
15902
16066
  });
15903
16067
 
@@ -15928,6 +16092,17 @@ var init_types3 = __esm({
15928
16092
  });
15929
16093
 
15930
16094
  // src/scheduler/wizard.ts
16095
+ var wizard_exports = {};
16096
+ __export(wizard_exports, {
16097
+ cancelWizard: () => cancelWizard,
16098
+ getPendingJob: () => getPendingJob,
16099
+ handleWizardCallback: () => handleWizardCallback,
16100
+ handleWizardText: () => handleWizardText,
16101
+ hasPendingWizard: () => hasPendingWizard,
16102
+ resolveSlotLabel: () => resolveSlotLabel,
16103
+ startEditWizard: () => startEditWizard,
16104
+ startWizard: () => startWizard
16105
+ });
15931
16106
  function hasPendingWizard(chatId) {
15932
16107
  return pendingJobs.has(chatId);
15933
16108
  }
@@ -15948,6 +16123,9 @@ function resetWizardTimeout(chatId) {
15948
16123
  log(`[wizard] Auto-cancelled stale wizard for chat ${chatId}`);
15949
16124
  }, WIZARD_TIMEOUT_MS));
15950
16125
  }
16126
+ function getPendingJob(chatId) {
16127
+ return pendingJobs.get(chatId);
16128
+ }
15951
16129
  function parseNaturalLanguage(input) {
15952
16130
  const lower = input.toLowerCase();
15953
16131
  const minMatch = lower.match(/every\s+(\d+)\s+min/);
@@ -16124,8 +16302,8 @@ async function handleWizardText(chatId, text, channel) {
16124
16302
  case "thinking": {
16125
16303
  const level = text.trim().toLowerCase();
16126
16304
  pending.thinking = level || "auto";
16127
- pending.step = "timeout";
16128
- await promptTimeout(chatId, channel);
16305
+ pending.step = "account";
16306
+ await promptAccount(chatId, channel);
16129
16307
  break;
16130
16308
  }
16131
16309
  case "timeout": {
@@ -16218,6 +16396,17 @@ async function handleWizardCallback(chatId, data, channel) {
16218
16396
  await promptThinking(chatId, channel);
16219
16397
  } else if (data.startsWith("sched:thinking:")) {
16220
16398
  pending.thinking = data.slice(15);
16399
+ pending.step = "account";
16400
+ await promptAccount(chatId, channel);
16401
+ } else if (data.startsWith("sched:slot:")) {
16402
+ const val = data.slice(11);
16403
+ if (val === "auto") {
16404
+ pending.credentialSlotId = null;
16405
+ } else {
16406
+ pending.credentialSlotId = parseInt(val, 10);
16407
+ }
16408
+ const slotLabel = resolveSlotLabel(pending.backend, pending.credentialSlotId);
16409
+ await channel.sendText(chatId, `Account: ${slotLabel}`, { parseMode: "plain" });
16221
16410
  pending.step = "timeout";
16222
16411
  await promptTimeout(chatId, channel);
16223
16412
  } else if (data.startsWith("sched:timeout:")) {
@@ -16323,9 +16512,52 @@ async function promptThinking(chatId, channel) {
16323
16512
  await channel.sendKeyboard(chatId, "Thinking/effort level for this job?", buttons);
16324
16513
  } else {
16325
16514
  pending.thinking = "auto";
16515
+ pending.step = "account";
16516
+ await promptAccount(chatId, channel);
16517
+ }
16518
+ }
16519
+ async function promptAccount(chatId, channel) {
16520
+ const pending = pendingJobs.get(chatId);
16521
+ if (!pending?.backend) {
16522
+ if (pending) pending.step = "timeout";
16523
+ await promptTimeout(chatId, channel);
16524
+ return;
16525
+ }
16526
+ const isGemini = pending.backend === BACKEND.GEMINI;
16527
+ const slots = isGemini ? getGeminiSlots() : getBackendSlots(pending.backend);
16528
+ const enabledSlots = slots.filter((s) => s.enabled);
16529
+ if (enabledSlots.length === 0) {
16530
+ pending.credentialSlotId = null;
16326
16531
  pending.step = "timeout";
16327
16532
  await promptTimeout(chatId, channel);
16533
+ return;
16534
+ }
16535
+ if (typeof channel.sendKeyboard !== "function") {
16536
+ await channel.sendText(chatId, `Enter account slot ID, or "auto" for rotation:`, { parseMode: "plain" });
16537
+ return;
16328
16538
  }
16539
+ const buttons = [
16540
+ [{ label: "\u{1F504} Auto (rotate)", data: "sched:slot:auto" }]
16541
+ ];
16542
+ for (const slot of enabledSlots) {
16543
+ const s = slot;
16544
+ const icon = s.slotType === "api_key" ? "\u{1F511}" : "\u{1F4E7}";
16545
+ const label2 = s.label || s.email || `Slot #${s.id}`;
16546
+ const type = s.slotType === "api_key" ? "API key" : "OAuth";
16547
+ buttons.push([{ label: `${icon} ${label2} (${type})`, data: `sched:slot:${s.id}` }]);
16548
+ }
16549
+ await channel.sendKeyboard(chatId, "Which account should this job use?", buttons);
16550
+ }
16551
+ function resolveSlotLabel(backend2, slotId) {
16552
+ if (!slotId || !backend2) return "Auto (rotate)";
16553
+ const isGemini = backend2 === BACKEND.GEMINI;
16554
+ const slots = isGemini ? getGeminiSlots() : getBackendSlots(backend2);
16555
+ const slot = slots.find((s) => s.id === slotId);
16556
+ if (!slot) return `Slot #${slotId} (unknown)`;
16557
+ const icon = slot.slotType === "api_key" ? "\u{1F511}" : "\u{1F4E7}";
16558
+ const label2 = slot.label || slot.email || `Slot #${slot.id}`;
16559
+ const type = slot.slotType === "api_key" ? "API key" : "OAuth";
16560
+ return `${icon} ${label2} (${type})`;
16329
16561
  }
16330
16562
  async function promptTimeout(chatId, channel) {
16331
16563
  if (typeof channel.sendKeyboard !== "function") {
@@ -16380,6 +16612,7 @@ async function promptConfirm(chatId, channel) {
16380
16612
  if (!pending) return;
16381
16613
  const backendName = pending.backend ? getAdapter(pending.backend).displayName : "default";
16382
16614
  const timeoutLabel = pending.timeout ? `${pending.timeout}s (${Math.round(pending.timeout / 60)} min)` : `Default (${TIMEOUT_DEFAULT_SECONDS}s)`;
16615
+ const accountLabel = resolveSlotLabel(pending.backend, pending.credentialSlotId);
16383
16616
  const lines = [
16384
16617
  "Job configuration:",
16385
16618
  "",
@@ -16388,6 +16621,7 @@ async function promptConfirm(chatId, channel) {
16388
16621
  ` Timezone: ${pending.timezone ?? "UTC"}`,
16389
16622
  ` Backend: ${backendName}`,
16390
16623
  ` Model: ${pending.model ?? "default"}`,
16624
+ ` Account: ${accountLabel}`,
16391
16625
  ` Thinking: ${pending.thinking ?? "auto"}`,
16392
16626
  ` Timeout: ${timeoutLabel}`,
16393
16627
  ` Session: ${pending.sessionType ?? "isolated"}`,
@@ -16445,7 +16679,8 @@ ${pending.task}`, {
16445
16679
  channel: pending.channel ?? null,
16446
16680
  target: pending.target ?? null,
16447
16681
  deliveryMode: pending.deliveryMode ?? "announce",
16448
- timezone: pending.timezone ?? "UTC"
16682
+ timezone: pending.timezone ?? "UTC",
16683
+ credentialSlotId: pending.credentialSlotId ?? null
16449
16684
  };
16450
16685
  try {
16451
16686
  if (editJobId) {
@@ -16507,7 +16742,8 @@ async function startEditWizard(chatId, jobId, channel) {
16507
16742
  sessionType: job.sessionType,
16508
16743
  deliveryMode: job.deliveryMode,
16509
16744
  channel: job.channel ?? void 0,
16510
- target: job.target ?? void 0
16745
+ target: job.target ?? void 0,
16746
+ credentialSlotId: job.credentialSlotId ?? void 0
16511
16747
  };
16512
16748
  if (pendingJobs.has(chatId)) cancelWizard(chatId);
16513
16749
  pendingJobs.set(chatId, pending);
@@ -17273,10 +17509,11 @@ var init_live_status = __esm({
17273
17509
  "use strict";
17274
17510
  init_log();
17275
17511
  init_helpers();
17276
- FLUSH_INTERVAL_DM_MS = 1e3;
17277
- FLUSH_INTERVAL_GROUP_MS = 3e3;
17512
+ init_telegram_throttle();
17513
+ FLUSH_INTERVAL_DM_MS = 2e3;
17514
+ FLUSH_INTERVAL_GROUP_MS = 5e3;
17278
17515
  MAX_THINKING_CHARS = 800;
17279
- GLOBAL_MIN_GAP_MS = 500;
17516
+ GLOBAL_MIN_GAP_MS = 1e3;
17280
17517
  globalLastFlushAt = 0;
17281
17518
  TRIM_THRESHOLD = 3500;
17282
17519
  MAX_ENTRIES = 200;
@@ -17382,6 +17619,8 @@ var init_live_status = __esm({
17382
17619
  if (this.consecutiveEditFailures >= _LiveStatusMessage.MAX_EDIT_FAILURES) return;
17383
17620
  if (Date.now() < this.nextFlushAllowedAt) return;
17384
17621
  if (!canFlushGlobally()) return;
17622
+ const throttleState = getThrottleState();
17623
+ if (throttleState?.isPaused) return;
17385
17624
  const deduped = dedupThinking(this.entries);
17386
17625
  const body = renderEntries(deduped, this.modelLabel, Date.now() - this.startTime, this.hasTrimmed);
17387
17626
  if (body === this.lastRendered) return;
@@ -19850,15 +20089,20 @@ async function sendJobDetail(chatId, jobId, channel, messageId) {
19850
20089
  \u26A0\uFE0F ${job.consecutiveFailures} consecutive failures` : "";
19851
20090
  const runs = getJobRuns(jobId, 1);
19852
20091
  const lastRunStatus = runs.length > 0 ? runs[0].status : null;
20092
+ const thinking2 = job.thinking ?? "auto";
20093
+ const accountLabel = resolveSlotLabel(job.backend ?? void 0, job.credentialSlotId);
19853
20094
  const lines = [
19854
20095
  `Job #${job.id}: ${job.title ?? job.description}`,
19855
20096
  buildSectionHeader("", 22),
19856
20097
  ...job.title ? [`Task: ${job.description}`] : [],
19857
20098
  `Runs: ${schedule2}${tz}`,
19858
- `Backend: ${backend2} | Model: ${model2}`,
20099
+ "",
19859
20100
  `Last run: ${lastRun}${lastRunStatus ? ` (${lastRunStatus})` : ""}`,
19860
20101
  `Next run: ${nextRun}`,
19861
- `Status: ${status}${failures}`
20102
+ `Status: ${status}${failures}`,
20103
+ "",
20104
+ `\u{1F3F7} ${backend2} \xB7 ${model2} \xB7 ${thinking2}`,
20105
+ `\u{1F464} ${accountLabel}`
19862
20106
  ];
19863
20107
  const text = lines.join("\n");
19864
20108
  if (typeof channel.sendKeyboard !== "function") {
@@ -19875,6 +20119,7 @@ async function sendJobDetail(chatId, jobId, channel, messageId) {
19875
20119
  }
19876
20120
  actionRow1.push({ label: "Edit", data: `job:edit:${job.id}` });
19877
20121
  const actionRow2 = [
20122
+ { label: "\u{1F9EA} Test Model", data: `job:test:${job.id}` },
19878
20123
  { label: "View Runs", data: `job:runs:${job.id}`, style: "primary" }
19879
20124
  ];
19880
20125
  if (job.active) {
@@ -20071,6 +20316,7 @@ var init_ui = __esm({
20071
20316
  init_format_time();
20072
20317
  init_cron();
20073
20318
  init_humanize();
20319
+ init_wizard();
20074
20320
  init_stt();
20075
20321
  init_helpers();
20076
20322
  ROTATION_MODE_LABELS = {
@@ -21933,8 +22179,8 @@ var init_types4 = __esm({
21933
22179
  });
21934
22180
 
21935
22181
  // src/council/wizard.ts
21936
- var wizard_exports = {};
21937
- __export(wizard_exports, {
22182
+ var wizard_exports2 = {};
22183
+ __export(wizard_exports2, {
21938
22184
  buildSelectKeyboard: () => buildSelectKeyboard,
21939
22185
  cancelCouncil: () => cancelCouncil,
21940
22186
  getCouncilState: () => getCouncilState,
@@ -23777,7 +24023,7 @@ async function handleCouncilCommand(chatId, commandArgs, msg, channel) {
23777
24023
  await channel.sendText(chatId, "Council requires DASHBOARD_ENABLED=1 (uses the agent orchestrator).", { parseMode: "plain" });
23778
24024
  return;
23779
24025
  }
23780
- const { startCouncilWizard: startCouncilWizard2, buildSelectKeyboard: buildSelectKeyboard2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports));
24026
+ const { startCouncilWizard: startCouncilWizard2, buildSelectKeyboard: buildSelectKeyboard2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports2));
23781
24027
  startCouncilWizard2(chatId);
23782
24028
  if (typeof channel.sendKeyboard === "function") {
23783
24029
  const { text, buttons } = buildSelectKeyboard2(chatId);
@@ -24378,12 +24624,99 @@ ${plan.originalMessage}`;
24378
24624
  } else if (data.startsWith("sched:")) {
24379
24625
  await handleWizardCallback(chatId, data, channel);
24380
24626
  } else if (data.startsWith("job:")) {
24627
+ async function showJobAccountPicker(cid, jobId, backend2, model2, thinking2, ch) {
24628
+ const isGemini = backend2 === "gemini";
24629
+ const slots = isGemini ? getGeminiSlots() : getBackendSlots(backend2);
24630
+ const enabledSlots = slots.filter((s) => s.enabled);
24631
+ if (enabledSlots.length === 0 || typeof ch.sendKeyboard !== "function") {
24632
+ const { updateJob: updateJobFields } = await Promise.resolve().then(() => (init_store5(), store_exports5));
24633
+ updateJobFields(jobId, { backend: backend2, model: model2, thinking: thinking2, credentialSlotId: null });
24634
+ const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
24635
+ const adapter = getAdapter4(backend2);
24636
+ await ch.sendText(cid, `Job #${jobId} updated: ${adapter.displayName} / ${model2} / ${thinking2}`, { parseMode: "plain" });
24637
+ await sendJobDetail(cid, jobId, ch);
24638
+ return;
24639
+ }
24640
+ const rows = [
24641
+ [{ label: "\u{1F504} Auto (rotate)", data: `job:setaccount:${jobId}:${backend2}:${model2}:${thinking2}:auto` }]
24642
+ ];
24643
+ for (const slot of enabledSlots) {
24644
+ const s = slot;
24645
+ const icon = s.slotType === "api_key" ? "\u{1F511}" : "\u{1F4E7}";
24646
+ const label2 = s.label || s.email || `Slot #${s.id}`;
24647
+ const type = s.slotType === "api_key" ? "API key" : "OAuth";
24648
+ rows.push([{ label: `${icon} ${label2} (${type})`, data: `job:setaccount:${jobId}:${backend2}:${model2}:${thinking2}:${s.id}` }]);
24649
+ }
24650
+ rows.push([{ label: "\u2190 Back", data: `job:editbackend:${jobId}` }]);
24651
+ await ch.sendKeyboard(cid, `Which account for Job #${jobId}?`, rows);
24652
+ }
24381
24653
  const rest = data.slice(4);
24382
24654
  if (rest === "back") {
24383
24655
  await sendJobsBoard(chatId, channel, 1);
24384
24656
  } else if (rest.startsWith("view:")) {
24385
24657
  const id = parseInt(rest.slice(5), 10);
24386
24658
  await sendJobDetail(chatId, id, channel, messageId);
24659
+ } else if (rest.startsWith("test:")) {
24660
+ const id = parseInt(rest.slice(5), 10);
24661
+ const { getJobById: getJob } = await Promise.resolve().then(() => (init_store5(), store_exports5));
24662
+ const testJob = getJob(id);
24663
+ if (!testJob) {
24664
+ await channel.sendText(chatId, `Job #${id} not found.`, { parseMode: "plain" });
24665
+ } else {
24666
+ await channel.sendText(chatId, `\u{1F9EA} Testing model: ${testJob.backend ?? "default"}/${testJob.model ?? "default"}...`, { parseMode: "plain" });
24667
+ try {
24668
+ const { askAgent: askAgent3 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
24669
+ const { BACKEND: BACKEND2 } = await Promise.resolve().then(() => (init_types(), types_exports));
24670
+ const { pinChatGeminiSlot: pinChatGeminiSlot4, pinChatBackendSlot: pinChatBackendSlot3 } = await Promise.resolve().then(() => (init_store5(), store_exports5));
24671
+ const testChatId = `cron:test:${id}:${Date.now()}`;
24672
+ const backend2 = testJob.backend ?? "claude";
24673
+ if (testJob.credentialSlotId) {
24674
+ if (backend2 === BACKEND2.GEMINI) {
24675
+ pinChatGeminiSlot4(testChatId, testJob.credentialSlotId);
24676
+ } else {
24677
+ pinChatBackendSlot3(testChatId, backend2, testJob.credentialSlotId);
24678
+ }
24679
+ }
24680
+ const t0 = Date.now();
24681
+ const resp = await askAgent3(testChatId, 'Reply with exactly: "pong"', {
24682
+ backend: backend2,
24683
+ model: testJob.model ?? void 0,
24684
+ bootstrapTier: "chat",
24685
+ maxTurns: 1,
24686
+ timeoutMs: 3e4,
24687
+ permMode: "yolo"
24688
+ });
24689
+ const elapsed = ((Date.now() - t0) / 1e3).toFixed(1);
24690
+ const got = resp.text?.trim().slice(0, 100) || "(empty)";
24691
+ const { resolveSlotLabel: resolveSlotLabel2 } = await Promise.resolve().then(() => (init_wizard(), wizard_exports));
24692
+ const acct = resolveSlotLabel2(backend2, testJob.credentialSlotId);
24693
+ if (typeof channel.sendKeyboard === "function") {
24694
+ await channel.sendKeyboard(
24695
+ chatId,
24696
+ `\u2705 Model test passed (${elapsed}s)
24697
+
24698
+ Backend: ${backend2}
24699
+ Model: ${testJob.model ?? "default"}
24700
+ Account: ${acct}
24701
+ Response: "${got}"`,
24702
+ [[{ label: "\u2190 Back to Job", data: `job:view:${id}` }]]
24703
+ );
24704
+ }
24705
+ } catch (err) {
24706
+ const msg = err instanceof Error ? err.message : String(err);
24707
+ if (typeof channel.sendKeyboard === "function") {
24708
+ await channel.sendKeyboard(
24709
+ chatId,
24710
+ `\u274C Model test failed
24711
+
24712
+ ${msg.slice(0, 500)}`,
24713
+ [[{ label: "\u2190 Back to Job", data: `job:view:${id}` }]]
24714
+ );
24715
+ } else {
24716
+ await channel.sendText(chatId, `\u274C Model test failed: ${msg.slice(0, 300)}`, { parseMode: "plain" });
24717
+ }
24718
+ }
24719
+ }
24387
24720
  } else if (rest.startsWith("run:")) {
24388
24721
  const id = parseInt(rest.slice(4), 10);
24389
24722
  await channel.sendText(chatId, `Triggering job #${id}...`, { parseMode: "plain" });
@@ -24522,11 +24855,42 @@ What do you want to change?`,
24522
24855
  const id = parseInt(parts[0], 10);
24523
24856
  const backend2 = parts[1];
24524
24857
  const model2 = parts.slice(2).join(":");
24858
+ const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
24859
+ const adapter = getAdapter4(backend2);
24860
+ const modelInfo = adapter.availableModels[model2];
24861
+ if (modelInfo?.thinking === "adjustable" && modelInfo.thinkingLevels && typeof channel.sendKeyboard === "function") {
24862
+ const rows = modelInfo.thinkingLevels.map((level) => [{
24863
+ label: level === "auto" ? "Auto (default)" : level.replace("_", " ").replace(/\b\w/g, (c) => c.toUpperCase()),
24864
+ data: `job:setthinking:${id}:${backend2}:${model2}:${level}`
24865
+ }]);
24866
+ rows.push([{ label: "\u2190 Back", data: `job:setbackend:${id}:${backend2}` }]);
24867
+ await channel.sendKeyboard(chatId, `Thinking level for Job #${id}?`, rows);
24868
+ } else {
24869
+ await showJobAccountPicker(chatId, id, backend2, model2, "auto", channel);
24870
+ }
24871
+ } else if (rest.startsWith("setthinking:")) {
24872
+ const parts = rest.slice(12).split(":");
24873
+ const id = parseInt(parts[0], 10);
24874
+ const backend2 = parts[1];
24875
+ const model2 = parts[2];
24876
+ const thinking2 = parts[3] ?? "auto";
24877
+ await showJobAccountPicker(chatId, id, backend2, model2, thinking2, channel);
24878
+ } else if (rest.startsWith("setaccount:")) {
24879
+ const parts = rest.slice(11).split(":");
24880
+ const id = parseInt(parts[0], 10);
24881
+ const backend2 = parts[1];
24882
+ const model2 = parts[2];
24883
+ const thinking2 = parts[3];
24884
+ const slotVal = parts[4];
24885
+ const credentialSlotId = slotVal === "auto" ? null : parseInt(slotVal, 10);
24525
24886
  const { updateJob: updateJobFields } = await Promise.resolve().then(() => (init_store5(), store_exports5));
24526
- updateJobFields(id, { backend: backend2, model: model2 });
24887
+ updateJobFields(id, { backend: backend2, model: model2, thinking: thinking2, credentialSlotId });
24527
24888
  const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
24528
24889
  const adapter = getAdapter4(backend2);
24529
- await channel.sendText(chatId, `Job #${id} updated: ${adapter.displayName} / ${model2}`, { parseMode: "plain" });
24890
+ const { resolveSlotLabel: resolveSlotLabel2 } = await Promise.resolve().then(() => (init_wizard(), wizard_exports));
24891
+ const accountLabel = resolveSlotLabel2(backend2, credentialSlotId);
24892
+ await channel.sendText(chatId, `Job #${id} updated: ${adapter.displayName} / ${model2} / ${thinking2}
24893
+ Account: ${accountLabel}`, { parseMode: "plain" });
24530
24894
  await sendJobDetail(chatId, id, channel);
24531
24895
  } else if (rest.startsWith("edittimeout:")) {
24532
24896
  const id = parseInt(rest.slice(12), 10);
@@ -24863,7 +25227,7 @@ ${rotationNote}`, { parseMode: "html" });
24863
25227
  const { getAdapter: getAdapter4 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
24864
25228
  const toggleAdapter = getAdapter4(backend2);
24865
25229
  const label2 = toggleAdapter.availableModels[model2]?.label ?? model2;
24866
- const { toggleParticipant: toggleParticipant2, buildSelectKeyboard: buildSelectKeyboard2, hasPendingCouncil: hasPendingCouncil2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports));
25230
+ const { toggleParticipant: toggleParticipant2, buildSelectKeyboard: buildSelectKeyboard2, hasPendingCouncil: hasPendingCouncil2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports2));
24867
25231
  if (!hasPendingCouncil2(chatId)) {
24868
25232
  await channel.sendText(chatId, "No council wizard active. Use /council to start.", { parseMode: "plain" });
24869
25233
  return;
@@ -24876,7 +25240,7 @@ ${rotationNote}`, { parseMode: "html" });
24876
25240
  return;
24877
25241
  }
24878
25242
  if (action === "start") {
24879
- const { getCouncilState: getCouncilState2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports));
25243
+ const { getCouncilState: getCouncilState2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports2));
24880
25244
  const state = getCouncilState2(chatId);
24881
25245
  if (!state || state.selected.size < 2) {
24882
25246
  await channel.sendText(chatId, "Select at least 2 models first.", { parseMode: "plain" });
@@ -24890,7 +25254,7 @@ Now type the question you want them to debate.`, { parseMode: "plain" });
24890
25254
  return;
24891
25255
  }
24892
25256
  if (action === "cancel") {
24893
- const { cancelCouncil: cancelCouncil2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports));
25257
+ const { cancelCouncil: cancelCouncil2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports2));
24894
25258
  cancelCouncil2(chatId);
24895
25259
  await channel.sendText(chatId, "Council cancelled.", { parseMode: "plain" });
24896
25260
  return;
@@ -25937,7 +26301,7 @@ async function handleText(msg, channel) {
25937
26301
  await channel.sendText(chatId, limitMsg, { parseMode: "plain" });
25938
26302
  return;
25939
26303
  }
25940
- let intent = classifyIntent(text, chatId);
26304
+ let intent = await classifyIntentAsync(text, chatId);
25941
26305
  const cleanText = text.startsWith(">>") ? text.slice(2).trim() : text;
25942
26306
  let bootstrapTier = intent === "chat" ? "chat" : void 0;
25943
26307
  let maxTurns = void 0;
@@ -25981,9 +26345,10 @@ async function handleText(msg, channel) {
25981
26345
  agentMode: effectiveAgentMode
25982
26346
  });
25983
26347
  if (response.text) {
25984
- storePendingPlan(chatId, response.text, text);
26348
+ const revisedPlan = response.text.replace(/\[REACT:.+?\]/g, "").replace(/\[SEND_FILE:.+?\]/g, "").replace(/\[GENERATE_IMAGE:.+?\]/g, "").replace(/\[HISTORY_SEARCH:[^\]]+\]/g, "").trim();
26349
+ storePendingPlan(chatId, revisedPlan, text);
25985
26350
  if (typeof channel.sendKeyboard === "function") {
25986
- await channel.sendKeyboard(chatId, `\u{1F50D} ${response.text}`, [
26351
+ await channel.sendKeyboard(chatId, `\u{1F50D} ${revisedPlan}`, [
25987
26352
  [
25988
26353
  { label: "\u2705 Approve", data: "exec:approve", style: "success" },
25989
26354
  { label: "\u274C Reject", data: "exec:reject", style: "danger" }
@@ -26055,7 +26420,7 @@ You're still in discussion mode \u2014 try again or click a button to exit.`, {
26055
26420
  }
26056
26421
  }
26057
26422
  {
26058
- const { hasPendingCouncil: hasPendingCouncil2, getCouncilState: getCouncilState2, setCouncilQuestion: setCouncilQuestion2, cancelCouncil: cancelCouncil2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports));
26423
+ const { hasPendingCouncil: hasPendingCouncil2, getCouncilState: getCouncilState2, setCouncilQuestion: setCouncilQuestion2, cancelCouncil: cancelCouncil2 } = await Promise.resolve().then(() => (init_wizard2(), wizard_exports2));
26059
26424
  if (hasPendingCouncil2(chatId)) {
26060
26425
  const state = getCouncilState2(chatId);
26061
26426
  if (state?.step === "question") {
@@ -26119,7 +26484,7 @@ Debating: "${question.slice(0, 100)}${question.length > 100 ? "\u2026" : ""}"`,
26119
26484
  })) {
26120
26485
  const planDirective = buildPlanningDirective();
26121
26486
  let typingActive2 = true;
26122
- const typingLoop2 = async () => {
26487
+ const typingLoop = async () => {
26123
26488
  while (typingActive2) {
26124
26489
  try {
26125
26490
  await channel.sendTyping?.(chatId);
@@ -26128,7 +26493,7 @@ Debating: "${question.slice(0, 100)}${question.length > 100 ? "\u2026" : ""}"`,
26128
26493
  await new Promise((r) => setTimeout(r, 4e3));
26129
26494
  }
26130
26495
  };
26131
- typingLoop2().catch(() => {
26496
+ typingLoop().catch(() => {
26132
26497
  });
26133
26498
  try {
26134
26499
  const planResponse = await askAgent(chatId, cleanText || text, {
@@ -26169,18 +26534,23 @@ Debating: "${question.slice(0, 100)}${question.length > 100 ? "\u2026" : ""}"`,
26169
26534
  }
26170
26535
  return;
26171
26536
  }
26172
- let typingActive = true;
26173
- const typingLoop = async () => {
26174
- while (typingActive) {
26175
- try {
26176
- await channel.sendTyping?.(chatId);
26177
- } catch {
26537
+ const verboseForTyping = settings.getVerboseLevel();
26538
+ const showThinkingForTyping = settings.getShowThinkingUi();
26539
+ const needsLiveStatusForTyping = verboseForTyping !== "off" || showThinkingForTyping;
26540
+ let typingActive = !needsLiveStatusForTyping;
26541
+ if (typingActive) {
26542
+ const typingLoop = async () => {
26543
+ while (typingActive) {
26544
+ try {
26545
+ await channel.sendTyping?.(chatId);
26546
+ } catch {
26547
+ }
26548
+ await new Promise((r) => setTimeout(r, 4e3));
26178
26549
  }
26179
- await new Promise((r) => setTimeout(r, 4e3));
26180
- }
26181
- };
26182
- typingLoop().catch(() => {
26183
- });
26550
+ };
26551
+ typingLoop().catch(() => {
26552
+ });
26553
+ }
26184
26554
  try {
26185
26555
  const tMode = settings.getMode();
26186
26556
  const tVerbose = settings.getVerboseLevel();
@@ -26803,6 +27173,13 @@ async function runWithRetry(job, model2, runId, t0) {
26803
27173
  const cronBackend = currentBackend;
26804
27174
  setAllowPaidSlots(chatId, cronBackend);
26805
27175
  }
27176
+ if (job.credentialSlotId) {
27177
+ if (currentBackend === BACKEND.GEMINI) {
27178
+ pinChatGeminiSlot(chatId, job.credentialSlotId);
27179
+ } else {
27180
+ pinChatBackendSlot(chatId, currentBackend, job.credentialSlotId);
27181
+ }
27182
+ }
26806
27183
  const response = await askAgent(chatId, job.description, {
26807
27184
  model: currentModel,
26808
27185
  backend: currentBackend,
@@ -26874,6 +27251,7 @@ var init_cron = __esm({
26874
27251
  "src/scheduler/cron.ts"() {
26875
27252
  "use strict";
26876
27253
  init_store5();
27254
+ init_types();
26877
27255
  init_backends();
26878
27256
  init_agent();
26879
27257
  init_log();
@@ -27564,6 +27942,115 @@ ${body.replace(/<[^>]*>/g, "").trim()}</code>
27564
27942
  }
27565
27943
  });
27566
27944
 
27945
+ // src/channels/health.ts
27946
+ function trackChannel(channel) {
27947
+ healthState.set(channel.name, {
27948
+ status: "healthy",
27949
+ startedAt: Date.now(),
27950
+ lastHealthyAt: Date.now(),
27951
+ lastErrorAt: null,
27952
+ lastError: null,
27953
+ reconnectAttempts: 0
27954
+ });
27955
+ }
27956
+ function markChannelHealthy(name) {
27957
+ const state = healthState.get(name);
27958
+ if (state) {
27959
+ state.status = "healthy";
27960
+ state.lastHealthyAt = Date.now();
27961
+ state.reconnectAttempts = 0;
27962
+ }
27963
+ }
27964
+ function markChannelDown(name, error3) {
27965
+ const state = healthState.get(name);
27966
+ if (state) {
27967
+ state.status = "down";
27968
+ state.lastErrorAt = Date.now();
27969
+ state.lastError = error3;
27970
+ warn(`[channel-health] ${name} marked as down: ${error3}`);
27971
+ }
27972
+ }
27973
+ function getChannelHealth(name) {
27974
+ const state = healthState.get(name);
27975
+ if (!state) return null;
27976
+ return {
27977
+ name,
27978
+ status: state.status,
27979
+ lastHealthyAt: state.lastHealthyAt,
27980
+ lastErrorAt: state.lastErrorAt,
27981
+ lastError: state.lastError,
27982
+ reconnectAttempts: state.reconnectAttempts,
27983
+ uptimeMs: state.status === "healthy" ? Date.now() - state.startedAt : 0
27984
+ };
27985
+ }
27986
+ async function attemptReconnect(channel, handler) {
27987
+ const state = healthState.get(channel.name);
27988
+ if (!state) return false;
27989
+ if (reconnecting.has(channel.name)) {
27990
+ log(`[channel-health] ${channel.name}: reconnect already in progress, skipping`);
27991
+ return false;
27992
+ }
27993
+ if (state.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
27994
+ error(`[channel-health] ${channel.name}: max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached`);
27995
+ return false;
27996
+ }
27997
+ reconnecting.add(channel.name);
27998
+ state.reconnectAttempts++;
27999
+ const backoffMs = RECONNECT_BASE_MS * Math.pow(2, state.reconnectAttempts - 1);
28000
+ log(`[channel-health] ${channel.name}: reconnect attempt ${state.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} in ${backoffMs}ms`);
28001
+ await new Promise((r) => setTimeout(r, backoffMs));
28002
+ try {
28003
+ await channel.stop().catch(() => {
28004
+ });
28005
+ await channel.start(handler);
28006
+ markChannelHealthy(channel.name);
28007
+ log(`[channel-health] ${channel.name}: reconnected successfully`);
28008
+ return true;
28009
+ } catch (err) {
28010
+ const msg = err instanceof Error ? err.message : String(err);
28011
+ markChannelDown(channel.name, msg);
28012
+ return false;
28013
+ } finally {
28014
+ reconnecting.delete(channel.name);
28015
+ }
28016
+ }
28017
+ function startHealthMonitor3(channels, handler) {
28018
+ registeredHandler = handler;
28019
+ for (const ch of channels) {
28020
+ trackChannel(ch);
28021
+ }
28022
+ healthInterval = setInterval(() => {
28023
+ for (const ch of channels) {
28024
+ const health = getChannelHealth(ch.name);
28025
+ if (health?.status === "down" && registeredHandler) {
28026
+ attemptReconnect(ch, registeredHandler).catch(() => {
28027
+ });
28028
+ }
28029
+ }
28030
+ }, HEALTH_CHECK_INTERVAL_MS);
28031
+ log(`[channel-health] Monitoring ${channels.length} channel(s)`);
28032
+ }
28033
+ function stopHealthMonitor3() {
28034
+ if (healthInterval) {
28035
+ clearInterval(healthInterval);
28036
+ healthInterval = null;
28037
+ }
28038
+ }
28039
+ var healthState, MAX_RECONNECT_ATTEMPTS, RECONNECT_BASE_MS, HEALTH_CHECK_INTERVAL_MS, healthInterval, registeredHandler, reconnecting;
28040
+ var init_health3 = __esm({
28041
+ "src/channels/health.ts"() {
28042
+ "use strict";
28043
+ init_log();
28044
+ healthState = /* @__PURE__ */ new Map();
28045
+ MAX_RECONNECT_ATTEMPTS = 5;
28046
+ RECONNECT_BASE_MS = 5e3;
28047
+ HEALTH_CHECK_INTERVAL_MS = 15e3;
28048
+ healthInterval = null;
28049
+ registeredHandler = null;
28050
+ reconnecting = /* @__PURE__ */ new Set();
28051
+ }
28052
+ });
28053
+
27567
28054
  // src/channels/telegram.ts
27568
28055
  import { API_CONSTANTS, Bot, GrammyError as GrammyError2, InlineKeyboard, InputFile } from "grammy";
27569
28056
  function isFastPathMessage(msg) {
@@ -27594,10 +28081,11 @@ var init_telegram2 = __esm({
27594
28081
  "use strict";
27595
28082
  init_telegram();
27596
28083
  init_log();
28084
+ init_health3();
27597
28085
  init_store5();
27598
28086
  init_telegram_throttle();
27599
28087
  FAST_PATH_COMMANDS = /* @__PURE__ */ new Set(["stop", "status", "new", "newchat"]);
27600
- TelegramChannel = class {
28088
+ TelegramChannel = class _TelegramChannel {
27601
28089
  name = "telegram";
27602
28090
  bot;
27603
28091
  allowedChatIds;
@@ -27607,6 +28095,19 @@ var init_telegram2 = __esm({
27607
28095
  // messageId → chatId
27608
28096
  reactionHandlers = [];
27609
28097
  throttle;
28098
+ // ── Polling health tracking ─────────────────────────────────────────
28099
+ /** Timestamp of last update received from Telegram (message, callback, reaction) */
28100
+ lastUpdateAt = 0;
28101
+ /** True while polling is expected to be active (between start() and stop()) */
28102
+ pollingExpected = false;
28103
+ /** Watchdog interval that detects silent polling death */
28104
+ pollingWatchdog = null;
28105
+ /** Max time without any update before we consider polling dead (ms) */
28106
+ static POLLING_SILENCE_THRESHOLD_MS = 2 * 60 * 1e3;
28107
+ // 2 minutes
28108
+ /** How often the watchdog checks for polling health (ms) */
28109
+ static POLLING_WATCHDOG_INTERVAL_MS = 60 * 1e3;
28110
+ // 60 seconds
27610
28111
  constructor() {
27611
28112
  const token = process.env.TELEGRAM_BOT_TOKEN;
27612
28113
  if (!token) {
@@ -27624,6 +28125,10 @@ var init_telegram2 = __esm({
27624
28125
  this.bot = new Bot(token);
27625
28126
  this.throttle = new TelegramThrottle();
27626
28127
  this.throttle.setResumeNotifier(async (chatId, pausedSec, queuedCount) => {
28128
+ if (pausedSec > 60) {
28129
+ log(`[telegram] Skipping resume notification (paused ${pausedSec}s \u2014 too long, would risk another 429)`);
28130
+ return;
28131
+ }
27627
28132
  try {
27628
28133
  await this.bot.api.sendMessage(
27629
28134
  numericChatId(chatId),
@@ -27735,6 +28240,7 @@ var init_telegram2 = __esm({
27735
28240
  { command: "council", description: "Multi-model debate (select models, anonymous rounds)" }
27736
28241
  ]);
27737
28242
  this.bot.on("message", async (ctx) => {
28243
+ this.lastUpdateAt = Date.now();
27738
28244
  const chatId = ctx.chat.id.toString();
27739
28245
  const senderId = ctx.from?.id?.toString() ?? "";
27740
28246
  const authorized = this.isAuthorized(chatId) || this.isAuthorized(senderId);
@@ -27762,6 +28268,7 @@ var init_telegram2 = __esm({
27762
28268
  });
27763
28269
  });
27764
28270
  this.bot.on("callback_query:data", (ctx) => {
28271
+ this.lastUpdateAt = Date.now();
27765
28272
  const userId = ctx.from.id.toString();
27766
28273
  const chatId = ctx.callbackQuery.message?.chat?.id?.toString() ?? userId;
27767
28274
  log(`[telegram] Callback from user ${userId} in chat ${chatId}: ${ctx.callbackQuery.data}`);
@@ -27789,6 +28296,7 @@ var init_telegram2 = __esm({
27789
28296
  });
27790
28297
  });
27791
28298
  this.bot.on("message_reaction", async (ctx) => {
28299
+ this.lastUpdateAt = Date.now();
27792
28300
  const chatId = String(ctx.chat.id);
27793
28301
  const messageId = ctx.messageReaction.message_id;
27794
28302
  if (!this.agentMessageIds.has(messageId)) return;
@@ -27805,6 +28313,7 @@ var init_telegram2 = __esm({
27805
28313
  }
27806
28314
  });
27807
28315
  this.bot.on("inline_query", (ctx) => {
28316
+ this.lastUpdateAt = Date.now();
27808
28317
  if (!this.isAuthorized(ctx.from.id.toString())) return;
27809
28318
  this.handleInlineQuery(ctx).catch((err) => {
27810
28319
  error("[telegram] Inline query error:", err);
@@ -27818,25 +28327,64 @@ var init_telegram2 = __esm({
27818
28327
  error("[telegram] Unhandled error:", err);
27819
28328
  }
27820
28329
  });
27821
- this.bot.start({
28330
+ this.pollingExpected = true;
28331
+ this.lastUpdateAt = Date.now();
28332
+ const pollingPromise = this.bot.start({
27822
28333
  allowed_updates: [...API_CONSTANTS.ALL_UPDATE_TYPES],
27823
28334
  onStart: () => log("[telegram] Polling for messages...")
27824
- }).catch((err) => {
27825
- error("[telegram] Fatal: bot.start() failed:", err);
27826
- error("[telegram] Check TELEGRAM_BOT_TOKEN in ~/.cc-claw/.env \u2014 it may be invalid or revoked.");
27827
- error("[telegram] To regenerate: message @BotFather on Telegram, use /revoke, then 'cc-claw setup'");
27828
- process.exit(1);
27829
28335
  });
28336
+ pollingPromise.then(
28337
+ () => {
28338
+ if (this.pollingExpected) {
28339
+ error("[telegram] CRITICAL: Polling loop exited unexpectedly (resolved without stop)");
28340
+ markChannelDown("telegram", "Polling loop exited unexpectedly");
28341
+ }
28342
+ },
28343
+ (err) => {
28344
+ if (this.pollingExpected) {
28345
+ error("[telegram] Fatal: bot.start() failed:", err);
28346
+ error("[telegram] Check TELEGRAM_BOT_TOKEN in ~/.cc-claw/.env \u2014 it may be invalid or revoked.");
28347
+ error("[telegram] To regenerate: message @BotFather on Telegram, use /revoke, then 'cc-claw setup'");
28348
+ markChannelDown("telegram", `Polling error: ${err instanceof Error ? err.message : String(err)}`);
28349
+ }
28350
+ }
28351
+ );
28352
+ this.pollingWatchdog = setInterval(() => {
28353
+ if (!this.pollingExpected) return;
28354
+ const silenceMs = Date.now() - this.lastUpdateAt;
28355
+ if (silenceMs > _TelegramChannel.POLLING_SILENCE_THRESHOLD_MS) {
28356
+ error(
28357
+ `[telegram] CRITICAL: No updates received for ${Math.round(silenceMs / 1e3)}s \u2014 polling likely dead, triggering reconnect`
28358
+ );
28359
+ markChannelDown("telegram", `No updates for ${Math.round(silenceMs / 1e3)}s`);
28360
+ this.lastUpdateAt = Date.now();
28361
+ }
28362
+ }, _TelegramChannel.POLLING_WATCHDOG_INTERVAL_MS);
27830
28363
  }
27831
28364
  async stop() {
27832
- await this.bot.stop();
28365
+ this.pollingExpected = false;
28366
+ if (this.pollingWatchdog) {
28367
+ clearInterval(this.pollingWatchdog);
28368
+ this.pollingWatchdog = null;
28369
+ }
28370
+ try {
28371
+ await this.bot.stop();
28372
+ } catch {
28373
+ }
28374
+ const token = process.env.TELEGRAM_BOT_TOKEN;
28375
+ if (token) {
28376
+ this.bot = new Bot(token);
28377
+ }
27833
28378
  }
27834
28379
  async sendTyping(chatId, threadId) {
27835
- if (this.throttle.isPaused()) return;
27836
28380
  try {
27837
- await this.bot.api.sendChatAction(numericChatId(chatId), "typing", {
27838
- ...threadId ? { message_thread_id: threadId } : {}
27839
- });
28381
+ await this.throttle.tryBestEffort(
28382
+ chatId,
28383
+ "typing",
28384
+ () => this.bot.api.sendChatAction(numericChatId(chatId), "typing", {
28385
+ ...threadId ? { message_thread_id: threadId } : {}
28386
+ })
28387
+ );
27840
28388
  } catch {
27841
28389
  }
27842
28390
  }
@@ -27941,7 +28489,12 @@ var init_telegram2 = __esm({
27941
28489
  );
27942
28490
  return true;
27943
28491
  } catch (err) {
27944
- warn("[telegram] editText HTML failed, trying plain fallback:", err instanceof Error ? err.message : err);
28492
+ const errMsg = err instanceof Error ? err.message : String(err);
28493
+ if (errMsg.includes("overflow") || errMsg.includes("rate limit") || errMsg.includes("max wait")) {
28494
+ warn("[telegram] editText skipped fallback (throttle overload):", errMsg);
28495
+ return false;
28496
+ }
28497
+ warn("[telegram] editText HTML failed, trying plain fallback:", errMsg);
27945
28498
  try {
27946
28499
  await this.throttle.send(
27947
28500
  chatId,
@@ -28079,9 +28632,13 @@ var init_telegram2 = __esm({
28079
28632
  }
28080
28633
  async reactToMessage(chatId, messageId, emoji) {
28081
28634
  try {
28082
- await this.bot.api.setMessageReaction(numericChatId(chatId), parseInt(messageId), [
28083
- { type: "emoji", emoji }
28084
- ]);
28635
+ await this.throttle.tryBestEffort(
28636
+ chatId,
28637
+ "reaction",
28638
+ () => this.bot.api.setMessageReaction(numericChatId(chatId), parseInt(messageId), [
28639
+ { type: "emoji", emoji }
28640
+ ])
28641
+ );
28085
28642
  } catch (err) {
28086
28643
  log(`[telegram] reactToMessage failed (chat=${chatId} msg=${messageId}): ${err}`);
28087
28644
  }
@@ -28499,115 +29056,6 @@ var init_bootstrap2 = __esm({
28499
29056
  }
28500
29057
  });
28501
29058
 
28502
- // src/channels/health.ts
28503
- function trackChannel(channel) {
28504
- healthState.set(channel.name, {
28505
- status: "healthy",
28506
- startedAt: Date.now(),
28507
- lastHealthyAt: Date.now(),
28508
- lastErrorAt: null,
28509
- lastError: null,
28510
- reconnectAttempts: 0
28511
- });
28512
- }
28513
- function markChannelHealthy(name) {
28514
- const state = healthState.get(name);
28515
- if (state) {
28516
- state.status = "healthy";
28517
- state.lastHealthyAt = Date.now();
28518
- state.reconnectAttempts = 0;
28519
- }
28520
- }
28521
- function markChannelDown(name, error3) {
28522
- const state = healthState.get(name);
28523
- if (state) {
28524
- state.status = "down";
28525
- state.lastErrorAt = Date.now();
28526
- state.lastError = error3;
28527
- warn(`[channel-health] ${name} marked as down: ${error3}`);
28528
- }
28529
- }
28530
- function getChannelHealth(name) {
28531
- const state = healthState.get(name);
28532
- if (!state) return null;
28533
- return {
28534
- name,
28535
- status: state.status,
28536
- lastHealthyAt: state.lastHealthyAt,
28537
- lastErrorAt: state.lastErrorAt,
28538
- lastError: state.lastError,
28539
- reconnectAttempts: state.reconnectAttempts,
28540
- uptimeMs: state.status === "healthy" ? Date.now() - state.startedAt : 0
28541
- };
28542
- }
28543
- async function attemptReconnect(channel, handler) {
28544
- const state = healthState.get(channel.name);
28545
- if (!state) return false;
28546
- if (reconnecting.has(channel.name)) {
28547
- log(`[channel-health] ${channel.name}: reconnect already in progress, skipping`);
28548
- return false;
28549
- }
28550
- if (state.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
28551
- error(`[channel-health] ${channel.name}: max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached`);
28552
- return false;
28553
- }
28554
- reconnecting.add(channel.name);
28555
- state.reconnectAttempts++;
28556
- const backoffMs = RECONNECT_BASE_MS * Math.pow(2, state.reconnectAttempts - 1);
28557
- log(`[channel-health] ${channel.name}: reconnect attempt ${state.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS} in ${backoffMs}ms`);
28558
- await new Promise((r) => setTimeout(r, backoffMs));
28559
- try {
28560
- await channel.stop().catch(() => {
28561
- });
28562
- await channel.start(handler);
28563
- markChannelHealthy(channel.name);
28564
- log(`[channel-health] ${channel.name}: reconnected successfully`);
28565
- return true;
28566
- } catch (err) {
28567
- const msg = err instanceof Error ? err.message : String(err);
28568
- markChannelDown(channel.name, msg);
28569
- return false;
28570
- } finally {
28571
- reconnecting.delete(channel.name);
28572
- }
28573
- }
28574
- function startHealthMonitor3(channels, handler) {
28575
- registeredHandler = handler;
28576
- for (const ch of channels) {
28577
- trackChannel(ch);
28578
- }
28579
- healthInterval = setInterval(() => {
28580
- for (const ch of channels) {
28581
- const health = getChannelHealth(ch.name);
28582
- if (health?.status === "down" && registeredHandler) {
28583
- attemptReconnect(ch, registeredHandler).catch(() => {
28584
- });
28585
- }
28586
- }
28587
- }, HEALTH_CHECK_INTERVAL_MS);
28588
- log(`[channel-health] Monitoring ${channels.length} channel(s)`);
28589
- }
28590
- function stopHealthMonitor3() {
28591
- if (healthInterval) {
28592
- clearInterval(healthInterval);
28593
- healthInterval = null;
28594
- }
28595
- }
28596
- var healthState, MAX_RECONNECT_ATTEMPTS, RECONNECT_BASE_MS, HEALTH_CHECK_INTERVAL_MS, healthInterval, registeredHandler, reconnecting;
28597
- var init_health3 = __esm({
28598
- "src/channels/health.ts"() {
28599
- "use strict";
28600
- init_log();
28601
- healthState = /* @__PURE__ */ new Map();
28602
- MAX_RECONNECT_ATTEMPTS = 5;
28603
- RECONNECT_BASE_MS = 5e3;
28604
- HEALTH_CHECK_INTERVAL_MS = 15e3;
28605
- healthInterval = null;
28606
- registeredHandler = null;
28607
- reconnecting = /* @__PURE__ */ new Set();
28608
- }
28609
- });
28610
-
28611
29059
  // src/cli/commands/ai-skill.ts
28612
29060
  var ai_skill_exports = {};
28613
29061
  __export(ai_skill_exports, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.20.14",
3
+ "version": "0.20.16",
4
4
  "description": "CC-Claw: Personal AI assistant on Telegram — multi-backend (Claude, Gemini, Codex, Cursor), sub-agent orchestration, MCP management",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",