@inetafrica/open-claudia 1.14.9 → 1.16.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +26 -16
  3. package/bot.js +197 -25
  4. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.16.0
4
+ - OpenAI Codex backend support: `/codex` to switch, `/backend` picker now has 3 options
5
+ - Auto-detects `codex` binary via `which codex` at startup; opt-in via `CODEX_PATH` in .env
6
+ - Codex sessions persist (`codexSessionId`) across restarts and resume via `codex exec resume <id>`
7
+ - Per-backend session IDs maintained independently — switch freely without losing context
8
+ - Stream parser handles Codex JSONL events: `thread.started`, `item.started`, `item.completed`, `turn.completed` (with usage)
9
+ - Plan mode on Codex maps to `--sandbox read-only`; outside plan mode uses `--dangerously-bypass-approvals-and-sandbox` (bot is the sandbox)
10
+ - `/model` picker shows Codex models (gpt-5, gpt-5-codex, o3, o4-mini) when on Codex
11
+ - Effort/budget gracefully reject on Codex (not exposed as CLI flags); Claude auth preflight scoped strictly to the Claude backend
12
+ - Codex auth errors in stderr produce an actionable "run `codex login`" recovery message
13
+
3
14
  ## v1.14.9
4
15
  - Fix `/upgrade`: `latest` was const-scoped to the version-check try
5
16
  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,
