@inetafrica/open-claudia 1.4.5 → 1.6.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 (2) hide show
  1. package/bot.js +317 -55
  2. package/package.json +1 -1
package/bot.js CHANGED
@@ -7,10 +7,40 @@ const cron = require("node-cron");
7
7
  const Vault = require("./vault");
8
8
  const CONFIG_DIR = require("./config-dir");
9
9
 
10
- // ── Graceful shutdown ──────────────────────────────────────────────
10
+ // ── Graceful shutdown & error handling ─────────────────────────────
11
11
  process.on("SIGINT", () => process.exit(0));
12
12
  process.on("SIGTERM", () => process.exit(0));
13
13
 
14
+ // Notify user of crashes via Telegram before exiting
15
+ function notifyError(label, err) {
16
+ const msg = `${label}: ${err?.message || err}`.slice(0, 1000);
17
+ console.error(msg, err?.stack || "");
18
+ try {
19
+ // Synchronous-style notification using the Telegram API directly
20
+ const token = process.env.TELEGRAM_BOT_TOKEN;
21
+ const chatId = process.env.TELEGRAM_CHAT_ID?.split(",")[0];
22
+ if (token && chatId) {
23
+ const data = JSON.stringify({ chat_id: chatId, text: msg });
24
+ const req = require("https").request({
25
+ hostname: "api.telegram.org", path: `/bot${token}/sendMessage`,
26
+ method: "POST", headers: { "Content-Type": "application/json", "Content-Length": data.length },
27
+ });
28
+ req.write(data);
29
+ req.end();
30
+ }
31
+ } catch (e) { /* last resort — ignore */ }
32
+ }
33
+
34
+ process.on("uncaughtException", (err) => {
35
+ notifyError("Uncaught exception", err);
36
+ // Give the notification a moment to send before exiting
37
+ setTimeout(() => process.exit(1), 2000);
38
+ });
39
+
40
+ process.on("unhandledRejection", (reason) => {
41
+ notifyError("Unhandled rejection", reason);
42
+ });
43
+
14
44
  // ── Load Config from .env ───────────────────────────────────────────
