claude-telegram-bot 0.2.7 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.ko.md +14 -8
  2. package/README.md +23 -15
  3. package/bot.mjs +70 -13
  4. package/package.json +1 -1
package/README.ko.md CHANGED
@@ -68,9 +68,10 @@ OpenClaw처럼 웹 UI까지 갖춘 구성을 써봤다면, 이 프로젝트는
68
68
  **npx로 바로 실행**
69
69
 
70
70
  ```sh
71
- npx claude-telegram-bot init # 현재 폴더에 config.json 생성
72
- # config.json 편집 (token, projectDir 등)
73
- npx claude-telegram-bot # config.json 으로 실행
71
+ npx claude-telegram-bot init # 현재 폴더에 mybot.json 생성
72
+ npx claude-telegram-bot init myapp.json # 파일명 직접 지정도 가능
73
+ # 설정 편집 (token, projectDir 등)
74
+ npx claude-telegram-bot # mybot.json 으로 실행 (없으면 config.json 폴백)
74
75
  ```
75
76
 
76
77
  **전역 설치 (상시 가동에 권장)**
@@ -78,16 +79,17 @@ npx claude-telegram-bot # config.json 으로 실행
78
79
  ```sh
79
80
  npm i -g claude-telegram-bot
80
81
 
81
- claude-telegram-bot init ~/botconfigs/myproj # 해당 경로에 config.json 생성
82
- # config.json 편집
83
- claude-telegram-bot ~/botconfigs/myproj/config.json
82
+ claude-telegram-bot init ~/botconfigs/myproj # mybot.json 생성
83
+ claude-telegram-bot init ~/botconfigs/myproj/myapp.json # 또는 파일명 지정
84
+ # 설정 편집
85
+ claude-telegram-bot ~/botconfigs/myproj/mybot.json
84
86
  ```
85
87
 
86
88
  > **설정 파일은 git에 올리지 마세요.** config 파일에는 봇 토큰이 들어 있습니다. git 레포 안에 둔다면 `config.json`, `state*.json`, `attachments/`를 그 프로젝트의 `.gitignore`에 추가하세요. 이 레포는 해당 패턴을 이미 무시하므로 `claudebot.config.json` 같은 이름도 안전하지만, 다른 프로젝트는 직접 지정해야 합니다.
87
89
 
88
90
  ## 설정
89
91
 
90
- `config.json`의 키는 다음과 같습니다.
92
+ `mybot.json`(또는 사용 중인 config 파일)의 키는 다음과 같습니다.
91
93
 
92
94
  | 키 | 설명 |
93
95
  |---|---|
@@ -114,13 +116,17 @@ claude-telegram-bot ~/botconfigs/myproj/config.json
114
116
  - `테스트 돌려보고 통과하면 커밋하고 push 해줘`
115
117
  - `api.ts 에 에러 핸들링 추가해줘`
116
118
 
117
- 명령어: `/new`(맥락 초기화) · `/cron`(예약 작업 보기·추가·삭제) · `/restart`(문법 검사 후 재시작) · `/status`(봇 상태·버전) · `/model`(모델 보기·전환) · `/id`(채팅 ID 확인) · `/help`(도움말)
119
+ 명령어: `/new`(맥락 초기화) · `/stop`(작업 중단; `--reset`으로 세션도 롤백) · `/cron`(예약 작업 보기·추가·삭제) · `/restart`(문법 검사 후 재시작) · `/status`(봇 상태·버전) · `/model`(모델 보기·전환) · `/id`(채팅 ID 확인) · `/help`(도움말)
120
+
121
+ > **`/stop`** 은 실행 중인 Claude 프로세스를 즉시 종료하고 대기 중인 메시지 큐도 비웁니다. `--reset`을 붙이면 세션을 작업 시작 이전 상태로 되돌려 중단된 작업이 대화 맥락에 남지 않습니다.
118
122
 