@@ -274,6 +285,7 @@ bot.setMyCommands([
274
285
  { command: "ask", description: "Toggle ask mode (Cursor only)" },
275
286
  { command: "sessions", description: "List conversations for this project" },
276
287
  { command: "compact", description: "Summarize conversation context" },
288
+ { command: "usage", description: "Show token usage & cost for this session" },
277
289
  { command: "continue", description: "Resume last conversation" },
278
290
  { command: "worktree", description: "Toggle isolated git branch" },
279
291
  { command: "cron", description: "Manage scheduled tasks" },
@@ -282,6 +294,7 @@ bot.setMyCommands([
282
294
  { command: "status", description: "Session & settings info" },
283
295
  { command: "cursor", description: "Switch to Cursor Agent backend" },
284
296
  { command: "claude", description: "Switch to Claude Code backend" },
297
+ { command: "codex", description: "Switch to OpenAI Codex backend" },
285
298
  { command: "backend", description: "Show/switch active backend" },
286
299
  { command: "auth_status", description: "Check Claude Code auth" },
287
300
  { command: "login", description: "Start Claude Code login" },
@@ -324,7 +337,9 @@ function saveState() {
324
337
  currentSession,
325
338
  lastSessionId,
326
339
  cursorSessionId,
340
+ codexSessionId,
327
341
  settings,
342
+ sessionUsage,
328
343
  };
329
344
  try { fs.writeFileSync(STATE_FILE, JSON.stringify(data)); } catch (e) {}
330
345
  }
@@ -393,6 +408,7 @@ let streamBuffer = "";
393
408
  let streamInterval = null;
394
409
  let lastSessionId = savedState.lastSessionId || null;
395
410
  let cursorSessionId = savedState.cursorSessionId || null;
411
+ let codexSessionId = savedState.codexSessionId || null;
396
412
  let messageQueue = [];
397
413
  let activeCrons = new Map();
398
414
  let pendingVaultUnlock = false; // Waiting for password
@@ -402,6 +418,20 @@ let pendingClaudeAuthLabel = null;
402
418
  let isFirstMessage = !lastSessionId; // Track if this is first message in session
403
419
  let lastInputWasVoice = false; // Reply with voice when input was voice
404
420
 
421
+ // Per-session usage tracking (resets on /end or new session)
422
+ let sessionUsage = savedState.sessionUsage || {
423
+ turns: 0,
424
+ inputTokens: 0,
425
+ outputTokens: 0,
426
+ cacheReadTokens: 0,
427
+ cacheCreationTokens: 0,
428
+ costUsd: 0,
429
+ lastInputTokens: 0, // Last turn's input tokens (for compact suggestion)
430
+ };
431
+ function resetSessionUsage() {
432
+ sessionUsage = { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0 };
433
+ }
434
+
405
435
  let settings = savedState.settings || {
406
436
  model: null, effort: null, budget: null, permissionMode: null, worktree: false, backend: "claude",
407
437
  };
@@ -949,7 +979,7 @@ function claudeAuthRecoveryMessage(reason = "Claude Code authentication failed")
949
979
  }
950
980
 
951
981
  function preflightClaudeAuthMessage() {
952
- if (settings.backend === "cursor") return null;
982
+ if (settings.backend !== "claude") return null;
953
983
  if (getClaudeOAuthToken().value) return null;
954
984
  try {
955
985
  const output = execSync(`"${CLAUDE_PATH}" auth status`, {
@@ -1144,6 +1174,7 @@ function parseStreamEvents(data) {
1144
1174
 
1145
1175
  function buildClaudeArgs(prompt, opts = {}) {
1146
1176
  if (settings.backend === "cursor") return buildCursorArgs(prompt, opts);
1177
+ if (settings.backend === "codex") return buildCodexArgs(prompt, opts);
1147
1178
  const args = ["-p", "--verbose", "--output-format", "stream-json",
1148
1179
  "--append-system-prompt", buildSystemPrompt()];
1149
1180
  if (opts.continueSession) args.push("--continue");
@@ -1170,13 +1201,40 @@ function buildCursorArgs(prompt, opts = {}) {
1170
1201
  return args;
1171
1202
  }
1172
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
+
1173
1228
  function getActiveBinary() {
1174
1229
  if (settings.backend === "cursor") return resolvedCursorPath;
1230
+ if (settings.backend === "codex") return resolvedCodexPath;
1175
1231
  return CLAUDE_PATH;
1176
1232
  }
1177
1233
 
1178
1234
  function getActiveSessionId() {
1179
- return settings.backend === "cursor" ? cursorSessionId : lastSessionId;
1235
+ if (settings.backend === "cursor") return cursorSessionId;
1236
+ if (settings.backend === "codex") return codexSessionId;
1237
+ return lastSessionId;
1180
1238
  }
1181
1239
 
1182
1240
  async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
@@ -1321,9 +1379,59 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1321
1379
  currentToolDetail = "";
1322
1380
  }
1323
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
+ }
1324
1421
  if (evt.type === "result" && evt.session_id) {
1325
1422
  if (settings.backend === "cursor") { cursorSessionId = evt.session_id; }
1326
1423
  else { lastSessionId = evt.session_id; }
1424
+ if (evt.usage) {
1425
+ sessionUsage.turns += 1;
1426
+ sessionUsage.inputTokens += evt.usage.input_tokens || 0;
1427
+ sessionUsage.outputTokens += evt.usage.output_tokens || 0;
1428
+ sessionUsage.cacheReadTokens += evt.usage.cache_read_input_tokens || 0;
1429
+ sessionUsage.cacheCreationTokens += evt.usage.cache_creation_input_tokens || 0;
1430
+ sessionUsage.lastInputTokens = (evt.usage.input_tokens || 0) +
1431
+ (evt.usage.cache_read_input_tokens || 0) +
1432
+ (evt.usage.cache_creation_input_tokens || 0);
1433
+ }
1434
+ if (typeof evt.total_cost_usd === "number") sessionUsage.costUsd += evt.total_cost_usd;
1327
1435
  saveState();
1328
1436
  }
1329
1437
  if (evt.type === "result" && evt.result) assistantText = evt.result;
@@ -1343,7 +1451,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1343
1451
  clearTimeout(processTimeout);
1344
1452
 
1345
1453
  // Check for auth errors in stderr and give actionable Telegram recovery steps.
1346
- if (settings.backend !== "cursor" && isClaudeAuthErrorText(stderrBuffer)) {
1454
+ if (settings.backend === "claude" && isClaudeAuthErrorText(stderrBuffer)) {
1347
1455
  await send(claudeAuthRecoveryMessage(stderrBuffer.trim().slice(-800)), { replyTo: replyToMsgId });
1348
1456
  return;
1349
1457
  }
@@ -1351,6 +1459,10 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1351
1459
  await send("Cursor authentication error. Run `agent login` on this machine, then retry.", { replyTo: replyToMsgId });
1352
1460
  return;
1353
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
+ }
1354
1466
 
1355
1467
  try {
1356
1468
  if (code !== 0 && code !== null && !assistantText.trim()) {
@@ -1384,6 +1496,15 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1384
1496
  const voicePath = textToVoice(finalText);
1385
1497
  if (voicePath) await sendVoice(voicePath);
1386
1498
  }
1499
+
1500
+ // Suggest /compact when context gets large (once per threshold crossing)
1501
+ const ctx = sessionUsage.lastInputTokens;
1502
+ if (ctx > 150000 && !sessionUsage.warnedHighContext) {
1503
+ sessionUsage.warnedHighContext = true;
1504
+ await send(`Heads up: context is ${(ctx / 1000).toFixed(0)}k tokens. Use /compact to summarize or /end for a fresh start — keeps cost down.`);
1505
+ } else if (ctx < 80000 && sessionUsage.warnedHighContext) {
1506
+ sessionUsage.warnedHighContext = false;
1507
+ }
1387
1508
  } catch (e) {
1388
1509
  console.error("Final message delivery failed:", e.message);
1389
1510
  await send("Task completed but failed to deliver the response. Send /continue to see the result.");
@@ -1629,16 +1750,29 @@ bot.onText(/\/sessions$/, (msg) => {
1629
1750
 
1630
1751
  bot.onText(/\/model$/, (msg) => {
1631
1752
  if (!isAuthorized(msg)) return;
1632
- const keyboard = settings.backend === "cursor" ? [
1633
- [{ text: "Composer 2", callback_data: "m:composer-2" }, { text: "Composer 2 Fast", callback_data: "m:composer-2-fast" }],
1634
- [{ 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" }],
1635
- [{ text: "GPT-5.4", callback_data: "m:gpt-5.4-medium" }, { text: "GPT-5.4 High", callback_data: "m:gpt-5.4-high" }],
1636
- [{ text: "Auto", callback_data: "m:auto" }, { text: "Default", callback_data: "m:default" }],
1637
- ] : [
1638
- [{ 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" }],
1639
- [{ text: "Default", callback_data: "m:default" }],
1640
- ];
1641
- const label = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1753
+ let keyboard, label;
1754
+ if (settings.backend === "cursor") {
1755
+ keyboard = [
1756
+ [{ text: "Composer 2", callback_data: "m:composer-2" }, { text: "Composer 2 Fast", callback_data: "m:composer-2-fast" }],
1757
+ [{ 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" }],
1758
+ [{ text: "GPT-5.4", callback_data: "m:gpt-5.4-medium" }, { text: "GPT-5.4 High", callback_data: "m:gpt-5.4-high" }],
1759
+ [{ text: "Auto", callback_data: "m:auto" }, { text: "Default", callback_data: "m:default" }],
1760
+ ];
1761
+ label = "Cursor Agent";
1762
+ } else if (settings.backend === "codex") {
1763
+ keyboard = [
1764
+ [{ text: "gpt-5", callback_data: "m:gpt-5" }, { text: "gpt-5-codex", callback_data: "m:gpt-5-codex" }],
1765
+ [{ text: "o3", callback_data: "m:o3" }, { text: "o4-mini", callback_data: "m:o4-mini" }],
1766
+ [{ text: "Default", callback_data: "m:default" }],
1767
+ ];
1768
+ label = "Codex";
1769
+ } else {
1770
+ keyboard = [
1771
+ [{ 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" }],
1772
+ [{ text: "Default", callback_data: "m:default" }],
1773
+ ];
1774
+ label = "Claude Code";
1775
+ }
1642
1776
  send(`${label} model: ${settings.model || "default"}\n\nOr type /model <name> for any model.`, { keyboard: { inline_keyboard: keyboard } });
1643
1777
  });
1644
1778
  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"}`); });
@@ -1646,6 +1780,7 @@ bot.onText(/\/model (.+)/, (msg, match) => { if (!isAuthorized(msg)) return; set
1646
1780
  bot.onText(/\/effort$/, (msg) => {
1647
1781
  if (!isAuthorized(msg)) return;
1648
1782
  if (settings.backend === "cursor") return send("Effort levels are not supported on Cursor Agent.\nSwitch to Claude with /claude to use this.");
1783
+ 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.");
1649
1784
  send(`Effort: ${settings.effort || "default"}`, { keyboard: { inline_keyboard: [
1650
1785
  [{ text: "Low", callback_data: "e:low" }, { text: "Med", callback_data: "e:medium" }, { text: "High", callback_data: "e:high" }, { text: "Max", callback_data: "e:max" }],
1651
1786
  [{ text: "Default", callback_data: "e:default" }],
@@ -1654,12 +1789,14 @@ bot.onText(/\/effort$/, (msg) => {
1654
1789
  bot.onText(/\/effort (.+)/, (msg, match) => {
1655
1790
  if (!isAuthorized(msg)) return;
1656
1791
  if (settings.backend === "cursor") return send("Effort levels are not supported on Cursor Agent.");
1792
+ if (settings.backend === "codex") return send("Effort levels are not exposed as a flag on Codex.");
1657
1793
  const e = match[1].trim().toLowerCase(); settings.effort = ["low","medium","high","max"].includes(e) ? e : null; send(`Effort: ${settings.effort || "default"}`);
1658
1794
  });
1659
1795
 
1660
1796
  bot.onText(/\/budget$/, (msg) => {
1661
1797
  if (!isAuthorized(msg)) return;
1662
1798
  if (settings.backend === "cursor") return send("Budget limits are not supported on Cursor Agent.\nSwitch to Claude with /claude to use this.");
1799
+ if (settings.backend === "codex") return send("Budget limits are not supported on Codex.\nSwitch to Claude with /claude to use this.");
1663
1800
  send(`Budget: ${settings.budget ? "$" + settings.budget : "none"}`, { keyboard: { inline_keyboard: [
1664
1801
  [{ text: "$1", callback_data: "b:1" }, { text: "$5", callback_data: "b:5" }, { text: "$10", callback_data: "b:10" }, { text: "$25", callback_data: "b:25" }],
1665
1802
  [{ text: "No limit", callback_data: "b:none" }],
@@ -1668,6 +1805,7 @@ bot.onText(/\/budget$/, (msg) => {
1668
1805
  bot.onText(/\/budget (.+)/, (msg, match) => {
1669
1806
  if (!isAuthorized(msg)) return;
1670
1807
  if (settings.backend === "cursor") return send("Budget limits are not supported on Cursor Agent.");
1808
+ if (settings.backend === "codex") return send("Budget limits are not supported on Codex.");
1671
1809
  const v = parseFloat(match[1].replace("$","")); settings.budget = v > 0 ? v : null; send(`Budget: ${settings.budget ? "$"+settings.budget : "none"}`);
1672
1810
  });
1673
1811
 
@@ -1675,8 +1813,14 @@ bot.onText(/\/plan$/, (msg) => {
1675
1813
  if (!isAuthorized(msg)) return;
1676
1814
  const p = settings.permissionMode === "plan";
1677
1815
  settings.permissionMode = p ? null : "plan";
1678
- const label = settings.backend === "cursor" ? "read-only planning, no edits" : "plan permission mode";
1679
- if (p) cursorSessionId = null; // reset session so next message doesn't resume plan-mode session
1816
+ const label = settings.backend === "cursor" ? "read-only planning, no edits"
1817
+ : settings.backend === "codex" ? "read-only sandbox, no edits"
1818
+ : "plan permission mode";
1819
+ if (p) {
1820
+ // Reset session so next message doesn't resume in plan-mode for the active backend.
1821
+ if (settings.backend === "cursor") cursorSessionId = null;
1822
+ else if (settings.backend === "codex") codexSessionId = null;
1823
+ }
1680
1824
  send(p ? "Plan mode off (session reset)." : `Plan mode on (${label}).`);
1681
1825
  });
1682
1826
  bot.onText(/\/ask$/, (msg) => {
@@ -1697,7 +1841,7 @@ bot.onText(/\/cursor$/, async (msg) => {
1697
1841
  settings.model = null;
1698
1842
  saveState();
1699
1843
  const sid = cursorSessionId ? `\nSession: ${cursorSessionId.slice(0, 8)}...` : "\nNew session.";
1700
- send(`Switched to Cursor Agent.${sid}\n\n/claude — switch back`);
1844
+ send(`Switched to Cursor Agent.${sid}\n\n/claude — switch back · /codex — Codex`);
1701
1845
  });
1702
1846
 
1703
1847
  bot.onText(/\/claude$/, async (msg) => {
@@ -1706,15 +1850,26 @@ bot.onText(/\/claude$/, async (msg) => {
1706
1850
  settings.model = null;
1707
1851
  saveState();
1708
1852
  const sid = lastSessionId ? `\nSession: ${lastSessionId.slice(0, 8)}...` : "\nNew session.";
1709
- send(`Switched to Claude Code.${sid}\n\n/cursor — switch to Cursor`);
1853
+ send(`Switched to Claude Code.${sid}\n\n/cursor — Cursor · /codex — Codex`);
1854
+ });
1855
+
1856
+ bot.onText(/\/codex$/, async (msg) => {
1857
+ if (!isAuthorized(msg)) return;
1858
+ if (!resolvedCodexPath) return send("Codex CLI not found.\nInstall: npm install -g @openai/codex\nThen run `codex login` to authenticate.");
1859
+ settings.backend = "codex";
1860
+ settings.model = null;
1861
+ saveState();
1862
+ const sid = codexSessionId ? `\nSession: ${codexSessionId.slice(0, 8)}...` : "\nNew session.";
1863
+ send(`Switched to Codex.${sid}\n\n/claude — Claude · /cursor — Cursor`);
1710
1864
  });
1711
1865
 
1712
1866
  bot.onText(/\/backend$/, async (msg) => {
1713
1867
  if (!isAuthorized(msg)) return;
1714
- const label = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1868
+ const label = settings.backend === "cursor" ? "Cursor Agent" : settings.backend === "codex" ? "Codex" : "Claude Code";
1715
1869
  const cursorAvail = resolvedCursorPath ? "available" : "not found";
1716
- send(`Backend: ${label}\n\nClaude Code: available\nCursor Agent: ${cursorAvail}`, { keyboard: { inline_keyboard: [
1717
- [{ text: "Claude Code", callback_data: "be:claude" }, { text: "Cursor Agent", callback_data: "be:cursor" }],
1870
+ const codexAvail = resolvedCodexPath ? "available" : "not found";
1871
+ send(`Backend: ${label}\n\nClaude Code: available\nCursor Agent: ${cursorAvail}\nCodex: ${codexAvail}`, { keyboard: { inline_keyboard: [
1872
+ [{ text: "Claude Code", callback_data: "be:claude" }, { text: "Cursor Agent", callback_data: "be:cursor" }, { text: "Codex", callback_data: "be:codex" }],
1718
1873
  ] } });
1719
1874
  });
1720
1875
 
@@ -1747,7 +1902,7 @@ bot.onText(/\/stop/, async (msg) => {
1747
1902
  bot.onText(/\/status/, (msg) => {
1748
1903
  if (!isAuthorized(msg)) return;
1749
1904
  if (!currentSession) return send("No session.", { keyboard: { inline_keyboard: [[{ text: "Pick", callback_data: "show:projects" }]] } });
1750
- const backendLabel = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1905
+ const backendLabel = settings.backend === "cursor" ? "Cursor Agent" : settings.backend === "codex" ? "Codex" : "Claude Code";
1751
1906
  send([
1752
1907
  `Project: ${currentSession.name}`,
1753
1908
  `Backend: ${backendLabel}`,
@@ -1760,12 +1915,28 @@ bot.onText(/\/status/, (msg) => {
1760
1915
  bot.onText(/\/end/, (msg) => {
1761
1916
  if (!isAuthorized(msg)) return;
1762
1917
  if (currentSession) {
1763
- const n = currentSession.name; currentSession = null; lastSessionId = null; messageQueue = []; resetSettings();
1918
+ const n = currentSession.name; currentSession = null; lastSessionId = null; messageQueue = []; resetSettings(); resetSessionUsage();
1764
1919
  saveState();
1765
1920
  send(`Ended: ${n}`, { keyboard: { inline_keyboard: [[{ text: "New session", callback_data: "show:projects" }]] } });
1766
1921
  } else send("No session.");
1767
1922
  });
1768
1923
 
1924
+ bot.onText(/\/usage$/, (msg) => {
1925
+ if (!isAuthorized(msg)) return;
1926
+ const u = sessionUsage;
1927
+ if (u.turns === 0) return send("No usage yet in this session.");
1928
+ const fmt = (n) => n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n);
1929
+ const lines = [
1930
+ `*Session usage* (${u.turns} turn${u.turns === 1 ? "" : "s"})`,
1931
+ `Input: ${fmt(u.inputTokens)} · Output: ${fmt(u.outputTokens)}`,
1932
+ `Cache read: ${fmt(u.cacheReadTokens)} · Cache write: ${fmt(u.cacheCreationTokens)}`,
1933
+ `Cost: $${u.costUsd.toFixed(4)}`,
1934
+ `Last turn context: ${fmt(u.lastInputTokens)}`,
1935
+ ];
1936
+ if (u.lastInputTokens > 100000) lines.push("\nTip: context is large. Use /compact to summarize, or /end for a clean slate.");
1937
+ send(lines.join("\n"), { parseMode: "Markdown" });
1938
+ });
1939
+
1769
1940
  bot.onText(/\/soul$/, async (msg) => {
1770
1941
  if (!isAuthorized(msg)) return;
1771
1942
  const soul = loadSoul();
@@ -1965,23 +2136,24 @@ bot.on("callback_query", async (q) => {
1965
2136
  const sessions = loadSessions();
1966
2137
  // Don't delete history, just start fresh
1967
2138
  currentSession = { name: proj === "__workspace__" ? "Workspace" : proj, dir: proj === "__workspace__" ? WORKSPACE : path.join(WORKSPACE, proj) };
1968
- lastSessionId = null; isFirstMessage = true; messageQueue = []; resetSettings();
2139
+ lastSessionId = null; isFirstMessage = true; messageQueue = []; resetSettings(); resetSessionUsage();
1969
2140
  saveState();
1970
2141
  await send(`Session: ${currentSession.name}\nNew conversation\n\nSend text, voice, or images.\n\n/sessions — switch conversation\n/session — switch project`);
1971
2142
  return;
1972
2143
  }
1973
2144
  if (d === "a:continue") { if (currentSession) await runClaude("continue", currentSession.dir); else send("No session."); return; }
1974
- if (d === "a:end") { if (currentSession) { const n = currentSession.name; currentSession = null; lastSessionId = null; messageQueue = []; resetSettings(); saveState(); await send(`Ended: ${n}`, { keyboard: { inline_keyboard: [[{ text: "New", callback_data: "show:projects" }]] } }); } return; }
2145
+ 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; }
1975
2146
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
1976
2147
  if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
1977
2148
  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; }
1978
2149
  if (d.startsWith("be:")) {
1979
2150
  const be = d.slice(3);
1980
2151
  if (be === "cursor" && !resolvedCursorPath) { await send("Cursor Agent CLI not found."); return; }
2152
+ if (be === "codex" && !resolvedCodexPath) { await send("Codex CLI not found. Install: npm install -g @openai/codex"); return; }
1981
2153
  settings.backend = be;
1982
2154
  settings.model = null;
1983
2155
  saveState();
1984
- const label = be === "cursor" ? "Cursor Agent" : "Claude Code";
2156
+ const label = be === "cursor" ? "Cursor Agent" : be === "codex" ? "Codex" : "Claude Code";
1985
2157
  await send(`Switched to ${label}.`);
1986
2158
  return;
1987
2159
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.14.9",
4
- "description": "Your always-on AI coding assistant — Claude Code via Telegram",
3
+ "version": "1.16.0",
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"