@inetafrica/open-claudia 1.19.0 → 1.19.2

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.19.2
4
+ - Docker images now bake in the OpenAI Codex CLI (`@openai/codex`), so `/codex` and Codex auth commands are available in container/Kubernetes deployments after rollout.
5
+ - Documented that direct npm installs still need optional backend CLIs installed on the host, while Docker images include the Codex CLI.
6
+
7
+ ## v1.19.1
8
+ - `/auth` requests now send the owner Telegram Approve/Deny buttons, while preserving the existing `auth.json` and CLI approval flow.
9
+
3
10
  ## v1.19.0
4
11
  - Added `/doctor` / `/requirements`: mobile-friendly checks for Node.js, Claude/Cursor/Codex CLI versions and auth, optional ffmpeg/Whisper voice stack, workspace writability, and config dir writability
5
12
  - `/upgrade` now includes a post-upgrade requirements/auth summary before restarting, so missing CLIs or auth breakage is visible immediately
package/Dockerfile CHANGED
@@ -11,6 +11,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
11
11
  RUN curl -fsSL https://claude.ai/install.sh | sh || \
12
12
  npm install -g @anthropic-ai/claude-code
13
13
 
14
+ # Install OpenAI Codex CLI for the /codex backend
15
+ RUN npm install -g @openai/codex
16
+
14
17
  # Create non-root user (Claude Code refuses --dangerously-skip-permissions as root)
15
18
  # node:20-slim already has uid/gid 1000 (node user). Create claudia with different IDs.
16
19
  RUN groupadd -g 1001 claudia && useradd -u 1001 -g 1001 -m -d /data claudia
package/README.md CHANGED
@@ -66,6 +66,8 @@ codex login # Opens browser to authenticate
66
66
  codex --version # Verify it works
67
67
  ```
68
68
 
69
+ Docker images include the Codex CLI. Direct npm installs still need optional backend CLIs installed on the host.
70
+
69
71
  > **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.
70
72
 
71
73
  ### 2. Install Open Claudia
@@ -249,14 +251,14 @@ Voice notes are transcribed locally — nothing sent to external services.
249
251
  The setup owner is automatically authorized. To add more users:
250
252
 
251
253
  **From Telegram** (unauthorized users):
252
- - Send `/auth` to the bot — the owner gets notified
254
+ - Send `/auth` to the bot — the owner gets an Approve/Deny button prompt in Telegram
253
255
 
254
256
  **From the terminal** (owner):
255
257
  ```bash
256
258
  open-claudia auth
