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.
- package/README.ko.md +14 -8
- package/README.md +23 -15
- package/bot.mjs +70 -13
- 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
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
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 `
|
|
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
|
|
172
|
-
> then `node bot.mjs [
|
|
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
|
|
185
|
+
cp config.example.json mybot.json
|
|
180
186
|
```
|
|
181
187
|
|
|
182
|
-
Edit `
|
|
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
|
-
(`
|
|
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
|
|
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.
|
|
74
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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": {
|