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.
@@ -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,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] 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` +
@@ -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 Claude, resuming the bot's session
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 claudeArgs = looksLikeConfig ? args.slice(1) : args;
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")).sessionId;
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
- 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
- });
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 finalArgs = sessionId ? ["--resume", sessionId, ...claudeArgs] : claudeArgs;
144
- const child = spawn("claude", finalArgs, { stdio: "inherit" });
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 child = spawn("claude", [
158
- "--resume", sid,
159
- "-p", langInstruction,
160
- "--output-format", "json",
161
- ], { stdio: ["ignore", "pipe", "ignore"] });
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
- const text = (JSON.parse(out).result || "").trim();
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.3.36",
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.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",