119
123
  > **`/restart`** 는 먼저 `bot.mjs` 에 `node --check` 를 돌려 **문법 오류가 있으면 재시작을 취소**합니다(잘못된 수정이 봇을 크래시 루프에 빠뜨리는 것 방지). 통과하면 프로세스를 종료하고, 다시 띄우는 건 프로세스 관리자에게 맡깁니다. [launchd 설정](#상시-실행-launchd)(`KeepAlive`)이면 바로 동작하고, 관리자 없이 `node bot.mjs` 로만 돌리면 그냥 멈춥니다. 재시작 후 대화 세션은 `state.json` 의 ID로 이어집니다.
120
124
 
121
125
  ## 사용 메모
122
126
 
123
127
  - **세션 유지** — 대화는 `--resume`으로 자동으로 이어집니다. 마지막 세션 ID가 `state.json`에 저장되므로 봇을 재시작해도 맥락이 남습니다. 새로 시작하려면 `/new`.
128
+ - **메시지 큐** — 작업 중에 새 메시지가 오면 버리지 않고 큐에 쌓아둡니다. 작업이 끝나면 대기 중인 메시지를 모두 하나의 프롬프트로 합쳐서 처리합니다(예: "A 해줘" → "아니다 B 해줘"를 한 번에 처리). `/stop`으로 실행 중인 작업과 큐를 동시에 취소할 수 있습니다.
129
+ - **모델 권유** — 봇이 Claude에게 현재 모델을 알려줍니다. 질문 난이도가 현재 모델 수준을 넘는다고 판단되면 답변 끝에 전환 권유 한 줄이 붙습니다(예: 💡 `/model sonnet`). `/model <이름>`으로 전환(`haiku`, `sonnet`, `opus`, `fable`, 또는 전체 모델 ID) — `state.json`에 저장돼 재시작 후에도 유지됩니다.
124
130
  - **간결한 답변** — 텔레그램에 맞게 짧게 답하도록 시스템 프롬프트가 기본으로 붙습니다. 바꾸려면 `appendSystemPrompt`에 직접 넣으세요 (빈 문자열이면 끔).
125
131
  - **언어** — 봇 자체 문구(`/help`, 명령 메뉴, 상태 메시지)는 **기본 영어**, 텔레그램이 한국어인 사용자에겐 한국어로 나옵니다. `lang`(`"en"`/`"ko"`)으로 고정할 수 있습니다. Claude의 실제 답변은 **사용자가 쓴 언어**를 따라갑니다. `/` 명령 메뉴는 `setMyCommands`로 언어별 등록됩니다.
126
132
  - **서식 변환** — 답변의 마크다운(굵게·코드·표 등)을 텔레그램 HTML로 바꿔 보냅니다. 변환이 깨지는 경우엔 평문으로 다시 보냅니다.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  A tiny bridge that takes your Telegram messages, runs `claude -p` (Claude Code headless mode)
12
12
  in a project folder, and sends the result back to the chat. One `.mjs` file on Node 18+ built-ins —
13
- nothing to `npm install`, nothing to audit but ~400 readable lines.
13
+ nothing to `npm install`, nothing to audit but under 1 000 readable lines.
14
14
 
15
15
  ```
16
16
  [you] → Telegram → bot.mjs → claude -p (config.projectDir) → result → Telegram
@@ -118,9 +118,10 @@ Prerequisites: **Node 18+** and the **`claude` CLI installed and authenticated**
118
118
  **Option A — npx (no install)**
119
119
 
120
120
  ```sh
121
- npx claude-telegram-bot init # writes ./config.json
122
- # edit config.json (token, projectDir, …)
123
- npx claude-telegram-bot # runs ./config.json
121
+ npx claude-telegram-bot init # writes ./mybot.json
122
+ npx claude-telegram-bot init myapp.json # or pick your own filename
123
+ # edit the config (token, projectDir, …)
124
+ npx claude-telegram-bot # runs ./mybot.json (falls back to config.json)
124
125
  ```
125
126
 
126
127
  **Option B — global install (recommended for an always-on daemon)**
@@ -128,9 +129,10 @@ npx claude-telegram-bot # runs ./config.json
128
129
  ```sh
129
130
  npm i -g claude-telegram-bot
130
131
 
131
- claude-telegram-bot init ~/botconfigs/myproj # writes ~/botconfigs/myproj/config.json
132
- # edit that config.json (token, projectDir, …)
133
- claude-telegram-bot ~/botconfigs/myproj/config.json
132
+ claude-telegram-bot init ~/botconfigs/myproj # writes ~/botconfigs/myproj/mybot.json
133
+ claude-telegram-bot init ~/botconfigs/myproj/myapp.json # or a custom filename
134
+ # edit the config (token, projectDir, …)
135
+ claude-telegram-bot ~/botconfigs/myproj/mybot.json
134
136
  ```
135
137
 
136
138
  Run several projects/personas by making one config file each and passing its path —
@@ -149,7 +151,7 @@ Run several projects/personas by making one config file each and passing its pat
149
151
  leave `allowedChatId` empty for now.
150
152
 
151
153
  **2) Find your chatId and lock the bot to it** — Start the bot (`claude-telegram-bot …`), send it any
152
- message in Telegram; it replies with this chat's `chatId`. Put that number into `config.json`
154
+ message in Telegram; it replies with this chat's `chatId`. Put that number into `mybot.json`
153
155
  `allowedChatId` and restart. Now only you can use it. (See [Security](#security) — this is your only
154
156
  auth layer.)
155
157
 
@@ -158,7 +160,11 @@ auth layer.)
158
160
  - `run the solver tests and commit + push if they pass`
159
161
  - `add an edge case to solve-2nd-floor-edges.ts`
160
162
 
161
- Commands: `/new` (reset context / new 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`.
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`.
164
+
165
+ > **`/stop`** kills the running Claude process immediately and clears any queued messages.
166
+ > Add `--reset` to also restore the session to the state it was in *before* the task started,
167
+ > so the conversation history doesn't include the interrupted work.
162
168
 
163
169
  > **`/restart`** runs `node --check` on `bot.mjs` first and **aborts the restart if it has a syntax
164
170
  > error** (so a bad edit can't crash-loop the bot), then exits — relying on a process supervisor
@@ -168,18 +174,18 @@ Commands: `/new` (reset context / new session) · `/cron` (list / add / remove s
168
174
 
169
175
  **4) Keep it always on (optional)** — see [Always-on with launchd](#always-on-with-launchd-macos).
170
176
 
171
- > **From source** (for hacking on the bot): clone the repo, `cp config.example.json config.json`,
172
- > then `node bot.mjs [config.json]`. Same behavior as the CLI.
177
+ > **From source** (for hacking on the bot): clone the repo, `cp config.example.json mybot.json`,
178
+ > then `node bot.mjs [mybot.json]`. Same behavior as the CLI.
173
179
 
174
180
  ---
175
181
 
176
182
  ## Configuration
177
183
 
178
184
  ```sh
179
- cp config.example.json config.json
185
+ cp config.example.json mybot.json
180
186
  ```
181
187
 
182
- Edit `config.json`:
188
+ Edit `mybot.json`:
183
189
 
184
190
  | Key | Description |
185
191
  |---|---|
@@ -215,6 +221,8 @@ your launchd plist points them.
215
221
  absolute path is handed to Claude (caption included as the message). Images can be opened with Read.
216
222
  - **Sessions**: conversations resume automatically (`--resume`); the last session id is saved in
217
223
  `state.json`, so context survives restarts. Use `/new` to start fresh.
224
+ - **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
+ - **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.
218
226
 
219
227
  ### Scheduled tasks (cron)
220
228
 
@@ -264,7 +272,7 @@ Config-defined jobs still require a restart to change; only chat-added jobs are
264
272
 
265
273
  The code is project-agnostic: make **one config file per project** and run several at once.
266
274
 
267
- - Run: `node bot.mjs /absolute/path/to/project.config.json` (no arg → `./config.json`)
275
+ - Run: `node bot.mjs /absolute/path/to/project.config.json` (no arg → `./mybot.json`, fallback `./config.json`)
268
276
  - `state.json` and `attachments/` live in the **config file's folder**, so projects don't mix.
269
277
  - **Note**: Telegram allows only one poller per token → each project needs its **own BotFather
270
278
  token**.
@@ -290,7 +298,7 @@ One codebase, **a separate config file per role**.
290
298
  shell-using bot (`bypassPermissions`) to **just one** to avoid concurrent-edit conflicts. For
291
299
  read/plan-only, use `plan`.
292
300
  - **Session isolation**: the `state` filename is derived from the config name
293
- (`config.json` → `state.json`, `dev.config.json` → `dev.config.state.json`), so multiple configs
301
+ (`mybot.json` → `mybot.state.json`, `dev.config.json` → `dev.config.state.json`), so multiple configs
294
302
  in one folder don't share context.
295
303
  - **One token per bot**: each bot needs its own BotFather token (`allowedChatId` can be the same).
296
304
 
package/bot.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  // 사용자 대상 문구는 영어 기본 + 한국어(STR 테이블). 언어는 텔레그램 from.language_code 로
14
14
  // 자동 판별하고, cfg.lang 을 주면 그 언어로 고정함. 콘솔/CLI 출력은 영어 단일.
15
15
 
16
- import { basename, dirname, join } from "node:path";
16
+ import { basename, dirname, join, resolve } from "node:path";
17
17
  import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
18
18
 
19
19
  import dns from "node:dns";
@@ -57,7 +57,8 @@ Requires: the claude CLI installed and authenticated on the host.`);
57
57
  process.exit(0);
