claude-telegram-bot 0.3.40 → 0.4.1
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 +332 -0
- package/README.ko.md +94 -24
- package/README.md +107 -30
- package/bot.mjs +413 -63
- package/config.example.json +11 -1
- package/ctb.mjs +87 -31
- 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,17 +67,20 @@ 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` +
|
|
73
75
|
`config.json defaults to $BOT_CONFIG or the package's own config.json.\n` +
|
|
74
76
|
`A bare name like "planner.json" resolves relative to the package directory.\n\n` +
|
|
77
|
+
`Provider precedence: --provider flag → config.provider → claude.\n\n` +
|
|
75
78
|
`Examples:\n` +
|
|
76
|
-
` ctb Interactive
|
|
77
|
-
` ctb -p "what did we do?" Headless
|
|
79
|
+
` ctb Interactive configured provider, continuing its session\n` +
|
|
80
|
+
` ctb -p "what did we do?" Headless configured provider with session context\n` +
|
|
78
81
|
` ctb planner.json Resume planner persona session interactively\n` +
|
|
79
82
|
` ctb planner.json -p "..." Headless with planner session\n` +
|
|
83
|
+
` ctb planner.json --provider codex Interactive Codex with its Telegram session\n` +
|
|
80
84
|
` ctb bot Start the bot with default config\n` +
|
|
81
85
|
` ctb bot planner.json Start the bot with planner config`,
|
|
82
86
|
);
|
|
@@ -98,10 +102,28 @@ async function main() {
|
|
|
98
102
|
return;
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
// Run
|
|
105
|
+
// Run the selected provider, resuming that provider's bot session.
|
|
102
106
|
const looksLikeConfig = a && a.endsWith(".json");
|
|
103
107
|
const configPath = resolveConfig(looksLikeConfig ? a : undefined);
|
|
104
|
-
const
|
|
108
|
+
const providerArgs = looksLikeConfig ? args.slice(1) : args;
|
|
109
|
+
const cfg = JSON.parse(readFileSync(configPath, "utf8"));
|
|
110
|
+
let providerOverride;
|
|
111
|
+
const forwardedArgs = [];
|
|
112
|
+
for (let i = 0; i < providerArgs.length; i++) {
|
|
113
|
+
const arg = providerArgs[i];
|
|
114
|
+
if (arg === "--provider") {
|
|
115
|
+
providerOverride = providerArgs[++i];
|
|
116
|
+
if (!providerOverride) throw new Error("--provider requires claude or codex");
|
|
117
|
+
} else if (arg.startsWith("--provider=")) {
|
|
118
|
+
providerOverride = arg.slice("--provider=".length);
|
|
119
|
+
} else {
|
|
120
|
+
forwardedArgs.push(arg);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const provider = providerOverride || cfg.provider || "claude";
|
|
124
|
+
if (!["claude", "codex"].includes(provider)) {
|
|
125
|
+
throw new Error(`Unsupported provider: ${provider} (expected claude or codex)`);
|
|
126
|
+
}
|
|
105
127
|
|
|
106
128
|
const dataDir = dirname(configPath);
|
|
107
129
|
const botDir = join(dataDir, ".claude-bot");
|
|
@@ -122,48 +144,79 @@ async function main() {
|
|
|
122
144
|
process.on("SIGINT", () => { signalExitCode = 130; });
|
|
123
145
|
process.on("SIGTERM", () => { signalExitCode = 143; });
|
|
124
146
|
|
|
147
|
+
const sessionKey = provider === "codex" ? "codexSessionId" : "sessionId";
|
|
125
148
|
let sessionId;
|
|
126
149
|
try {
|
|
127
|
-
sessionId = JSON.parse(readFileSync(statePath, "utf8"))
|
|
150
|
+
sessionId = JSON.parse(readFileSync(statePath, "utf8"))[sessionKey];
|
|
128
151
|
} catch {}
|
|
129
152
|
|
|
130
153
|
if (sessionId) {
|
|
131
|
-
process.stderr.write(`Resuming session: ${sessionId}\n`);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
154
|
+
process.stderr.write(`Resuming ${provider} session: ${sessionId}\n`);
|
|
155
|
+
if (provider === "claude") {
|
|
156
|
+
// 텔레그램 이전 대화와 구분하기 위해 Claude 세션에 시작 마커 삽입
|
|
157
|
+
await new Promise((resolve) => {
|
|
158
|
+
const marker = spawn(cfg.claudeBin || "claude", [
|
|
159
|
+
"--resume", sessionId, "-p", "---ctb:start---", "--output-format", "json",
|
|
160
|
+
], { cwd: cfg.projectDir, env: { ...process.env, ...(cfg.env || {}) }, stdio: ["ignore", "ignore", "ignore"] });
|
|
161
|
+
marker.on("close", resolve);
|
|
162
|
+
marker.on("error", resolve);
|
|
163
|
+
setTimeout(() => { marker.kill(); resolve(); }, 15000);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
141
166
|
}
|
|
142
167
|
|
|
143
|
-
const
|
|
144
|
-
const
|
|
168
|
+
const bin = provider === "codex" ? (cfg.codexBin || "codex") : (cfg.claudeBin || "claude");
|
|
169
|
+
const finalArgs = provider === "codex"
|
|
170
|
+
? (sessionId ? ["resume", sessionId, ...forwardedArgs] : forwardedArgs)
|
|
171
|
+
: (sessionId ? ["--resume", sessionId, ...forwardedArgs] : forwardedArgs);
|
|
172
|
+
const child = spawn(bin, finalArgs, {
|
|
173
|
+
cwd: cfg.projectDir,
|
|
174
|
+
env: { ...process.env, ...(cfg.env || {}) },
|
|
175
|
+
stdio: "inherit",
|
|
176
|
+
});
|
|
145
177
|
child.on("close", async (code) => {
|
|
146
178
|
cleanup();
|
|
147
|
-
if (sessionId) await notifyTelegram(configPath, sessionId);
|
|
179
|
+
if (sessionId) await notifyTelegram(configPath, provider, sessionId);
|
|
148
180
|
process.exit(signalExitCode ?? code ?? 0);
|
|
149
181
|
});
|
|
182
|
+
child.on("error", (e) => {
|
|
183
|
+
cleanup();
|
|
184
|
+
process.stderr.write(`ctb: failed to start ${provider}: ${e.message}\n`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
|
150
187
|
}
|
|
151
188
|
|
|
152
|
-
async function summarizeSession(sid, lang) {
|
|
189
|
+
async function summarizeSession(provider, sid, lang, cfg) {
|
|
153
190
|
const langInstruction = lang && lang.startsWith("ko")
|
|
154
191
|
? "방금 로컬 터미널 코딩 세션이 끝났어. 이 대화에서 가장 최근에 나눈 내용(이 메시지 직전까지)을 바탕으로, 그 세션에서 무엇을 했는지 한국어로 짧은 구문(10단어 이내)으로 요약해줘. 마크다운 없이 텍스트만. 중요한 작업이 없었으면 정확히 이렇게만 답해: SKIP"
|
|
155
192
|
: "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
193
|
return new Promise((resolve) => {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"
|
|
161
|
-
|
|
194
|
+
const isCodex = provider === "codex";
|
|
195
|
+
const bin = isCodex ? (cfg.codexBin || "codex") : (cfg.claudeBin || "claude");
|
|
196
|
+
const args = isCodex
|
|
197
|
+
? ["exec", "resume", "--json", sid, langInstruction]
|
|
198
|
+
: ["--resume", sid, "-p", langInstruction, "--output-format", "json"];
|
|
199
|
+
const child = spawn(bin, args, {
|
|
200
|
+
cwd: cfg.projectDir,
|
|
201
|
+
env: { ...process.env, ...(cfg.env || {}) },
|
|
202
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
203
|
+
});
|
|
162
204
|
let out = "";
|
|
163
205
|
child.stdout.on("data", (d) => { out += d; });
|
|
164
206
|
child.on("close", () => {
|
|
165
207
|
try {
|
|
166
|
-
|
|
208
|
+
let text = "";
|
|
209
|
+
if (isCodex) {
|
|
210
|
+
for (const line of out.split("\n")) {
|
|
211
|
+
try {
|
|
212
|
+
const event = JSON.parse(line);
|
|
213
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message") text = event.item.text || text;
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
text = JSON.parse(out).result || "";
|
|
218
|
+
}
|
|
219
|
+
text = text.trim();
|
|
167
220
|
resolve(/^skip$/i.test(text) ? null : text);
|
|
168
221
|
} catch { resolve(null); }
|
|
169
222
|
});
|
|
@@ -172,13 +225,13 @@ async function summarizeSession(sid, lang) {
|
|
|
172
225
|
});
|
|
173
226
|
}
|
|
174
227
|
|
|
175
|
-
async function notifyTelegram(configPath, sessionId) {
|
|
228
|
+
async function notifyTelegram(configPath, provider, sessionId) {
|
|
176
229
|
try {
|
|
177
230
|
const cfg = JSON.parse(readFileSync(configPath, "utf8"));
|
|
178
231
|
if (!cfg.token || !cfg.allowedChatId || cfg.ctbNotify === false) return;
|
|
179
232
|
const lang = cfg.lang || process.env.LANG || "";
|
|
180
233
|
process.stderr.write("ctb: summarizing session...\n");
|
|
181
|
-
const summary = await summarizeSession(sessionId, lang);
|
|
234
|
+
const summary = await summarizeSession(provider, sessionId, lang, cfg);
|
|
182
235
|
if (!summary) { process.stderr.write("ctb: nothing to summarize (SKIP)\n"); return; }
|
|
183
236
|
process.stderr.write(`ctb: sending to Telegram — ${summary}\n`);
|
|
184
237
|
const label = lang.startsWith("ko") ? "[터미널]" : "[local]";
|
|
@@ -194,4 +247,7 @@ async function notifyTelegram(configPath, sessionId) {
|
|
|
194
247
|
}
|
|
195
248
|
}
|
|
196
249
|
|
|
197
|
-
main()
|
|
250
|
+
main().catch((e) => {
|
|
251
|
+
process.stderr.write(`ctb: ${e.message}\n`);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
});
|
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.1",
|
|
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",
|