claude-telegram-bot 0.3.18 → 0.3.23
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/bot.mjs +122 -5
- package/package.json +1 -1
package/bot.mjs
CHANGED
|
@@ -134,6 +134,7 @@ const STR = {
|
|
|
134
134
|
`${cfg.name || "Claude Code Telegram bot"}\n\n` +
|
|
135
135
|
"• Just send a message and Claude works in the project.\n" +
|
|
136
136
|
"• /new — reset conversation context (new session)\n" +
|
|
137
|
+
"• /compact — compress context to free up space (keeps the session)\n" +
|
|
137
138
|
"• /stop — stop the current task · /stop --reset to also roll back the session\n" +
|
|
138
139
|
"• /cron — list tasks · /cron add <natural language> to add · /cron rm <id> to remove\n" +
|
|
139
140
|
"• /remember <text> — save to persistent memory (survives /new)\n" +
|
|
@@ -145,6 +146,11 @@ const STR = {
|
|
|
145
146
|
"• /id — show this chat ID\n" +
|
|
146
147
|
`\nWorking dir: ${cfg.projectDir}\nPermission mode: ${cfg.permissionMode}`,
|
|
147
148
|
newSession: "🆕 Started a new conversation (previous context cleared).",
|
|
149
|
+
compactOk: "🗜️ Context compacted. The conversation continues with a summary.",
|
|
150
|
+
compactFail: (m) => `⚠️ Compact failed: ${m}`,
|
|
151
|
+
compactNoSession: "No active session to compact. Just send a message to start one.",
|
|
152
|
+
testFallbackDisabled: "⚠️ Ollama fallback is not enabled. Set `\"ollamaFallback\": true` in config.json.",
|
|
153
|
+
testFallbackFail: (m) => `⚠️ Ollama test failed: ${m}`,
|
|
148
154
|
busy: "⏳ A previous task is still running. Please try again when it finishes.",
|
|
149
155
|
queued: (n) => `⏳ Queued (#${n}). Will run when the current task finishes.`,
|
|
150
156
|
stopOk: "🛑 Task stopped.",
|
|
@@ -203,12 +209,14 @@ const STR = {
|
|
|
203
209
|
reserveRm: "🚫 Scheduled retry canceled.",
|
|
204
210
|
reserveNone: "No retry is scheduled.",
|
|
205
211
|
reserveNoLimit: "No recent usage limit error. Send a message first.",
|
|
212
|
+
contextTooLong: "⚠️ Prompt is too long. Use `/compact` to compress context, or `/new` to start fresh.",
|
|
206
213
|
},
|
|
207
214
|
ko: {
|
|
208
215
|
help: () =>
|
|
209
216
|
`${cfg.name || "Claude Code 텔레그램 봇"}\n\n` +
|
|
210
217
|
"• 그냥 메시지를 보내면 Claude가 프로젝트에서 작업합니다.\n" +
|
|
211
218
|
"• /new — 대화 맥락 초기화 (새 세션)\n" +
|
|
219
|
+
"• /compact — 컨텍스트 압축 (세션 유지, 공간 확보)\n" +
|
|
212
220
|
"• /stop — 진행 중인 작업 중단 · /stop --reset 으로 세션도 되돌리기\n" +
|
|
213
221
|
"• /cron — 예약 작업 보기 · /cron add <자연어>로 추가 · /cron rm <번호>로 삭제\n" +
|
|
214
222
|
"• /remember <내용> — 퍼시스턴트 메모리에 저장 (/new 로 초기화해도 유지)\n" +
|
|
@@ -277,6 +285,12 @@ const STR = {
|
|
|
277
285
|
reserveRm: "🚫 예약된 재시도를 취소했습니다.",
|
|
278
286
|
reserveNone: "예약된 재시도가 없습니다.",
|
|
279
287
|
reserveNoLimit: "최근 한도 초과 에러가 없습니다. 먼저 메시지를 보내주세요.",
|
|
288
|
+
compactOk: "🗜️ 컨텍스트를 압축했습니다. 대화가 요약본으로 이어집니다.",
|
|
289
|
+
compactFail: (m) => `⚠️ compact 실패: ${m}`,
|
|
290
|
+
compactNoSession: "압축할 활성 세션이 없습니다. 메시지를 보내 세션을 시작하세요.",
|
|
291
|
+
contextTooLong: "⚠️ 프롬프트가 너무 깁니다. `/compact` 로 컨텍스트를 압축하거나 `/new` 로 새 세션을 시작하세요.",
|
|
292
|
+
testFallbackDisabled: "⚠️ Ollama 폴백이 비활성화 상태입니다. config.json에 `\"ollamaFallback\": true` 를 추가하세요.",
|
|
293
|
+
testFallbackFail: (m) => `⚠️ Ollama 테스트 실패: ${m}`,
|
|
280
294
|
},
|
|
281
295
|
};
|
|
282
296
|
const t = (l, key, ...a) => {
|
|
@@ -291,6 +305,7 @@ const MODEL_SUGGESTIONS = ["fable", "opus", "sonnet", "haiku"];
|
|
|
291
305
|
const COMMANDS = {
|
|
292
306
|
en: [
|
|
293
307
|
{ command: "new", description: "Reset context (new session)" },
|
|
308
|
+
{ command: "compact", description: "Compress context to free up space (keeps session)" },
|
|
294
309
|
{ command: "stop", description: "Stop the current task (--reset to roll back session)" },
|
|
295
310
|
{ command: "remember", description: "Save to persistent memory (survives /new)" },
|
|
296
311
|
{ command: "memory", description: "View or clear persistent memory" },
|
|
@@ -304,6 +319,7 @@ const COMMANDS = {
|
|
|
304
319
|
],
|
|
305
320
|
ko: [
|
|
306
321
|
{ command: "new", description: "대화 맥락 초기화 (새 세션)" },
|
|
322
|
+
{ command: "compact", description: "컨텍스트 압축 (세션 유지, 공간 확보)" },
|
|
307
323
|
{ command: "stop", description: "작업 중단 (--reset 으로 세션 되돌리기)" },
|
|
308
324
|
{ command: "remember", description: "퍼시스턴트 메모리에 저장 (/new 후에도 유지)" },
|
|
309
325
|
{ command: "memory", description: "메모리 보기·삭제" },
|
|
@@ -524,6 +540,14 @@ function parseResetTime(raw) {
|
|
|
524
540
|
return null;
|
|
525
541
|
}
|
|
526
542
|
|
|
543
|
+
function isFallbackError(raw, code) {
|
|
544
|
+
const t = (raw || "").toLowerCase();
|
|
545
|
+
return t.includes("credit") || t.includes("balance") || t.includes("billing") || t.includes("payment")
|
|
546
|
+
|| t.includes("rate_limit") || t.includes("rate limit") || t.includes("too many requests") || code === 429
|
|
547
|
+
|| t.includes("usage limit") || t.includes("monthly limit")
|
|
548
|
+
|| t.includes("overloaded") || code === 529;
|
|
549
|
+
}
|
|
550
|
+
|
|
527
551
|
function classifyClaudeError(raw, code) {
|
|
528
552
|
const t = raw.toLowerCase();
|
|
529
553
|
if (t.includes("credit") || t.includes("balance") || t.includes("billing") || t.includes("payment"))
|
|
@@ -533,8 +557,8 @@ function classifyClaudeError(raw, code) {
|
|
|
533
557
|
return "⏱️ 요청이 너무 많습니다. 잠시 후 다시 시도해주세요.";
|
|
534
558
|
if (t.includes("overloaded") || code === 529)
|
|
535
559
|
return "🔄 Claude 서버가 일시적으로 과부하 상태입니다. 잠시 후 다시 시도해주세요.";
|
|
536
|
-
if (t.includes("context") && (t.includes("length") || t.includes("limit") || t.includes("window")))
|
|
537
|
-
return "
|
|
560
|
+
if (t.includes("prompt is too long") || (t.includes("context") && (t.includes("length") || t.includes("limit") || t.includes("window"))))
|
|
561
|
+
return "contextTooLong";
|
|
538
562
|
return `Execution error (exit ${code}):\n${raw}`;
|
|
539
563
|
}
|
|
540
564
|
|
|
@@ -591,15 +615,34 @@ function runClaude(prompt, sessionId, opts = {}) {
|
|
|
591
615
|
const rawErr = j.result ?? "";
|
|
592
616
|
const text = j.is_error ? classifyClaudeError(rawErr, code) : (rawErr || "(empty response)");
|
|
593
617
|
const resetAt = j.is_error ? parseResetTime(rawErr) : null;
|
|
594
|
-
|
|
618
|
+
const canFallback = j.is_error && isFallbackError(rawErr, code);
|
|
619
|
+
resolve({ ok: !j.is_error, text, sessionId: j.session_id, cost: j.total_cost_usd, resetAt, canFallback });
|
|
595
620
|
} catch {
|
|
596
621
|
const raw = (err || out || "no output").slice(0, 3500);
|
|
597
|
-
resolve({ ok: false, text: classifyClaudeError(raw, code), resetAt: parseResetTime(raw) });
|
|
622
|
+
resolve({ ok: false, text: classifyClaudeError(raw, code), resetAt: parseResetTime(raw), canFallback: isFallbackError(raw, code) });
|
|
598
623
|
}
|
|
599
624
|
});
|
|
600
625
|
});
|
|
601
626
|
}
|
|
602
627
|
|
|
628
|
+
// ── Ollama 폴백 실행 ──────────────────────────────────────────────────────
|
|
629
|
+
async function runOllama(prompt, lang = "en") {
|
|
630
|
+
const header = lang === "ko"
|
|
631
|
+
? "🌙 Claude가 잠시 쉬고 있어요. 제가 대신 도와드릴게요. (세션은 이어지지 않아요)\n\n"
|
|
632
|
+
: "🌙 Claude is resting right now. I'll help in the meantime. (Session won't continue)\n\n";
|
|
633
|
+
const model = cfg.ollamaModel || "phi3:mini";
|
|
634
|
+
const r = await fetch("http://localhost:11434/api/generate", {
|
|
635
|
+
method: "POST",
|
|
636
|
+
headers: { "Content-Type": "application/json" },
|
|
637
|
+
body: JSON.stringify({ model, prompt, stream: false }),
|
|
638
|
+
signal: AbortSignal.timeout(60_000),
|
|
639
|
+
});
|
|
640
|
+
if (!r.ok) return { ok: false, text: `Ollama HTTP ${r.status}` };
|
|
641
|
+
const j = await r.json();
|
|
642
|
+
const text = (j.response || "").trim();
|
|
643
|
+
return text ? { ok: true, text: header + text } : { ok: false, text: "no response" };
|
|
644
|
+
}
|
|
645
|
+
|
|
603
646
|
// ── 크론 스케줄러 ─────────────────────────────────────────────────────────
|
|
604
647
|
// 표준 cron 5필드 "분 시 일 월 요일"을 의존성 0 유지를 위해 최소 파서로 직접 구현.
|
|
605
648
|
// 지원: * · 목록(1,3,5) · 범위(1-5) · 스텝(*/15, 9-17/2). 요일 0·7 = 일요일.
|
|
@@ -834,6 +877,44 @@ async function downloadAttachment(att) {
|
|
|
834
877
|
return { dest, name };
|
|
835
878
|
}
|
|
836
879
|
|
|
880
|
+
// ── 텔레그램 메시지 메타데이터 추출 ──────────────────────────────────────
|
|
881
|
+
function buildMsgMeta(msg) {
|
|
882
|
+
const parts = [];
|
|
883
|
+
|
|
884
|
+
// 포워드 출처 (신규 API: forward_origin, 구버전 폴백: forward_from / forward_from_chat)
|
|
885
|
+
const fo = msg.forward_origin;
|
|
886
|
+
if (fo) {
|
|
887
|
+
if (fo.type === "user" && fo.sender_user)
|
|
888
|
+
parts.push(`[Forwarded from: ${fo.sender_user.first_name}${fo.sender_user.username ? ` (@${fo.sender_user.username})` : ""}]`);
|
|
889
|
+
else if (fo.type === "channel" && fo.chat)
|
|
890
|
+
parts.push(`[Forwarded from channel: ${fo.chat.title}${fo.chat.username ? ` (@${fo.chat.username})` : ""}]`);
|
|
891
|
+
else if (fo.type === "chat" && fo.sender_chat)
|
|
892
|
+
parts.push(`[Forwarded from: ${fo.sender_chat.title}]`);
|
|
893
|
+
else if (fo.type === "hidden_user" && fo.sender_user_name)
|
|
894
|
+
parts.push(`[Forwarded from: ${fo.sender_user_name} (hidden)]`);
|
|
895
|
+
else
|
|
896
|
+
parts.push(`[Forwarded message]`);
|
|
897
|
+
} else if (msg.forward_from) {
|
|
898
|
+
const u = msg.forward_from;
|
|
899
|
+
parts.push(`[Forwarded from: ${u.first_name}${u.username ? ` (@${u.username})` : ""}]`);
|
|
900
|
+
} else if (msg.forward_from_chat) {
|
|
901
|
+
const c = msg.forward_from_chat;
|
|
902
|
+
parts.push(`[Forwarded from: ${c.title}${c.username ? ` (@${c.username})` : ""}]`);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// 리플라이 컨텍스트
|
|
906
|
+
const reply = msg.reply_to_message;
|
|
907
|
+
if (reply) {
|
|
908
|
+
const replyText = (reply.text || reply.caption || "").trim().slice(0, 300);
|
|
909
|
+
const replyFrom = reply.from?.first_name || "unknown";
|
|
910
|
+
parts.push(replyText
|
|
911
|
+
? `[Replying to ${replyFrom}: "${replyText}${replyText.length >= 300 ? "…" : ""}"]`
|
|
912
|
+
: `[Replying to ${replyFrom}'s message]`);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return parts.join("\n");
|
|
916
|
+
}
|
|
917
|
+
|
|
837
918
|
// ── 메시지 처리 ───────────────────────────────────────────────────────────
|
|
838
919
|
let busy = false;
|
|
839
920
|
const msgQueue = []; // { msg, receivedAt } — busy 중 수신 메시지 대기열
|
|
@@ -932,6 +1013,32 @@ async function handle(msg) {
|
|
|
932
1013
|
});
|
|
933
1014
|
return;
|
|
934
1015
|
}
|
|
1016
|
+
if (text === "/compact") {
|
|
1017
|
+
if (!state.sessionId) { await send(chatId, t(l, "compactNoSession")); return; }
|
|
1018
|
+
try {
|
|
1019
|
+
const res = await runClaude("/compact", state.sessionId);
|
|
1020
|
+
if (res.ok !== false) {
|
|
1021
|
+
await send(chatId, t(l, "compactOk"));
|
|
1022
|
+
} else {
|
|
1023
|
+
await send(chatId, t(l, "compactFail", res.text));
|
|
1024
|
+
}
|
|
1025
|
+
} catch (e) {
|
|
1026
|
+
await send(chatId, t(l, "compactFail", e.message));
|
|
1027
|
+
}
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (text === "/testfallback") {
|
|
1031
|
+
if (!cfg.ollamaFallback) { await send(chatId, t(l, "testFallbackDisabled")); return; }
|
|
1032
|
+
await send(chatId, "🧪 Ollama 연결 테스트 중…");
|
|
1033
|
+
try {
|
|
1034
|
+
const res = await runOllama("Reply with exactly one sentence: Ollama fallback is working.", l);
|
|
1035
|
+
if (res.ok) await send(chatId, res.text);
|
|
1036
|
+
else await send(chatId, t(l, "testFallbackFail", res.text));
|
|
1037
|
+
} catch (e) {
|
|
1038
|
+
await send(chatId, t(l, "testFallbackFail", e.message));
|
|
1039
|
+
}
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
935
1042
|
if (text === "/new") {
|
|
936
1043
|
state.sessionId = undefined;
|
|
937
1044
|
saveState(state);
|
|
@@ -1043,6 +1150,8 @@ async function handle(msg) {
|
|
|
1043
1150
|
await send(chatId, t(l, "attachFail", e.message));
|
|
1044
1151
|
}
|
|
1045
1152
|
}
|
|
1153
|
+
const meta = buildMsgMeta(msg);
|
|
1154
|
+
if (meta) prompt = prompt ? `${meta}\n\n${prompt}` : meta;
|
|
1046
1155
|
prevSessionId = state.sessionId; // /stop --reset 복원 대상 저장
|
|
1047
1156
|
const res = await runClaude(prompt, state.sessionId, { modelHint: true, trackChild: true, injectMemory: true });
|
|
1048
1157
|
if (res.sessionId) {
|
|
@@ -1054,7 +1163,15 @@ async function handle(msg) {
|
|
|
1054
1163
|
// 레이트 리밋이고 리셋 시간을 알면 /reserve 힌트 추가
|
|
1055
1164
|
const hint = res.resetAt ? t(l, "reserveHint") : "";
|
|
1056
1165
|
rateLimitState = res.resetAt ? { prompt, resetAt: res.resetAt } : null;
|
|
1057
|
-
|
|
1166
|
+
// Ollama 폴백: 레이트리밋·크레딧 에러이고 ollamaFallback 켜져 있으면 Ollama로 재시도
|
|
1167
|
+
if (cfg.ollamaFallback && res.canFallback && !stopping) {
|
|
1168
|
+
try {
|
|
1169
|
+
const oRes = await runOllama(prompt, l);
|
|
1170
|
+
if (oRes.ok) { await send(chatId, oRes.text); return; }
|
|
1171
|
+
} catch {}
|
|
1172
|
+
}
|
|
1173
|
+
const errMsg = res.text === "contextTooLong" ? t(l, "contextTooLong") : `⚠️ ${res.text}${hint}`;
|
|
1174
|
+
if (!stopping) await send(chatId, errMsg);
|
|
1058
1175
|
} else {
|
|
1059
1176
|
rateLimitState = null;
|
|
1060
1177
|
const footer = `\n\n— ${secs}s${res.cost ? ` · $${res.cost.toFixed(4)}` : ""}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-telegram-bot",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
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": {
|