claude-telegram-bot 0.3.24 → 0.3.28
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 +82 -46
- package/package.json +1 -1
package/bot.mjs
CHANGED
|
@@ -135,6 +135,7 @@ const STR = {
|
|
|
135
135
|
"• Just send a message and Claude works in the project.\n" +
|
|
136
136
|
"• /new — reset conversation context (new session)\n" +
|
|
137
137
|
"• /compact — compress context to free up space (keeps the session)\n" +
|
|
138
|
+
"• /ollama — toggle Ollama chat mode (bypass Claude, use local LLM)\n" +
|
|
138
139
|
"• /stop — stop the current task · /stop --reset to also roll back the session\n" +
|
|
139
140
|
"• /cron — list tasks · /cron add <natural language> to add · /cron rm <id> to remove\n" +
|
|
140
141
|
"• /remember <text> — save to persistent memory (survives /new)\n" +
|
|
@@ -151,6 +152,8 @@ const STR = {
|
|
|
151
152
|
compactNoSession: "No active session to compact. Just send a message to start one.",
|
|
152
153
|
testFallbackDisabled: "⚠️ Ollama fallback is not enabled. Set `\"ollamaFallback\": true` in config.json.",
|
|
153
154
|
testFallbackFail: (m) => `⚠️ Ollama test failed: ${m}`,
|
|
155
|
+
ollamaOn: "🌙 Ollama mode on. Messages will now go to Ollama. Your Claude session is preserved.",
|
|
156
|
+
ollamaOff: "✅ Ollama mode off. Back to Claude.",
|
|
154
157
|
busy: "⏳ A previous task is still running. Please try again when it finishes.",
|
|
155
158
|
queued: (n) => `⏳ Queued (#${n}). Will run when the current task finishes.`,
|
|
156
159
|
stopOk: "🛑 Task stopped.",
|
|
@@ -204,11 +207,11 @@ const STR = {
|
|
|
204
207
|
remembered: "💾 Saved to memory.",
|
|
205
208
|
rememberUsage: "Usage: /remember <text to remember>",
|
|
206
209
|
memoryUsage: "Usage: /memory · /memory clear",
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
rateLimitQueued: (n, time) => `⏳ Queued (#${n}). Will retry at ${time}. /reserve rm to cancel.`,
|
|
211
|
+
reserveStatus: (n, time) => `⏳ ${n} message(s) queued. Retrying at ${time}. /reserve rm to cancel.`,
|
|
212
|
+
reserveAuto: (time) => `⏰ Auto-retry scheduled for ${time}. Cancel with /reserve rm.`,
|
|
213
|
+
reserveRm: "🚫 Queue cleared. No retry scheduled.",
|
|
210
214
|
reserveNone: "No retry is scheduled.",
|
|
211
|
-
reserveNoLimit: "No recent usage limit error. Send a message first.",
|
|
212
215
|
contextTooLong: "⚠️ Prompt is too long. Use `/compact` to compress context, or `/new` to start fresh.",
|
|
213
216
|
},
|
|
214
217
|
ko: {
|
|
@@ -217,6 +220,7 @@ const STR = {
|
|
|
217
220
|
"• 그냥 메시지를 보내면 Claude가 프로젝트에서 작업합니다.\n" +
|
|
218
221
|
"• /new — 대화 맥락 초기화 (새 세션)\n" +
|
|
219
222
|
"• /compact — 컨텍스트 압축 (세션 유지, 공간 확보)\n" +
|
|
223
|
+
"• /ollama — Ollama 채팅 모드 토글 (Claude 우회, 로컬 LLM 사용)\n" +
|
|
220
224
|
"• /stop — 진행 중인 작업 중단 · /stop --reset 으로 세션도 되돌리기\n" +
|
|
221
225
|
"• /cron — 예약 작업 보기 · /cron add <자연어>로 추가 · /cron rm <번호>로 삭제\n" +
|
|
222
226
|
"• /remember <내용> — 퍼시스턴트 메모리에 저장 (/new 로 초기화해도 유지)\n" +
|
|
@@ -280,17 +284,19 @@ const STR = {
|
|
|
280
284
|
remembered: "💾 메모리에 저장했습니다.",
|
|
281
285
|
rememberUsage: "사용법: /remember <기억할 내용>",
|
|
282
286
|
memoryUsage: "사용법: /memory · /memory clear",
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
rateLimitQueued: (n, time) => `⏳ 대기열에 추가됨 (${n}번째). ${time}에 자동 재시도. 취소: /reserve rm`,
|
|
288
|
+
reserveStatus: (n, time) => `⏳ 대기 중인 메시지 ${n}개. ${time}에 재시도 예약됨. 취소: /reserve rm`,
|
|
289
|
+
reserveAuto: (time) => `⏰ ${time}에 자동 재시도 예약됨. 취소: /reserve rm`,
|
|
290
|
+
reserveRm: "🚫 대기열을 비웠습니다. 예약 취소됨.",
|
|
286
291
|
reserveNone: "예약된 재시도가 없습니다.",
|
|
287
|
-
reserveNoLimit: "최근 한도 초과 에러가 없습니다. 먼저 메시지를 보내주세요.",
|
|
288
292
|
compactOk: "🗜️ 컨텍스트를 압축했습니다. 대화가 요약본으로 이어집니다.",
|
|
289
293
|
compactFail: (m) => `⚠️ compact 실패: ${m}`,
|
|
290
294
|
compactNoSession: "압축할 활성 세션이 없습니다. 메시지를 보내 세션을 시작하세요.",
|
|
291
295
|
contextTooLong: "⚠️ 프롬프트가 너무 깁니다. `/compact` 로 컨텍스트를 압축하거나 `/new` 로 새 세션을 시작하세요.",
|
|
292
296
|
testFallbackDisabled: "⚠️ Ollama 폴백이 비활성화 상태입니다. config.json에 `\"ollamaFallback\": true` 를 추가하세요.",
|
|
293
297
|
testFallbackFail: (m) => `⚠️ Ollama 테스트 실패: ${m}`,
|
|
298
|
+
ollamaOn: "🌙 Ollama 모드 켜짐. 이제 메시지는 Ollama로 처리됩니다. Claude 세션은 유지됩니다.",
|
|
299
|
+
ollamaOff: "✅ Ollama 모드 꺼짐. 다시 Claude로 처리합니다.",
|
|
294
300
|
},
|
|
295
301
|
};
|
|
296
302
|
const t = (l, key, ...a) => {
|
|
@@ -306,6 +312,7 @@ const COMMANDS = {
|
|
|
306
312
|
en: [
|
|
307
313
|
{ command: "new", description: "Reset context (new session)" },
|
|
308
314
|
{ command: "compact", description: "Compress context to free up space (keeps session)" },
|
|
315
|
+
{ command: "ollama", description: "Toggle Ollama chat mode (bypass Claude, use local LLM)" },
|
|
309
316
|
{ command: "stop", description: "Stop the current task (--reset to roll back session)" },
|
|
310
317
|
{ command: "remember", description: "Save to persistent memory (survives /new)" },
|
|
311
318
|
{ command: "memory", description: "View or clear persistent memory" },
|
|
@@ -320,6 +327,7 @@ const COMMANDS = {
|
|
|
320
327
|
ko: [
|
|
321
328
|
{ command: "new", description: "대화 맥락 초기화 (새 세션)" },
|
|
322
329
|
{ command: "compact", description: "컨텍스트 압축 (세션 유지, 공간 확보)" },
|
|
330
|
+
{ command: "ollama", description: "Ollama 채팅 모드 토글 (Claude 우회, 로컬 LLM)" },
|
|
323
331
|
{ command: "stop", description: "작업 중단 (--reset 으로 세션 되돌리기)" },
|
|
324
332
|
{ command: "remember", description: "퍼시스턴트 메모리에 저장 (/new 후에도 유지)" },
|
|
325
333
|
{ command: "memory", description: "메모리 보기·삭제" },
|
|
@@ -527,8 +535,8 @@ function parseResetTime(raw) {
|
|
|
527
535
|
if (inMin) return new Date(Date.now() + parseInt(inMin[1]) * 60000);
|
|
528
536
|
const inHour = raw.match(/in (\d+)\s*hour/i);
|
|
529
537
|
if (inHour) return new Date(Date.now() + parseInt(inHour[1]) * 3600000);
|
|
530
|
-
// "resets at HH:MM"
|
|
531
|
-
const atTime = raw.match(/(?:resets?|
|
|
538
|
+
// "resets at HH:MM" / "resets HH:MM" / "available at HH:MM"
|
|
539
|
+
const atTime = raw.match(/(?:resets?|available|retry)(?:\s+at)?\s+(\d{1,2}):(\d{2})\s*(AM|PM)?/i);
|
|
532
540
|
if (atTime) {
|
|
533
541
|
let h = parseInt(atTime[1]);
|
|
534
542
|
const m = parseInt(atTime[2]);
|
|
@@ -544,7 +552,7 @@ function isFallbackError(raw, code) {
|
|
|
544
552
|
const t = (raw || "").toLowerCase();
|
|
545
553
|
return t.includes("credit") || t.includes("balance") || t.includes("billing") || t.includes("payment")
|
|
546
554
|
|| t.includes("rate_limit") || t.includes("rate limit") || t.includes("too many requests") || code === 429
|
|
547
|
-
|| t.includes("usage limit") || t.includes("monthly limit")
|
|
555
|
+
|| t.includes("usage limit") || t.includes("monthly limit") || t.includes("session limit")
|
|
548
556
|
|| t.includes("overloaded") || code === 529;
|
|
549
557
|
}
|
|
550
558
|
|
|
@@ -553,7 +561,7 @@ function classifyClaudeError(raw, code) {
|
|
|
553
561
|
if (t.includes("credit") || t.includes("balance") || t.includes("billing") || t.includes("payment"))
|
|
554
562
|
return "💳 API 크레딧이 부족합니다. console.anthropic.com 에서 충전해주세요.";
|
|
555
563
|
if (t.includes("rate_limit") || t.includes("rate limit") || t.includes("too many requests") || code === 429
|
|
556
|
-
|| t.includes("usage limit") || t.includes("monthly limit"))
|
|
564
|
+
|| t.includes("usage limit") || t.includes("monthly limit") || t.includes("session limit"))
|
|
557
565
|
return "⏱️ 요청이 너무 많습니다. 잠시 후 다시 시도해주세요.";
|
|
558
566
|
if (t.includes("overloaded") || code === 529)
|
|
559
567
|
return "🔄 Claude 서버가 일시적으로 과부하 상태입니다. 잠시 후 다시 시도해주세요.";
|
|
@@ -566,8 +574,7 @@ function classifyClaudeError(raw, code) {
|
|
|
566
574
|
function runClaude(prompt, sessionId, opts = {}) {
|
|
567
575
|
return new Promise((resolve) => {
|
|
568
576
|
const args = [
|
|
569
|
-
|
|
570
|
-
prompt,
|
|
577
|
+
`--print=${prompt}`,
|
|
571
578
|
"--output-format",
|
|
572
579
|
"json",
|
|
573
580
|
"--permission-mode",
|
|
@@ -626,20 +633,25 @@ function runClaude(prompt, sessionId, opts = {}) {
|
|
|
626
633
|
}
|
|
627
634
|
|
|
628
635
|
// ── Ollama 폴백 실행 ──────────────────────────────────────────────────────
|
|
629
|
-
async function runOllama(prompt, lang = "en") {
|
|
630
|
-
const header = lang === "ko"
|
|
636
|
+
async function runOllama(prompt, lang = "en", opts = {}) {
|
|
637
|
+
const header = opts.noHeader ? "" : (lang === "ko"
|
|
631
638
|
? "🌙 Claude가 잠시 쉬고 있어요. 제가 대신 도와드릴게요. (세션은 이어지지 않아요)\n\n"
|
|
632
|
-
: "🌙 Claude is resting right now. I'll help in the meantime. (Session won't continue)\n\n";
|
|
639
|
+
: "🌙 Claude is resting right now. I'll help in the meantime. (Session won't continue)\n\n");
|
|
633
640
|
const model = cfg.ollamaModel || "phi3:mini";
|
|
634
|
-
const
|
|
641
|
+
const brevity = "This reply is delivered over Telegram. Be concise — short paragraphs and lists, no filler intro/summary. Reply in the user's language.";
|
|
642
|
+
const systemParts = [cfg.persona, brevity].filter(Boolean);
|
|
643
|
+
const messages = [];
|
|
644
|
+
if (systemParts.length) messages.push({ role: "system", content: systemParts.join("\n\n") });
|
|
645
|
+
messages.push({ role: "user", content: prompt });
|
|
646
|
+
const r = await fetch("http://localhost:11434/api/chat", {
|
|
635
647
|
method: "POST",
|
|
636
648
|
headers: { "Content-Type": "application/json" },
|
|
637
|
-
body: JSON.stringify({ model,
|
|
649
|
+
body: JSON.stringify({ model, messages, stream: false }),
|
|
638
650
|
signal: AbortSignal.timeout(60_000),
|
|
639
651
|
});
|
|
640
652
|
if (!r.ok) return { ok: false, text: `Ollama HTTP ${r.status}` };
|
|
641
653
|
const j = await r.json();
|
|
642
|
-
const text = (j.
|
|
654
|
+
const text = (j.message?.content || "").trim();
|
|
643
655
|
return text ? { ok: true, text: header + text } : { ok: false, text: "no response" };
|
|
644
656
|
}
|
|
645
657
|
|
|
@@ -923,8 +935,8 @@ let currentChild = null; // 실행 중인 claude child process (/stop 용)
|
|
|
923
935
|
let currentTyping = null; // 타이핑 인터벌 (/stop 시 정리용)
|
|
924
936
|
let prevSessionId; // /stop --reset 복원 대상
|
|
925
937
|
let stopping = false; // /stop 처리 중 오류 메시지 억제 플래그
|
|
926
|
-
let
|
|
927
|
-
let
|
|
938
|
+
let rateLimitUntil = null; // 레이트 리밋 활성 시 리셋 Date — 이 시간까지 메시지를 큐에 쌓음
|
|
939
|
+
let rateLimitTimer = null; // 리셋 시간에 큐를 드레인하는 타이머
|
|
928
940
|
|
|
929
941
|
async function handle(msg) {
|
|
930
942
|
const chatId = msg.chat?.id;
|
|
@@ -944,6 +956,14 @@ async function handle(msg) {
|
|
|
944
956
|
return;
|
|
945
957
|
}
|
|
946
958
|
|
|
959
|
+
// 레이트리밋 활성 중: 일반 메시지는 큐에 추가, 명령어는 통과
|
|
960
|
+
if (rateLimitUntil && Date.now() < rateLimitUntil && !text.startsWith("/")) {
|
|
961
|
+
msgQueue.push({ msg, receivedAt: Date.now() });
|
|
962
|
+
const timeStr = rateLimitUntil.toLocaleTimeString(l === "ko" ? "ko-KR" : "en-US", { hour: "2-digit", minute: "2-digit" });
|
|
963
|
+
await send(chatId, t(l, "rateLimitQueued", msgQueue.length, timeStr));
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
947
967
|
// 명령어
|
|
948
968
|
if (text === "/start" || text === "/help") {
|
|
949
969
|
await send(chatId, t(l, "help"));
|
|
@@ -1027,6 +1047,13 @@ async function handle(msg) {
|
|
|
1027
1047
|
}
|
|
1028
1048
|
return;
|
|
1029
1049
|
}
|
|
1050
|
+
if (text === "/ollama") {
|
|
1051
|
+
if (!cfg.ollamaFallback) { await send(chatId, t(l, "testFallbackDisabled")); return; }
|
|
1052
|
+
state.ollamaMode = !state.ollamaMode;
|
|
1053
|
+
saveState(state);
|
|
1054
|
+
await send(chatId, t(l, state.ollamaMode ? "ollamaOn" : "ollamaOff"));
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1030
1057
|
if (text === "/testfallback") {
|
|
1031
1058
|
if (!cfg.ollamaFallback) { await send(chatId, t(l, "testFallbackDisabled")); return; }
|
|
1032
1059
|
await send(chatId, "🧪 Ollama 연결 테스트 중…");
|
|
@@ -1083,27 +1110,16 @@ async function handle(msg) {
|
|
|
1083
1110
|
if (text === "/reserve" || text.startsWith("/reserve ")) {
|
|
1084
1111
|
const arg = text.slice(8).trim();
|
|
1085
1112
|
if (arg === "rm") {
|
|
1086
|
-
if (!
|
|
1087
|
-
clearTimeout(
|
|
1113
|
+
if (!rateLimitUntil && !rateLimitTimer) { await send(chatId, t(l, "reserveNone")); return; }
|
|
1114
|
+
if (rateLimitTimer) { clearTimeout(rateLimitTimer); rateLimitTimer = null; }
|
|
1115
|
+
rateLimitUntil = null;
|
|
1116
|
+
msgQueue.length = 0;
|
|
1088
1117
|
await send(chatId, t(l, "reserveRm"));
|
|
1089
1118
|
return;
|
|
1090
1119
|
}
|
|
1091
|
-
if (!
|
|
1092
|
-
const {
|
|
1093
|
-
|
|
1094
|
-
if (pendingRetry) clearTimeout(pendingRetry.timer);
|
|
1095
|
-
const capturedPrompt = reservePrompt;
|
|
1096
|
-
const capturedMsg = msg;
|
|
1097
|
-
const delay = Math.max(resetAt - Date.now(), 1000);
|
|
1098
|
-
pendingRetry = {
|
|
1099
|
-
resetAt,
|
|
1100
|
-
timer: setTimeout(() => {
|
|
1101
|
-
pendingRetry = null;
|
|
1102
|
-
handle({ ...capturedMsg, text: capturedPrompt, caption: undefined });
|
|
1103
|
-
}, delay),
|
|
1104
|
-
};
|
|
1105
|
-
const timeStr = resetAt.toLocaleTimeString(l === "ko" ? "ko-KR" : "en-US", { hour: "2-digit", minute: "2-digit" });
|
|
1106
|
-
await send(chatId, t(l, "reserveOk", timeStr));
|
|
1120
|
+
if (!rateLimitUntil) { await send(chatId, t(l, "reserveNone")); return; }
|
|
1121
|
+
const timeStr = rateLimitUntil.toLocaleTimeString(l === "ko" ? "ko-KR" : "en-US", { hour: "2-digit", minute: "2-digit" });
|
|
1122
|
+
await send(chatId, t(l, "reserveStatus", msgQueue.length, timeStr));
|
|
1107
1123
|
return;
|
|
1108
1124
|
}
|
|
1109
1125
|
|
|
@@ -1152,6 +1168,16 @@ async function handle(msg) {
|
|
|
1152
1168
|
}
|
|
1153
1169
|
const meta = buildMsgMeta(msg);
|
|
1154
1170
|
if (meta) prompt = prompt ? `${meta}\n\n${prompt}` : meta;
|
|
1171
|
+
if (state.ollamaMode) {
|
|
1172
|
+
try {
|
|
1173
|
+
const oRes = await runOllama(prompt, l, { noHeader: true });
|
|
1174
|
+
if (oRes.ok) await send(chatId, oRes.text);
|
|
1175
|
+
else await send(chatId, t(l, "testFallbackFail", oRes.text));
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
await send(chatId, t(l, "testFallbackFail", e.message));
|
|
1178
|
+
}
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1155
1181
|
prevSessionId = state.sessionId; // /stop --reset 복원 대상 저장
|
|
1156
1182
|
const res = await runClaude(prompt, state.sessionId, { modelHint: true, trackChild: true, injectMemory: true });
|
|
1157
1183
|
if (res.sessionId) {
|
|
@@ -1160,9 +1186,6 @@ async function handle(msg) {
|
|
|
1160
1186
|
}
|
|
1161
1187
|
const secs = Math.round((Date.now() - started) / 1000);
|
|
1162
1188
|
if (!res.ok) {
|
|
1163
|
-
// 레이트 리밋이고 리셋 시간을 알면 /reserve 힌트 추가
|
|
1164
|
-
const hint = res.resetAt ? t(l, "reserveHint") : "";
|
|
1165
|
-
rateLimitState = res.resetAt ? { prompt, resetAt: res.resetAt } : null;
|
|
1166
1189
|
// Ollama 폴백: 레이트리밋·크레딧 에러이고 ollamaFallback 켜져 있으면 Ollama로 재시도
|
|
1167
1190
|
if (cfg.ollamaFallback && res.canFallback && !stopping) {
|
|
1168
1191
|
try {
|
|
@@ -1170,10 +1193,23 @@ async function handle(msg) {
|
|
|
1170
1193
|
if (oRes.ok) { await send(chatId, oRes.text); return; }
|
|
1171
1194
|
} catch {}
|
|
1172
1195
|
}
|
|
1173
|
-
|
|
1196
|
+
// 리셋 시간을 알면 현재 메시지를 큐 앞에 다시 넣고 타이머 설정
|
|
1197
|
+
let autoRetryMsg = "";
|
|
1198
|
+
if (res.resetAt && !stopping) {
|
|
1199
|
+
msgQueue.unshift({ msg, receivedAt: Date.now() });
|
|
1200
|
+
rateLimitUntil = res.resetAt;
|
|
1201
|
+
if (rateLimitTimer) clearTimeout(rateLimitTimer);
|
|
1202
|
+
rateLimitTimer = setTimeout(() => {
|
|
1203
|
+
rateLimitTimer = null;
|
|
1204
|
+
rateLimitUntil = null;
|
|
1205
|
+
if (msgQueue.length > 0) handle(drainQueue());
|
|
1206
|
+
}, Math.max(res.resetAt - Date.now(), 1000));
|
|
1207
|
+
const timeStr = res.resetAt.toLocaleTimeString(l === "ko" ? "ko-KR" : "en-US", { hour: "2-digit", minute: "2-digit" });
|
|
1208
|
+
autoRetryMsg = "\n\n" + t(l, "reserveAuto", timeStr);
|
|
1209
|
+
}
|
|
1210
|
+
const errMsg = res.text === "contextTooLong" ? t(l, "contextTooLong") : `⚠️ ${res.text}${autoRetryMsg}`;
|
|
1174
1211
|
if (!stopping) await send(chatId, errMsg);
|
|
1175
1212
|
} else {
|
|
1176
|
-
rateLimitState = null;
|
|
1177
1213
|
const footer = `\n\n— ${secs}s${res.cost ? ` · $${res.cost.toFixed(4)}` : ""}`;
|
|
1178
1214
|
if (!stopping) await send(chatId, res.text + footer);
|
|
1179
1215
|
}
|
|
@@ -1184,7 +1220,7 @@ async function handle(msg) {
|
|
|
1184
1220
|
currentTyping = null;
|
|
1185
1221
|
stopping = false;
|
|
1186
1222
|
busy = false;
|
|
1187
|
-
if (msgQueue.length > 0) setImmediate(() => handle(drainQueue()));
|
|
1223
|
+
if (msgQueue.length > 0 && !rateLimitUntil) setImmediate(() => handle(drainQueue()));
|
|
1188
1224
|
}
|
|
1189
1225
|
}
|
|
1190
1226
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-telegram-bot",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.28",
|
|
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": {
|