claude-telegram-bot 0.3.24 → 0.3.27
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 +81 -44
- 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 서버가 일시적으로 과부하 상태입니다. 잠시 후 다시 시도해주세요.";
|
|
@@ -626,20 +634,25 @@ function runClaude(prompt, sessionId, opts = {}) {
|
|
|
626
634
|
}
|
|
627
635
|
|
|
628
636
|
// ── Ollama 폴백 실행 ──────────────────────────────────────────────────────
|
|
629
|
-
async function runOllama(prompt, lang = "en") {
|
|
630
|
-
const header = lang === "ko"
|
|
637
|
+
async function runOllama(prompt, lang = "en", opts = {}) {
|
|
638
|
+
const header = opts.noHeader ? "" : (lang === "ko"
|
|
631
639
|
? "🌙 Claude가 잠시 쉬고 있어요. 제가 대신 도와드릴게요. (세션은 이어지지 않아요)\n\n"
|
|
632
|
-
: "🌙 Claude is resting right now. I'll help in the meantime. (Session won't continue)\n\n";
|
|
640
|
+
: "🌙 Claude is resting right now. I'll help in the meantime. (Session won't continue)\n\n");
|
|
633
641
|
const model = cfg.ollamaModel || "phi3:mini";
|
|
634
|
-
const
|
|
642
|
+
const brevity = "This reply is delivered over Telegram. Be concise — short paragraphs and lists, no filler intro/summary. Reply in the user's language.";
|
|
643
|
+
const systemParts = [cfg.persona, brevity].filter(Boolean);
|
|
644
|
+
const messages = [];
|
|
645
|
+
if (systemParts.length) messages.push({ role: "system", content: systemParts.join("\n\n") });
|
|
646
|
+
messages.push({ role: "user", content: prompt });
|
|
647
|
+
const r = await fetch("http://localhost:11434/api/chat", {
|
|
635
648
|
method: "POST",
|
|
636
649
|
headers: { "Content-Type": "application/json" },
|
|
637
|
-
body: JSON.stringify({ model,
|
|
650
|
+
body: JSON.stringify({ model, messages, stream: false }),
|
|
638
651
|
signal: AbortSignal.timeout(60_000),
|
|
639
652
|
});
|
|
640
653
|
if (!r.ok) return { ok: false, text: `Ollama HTTP ${r.status}` };
|
|
641
654
|
const j = await r.json();
|
|
642
|
-
const text = (j.
|
|
655
|
+
const text = (j.message?.content || "").trim();
|
|
643
656
|
return text ? { ok: true, text: header + text } : { ok: false, text: "no response" };
|
|
644
657
|
}
|
|
645
658
|
|
|
@@ -923,8 +936,8 @@ let currentChild = null; // 실행 중인 claude child process (/stop 용)
|
|
|
923
936
|
let currentTyping = null; // 타이핑 인터벌 (/stop 시 정리용)
|
|
924
937
|
let prevSessionId; // /stop --reset 복원 대상
|
|
925
938
|
let stopping = false; // /stop 처리 중 오류 메시지 억제 플래그
|
|
926
|
-
let
|
|
927
|
-
let
|
|
939
|
+
let rateLimitUntil = null; // 레이트 리밋 활성 시 리셋 Date — 이 시간까지 메시지를 큐에 쌓음
|
|
940
|
+
let rateLimitTimer = null; // 리셋 시간에 큐를 드레인하는 타이머
|
|
928
941
|
|
|
929
942
|
async function handle(msg) {
|
|
930
943
|
const chatId = msg.chat?.id;
|
|
@@ -944,6 +957,14 @@ async function handle(msg) {
|
|
|
944
957
|
return;
|
|
945
958
|
}
|
|
946
959
|
|
|
960
|
+
// 레이트리밋 활성 중: 일반 메시지는 큐에 추가, 명령어는 통과
|
|
961
|
+
if (rateLimitUntil && Date.now() < rateLimitUntil && !text.startsWith("/")) {
|
|
962
|
+
msgQueue.push({ msg, receivedAt: Date.now() });
|
|
963
|
+
const timeStr = rateLimitUntil.toLocaleTimeString(l === "ko" ? "ko-KR" : "en-US", { hour: "2-digit", minute: "2-digit" });
|
|
964
|
+
await send(chatId, t(l, "rateLimitQueued", msgQueue.length, timeStr));
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
947
968
|
// 명령어
|
|
948
969
|
if (text === "/start" || text === "/help") {
|
|
949
970
|
await send(chatId, t(l, "help"));
|
|
@@ -1027,6 +1048,13 @@ async function handle(msg) {
|
|
|
1027
1048
|
}
|
|
1028
1049
|
return;
|
|
1029
1050
|
}
|
|
1051
|
+
if (text === "/ollama") {
|
|
1052
|
+
if (!cfg.ollamaFallback) { await send(chatId, t(l, "testFallbackDisabled")); return; }
|
|
1053
|
+
state.ollamaMode = !state.ollamaMode;
|
|
1054
|
+
saveState(state);
|
|
1055
|
+
await send(chatId, t(l, state.ollamaMode ? "ollamaOn" : "ollamaOff"));
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1030
1058
|
if (text === "/testfallback") {
|
|
1031
1059
|
if (!cfg.ollamaFallback) { await send(chatId, t(l, "testFallbackDisabled")); return; }
|
|
1032
1060
|
await send(chatId, "🧪 Ollama 연결 테스트 중…");
|
|
@@ -1083,27 +1111,16 @@ async function handle(msg) {
|
|
|
1083
1111
|
if (text === "/reserve" || text.startsWith("/reserve ")) {
|
|
1084
1112
|
const arg = text.slice(8).trim();
|
|
1085
1113
|
if (arg === "rm") {
|
|
1086
|
-
if (!
|
|
1087
|
-
clearTimeout(
|
|
1114
|
+
if (!rateLimitUntil && !rateLimitTimer) { await send(chatId, t(l, "reserveNone")); return; }
|
|
1115
|
+
if (rateLimitTimer) { clearTimeout(rateLimitTimer); rateLimitTimer = null; }
|
|
1116
|
+
rateLimitUntil = null;
|
|
1117
|
+
msgQueue.length = 0;
|
|
1088
1118
|
await send(chatId, t(l, "reserveRm"));
|
|
1089
1119
|
return;
|
|
1090
1120
|
}
|
|
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));
|
|
1121
|
+
if (!rateLimitUntil) { await send(chatId, t(l, "reserveNone")); return; }
|
|
1122
|
+
const timeStr = rateLimitUntil.toLocaleTimeString(l === "ko" ? "ko-KR" : "en-US", { hour: "2-digit", minute: "2-digit" });
|
|
1123
|
+
await send(chatId, t(l, "reserveStatus", msgQueue.length, timeStr));
|
|
1107
1124
|
return;
|
|
1108
1125
|
}
|
|
1109
1126
|
|
|
@@ -1152,6 +1169,16 @@ async function handle(msg) {
|
|
|
1152
1169
|
}
|
|
1153
1170
|
const meta = buildMsgMeta(msg);
|
|
1154
1171
|
if (meta) prompt = prompt ? `${meta}\n\n${prompt}` : meta;
|
|
1172
|
+
if (state.ollamaMode) {
|
|
1173
|
+
try {
|
|
1174
|
+
const oRes = await runOllama(prompt, l, { noHeader: true });
|
|
1175
|
+
if (oRes.ok) await send(chatId, oRes.text);
|
|
1176
|
+
else await send(chatId, t(l, "testFallbackFail", oRes.text));
|
|
1177
|
+
} catch (e) {
|
|
1178
|
+
await send(chatId, t(l, "testFallbackFail", e.message));
|
|
1179
|
+
}
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1155
1182
|
prevSessionId = state.sessionId; // /stop --reset 복원 대상 저장
|
|
1156
1183
|
const res = await runClaude(prompt, state.sessionId, { modelHint: true, trackChild: true, injectMemory: true });
|
|
1157
1184
|
if (res.sessionId) {
|
|
@@ -1160,9 +1187,6 @@ async function handle(msg) {
|
|
|
1160
1187
|
}
|
|
1161
1188
|
const secs = Math.round((Date.now() - started) / 1000);
|
|
1162
1189
|
if (!res.ok) {
|
|
1163
|
-
// 레이트 리밋이고 리셋 시간을 알면 /reserve 힌트 추가
|
|
1164
|
-
const hint = res.resetAt ? t(l, "reserveHint") : "";
|
|
1165
|
-
rateLimitState = res.resetAt ? { prompt, resetAt: res.resetAt } : null;
|
|
1166
1190
|
// Ollama 폴백: 레이트리밋·크레딧 에러이고 ollamaFallback 켜져 있으면 Ollama로 재시도
|
|
1167
1191
|
if (cfg.ollamaFallback && res.canFallback && !stopping) {
|
|
1168
1192
|
try {
|
|
@@ -1170,10 +1194,23 @@ async function handle(msg) {
|
|
|
1170
1194
|
if (oRes.ok) { await send(chatId, oRes.text); return; }
|
|
1171
1195
|
} catch {}
|
|
1172
1196
|
}
|
|
1173
|
-
|
|
1197
|
+
// 리셋 시간을 알면 현재 메시지를 큐 앞에 다시 넣고 타이머 설정
|
|
1198
|
+
let autoRetryMsg = "";
|
|
1199
|
+
if (res.resetAt && !stopping) {
|
|
1200
|
+
msgQueue.unshift({ msg, receivedAt: Date.now() });
|
|
1201
|
+
rateLimitUntil = res.resetAt;
|
|
1202
|
+
if (rateLimitTimer) clearTimeout(rateLimitTimer);
|
|
1203
|
+
rateLimitTimer = setTimeout(() => {
|
|
1204
|
+
rateLimitTimer = null;
|
|
1205
|
+
rateLimitUntil = null;
|
|
1206
|
+
if (msgQueue.length > 0) handle(drainQueue());
|
|
1207
|
+
}, Math.max(res.resetAt - Date.now(), 1000));
|
|
1208
|
+
const timeStr = res.resetAt.toLocaleTimeString(l === "ko" ? "ko-KR" : "en-US", { hour: "2-digit", minute: "2-digit" });
|
|
1209
|
+
autoRetryMsg = "\n\n" + t(l, "reserveAuto", timeStr);
|
|
1210
|
+
}
|
|
1211
|
+
const errMsg = res.text === "contextTooLong" ? t(l, "contextTooLong") : `⚠️ ${res.text}${autoRetryMsg}`;
|
|
1174
1212
|
if (!stopping) await send(chatId, errMsg);
|
|
1175
1213
|
} else {
|
|
1176
|
-
rateLimitState = null;
|
|
1177
1214
|
const footer = `\n\n— ${secs}s${res.cost ? ` · $${res.cost.toFixed(4)}` : ""}`;
|
|
1178
1215
|
if (!stopping) await send(chatId, res.text + footer);
|
|
1179
1216
|
}
|
|
@@ -1184,7 +1221,7 @@ async function handle(msg) {
|
|
|
1184
1221
|
currentTyping = null;
|
|
1185
1222
|
stopping = false;
|
|
1186
1223
|
busy = false;
|
|
1187
|
-
if (msgQueue.length > 0) setImmediate(() => handle(drainQueue()));
|
|
1224
|
+
if (msgQueue.length > 0 && !rateLimitUntil) setImmediate(() => handle(drainQueue()));
|
|
1188
1225
|
}
|
|
1189
1226
|
}
|
|
1190
1227
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-telegram-bot",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.27",
|
|
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": {
|