claude-telegram-bot 0.3.36 → 0.4.0
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/CHANGELOG.md +326 -0
- package/README.ko.md +95 -25
- package/README.md +108 -31
- package/bot.mjs +456 -58
- package/config.example.json +11 -1
- package/ctb.mjs +84 -29
- package/package.json +5 -2
package/config.example.json
CHANGED
|
@@ -3,13 +3,23 @@
|
|
|
3
3
|
"token": "BOT_TOKEN_FROM_BOTFATHER",
|
|
4
4
|
"allowedChatId": "",
|
|
5
5
|
"projectDir": "/ABSOLUTE/PATH/TO/PROJECT",
|
|
6
|
+
"provider": "claude",
|
|
6
7
|
"claudeBin": "/ABSOLUTE/PATH/TO/claude",
|
|
7
|
-
"permissionMode": "
|
|
8
|
+
"permissionMode": "acceptEdits",
|
|
8
9
|
"model": "",
|
|
9
10
|
"lang": "",
|
|
10
11
|
"persona": "System prompt defining this bot's role/voice (optional). Used to differentiate persona bots (developer/planner/...).",
|
|
11
12
|
"appendSystemPrompt": "",
|
|
12
13
|
"env": {},
|
|
14
|
+
"codexFallback": false,
|
|
15
|
+
"codexBin": "/ABSOLUTE/PATH/TO/codex",
|
|
16
|
+
"codexModel": "",
|
|
17
|
+
"codexSandbox": "workspace-write",
|
|
18
|
+
"codexTimeout": 600000,
|
|
19
|
+
"ollamaFallback": false,
|
|
20
|
+
"ollamaModel": "qwen3.5:4b",
|
|
21
|
+
"ollamaBin": "",
|
|
22
|
+
"ollamaTimeout": 360000,
|
|
13
23
|
"schedule": [
|
|
14
24
|
{ "cron": "0 9 * * 1-5", "label": "Morning brief", "prompt": "Summarize today's open issues and TODOs (optional; standard 5-field cron: min hour day month weekday)" }
|
|
15
25
|
]
|
package/ctb.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// ctb — short-form CLI for claude-telegram-bot
|
|
3
3
|
//
|
|
4
|
-
// ctb [config.json] [
|
|
4
|
+
// ctb [config.json] [--provider claude|codex] [...provider args]
|
|
5
|
+
// Resume the provider's Telegram session
|
|
5
6
|
// ctb bot [config.json] Start the Telegram bot daemon (delegates to bot.mjs)
|
|
6
7
|
// ctb init [dir] Create a config.json template
|
|
7
8
|
// ctb --help | --version
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
// package directory (where bot configs typically live alongside bot.mjs).
|
|
11
12
|
// Absolute or explicitly relative paths (/ or ./) resolve as-is.
|
|
12
13
|
//
|
|
13
|
-
// While
|
|
14
|
+
// While a provider runs, .claude-bot/local.lock (PID) is created so the bot defers
|
|
14
15
|
// incoming Telegram messages until the local session ends.
|
|
15
16
|
|
|
16
17
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -66,7 +67,8 @@ async function main() {
|
|
|
66
67
|
console.log(
|
|
67
68
|
`ctb v${VERSION} — claude-telegram-bot short CLI\n\n` +
|
|
68
69
|
`Usage:\n` +
|
|
69
|
-
` ctb [config.json] [...args]
|
|
70
|
+
` ctb [config.json] [--provider claude|codex] [...args]\n` +
|
|
71
|
+
` Resume the provider's Telegram session\n` +
|
|
70
72
|
` ctb bot [config.json] Start the Telegram bot daemon\n` +
|
|
71
73
|
` ctb init [dir] Create a config.json template\n` +
|
|
72
74
|
` ctb --help | --version\n\n` +
|
|
@@ -77,6 +79,7 @@ async function main() {
|
|
|
77
79
|
` ctb -p "what did we do?" Headless Claude with session context\n` +
|
|
78
80
|
` ctb planner.json Resume planner persona session interactively\n` +
|
|
79
81
|
` ctb planner.json -p "..." Headless with planner session\n` +
|
|
82
|
+
` ctb planner.json --provider codex Interactive Codex with its Telegram session\n` +
|
|
80
83
|
` ctb bot Start the bot with default config\n` +
|
|
81
84
|
` ctb bot planner.json Start the bot with planner config`,
|
|
82
85
|
);
|
|
@@ -98,10 +101,28 @@ async function main() {
|
|
|
98
101
|
return;
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
// Run
|
|
104
|
+
// Run the selected provider, resuming that provider's bot session.
|
|
102
105
|
const looksLikeConfig = a && a.endsWith(".json");
|
|
103
106
|
const configPath = resolveConfig(looksLikeConfig ? a : undefined);
|
|
104
|
-
const
|
|
107
|
+
const providerArgs = looksLikeConfig ? args.slice(1) : args;
|
|
108
|
+
const cfg = JSON.parse(readFileSync(configPath, "utf8"));
|
|
109
|
+
let providerOverride;
|
|
110
|
+
const forwardedArgs = [];
|
|
111
|
+
for (let i = 0; i < providerArgs.length; i++) {
|
|
112
|
+
const arg = providerArgs[i];
|
|
113
|
+
if (arg === "--provider") {
|
|
114
|
+
providerOverride = providerArgs[++i];
|
|
115
|
+
if (!providerOverride) throw new Error("--provider requires claude or codex");
|
|
116
|
+
} else if (arg.startsWith("--provider=")) {
|
|
117
|
+
providerOverride = arg.slice("--provider=".length);
|
|
118
|
+
} else {
|
|
119
|
+
forwardedArgs.push(arg);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const provider = providerOverride || cfg.provider || "claude";
|
|
123
|
+
if (!["claude", "codex"].includes(provider)) {
|
|
124
|
+
throw new Error(`Unsupported provider: ${provider} (expected claude or codex)`);
|
|
125
|
+
}
|
|
105
126
|
|
|
106
127
|
const dataDir = dirname(configPath);
|
|
107
128
|
const botDir = join(dataDir, ".claude-bot");
|
|
@@ -122,48 +143,79 @@ async function main() {
|
|
|
122
143
|
process.on("SIGINT", () => { signalExitCode = 130; });
|
|
123
144
|
process.on("SIGTERM", () => { signalExitCode = 143; });
|
|
124
145
|
|
|
146
|
+
const sessionKey = provider === "codex" ? "codexSessionId" : "sessionId";
|
|
125
147
|
let sessionId;
|
|
126
148
|
try {
|
|
127
|
-
sessionId = JSON.parse(readFileSync(statePath, "utf8"))
|
|
149
|
+
sessionId = JSON.parse(readFileSync(statePath, "utf8"))[sessionKey];
|
|
128
150
|
} catch {}
|
|
129
151
|
|
|
130
152
|
if (sessionId) {
|
|
131
|
-
process.stderr.write(`Resuming session: ${sessionId}\n`);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
process.stderr.write(`Resuming ${provider} session: ${sessionId}\n`);
|
|
154
|
+
if (provider === "claude") {
|
|
155
|
+
// 텔레그램 이전 대화와 구분하기 위해 Claude 세션에 시작 마커 삽입
|
|
156
|
+
await new Promise((resolve) => {
|
|
157
|
+
const marker = spawn(cfg.claudeBin || "claude", [
|
|
158
|
+
"--resume", sessionId, "-p", "---ctb:start---", "--output-format", "json",
|
|
159
|
+
], { cwd: cfg.projectDir, env: { ...process.env, ...(cfg.env || {}) }, stdio: ["ignore", "ignore", "ignore"] });
|
|
160
|
+
marker.on("close", resolve);
|
|
161
|
+
marker.on("error", resolve);
|
|
162
|
+
setTimeout(() => { marker.kill(); resolve(); }, 15000);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
141
165
|
}
|
|
142
166
|
|
|
143
|
-
const
|
|
144
|
-
const
|
|
167
|
+
const bin = provider === "codex" ? (cfg.codexBin || "codex") : (cfg.claudeBin || "claude");
|
|
168
|
+
const finalArgs = provider === "codex"
|
|
169
|
+
? (sessionId ? ["resume", sessionId, ...forwardedArgs] : forwardedArgs)
|
|
170
|
+
: (sessionId ? ["--resume", sessionId, ...forwardedArgs] : forwardedArgs);
|
|
171
|
+
const child = spawn(bin, finalArgs, {
|
|
172
|
+
cwd: cfg.projectDir,
|
|
173
|
+
env: { ...process.env, ...(cfg.env || {}) },
|
|
174
|
+
stdio: "inherit",
|
|
175
|
+
});
|
|
145
176
|
child.on("close", async (code) => {
|
|
146
177
|
cleanup();
|
|
147
|
-
if (sessionId) await notifyTelegram(configPath, sessionId);
|
|
178
|
+
if (sessionId) await notifyTelegram(configPath, provider, sessionId);
|
|
148
179
|
process.exit(signalExitCode ?? code ?? 0);
|
|
149
180
|
});
|
|
181
|
+
child.on("error", (e) => {
|
|
182
|
+
cleanup();
|
|
183
|
+
process.stderr.write(`ctb: failed to start ${provider}: ${e.message}\n`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|
|
150
186
|
}
|
|
151
187
|
|
|
152
|
-
async function summarizeSession(sid, lang) {
|
|
188
|
+
async function summarizeSession(provider, sid, lang, cfg) {
|
|
153
189
|
const langInstruction = lang && lang.startsWith("ko")
|
|
154
190
|
? "방금 로컬 터미널 코딩 세션이 끝났어. 이 대화에서 가장 최근에 나눈 내용(이 메시지 직전까지)을 바탕으로, 그 세션에서 무엇을 했는지 한국어로 짧은 구문(10단어 이내)으로 요약해줘. 마크다운 없이 텍스트만. 중요한 작업이 없었으면 정확히 이렇게만 답해: SKIP"
|
|
155
191
|
: "A local terminal coding session just ended. Based on the most recent exchanges in this conversation (just before this message), summarize in one short phrase (10 words max) what was accomplished. Plain text only. If nothing significant was done, reply exactly: SKIP";
|
|
156
192
|
return new Promise((resolve) => {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"
|
|
161
|
-
|
|
193
|
+
const isCodex = provider === "codex";
|
|
194
|
+
const bin = isCodex ? (cfg.codexBin || "codex") : (cfg.claudeBin || "claude");
|
|
195
|
+
const args = isCodex
|
|
196
|
+
? ["exec", "resume", "--json", sid, langInstruction]
|
|
197
|
+
: ["--resume", sid, "-p", langInstruction, "--output-format", "json"];
|
|
198
|
+
const child = spawn(bin, args, {
|
|
199
|
+
cwd: cfg.projectDir,
|
|
200
|
+
env: { ...process.env, ...(cfg.env || {}) },
|
|
201
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
202
|
+
});
|
|
162
203
|
let out = "";
|
|
163
204
|
child.stdout.on("data", (d) => { out += d; });
|
|
164
205
|
child.on("close", () => {
|
|
165
206
|
try {
|
|
166
|
-
|
|
207
|
+
let text = "";
|
|
208
|
+
if (isCodex) {
|
|
209
|
+
for (const line of out.split("\n")) {
|
|
210
|
+
try {
|
|
211
|
+
const event = JSON.parse(line);
|
|
212
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message") text = event.item.text || text;
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
text = JSON.parse(out).result || "";
|
|
217
|
+
}
|
|
218
|
+
text = text.trim();
|
|
167
219
|
resolve(/^skip$/i.test(text) ? null : text);
|
|
168
220
|
} catch { resolve(null); }
|
|
169
221
|
});
|
|
@@ -172,13 +224,13 @@ async function summarizeSession(sid, lang) {
|
|
|
172
224
|
});
|
|
173
225
|
}
|
|
174
226
|
|
|
175
|
-
async function notifyTelegram(configPath, sessionId) {
|
|
227
|
+
async function notifyTelegram(configPath, provider, sessionId) {
|
|
176
228
|
try {
|
|
177
229
|
const cfg = JSON.parse(readFileSync(configPath, "utf8"));
|
|
178
230
|
if (!cfg.token || !cfg.allowedChatId || cfg.ctbNotify === false) return;
|
|
179
231
|
const lang = cfg.lang || process.env.LANG || "";
|
|
180
232
|
process.stderr.write("ctb: summarizing session...\n");
|
|
181
|
-
const summary = await summarizeSession(sessionId, lang);
|
|
233
|
+
const summary = await summarizeSession(provider, sessionId, lang, cfg);
|
|
182
234
|
if (!summary) { process.stderr.write("ctb: nothing to summarize (SKIP)\n"); return; }
|
|
183
235
|
process.stderr.write(`ctb: sending to Telegram — ${summary}\n`);
|
|
184
236
|
const label = lang.startsWith("ko") ? "[터미널]" : "[local]";
|
|
@@ -194,4 +246,7 @@ async function notifyTelegram(configPath, sessionId) {
|
|
|
194
246
|
}
|
|
195
247
|
}
|
|
196
248
|
|
|
197
|
-
main()
|
|
249
|
+
main().catch((e) => {
|
|
250
|
+
process.stderr.write(`ctb: ${e.message}\n`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-telegram-bot",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Drive Claude Code from Telegram
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Drive Claude Code or Codex from Telegram with persistent sessions, provider switching, and zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-telegram-bot": "bot.mjs",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"bot.mjs",
|
|
15
15
|
"ctb.mjs",
|
|
16
16
|
"session.mjs",
|
|
17
|
+
"CHANGELOG.md",
|
|
17
18
|
"config.example.json",
|
|
18
19
|
"com.claudebot.example.plist"
|
|
19
20
|
],
|
|
@@ -27,6 +28,8 @@
|
|
|
27
28
|
"claude",
|
|
28
29
|
"claude-code",
|
|
29
30
|
"anthropic",
|
|
31
|
+
"codex",
|
|
32
|
+
"openai",
|
|
30
33
|
"telegram",
|
|
31
34
|
"bot",
|
|
32
35
|
"cli",
|