@inetafrica/open-claudia 1.15.0 → 1.16.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +26 -16
  3. package/bot.js +175 -23
  4. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.16.1
4
+ - `/model` is now a single unified picker — all Claude, Cursor, and Codex models in one keyboard, with section dividers per backend
5
+ - Tapping any model auto-switches the active backend if needed (e.g. picking gpt-5 from Claude flips you to Codex). No more `/codex` + `/model` two-step
6
+ - Only backends with a detected CLI binary appear in the picker (Cursor/Codex rows hidden if not installed)
7
+
8
+ ## v1.16.0
9
+ - OpenAI Codex backend support: `/codex` to switch, `/backend` picker now has 3 options
10
+ - Auto-detects `codex` binary via `which codex` at startup; opt-in via `CODEX_PATH` in .env
11
+ - Codex sessions persist (`codexSessionId`) across restarts and resume via `codex exec resume <id>`
12
+ - Per-backend session IDs maintained independently — switch freely without losing context
13
+ - Stream parser handles Codex JSONL events: `thread.started`, `item.started`, `item.completed`, `turn.completed` (with usage)
14
+ - Plan mode on Codex maps to `--sandbox read-only`; outside plan mode uses `--dangerously-bypass-approvals-and-sandbox` (bot is the sandbox)
15
+ - `/model` picker shows Codex models (gpt-5, gpt-5-codex, o3, o4-mini) when on Codex
16
+ - Effort/budget gracefully reject on Codex (not exposed as CLI flags); Claude auth preflight scoped strictly to the Claude backend
17
+ - Codex auth errors in stderr produce an actionable "run `codex login`" recovery message
18
+
3
19
  ## v1.14.9
4
20
  - Fix `/upgrade`: `latest` was const-scoped to the version-check try
5
21
  block, so the install step ran with `latest` undefined and threw
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Open Claudia
2
2
 
3
- Your always-on AI coding assistant — Claude Code and Cursor Agent via Telegram.
3
+ Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram.
4
4
 
5
5
  Send text, voice notes, screenshots, and files from your phone. Your chosen AI agent works on your projects and reports back.
6
6
 
7
7
  ## Features
8
8
 
9
- - **Multi-backend** — switch between Claude Code and Cursor Agent on the fly (`/cursor`, `/claude`)
9
+ - **Multi-backend** — switch between Claude Code, Cursor Agent, and OpenAI Codex on the fly (`/claude`, `/cursor`, `/codex`)
10
10
  - **Multi-project sessions** — switch between workspace projects
11
11
  - **Per-project conversation history** — auto-resumes last conversation, switch with `/sessions`
12
12
  - **Separate session persistence** — Claude and Cursor each maintain their own conversation state
@@ -55,6 +55,14 @@ agent login # Opens browser to authenticate
55
55
  agent status # Verify: should show your email and plan