15
45
  function loadEnv() {
16
46
  const envPath = path.join(CONFIG_DIR, ".env");
@@ -140,6 +170,7 @@ bot.setMyCommands([
140
170
  { command: "compact", description: "Summarize conversation context" },
141
171
  { command: "continue", description: "Resume last conversation" },
142
172
  { command: "worktree", description: "Toggle isolated git branch" },
173
+ { command: "agents", description: "Toggle direct/parallel agent mode" },
143
174
  { command: "cron", description: "Manage scheduled tasks" },
144
175
  { command: "vault", description: "Manage credentials (password required)" },
145
176
  { command: "soul", description: "View/edit assistant identity" },
@@ -237,7 +268,8 @@ const savedState = loadState();
237
268
 
238
269
  // ── State ───────────────────────────────────────────────────────────
239
270
  let currentSession = savedState.currentSession || null;
240
- let runningProcess = null;
271
+ let runningProcess = null; // Primary process (direct mode)
272
+ let runningTasks = new Map(); // taskId -> { proc, statusMessageId, streamInterval, startTime } (parallel mode)
241
273
  let statusMessageId = null;
242
274
  let streamBuffer = "";
243
275
  let streamInterval = null;
@@ -247,13 +279,14 @@ let activeCrons = new Map();
247
279
  let pendingVaultUnlock = false; // Waiting for password
248
280
  let pendingVaultAction = null; // What to do after unlock
249
281
  let isFirstMessage = !lastSessionId; // Track if this is first message in session
282
+ let taskCounter = 0;
250
283
 
251
284
  let settings = savedState.settings || {
252
- model: null, effort: null, budget: null, permissionMode: null, worktree: false,
285
+ model: null, effort: null, budget: null, permissionMode: null, worktree: false, agentMode: "direct",
253
286
  };
254
287
 
255
288
  function resetSettings() {
256
- settings = { model: null, effort: null, budget: null, permissionMode: null, worktree: false };
289
+ settings = { model: null, effort: null, budget: null, permissionMode: null, worktree: false, agentMode: settings.agentMode };
257
290
  }
258
291
 
259
292
  function isAuthorized(msg) {
@@ -524,19 +557,41 @@ async function send(text, opts = {}) {
524
557
  if (opts.parseMode) o.parse_mode = opts.parseMode;
525
558
  if (opts.keyboard) o.reply_markup = opts.keyboard;
526
559
  if (opts.replyTo) o.reply_to_message_id = opts.replyTo;
527
- try {
528
- const msg = await bot.sendMessage(CHAT_ID, text, o);
529
- return msg.message_id;
530
- } catch (e) {
531
- if (opts.parseMode) {
532
- try {
533
- const f = {}; if (opts.keyboard) f.reply_markup = opts.keyboard;
534
- return (await bot.sendMessage(CHAT_ID, text, f)).message_id;
535
- } catch (e2) { /* ignore */ }
560
+
561
+ for (let attempt = 0; attempt < 3; attempt++) {
562
+ try {
563
+ const msg = await bot.sendMessage(CHAT_ID, text, o);
564
+ return msg.message_id;
565
+ } catch (e) {
566
+ const errMsg = e.message || "";
567
+
568
+ // replyTo message was deleted or not found — retry without it
569
+ if (o.reply_to_message_id && errMsg.includes("message to be replied not found")) {
570
+ delete o.reply_to_message_id;
571
+ continue;
572
+ }
573
+
574
+ // Rate limited — wait and retry
575
+ const retryMatch = errMsg.match(/retry after (\d+)/i);
576
+ if (retryMatch) {
577
+ const waitSec = Math.min(parseInt(retryMatch[1], 10), 30);
578
+ console.error(`Send: rate limited, waiting ${waitSec}s`);
579
+ await new Promise((r) => setTimeout(r, waitSec * 1000));
580
+ continue;
581
+ }
582
+
583
+ // Parse mode failed — retry without it
584
+ if (opts.parseMode && o.parse_mode) {
585
+ delete o.parse_mode;
586
+ continue;
587
+ }
588
+
589
+ console.error("Send error:", errMsg);
590
+ return null;
536
591
  }
537
- console.error("Send error:", e.message);
538
- return null;
539
592
  }
593
+ console.error("Send: exhausted retries");
594
+ return null;
540
595
  }
541
596
 
542
597
  async function editMessage(messageId, text, opts = {}) {
@@ -544,7 +599,17 @@ async function editMessage(messageId, text, opts = {}) {
544
599
  const o = { chat_id: CHAT_ID, message_id: messageId };
545
600
  if (opts.keyboard) o.reply_markup = opts.keyboard;
546
601
  await bot.editMessageText(text, o);
547
- } catch (e) { /* ignore */ }
602
+ } catch (e) {
603
+ const errMsg = e.message || "";
604
+ // Rate limited — skip this update (next interval will catch up)
605
+ if (errMsg.includes("retry after")) return;
606
+ // Message unchanged — ignore
607
+ if (errMsg.includes("message is not modified")) return;
608
+ // Log anything unexpected
609
+ if (!errMsg.includes("message to edit not found")) {
610
+ console.error("Edit error:", errMsg);
611
+ }
612
+ }
548
613
  }
549
614
 
550
615
  function splitMessage(text, maxLen = 4000) {
@@ -591,6 +656,19 @@ function parseStreamEvents(data) {
591
656
  return events;
592
657
  }
593
658
 
659
+ /**
660
+ * Route to the appropriate runner based on agent mode setting.
661
+ * In direct mode: serial execution with shared session.
662
+ * In parallel mode: concurrent independent processes.
663
+ * Commands like /compact and /continue always use direct mode (they need session context).
664
+ */
665
+ async function runClaudeRouted(prompt, cwd, replyToMsgId, opts = {}) {
666
+ if (opts.forceDirect || settings.agentMode !== "parallel") {
667
+ return runClaude(prompt, cwd, replyToMsgId, opts);
668
+ }
669
+ return runClaudeParallel(prompt, cwd, replyToMsgId, opts);
670
+ }
671
+
594
672
  function buildClaudeArgs(prompt, opts = {}) {
595
673
  const args = ["-p", "--verbose", "--output-format", "stream-json",
596
674
  "--append-system-prompt", buildSystemPrompt()];
@@ -634,24 +712,36 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
634
712
  let longRunningNotified = false;
635
713
 
636
714
  let lastUpdate = "";
637
- streamInterval = setInterval(async () => {
638
- bot.sendChatAction(CHAT_ID, "typing");
715
+ // Adaptive update interval: 2s for first 2min, then 5s to avoid rate limits
716
+ const scheduleUpdate = () => {
639
717
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
640
- const display = formatProgress(assistantText, toolUses, currentTool, elapsed, currentToolDetail);
641
- if (display && display !== lastUpdate) {
642
- if (!statusMessageId && assistantText) {
643
- statusMessageId = await send(display.length > 4000 ? display.slice(-4000) : display, { replyTo: replyToMsgId });
644
- } else if (statusMessageId) {
645
- await editMessage(statusMessageId, display.length > 4000 ? display.slice(-4000) : display);
718
+ const interval = elapsed > 120 ? 5000 : 2000;
719
+ streamInterval = setTimeout(updateProgress, interval);
720
+ };
721
+ const updateProgress = async () => {
722
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
723
+ try {
724
+ bot.sendChatAction(CHAT_ID, "typing").catch(() => {});
725
+ const display = formatProgress(assistantText, toolUses, currentTool, elapsed, currentToolDetail);
726
+ if (display && display !== lastUpdate) {
727
+ if (!statusMessageId && assistantText) {
728
+ statusMessageId = await send(display.length > 4000 ? display.slice(-4000) : display, { replyTo: replyToMsgId });
729
+ } else if (statusMessageId) {
730
+ await editMessage(statusMessageId, display.length > 4000 ? display.slice(-4000) : display);
731
+ }
732
+ lastUpdate = display;
646
733
  }
647
- lastUpdate = display;
648
- }
649
- // Notify after 5 minutes that it's still running
650
- if (elapsed > 300 && !longRunningNotified) {
651
- longRunningNotified = true;
652
- await send(`Still working (${Math.floor(elapsed / 60)}min)... Send /stop to cancel.`);
734
+ // Notify after 5 minutes that it's still running
735
+ if (elapsed > 300 && !longRunningNotified) {
736
+ longRunningNotified = true;
737
+ await send(`Still working (${Math.floor(elapsed / 60)}min)... Send /stop to cancel.`);
738
+ }
739
+ } catch (e) {
740
+ console.error("Progress update error:", e.message);
653
741
  }
654
- }, 1500);
742
+ if (runningProcess) scheduleUpdate();
743
+ };
744
+ scheduleUpdate();
655
745
 
656
746
  proc.stdout.on("data", (data) => {
657
747
  streamBuffer += data.toString();
@@ -686,16 +776,25 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
686
776
 
687
777
  proc.on("close", async (code) => {
688
778
  runningProcess = null;
689
- clearInterval(streamInterval); streamInterval = null;
690
- const finalText = assistantText || "(no output)";
691
- const chunks = splitMessage(finalText);
692
- // Keep the streaming progress message and send final result as a new message
693
- // This ensures the user gets a notification for the final answer
694
- await send(chunks[0], { replyTo: replyToMsgId });
695
- for (let i = 1; i < chunks.length; i++) {
696
- await send(chunks[i]);
779
+ clearTimeout(streamInterval); streamInterval = null;
780
+ try {
781
+ const finalText = assistantText || "(no output)";
782
+ const chunks = splitMessage(finalText);
783
+ // Send final result as a new message (triggers notification)
784
+ const sent = await send(chunks[0], { replyTo: replyToMsgId });
785
+ if (!sent) {
786
+ // Fallback: if the first send failed completely, try without any options
787
+ await send(chunks[0]);
788
+ }
789
+ for (let i = 1; i < chunks.length; i++) {
790
+ await send(chunks[i]);
791
+ }
792
+ if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
793
+ } catch (e) {
794
+ console.error("Final message delivery failed:", e.message);
795
+ // Last-resort attempt to notify user something went wrong
796
+ await send("Task completed but failed to deliver the response. Send /continue to see the result.");
697
797
  }
698
- if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
699
798
  if (settings.budget) settings.budget = null;
700
799
  statusMessageId = null;
701
800
 
@@ -712,11 +811,131 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
712
811
  });
713
812
 
714
813
  proc.on("error", async (err) => {
715
- runningProcess = null; clearInterval(streamInterval);
814
+ runningProcess = null; clearTimeout(streamInterval);
716
815
  await send(`Error: ${err.message}`); statusMessageId = null;
717
816
  });
718
817
  }
719
818
 
819
+ /**
820
+ * Run Claude in parallel mode — each call gets its own independent process.
821
+ * No queueing, no shared session. Multiple tasks can run simultaneously.
822
+ */
823
+ async function runClaudeParallel(prompt, cwd, replyToMsgId, opts = {}) {
824
+ bot.sendChatAction(CHAT_ID, "typing").catch(() => {});
825
+
826
+ const taskId = `task-${++taskCounter}`;
827
+ let taskStatusMsgId = null;
828
+ let taskStreamBuf = "";
829
+ let taskText = "";
830
+ let taskToolUses = [];
831
+ let taskCurrentTool = null;
832
+ let taskCurrentToolDetail = "";
833
+ let taskSessionId = null;
834
+
835
+ // Build args — always fresh session in parallel mode (no --resume)
836
+ const args = ["-p", "--verbose", "--output-format", "stream-json",
837
+ "--append-system-prompt", buildSystemPrompt()];
838
+ if (settings.model) args.push("--model", settings.model);
839
+ if (settings.effort) args.push("--effort", settings.effort);
840
+ if (settings.budget) args.push("--max-budget-usd", String(settings.budget));
841
+ if (settings.permissionMode) args.push("--permission-mode", settings.permissionMode);
842
+ else args.push("--dangerously-skip-permissions");
843
+ if (settings.worktree) args.push("--worktree");
844
+ args.push(prompt);
845
+
846
+ const proc = spawn(CLAUDE_PATH, args, {
847
+ cwd,
848
+ env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME },
849
+ stdio: ["ignore", "pipe", "pipe"],
850
+ detached: process.platform !== "win32",
851
+ });
852
+
853
+ const startTime = Date.now();
854
+ let taskInterval = null;
855
+ let lastUpdate = "";
856
+
857
+ // Adaptive progress updates
858
+ const scheduleTaskUpdate = () => {
859
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
860
+ const interval = elapsed > 120 ? 5000 : 2000;
861
+ taskInterval = setTimeout(doTaskUpdate, interval);
862
+ };
863
+ const doTaskUpdate = async () => {
864
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
865
+ try {
866
+ bot.sendChatAction(CHAT_ID, "typing").catch(() => {});
867
+ const display = formatProgress(taskText, taskToolUses, taskCurrentTool, elapsed, taskCurrentToolDetail);
868
+ if (display && display !== lastUpdate) {
869
+ if (!taskStatusMsgId && taskText) {
870
+ taskStatusMsgId = await send(display.length > 4000 ? display.slice(-4000) : display, { replyTo: replyToMsgId });
871
+ } else if (taskStatusMsgId) {
872
+ await editMessage(taskStatusMsgId, display.length > 4000 ? display.slice(-4000) : display);
873
+ }
874
+ lastUpdate = display;
875
+ }
876
+ } catch (e) {
877
+ console.error(`Task ${taskId} progress error:`, e.message);
878
+ }
879
+ if (runningTasks.has(taskId)) scheduleTaskUpdate();
880
+ };
881
+
882
+ runningTasks.set(taskId, { proc, startTime, prompt: prompt.slice(0, 50) });
883
+ scheduleTaskUpdate();
884
+
885
+ proc.stdout.on("data", (data) => {
886
+ taskStreamBuf += data.toString();
887
+ const events = parseStreamEvents(taskStreamBuf);
888
+ const lastNewline = taskStreamBuf.lastIndexOf("\n");
889
+ taskStreamBuf = lastNewline >= 0 ? taskStreamBuf.slice(lastNewline + 1) : taskStreamBuf;
890
+ for (const evt of events) {
891
+ if (evt.type === "assistant" && evt.message?.content) {
892
+ for (const block of evt.message.content) {
893
+ if (block.type === "text") taskText += block.text;
894
+ else if (block.type === "tool_use") {
895
+ taskCurrentTool = block.name;
896
+ taskToolUses.push(block.name);
897
+ const input = block.input || {};
898
+ if (block.name === "Bash" && input.command) taskCurrentToolDetail = input.command.slice(0, 80);
899
+ else if ((block.name === "Read" || block.name === "Edit" || block.name === "Write") && input.file_path)
900
+ taskCurrentToolDetail = input.file_path.split("/").slice(-2).join("/");
901
+ else if (block.name === "Grep" && input.pattern) taskCurrentToolDetail = input.pattern.slice(0, 40);
902
+ else if (block.name === "Glob" && input.pattern) taskCurrentToolDetail = input.pattern;
903
+ else taskCurrentToolDetail = "";
904
+ }
905
+ }
906
+ }
907
+ if (evt.type === "result" && evt.session_id) taskSessionId = evt.session_id;
908
+ if (evt.type === "result" && evt.result) taskText = evt.result;
909
+ }
910
+ });
911
+
912
+ proc.stderr.on("data", (d) => console.error(`Task ${taskId} STDERR:`, d.toString()));
913
+
914
+ proc.on("close", async (code) => {
915
+ runningTasks.delete(taskId);
916
+ clearTimeout(taskInterval);
917
+ try {
918
+ const finalText = taskText || "(no output)";
919
+ const chunks = splitMessage(finalText);
920
+ const sent = await send(chunks[0], { replyTo: replyToMsgId });
921
+ if (!sent) await send(chunks[0]);
922
+ for (let i = 1; i < chunks.length; i++) await send(chunks[i]);
923
+ if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
924
+ } catch (e) {
925
+ console.error(`Task ${taskId} final delivery failed:`, e.message);
926
+ await send("Task completed but failed to deliver the response.");
927
+ }
928
+ });
929
+
930
+ proc.on("error", async (err) => {
931
+ runningTasks.delete(taskId);
932
+ clearTimeout(taskInterval);
933
+ await send(`Task error: ${err.message}`, { replyTo: replyToMsgId });
934
+ });
935
+
936
+ return taskId;
937
+ }
938
+
720
939
  async function runClaudeSilent(prompt, cwd, label) {
721
940
  return new Promise((resolve) => {
722
941
  const args = ["-p", "--output-format", "text", "--verbose",
@@ -827,7 +1046,7 @@ bot.onText(/\/help/, (msg) => {
827
1046
  if (!isAuthorized(msg)) return;
828
1047
  send([
829
1048
  "Session: /session /sessions /projects /continue /status /stop /end",
830
- "Settings: /model /effort /budget /plan /compact /worktree",
1049
+ "Settings: /model /effort /budget /plan /compact /worktree /agents",
831
1050
  "Automation: /cron /vault /soul",
832
1051
  "System: /restart /upgrade",
833
1052
  "",
@@ -935,37 +1154,79 @@ bot.onText(/\/compact/, async (msg) => { if (!isAuthorized(msg)) return; if (!re
935
1154
  bot.onText(/\/continue$/, async (msg) => { if (!isAuthorized(msg)) return; if (!requireSession(msg)) return; await runClaude("continue where we left off", currentSession.dir, msg.message_id, { continueSession: true }); });
936
1155
  bot.onText(/\/worktree$/, (msg) => { if (!isAuthorized(msg)) return; settings.worktree = !settings.worktree; send(settings.worktree ? "Worktree on." : "Worktree off."); });
937
1156
 
1157
+ bot.onText(/\/agents$/, (msg) => {
1158
+ if (!isAuthorized(msg)) return;
1159
+ const mode = settings.agentMode || "direct";
1160
+ send(`Agent mode: *${mode}*\n\n` +
1161
+ `*direct* — one task at a time, shared session context, queues messages\n` +
1162
+ `*parallel* — concurrent tasks, independent sessions, no queue`, {
1163
+ parseMode: "Markdown",
1164
+ keyboard: { inline_keyboard: [
1165
+ [{ text: mode === "direct" ? "Direct (active)" : "Switch to Direct", callback_data: "am:direct" },
1166
+ { text: mode === "parallel" ? "Parallel (active)" : "Switch to Parallel", callback_data: "am:parallel" }],
1167
+ ] },
1168
+ });
1169
+ });
1170
+
938
1171
  bot.onText(/\/stop/, async (msg) => {
939
1172
  if (!isAuthorized(msg)) return;
1173
+
1174
+ // Stop parallel tasks
1175
+ if (runningTasks.size > 0) {
1176
+ for (const [taskId, task] of runningTasks) {
1177
+ try { process.kill(-task.proc.pid, "SIGTERM"); } catch (e) {
1178
+ try { task.proc.kill("SIGTERM"); } catch (e2) {}
1179
+ }
1180
+ setTimeout(() => { try { process.kill(-task.proc.pid, "SIGKILL"); } catch (e) {} }, 3000);
1181
+ }
1182
+ const count = runningTasks.size;
1183
+ runningTasks.clear();
1184
+ await send(`Cancelled ${count} task${count > 1 ? "s" : ""}.`);
1185
+ return;
1186
+ }
1187
+
1188
+ // Stop direct mode process
940
1189
  if (runningProcess) {
941
1190
  const pid = runningProcess.pid;
942
1191
  try {
943
- // Kill entire process group to catch child processes (dev servers, etc.)
944
1192
  process.kill(-pid, "SIGTERM");
945
1193
  } catch (e) {
946
1194
  try { runningProcess.kill("SIGTERM"); } catch (e2) {}
947
1195
  }
948
- // Force kill after 3 seconds if still alive
949
1196
  setTimeout(() => {
950
1197
  try { process.kill(-pid, "SIGKILL"); } catch (e) {}
951
1198
  }, 3000);
952
1199
  runningProcess = null;
953
- if (streamInterval) clearInterval(streamInterval);
1200
+ if (streamInterval) clearTimeout(streamInterval);
954
1201
  messageQueue = [];
955
1202
  await send("Cancelled.");
1203
+ return;
956
1204
  }
957
- else await send("Nothing running.");
1205
+
1206
+ await send("Nothing running.");
958
1207
  });
959
1208
 
960
1209
  bot.onText(/\/status/, (msg) => {
961
1210
  if (!isAuthorized(msg)) return;
962
1211
  if (!currentSession) return send("No session.", { keyboard: { inline_keyboard: [[{ text: "Pick", callback_data: "show:projects" }]] } });
963
- send([
1212
+ const mode = settings.agentMode || "direct";
1213
+ const lines = [
964
1214
  `Project: ${currentSession.name}`,
965
- `Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}`,
1215
+ `Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"} | Mode: ${mode}`,
966
1216
  `Vault: ${vault.isUnlocked() ? "unlocked" : "locked"} | Crons: ${activeCrons.size}`,
967
- runningProcess ? "Working..." : "Ready.",
968
- ].join("\n"));
1217
+ ];
1218
+ if (mode === "parallel" && runningTasks.size > 0) {
1219
+ lines.push(`Running: ${runningTasks.size} task${runningTasks.size > 1 ? "s" : ""}`);
1220
+ for (const [id, task] of runningTasks) {
1221
+ const elapsed = Math.floor((Date.now() - task.startTime) / 1000);
1222
+ lines.push(` ${id}: ${task.prompt}... (${elapsed}s)`);
1223
+ }
1224
+ } else if (runningProcess) {
1225
+ lines.push("Working...");
1226
+ } else {
1227
+ lines.push("Ready.");
1228
+ }
1229
+ send(lines.join("\n"));
969
1230
  });
970
1231
 
971
1232
  bot.onText(/\/end/, (msg) => {
@@ -1101,6 +1362,7 @@ bot.on("callback_query", async (q) => {
1101
1362
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
1102
1363
  if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
1103
1364
  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; }
1365
+ if (d.startsWith("am:")) { settings.agentMode = d.slice(3); saveState(); await send(`Agent mode: ${settings.agentMode}`); return; }
1104
1366
 
1105
1367
  // Cron presets
1106
1368
  if (d.startsWith("cp:") && d !== "cp:clear") {
@@ -1134,7 +1396,7 @@ bot.on("voice", async (msg) => {
1134
1396
  try { fs.unlinkSync(oggPath); } catch (e) {}
1135
1397
  if (!transcript) return send("Couldn't transcribe. Try typing it.");
1136
1398
  await send(`Heard: "${transcript}"`, { replyTo: msg.message_id });
1137
- await runClaude(transcript, currentSession.dir, msg.message_id);
1399
+ await runClaudeRouted(transcript, currentSession.dir, msg.message_id);
1138
1400
  } catch (err) { await send(`Voice failed: ${err.message}`); }
1139
1401
  });
1140
1402
 
@@ -1149,7 +1411,7 @@ bot.on("audio", async (msg) => {
1149
1411
  try { fs.unlinkSync(p); } catch (e) {}
1150
1412
  if (!t) return send("Couldn't transcribe.");
1151
1413
  await send(`Heard: "${t}"`, { replyTo: msg.message_id });
1152
- await runClaude(t, currentSession.dir, msg.message_id);
1414
+ await runClaudeRouted(t, currentSession.dir, msg.message_id);
1153
1415
  } catch (err) { await send(`Audio failed: ${err.message}`); }
1154
1416
  });
1155
1417
 
@@ -1160,7 +1422,7 @@ bot.on("photo", async (msg) => {
1160
1422
  try {
1161
1423
  const p = await downloadFile(msg.photo[msg.photo.length - 1].file_id, ".jpg");
1162
1424
  const caption = msg.caption || "Describe this image. If code/UI/error — explain and fix.";
1163
- await runClaude(`Image at ${p}\n\nView it, then: ${caption}`, currentSession.dir, msg.message_id);
1425
+ await runClaudeRouted(`Image at ${p}\n\nView it, then: ${caption}`, currentSession.dir, msg.message_id);
1164
1426
  } catch (err) { await send(`Image failed: ${err.message}`); }
1165
1427
  });
1166
1428
 
@@ -1188,7 +1450,7 @@ bot.on("document", async (msg) => {
1188
1450
  prompt = `File received: ${fileName} (${mime})\nSaved at: ${savePath}\n\nRead this file and ${caption || "summarize its contents. If it's code, explain what it does. If it's a document, give key points."}`;
1189
1451
  }
1190
1452
  await send(`File saved: ${fileName}`, { replyTo: msg.message_id });
1191
- await runClaude(prompt, currentSession.dir, msg.message_id);
1453
+ await runClaudeRouted(prompt, currentSession.dir, msg.message_id);
1192
1454
  } catch (err) { await send(`Failed: ${err.message}`); }
1193
1455
  });
1194
1456
 
@@ -1293,7 +1555,7 @@ bot.on("message", async (msg) => {
1293
1555
  }
1294
1556
  }
1295
1557
 
1296
- await runClaude(prompt, currentSession.dir, msg.message_id);
1558
+ await runClaudeRouted(prompt, currentSession.dir, msg.message_id);
1297
1559
  });
1298
1560
 
1299
1561
  // ── Startup ─────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.4.5",
3
+ "version": "1.6.0",
4
4
  "description": "Your always-on AI coding assistant — Claude Code via Telegram",
5
5
  "main": "bot.js",
6
6
  "bin": {