@inetafrica/open-claudia 1.9.2 → 1.10.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/.env.example CHANGED
@@ -2,6 +2,7 @@ TELEGRAM_BOT_TOKEN=your-bot-token-from-botfather
2
2
  TELEGRAM_CHAT_ID=your-chat-id
3
3
  WORKSPACE=/path/to/your/workspace
4
4
  CLAUDE_PATH=/path/to/claude
5
+ CURSOR_PATH=
5
6
  WHISPER_CLI=
6
7
  WHISPER_MODEL=
7
8
  FFMPEG=
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.10.0
4
+ - Cursor Agent backend: switch between Claude Code and Cursor Agent CLI
5
+ - New commands: /cursor, /claude, /backend with inline keyboard
6
+ - Separate session persistence per backend (Claude and Cursor sessions don't clash)
7
+ - Auto-discovers `agent` CLI in PATH if CURSOR_PATH not set
8
+ - /status shows active backend
9
+
3
10
  ## v1.9.2
4
11
  - Fix: show what's new after upgrade
5
12
  - Startup message shows version
package/bot-agent.js CHANGED
@@ -76,6 +76,17 @@ const CHAT_IDS = (config.TELEGRAM_CHAT_ID || "").split(",").map((s) => s.trim())
76
76
  const CHAT_ID = CHAT_IDS[0]; // Primary owner chat for crons/notifications
77
77
  const WORKSPACE = config.WORKSPACE;
78
78
  const CLAUDE_PATH = config.CLAUDE_PATH;
79
+ const CURSOR_PATH = config.CURSOR_PATH || null;
80
+
81
+ // Resolve Cursor Agent CLI (optional)
82
+ let resolvedCursorPath = CURSOR_PATH;
83
+ if (!resolvedCursorPath) {
84
+ try {
85
+ resolvedCursorPath = execSync("which agent 2>/dev/null", { encoding: "utf-8" }).trim() || null;
86
+ } catch (e) { resolvedCursorPath = null; }
87
+ }
88
+ if (resolvedCursorPath) console.log(`Cursor Agent CLI: ${resolvedCursorPath}`);
89
+
79
90
  const WHISPER_CLI = config.WHISPER_CLI || "";
80
91
  const WHISPER_MODEL = config.WHISPER_MODEL || "";
81
92
  const FFMPEG = config.FFMPEG || "";
@@ -88,6 +99,7 @@ const BOT_DIR = __dirname;
88
99
  // Detect PATH for subprocess
89
100
  const FULL_PATH = [
90
101
  path.dirname(CLAUDE_PATH),
102
+ resolvedCursorPath ? path.dirname(resolvedCursorPath) : null,
91
103
  path.dirname(process.execPath),
92
104
  FFMPEG ? path.dirname(FFMPEG) : null,
93
105
  WHISPER_CLI ? path.dirname(WHISPER_CLI) : null,
@@ -174,6 +186,9 @@ bot.setMyCommands([
174
186
  { command: "vault", description: "Manage credentials (password required)" },
175
187
  { command: "soul", description: "View/edit assistant identity" },
176
188
  { command: "status", description: "Session & settings info" },
189
+ { command: "cursor", description: "Switch to Cursor Agent backend" },
190
+ { command: "claude", description: "Switch to Claude Code backend" },
191
+ { command: "backend", description: "Show/switch active backend" },
177
192
  { command: "stop", description: "Cancel running task" },
178
193
  { command: "end", description: "End current session" },
179
194
  { command: "version", description: "Show current version" },
@@ -204,6 +219,7 @@ function saveState() {
204
219
  const data = {
205
220
  currentSession,
206
221
  lastSessionId,
222
+ cursorSessionId,
207
223
  settings,
208
224
  };
209
225
  try { fs.writeFileSync(STATE_FILE, JSON.stringify(data)); } catch (e) {}
@@ -276,6 +292,7 @@ let statusMessageId = null;
276
292
  let streamBuffer = "";
277
293
  let streamInterval = null;
278
294
  let lastSessionId = savedState.lastSessionId || null;
295
+ let cursorSessionId = savedState.cursorSessionId || null;
279
296
  let messageQueue = []; // Fallback queue (only used if chat process also fails)
280
297
  let activeCrons = new Map();
281
298
  let pendingVaultUnlock = false;
@@ -283,11 +300,12 @@ let pendingVaultAction = null;
283
300
  let isFirstMessage = !lastSessionId;
284
301
 
285
302
  let settings = savedState.settings || {
286
- model: null, effort: null, budget: null, permissionMode: null, worktree: false,
303
+ model: null, effort: null, budget: null, permissionMode: null, worktree: false, backend: "claude",
287
304
  };
305
+ if (!settings.backend) settings.backend = "claude";
288
306
 
289
307
  function resetSettings() {
290
- settings = { model: null, effort: null, budget: null, permissionMode: null, worktree: false };
308
+ settings = { model: null, effort: null, budget: null, permissionMode: null, worktree: false, backend: "claude" };
291
309
  }
292
310
 
293
311
  function isAuthorized(msg) {
@@ -658,6 +676,7 @@ function parseStreamEvents(data) {
658
676
  }
659
677
 
660
678
  function buildClaudeArgs(prompt, opts = {}) {
679
+ if (settings.backend === "cursor") return buildCursorArgs(prompt, opts);
661
680
  const args = ["-p", "--verbose", "--output-format", "stream-json",
662
681
  "--append-system-prompt", buildSystemPrompt()];
663
682
  if (opts.continueSession) args.push("--continue");
@@ -672,6 +691,24 @@ function buildClaudeArgs(prompt, opts = {}) {
672
691
  return args;
673
692
  }
674
693
 
694
+ function buildCursorArgs(prompt, opts = {}) {
695
+ const args = ["--print", "--trust", "--output-format", "stream-json"];
696
+ if (opts.continueSession) args.push("--continue");
697
+ else if (cursorSessionId && !opts.fresh) args.push("--resume", cursorSessionId);
698
+ if (settings.model) args.push("--model", settings.model);
699
+ args.push(prompt);
700
+ return args;
701
+ }
702
+
703
+ function getActiveBinary() {
704
+ if (settings.backend === "cursor") return resolvedCursorPath;
705
+ return CLAUDE_PATH;
706
+ }
707
+
708
+ function getActiveSessionId() {
709
+ return settings.backend === "cursor" ? cursorSessionId : lastSessionId;
710
+ }
711
+
675
712
  /**
676
713
  * Quick chat process — handles messages while a heavy task is running.
677
714
  * Uses a fresh session (no --resume) with context about what's running.
@@ -747,7 +784,8 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
747
784
  let currentToolDetail = "";
748
785
 
749
786
  const args = buildClaudeArgs(prompt, opts);
750
- const proc = spawn(CLAUDE_PATH, args, {
787
+ const binaryPath = getActiveBinary();
788
+ const proc = spawn(binaryPath, args, {
751
789
  cwd,
752
790
  env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME },
753
791
  stdio: ["ignore", "pipe", "pipe"],
@@ -815,7 +853,11 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
815
853
  }
816
854
  }
817
855
  }
818
- if (evt.type === "result" && evt.session_id) { lastSessionId = evt.session_id; saveState(); }
856
+ if (evt.type === "result" && evt.session_id) {
857
+ if (settings.backend === "cursor") { cursorSessionId = evt.session_id; }
858
+ else { lastSessionId = evt.session_id; }
859
+ saveState();
860
+ }
819
861
  if (evt.type === "result" && evt.result) assistantText = evt.result;
820
862
  }
821
863
  });
@@ -1094,10 +1136,38 @@ bot.onText(/\/budget$/, (msg) => {
1094
1136
  bot.onText(/\/budget (.+)/, (msg, match) => { if (!isAuthorized(msg)) return; const v = parseFloat(match[1].replace("$","")); settings.budget = v > 0 ? v : null; send(`Budget: ${settings.budget ? "$"+settings.budget : "none"}`); });
1095
1137
 
1096
1138
  bot.onText(/\/plan$/, (msg) => { if (!isAuthorized(msg)) return; const p = settings.permissionMode === "plan"; settings.permissionMode = p ? null : "plan"; send(p ? "Plan mode off." : "Plan mode on."); });
1097
- bot.onText(/\/compact/, async (msg) => { if (!isAuthorized(msg)) return; if (!requireSession(msg)) return; if (!lastSessionId) return send("No conversation."); await runClaude("Summarize: key decisions, code state, next steps.", currentSession.dir, msg.message_id); });
1139
+ bot.onText(/\/compact/, async (msg) => { if (!isAuthorized(msg)) return; if (!requireSession(msg)) return; if (!getActiveSessionId()) return send("No conversation."); await runClaude("Summarize: key decisions, code state, next steps.", currentSession.dir, msg.message_id); });
1098
1140
  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 }); });
1099
1141
  bot.onText(/\/worktree$/, (msg) => { if (!isAuthorized(msg)) return; settings.worktree = !settings.worktree; send(settings.worktree ? "Worktree on." : "Worktree off."); });
1100
1142
 
1143
+ bot.onText(/\/cursor$/, async (msg) => {
1144
+ if (!isAuthorized(msg)) return;
1145
+ if (!resolvedCursorPath) return send("Cursor Agent CLI not found.\nSet CURSOR_PATH in .env or install: https://docs.cursor.com/agent");
1146
+ settings.backend = "cursor";
1147
+ settings.model = null;
1148
+ saveState();
1149
+ const sid = cursorSessionId ? `\nSession: ${cursorSessionId.slice(0, 8)}...` : "\nNew session.";
1150
+ send(`Switched to Cursor Agent.${sid}\n\n/claude — switch back`);
1151
+ });
1152
+
1153
+ bot.onText(/\/claude$/, async (msg) => {
1154
+ if (!isAuthorized(msg)) return;
1155
+ settings.backend = "claude";
1156
+ settings.model = null;
1157
+ saveState();
1158
+ const sid = lastSessionId ? `\nSession: ${lastSessionId.slice(0, 8)}...` : "\nNew session.";
1159
+ send(`Switched to Claude Code.${sid}\n\n/cursor — switch to Cursor`);
1160
+ });
1161
+
1162
+ bot.onText(/\/backend$/, async (msg) => {
1163
+ if (!isAuthorized(msg)) return;
1164
+ const label = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1165
+ const cursorAvail = resolvedCursorPath ? "available" : "not found";
1166
+ send(`Backend: ${label}\n\nClaude Code: available\nCursor Agent: ${cursorAvail}`, { keyboard: { inline_keyboard: [
1167
+ [{ text: "Claude Code", callback_data: "be:claude" }, { text: "Cursor Agent", callback_data: "be:cursor" }],
1168
+ ] } });
1169
+ });
1170
+
1101
1171
  bot.onText(/\/mode$/, async (msg) => {
1102
1172
  if (!isAuthorized(msg)) return;
1103
1173
  await send("Bot mode: *agent* (non-blocking)\n\nHeavy tasks run in the background. You can keep chatting while they work.\nSwitch to direct mode for serial execution with shared session context.", {
@@ -1133,8 +1203,10 @@ bot.onText(/\/stop/, async (msg) => {
1133
1203
  bot.onText(/\/status/, (msg) => {
1134
1204
  if (!isAuthorized(msg)) return;
1135
1205
  if (!currentSession) return send("No session.", { keyboard: { inline_keyboard: [[{ text: "Pick", callback_data: "show:projects" }]] } });
1206
+ const backendLabel = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1136
1207
  const lines = [
1137
1208
  `Project: ${currentSession.name} (agent mode)`,
1209
+ `Backend: ${backendLabel}`,
1138
1210
  `Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}`,
1139
1211
  `Vault: ${vault.isUnlocked() ? "unlocked" : "locked"} | Crons: ${activeCrons.size}`,
1140
1212
  ];
@@ -1281,6 +1353,16 @@ bot.on("callback_query", async (q) => {
1281
1353
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
1282
1354
  if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
1283
1355
  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; }
1356
+ if (d.startsWith("be:")) {
1357
+ const be = d.slice(3);
1358
+ if (be === "cursor" && !resolvedCursorPath) { await send("Cursor Agent CLI not found."); return; }
1359
+ settings.backend = be;
1360
+ settings.model = null;
1361
+ saveState();
1362
+ const label = be === "cursor" ? "Cursor Agent" : "Claude Code";
1363
+ await send(`Switched to ${label}.`);
1364
+ return;
1365
+ }
1284
1366
 
1285
1367
  // Mode switching
1286
1368
  if (d.startsWith("mode:")) {
package/bot.js CHANGED
@@ -107,6 +107,7 @@ const CHAT_IDS = (config.TELEGRAM_CHAT_ID || "").split(",").map((s) => s.trim())
107
107
  const CHAT_ID = CHAT_IDS[0]; // Primary owner chat for crons/notifications
108
108
  const WORKSPACE = config.WORKSPACE;
109
109
  const CLAUDE_PATH = config.CLAUDE_PATH;
110
+ const CURSOR_PATH = config.CURSOR_PATH || null;
110
111
 
111
112
  // Validate critical config at startup
112
113
  if (!TOKEN) { console.error("TELEGRAM_BOT_TOKEN not set"); process.exit(1); }
@@ -135,6 +136,15 @@ if (!fs.existsSync(CLAUDE_PATH)) {
135
136
  process.exit(1);
136
137
  }
137
138
  }
139
+
140
+ // Resolve Cursor Agent CLI (optional — discovered at startup)
141
+ let resolvedCursorPath = CURSOR_PATH;
142
+ if (!resolvedCursorPath) {
143
+ try {
144
+ resolvedCursorPath = execSync("which agent 2>/dev/null", { encoding: "utf-8" }).trim() || null;
145
+ } catch (e) { resolvedCursorPath = null; }
146
+ }
147
+ if (resolvedCursorPath) console.log(`Cursor Agent CLI: ${resolvedCursorPath}`);
138
148
  const WHISPER_CLI = config.WHISPER_CLI || "";
139
149
  const WHISPER_MODEL = config.WHISPER_MODEL || "";
140
150
  const FFMPEG = config.FFMPEG || "";
@@ -147,6 +157,7 @@ const BOT_DIR = __dirname;
147
157
  // Detect PATH for subprocess
148
158
  const FULL_PATH = [
149
159
  path.dirname(CLAUDE_PATH),
160
+ resolvedCursorPath ? path.dirname(resolvedCursorPath) : null,
150
161
  path.dirname(process.execPath),
151
162
  FFMPEG ? path.dirname(FFMPEG) : null,
152
163
  WHISPER_CLI ? path.dirname(WHISPER_CLI) : null,
@@ -233,6 +244,9 @@ bot.setMyCommands([
233
244
  { command: "vault", description: "Manage credentials (password required)" },
234
245
  { command: "soul", description: "View/edit assistant identity" },
235
246
  { command: "status", description: "Session & settings info" },
247
+ { command: "cursor", description: "Switch to Cursor Agent backend" },
248
+ { command: "claude", description: "Switch to Claude Code backend" },
249
+ { command: "backend", description: "Show/switch active backend" },
236
250
  { command: "stop", description: "Cancel running task" },
237
251
  { command: "end", description: "End current session" },
238
252
  { command: "version", description: "Show current version" },
@@ -270,6 +284,7 @@ function saveState() {
270
284
  const data = {
271
285
  currentSession,
272
286
  lastSessionId,
287
+ cursorSessionId,
273
288
  settings,
274
289
  };
275
290
  try { fs.writeFileSync(STATE_FILE, JSON.stringify(data)); } catch (e) {}
@@ -338,6 +353,7 @@ let statusMessageId = null;
338
353
  let streamBuffer = "";
339
354
  let streamInterval = null;
340
355
  let lastSessionId = savedState.lastSessionId || null;
356
+ let cursorSessionId = savedState.cursorSessionId || null;
341
357
  let messageQueue = [];
342
358
  let activeCrons = new Map();
343
359
  let pendingVaultUnlock = false; // Waiting for password
@@ -345,11 +361,12 @@ let pendingVaultAction = null; // What to do after unlock
345
361
  let isFirstMessage = !lastSessionId; // Track if this is first message in session
346
362
 
347
363
  let settings = savedState.settings || {
348
- model: null, effort: null, budget: null, permissionMode: null, worktree: false,
364
+ model: null, effort: null, budget: null, permissionMode: null, worktree: false, backend: "claude",
349
365
  };
366
+ if (!settings.backend) settings.backend = "claude";
350
367
 
351
368
  function resetSettings() {
352
- settings = { model: null, effort: null, budget: null, permissionMode: null, worktree: false };
369
+ settings = { model: null, effort: null, budget: null, permissionMode: null, worktree: false, backend: "claude" };
353
370
  }
354
371
 
355
372
  function isAuthorized(msg) {
@@ -720,6 +737,7 @@ function parseStreamEvents(data) {
720
737
  }
721
738
 
722
739
  function buildClaudeArgs(prompt, opts = {}) {
740
+ if (settings.backend === "cursor") return buildCursorArgs(prompt, opts);
723
741
  const args = ["-p", "--verbose", "--output-format", "stream-json",
724
742
  "--append-system-prompt", buildSystemPrompt()];
725
743
  if (opts.continueSession) args.push("--continue");
@@ -734,6 +752,24 @@ function buildClaudeArgs(prompt, opts = {}) {
734
752
  return args;
735
753
  }
736
754
 
755
+ function buildCursorArgs(prompt, opts = {}) {
756
+ const args = ["--print", "--trust", "--output-format", "stream-json"];
757
+ if (opts.continueSession) args.push("--continue");
758
+ else if (cursorSessionId && !opts.fresh) args.push("--resume", cursorSessionId);
759
+ if (settings.model) args.push("--model", settings.model);
760
+ args.push(prompt);
761
+ return args;
762
+ }
763
+
764
+ function getActiveBinary() {
765
+ if (settings.backend === "cursor") return resolvedCursorPath;
766
+ return CLAUDE_PATH;
767
+ }
768
+
769
+ function getActiveSessionId() {
770
+ return settings.backend === "cursor" ? cursorSessionId : lastSessionId;
771
+ }
772
+
737
773
  async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
738
774
  if (runningProcess) {
739
775
  messageQueue.push({ prompt, replyToMsgId, opts });
@@ -750,11 +786,12 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
750
786
  let currentToolDetail = "";
751
787
 
752
788
  const args = buildClaudeArgs(prompt, opts);
753
- const proc = spawn(CLAUDE_PATH, args, {
789
+ const binaryPath = getActiveBinary();
790
+ const proc = spawn(binaryPath, args, {
754
791
  cwd,
755
792
  env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME },
756
793
  stdio: ["ignore", "pipe", "pipe"],
757
- detached: process.platform !== "win32", // Create process group so /stop kills children too
794
+ detached: process.platform !== "win32",
758
795
  });
759
796
 
760
797
  runningProcess = proc;
@@ -832,7 +869,11 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
832
869
  }
833
870
  }
834
871
  }
835
- if (evt.type === "result" && evt.session_id) { lastSessionId = evt.session_id; saveState(); }
872
+ if (evt.type === "result" && evt.session_id) {
873
+ if (settings.backend === "cursor") { cursorSessionId = evt.session_id; }
874
+ else { lastSessionId = evt.session_id; }
875
+ saveState();
876
+ }
836
877
  if (evt.type === "result" && evt.result) assistantText = evt.result;
837
878
  }
838
879
  });
@@ -853,7 +894,8 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
853
894
  const stderrLower = stderrBuffer.toLowerCase();
854
895
  if (stderrLower.includes("unauthorized") || stderrLower.includes("auth") && stderrLower.includes("fail") ||
855
896
  stderrLower.includes("api key") || stderrLower.includes("not logged in")) {
856
- await send("Claude authentication error. Run `claude auth` to re-authenticate.");
897
+ const hint = settings.backend === "cursor" ? "Run `agent login` to authenticate." : "Run `claude auth` to re-authenticate.";
898
+ await send(`Authentication error. ${hint}`);
857
899
  return;
858
900
  }
859
901
 
@@ -1137,10 +1179,38 @@ bot.onText(/\/budget$/, (msg) => {
1137
1179
  bot.onText(/\/budget (.+)/, (msg, match) => { if (!isAuthorized(msg)) return; const v = parseFloat(match[1].replace("$","")); settings.budget = v > 0 ? v : null; send(`Budget: ${settings.budget ? "$"+settings.budget : "none"}`); });
1138
1180
 
1139
1181
  bot.onText(/\/plan$/, (msg) => { if (!isAuthorized(msg)) return; const p = settings.permissionMode === "plan"; settings.permissionMode = p ? null : "plan"; send(p ? "Plan mode off." : "Plan mode on."); });
1140
- bot.onText(/\/compact/, async (msg) => { if (!isAuthorized(msg)) return; if (!requireSession(msg)) return; if (!lastSessionId) return send("No conversation."); await runClaude("Summarize: key decisions, code state, next steps.", currentSession.dir, msg.message_id); });
1182
+ bot.onText(/\/compact/, async (msg) => { if (!isAuthorized(msg)) return; if (!requireSession(msg)) return; if (!getActiveSessionId()) return send("No conversation."); await runClaude("Summarize: key decisions, code state, next steps.", currentSession.dir, msg.message_id); });
1141
1183
  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 }); });
1142
1184
  bot.onText(/\/worktree$/, (msg) => { if (!isAuthorized(msg)) return; settings.worktree = !settings.worktree; send(settings.worktree ? "Worktree on." : "Worktree off."); });
1143
1185
 
1186
+ bot.onText(/\/cursor$/, async (msg) => {
1187
+ if (!isAuthorized(msg)) return;
1188
+ if (!resolvedCursorPath) return send("Cursor Agent CLI not found.\nSet CURSOR_PATH in .env or install: https://docs.cursor.com/agent");
1189
+ settings.backend = "cursor";
1190
+ settings.model = null;
1191
+ saveState();
1192
+ const sid = cursorSessionId ? `\nSession: ${cursorSessionId.slice(0, 8)}...` : "\nNew session.";
1193
+ send(`Switched to Cursor Agent.${sid}\n\n/claude — switch back`);
1194
+ });
1195
+
1196
+ bot.onText(/\/claude$/, async (msg) => {
1197
+ if (!isAuthorized(msg)) return;
1198
+ settings.backend = "claude";
1199
+ settings.model = null;
1200
+ saveState();
1201
+ const sid = lastSessionId ? `\nSession: ${lastSessionId.slice(0, 8)}...` : "\nNew session.";
1202
+ send(`Switched to Claude Code.${sid}\n\n/cursor — switch to Cursor`);
1203
+ });
1204
+
1205
+ bot.onText(/\/backend$/, async (msg) => {
1206
+ if (!isAuthorized(msg)) return;
1207
+ const label = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1208
+ const cursorAvail = resolvedCursorPath ? "available" : "not found";
1209
+ send(`Backend: ${label}\n\nClaude Code: available\nCursor Agent: ${cursorAvail}`, { keyboard: { inline_keyboard: [
1210
+ [{ text: "Claude Code", callback_data: "be:claude" }, { text: "Cursor Agent", callback_data: "be:cursor" }],
1211
+ ] } });
1212
+ });
1213
+
1144
1214
  bot.onText(/\/mode$/, async (msg) => {
1145
1215
  if (!isAuthorized(msg)) return;
1146
1216
  await send("Bot mode: *direct* (default)\n\nSwitch to agent mode for non-blocking execution.\nIn agent mode, heavy tasks run in the background and you can keep chatting.", {
@@ -1176,8 +1246,10 @@ bot.onText(/\/stop/, async (msg) => {
1176
1246
  bot.onText(/\/status/, (msg) => {
1177
1247
  if (!isAuthorized(msg)) return;
1178
1248
  if (!currentSession) return send("No session.", { keyboard: { inline_keyboard: [[{ text: "Pick", callback_data: "show:projects" }]] } });
1249
+ const backendLabel = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1179
1250
  send([
1180
1251
  `Project: ${currentSession.name}`,
1252
+ `Backend: ${backendLabel}`,
1181
1253
  `Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}`,
1182
1254
  `Vault: ${vault.isUnlocked() ? "unlocked" : "locked"} | Crons: ${activeCrons.size}`,
1183
1255
  runningProcess ? "Working..." : "Ready.",
@@ -1317,6 +1389,16 @@ bot.on("callback_query", async (q) => {
1317
1389
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
1318
1390
  if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
1319
1391
  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; }
1392
+ if (d.startsWith("be:")) {
1393
+ const be = d.slice(3);
1394
+ if (be === "cursor" && !resolvedCursorPath) { await send("Cursor Agent CLI not found."); return; }
1395
+ settings.backend = be;
1396
+ settings.model = null;
1397
+ saveState();
1398
+ const label = be === "cursor" ? "Cursor Agent" : "Claude Code";
1399
+ await send(`Switched to ${label}.`);
1400
+ return;
1401
+ }
1320
1402
 
1321
1403
  // Mode switching — writes mode file and restarts bot
1322
1404
  if (d.startsWith("mode:")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.9.2",
3
+ "version": "1.10.0",
4
4
  "description": "Your always-on AI coding assistant — Claude Code via Telegram",
5
5
  "main": "bot.js",
6
6
  "bin": {