@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.
- package/CHANGELOG.md +11 -0
- package/README.md +26 -16
- package/bot.js +197 -25
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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"
|
|
1679
|
-
|
|
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 —
|
|
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
|
-
|
|
1717
|
-
|
|
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.
|
|
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"
|