58
58
  }
59
59
  if (a === "init") {
60
- const target = join(process.argv[3] || process.cwd(), "config.json");
60
+ const arg = process.argv[3];
61
+ const target = arg?.endsWith(".json") ? resolve(arg) : join(arg || process.cwd(), "mybot.json");
61
62
  if (existsSync(target)) {
62
63
  console.error(`Already exists: ${target}`);
63
64
  process.exit(1);
@@ -70,8 +71,9 @@ Requires: the claude CLI installed and authenticated on the host.`);
70
71
 
71
72
  // Config path via arg or BOT_CONFIG env so one shared codebase can drive many
72
73
  // projects; state + attachments live next to that config, keeping projects
73
- // isolated. Falls back to ./config.json for the single-project setup.
74
- const CONFIG_PATH = process.argv[2] || process.env.BOT_CONFIG || join(HERE, "config.json");
74
+ // isolated. Defaults to mybot.json, falls back to config.json for existing setups.
75
+ const _defaultCfg = existsSync(join(HERE, "mybot.json")) ? join(HERE, "mybot.json") : join(HERE, "config.json");
76
+ const CONFIG_PATH = process.argv[2] || process.env.BOT_CONFIG || _defaultCfg;
75
77
  const DATA_DIR = dirname(CONFIG_PATH);
76
78
  // 데이터(state·attachments)는 config 폴더 아래 숨김 폴더 .claude-bot/ 에 모은다.
77
79
  // state 파일명은 config 이름에서 파생 → 여러 페르소나 config 가 한 .claude-bot/ 를 공유해도 안 섞임
@@ -126,6 +128,7 @@ const STR = {
126
128
  `${cfg.name || "Claude Code Telegram bot"}\n\n` +
127
129
  "• Just send a message and Claude works in the project.\n" +
128
130
  "• /new — reset conversation context (new session)\n" +
131
+ "• /stop — stop the current task · /stop --reset to also roll back the session\n" +
129
132
  "• /cron — list tasks · /cron add <natural language> to add · /cron rm <id> to remove\n" +
130
133
  "• /restart — restart the bot (after a syntax check)\n" +
131
134
  "• /status — bot status & version\n" +
@@ -134,6 +137,10 @@ const STR = {
134
137
  `\nWorking dir: ${cfg.projectDir}\nPermission mode: ${cfg.permissionMode}`,
135
138
  newSession: "🆕 Started a new conversation (previous context cleared).",
136
139
  busy: "⏳ A previous task is still running. Please try again when it finishes.",
140
+ queued: (n) => `⏳ Queued (#${n}). Will run when the current task finishes.`,
141
+ stopOk: "🛑 Task stopped.",
142
+ stopReset: "🛑 Task stopped and session rolled back to before the task.",
143
+ stopNoop: "No task is running.",
137
144
  localBusy: "💻 A local `ctb claude` session is active. Send a message when it's done.",
138
145
  needChatId: (id) => `Add this chat ID to "allowedChatId" in config.json:\n${id}`,
139
146
  cronEmpty:
@@ -182,6 +189,7 @@ const STR = {
182
189
  `${cfg.name || "Claude Code 텔레그램 봇"}\n\n` +
183
190
  "• 그냥 메시지를 보내면 Claude가 프로젝트에서 작업합니다.\n" +
184
191
  "• /new — 대화 맥락 초기화 (새 세션)\n" +
192
+ "• /stop — 진행 중인 작업 중단 · /stop --reset 으로 세션도 되돌리기\n" +
185
193
  "• /cron — 예약 작업 보기 · /cron add <자연어>로 추가 · /cron rm <번호>로 삭제\n" +
186
194
  "• /restart — 봇 재시작 (문법 검사 후 안전하게)\n" +
187
195
  "• /status — 봇 상태·버전 보기\n" +
@@ -190,6 +198,10 @@ const STR = {
190
198
  `\n작업 폴더: ${cfg.projectDir}\n권한 모드: ${cfg.permissionMode}`,
191
199
  newSession: "🆕 새 대화를 시작합니다 (이전 맥락 초기화).",
192
200
  busy: "⏳ 이전 작업이 아직 진행 중입니다. 끝나면 다시 보내주세요.",
201
+ queued: (n) => `⏳ 대기열에 추가됐습니다 (${n}번째). 현재 작업이 끝나면 자동으로 실행됩니다.`,
202
+ stopOk: "🛑 작업을 중단했습니다.",
203
+ stopReset: "🛑 작업을 중단하고 세션을 작업 이전으로 되돌렸습니다.",
204
+ stopNoop: "실행 중인 작업이 없습니다.",
193
205
  localBusy: "💻 로컬 `ctb claude` 세션이 활성화되어 있습니다. 종료 후 메시지를 보내주세요.",
194
206
  needChatId: (id) => `이 채팅 ID를 config.json 의 allowedChatId 에 넣으세요:\n${id}`,
195
207
  cronEmpty:
@@ -245,6 +257,7 @@ const MODEL_SUGGESTIONS = ["fable", "opus", "sonnet", "haiku"];
245
257
  const COMMANDS = {
246
258
  en: [
247
259
  { command: "new", description: "Reset context (new session)" },
260
+ { command: "stop", description: "Stop the current task (--reset to roll back session)" },
248
261
  { command: "cron", description: "List / add / remove scheduled tasks" },
249
262
  { command: "restart", description: "Restart the bot (after syntax check)" },
250
263
  { command: "status", description: "Bot status / version" },
@@ -254,6 +267,7 @@ const COMMANDS = {
254
267
  ],
255
268
  ko: [
256
269
  { command: "new", description: "대화 맥락 초기화 (새 세션)" },
270
+ { command: "stop", description: "작업 중단 (--reset 으로 세션 되돌리기)" },
257
271
  { command: "cron", description: "예약 작업 보기·추가·삭제" },
258
272
  { command: "restart", description: "봇 재시작 (문법 검사 후)" },
259
273
  { command: "status", description: "봇 상태·버전 보기" },
@@ -426,6 +440,7 @@ function runClaude(prompt, sessionId, opts = {}) {
426
440
  cwd: cfg.projectDir,
427
441
  env: { ...process.env, ...(cfg.env || {}) },
428
442
  });
443
+ if (opts.trackChild) currentChild = child; // /stop 에서 kill 가능하도록 노출
429
444
 
430
445
  let out = "",
431
446
  err = "";
@@ -435,10 +450,12 @@ function runClaude(prompt, sessionId, opts = {}) {
435
450
  child.stderr.on("data", (d) => {
436
451
  err += d;
437
452
  });
438
- child.on("error", (e) =>
439
- resolve({ ok: false, text: `Failed to start claude: ${e.message}` }),
440
- );
453
+ child.on("error", (e) => {
454
+ currentChild = null;
455
+ resolve({ ok: false, text: `Failed to start claude: ${e.message}` });
456
+ });
441
457
  child.on("close", (code) => {
458
+ currentChild = null;
442
459
  try {
443
460
  const j = JSON.parse(out);
444
461
  resolve({
@@ -693,6 +710,11 @@ async function downloadAttachment(att) {
693
710
 
694
711
  // ── 메시지 처리 ───────────────────────────────────────────────────────────
695
712
  let busy = false;
713
+ const msgQueue = []; // { msg, receivedAt } — busy 중 수신 메시지 대기열
714
+ let currentChild = null; // 실행 중인 claude child process (/stop 용)
715
+ let currentTyping = null; // 타이핑 인터벌 (/stop 시 정리용)
716
+ let prevSessionId; // /stop --reset 복원 대상
717
+ let stopping = false; // /stop 처리 중 오류 메시지 억제 플래그
696
718
 
697
719
  async function handle(msg) {
698
720
  const chatId = msg.chat?.id;
@@ -783,9 +805,26 @@ async function handle(msg) {
783
805
  await send(chatId, t(l, "newSession"));
784
806
  return;
785
807
  }
808
+ if (text === "/stop" || text.startsWith("/stop ")) {
809
+ if (!busy || !currentChild) {
810
+ await send(chatId, t(l, "stopNoop"));
811
+ return;
812
+ }
813
+ const reset = text.includes("--reset");
814
+ stopping = true;
815
+ msgQueue.length = 0; // 대기 메시지도 취소
816
+ currentChild.kill();
817
+ if (reset) {
818
+ state.sessionId = prevSessionId;
819
+ saveState(state);
820
+ }
821
+ await send(chatId, t(l, reset ? "stopReset" : "stopOk"));
822
+ return;
823
+ }
786
824
 
787
825
  if (busy) {
788
- await send(chatId, t(l, "busy"));
826
+ msgQueue.push({ msg, receivedAt: Date.now() });
827
+ await send(chatId, t(l, "queued", msgQueue.length));
789
828
  return;
790
829
  }
791
830
  if (checkLocalLock()) {
@@ -796,7 +835,7 @@ async function handle(msg) {
796
835
  await tg("sendChatAction", { chat_id: chatId, action: "typing" });
797
836
  const started = Date.now();
798
837
  // 긴 작업 동안 타이핑 표시 유지
799
- const typing = setInterval(
838
+ currentTyping = setInterval(
800
839
  () =>
801
840
  tg("sendChatAction", { chat_id: chatId, action: "typing" }).catch(
802
841
  () => {},
@@ -815,7 +854,8 @@ async function handle(msg) {
815
854
  await send(chatId, t(l, "attachFail", e.message));
816
855
  }
817
856
  }
818
- const res = await runClaude(prompt, state.sessionId, { modelHint: true });
857
+ prevSessionId = state.sessionId; // /stop --reset 복원 대상 저장
858
+ const res = await runClaude(prompt, state.sessionId, { modelHint: true, trackChild: true });
819
859
  if (res.sessionId) {
820
860
  state.sessionId = res.sessionId;
821
861
  saveState(state);
@@ -824,15 +864,32 @@ async function handle(msg) {
824
864
  const footer = res.ok
825
865
  ? `\n\n— ${secs}s${res.cost ? ` · $${res.cost.toFixed(4)}` : ""}`
826
866
  : "";
827
- await send(chatId, (res.ok ? res.text : `⚠️ ${res.text}`) + footer);
867
+ if (!stopping) await send(chatId, (res.ok ? res.text : `⚠️ ${res.text}`) + footer);
828
868
  } catch (e) {
829
- await send(chatId, t(l, "botError", e.message));
869
+ if (!stopping) await send(chatId, t(l, "botError", e.message));
830
870
  } finally {
831
- clearInterval(typing);
871
+ clearInterval(currentTyping);
872
+ currentTyping = null;
873
+ stopping = false;
832
874
  busy = false;
875
+ if (msgQueue.length > 0) setImmediate(() => handle(drainQueue()));
833
876
  }
834
877
  }
835
878
 
879
+ // 큐 전체를 꺼내 하나의 메시지로 합침. 여러 개면 번호+경과시간 붙여 병합 → Claude가 맥락 일괄 파악.
880
+ function drainQueue() {
881
+ if (msgQueue.length === 1) return msgQueue.shift().msg;
882
+ const group = msgQueue.splice(0);
883
+ const merged = group
884
+ .map((item, i) => {
885
+ const text = item.msg.text || item.msg.caption || "";
886
+ const dt = Math.round((item.receivedAt - group[0].receivedAt) / 1000);
887
+ return i === 0 ? `[1] ${text}` : `[${i + 1}, +${dt}s] ${text}`;
888
+ })
889
+ .join("\n");
890
+ return { ...group[group.length - 1].msg, text: merged, caption: undefined };
891
+ }
892
+
836
893
  // ── 롱폴링 루프 ───────────────────────────────────────────────────────────
837
894
  async function main() {
838
895
  console.log("Bot started. Polling Telegram...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-telegram-bot",
3
- "version": "0.2.7",
3
+ "version": "0.3.1",
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": {