cc-claw 0.19.0 → 0.19.2

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 +122 -34
  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.19.0" : (() => {
36
+ VERSION = true ? "0.19.2" : (() => {
37
37
  try {
38
38
  return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
39
39
  } catch {
@@ -623,7 +623,7 @@ function listMcpServers(db3) {
623
623
  }
624
624
  function addPropagation(db3, mcpName, runnerId, scope) {
625
625
  db3.prepare(`
626
- INSERT INTO mcp_propagation (mcpName, runnerId, scope, addedAt) VALUES (?, ?, ?, datetime('now'))
626
+ INSERT OR IGNORE INTO mcp_propagation (mcpName, runnerId, scope, addedAt) VALUES (?, ?, ?, datetime('now'))
627
627
  `).run(mcpName, runnerId, scope);
628
628
  }
629
629
  function removePropagation(db3, mcpName, runnerId, scope) {
@@ -13122,7 +13122,14 @@ function spawnQuery(adapter, config2, model2, cancelState, thinkingLevel, timeou
13122
13122
  const elapsed = () => `${((Date.now() - t0) / 1e3).toFixed(1)}s`;
13123
13123
  const pendingTools = /* @__PURE__ */ new Map();
13124
13124
  const stderrChunks = [];
13125
- proc.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
13125
+ let stderrBytes = 0;
13126
+ const STDERR_CAP = 1e6;
13127
+ proc.stderr?.on("data", (chunk) => {
13128
+ if (stderrBytes < STDERR_CAP) {
13129
+ stderrChunks.push(chunk);
13130
+ stderrBytes += chunk.length;
13131
+ }
13132
+ });
13126
13133
  const rl2 = createInterface5({ input: proc.stdout });
13127
13134
  let firstLine = true;
13128
13135
  const frTimeoutMs = adapter.id === BACKEND.GEMINI ? opts?.firstResponseTimeoutMs ?? FIRST_RESPONSE_TIMEOUT_MS : 0;
@@ -15624,7 +15631,7 @@ var init_live_status = __esm({
15624
15631
  globalLastFlushAt = 0;
15625
15632
  TRIM_THRESHOLD = 3500;
15626
15633
  MAX_ENTRIES = 200;
15627
- LiveStatusMessage = class {
15634
+ LiveStatusMessage = class _LiveStatusMessage {
15628
15635
  constructor(chatId, channel, modelLabel, verboseLevel, showThinking = false) {
15629
15636
  this.chatId = chatId;
15630
15637
  this.channel = channel;
@@ -15642,6 +15649,9 @@ var init_live_status = __esm({
15642
15649
  nextFlushAllowedAt = 0;
15643
15650
  /** Tracks whether entries have been trimmed at least once (for display hint). */
15644
15651
  hasTrimmed = false;
15652
+ /** Consecutive non-429 edit failures. After 3, stop trying (message likely deleted). */
15653
+ consecutiveEditFailures = 0;
15654
+ static MAX_EDIT_FAILURES = 3;
15645
15655
  /** Resolve flush interval based on chat type (group chats are rate-limited more aggressively). */
15646
15656
  get flushIntervalMs() {
15647
15657
  const numericId = parseInt(this.chatId);
@@ -15720,6 +15730,7 @@ var init_live_status = __esm({
15720
15730
  // ── Internal ──────────────────────────────────────────────────────────
15721
15731
  async flush() {
15722
15732
  if (this.finalized || !this.messageId || !this.channel.editText) return;
15733
+ if (this.consecutiveEditFailures >= _LiveStatusMessage.MAX_EDIT_FAILURES) return;
15723
15734
  if (Date.now() < this.nextFlushAllowedAt) return;
15724
15735
  if (!canFlushGlobally()) return;
15725
15736
  const deduped = dedupThinking(this.entries);
@@ -15729,6 +15740,7 @@ var init_live_status = __esm({
15729
15740
  markGlobalFlush();
15730
15741
  try {
15731
15742
  await this.channel.editText(this.chatId, this.messageId, body, "plain");
15743
+ this.consecutiveEditFailures = 0;
15732
15744
  } catch (err) {
15733
15745
  this.handleRateLimit(err);
15734
15746
  }
@@ -15762,7 +15774,12 @@ var init_live_status = __esm({
15762
15774
  this.nextFlushAllowedAt = Date.now() + retrySec * 1e3;
15763
15775
  log(`[live-status] 429 rate-limited, backing off ${retrySec}s`);
15764
15776
  } else {
15765
- log(`[live-status] edit failed: ${msg}`);
15777
+ this.consecutiveEditFailures++;
15778
+ if (this.consecutiveEditFailures >= _LiveStatusMessage.MAX_EDIT_FAILURES) {
15779
+ log(`[live-status] ${this.consecutiveEditFailures} consecutive edit failures \u2014 abandoning status updates (message likely deleted)`);
15780
+ } else {
15781
+ log(`[live-status] edit failed (${this.consecutiveEditFailures}/${_LiveStatusMessage.MAX_EDIT_FAILURES}): ${msg}`);
15782
+ }
15766
15783
  }
15767
15784
  }
15768
15785
  };
@@ -16319,7 +16336,11 @@ async function sendResponse(chatId, channel, text, messageId, replyToMessageId)
16319
16336
  error("[router] TTS failed, falling back to text:", err);
16320
16337
  }
16321
16338
  }
16322
- await channel.sendText(chatId, cleanText, replyToMessageId ? { replyToMessageId } : void 0);
16339
+ try {
16340
+ await channel.sendText(chatId, cleanText, replyToMessageId ? { replyToMessageId } : void 0);
16341
+ } catch (err) {
16342
+ error("[response] Final sendText failed \u2014 response lost:", err instanceof Error ? err.message : err);
16343
+ }
16323
16344
  }
16324
16345
  async function handleResponseExhaustion(responseText, chatId, msg, channel) {
16325
16346
  const raw = responseText.replace(/\n\n🧠 \[.+$/, "").trim();
@@ -24669,10 +24690,16 @@ import { marked } from "marked";
24669
24690
  function escapeHtml2(text) {
24670
24691
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
24671
24692
  }
24693
+ function stripNonTelegramTags(html) {
24694
+ return html.replace(/<\/?([a-zA-Z][a-zA-Z0-9_-]*)\b[^>]*>/g, (fullMatch, tagName) => {
24695
+ if (TELEGRAM_ALLOWED_TAGS.has(tagName.toLowerCase())) return fullMatch;
24696
+ return fullMatch.replace(/</g, "&lt;").replace(/>/g, "&gt;");
24697
+ });
24698
+ }
24672
24699
  function formatForTelegram(markdown) {
24673
24700
  try {
24674
24701
  const html = marked(markdown);
24675
- return html.trimEnd();
24702
+ return stripNonTelegramTags(html).trimEnd();
24676
24703
  } catch {
24677
24704
  return formatForTelegramFallback(markdown);
24678
24705
  }
@@ -24759,7 +24786,7 @@ function findSafeSplitPoint(text, maxLen) {
24759
24786
  }
24760
24787
  return safePositions[safePositions.length - 1];
24761
24788
  }
24762
- var MAX_MESSAGE_LENGTH;
24789
+ var MAX_MESSAGE_LENGTH, TELEGRAM_ALLOWED_TAGS;
24763
24790
  var init_telegram = __esm({
24764
24791
  "src/formatter/telegram.ts"() {
24765
24792
  "use strict";
@@ -24823,6 +24850,35 @@ ${body.replace(/<[^>]*>/g, "").trim()}</code>
24823
24850
  }
24824
24851
  }
24825
24852
  });
24853
+ TELEGRAM_ALLOWED_TAGS = /* @__PURE__ */ new Set([
24854
+ "b",
24855
+ "strong",
24856
+ // bold
24857
+ "i",
24858
+ "em",
24859
+ // italic
24860
+ "u",
24861
+ "ins",
24862
+ // underline
24863
+ "s",
24864
+ "del",
24865
+ "strike",
24866
+ // strikethrough
24867
+ "code",
24868
+ "pre",
24869
+ // code
24870
+ "a",
24871
+ // link
24872
+ "span",
24873
+ // used for <span class="tg-spoiler">
24874
+ "tg-spoiler",
24875
+ "tg-emoji",
24876
+ // Telegram custom tags
24877
+ "blockquote",
24878
+ // block quote
24879
+ "expandable-blockquote"
24880
+ // expandable block quote
24881
+ ]);
24826
24882
  }
24827
24883
  });
24828
24884
 
@@ -24832,22 +24888,24 @@ function tripCircuitBreaker(retrySec) {
24832
24888
  const until = Date.now() + retrySec * 1e3;
24833
24889
  if (until > circuitBreakerUntil) {
24834
24890
  circuitBreakerUntil = until;
24835
- warn(`[telegram] Circuit breaker tripped \u2014 pausing ALL Telegram API calls for ${retrySec}s`);
24891
+ warn(`[telegram] Circuit breaker tripped \u2014 blocking ALL Telegram API calls for ${retrySec}s`);
24836
24892
  }
24837
24893
  }
24838
- async function waitForCircuitBreaker() {
24839
- const remaining = circuitBreakerUntil - Date.now();
24840
- if (remaining <= 0) return false;
24841
- if (remaining > GIVE_UP_THRESHOLD_SEC * 1e3) return true;
24842
- await new Promise((r) => setTimeout(r, remaining));
24843
- return false;
24894
+ function isCircuitBreakerActive() {
24895
+ return Date.now() < circuitBreakerUntil;
24896
+ }
24897
+ function isRateLimitError(err) {
24898
+ return err instanceof GrammyError && err.error_code === 429;
24844
24899
  }
24845
24900
  async function withRetry(label2, fn) {
24846
24901
  for (let attempt = 0; attempt <= MAX_RETRIES2; attempt++) {
24847
- const tooLong = await waitForCircuitBreaker();
24848
- if (tooLong) {
24849
- warn(`[telegram] 429 on ${label2} \u2014 circuit breaker cooldown too long, giving up`);
24850
- throw new GrammyError(`Circuit breaker active \u2014 skipping ${label2}`, { ok: false, error_code: 429, description: "Rate limited (circuit breaker)" }, label2, {});
24902
+ if (isCircuitBreakerActive()) {
24903
+ throw new GrammyError(
24904
+ `Circuit breaker active \u2014 skipping ${label2}`,
24905
+ { ok: false, error_code: 429, description: "Rate limited (circuit breaker)" },
24906
+ label2,
24907
+ {}
24908
+ );
24851
24909
  }
24852
24910
  try {
24853
24911
  return await fn();
@@ -24883,6 +24941,9 @@ function isFastPathMessage(msg) {
24883
24941
  }
24884
24942
  return false;
24885
24943
  }
24944
+ function sanitizeForTelegram(text) {
24945
+ return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFFFD\uFFFE\uFFFF]/g, "");
24946
+ }
24886
24947
  function numericChatId(chatId) {
24887
24948
  if (chatId.startsWith("sq:") || chatId.startsWith("cron:")) {
24888
24949
  throw new Error(`Synthetic chatId "${chatId}" passed to Telegram API`);
@@ -24897,9 +24958,9 @@ var init_telegram2 = __esm({
24897
24958
  init_telegram();
24898
24959
  init_log();
24899
24960
  init_store5();
24900
- MAX_RETRIES2 = 3;
24961
+ MAX_RETRIES2 = 2;
24901
24962
  FALLBACK_RETRY_SEC = 3;
24902
- GIVE_UP_THRESHOLD_SEC = 60;
24963
+ GIVE_UP_THRESHOLD_SEC = 30;
24903
24964
  circuitBreakerUntil = 0;
24904
24965
  FAST_PATH_COMMANDS = /* @__PURE__ */ new Set(["stop", "status", "new", "newchat"]);
24905
24966
  TelegramChannel = class {
@@ -25107,14 +25168,17 @@ var init_telegram2 = __esm({
25107
25168
  await this.bot.stop();
25108
25169
  }
25109
25170
  async sendTyping(chatId) {
25110
- await this.bot.api.sendChatAction(numericChatId(chatId), "typing");
25171
+ try {
25172
+ await this.bot.api.sendChatAction(numericChatId(chatId), "typing");
25173
+ } catch {
25174
+ }
25111
25175
  }
25112
25176
  async sendText(chatId, text, opts) {
25113
25177
  const { parseMode, threadId, replyToMessageId } = opts ?? {};
25114
25178
  const threadOpts = threadId ? { message_thread_id: threadId } : {};
25115
25179
  const replyOpts = replyToMessageId ? { reply_parameters: { message_id: replyToMessageId } } : {};
25116
25180
  if (parseMode === "plain") {
25117
- const plainChunks = splitMessage(text);
25181
+ const plainChunks = splitMessage(sanitizeForTelegram(text));
25118
25182
  for (const chunk of plainChunks) {
25119
25183
  const sent = await withRetry(
25120
25184
  "sendText:plain",
@@ -25124,7 +25188,7 @@ var init_telegram2 = __esm({
25124
25188
  }
25125
25189
  return;
25126
25190
  }
25127
- const formatted = parseMode === "html" ? text : formatForTelegram(text);
25191
+ const formatted = sanitizeForTelegram(parseMode === "html" ? text : formatForTelegram(text));
25128
25192
  const chunks = splitMessage(formatted);
25129
25193
  for (const chunk of chunks) {
25130
25194
  try {
@@ -25138,6 +25202,7 @@ var init_telegram2 = __esm({
25138
25202
  );
25139
25203
  this.trackAgentMessage(sent.message_id, chatId);
25140
25204
  } catch (err) {
25205
+ if (isRateLimitError(err)) throw err;
25141
25206
  warn("[telegram] sendText HTML failed, falling back to plain:", err instanceof Error ? err.message : err);
25142
25207
  const sent = await withRetry(
25143
25208
  "sendText:fallback",
@@ -25177,7 +25242,7 @@ var init_telegram2 = __esm({
25177
25242
  }
25178
25243
  async sendTextReturningId(chatId, text, parseMode) {
25179
25244
  try {
25180
- const formatted = parseMode === "html" ? text : parseMode === "plain" ? text : formatForTelegram(text);
25245
+ const formatted = sanitizeForTelegram(parseMode === "html" ? text : parseMode === "plain" ? text : formatForTelegram(text));
25181
25246
  const opts = parseMode === "plain" ? {} : { parse_mode: "HTML" };
25182
25247
  const msg = await withRetry(
25183
25248
  "sendTextReturningId",
@@ -25190,7 +25255,7 @@ var init_telegram2 = __esm({
25190
25255
  }
25191
25256
  }
25192
25257
  async editText(chatId, messageId, text, parseMode) {
25193
- const formatted = parseMode === "html" ? text : formatForTelegram(text);
25258
+ const formatted = sanitizeForTelegram(parseMode === "html" ? text : formatForTelegram(text));
25194
25259
  try {
25195
25260
  await withRetry(
25196
25261
  "editText:html",
@@ -25200,6 +25265,7 @@ var init_telegram2 = __esm({
25200
25265
  );
25201
25266
  return true;
25202
25267
  } catch (err) {
25268
+ if (isRateLimitError(err)) return false;
25203
25269
  warn("[telegram] editText HTML failed, trying plain fallback:", err instanceof Error ? err.message : err);
25204
25270
  try {
25205
25271
  await withRetry(
@@ -25238,7 +25304,7 @@ var init_telegram2 = __esm({
25238
25304
  }
25239
25305
  const MAX_KEYBOARD_TEXT = 4e3;
25240
25306
  const safeText = text.length > MAX_KEYBOARD_TEXT ? text.slice(0, MAX_KEYBOARD_TEXT) + "\n\n\u2026(truncated)" : text;
25241
- const formatted = formatForTelegram(safeText);
25307
+ const formatted = sanitizeForTelegram(formatForTelegram(safeText));
25242
25308
  try {
25243
25309
  const msg = await withRetry(
25244
25310
  "sendKeyboard",
@@ -25249,6 +25315,10 @@ var init_telegram2 = __esm({
25249
25315
  );
25250
25316
  return msg.message_id.toString();
25251
25317
  } catch (err) {
25318
+ if (isRateLimitError(err)) {
25319
+ warn(`[telegram] sendKeyboard rate-limited, skipping all fallbacks`);
25320
+ return void 0;
25321
+ }
25252
25322
  error(`[telegram] sendKeyboard failed (chat=${chatId}, textLen=${text.length}):`, err);
25253
25323
  try {
25254
25324
  const escaped = safeText.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -25260,6 +25330,7 @@ var init_telegram2 = __esm({
25260
25330
  );
25261
25331
  return retryMsg.message_id.toString();
25262
25332
  } catch (plainErr) {
25333
+ if (isRateLimitError(plainErr)) return void 0;
25263
25334
  error(`[telegram] sendKeyboard plain retry also failed:`, plainErr);
25264
25335
  }
25265
25336
  try {
@@ -27405,19 +27476,20 @@ async function doctorCommand(globalOpts, localOpts) {
27405
27476
  else if (/spawn timeout|timeout after \d+s/i.test(line)) spawnTimeout++;
27406
27477
  else other++;
27407
27478
  }
27479
+ const logFix = "cc-claw doctor --fix to clear stale errors";
27408
27480
  if (rate429 > 10) {
27409
- checks.push({ name: "Telegram rate limits", status: "error", message: `${rate429} rate-limit (429) errors in last 24h \u2014 message delivery blocked`, fix: "cc-claw logs --error" });
27481
+ checks.push({ name: "Telegram rate limits", status: "error", message: `${rate429} rate-limit (429) errors in last 24h \u2014 message delivery blocked`, fix: logFix });
27410
27482
  } else if (rate429 > 0) {
27411
- checks.push({ name: "Telegram rate limits", status: "warning", message: `${rate429} rate-limit (429) errors in last 24h`, fix: "cc-claw logs --error" });
27483
+ checks.push({ name: "Telegram rate limits", status: "warning", message: `${rate429} rate-limit (429) errors in last 24h`, fix: logFix });
27412
27484
  }
27413
27485
  if (contentSilence > 0) {
27414
- checks.push({ name: "Content silence", status: "warning", message: `${contentSilence} agent silence timeout(s) in last 24h \u2014 API went unresponsive`, fix: "cc-claw logs --error" });
27486
+ checks.push({ name: "Content silence", status: "warning", message: `${contentSilence} agent silence timeout(s) in last 24h \u2014 API went unresponsive`, fix: logFix });
27415
27487
  }
27416
27488
  if (spawnTimeout > 0) {
27417
- checks.push({ name: "Spawn timeouts", status: "warning", message: `${spawnTimeout} backend timeout(s) in last 24h`, fix: "cc-claw logs --error" });
27489
+ checks.push({ name: "Spawn timeouts", status: "warning", message: `${spawnTimeout} backend timeout(s) in last 24h`, fix: logFix });
27418
27490
  }
27419
27491
  if (other > 0) {
27420
- checks.push({ name: "Other errors", status: "warning", message: `${other} other error(s) in last 24h`, fix: "cc-claw logs --error" });
27492
+ checks.push({ name: "Other errors", status: "warning", message: `${other} other error(s) in last 24h`, fix: logFix });
27421
27493
  }
27422
27494
  if (rate429 === 0 && contentSilence === 0 && spawnTimeout === 0 && other === 0) {
27423
27495
  checks.push({ name: "Recent errors", status: "ok", message: "none in last 24h" });
@@ -27508,6 +27580,21 @@ async function doctorCommand(globalOpts, localOpts) {
27508
27580
  }
27509
27581
  }
27510
27582
  }
27583
+ const errorChecks = checks.filter(
27584
+ (c) => ["Telegram rate limits", "Content silence", "Spawn timeouts", "Other errors"].includes(c.name) && c.status !== "ok"
27585
+ );
27586
+ if (errorChecks.length > 0 && existsSync30(ERROR_LOG_PATH)) {
27587
+ try {
27588
+ const { writeFileSync: writeFileSync13 } = await import("fs");
27589
+ writeFileSync13(ERROR_LOG_PATH, "");
27590
+ for (const c of errorChecks) {
27591
+ c.status = "ok";
27592
+ c.message = "cleared (log truncated)";
27593
+ delete c.fix;
27594
+ }
27595
+ } catch {
27596
+ }
27597
+ }
27511
27598
  }
27512
27599
  const errors = checks.filter((c) => c.status === "error").length;
27513
27600
  const warnings = checks.filter((c) => c.status === "warning").length;
@@ -27533,8 +27620,9 @@ async function doctorCommand(globalOpts, localOpts) {
27533
27620
  lines.push(` ${parts.join(", ")}`);
27534
27621
  const fixable = r.checks.filter((c) => c.fix);
27535
27622
  if (fixable.length > 0 && !localOpts.fix) {
27536
- for (const f of fixable) {
27537
- lines.push(muted(` Fix: ${f.fix}`));
27623
+ const uniqueFixes = [...new Set(fixable.map((f) => f.fix))];
27624
+ for (const fix of uniqueFixes) {
27625
+ lines.push(muted(` Fix: ${fix}`));
27538
27626
  }
27539
27627
  }
27540
27628
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-claw",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
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",