claude-telegram-bot 0.2.7 → 0.3.0

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
@@ -68,9 +68,11 @@ 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 # 현재 폴더에 config.json 생성
72
+ npx claude-telegram-bot init mybot.json # 파일명 직접 지정도 가능
73
+ # 설정 편집 (token, projectDir 등)
74
+ npx claude-telegram-bot # config.json 으로 실행
75
+ npx claude-telegram-bot mybot.json # 또는 경로를 직접 전달
74
76
  ```
75
77
 
76
78
  **전역 설치 (상시 가동에 권장)**
@@ -78,9 +80,10 @@ npx claude-telegram-bot # config.json 으로 실행
78
80
  ```sh
79
81
  npm i -g claude-telegram-bot
80
82
 
81
- claude-telegram-bot init ~/botconfigs/myproj # 해당 경로에 config.json 생성
82
- # config.json 편집
83
- claude-telegram-bot ~/botconfigs/myproj/config.json
83
+ claude-telegram-bot init ~/botconfigs/myproj # config.json 생성
84
+ claude-telegram-bot init ~/botconfigs/myproj/mybot.json # 또는 파일명 지정
85
+ # 설정 편집
86
+ claude-telegram-bot ~/botconfigs/myproj/mybot.json
84
87
  ```
85
88
 
86
89
  > **설정 파일은 git에 올리지 마세요.** config 파일에는 봇 토큰이 들어 있습니다. git 레포 안에 둔다면 `config.json`, `state*.json`, `attachments/`를 그 프로젝트의 `.gitignore`에 추가하세요. 이 레포는 해당 패턴을 이미 무시하므로 `claudebot.config.json` 같은 이름도 안전하지만, 다른 프로젝트는 직접 지정해야 합니다.
@@ -114,13 +117,17 @@ claude-telegram-bot ~/botconfigs/myproj/config.json
114
117
  - `테스트 돌려보고 통과하면 커밋하고 push 해줘`
115
118
  - `api.ts 에 에러 핸들링 추가해줘`
116
119
 
117
- 명령어: `/new`(맥락 초기화) · `/cron`(예약 작업 보기·추가·삭제) · `/restart`(문법 검사 후 재시작) · `/status`(봇 상태·버전) · `/model`(모델 보기·전환) · `/id`(채팅 ID 확인) · `/help`(도움말)
120
+ 명령어: `/new`(맥락 초기화) · `/stop`(작업 중단; `--reset`으로 세션도 롤백) · `/cron`(예약 작업 보기·추가·삭제) · `/restart`(문법 검사 후 재시작) · `/status`(봇 상태·버전) · `/model`(모델 보기·전환) · `/id`(채팅 ID 확인) · `/help`(도움말)
121
+
122
+ > **`/stop`** 은 실행 중인 Claude 프로세스를 즉시 종료하고 대기 중인 메시지 큐도 비웁니다. `--reset`을 붙이면 세션을 작업 시작 이전 상태로 되돌려 중단된 작업이 대화 맥락에 남지 않습니다.
118
123
 
119
124
  > **`/restart`** 는 먼저 `bot.mjs` 에 `node --check` 를 돌려 **문법 오류가 있으면 재시작을 취소**합니다(잘못된 수정이 봇을 크래시 루프에 빠뜨리는 것 방지). 통과하면 프로세스를 종료하고, 다시 띄우는 건 프로세스 관리자에게 맡깁니다. [launchd 설정](#상시-실행-launchd)(`KeepAlive`)이면 바로 동작하고, 관리자 없이 `node bot.mjs` 로만 돌리면 그냥 멈춥니다. 재시작 후 대화 세션은 `state.json` 의 ID로 이어집니다.
120
125
 
121
126
  ## 사용 메모
122
127
 
123
128
  - **세션 유지** — 대화는 `--resume`으로 자동으로 이어집니다. 마지막 세션 ID가 `state.json`에 저장되므로 봇을 재시작해도 맥락이 남습니다. 새로 시작하려면 `/new`.
129
+ - **메시지 큐** — 작업 중에 새 메시지가 오면 버리지 않고 큐에 쌓아둡니다. 작업이 끝나면 대기 중인 메시지를 모두 하나의 프롬프트로 합쳐서 처리합니다(예: "A 해줘" → "아니다 B 해줘"를 한 번에 처리). `/stop`으로 실행 중인 작업과 큐를 동시에 취소할 수 있습니다.
130
+ - **모델 권유** — 봇이 Claude에게 현재 모델을 알려줍니다. 질문 난이도가 현재 모델 수준을 넘는다고 판단되면 답변 끝에 전환 권유 한 줄이 붙습니다(예: 💡 `/model sonnet`). `/model <이름>`으로 전환(`haiku`, `sonnet`, `opus`, `fable`, 또는 전체 모델 ID) — `state.json`에 저장돼 재시작 후에도 유지됩니다.
124
131
  - **간결한 답변** — 텔레그램에 맞게 짧게 답하도록 시스템 프롬프트가 기본으로 붙습니다. 바꾸려면 `appendSystemPrompt`에 직접 넣으세요 (빈 문자열이면 끔).
125
132
  - **언어** — 봇 자체 문구(`/help`, 명령 메뉴, 상태 메시지)는 **기본 영어**, 텔레그램이 한국어인 사용자에겐 한국어로 나옵니다. `lang`(`"en"`/`"ko"`)으로 고정할 수 있습니다. Claude의 실제 답변은 **사용자가 쓴 언어**를 따라갑니다. `/` 명령 메뉴는 `setMyCommands`로 언어별 등록됩니다.
126
133
  - **서식 변환** — 답변의 마크다운(굵게·코드·표 등)을 텔레그램 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,11 @@ 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 ./config.json
122
+ npx claude-telegram-bot init mybot.json # or pick your own filename
123
+ # edit the config (token, projectDir, …)
124
+ npx claude-telegram-bot # runs ./config.json
125
+ npx claude-telegram-bot mybot.json # or pass the path directly
124
126
  ```