257
259
  ```
258
260
 
259
- This shows authorized chats, pending requests, and lets you approve/deny or add new users with code verification.
261
+ This shows authorized chats, pending requests, and lets you approve/deny or add new users with code verification. Telegram button approvals use the same `auth.json` authorization list.
260
262
 
261
263
  Authorized chats are separate from identity links. By default each Telegram chat uses `telegram:<chatId>` as its user id. To share one session/history across channels later, link the chat to a canonical id:
262
264
 
@@ -347,6 +349,8 @@ Send `/upgrade` to update. The bot will:
347
349
  2. Go offline briefly
348
350
  3. Restart and notify you it's back
349
351
 
352
+ For direct npm installs, `/upgrade` updates Open Claudia itself and does not install optional backend CLIs such as Codex or Cursor Agent. Container deployments should roll out a new Docker image when bundled CLI tools change.
353
+
350
354
  ## Cron Jobs
351
355
 
352
356
  Schedule recurring tasks:
package/bot-agent.js CHANGED
@@ -190,6 +190,7 @@ bot.setMyCommands([
190
190
  { command: "cursor", description: "Switch to Cursor Agent backend" },
191
191
  { command: "claude", description: "Switch to Claude Code backend" },
192
192
  { command: "backend", description: "Show/switch active backend" },
193
+ { command: "auth", description: "Request access to this bot" },
193
194
  { command: "auth_status", description: "Check Claude Code auth" },
194
195
  { command: "login", description: "Start Claude Code login" },
195
196
  { command: "setup_token", description: "Create Claude OAuth token" },
@@ -321,7 +322,7 @@ function isAuthorized(msg) {
321
322
  // Also check auth.json for dynamically added chats
322
323
  try {
323
324
  const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
324
- return auth.authorized.some((a) => a.chatId === chatId);
325
+ return Array.isArray(auth.authorized) && auth.authorized.some((a) => String(a.chatId) === chatId);
325
326
  } catch (e) {}
326
327
  return false;
327
328
  }
@@ -330,6 +331,69 @@ function isOwner(msg) {
330
331
  return String(msg.chat.id) === CHAT_ID;
331
332
  }
332
333
 
334
+ function loadAuth() {
335
+ try {
336
+ const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
337
+ return {
338
+ authorized: Array.isArray(auth.authorized) ? auth.authorized : [],
339
+ pending: Array.isArray(auth.pending) ? auth.pending : [],
340
+ };
341
+ } catch (e) {
342
+ return { authorized: [], pending: [] };
343
+ }
344
+ }
345
+
346
+ function saveAuth(auth) {
347
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
348
+ }
349
+
350
+ function authRequestLabel(request) {
351
+ return request.username ? `@${request.username}` : request.name || request.chatId;
352
+ }
353
+
354
+ function updateAuthorizedChatEnv(auth) {
355
+ const ids = new Set(CHAT_IDS);
356
+ for (const user of auth.authorized) {
357
+ if (user.chatId) ids.add(String(user.chatId));
358
+ }
359
+ saveEnvKey("TELEGRAM_CHAT_ID", [...ids].join(","));
360
+ }
361
+
362
+ async function approveAuthRequest(chatId) {
363
+ const auth = loadAuth();
364
+ if (auth.authorized.some((a) => String(a.chatId) === chatId)) {
365
+ auth.pending = auth.pending.filter((p) => String(p.chatId) !== chatId);
366
+ saveAuth(auth);
367
+ updateAuthorizedChatEnv(auth);
368
+ return { status: "already_authorized" };
369
+ }
370
+
371
+ const idx = auth.pending.findIndex((p) => String(p.chatId) === chatId);
372
+ if (idx < 0) return { status: "not_found" };
373
+
374
+ const approved = auth.pending.splice(idx, 1)[0];
375
+ auth.authorized.push({
376
+ chatId: approved.chatId,
377
+ name: approved.name,
378
+ username: approved.username,
379
+ isOwner: false,
380
+ authorizedAt: new Date().toISOString(),
381
+ });
382
+ saveAuth(auth);
383
+ updateAuthorizedChatEnv(auth);
384
+ return { status: "approved", request: approved };
385
+ }
386
+
387
+ async function denyAuthRequest(chatId) {
388
+ const auth = loadAuth();
389
+ const idx = auth.pending.findIndex((p) => String(p.chatId) === chatId);
390
+ if (idx < 0) return { status: "not_found" };
391
+
392
+ const denied = auth.pending.splice(idx, 1)[0];
393
+ saveAuth(auth);
394
+ return { status: "denied", request: denied };
395
+ }
396
+
333
397
  // ── Auth request handler (for unauthorized users) ──────────────────
334
398
  bot.onText(/\/auth$/, async (msg) => {
335
399
  if (isAuthorized(msg)) {
@@ -341,15 +405,10 @@ bot.onText(/\/auth$/, async (msg) => {
341
405
  const username = msg.from?.username || "";
342
406
 
343
407
  // Add to pending in auth.json
344
- let auth;
345
- try {
346
- auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
347
- } catch (e) {
348
- auth = { authorized: [], pending: [] };
349
- }
408
+ const auth = loadAuth();
350
409
 
351
410
  // Check if already pending
352
- if (auth.pending.some((p) => p.chatId === chatId)) {
411
+ if (auth.pending.some((p) => String(p.chatId) === chatId)) {
353
412
  bot.sendMessage(msg.chat.id, "Your request is already pending. The bot owner will review it.");
354
413
  return;
355
414
  }
@@ -360,13 +419,20 @@ bot.onText(/\/auth$/, async (msg) => {
360
419
  username,
361
420
  requestedAt: new Date().toISOString(),
362
421
  });
363
- fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
422
+ saveAuth(auth);
364
423
 
365
424
  bot.sendMessage(msg.chat.id, "Access requested! The bot owner will review your request.");
366
425
 
367
426
  // Notify owner
368
- const label = username ? `@${username}` : name;
369
- bot.sendMessage(CHAT_ID, `New auth request from ${label} (${chatId}).\nRun 'open-claudia auth' to approve or deny.`);
427
+ const label = authRequestLabel({ chatId, name, username });
428
+ bot.sendMessage(CHAT_ID, `New auth request from ${label} (${chatId}).`, {
429
+ reply_markup: {
430
+ inline_keyboard: [[
431
+ { text: "Approve", callback_data: `auth:approve:${chatId}` },
432
+ { text: "Deny", callback_data: `auth:deny:${chatId}` },
433
+ ]],
434
+ },
435
+ });
370
436
  });
371
437
 
372
438
  // ── Onboarding ──────────────────────────────────────────────────────
@@ -1893,6 +1959,58 @@ bot.on("callback_query", async (q) => {
1893
1959
  const d = q.data;
1894
1960
  await bot.answerCallbackQuery(q.id);
1895
1961
 
1962
+ if (d.startsWith("auth:")) {
1963
+ const callbackFromOwner = String(q.from?.id) === CHAT_ID || String(q.message?.chat?.id) === CHAT_ID;
1964
+ if (!callbackFromOwner) {
1965
+ await send("Owner only — auth approvals are restricted.");
1966
+ return;
1967
+ }
1968
+
1969
+ const [, action, chatId] = d.split(":");
1970
+ if (!chatId || !["approve", "deny"].includes(action)) return;
1971
+
1972
+ const result = action === "approve"
1973
+ ? await approveAuthRequest(chatId)
1974
+ : await denyAuthRequest(chatId);
1975
+
1976
+ if (result.status === "not_found") {
1977
+ await send(`No pending auth request found for ${chatId}.`);
1978
+ return;
1979
+ }
1980
+
1981
+ if (result.status === "already_authorized") {
1982
+ await send(`Chat ${chatId} is already authorized.`);
1983
+ return;
1984
+ }
1985
+
1986
+ const label = authRequestLabel(result.request);
1987
+ if (result.status === "approved") {
1988
+ await bot.sendMessage(chatId, "Your access has been approved! You can now use the bot. Send /start to begin.").catch(() => {});
1989
+ let edited = false;
1990
+ if (q.message) {
1991
+ edited = await bot.editMessageText(`Approved auth request from ${label} (${chatId}).`, {
1992
+ chat_id: q.message.chat.id,
1993
+ message_id: q.message.message_id,
1994
+ reply_markup: { inline_keyboard: [] },
1995
+ }).then(() => true).catch(() => false);
1996
+ }
1997
+ if (!edited) await send(`Approved ${label} (${chatId}).`);
1998
+ return;
1999
+ }
2000
+
2001
+ await bot.sendMessage(chatId, "Your access request was denied.").catch(() => {});
2002
+ let edited = false;
2003
+ if (q.message) {
2004
+ edited = await bot.editMessageText(`Denied auth request from ${label} (${chatId}).`, {
2005
+ chat_id: q.message.chat.id,
2006
+ message_id: q.message.message_id,
2007
+ reply_markup: { inline_keyboard: [] },
2008
+ }).then(() => true).catch(() => false);
2009
+ }
2010
+ if (!edited) await send(`Denied ${label} (${chatId}).`);
2011
+ return;
2012
+ }
2013
+
1896
2014
  // Onboarding style selection
1897
2015
  if (d.startsWith("ob:")) { finishOnboarding(d.slice(3)); return; }
1898
2016
 
package/bot.js CHANGED
@@ -306,6 +306,7 @@ bot.setMyCommands([
306
306
  { command: "backend", description: "Show/switch active backend" },
307
307
  { command: "doctor", description: "Check CLI requirements" },
308
308
  { command: "requirements", description: "Check CLI requirements" },
309
+ { command: "auth", description: "Request access to this bot" },
309
310
  { command: "link", description: "Link this chat to a canonical user id" },
310
311
  { command: "whoami", description: "Show your canonical user id" },
311
312
  { command: "auth_status", description: "Check Claude Code auth" },
@@ -652,7 +653,7 @@ function isAuthorized(msg) {
652
653
  // Also check auth.json for dynamically added chats
653
654
  try {
654
655
  const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
655
- return auth.authorized.some((a) => a.chatId === chatId);
656
+ return Array.isArray(auth.authorized) && auth.authorized.some((a) => String(a.chatId) === chatId);
656
657
  } catch (e) {}
657
658
  return false;
658
659
  }
@@ -661,6 +662,69 @@ function isOwner(msg) {
661
662
  return String(msg.chat.id) === CHAT_ID;
662
663
  }
663
664
 
665
+ function loadAuth() {
666
+ try {
667
+ const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
668
+ return {
669
+ authorized: Array.isArray(auth.authorized) ? auth.authorized : [],
670
+ pending: Array.isArray(auth.pending) ? auth.pending : [],
671
+ };
672
+ } catch (e) {
673
+ return { authorized: [], pending: [] };
674
+ }
675
+ }
676
+
677
+ function saveAuth(auth) {
678
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
679
+ }
680
+
681
+ function authRequestLabel(request) {
682
+ return request.username ? `@${request.username}` : request.name || request.chatId;
683
+ }
684
+
685
+ function updateAuthorizedChatEnv(auth) {
686
+ const ids = new Set(CHAT_IDS);
687
+ for (const user of auth.authorized) {
688
+ if (user.chatId) ids.add(String(user.chatId));
689
+ }
690
+ saveEnvKey("TELEGRAM_CHAT_ID", [...ids].join(","));
691
+ }
692
+
693
+ async function approveAuthRequest(chatId) {
694
+ const auth = loadAuth();
695
+ if (auth.authorized.some((a) => String(a.chatId) === chatId)) {
696
+ auth.pending = auth.pending.filter((p) => String(p.chatId) !== chatId);
697
+ saveAuth(auth);
698
+ updateAuthorizedChatEnv(auth);
699
+ return { status: "already_authorized" };
700
+ }
701
+
702
+ const idx = auth.pending.findIndex((p) => String(p.chatId) === chatId);
703
+ if (idx < 0) return { status: "not_found" };
704
+
705
+ const approved = auth.pending.splice(idx, 1)[0];
706
+ auth.authorized.push({
707
+ chatId: approved.chatId,
708
+ name: approved.name,
709
+ username: approved.username,
710
+ isOwner: false,
711
+ authorizedAt: new Date().toISOString(),
712
+ });
713
+ saveAuth(auth);
714
+ updateAuthorizedChatEnv(auth);
715
+ return { status: "approved", request: approved };
716
+ }
717
+
718
+ async function denyAuthRequest(chatId) {
719
+ const auth = loadAuth();
720
+ const idx = auth.pending.findIndex((p) => String(p.chatId) === chatId);
721
+ if (idx < 0) return { status: "not_found" };
722
+
723
+ const denied = auth.pending.splice(idx, 1)[0];
724
+ saveAuth(auth);
725
+ return { status: "denied", request: denied };
726
+ }
727
+
664
728
  // ── Auth request handler (for unauthorized users) ──────────────────
665
729
  bot.onText(/\/auth$/, async (msg) => {
666
730
  if (isAuthorized(msg)) {
@@ -672,15 +736,10 @@ bot.onText(/\/auth$/, async (msg) => {
672
736
  const username = msg.from?.username || "";
673
737
 
674
738
  // Add to pending in auth.json
675
- let auth;
676
- try {
677
- auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
678
- } catch (e) {
679
- auth = { authorized: [], pending: [] };
680
- }
739
+ const auth = loadAuth();
681
740
 
682
741
  // Check if already pending
683
- if (auth.pending.some((p) => p.chatId === chatId)) {
742
+ if (auth.pending.some((p) => String(p.chatId) === chatId)) {
684
743
  bot.sendMessage(msg.chat.id, "Your request is already pending. The bot owner will review it.");
685
744
  return;
686
745
  }
@@ -691,13 +750,20 @@ bot.onText(/\/auth$/, async (msg) => {
691
750
  username,
692
751
  requestedAt: new Date().toISOString(),
693
752
  });
694
- fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
753
+ saveAuth(auth);
695
754
 
696
755
  bot.sendMessage(msg.chat.id, "Access requested! The bot owner will review your request.");
697
756
 
698
757
  // Notify owner
699
- const label = username ? `@${username}` : name;
700
- bot.sendMessage(CHAT_ID, `New auth request from ${label} (${chatId}).\nRun 'open-claudia auth' to approve or deny.`);
758
+ const label = authRequestLabel({ chatId, name, username });
759
+ bot.sendMessage(CHAT_ID, `New auth request from ${label} (${chatId}).`, {
760
+ reply_markup: {
761
+ inline_keyboard: [[
762
+ { text: "Approve", callback_data: `auth:approve:${chatId}` },
763
+ { text: "Deny", callback_data: `auth:deny:${chatId}` },
764
+ ]],
765
+ },
766
+ });
701
767
  });
702
768
 
703
769
  // ── Onboarding ──────────────────────────────────────────────────────
@@ -2919,6 +2985,58 @@ bot.on("callback_query", wrapHandler(async (q) => {
2919
2985
  await bot.answerCallbackQuery(q.id);
2920
2986
  const state = currentState();
2921
2987
 
2988
+ if (d.startsWith("auth:")) {
2989
+ const callbackFromOwner = String(q.from?.id) === CHAT_ID || String(q.message?.chat?.id) === CHAT_ID;
2990
+ if (!callbackFromOwner) {
2991
+ await send("Owner only — auth approvals are restricted.");
2992
+ return;
2993
+ }
2994
+
2995
+ const [, action, chatId] = d.split(":");
2996
+ if (!chatId || !["approve", "deny"].includes(action)) return;
2997
+
2998
+ const result = action === "approve"
2999
+ ? await approveAuthRequest(chatId)
3000
+ : await denyAuthRequest(chatId);
3001
+
3002
+ if (result.status === "not_found") {
3003
+ await send(`No pending auth request found for ${chatId}.`);
3004
+ return;
3005
+ }
3006
+
3007
+ if (result.status === "already_authorized") {
3008
+ await send(`Chat ${chatId} is already authorized.`);
3009
+ return;
3010
+ }
3011
+
3012
+ const label = authRequestLabel(result.request);
3013
+ if (result.status === "approved") {
3014
+ await bot.sendMessage(chatId, "Your access has been approved! You can now use the bot. Send /start to begin.").catch(() => {});
3015
+ let edited = false;
3016
+ if (q.message) {
3017
+ edited = await bot.editMessageText(`Approved auth request from ${label} (${chatId}).`, {
3018
+ chat_id: q.message.chat.id,
3019
+ message_id: q.message.message_id,
3020
+ reply_markup: { inline_keyboard: [] },
3021
+ }).then(() => true).catch(() => false);
3022
+ }
3023
+ if (!edited) await send(`Approved ${label} (${chatId}).`);
3024
+ return;
3025
+ }
3026
+
3027
+ await bot.sendMessage(chatId, "Your access request was denied.").catch(() => {});
3028
+ let edited = false;
3029
+ if (q.message) {
3030
+ edited = await bot.editMessageText(`Denied auth request from ${label} (${chatId}).`, {
3031
+ chat_id: q.message.chat.id,
3032
+ message_id: q.message.message_id,
3033
+ reply_markup: { inline_keyboard: [] },
3034
+ }).then(() => true).catch(() => false);
3035
+ }
3036
+ if (!edited) await send(`Denied ${label} (${chatId}).`);
3037
+ return;
3038
+ }
3039
+
2922
3040
  // Onboarding style selection
2923
3041
  if (d.startsWith("ob:")) { finishOnboarding(d.slice(3)); return; }
2924
3042
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.19.0",
3
+ "version": "1.19.2",
4
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": {