claude-telegram-bot 0.2.3 → 0.2.5
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 +3 -3
- package/README.md +6 -4
- package/bot.mjs +76 -15
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -92,7 +92,7 @@ claude-telegram-bot ~/botconfigs/myproj/config.json
|
|
|
92
92
|
| `projectDir` | Claude가 작업할 폴더의 절대경로 |
|
|
93
93
|
| `claudeBin` | `which claude` 결과 (절대경로 권장) |
|
|
94
94
|
| `permissionMode` | `plan`(읽기·계획만) / `acceptEdits`(편집 자동 승인) / `bypassPermissions`(쉘 포함 전부 자동) |
|
|
95
|
-
| `model` | 비우면 기본 모델. `opus`, `sonnet`
|
|
95
|
+
| `model` | 비우면 기본 모델. `opus`, `sonnet` 등. 런타임에 `/model`로 전환 가능(state에 저장) |
|
|
96
96
|
| `lang` | (선택) UI 언어. 비우면 사용자별 자동 판별(기본 영어, 텔레그램이 한국어면 한국어). `"en"`/`"ko"`로 고정 가능 |
|
|
97
97
|
| `name` | (선택) `/help`에 표시되는 봇 이름. 여러 봇 구분용 |
|
|
98
98
|
| `persona` | (선택) 역할 시스템 프롬프트. 페르소나 봇 정의용 |
|
|
@@ -100,7 +100,7 @@ claude-telegram-bot ~/botconfigs/myproj/config.json
|
|
|
100
100
|
| `env` | (선택) `claude` 프로세스에 넘길 환경 변수 |
|
|
101
101
|
| `schedule` | (선택) 정해진 시각에 프롬프트를 실행하는 cron 작업 — [예약 작업](#예약-작업-cron) 참고 |
|
|
102
102
|
|
|
103
|
-
`state
|
|
103
|
+
`state`와 첨부 파일은 config 파일 옆 **`.claude-bot/` 숨김 폴더**에 저장됩니다(프로젝트 격리). 구버전에서 올리면 첫 시작 때 기존 `state.json`·`attachments/`를 `.claude-bot/`로 **자동 이동**합니다(무손실). 로그는 launchd plist가 가리키는 위치 그대로입니다.
|
|
104
104
|
|
|
105
105
|
## 첫 실행
|
|
106
106
|
|
|
@@ -110,7 +110,7 @@ claude-telegram-bot ~/botconfigs/myproj/config.json
|
|
|
110
110
|
- `테스트 돌려보고 통과하면 커밋하고 push 해줘`
|
|
111
111
|
- `api.ts 에 에러 핸들링 추가해줘`
|
|
112
112
|
|
|
113
|
-
명령어: `/new`(맥락 초기화) · `/cron`(예약 작업 보기·추가·삭제) · `/restart`(문법 검사 후 재시작) · `/status`(봇 상태·버전) · `/id`(채팅 ID 확인) · `/help`(도움말)
|
|
113
|
+
명령어: `/new`(맥락 초기화) · `/cron`(예약 작업 보기·추가·삭제) · `/restart`(문법 검사 후 재시작) · `/status`(봇 상태·버전) · `/model`(모델 보기·전환) · `/id`(채팅 ID 확인) · `/help`(도움말)
|
|
114
114
|
|
|
115
115
|
> **`/restart`** 는 먼저 `bot.mjs` 에 `node --check` 를 돌려 **문법 오류가 있으면 재시작을 취소**합니다(잘못된 수정이 봇을 크래시 루프에 빠뜨리는 것 방지). 통과하면 프로세스를 종료하고, 다시 띄우는 건 프로세스 관리자에게 맡깁니다. [launchd 설정](#상시-실행-launchd)(`KeepAlive`)이면 바로 동작하고, 관리자 없이 `node bot.mjs` 로만 돌리면 그냥 멈춥니다. 재시작 후 대화 세션은 `state.json` 의 ID로 이어집니다.
|
|
116
116
|
|
package/README.md
CHANGED
|
@@ -154,7 +154,7 @@ auth layer.)
|
|
|
154
154
|
- `run the solver tests and commit + push if they pass`
|
|
155
155
|
- `add an edge case to solve-2nd-floor-edges.ts`
|
|
156
156
|
|
|
157
|
-
Commands: `/new` (reset context / new session) · `/cron` (list / add / remove scheduled tasks) · `/restart` (syntax-check & restart the bot) · `/status` (bot status & version) · `/id` (show chat ID) · `/help`.
|
|
157
|
+
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`.
|
|
158
158
|
|
|
159
159
|
> **`/restart`** runs `node --check` on `bot.mjs` first and **aborts the restart if it has a syntax
|
|
160
160
|
> error** (so a bad edit can't crash-loop the bot), then exits — relying on a process supervisor
|
|
@@ -184,7 +184,7 @@ Edit `config.json`:
|
|
|
184
184
|
| `projectDir` | Absolute path to the working folder Claude runs in |
|
|
185
185
|
| `claudeBin` | Output of `which claude` (absolute path recommended) |
|
|
186
186
|
| `permissionMode` | `plan` / `acceptEdits` / `bypassPermissions` — see [Security](#security) |
|
|
187
|
-
| `model` | Empty = default. Or `opus` / `sonnet`, etc. |
|
|
187
|
+
| `model` | Empty = default. Or `opus` / `sonnet`, etc. Override at runtime with `/model` (persists in state). |
|
|
188
188
|
| `lang` | (optional) UI language. Empty = auto-detect per user (English default, Korean for Korean Telegram clients). Force with `"en"` / `"ko"`. |
|
|
189
189
|
| `name` | (optional) Bot name shown in `/help` — handy for telling multiple bots apart |
|
|
190
190
|
| `persona` | (optional) Role system prompt — defines a persona (developer/planner/…). See below |
|
|
@@ -192,8 +192,10 @@ Edit `config.json`:
|
|
|
192
192
|
| `env` | (optional) Extra environment variables passed to the `claude` process |
|
|
193
193
|
| `schedule` | (optional) Cron jobs that run a prompt on a timer — see [Scheduled tasks](#scheduled-tasks-cron) |
|
|
194
194
|
|
|
195
|
-
State
|
|
196
|
-
projects stay isolated.
|
|
195
|
+
State and downloaded attachments live in a hidden **`.claude-bot/`** folder next to the config
|
|
196
|
+
file, so projects stay isolated. Upgrading from an older version **auto-moves** an existing
|
|
197
|
+
`state.json` / `attachments/` into `.claude-bot/` on first start (no data loss). Logs stay wherever
|
|
198
|
+
your launchd plist points them.
|
|
197
199
|
|
|
198
200
|
### Usage details
|
|
199
201
|
|
package/bot.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// 자동 판별하고, cfg.lang 을 주면 그 언어로 고정함. 콘솔/CLI 출력은 영어 단일.
|
|
15
15
|
|
|
16
16
|
import { basename, dirname, join } from "node:path";
|
|
17
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
18
18
|
|
|
19
19
|
import dns from "node:dns";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
@@ -73,13 +73,33 @@ Requires: the claude CLI installed and authenticated on the host.`);
|
|
|
73
73
|
// isolated. Falls back to ./config.json for the single-project setup.
|
|
74
74
|
const CONFIG_PATH = process.argv[2] || process.env.BOT_CONFIG || join(HERE, "config.json");
|
|
75
75
|
const DATA_DIR = dirname(CONFIG_PATH);
|
|
76
|
-
// state
|
|
77
|
-
// config
|
|
76
|
+
// 데이터(state·attachments)는 config 폴더 아래 숨김 폴더 .claude-bot/ 에 모은다.
|
|
77
|
+
// state 파일명은 config 이름에서 파생 → 여러 페르소나 config 가 한 .claude-bot/ 를 공유해도 안 섞임
|
|
78
|
+
// (config.json → state.json, 그 외 foo.json → foo.state.json).
|
|
78
79
|
const stateBase = basename(CONFIG_PATH, ".json");
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
80
|
+
const stateFile = stateBase === "config" ? "state.json" : `${stateBase}.state.json`;
|
|
81
|
+
const BOT_DIR = join(DATA_DIR, ".claude-bot");
|
|
82
|
+
const STATE_PATH = join(BOT_DIR, stateFile);
|
|
83
|
+
const ATTACH_DIR = join(BOT_DIR, "attachments");
|
|
84
|
+
const LEGACY_STATE_PATH = join(DATA_DIR, stateFile); // 구버전(루트 직하) 호환
|
|
85
|
+
const LEGACY_ATTACH_DIR = join(DATA_DIR, "attachments");
|
|
86
|
+
|
|
87
|
+
// 구버전에서 올라온 경우, 루트 직하 데이터를 .claude-bot/ 로 1회 이동(무손실, 실패 시 기존 경로 폴백).
|
|
88
|
+
function migrateData() {
|
|
89
|
+
try {
|
|
90
|
+
mkdirSync(BOT_DIR, { recursive: true });
|
|
91
|
+
if (!existsSync(STATE_PATH) && existsSync(LEGACY_STATE_PATH)) {
|
|
92
|
+
renameSync(LEGACY_STATE_PATH, STATE_PATH);
|
|
93
|
+
console.log(`Migrated state → ${STATE_PATH}`);
|
|
94
|
+
}
|
|
95
|
+
if (!existsSync(ATTACH_DIR) && existsSync(LEGACY_ATTACH_DIR)) {
|
|
96
|
+
renameSync(LEGACY_ATTACH_DIR, ATTACH_DIR);
|
|
97
|
+
console.log(`Migrated attachments → ${ATTACH_DIR}`);
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error("Data migration skipped:", e.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
83
103
|
|
|
84
104
|
if (!existsSync(CONFIG_PATH)) {
|
|
85
105
|
console.error(
|
|
@@ -109,6 +129,7 @@ const STR = {
|
|
|
109
129
|
"• /cron — list tasks · /cron add <natural language> to add · /cron rm <id> to remove\n" +
|
|
110
130
|
"• /restart — restart the bot (after a syntax check)\n" +
|
|
111
131
|
"• /status — bot status & version\n" +
|
|
132
|
+
"• /model — view / switch the model\n" +
|
|
112
133
|
"• /id — show this chat ID\n" +
|
|
113
134
|
`\nWorking dir: ${cfg.projectDir}\nPermission mode: ${cfg.permissionMode}`,
|
|
114
135
|
newSession: "🆕 Started a new conversation (previous context cleared).",
|
|
@@ -148,6 +169,12 @@ const STR = {
|
|
|
148
169
|
`• Scheduled jobs: ${i.jobs}\n` +
|
|
149
170
|
`• Project: ${i.projectDir}\n` +
|
|
150
171
|
`• Permission: ${i.permissionMode}`,
|
|
172
|
+
modelStatus: (cur, list) =>
|
|
173
|
+
`🧠 Model: ${cur}\n` +
|
|
174
|
+
`Switch: ${list.map((x) => `/model ${x}`).join(" · ")} (or a full model id)\n` +
|
|
175
|
+
`/model default — clear the override`,
|
|
176
|
+
modelSet: (m) => `🧠 Model set to: ${m}`,
|
|
177
|
+
modelReset: (def) => `🧠 Model reset to default (${def}).`,
|
|
151
178
|
},
|
|
152
179
|
ko: {
|
|
153
180
|
help: () =>
|
|
@@ -157,6 +184,7 @@ const STR = {
|
|
|
157
184
|
"• /cron — 예약 작업 보기 · /cron add <자연어>로 추가 · /cron rm <번호>로 삭제\n" +
|
|
158
185
|
"• /restart — 봇 재시작 (문법 검사 후 안전하게)\n" +
|
|
159
186
|
"• /status — 봇 상태·버전 보기\n" +
|
|
187
|
+
"• /model — 모델 보기·전환\n" +
|
|
160
188
|
"• /id — 이 채팅 ID 확인\n" +
|
|
161
189
|
`\n작업 폴더: ${cfg.projectDir}\n권한 모드: ${cfg.permissionMode}`,
|
|
162
190
|
newSession: "🆕 새 대화를 시작합니다 (이전 맥락 초기화).",
|
|
@@ -195,6 +223,12 @@ const STR = {
|
|
|
195
223
|
`• 예약 작업: ${i.jobs}개\n` +
|
|
196
224
|
`• 작업 폴더: ${i.projectDir}\n` +
|
|
197
225
|
`• 권한 모드: ${i.permissionMode}`,
|
|
226
|
+
modelStatus: (cur, list) =>
|
|
227
|
+
`🧠 현재 모델: ${cur}\n` +
|
|
228
|
+
`전환: ${list.map((x) => `/model ${x}`).join(" · ")} (또는 전체 모델 ID)\n` +
|
|
229
|
+
`/model default — 오버라이드 해제`,
|
|
230
|
+
modelSet: (m) => `🧠 모델을 ${m} (으)로 설정했습니다.`,
|
|
231
|
+
modelReset: (def) => `🧠 모델을 기본값(${def})으로 되돌렸습니다.`,
|
|
198
232
|
},
|
|
199
233
|
};
|
|
200
234
|
const t = (l, key, ...a) => {
|
|
@@ -202,6 +236,9 @@ const t = (l, key, ...a) => {
|
|
|
202
236
|
return typeof v === "function" ? v(...a) : v;
|
|
203
237
|
};
|
|
204
238
|
|
|
239
|
+
// /model 에서 보여줄 추천 별칭(claude CLI 가 별칭·전체 모델 ID 모두 허용).
|
|
240
|
+
const MODEL_SUGGESTIONS = ["opus", "sonnet", "haiku"];
|
|
241
|
+
|
|
205
242
|
// /(슬래시) 자동완성 메뉴용 명령 목록 (언어별). setMyCommands 로 등록.
|
|
206
243
|
const COMMANDS = {
|
|
207
244
|
en: [
|
|
@@ -209,6 +246,7 @@ const COMMANDS = {
|
|
|
209
246
|
{ command: "cron", description: "List / add / remove scheduled tasks" },
|
|
210
247
|
{ command: "restart", description: "Restart the bot (after syntax check)" },
|
|
211
248
|
{ command: "status", description: "Bot status / version" },
|
|
249
|
+
{ command: "model", description: "View / switch the model" },
|
|
212
250
|
{ command: "id", description: "Show this chat ID" },
|
|
213
251
|
{ command: "help", description: "Help" },
|
|
214
252
|
],
|
|
@@ -217,6 +255,7 @@ const COMMANDS = {
|
|
|
217
255
|
{ command: "cron", description: "예약 작업 보기·추가·삭제" },
|
|
218
256
|
{ command: "restart", description: "봇 재시작 (문법 검사 후)" },
|
|
219
257
|
{ command: "status", description: "봇 상태·버전 보기" },
|
|
258
|
+
{ command: "model", description: "모델 보기·전환" },
|
|
220
259
|
{ command: "id", description: "이 채팅 ID 확인" },
|
|
221
260
|
{ command: "help", description: "도움말" },
|
|
222
261
|
],
|
|
@@ -224,11 +263,13 @@ const COMMANDS = {
|
|
|
224
263
|
|
|
225
264
|
// ── 상태 (세션 이어가기용) ────────────────────────────────────────────────
|
|
226
265
|
function loadState() {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
266
|
+
// 새 경로(.claude-bot/) 우선, 없으면 구버전 루트 경로로 폴백(이주 실패 시 안전망).
|
|
267
|
+
for (const p of [STATE_PATH, LEGACY_STATE_PATH]) {
|
|
268
|
+
try {
|
|
269
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
270
|
+
} catch {}
|
|
231
271
|
}
|
|
272
|
+
return {};
|
|
232
273
|
}
|
|
233
274
|
function saveState(s) {
|
|
234
275
|
try {
|
|
@@ -237,7 +278,8 @@ function saveState(s) {
|
|
|
237
278
|
console.error("Failed to save state", e);
|
|
238
279
|
}
|
|
239
280
|
}
|
|
240
|
-
|
|
281
|
+
migrateData(); // 루트 직하 → .claude-bot/ 1회 이주(있으면) 후 state 로드
|
|
282
|
+
let state = loadState(); // { sessionId?, cron?: [{ id, cron, prompt, label? }], restartNotify?, model? }
|
|
241
283
|
|
|
242
284
|
// ── 텔레그램 헬퍼 ─────────────────────────────────────────────────────────
|
|
243
285
|
async function tg(method, body) {
|
|
@@ -353,7 +395,8 @@ function runClaude(prompt, sessionId) {
|
|
|
353
395
|
// 페르소나(cfg.persona) + 간결 지침을 함께 주입 → 멀티 봇(역할별) 운영용
|
|
354
396
|
const appendSys = [cfg.persona, brevity].filter(Boolean).join("\n\n");
|
|
355
397
|
if (appendSys) args.push("--append-system-prompt", appendSys);
|
|
356
|
-
|
|
398
|
+
const model = state.model || cfg.model; // /model 로 바꾸면 state.model 우선
|
|
399
|
+
if (model) args.push("--model", model);
|
|
357
400
|
if (sessionId) args.push("--resume", sessionId);
|
|
358
401
|
|
|
359
402
|
const child = spawn(cfg.claudeBin || "claude", args, {
|
|
@@ -616,7 +659,7 @@ async function downloadAttachment(att) {
|
|
|
616
659
|
const r = await fetch(`https://api.telegram.org/file/bot${cfg.token}/${filePath}`);
|
|
617
660
|
if (!r.ok) throw new Error(`download failed ${r.status}`);
|
|
618
661
|
const buf = Buffer.from(await r.arrayBuffer());
|
|
619
|
-
const dir =
|
|
662
|
+
const dir = ATTACH_DIR;
|
|
620
663
|
mkdirSync(dir, { recursive: true });
|
|
621
664
|
const ext = filePath.includes(".") ? filePath.slice(filePath.lastIndexOf(".")) : "";
|
|
622
665
|
const name = att.name || `tg-${att.fileId.slice(-10)}${ext}`;
|
|
@@ -661,7 +704,7 @@ async function handle(msg) {
|
|
|
661
704
|
t(l, "status", {
|
|
662
705
|
version: VERSION,
|
|
663
706
|
name: cfg.name || "claude-telegram-bot",
|
|
664
|
-
model: cfg.model || (l === "ko" ? "(기본값)" : "(default)"),
|
|
707
|
+
model: state.model || cfg.model || (l === "ko" ? "(기본값)" : "(default)"),
|
|
665
708
|
hasSession: Boolean(state.sessionId),
|
|
666
709
|
jobs: schedule.length,
|
|
667
710
|
projectDir: cfg.projectDir,
|
|
@@ -670,6 +713,24 @@ async function handle(msg) {
|
|
|
670
713
|
);
|
|
671
714
|
return;
|
|
672
715
|
}
|
|
716
|
+
if (text === "/model" || text.startsWith("/model ")) {
|
|
717
|
+
const arg = text.slice(6).trim();
|
|
718
|
+
if (!arg) {
|
|
719
|
+
const cur = state.model || cfg.model || (l === "ko" ? "(기본값)" : "(default)");
|
|
720
|
+
await send(chatId, t(l, "modelStatus", cur, MODEL_SUGGESTIONS));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (arg === "default" || arg === "reset") {
|
|
724
|
+
state.model = undefined;
|
|
725
|
+
saveState(state);
|
|
726
|
+
await send(chatId, t(l, "modelReset", cfg.model || (l === "ko" ? "기본값" : "default")));
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
state.model = arg;
|
|
730
|
+
saveState(state);
|
|
731
|
+
await send(chatId, t(l, "modelSet", arg));
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
673
734
|
if (text === "/cron" || text.startsWith("/cron ")) {
|
|
674
735
|
await handleCron(chatId, text.slice(5).trim(), l);
|
|
675
736
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-telegram-bot",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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": {
|