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.
@@ -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": "bypassPermissions",
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] [...claude args] Run Claude, resuming the shared Telegram session
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 Claude runs, .claude-bot/local.lock (PID) is created so the bot defers
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] Resume Telegram session and run Claude\n` +
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 Claude, continuing the Telegram session\n` +
77
- ` ctb -p "what did we do?" Headless Claude with session context\n` +
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 Claude, resuming the bot's session
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 claudeArgs = looksLikeConfig ? args.slice(1) : args;
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")).sessionId;
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
- 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
- });
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 finalArgs = sessionId ? ["--resume", sessionId, ...claudeArgs] : claudeArgs;
144
- const child = spawn("claude", finalArgs, { stdio: "inherit" });
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 child = spawn("claude", [
158
- "--resume", sid,
159
- "-p", langInstruction,
160
- "--output-format", "json",
161
- ], { stdio: ["ignore", "pipe", "ignore"] });
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
- const text = (JSON.parse(out).result || "").trim();
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.3.40",
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.",
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",