claude-telegram-bot 0.3.14 → 0.3.17

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.ko.md CHANGED
@@ -6,6 +6,8 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/claude-telegram-bot.svg)](https://www.npmjs.com/package/claude-telegram-bot)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
 
9
+ 텔레그램으로 Claude Code를 — 어디서든, 어떤 기기에서도.
10
+
9
11
  텔레그램으로 메시지를 보내면, 집이나 서버에 켜둔 Claude Code가 작업하고 결과를 다시 텔레그램으로 돌려주는 봇입니다.
10
12
 
11
13
  ```
@@ -116,7 +118,7 @@ claude-telegram-bot ~/botconfigs/myproj/mybot.json
116
118
  - `테스트 돌려보고 통과하면 커밋하고 push 해줘`
117
119
  - `api.ts 에 에러 핸들링 추가해줘`
118
120
 
119
- 명령어: `/new`(맥락 초기화) · `/stop`(작업 중단; `--reset`으로 세션도 롤백) · `/cron`(예약 작업 보기·추가·삭제) · `/restart`(문법 검사 후 재시작) · `/status`(봇 상태·버전) · `/model`(모델 보기·전환) · `/id`(채팅 ID 확인) · `/help`(도움말)
121
+ 명령어: `/new`(맥락 초기화) · `/stop`(작업 중단; `--reset`으로 세션도 롤백) · `/cron`(예약 작업 보기·추가·삭제) · `/reserve`(한도 리셋 시 재시도 예약) · `/restart`(문법 검사 후 재시작) · `/status`(봇 상태·버전) · `/model`(모델 보기·전환) · `/id`(채팅 ID 확인) · `/help`(도움말)
120
122
 
121
123
  > **`/stop`** 은 실행 중인 Claude 프로세스를 즉시 종료하고 대기 중인 메시지 큐도 비웁니다. `--reset`을 붙이면 세션을 작업 시작 이전 상태로 되돌려 중단된 작업이 대화 맥락에 남지 않습니다.
122
124
 
@@ -127,6 +129,7 @@ claude-telegram-bot ~/botconfigs/myproj/mybot.json
127
129
  - **세션 유지** — 대화는 `--resume`으로 자동으로 이어집니다. 마지막 세션 ID가 `state.json`에 저장되므로 봇을 재시작해도 맥락이 남습니다. 새로 시작하려면 `/new`.
128
130
  - **메시지 큐** — 작업 중에 새 메시지가 오면 버리지 않고 큐에 쌓아둡니다. 작업이 끝나면 대기 중인 메시지를 모두 하나의 프롬프트로 합쳐서 처리합니다(예: "A 해줘" → "아니다 B 해줘"를 한 번에 처리). `/stop`으로 실행 중인 작업과 큐를 동시에 취소할 수 있습니다.
129
131
  - **모델 권유** — 봇이 Claude에게 현재 모델을 알려줍니다. 질문 난이도가 현재 모델 수준을 넘는다고 판단되면 답변 끝에 전환 권유 한 줄이 붙습니다(예: 💡 `/model sonnet`). `/model <이름>`으로 전환(`haiku`, `sonnet`, `opus`, `fable`, 또는 전체 모델 ID) — `state.json`에 저장돼 재시작 후에도 유지됩니다.
132
+ - **한도 초과 재시도** — Claude Max / API 레이트 리밋 에러에 리셋 시간이 포함되면 `/reserve` 힌트를 표시합니다. `/reserve`를 입력하면 마지막 메시지를 해당 시각에 자동 재전송 예약. `/reserve <다른 메시지>`로 내용 변경, `/reserve rm`으로 취소.
130
133
  - **간결한 답변** — 텔레그램에 맞게 짧게 답하도록 시스템 프롬프트가 기본으로 붙습니다. 바꾸려면 `appendSystemPrompt`에 직접 넣으세요 (빈 문자열이면 끔).
131
134
  - **언어** — 봇 자체 문구(`/help`, 명령 메뉴, 상태 메시지)는 **기본 영어**, 텔레그램이 한국어인 사용자에겐 한국어로 나옵니다. `lang`(`"en"`/`"ko"`)으로 고정할 수 있습니다. Claude의 실제 답변은 **사용자가 쓴 언어**를 따라갑니다. `/` 명령 메뉴는 `setMyCommands`로 언어별 등록됩니다.
132
135
  - **서식 변환** — 답변의 마크다운(굵게·코드·표 등)을 텔레그램 HTML로 바꿔 보냅니다. 변환이 깨지는 경우엔 평문으로 다시 보냅니다.
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
  [![npm downloads](https://img.shields.io/npm/dm/claude-telegram-bot.svg)](https://www.npmjs.com/package/claude-telegram-bot)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
 
9
+ Use Claude Code from Telegram — run it anywhere, from any device.
10
+
9
11
  **A zero-dependency, single-file, daemonized Claude Code bot — no Bun, no Python, no open session.**
10
12
 
11
13
  A tiny bridge that takes your Telegram messages, runs `claude -p` (Claude Code headless mode)
@@ -160,7 +162,7 @@ auth layer.)
160
162
  - `run the solver tests and commit + push if they pass`
161
163
  - `add an edge case to solve-2nd-floor-edges.ts`
162
164
 
163
- Commands: `/new` (reset context / new session) · `/stop` (stop current task; `--reset` to also roll back the session) · `/cron` (list / add / remove scheduled tasks) · `/restart` (syntax-check & restart the bot) · `/status` (bot status & version) · `/model` (view / switch the model) · `/id` (show chat ID) · `/help`.
165
+ Commands: `/new` (reset context / new session) · `/stop` (stop current task; `--reset` to also roll back the session) · `/cron` (list / add / remove scheduled tasks) · `/reserve` (schedule retry at usage-limit reset) · `/restart` (syntax-check & restart the bot) · `/status` (bot status & version) · `/model` (view / switch the model) · `/id` (show chat ID) · `/help`.
164
166
 
165
167
  > **`/stop`** kills the running Claude process immediately and clears any queued messages.
166
168
  > Add `--reset` to also restore the session to the state it was in *before* the task started,
@@ -223,6 +225,7 @@ your launchd plist points them.
223
225
  `state.json`, so context survives restarts. Use `/new` to start fresh.
224
226
  - **Message queue**: if you send a message while a task is running, it is queued (not dropped). When the task finishes, all queued messages are merged into a single prompt so Claude can resolve corrections and follow-ups in one pass (e.g. "do X" then "never mind, do Y" → handled together). Use `/stop` to cancel the running task and discard the queue.
225
227
  - **Model hint**: the bot tells Claude which model it is running as. If Claude judges a question to be beyond its current tier, it appends a one-line suggestion at the end of the reply (e.g. 💡 `/model sonnet`). Switch with `/model <name>` — `haiku`, `sonnet`, `opus`, `fable`, or a full model id. The choice persists in `state.json` across restarts.
228
+ - **Usage-limit retry**: when a Claude Max / API rate-limit error includes a reset time, the bot shows a `/reserve` hint. `/reserve` schedules your last message to be resent automatically at that time. Use `/reserve <different text>` to change what's sent, or `/reserve rm` to cancel.
226
229
 
227
230
  ### Scheduled tasks (cron)
228
231
 
package/bot.mjs CHANGED
@@ -114,6 +114,11 @@ if (!existsSync(CONFIG_PATH)) {
114
114
  const cfg = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
115
115
  console.log({ ...cfg, token: cfg.token ? "<redacted>" : "(none)" });
116
116
  const TG = `https://api.telegram.org/bot${cfg.token}`;
117
+ // allowedChatId 는 문자열 또는 배열 모두 허용 (하위 호환)
118
+ const allowedIds = []
119
+ .concat(cfg.allowedChatId)
120
+ .filter(Boolean)
121
+ .map(String);
117
122
 
118
123
  // ── i18n (영어 기본 + 한국어) ─────────────────────────────────────────────
119
124
  // cfg.lang 를 "en"/"ko" 로 주면 그 언어로 고정. 비우면 메시지의 from.language_code 로
@@ -133,6 +138,7 @@ const STR = {
133
138
  "• /cron — list tasks · /cron add <natural language> to add · /cron rm <id> to remove\n" +
134
139
  "• /remember <text> — save to persistent memory (survives /new)\n" +
135
140
  "• /memory — view memory · /memory clear to wipe\n" +
141
+ "• /reserve — retry at usage-limit reset time · /reserve <msg> for a different message · /reserve rm to cancel\n" +
136
142
  "• /restart — restart the bot (after a syntax check)\n" +
137
143
  "• /status — bot status & version\n" +
138
144
  "• /model — view / switch the model\n" +
@@ -192,6 +198,11 @@ const STR = {
192
198
  remembered: "💾 Saved to memory.",
193
199
  rememberUsage: "Usage: /remember <text to remember>",
194
200
  memoryUsage: "Usage: /memory · /memory clear",
201
+ reserveHint: "\n\nTo retry when the limit resets, send `/reserve` (or `/reserve <different message>`).",
202
+ reserveOk: (time) => `⏰ Retry scheduled for ${time}. Cancel with /reserve rm.`,
203
+ reserveRm: "🚫 Scheduled retry canceled.",
204
+ reserveNone: "No retry is scheduled.",
205
+ reserveNoLimit: "No recent usage limit error. Send a message first.",
195
206
  },
196
207
  ko: {
197
208
  help: () =>
@@ -202,6 +213,7 @@ const STR = {
202
213
  "• /cron — 예약 작업 보기 · /cron add <자연어>로 추가 · /cron rm <번호>로 삭제\n" +
203
214
  "• /remember <내용> — 퍼시스턴트 메모리에 저장 (/new 로 초기화해도 유지)\n" +
204
215
  "• /memory — 메모리 보기 · /memory clear 로 삭제\n" +
216
+ "• /reserve — 한도 리셋 시 재시도 예약 · /reserve <다른 메시지> 로 내용 변경 · /reserve rm 으로 취소\n" +
205
217
  "• /restart — 봇 재시작 (문법 검사 후 안전하게)\n" +
206
218
  "• /status — 봇 상태·버전 보기\n" +
207
219
  "• /model — 모델 보기·전환\n" +
@@ -260,6 +272,11 @@ const STR = {
260
272
  remembered: "💾 메모리에 저장했습니다.",
261
273
  rememberUsage: "사용법: /remember <기억할 내용>",
262
274
  memoryUsage: "사용법: /memory · /memory clear",
275
+ reserveHint: "\n\n리셋 후 재시도하려면 `/reserve` (또는 `/reserve <다른 메시지>`)를 입력하세요.",
276
+ reserveOk: (time) => `⏰ ${time}에 재시도 예약됨. 취소: /reserve rm`,
277
+ reserveRm: "🚫 예약된 재시도를 취소했습니다.",
278
+ reserveNone: "예약된 재시도가 없습니다.",
279
+ reserveNoLimit: "최근 한도 초과 에러가 없습니다. 먼저 메시지를 보내주세요.",
263
280
  },
264
281
  };
265
282
  const t = (l, key, ...a) => {
@@ -281,6 +298,7 @@ const COMMANDS = {
281
298
  { command: "restart", description: "Restart the bot (after syntax check)" },
282
299
  { command: "status", description: "Bot status / version" },
283
300
  { command: "model", description: "View / switch the model" },
301
+ { command: "reserve", description: "Schedule retry when usage limit resets · /reserve rm to cancel" },
284
302
  { command: "id", description: "Show this chat ID" },
285
303
  { command: "help", description: "Help" },
286
304
  ],
@@ -293,6 +311,7 @@ const COMMANDS = {
293
311
  { command: "restart", description: "봇 재시작 (문법 검사 후)" },
294
312
  { command: "status", description: "봇 상태·버전 보기" },
295
313
  { command: "model", description: "모델 보기·전환" },
314
+ { command: "reserve", description: "한도 리셋 시 재시도 예약 · /reserve rm 으로 취소" },
296
315
  { command: "id", description: "이 채팅 ID 확인" },
297
316
  { command: "help", description: "도움말" },
298
317
  ],
@@ -340,7 +359,7 @@ function isNewer(latest, current) {
340
359
  }
341
360
 
342
361
  async function checkForUpdate() {
343
- if (!cfg.allowedChatId) return;
362
+ if (!allowedIds.length) return;
344
363
  const now = Date.now();
345
364
  if (state.lastVersionCheck && now - state.lastVersionCheck < VERSION_CHECK_INTERVAL) return;
346
365
  const latest = await fetchLatestVersion();
@@ -350,9 +369,10 @@ async function checkForUpdate() {
350
369
  if (state.notifiedVersion === latest) return; // 이미 알린 버전
351
370
  state.notifiedVersion = latest;
352
371
  saveState(state);
353
- await send(cfg.allowedChatId,
354
- `✨ 업데이트가 있습니다: ${VERSION} → ${latest}\n\`npm i -g claude-telegram-bot\` 후 /restart`
355
- ).catch(() => {});
372
+ for (const id of allowedIds)
373
+ await send(id,
374
+ `✨ 업데이트가 있습니다: ${VERSION} → ${latest}\n\`npm i -g claude-telegram-bot\` 후 /restart`
375
+ ).catch(() => {});
356
376
  }
357
377
 
358
378
  // ── 퍼시스턴트 메모리 ─────────────────────────────────────────────────────
@@ -482,11 +502,34 @@ async function send(chatId, text) {
482
502
  }
483
503
 
484
504
  // ── Claude 에러 분류 ──────────────────────────────────────────────────────
505
+ function parseResetTime(raw) {
506
+ // ISO timestamp: 2026-06-17T14:00:00Z
507
+ const iso = raw.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?/);
508
+ if (iso) { const d = new Date(iso[0]); if (!isNaN(d)) return d; }
509
+ // "in X minutes" / "in X hours"
510
+ const inMin = raw.match(/in (\d+)\s*minute/i);
511
+ if (inMin) return new Date(Date.now() + parseInt(inMin[1]) * 60000);
512
+ const inHour = raw.match(/in (\d+)\s*hour/i);
513
+ if (inHour) return new Date(Date.now() + parseInt(inHour[1]) * 3600000);
514
+ // "resets at HH:MM" or "available at HH:MM"
515
+ const atTime = raw.match(/(?:resets?|reset|available|retry)\s+at\s+(\d{1,2}):(\d{2})(?:\s*(AM|PM))?/i);
516
+ if (atTime) {
517
+ let h = parseInt(atTime[1]);
518
+ const m = parseInt(atTime[2]);
519
+ if (atTime[3]) { if (/pm/i.test(atTime[3]) && h < 12) h += 12; if (/am/i.test(atTime[3]) && h === 12) h = 0; }
520
+ const d = new Date(); d.setHours(h, m, 0, 0);
521
+ if (d <= new Date()) d.setDate(d.getDate() + 1);
522
+ return d;
523
+ }
524
+ return null;
525
+ }
526
+
485
527
  function classifyClaudeError(raw, code) {
486
528
  const t = raw.toLowerCase();
487
529
  if (t.includes("credit") || t.includes("balance") || t.includes("billing") || t.includes("payment"))
488
530
  return "💳 API 크레딧이 부족합니다. console.anthropic.com 에서 충전해주세요.";
489
- if (t.includes("rate_limit") || t.includes("rate limit") || t.includes("too many requests") || code === 429)
531
+ if (t.includes("rate_limit") || t.includes("rate limit") || t.includes("too many requests") || code === 429
532
+ || t.includes("usage limit") || t.includes("monthly limit"))
490
533
  return "⏱️ 요청이 너무 많습니다. 잠시 후 다시 시도해주세요.";
491
534
  if (t.includes("overloaded") || code === 529)
492
535
  return "🔄 Claude 서버가 일시적으로 과부하 상태입니다. 잠시 후 다시 시도해주세요.";
@@ -516,9 +559,9 @@ function runClaude(prompt, sessionId, opts = {}) {
516
559
  : null;
517
560
  // opts.injectMemory: 퍼시스턴트 메모리를 시스템 프롬프트에 주입 (/new 로 초기화해도 유지)
518
561
  const mem = opts.injectMemory ? loadMemory() : "";
519
- const memoryBlock = mem ? `## Persistent memory (survives /new)\n${mem}` : null;
520
- // 페르소나(cfg.persona) + 간결 지침 + 모델 힌트 + 메모리를 함께 주입
521
- const appendSys = [cfg.persona, brevity, modelHint, memoryBlock].filter(Boolean).join("\n\n");
562
+ // 메모리는 persona보다 앞에 배치하고 헤더를 강화 persona가 덮어쓰는 방지
563
+ const memoryBlock = mem ? `## RULES (must follow before anything else)\n${mem}` : null;
564
+ const appendSys = [memoryBlock, cfg.persona, brevity, modelHint].filter(Boolean).join("\n\n");
522
565
  if (appendSys) args.push("--append-system-prompt", appendSys);
523
566
  if (model) args.push("--model", model);
524
567
  if (sessionId) args.push("--resume", sessionId);
@@ -545,13 +588,13 @@ function runClaude(prompt, sessionId, opts = {}) {
545
588
  currentChild = null;
546
589
  try {
547
590
  const j = JSON.parse(out);
548
- const text = j.is_error
549
- ? classifyClaudeError(j.result ?? "", code)
550
- : (j.result ?? "(empty response)");
551
- resolve({ ok: !j.is_error, text, sessionId: j.session_id, cost: j.total_cost_usd });
591
+ const rawErr = j.result ?? "";
592
+ const text = j.is_error ? classifyClaudeError(rawErr, code) : (rawErr || "(empty response)");
593
+ const resetAt = j.is_error ? parseResetTime(rawErr) : null;
594
+ resolve({ ok: !j.is_error, text, sessionId: j.session_id, cost: j.total_cost_usd, resetAt });
552
595
  } catch {
553
596
  const raw = (err || out || "no output").slice(0, 3500);
554
- resolve({ ok: false, text: classifyClaudeError(raw, code) });
597
+ resolve({ ok: false, text: classifyClaudeError(raw, code), resetAt: parseResetTime(raw) });
555
598
  }
556
599
  });
557
600
  });
@@ -643,9 +686,9 @@ async function runScheduled(job) {
643
686
  const footer = res.ok
644
687
  ? `\n\n— ⏰ ${label} · ${secs}s${res.cost ? ` · $${res.cost.toFixed(4)}` : ""}`
645
688
  : `\n\n— ⏰ ${label}`;
646
- await send(cfg.allowedChatId, (res.ok ? res.text : `⚠️ ${res.text}`) + footer);
689
+ for (const id of allowedIds) await send(id, (res.ok ? res.text : `⚠️ ${res.text}`) + footer);
647
690
  } catch (e) {
648
- await send(cfg.allowedChatId, t(BOT_LANG, "scheduledError", e.message));
691
+ for (const id of allowedIds) await send(id, t(BOT_LANG, "scheduledError", e.message));
649
692
  } finally {
650
693
  busy = false;
651
694
  }
@@ -654,7 +697,7 @@ async function runScheduled(job) {
654
697
  function startScheduler() {
655
698
  // allowedChatId 없으면 결과를 보낼 곳이 없으니 비활성화. /cron add 로 나중에 작업이
656
699
  // 늘 수 있으므로, schedule 이 지금 비어 있어도 인터벌은 항상 돌린다(없으면 no-op).
657
- if (!cfg.allowedChatId) {
700
+ if (!allowedIds.length) {
658
701
  console.warn("allowedChatId missing → scheduler disabled");
659
702
  return;
660
703
  }
@@ -795,10 +838,12 @@ async function downloadAttachment(att) {
795
838
  let busy = false;
796
839
  const msgQueue = []; // { msg, receivedAt } — busy 중 수신 메시지 대기열
797
840
  const mediaGroups = new Map(); // media_group_id → { msgs, timer } — 미디어 그룹 수집 대기
798
- let currentChild = null; // 실행 중인 claude child process (/stop 용)
799
- let currentTyping = null; // 타이핑 인터벌 (/stop 시 정리용)
800
- let prevSessionId; // /stop --reset 복원 대상
801
- let stopping = false; // /stop 처리 중 오류 메시지 억제 플래그
841
+ let currentChild = null; // 실행 중인 claude child process (/stop 용)
842
+ let currentTyping = null; // 타이핑 인터벌 (/stop 시 정리용)
843
+ let prevSessionId; // /stop --reset 복원 대상
844
+ let stopping = false; // /stop 처리 중 오류 메시지 억제 플래그
845
+ let rateLimitState = null; // 마지막 레이트 리밋 { prompt, resetAt } — /reserve 용
846
+ let pendingRetry = null; // /reserve 로 예약된 재시도 { timer, resetAt }
802
847
 
803
848
  async function handle(msg) {
804
849
  const chatId = msg.chat?.id;
@@ -809,11 +854,11 @@ async function handle(msg) {
809
854
  if (!text && !attachment && !msg._mediaGroup?.length) return;
810
855
 
811
856
  // 화이트리스트
812
- if (!cfg.allowedChatId) {
857
+ if (!allowedIds.length) {
813
858
  await send(chatId, t(l, "needChatId", chatId));
814
859
  return;
815
860
  }
816
- if (String(chatId) !== String(cfg.allowedChatId)) {
861
+ if (!allowedIds.includes(String(chatId))) {
817
862
  console.warn(`Ignoring unauthorized chatId ${chatId}`);
818
863
  return;
819
864
  }
@@ -928,6 +973,32 @@ async function handle(msg) {
928
973
  await send(chatId, mem ? t(l, "memoryShow", mem) : t(l, "memoryEmpty"));
929
974
  return;
930
975
  }
976
+ if (text === "/reserve" || text.startsWith("/reserve ")) {
977
+ const arg = text.slice(8).trim();
978
+ if (arg === "rm") {
979
+ if (!pendingRetry) { await send(chatId, t(l, "reserveNone")); return; }
980
+ clearTimeout(pendingRetry.timer); pendingRetry = null;
981
+ await send(chatId, t(l, "reserveRm"));
982
+ return;
983
+ }
984
+ if (!rateLimitState) { await send(chatId, t(l, "reserveNoLimit")); return; }
985
+ const { resetAt } = rateLimitState;
986
+ const reservePrompt = arg || rateLimitState.prompt;
987
+ if (pendingRetry) clearTimeout(pendingRetry.timer);
988
+ const capturedPrompt = reservePrompt;
989
+ const capturedMsg = msg;
990
+ const delay = Math.max(resetAt - Date.now(), 1000);
991
+ pendingRetry = {
992
+ resetAt,
993
+ timer: setTimeout(() => {
994
+ pendingRetry = null;
995
+ handle({ ...capturedMsg, text: capturedPrompt, caption: undefined });
996
+ }, delay),
997
+ };
998
+ const timeStr = resetAt.toLocaleTimeString(l === "ko" ? "ko-KR" : "en-US", { hour: "2-digit", minute: "2-digit" });
999
+ await send(chatId, t(l, "reserveOk", timeStr));
1000
+ return;
1001
+ }
931
1002
 
932
1003
  if (busy) {
933
1004
  msgQueue.push({ msg, receivedAt: Date.now() });
@@ -979,10 +1050,16 @@ async function handle(msg) {
979
1050
  saveState(state);
980
1051
  }
981
1052
  const secs = Math.round((Date.now() - started) / 1000);
982
- const footer = res.ok
983
- ? `\n\n— ${secs}s${res.cost ? ` · $${res.cost.toFixed(4)}` : ""}`
984
- : "";
985
- if (!stopping) await send(chatId, (res.ok ? res.text : `⚠️ ${res.text}`) + footer);
1053
+ if (!res.ok) {
1054
+ // 레이트 리밋이고 리셋 시간을 알면 /reserve 힌트 추가
1055
+ const hint = res.resetAt ? t(l, "reserveHint") : "";
1056
+ rateLimitState = res.resetAt ? { prompt, resetAt: res.resetAt } : null;
1057
+ if (!stopping) await send(chatId, `⚠️ ${res.text}${hint}`);
1058
+ } else {
1059
+ rateLimitState = null;
1060
+ const footer = `\n\n— ${secs}s${res.cost ? ` · $${res.cost.toFixed(4)}` : ""}`;
1061
+ if (!stopping) await send(chatId, res.text + footer);
1062
+ }
986
1063
  } catch (e) {
987
1064
  if (!stopping) await send(chatId, t(l, "botError", e.message));
988
1065
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-telegram-bot",
3
- "version": "0.3.14",
3
+ "version": "0.3.17",
4
4
  "description": "Drive Claude Code from Telegram — messages run headless `claude -p` in a project dir and replies come back to the chat. Zero dependencies.",
5
5
  "type": "module",
6
6
  "bin": {