125
127
 
126
128
  **Option B — global install (recommended for an always-on daemon)**
@@ -128,9 +130,10 @@ npx claude-telegram-bot # runs ./config.json
128
130
  ```sh
129
131
  npm i -g claude-telegram-bot
130
132
 
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
133
+ claude-telegram-bot init ~/botconfigs/myproj # writes ~/botconfigs/myproj/config.json
134
+ claude-telegram-bot init ~/botconfigs/myproj/mybot.json # or a custom filename
135
+ # edit the config (token, projectDir, …)
136
+ claude-telegram-bot ~/botconfigs/myproj/mybot.json
134
137
  ```
135
138
 
136
139
  Run several projects/personas by making one config file each and passing its path —
@@ -158,7 +161,11 @@ auth layer.)
158
161
  - `run the solver tests and commit + push if they pass`
159
162
  - `add an edge case to solve-2nd-floor-edges.ts`
160
163
 
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`.
164
+ 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
+
166
+ > **`/stop`** kills the running Claude process immediately and clears any queued messages.
167
+ > Add `--reset` to also restore the session to the state it was in *before* the task started,
168
+ > so the conversation history doesn't include the interrupted work.
162
169
 
163
170
  > **`/restart`** runs `node --check` on `bot.mjs` first and **aborts the restart if it has a syntax
164
171
  > error** (so a bad edit can't crash-loop the bot), then exits — relying on a process supervisor
@@ -215,6 +222,8 @@ your launchd plist points them.
215
222
  absolute path is handed to Claude (caption included as the message). Images can be opened with Read.
216
223
  - **Sessions**: conversations resume automatically (`--resume`); the last session id is saved in
217
224
  `state.json`, so context survives restarts. Use `/new` to start fresh.
225
+ - **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.
226
+ - **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
227
 
219
228
  ### Scheduled tasks (cron)
220
229
 
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(), "config.json");
61
62
  if (existsSync(target)) {
62
63
  console.error(`Already exists: ${target}`);
63
64
  process.exit(1);
@@ -126,6 +127,7 @@ const STR = {
126
127
  `${cfg.name || "Claude Code Telegram bot"}\n\n` +
127
128
  "• Just send a message and Claude works in the project.\n" +
128
129
  "• /new — reset conversation context (new session)\n" +
130
+ "• /stop — stop the current task · /stop --reset to also roll back the session\n" +
129
131
  "• /cron — list tasks · /cron add <natural language> to add · /cron rm <id> to remove\n" +
130
132
  "• /restart — restart the bot (after a syntax check)\n" +
131
133
  "• /status — bot status & version\n" +
@@ -134,6 +136,10 @@ const STR = {
134
136
  `\nWorking dir: ${cfg.projectDir}\nPermission mode: ${cfg.permissionMode}`,
135
137
  newSession: "🆕 Started a new conversation (previous context cleared).",
136
138
  busy: "⏳ A previous task is still running. Please try again when it finishes.",
139
+ queued: (n) => `⏳ Queued (#${n}). Will run when the current task finishes.`,
140
+ stopOk: "🛑 Task stopped.",
141
+ stopReset: "🛑 Task stopped and session rolled back to before the task.",
142
+ stopNoop: "No task is running.",
137
143
  localBusy: "💻 A local `ctb claude` session is active. Send a message when it's done.",
138
144
  needChatId: (id) => `Add this chat ID to "allowedChatId" in config.json:\n${id}`,
139
145
  cronEmpty:
@@ -182,6 +188,7 @@ const STR = {
182
188
  `${cfg.name || "Claude Code 텔레그램 봇"}\n\n` +
183
189
  "• 그냥 메시지를 보내면 Claude가 프로젝트에서 작업합니다.\n" +
184
190
  "• /new — 대화 맥락 초기화 (새 세션)\n" +
191
+ "• /stop — 진행 중인 작업 중단 · /stop --reset 으로 세션도 되돌리기\n" +
185
192
  "• /cron — 예약 작업 보기 · /cron add <자연어>로 추가 · /cron rm <번호>로 삭제\n" +
186
193
  "• /restart — 봇 재시작 (문법 검사 후 안전하게)\n" +
187
194
  "• /status — 봇 상태·버전 보기\n" +
@@ -190,6 +197,10 @@ const STR = {
190
197
  `\n작업 폴더: ${cfg.projectDir}\n권한 모드: ${cfg.permissionMode}`,
191
198
  newSession: "🆕 새 대화를 시작합니다 (이전 맥락 초기화).",
192
199
  busy: "⏳ 이전 작업이 아직 진행 중입니다. 끝나면 다시 보내주세요.",
200
+ queued: (n) => `⏳ 대기열에 추가됐습니다 (${n}번째). 현재 작업이 끝나면 자동으로 실행됩니다.`,
201
+ stopOk: "🛑 작업을 중단했습니다.",
202
+ stopReset: "🛑 작업을 중단하고 세션을 작업 이전으로 되돌렸습니다.",
203
+ stopNoop: "실행 중인 작업이 없습니다.",
193
204
  localBusy: "💻 로컬 `ctb claude` 세션이 활성화되어 있습니다. 종료 후 메시지를 보내주세요.",
194
205
  needChatId: (id) => `이 채팅 ID를 config.json 의 allowedChatId 에 넣으세요:\n${id}`,
195
206
  cronEmpty:
@@ -245,6 +256,7 @@ const MODEL_SUGGESTIONS = ["fable", "opus", "sonnet", "haiku"];
245
256
  const COMMANDS = {
246
257
  en: [
247
258
  { command: "new", description: "Reset context (new session)" },
259
+ { command: "stop", description: "Stop the current task (--reset to roll back session)" },
248
260
  { command: "cron", description: "List / add / remove scheduled tasks" },
249
261
  { command: "restart", description: "Restart the bot (after syntax check)" },
250
262
  { command: "status", description: "Bot status / version" },
@@ -254,6 +266,7 @@ const COMMANDS = {
254
266
  ],
255
267
  ko: [
256
268
  { command: "new", description: "대화 맥락 초기화 (새 세션)" },
269
+ { command: "stop", description: "작업 중단 (--reset 으로 세션 되돌리기)" },
257
270
  { command: "cron", description: "예약 작업 보기·추가·삭제" },
258
271
  { command: "restart", description: "봇 재시작 (문법 검사 후)" },
259
272
  { command: "status", description: "봇 상태·버전 보기" },
@@ -426,6 +439,7 @@ function runClaude(prompt, sessionId, opts = {}) {
426
439
  cwd: cfg.projectDir,
427
440
  env: { ...process.env, ...(cfg.env || {}) },
428
441
  });
442
+ if (opts.trackChild) currentChild = child; // /stop 에서 kill 가능하도록 노출
429
443
 
430
444
  let out = "",
431
445
  err = "";
@@ -435,10 +449,12 @@ function runClaude(prompt, sessionId, opts = {}) {
435
449
  child.stderr.on("data", (d) => {
436
450
  err += d;
437
451
  });
438
- child.on("error", (e) =>
439
- resolve({ ok: false, text: `Failed to start claude: ${e.message}` }),
440
- );
452
+ child.on("error", (e) => {
453
+ currentChild = null;
454
+ resolve({ ok: false, text: `Failed to start claude: ${e.message}` });
455
+ });
441
456
  child.on("close", (code) => {
457
+ currentChild = null;
442
458
  try {
443
459
  const j = JSON.parse(out);
444
460
  resolve({
@@ -693,6 +709,11 @@ async function downloadAttachment(att) {
693
709
 
694
710
  // ── 메시지 처리 ───────────────────────────────────────────────────────────
695
711
  let busy = false;
712
+ const msgQueue = []; // { msg, receivedAt } — busy 중 수신 메시지 대기열
713
+ let currentChild = null; // 실행 중인 claude child process (/stop 용)
714
+ let currentTyping = null; // 타이핑 인터벌 (/stop 시 정리용)
715
+ let prevSessionId; // /stop --reset 복원 대상
716
+ let stopping = false; // /stop 처리 중 오류 메시지 억제 플래그
696
717
 
697
718
  async function handle(msg) {
698
719
  const chatId = msg.chat?.id;
@@ -783,9 +804,26 @@ async function handle(msg) {
783
804
  await send(chatId, t(l, "newSession"));
784
805
  return;
785
806
  }
807
+ if (text === "/stop" || text.startsWith("/stop ")) {
808
+ if (!busy || !currentChild) {
809
+ await send(chatId, t(l, "stopNoop"));
810
+ return;
811
+ }
812
+ const reset = text.includes("--reset");
813
+ stopping = true;
814
+ msgQueue.length = 0; // 대기 메시지도 취소
815
+ currentChild.kill();
816
+ if (reset) {
817
+ state.sessionId = prevSessionId;
818
+ saveState(state);
819
+ }
820
+ await send(chatId, t(l, reset ? "stopReset" : "stopOk"));
821
+ return;
822
+ }
786
823
 
787
824
  if (busy) {
788
- await send(chatId, t(l, "busy"));
825
+ msgQueue.push({ msg, receivedAt: Date.now() });
826
+ await send(chatId, t(l, "queued", msgQueue.length));
789
827
  return;
790
828
  }
791
829
  if (checkLocalLock()) {
@@ -796,7 +834,7 @@ async function handle(msg) {
796
834
  await tg("sendChatAction", { chat_id: chatId, action: "typing" });
797
835
  const started = Date.now();
798
836
  // 긴 작업 동안 타이핑 표시 유지
799
- const typing = setInterval(
837
+ currentTyping = setInterval(
800
838
  () =>
801
839
  tg("sendChatAction", { chat_id: chatId, action: "typing" }).catch(
802
840
  () => {},
@@ -815,7 +853,8 @@ async function handle(msg) {
815
853
  await send(chatId, t(l, "attachFail", e.message));
816
854
  }
817
855
  }
818
- const res = await runClaude(prompt, state.sessionId, { modelHint: true });
856
+ prevSessionId = state.sessionId; // /stop --reset 복원 대상 저장
857
+ const res = await runClaude(prompt, state.sessionId, { modelHint: true, trackChild: true });
819
858
  if (res.sessionId) {
820
859
  state.sessionId = res.sessionId;
821
860
  saveState(state);
@@ -824,15 +863,32 @@ async function handle(msg) {
824
863
  const footer = res.ok
825
864
  ? `\n\n— ${secs}s${res.cost ? ` · $${res.cost.toFixed(4)}` : ""}`
826
865
  : "";
827
- await send(chatId, (res.ok ? res.text : `⚠️ ${res.text}`) + footer);
866
+ if (!stopping) await send(chatId, (res.ok ? res.text : `⚠️ ${res.text}`) + footer);
828
867
  } catch (e) {
829
- await send(chatId, t(l, "botError", e.message));
868
+ if (!stopping) await send(chatId, t(l, "botError", e.message));
830
869
  } finally {
831
- clearInterval(typing);
870
+ clearInterval(currentTyping);
871
+ currentTyping = null;
872
+ stopping = false;
832
873
  busy = false;
874
+ if (msgQueue.length > 0) setImmediate(() => handle(drainQueue()));
833
875
  }
834
876
  }
835
877
 
878
+ // 큐 전체를 꺼내 하나의 메시지로 합침. 여러 개면 번호+경과시간 붙여 병합 → Claude가 맥락 일괄 파악.
879
+ function drainQueue() {
880
+ if (msgQueue.length === 1) return msgQueue.shift().msg;
881
+ const group = msgQueue.splice(0);
882
+ const merged = group
883
+ .map((item, i) => {
884
+ const text = item.msg.text || item.msg.caption || "";
885
+ const dt = Math.round((item.receivedAt - group[0].receivedAt) / 1000);
886
+ return i === 0 ? `[1] ${text}` : `[${i + 1}, +${dt}s] ${text}`;
887
+ })
888
+ .join("\n");
889
+ return { ...group[group.length - 1].msg, text: merged, caption: undefined };
890
+ }
891
+
836
892
  // ── 롱폴링 루프 ───────────────────────────────────────────────────────────
837
893
  async function main() {
838
894
  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.0",
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": {