56
56
  ```
57
57
 
58
+ **OpenAI Codex** (optional — enables `/codex` backend):
59
+
60
+ ```bash
61
+ npm install -g @openai/codex
62
+ codex login # Opens browser to authenticate
63
+ codex --version # Verify it works
64
+ ```
65
+
58
66
  > **Important**: Claude Code can use macOS Keychain when you log in interactively, but a launchd/background bot may not be able to read that Keychain session. Open Claudia v1.14.0 adds Telegram auth helpers and supports `CLAUDE_CODE_OAUTH_TOKEN` for non-interactive Claude runs. Prefer `/setup_token` then `/use_oauth_token` if Telegram shows Claude auth/keychain errors.
59
67
 
60
68
  ### 2. Install Open Claudia
@@ -103,6 +111,7 @@ If installed as a background service, the bot starts automatically on login and
103
111
  |---------|-------------|
104
112
  | `/cursor` | Switch to Cursor Agent backend |
105
113
  | `/claude` | Switch to Claude Code backend |
114
+ | `/codex` | Switch to OpenAI Codex backend |
106
115
  | `/backend` | Show current backend with picker |
107
116
 
108
117
  Each backend keeps its own persistent session. Switching doesn't lose your place — you can go back and forth freely.
@@ -167,20 +176,20 @@ Tokens are redacted from Telegram output and logs. If the encrypted vault is unl
167
176
 
168
177
  ## Backend Comparison
169
178
 
170
- | | Claude Code | Cursor Agent |
171
- |---|---|---|
172
- | Binary | `claude` | `agent` |
173
- | Session flag | `--resume <id>` | `--resume <id>` |
174
- | Auth | `claude auth` | `agent login` |
175
- | Plan mode | Yes (`--permission-mode plan`) | Yes (`--mode plan`) |
176
- | Ask mode | No | Yes (`--mode ask`) |
177
- | Budget control | Yes (`--max-budget-usd`) | No |
178
- | Effort levels | Yes | No |
179
- | Worktree | Yes (`--worktree`) | Yes (`--worktree`) |
180
- | Model switching | Yes | Yes |
181
- | Dangerously skip permissions | Yes | Yes (`--trust`) |
182
-
183
- Both backends output `stream-json` which Open Claudia parses for real-time progress updates.
179
+ | | Claude Code | Cursor Agent | OpenAI Codex |
180
+ |---|---|---|---|
181
+ | Binary | `claude` | `agent` | `codex` |
182
+ | Session flag | `--resume <id>` | `--resume <id>` | `exec resume <id>` |
183
+ | Auth | `claude auth` | `agent login` | `codex login` |
184
+ | Plan mode | Yes (`--permission-mode plan`) | Yes (`--mode plan`) | Yes (`--sandbox read-only`) |
185
+ | Ask mode | No | Yes (`--mode ask`) | No |
186
+ | Budget control | Yes (`--max-budget-usd`) | No | No |
187
+ | Effort levels | Yes | No | Via config.toml |
188
+ | Worktree | Yes (`--worktree`) | Yes (`--worktree`) | No |
189
+ | Model switching | Yes | Yes | Yes (`--model`) |
190
+ | Dangerously skip permissions | Yes | Yes (`--trust`) | Yes (`--dangerously-bypass-approvals-and-sandbox`) |
191
+
192
+ All three backends output structured JSON which Open Claudia parses for real-time progress updates (Claude/Cursor: `stream-json`; Codex: JSONL events).
184
193
 
185
194
  ## Sending Files
186
195
 
@@ -258,6 +267,7 @@ All stored in `~/.open-claudia/`:
258
267
  | `WORKSPACE` | Yes | Path to your projects directory |
259
268
  | `CLAUDE_PATH` | Yes | Path to Claude Code CLI binary |
260
269
  | `CURSOR_PATH` | No | Path to Cursor Agent CLI binary (auto-detected if in PATH) |
270
+ | `CODEX_PATH` | No | Path to OpenAI Codex CLI binary (auto-detected if in PATH) |
261
271
  | `WHISPER_CLI` | No | Path to whisper.cpp binary |
262
272
  | `WHISPER_MODEL` | No | Whisper model to use |
263
273
  | `FFMPEG` | No | Path to ffmpeg binary |
package/bot.js CHANGED
@@ -143,6 +143,7 @@ const CHAT_ID = CHAT_IDS[0]; // Primary owner chat for crons/notifications
143
143
  const WORKSPACE = config.WORKSPACE;
144
144
  const CLAUDE_PATH = config.CLAUDE_PATH;
145
145
  const CURSOR_PATH = config.CURSOR_PATH || null;
146
+ const CODEX_PATH = config.CODEX_PATH || null;
146
147
 
147
148
  // Validate critical config at startup
148
149
  if (!TOKEN) { console.error("TELEGRAM_BOT_TOKEN not set"); process.exit(1); }
@@ -180,6 +181,15 @@ if (!resolvedCursorPath) {
180
181
  } catch (e) { resolvedCursorPath = null; }
181
182
  }
182
183
  if (resolvedCursorPath) console.log(`Cursor Agent CLI: ${resolvedCursorPath}`);
184
+
185
+ // Resolve Codex CLI (optional — discovered at startup)
186
+ let resolvedCodexPath = CODEX_PATH;
187
+ if (!resolvedCodexPath) {
188
+ try {
189
+ resolvedCodexPath = execSync("which codex 2>/dev/null", { encoding: "utf-8" }).trim() || null;
190
+ } catch (e) { resolvedCodexPath = null; }
191
+ }
192
+ if (resolvedCodexPath) console.log(`Codex CLI: ${resolvedCodexPath}`);
183
193
  const WHISPER_CLI = config.WHISPER_CLI || "";
184
194
  const WHISPER_MODEL = config.WHISPER_MODEL || "";
185
195
  const FFMPEG = config.FFMPEG || "";
@@ -193,6 +203,7 @@ const BOT_DIR = __dirname;
193
203
  const FULL_PATH = [
194
204
  path.dirname(CLAUDE_PATH),
195
205
  resolvedCursorPath ? path.dirname(resolvedCursorPath) : null,
206
+ resolvedCodexPath ? path.dirname(resolvedCodexPath) : null,
196
207
  path.dirname(process.execPath),
197
208
  FFMPEG ? path.dirname(FFMPEG) : null,
198
209
  WHISPER_CLI ? path.dirname(WHISPER_CLI) : null,
@@ -283,6 +294,7 @@ bot.setMyCommands([
283
294
  { command: "status", description: "Session & settings info" },
284
295
  { command: "cursor", description: "Switch to Cursor Agent backend" },
285
296
  { command: "claude", description: "Switch to Claude Code backend" },
297
+ { command: "codex", description: "Switch to OpenAI Codex backend" },
286
298
  { command: "backend", description: "Show/switch active backend" },
287
299
  { command: "auth_status", description: "Check Claude Code auth" },
288
300
  { command: "login", description: "Start Claude Code login" },
@@ -325,6 +337,7 @@ function saveState() {
325
337
  currentSession,
326
338
  lastSessionId,
327
339
  cursorSessionId,
340
+ codexSessionId,
328
341
  settings,
329
342
  sessionUsage,
330
343
  };
@@ -395,6 +408,7 @@ let streamBuffer = "";
395
408
  let streamInterval = null;
396
409
  let lastSessionId = savedState.lastSessionId || null;
397
410
  let cursorSessionId = savedState.cursorSessionId || null;
411
+ let codexSessionId = savedState.codexSessionId || null;
398
412
  let messageQueue = [];
399
413
  let activeCrons = new Map();
400
414
  let pendingVaultUnlock = false; // Waiting for password
@@ -965,7 +979,7 @@ function claudeAuthRecoveryMessage(reason = "Claude Code authentication failed")
965
979
  }
966
980
 
967
981
  function preflightClaudeAuthMessage() {
968
- if (settings.backend === "cursor") return null;
982
+ if (settings.backend !== "claude") return null;
969
983
  if (getClaudeOAuthToken().value) return null;
970
984
  try {
971
985
  const output = execSync(`"${CLAUDE_PATH}" auth status`, {
@@ -1160,6 +1174,7 @@ function parseStreamEvents(data) {
1160
1174
 
1161
1175
  function buildClaudeArgs(prompt, opts = {}) {
1162
1176
  if (settings.backend === "cursor") return buildCursorArgs(prompt, opts);
1177
+ if (settings.backend === "codex") return buildCodexArgs(prompt, opts);
1163
1178
  const args = ["-p", "--verbose", "--output-format", "stream-json",
1164
1179
  "--append-system-prompt", buildSystemPrompt()];
1165
1180
  if (opts.continueSession) args.push("--continue");
@@ -1186,13 +1201,40 @@ function buildCursorArgs(prompt, opts = {}) {
1186
1201
  return args;
1187
1202
  }
1188
1203
 
1204
+ function buildCodexArgs(prompt, opts = {}) {
1205
+ // Codex uses `exec [SESSION] [PROMPT]` or `exec resume <SESSION> [PROMPT]`.
1206
+ // Sandbox: bot runs headless, so bypass approvals (mirrors --dangerously-skip-permissions).
1207
+ // Plan mode: drop to read-only sandbox so the model can't write.
1208
+ const args = [];
1209
+ const resumeId = (!opts.fresh && !opts.continueSession) ? codexSessionId : null;
1210
+ if (opts.continueSession && codexSessionId) {
1211
+ args.push("exec", "resume", codexSessionId);
1212
+ } else if (resumeId) {
1213
+ args.push("exec", "resume", resumeId);
1214
+ } else {
1215
+ args.push("exec");
1216
+ }
1217
+ args.push("--json", "--skip-git-repo-check");
1218
+ if (settings.permissionMode === "plan") {
1219
+ args.push("--sandbox", "read-only");
1220
+ } else {
1221
+ args.push("--dangerously-bypass-approvals-and-sandbox");
1222
+ }
1223
+ if (settings.model) args.push("--model", settings.model);
1224
+ args.push(prompt);
1225
+ return args;
1226
+ }
1227
+
1189
1228
  function getActiveBinary() {
1190
1229
  if (settings.backend === "cursor") return resolvedCursorPath;
1230
+ if (settings.backend === "codex") return resolvedCodexPath;
1191
1231
  return CLAUDE_PATH;
1192
1232
  }
1193
1233
 
1194
1234
  function getActiveSessionId() {
1195
- return settings.backend === "cursor" ? cursorSessionId : lastSessionId;
1235
+ if (settings.backend === "cursor") return cursorSessionId;
1236
+ if (settings.backend === "codex") return codexSessionId;
1237
+ return lastSessionId;
1196
1238
  }
1197
1239
 
1198
1240
  async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
@@ -1337,6 +1379,45 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1337
1379
  currentToolDetail = "";
1338
1380
  }
1339
1381
  }
1382
+ // Codex stream events
1383
+ if (evt.type === "thread.started" && evt.thread_id) {
1384
+ codexSessionId = evt.thread_id;
1385
+ saveState();
1386
+ }
1387
+ if (evt.type === "item.started" && evt.item) {
1388
+ const it = evt.item;
1389
+ if (it.type === "command_execution" && it.command) {
1390
+ currentTool = "Shell"; toolUses.push("Shell");
1391
+ currentToolDetail = String(it.command).slice(0, 80);
1392
+ } else if (it.type === "file_change" && (it.path || it.file_path)) {
1393
+ currentTool = "Edit"; toolUses.push("Edit");
1394
+ const p = it.path || it.file_path;
1395
+ currentToolDetail = String(p).split("/").slice(-2).join("/");
1396
+ } else if (it.type === "web_search" && it.query) {
1397
+ currentTool = "Web"; toolUses.push("Web");
1398
+ currentToolDetail = String(it.query).slice(0, 60);
1399
+ } else if (it.type) {
1400
+ currentTool = it.type; toolUses.push(it.type);
1401
+ currentToolDetail = "";
1402
+ }
1403
+ }
1404
+ if (evt.type === "item.completed" && evt.item) {
1405
+ const it = evt.item;
1406
+ if (it.type === "agent_message" && typeof it.text === "string") {
1407
+ assistantText += (assistantText ? "\n" : "") + it.text;
1408
+ }
1409
+ }
1410
+ if (evt.type === "turn.completed" && evt.usage && settings.backend === "codex") {
1411
+ sessionUsage.turns += 1;
1412
+ const input = evt.usage.input_tokens || 0;
1413
+ const cached = evt.usage.cached_input_tokens || 0;
1414
+ const output = evt.usage.output_tokens || 0;
1415
+ sessionUsage.inputTokens += input;
1416
+ sessionUsage.outputTokens += output;
1417
+ sessionUsage.cacheReadTokens += cached;
1418
+ sessionUsage.lastInputTokens = input + cached;
1419
+ saveState();
1420
+ }
1340
1421
  if (evt.type === "result" && evt.session_id) {
1341
1422
  if (settings.backend === "cursor") { cursorSessionId = evt.session_id; }
1342
1423
  else { lastSessionId = evt.session_id; }
@@ -1370,7 +1451,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1370
1451
  clearTimeout(processTimeout);
1371
1452
 
1372
1453
  // Check for auth errors in stderr and give actionable Telegram recovery steps.
1373
- if (settings.backend !== "cursor" && isClaudeAuthErrorText(stderrBuffer)) {
1454
+ if (settings.backend === "claude" && isClaudeAuthErrorText(stderrBuffer)) {
1374
1455
  await send(claudeAuthRecoveryMessage(stderrBuffer.trim().slice(-800)), { replyTo: replyToMsgId });
1375
1456
  return;
1376
1457
  }
@@ -1378,6 +1459,10 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1378
1459
  await send("Cursor authentication error. Run `agent login` on this machine, then retry.", { replyTo: replyToMsgId });
1379
1460
  return;
1380
1461
  }
1462
+ if (settings.backend === "codex" && /not (?:logged in|authenticated)|please (?:log|sign) in|401 unauthorized|invalid api key/i.test(stderrBuffer)) {
1463
+ await send("Codex authentication error. Run `codex login` on this machine, then retry.", { replyTo: replyToMsgId });
1464
+ return;
1465
+ }
1381
1466
 
1382
1467
  try {
1383
1468
  if (code !== 0 && code !== null && !assistantText.trim()) {
@@ -1665,23 +1750,50 @@ bot.onText(/\/sessions$/, (msg) => {
1665
1750
 
1666
1751
  bot.onText(/\/model$/, (msg) => {
1667
1752
  if (!isAuthorized(msg)) return;
1668
- const keyboard = settings.backend === "cursor" ? [
1669
- [{ text: "Composer 2", callback_data: "m:composer-2" }, { text: "Composer 2 Fast", callback_data: "m:composer-2-fast" }],
1670
- [{ text: "Opus 4.6 Thinking", callback_data: "m:claude-4.6-opus-high-thinking" }, { text: "Sonnet 4.6", callback_data: "m:claude-4.6-sonnet-medium" }],
1671
- [{ text: "GPT-5.4", callback_data: "m:gpt-5.4-medium" }, { text: "GPT-5.4 High", callback_data: "m:gpt-5.4-high" }],
1672
- [{ text: "Auto", callback_data: "m:auto" }, { text: "Default", callback_data: "m:default" }],
1673
- ] : [
1674
- [{ text: "Opus 4.7", callback_data: "m:claude-opus-4-7" }, { text: "Opus 4.6", callback_data: "m:claude-opus-4-6" }, { text: "Sonnet 4.6", callback_data: "m:claude-sonnet-4-6" }, { text: "Haiku", callback_data: "m:claude-haiku-4-5-20251001" }],
1675
- [{ text: "Default", callback_data: "m:default" }],
1676
- ];
1677
- const label = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1678
- send(`${label} model: ${settings.model || "default"}\n\nOr type /model <name> for any model.`, { keyboard: { inline_keyboard: keyboard } });
1753
+ // Unified picker: every model from every available backend in one keyboard.
1754
+ // Tapping a button switches backend (if needed) and sets the model.
1755
+ // Callback format: `mb:<backend>:<model>` (model may itself contain `-`).
1756
+ const rows = [];
1757
+ rows.push([{ text: "── Claude ──", callback_data: "noop" }]);
1758
+ rows.push([
1759
+ { text: "Opus 4.7", callback_data: "mb:claude:claude-opus-4-7" },
1760
+ { text: "Opus 4.6", callback_data: "mb:claude:claude-opus-4-6" },
1761
+ { text: "Sonnet 4.6", callback_data: "mb:claude:claude-sonnet-4-6" },
1762
+ { text: "Haiku", callback_data: "mb:claude:claude-haiku-4-5-20251001" },
1763
+ ]);
1764
+ if (resolvedCursorPath) {
1765
+ rows.push([{ text: "── Cursor ──", callback_data: "noop" }]);
1766
+ rows.push([
1767
+ { text: "Composer 2", callback_data: "mb:cursor:composer-2" },
1768
+ { text: "Composer 2 Fast", callback_data: "mb:cursor:composer-2-fast" },
1769
+ { text: "Auto", callback_data: "mb:cursor:auto" },
1770
+ ]);
1771
+ rows.push([
1772
+ { text: "Opus 4.6 Thinking", callback_data: "mb:cursor:claude-4.6-opus-high-thinking" },
1773
+ { text: "GPT-5.4", callback_data: "mb:cursor:gpt-5.4-medium" },
1774
+ ]);
1775
+ }
1776
+ if (resolvedCodexPath) {
1777
+ rows.push([{ text: "── Codex ──", callback_data: "noop" }]);
1778
+ rows.push([
1779
+ { text: "gpt-5", callback_data: "mb:codex:gpt-5" },
1780
+ { text: "gpt-5-codex", callback_data: "mb:codex:gpt-5-codex" },
1781
+ ]);
1782
+ rows.push([
1783
+ { text: "o3", callback_data: "mb:codex:o3" },
1784
+ { text: "o4-mini", callback_data: "mb:codex:o4-mini" },
1785
+ ]);
1786
+ }
1787
+ rows.push([{ text: "Default (current backend)", callback_data: "m:default" }]);
1788
+ const beLabel = settings.backend === "cursor" ? "Cursor" : settings.backend === "codex" ? "Codex" : "Claude";
1789
+ send(`Current: ${beLabel} · ${settings.model || "default"}\n\nPick a model — backend switches automatically.\nOr type /model <name>.`, { keyboard: { inline_keyboard: rows } });
1679
1790
  });
1680
1791
  bot.onText(/\/model (.+)/, (msg, match) => { if (!isAuthorized(msg)) return; settings.model = match[1].trim().toLowerCase(); if (settings.model === "default") settings.model = null; send(`Model: ${settings.model || "default"}`); });
1681
1792
 
1682
1793
  bot.onText(/\/effort$/, (msg) => {
1683
1794
  if (!isAuthorized(msg)) return;
1684
1795
  if (settings.backend === "cursor") return send("Effort levels are not supported on Cursor Agent.\nSwitch to Claude with /claude to use this.");
1796
+ if (settings.backend === "codex") return send("Effort levels are not exposed as a flag on Codex.\nSet `model_reasoning_effort` in ~/.codex/config.toml, or switch to /claude.");
1685
1797
  send(`Effort: ${settings.effort || "default"}`, { keyboard: { inline_keyboard: [
1686
1798
  [{ text: "Low", callback_data: "e:low" }, { text: "Med", callback_data: "e:medium" }, { text: "High", callback_data: "e:high" }, { text: "Max", callback_data: "e:max" }],
1687
1799
  [{ text: "Default", callback_data: "e:default" }],
@@ -1690,12 +1802,14 @@ bot.onText(/\/effort$/, (msg) => {
1690
1802
  bot.onText(/\/effort (.+)/, (msg, match) => {
1691
1803
  if (!isAuthorized(msg)) return;
1692
1804
  if (settings.backend === "cursor") return send("Effort levels are not supported on Cursor Agent.");
1805
+ if (settings.backend === "codex") return send("Effort levels are not exposed as a flag on Codex.");
1693
1806
  const e = match[1].trim().toLowerCase(); settings.effort = ["low","medium","high","max"].includes(e) ? e : null; send(`Effort: ${settings.effort || "default"}`);
1694
1807
  });
1695
1808
 
1696
1809
  bot.onText(/\/budget$/, (msg) => {
1697
1810
  if (!isAuthorized(msg)) return;
1698
1811
  if (settings.backend === "cursor") return send("Budget limits are not supported on Cursor Agent.\nSwitch to Claude with /claude to use this.");
1812
+ if (settings.backend === "codex") return send("Budget limits are not supported on Codex.\nSwitch to Claude with /claude to use this.");
1699
1813
  send(`Budget: ${settings.budget ? "$" + settings.budget : "none"}`, { keyboard: { inline_keyboard: [
1700
1814
  [{ text: "$1", callback_data: "b:1" }, { text: "$5", callback_data: "b:5" }, { text: "$10", callback_data: "b:10" }, { text: "$25", callback_data: "b:25" }],
1701
1815
  [{ text: "No limit", callback_data: "b:none" }],
@@ -1704,6 +1818,7 @@ bot.onText(/\/budget$/, (msg) => {
1704
1818
  bot.onText(/\/budget (.+)/, (msg, match) => {
1705
1819
  if (!isAuthorized(msg)) return;
1706
1820
  if (settings.backend === "cursor") return send("Budget limits are not supported on Cursor Agent.");
1821
+ if (settings.backend === "codex") return send("Budget limits are not supported on Codex.");
1707
1822
  const v = parseFloat(match[1].replace("$","")); settings.budget = v > 0 ? v : null; send(`Budget: ${settings.budget ? "$"+settings.budget : "none"}`);
1708
1823
  });
1709
1824
 
@@ -1711,8 +1826,14 @@ bot.onText(/\/plan$/, (msg) => {
1711
1826
  if (!isAuthorized(msg)) return;
1712
1827
  const p = settings.permissionMode === "plan";
1713
1828
  settings.permissionMode = p ? null : "plan";
1714
- const label = settings.backend === "cursor" ? "read-only planning, no edits" : "plan permission mode";
1715
- if (p) cursorSessionId = null; // reset session so next message doesn't resume plan-mode session
1829
+ const label = settings.backend === "cursor" ? "read-only planning, no edits"
1830
+ : settings.backend === "codex" ? "read-only sandbox, no edits"
1831
+ : "plan permission mode";
1832
+ if (p) {
1833
+ // Reset session so next message doesn't resume in plan-mode for the active backend.
1834
+ if (settings.backend === "cursor") cursorSessionId = null;
1835
+ else if (settings.backend === "codex") codexSessionId = null;
1836
+ }
1716
1837
  send(p ? "Plan mode off (session reset)." : `Plan mode on (${label}).`);
1717
1838
  });
1718
1839
  bot.onText(/\/ask$/, (msg) => {
@@ -1733,7 +1854,7 @@ bot.onText(/\/cursor$/, async (msg) => {
1733
1854
  settings.model = null;
1734
1855
  saveState();
1735
1856
  const sid = cursorSessionId ? `\nSession: ${cursorSessionId.slice(0, 8)}...` : "\nNew session.";
1736
- send(`Switched to Cursor Agent.${sid}\n\n/claude — switch back`);
1857
+ send(`Switched to Cursor Agent.${sid}\n\n/claude — switch back · /codex — Codex`);
1737
1858
  });
1738
1859
 
1739
1860
  bot.onText(/\/claude$/, async (msg) => {
@@ -1742,15 +1863,26 @@ bot.onText(/\/claude$/, async (msg) => {
1742
1863
  settings.model = null;
1743
1864
  saveState();
1744
1865
  const sid = lastSessionId ? `\nSession: ${lastSessionId.slice(0, 8)}...` : "\nNew session.";
1745
- send(`Switched to Claude Code.${sid}\n\n/cursor — switch to Cursor`);
1866
+ send(`Switched to Claude Code.${sid}\n\n/cursor — Cursor · /codex — Codex`);
1867
+ });
1868
+
1869
+ bot.onText(/\/codex$/, async (msg) => {
1870
+ if (!isAuthorized(msg)) return;
1871
+ if (!resolvedCodexPath) return send("Codex CLI not found.\nInstall: npm install -g @openai/codex\nThen run `codex login` to authenticate.");
1872
+ settings.backend = "codex";
1873
+ settings.model = null;
1874
+ saveState();
1875
+ const sid = codexSessionId ? `\nSession: ${codexSessionId.slice(0, 8)}...` : "\nNew session.";
1876
+ send(`Switched to Codex.${sid}\n\n/claude — Claude · /cursor — Cursor`);
1746
1877
  });
1747
1878
 
1748
1879
  bot.onText(/\/backend$/, async (msg) => {
1749
1880
  if (!isAuthorized(msg)) return;
1750
- const label = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1881
+ const label = settings.backend === "cursor" ? "Cursor Agent" : settings.backend === "codex" ? "Codex" : "Claude Code";
1751
1882
  const cursorAvail = resolvedCursorPath ? "available" : "not found";
1752
- send(`Backend: ${label}\n\nClaude Code: available\nCursor Agent: ${cursorAvail}`, { keyboard: { inline_keyboard: [
1753
- [{ text: "Claude Code", callback_data: "be:claude" }, { text: "Cursor Agent", callback_data: "be:cursor" }],
1883
+ const codexAvail = resolvedCodexPath ? "available" : "not found";
1884
+ send(`Backend: ${label}\n\nClaude Code: available\nCursor Agent: ${cursorAvail}\nCodex: ${codexAvail}`, { keyboard: { inline_keyboard: [
1885
+ [{ text: "Claude Code", callback_data: "be:claude" }, { text: "Cursor Agent", callback_data: "be:cursor" }, { text: "Codex", callback_data: "be:codex" }],
1754
1886
  ] } });
1755
1887
  });
1756
1888
 
@@ -1783,7 +1915,7 @@ bot.onText(/\/stop/, async (msg) => {
1783
1915
  bot.onText(/\/status/, (msg) => {
1784
1916
  if (!isAuthorized(msg)) return;
1785
1917
  if (!currentSession) return send("No session.", { keyboard: { inline_keyboard: [[{ text: "Pick", callback_data: "show:projects" }]] } });
1786
- const backendLabel = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1918
+ const backendLabel = settings.backend === "cursor" ? "Cursor Agent" : settings.backend === "codex" ? "Codex" : "Claude Code";
1787
1919
  send([
1788
1920
  `Project: ${currentSession.name}`,
1789
1921
  `Backend: ${backendLabel}`,
@@ -2024,16 +2156,36 @@ bot.on("callback_query", async (q) => {
2024
2156
  }
2025
2157
  if (d === "a:continue") { if (currentSession) await runClaude("continue", currentSession.dir); else send("No session."); return; }
2026
2158
  if (d === "a:end") { if (currentSession) { const n = currentSession.name; currentSession = null; lastSessionId = null; messageQueue = []; resetSettings(); resetSessionUsage(); saveState(); await send(`Ended: ${n}`, { keyboard: { inline_keyboard: [[{ text: "New", callback_data: "show:projects" }]] } }); } return; }
2159
+ if (d === "noop") { return; }
2160
+ if (d.startsWith("mb:")) {
2161
+ // Unified picker: switch backend + set model in one tap.
2162
+ // Format: mb:<backend>:<model>. Model may contain `:` itself (unlikely), so split on first two.
2163
+ const rest = d.slice(3);
2164
+ const colon = rest.indexOf(":");
2165
+ if (colon < 0) return;
2166
+ const be = rest.slice(0, colon);
2167
+ const model = rest.slice(colon + 1);
2168
+ if (be === "cursor" && !resolvedCursorPath) { await send("Cursor Agent CLI not found."); return; }
2169
+ if (be === "codex" && !resolvedCodexPath) { await send("Codex CLI not found. Install: npm install -g @openai/codex"); return; }
2170
+ const switched = settings.backend !== be;
2171
+ settings.backend = be;
2172
+ settings.model = model;
2173
+ saveState();
2174
+ const beLabel = be === "cursor" ? "Cursor Agent" : be === "codex" ? "Codex" : "Claude Code";
2175
+ await send(switched ? `Switched to ${beLabel}.\nModel: ${model}` : `Model: ${model}`);
2176
+ return;
2177
+ }
2027
2178
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
2028
2179
  if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
2029
2180
  if (d.startsWith("b:")) { const b = d.slice(2); settings.budget = b === "none" ? null : parseFloat(b); await send(`Budget: ${settings.budget ? "$"+settings.budget : "none"}`); return; }
2030
2181
  if (d.startsWith("be:")) {
2031
2182
  const be = d.slice(3);
2032
2183
  if (be === "cursor" && !resolvedCursorPath) { await send("Cursor Agent CLI not found."); return; }
2184
+ if (be === "codex" && !resolvedCodexPath) { await send("Codex CLI not found. Install: npm install -g @openai/codex"); return; }
2033
2185
  settings.backend = be;
2034
2186
  settings.model = null;
2035
2187
  saveState();
2036
- const label = be === "cursor" ? "Cursor Agent" : "Claude Code";
2188
+ const label = be === "cursor" ? "Cursor Agent" : be === "codex" ? "Codex" : "Claude Code";
2037
2189
  await send(`Switched to ${label}.`);
2038
2190
  return;
2039
2191
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.15.0",
4
- "description": "Your always-on AI coding assistant — Claude Code via Telegram",
3
+ "version": "1.16.1",
4
+ "description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram",
5
5
  "main": "bot.js",
6
6
  "bin": {
7
7
  "open-claudia": "./bin/cli.js"