claude-telegram-bot 0.3.11 → 0.3.14

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.
Files changed (3) hide show
  1. package/bot.mjs +38 -4
  2. package/ctb.mjs +35 -11
  3. package/package.json +1 -1
package/bot.mjs CHANGED
@@ -794,6 +794,7 @@ async function downloadAttachment(att) {
794
794
  // ── 메시지 처리 ───────────────────────────────────────────────────────────
795
795
  let busy = false;
796
796
  const msgQueue = []; // { msg, receivedAt } — busy 중 수신 메시지 대기열
797
+ const mediaGroups = new Map(); // media_group_id → { msgs, timer } — 미디어 그룹 수집 대기
797
798
  let currentChild = null; // 실행 중인 claude child process (/stop 용)
798
799
  let currentTyping = null; // 타이핑 인터벌 (/stop 시 정리용)
799
800
  let prevSessionId; // /stop --reset 복원 대상
@@ -804,8 +805,8 @@ async function handle(msg) {
804
805
  if (!chatId) return;
805
806
  const l = langOf(msg);
806
807
  const text = (msg.text || msg.caption || "").trim();
807
- const attachment = pickAttachment(msg);
808
- if (!text && !attachment) return;
808
+ const attachment = msg._mediaGroup ? null : pickAttachment(msg);
809
+ if (!text && !attachment && !msg._mediaGroup?.length) return;
809
810
 
810
811
  // 화이트리스트
811
812
  if (!cfg.allowedChatId) {
@@ -951,7 +952,18 @@ async function handle(msg) {
951
952
 
952
953
  try {
953
954
  let prompt = text;
954
- if (attachment) {
955
+ if (msg._mediaGroup?.length) {
956
+ const notes = [];
957
+ for (const fileId of msg._mediaGroup) {
958
+ try {
959
+ const { dest, name } = await downloadAttachment({ fileId, name: null });
960
+ notes.push(`[Attachment] Absolute path: ${dest} (filename: ${name}). Open it with the Read tool if needed.`);
961
+ } catch (e) {
962
+ await send(chatId, t(l, "attachFail", e.message));
963
+ }
964
+ }
965
+ if (notes.length) prompt = text ? `${text}\n\n${notes.join("\n")}` : notes.join("\n");
966
+ } else if (attachment) {
955
967
  try {
956
968
  const { dest, name } = await downloadAttachment(attachment);
957
969
  const note = `[Attachment] Absolute path: ${dest} (filename: ${name}). Open it with the Read tool if needed.`;
@@ -996,6 +1008,28 @@ function drainQueue() {
996
1008
  return { ...group[group.length - 1].msg, text: merged, caption: undefined };
997
1009
  }
998
1010
 
1011
+ // 미디어 그룹(여러 장 동시 전송) — 1초 대기 후 일괄 처리
1012
+ function mergeMediaGroup(msgs) {
1013
+ const captions = msgs.map((m) => m.caption || "").filter(Boolean);
1014
+ const fileIds = msgs
1015
+ .filter((m) => m.photo?.length)
1016
+ .map((m) => m.photo[m.photo.length - 1].file_id);
1017
+ return { ...msgs[0], text: captions.join("\n"), caption: undefined, _mediaGroup: fileIds };
1018
+ }
1019
+
1020
+ function dispatch(msg) {
1021
+ const gid = msg.media_group_id;
1022
+ if (!gid) { handle(msg).catch((e) => console.error("Handle error:", e.message)); return; }
1023
+ if (!mediaGroups.has(gid)) mediaGroups.set(gid, { msgs: [], timer: null });
1024
+ const g = mediaGroups.get(gid);
1025
+ g.msgs.push(msg);
1026
+ clearTimeout(g.timer);
1027
+ g.timer = setTimeout(() => {
1028
+ mediaGroups.delete(gid);
1029
+ handle(mergeMediaGroup(g.msgs)).catch((e) => console.error("Handle error:", e.message));
1030
+ }, 1000);
1031
+ }
1032
+
999
1033
  // ── 롱폴링 루프 ───────────────────────────────────────────────────────────
1000
1034
  async function main() {
1001
1035
  console.log("Bot started. Polling Telegram...");
@@ -1033,7 +1067,7 @@ async function main() {
1033
1067
  }
1034
1068
  for (const upd of res.result) {
1035
1069
  offset = upd.update_id + 1;
1036
- if (upd.message) handle(upd.message).catch((e) => console.error("Handle error:", e.message));
1070
+ if (upd.message) dispatch(upd.message);
1037
1071
  }
1038
1072
  } catch (e) {
1039
1073
  console.error("Polling error:", e.message);
package/ctb.mjs CHANGED
@@ -17,6 +17,11 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
17
17
  import { basename, dirname, join } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { spawn } from "node:child_process";
20
+ import dns from "node:dns";
21
+ import net from "node:net";
22
+
23
+ dns.setDefaultResultOrder("ipv4first");
24
+ if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
20
25
 
21
26
  const HERE = dirname(fileURLToPath(import.meta.url));
22
27
  const args = process.argv.slice(2);
@@ -28,7 +33,7 @@ const VERSION = (() => {
28
33
  } catch {
29
34
  return "?";
30
35
  }
31
- })();
36
+ })();;
32
37
 
33
38
  function runBot(botArgs) {
34
39
  const child = spawn(process.execPath, [join(HERE, "bot.mjs"), ...botArgs], {
@@ -56,7 +61,7 @@ function resolveConfig(arg) {
56
61
  return existsSync(join(process.cwd(), arg)) ? join(process.cwd(), arg) : join(HERE, arg);
57
62
  }
58
63
 
59
- function main() {
64
+ async function main() {
60
65
  if (a === "-h" || a === "--help") {
61
66
  console.log(
62
67
  `ctb v${VERSION} — claude-telegram-bot short CLI\n\n` +
@@ -122,9 +127,20 @@ function main() {
122
127
  sessionId = JSON.parse(readFileSync(statePath, "utf8")).sessionId;
123
128
  } catch {}
124
129
 
125
- const finalArgs = sessionId ? ["--resume", sessionId, ...claudeArgs] : claudeArgs;
126
- if (sessionId) process.stderr.write(`Resuming session: ${sessionId}\n`);
130
+ if (sessionId) {
131
+ process.stderr.write(`Resuming session: ${sessionId}\n`);
132
+ // 텔레그램 이전 대화와 구분하기 위해 세션에 시작 마커 삽입
133
+ await new Promise((resolve) => {
134
+ const marker = spawn("claude", [
135
+ "--resume", sessionId, "-p", "---ctb:start---", "--output-format", "json",
136
+ ], { stdio: ["ignore", "ignore", "ignore"] });
137
+ marker.on("close", resolve);
138
+ marker.on("error", resolve);
139
+ setTimeout(() => { marker.kill(); resolve(); }, 15000);
140
+ });
141
+ }
127
142
 
143
+ const finalArgs = sessionId ? ["--resume", sessionId, ...claudeArgs] : claudeArgs;
128
144
  const child = spawn("claude", finalArgs, { stdio: "inherit" });
129
145
  child.on("close", async (code) => {
130
146
  cleanup();
@@ -135,8 +151,8 @@ function main() {
135
151
 
136
152
  async function summarizeSession(sid, lang) {
137
153
  const langInstruction = lang && lang.startsWith("ko")
138
- ? "한국어로 짧은 구문(10단어 이내)으로 이 세션에서 한 작업을 요약해줘. 마크다운 없이 텍스트만. 중요한 작업이 없으면 정확히 이렇게만 답해: SKIP"
139
- : "In one short phrase (10 words max), what was done this session? Plain text only. If nothing significant, reply: SKIP";
154
+ ? "---ctb:start--- 마커 이후에 이 터미널 세션에서 한 작업을 한국어로 짧은 구문(10단어 이내)으로 요약해줘. 마크다운 없이 텍스트만. 마커 이후 중요한 작업이 없으면 정확히 이렇게만 답해: SKIP"
155
+ : "After the ---ctb:start--- marker in this conversation, what was accomplished in this terminal session? One short phrase, 10 words max, plain text. If nothing significant after the marker, reply: SKIP";
140
156
  return new Promise((resolve) => {
141
157
  const child = spawn("claude", [
142
158
  "--resume", sid,
@@ -160,14 +176,22 @@ async function notifyTelegram(configPath, sessionId) {
160
176
  try {
161
177
  const cfg = JSON.parse(readFileSync(configPath, "utf8"));
162
178
  if (!cfg.token || !cfg.allowedChatId || cfg.ctbNotify === false) return;
163
- const summary = await summarizeSession(sessionId, cfg.lang);
164
- if (!summary) return;
165
- await fetch(`https://api.telegram.org/bot${cfg.token}/sendMessage`, {
179
+ const lang = cfg.lang || process.env.LANG || "";
180
+ process.stderr.write("ctb: summarizing session...\n");
181
+ const summary = await summarizeSession(sessionId, lang);
182
+ if (!summary) { process.stderr.write("ctb: nothing to summarize (SKIP)\n"); return; }
183
+ process.stderr.write(`ctb: sending to Telegram — ${summary}\n`);
184
+ const label = lang.startsWith("ko") ? "[터미널]" : "[local]";
185
+ const r = await fetch(`https://api.telegram.org/bot${cfg.token}/sendMessage`, {
166
186
  method: "POST",
167
187
  headers: { "content-type": "application/json" },
168
- body: JSON.stringify({ chat_id: cfg.allowedChatId, text: `💻 ${summary}` }),
188
+ body: JSON.stringify({ chat_id: cfg.allowedChatId, text: `💻 ${label} ${summary}` }),
169
189
  });
170
- } catch {}
190
+ const json = await r.json();
191
+ if (!json.ok) process.stderr.write(`ctb: Telegram error — ${JSON.stringify(json)}\n`);
192
+ } catch (e) {
193
+ process.stderr.write(`ctb: notify error — ${e.message}\n`);
194
+ }
171
195
  }
172
196
 
173
197
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-telegram-bot",
3
- "version": "0.3.11",
3
+ "version": "0.3.14",
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": {