@inetafrica/open-claudia 1.9.2 → 1.10.1

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/README.md CHANGED
@@ -1,41 +1,69 @@
1
1
  # Open Claudia
2
2
 
3
- Your always-on AI coding assistant — Claude Code via Telegram.
3
+ Your always-on AI coding assistant — Claude Code and Cursor Agent via Telegram.
4
4
 
5
- Send text, voice notes, screenshots, and files from your phone. Claude Code works on your projects and reports back.
5
+ Send text, voice notes, screenshots, and files from your phone. Your chosen AI agent works on your projects and reports back.
6
6
 
7
7
  ## Features
8
8
 
9
+ - **Multi-backend** — switch between Claude Code and Cursor Agent on the fly (`/cursor`, `/claude`)
9
10
  - **Multi-project sessions** — switch between workspace projects
10
11
  - **Per-project conversation history** — auto-resumes last conversation, switch with `/sessions`
12
+ - **Separate session persistence** — Claude and Cursor each maintain their own conversation state
11
13
  - **Voice notes** — speak instructions, transcribed locally via Whisper
12
14
  - **Screenshots & images** — send UI mockups, errors, or code screenshots
13
- - **File sharing** — send PDFs, code files, documents — saved and read by Claude
15
+ - **File sharing** — send PDFs, code files, documents — saved and read by the agent
14
16
  - **Reply context** — reply to any message (including files) for follow-up
15
- - **Streaming output** — see Claude working in real-time
17
+ - **Streaming output** — see the agent working in real-time
18
+ - **Agent mode** — non-blocking side conversations while heavy tasks run in the background
16
19
  - **Cron jobs** — scheduled tasks (standups, git digests, health checks)
17
20
  - **Encrypted vault** — store API keys and credentials securely
18
21
  - **Customizable soul** — define your assistant's personality and knowledge
19
- - **Model switching** — toggle between Opus, Sonnet, Haiku
20
- - **Plan mode, effort levels, budgets** — full Claude Code control from Telegram
22
+ - **Model switching** — toggle between models on either backend
23
+ - **Plan mode, effort levels, budgets** — full control from Telegram
21
24
  - **Auto-updates** — checks for new versions every 5 minutes, upgrade with `/upgrade`
22
25
  - **Multi-user auth** — authorize additional users with code verification
23
26
  - **Cross-platform** — works on macOS, Linux, and Windows
24
27
 
25
28
  ## Prerequisites
26
29
 
27
- - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
28
30
  - [Node.js](https://nodejs.org/) 18+
29
31
  - A Telegram bot token (from [@BotFather](https://t.me/BotFather))
32
+ - At least one authenticated CLI backend on the host machine (see below)
30
33
  - (Optional) [whisper.cpp](https://github.com/ggerganov/whisper.cpp) + ffmpeg for voice notes
31
34
 
32
- ## Install
35
+ ## Quick Start
36
+
37
+ ### 1. Install and authenticate the CLI backends
38
+
39
+ You need **at least one** of these authenticated on the machine where Open Claudia will run.
40
+
41
+ **Claude Code** (required):
42
+
43
+ ```bash
44
+ npm install -g @anthropic-ai/claude-code
45
+ claude # Opens browser to log in
46
+ claude --version # Verify it works
47
+ ```
48
+
49
+ **Cursor Agent** (optional — enables `/cursor` backend):
50
+
51
+ ```bash
52
+ # Install from Cursor IDE: Settings > General > Agent CLI
53
+ # Or download from https://docs.cursor.com/agent
54
+ agent login # Opens browser to authenticate
55
+ agent status # Verify: should show your email and plan
56
+ ```
57
+
58
+ > **Important**: Both CLIs store authentication locally on the machine. Open Claudia doesn't handle auth itself — it shells out to whichever CLI you've authenticated. If you see auth errors in Telegram, SSH into the machine and re-authenticate the relevant CLI.
59
+
60
+ ### 2. Install Open Claudia
33
61
 
34
62
  ```bash
35
63
  npm install -g @inetafrica/open-claudia
36
64
  ```
37
65
 
38
- ## Setup
66
+ ### 3. Run setup
39
67
 
40
68
  ```bash
41
69
  open-claudia setup
@@ -43,7 +71,7 @@ open-claudia setup
43
71
 
44
72
  The setup wizard will:
45
73
 
46
- 1. Detect Claude CLI, ffmpeg, and whisper on your system
74
+ 1. Detect Claude CLI, Cursor Agent CLI, ffmpeg, and whisper on your system
47
75
  2. Verify Claude is authenticated
48
76
  3. Ask for your Telegram bot token and verify it
49
77
  4. Generate a verification code — send it to your bot to prove your identity
@@ -69,6 +97,16 @@ If installed as a background service, the bot starts automatically on login and
69
97
 
70
98
  ## Telegram Commands
71
99
 
100
+ ### Backend Switching
101
+
102
+ | Command | Description |
103
+ |---------|-------------|
104
+ | `/cursor` | Switch to Cursor Agent backend |
105
+ | `/claude` | Switch to Claude Code backend |
106
+ | `/backend` | Show current backend with picker |
107
+
108
+ Each backend keeps its own persistent session. Switching doesn't lose your place — you can go back and forth freely.
109
+
72
110
  ### Session Management
73
111
 
74
112
  | Command | Description |
@@ -85,13 +123,14 @@ When you select a project, the last conversation is automatically resumed. Tap "
85
123
 
86
124
  | Command | Description |
87
125
  |---------|-------------|
88
- | `/model` | Switch model (opus / sonnet / haiku) |
126
+ | `/model` | Switch model (opus / sonnet / haiku for Claude; any model flag for Cursor) |
89
127
  | `/effort` | Set effort level (low / medium / high / max) |
90
- | `/budget` | Set max spend for next task (e.g. `/budget 0.50`) |
91
- | `/plan` | Toggle plan mode |
128
+ | `/budget` | Set max spend for next task (e.g. `/budget 0.50`) — Claude only |
129
+ | `/plan` | Toggle plan mode — Claude only |
92
130
  | `/compact` | Summarize conversation context |
93
- | `/worktree` | Toggle isolated git branch |
94
- | `/status` | Show current session and settings |
131
+ | `/worktree` | Toggle isolated git branch — Claude only |
132
+ | `/mode` | Switch between direct and agent bot modes |
133
+ | `/status` | Show current session, backend, and settings |
95
134
 
96
135
  ### Automation
97
136
 
@@ -111,11 +150,27 @@ When you select a project, the last conversation is automatically resumed. Tap "
111
150
  | `/stop` | Cancel a running task |
112
151
  | `/help` | Show all commands |
113
152
 
153
+ ## Backend Comparison
154
+
155
+ | | Claude Code | Cursor Agent |
156
+ |---|---|---|
157
+ | Binary | `claude` | `agent` |
158
+ | Session flag | `--resume <id>` | `--resume <id>` |
159
+ | Auth | `claude auth` | `agent login` |
160
+ | Plan mode | Yes (`--permission-mode plan`) | Yes (`--mode plan`) |
161
+ | Budget control | Yes (`--max-budget-usd`) | No |
162
+ | Effort levels | Yes | No |
163
+ | Worktree | Yes | No |
164
+ | Model switching | Yes | Yes |
165
+ | Dangerously skip permissions | Yes | Yes (`--trust`) |
166
+
167
+ Both backends output `stream-json` which Open Claudia parses for real-time progress updates.
168
+
114
169
  ## Sending Files
115
170
 
116
- Send any file to the bot — PDFs, code files, documents, images. Files are saved to `~/.open-claudia/files/` with their original names. Claude reads the file and responds based on content.
171
+ Send any file to the bot — PDFs, code files, documents, images. Files are saved to `~/.open-claudia/files/` with their original names. The agent reads the file and responds based on content.
117
172
 
118
- Add a caption to your file to give Claude specific instructions:
173
+ Add a caption to your file to give the agent specific instructions:
119
174
  - Send a PDF with caption "summarize the key findings"
120
175
  - Send a code file with caption "find bugs in this"
121
176
  - Send a screenshot with caption "implement this design"
@@ -152,11 +207,14 @@ This shows authorized chats, pending requests, and lets you approve/deny or add
152
207
  ## How It Works
153
208
 
154
209
  ```
155
- Phone (Telegram) --> Bot (Node.js) --> Claude Code CLI --> Your codebase
210
+ Phone (Telegram) --> Bot (Node.js) --> Claude Code CLI --> Your codebase
211
+ --> Cursor Agent CLI -->
156
212
  <-- <-- <--
157
213
  ```
158
214
 
159
- The bot spawns `claude -p` for each message, streaming output back to Telegram. It maintains conversation context via `--resume` and passes a system prompt that gives Claude awareness of the Telegram environment, its configuration files, and the ability to send files/images directly.
215
+ The bot spawns the active backend CLI in headless mode (`--print` / `-p`) for each message, streaming `stream-json` output back to Telegram. It maintains conversation context via `--resume` and passes a system prompt that gives the agent awareness of the Telegram environment, configuration files, and the ability to send files/images directly.
216
+
217
+ Use `/cursor` or `/claude` to switch which CLI handles your messages. Each maintains its own session ID, so switching doesn't lose context on either side.
160
218
 
161
219
  ## Configuration Files
162
220
 
@@ -164,17 +222,30 @@ All stored in `~/.open-claudia/`:
164
222
 
165
223
  | File | Purpose |
166
224
  |------|---------|
167
- | `.env` | Telegram token, workspace path, binary paths |
225
+ | `.env` | Telegram token, workspace path, binary paths (`CLAUDE_PATH`, `CURSOR_PATH`) |
168
226
  | `auth.json` | Authorized users and pending requests |
169
227
  | `vault.enc` | Encrypted credential store |
170
228
  | `soul.md` | Assistant identity and personality (editable via `/soul`) |
171
229
  | `crons.json` | Scheduled tasks |
172
230
  | `sessions.json` | Per-project conversation history |
173
- | `state.json` | Current session state (survives restarts) |
231
+ | `state.json` | Current session state including active backend (survives restarts) |
174
232
  | `bot.log` | Bot logs |
175
233
  | `files/` | Files received from Telegram |
176
234
  | `media/` | Temporary media (voice notes, photos) |
177
235
 
236
+ ### Environment Variables (.env)
237
+
238
+ | Variable | Required | Description |
239
+ |----------|----------|-------------|
240
+ | `TELEGRAM_BOT_TOKEN` | Yes | Bot token from BotFather |
241
+ | `TELEGRAM_CHAT_ID` | Yes | Comma-separated authorized chat IDs |
242
+ | `WORKSPACE` | Yes | Path to your projects directory |
243
+ | `CLAUDE_PATH` | Yes | Path to Claude Code CLI binary |
244
+ | `CURSOR_PATH` | No | Path to Cursor Agent CLI binary (auto-detected if in PATH) |
245
+ | `WHISPER_CLI` | No | Path to whisper.cpp binary |
246
+ | `WHISPER_MODEL` | No | Whisper model to use |
247
+ | `FFMPEG` | No | Path to ffmpeg binary |
248
+
178
249
  ## Background Service
179
250
 
180
251
  ### macOS (launchd)
@@ -188,6 +259,8 @@ launchctl load ~/Library/LaunchAgents/com.open-claudia.plist
188
259
  launchctl unload ~/Library/LaunchAgents/com.open-claudia.plist
189
260
  ```
190
261
 
262
+ **Important**: If `agent` is installed in a non-standard location (e.g. `~/.local/bin`), make sure that path is included in the launchd plist's `PATH` environment variable. Otherwise the bot won't detect it at startup.
263
+
191
264
  ### Linux (systemd)
192
265
 
193
266
  ```bash
@@ -202,7 +275,7 @@ sudo systemctl status claude-telegram-bot
202
275
 
203
276
  The bot checks npm for new versions every 5 minutes. When an update is available, you get a Telegram notification:
204
277
 
205
- > "Hey! A new version is available (v1.3.0). You're on v1.2.9."
278
+ > "Hey! A new version is available (v1.10.0). You're on v1.9.2."
206
279
 
207
280
  Send `/upgrade` to update. The bot will:
208
281
  1. Download and install the new version
@@ -232,7 +305,7 @@ Store sensitive credentials encrypted:
232
305
  /vault lock # Lock vault
233
306
  ```
234
307
 
235
- Claude can read vault credentials when unlocked — useful for deployment scripts and API calls.
308
+ The agent can read vault credentials when unlocked — useful for deployment scripts and API calls.
236
309
 
237
310
  ## License
238
311
 
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
  });
@@ -1068,10 +1110,17 @@ bot.onText(/\/sessions$/, (msg) => {
1068
1110
 
1069
1111
  bot.onText(/\/model$/, (msg) => {
1070
1112
  if (!isAuthorized(msg)) return;
1071
- send(`Model: ${settings.model || "default"}`, { keyboard: { inline_keyboard: [
1113
+ const keyboard = settings.backend === "cursor" ? [
1114
+ [{ text: "Composer 2", callback_data: "m:composer-2" }, { text: "Composer 2 Fast", callback_data: "m:composer-2-fast" }],
1115
+ [{ 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" }],
1116
+ [{ text: "GPT-5.4", callback_data: "m:gpt-5.4-medium" }, { text: "GPT-5.4 High", callback_data: "m:gpt-5.4-high" }],
1117
+ [{ text: "Auto", callback_data: "m:auto" }, { text: "Default", callback_data: "m:default" }],
1118
+ ] : [
1072
1119
  [{ text: "Opus", callback_data: "m:opus" }, { text: "Sonnet", callback_data: "m:sonnet" }, { text: "Haiku", callback_data: "m:haiku" }],
1073
1120
  [{ text: "Default", callback_data: "m:default" }],
1074
- ] } });
1121
+ ];
1122
+ const label = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1123
+ send(`${label} model: ${settings.model || "default"}\n\nOr type /model <name> for any model.`, { keyboard: { inline_keyboard: keyboard } });
1075
1124
  });
1076
1125
  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"}`); });
1077
1126
 
@@ -1094,10 +1143,38 @@ bot.onText(/\/budget$/, (msg) => {
1094
1143
  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
1144
 
1096
1145
  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); });
1146
+ 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
1147
  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
1148
  bot.onText(/\/worktree$/, (msg) => { if (!isAuthorized(msg)) return; settings.worktree = !settings.worktree; send(settings.worktree ? "Worktree on." : "Worktree off."); });
1100
1149
 
1150
+ bot.onText(/\/cursor$/, async (msg) => {
1151
+ if (!isAuthorized(msg)) return;
1152
+ if (!resolvedCursorPath) return send("Cursor Agent CLI not found.\nSet CURSOR_PATH in .env or install: https://docs.cursor.com/agent");
1153
+ settings.backend = "cursor";
1154
+ settings.model = null;
1155
+ saveState();
1156
+ const sid = cursorSessionId ? `\nSession: ${cursorSessionId.slice(0, 8)}...` : "\nNew session.";
1157
+ send(`Switched to Cursor Agent.${sid}\n\n/claude — switch back`);
1158
+ });
1159
+
1160
+ bot.onText(/\/claude$/, async (msg) => {
1161
+ if (!isAuthorized(msg)) return;
1162
+ settings.backend = "claude";
1163
+ settings.model = null;
1164
+ saveState();
1165
+ const sid = lastSessionId ? `\nSession: ${lastSessionId.slice(0, 8)}...` : "\nNew session.";
1166
+ send(`Switched to Claude Code.${sid}\n\n/cursor — switch to Cursor`);
1167
+ });
1168
+
1169
+ bot.onText(/\/backend$/, async (msg) => {
1170
+ if (!isAuthorized(msg)) return;
1171
+ const label = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1172
+ const cursorAvail = resolvedCursorPath ? "available" : "not found";
1173
+ send(`Backend: ${label}\n\nClaude Code: available\nCursor Agent: ${cursorAvail}`, { keyboard: { inline_keyboard: [
1174
+ [{ text: "Claude Code", callback_data: "be:claude" }, { text: "Cursor Agent", callback_data: "be:cursor" }],
1175
+ ] } });
1176
+ });
1177
+
1101
1178
  bot.onText(/\/mode$/, async (msg) => {
1102
1179
  if (!isAuthorized(msg)) return;
1103
1180
  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 +1210,10 @@ bot.onText(/\/stop/, async (msg) => {
1133
1210
  bot.onText(/\/status/, (msg) => {
1134
1211
  if (!isAuthorized(msg)) return;
1135
1212
  if (!currentSession) return send("No session.", { keyboard: { inline_keyboard: [[{ text: "Pick", callback_data: "show:projects" }]] } });
1213
+ const backendLabel = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1136
1214
  const lines = [
1137
1215
  `Project: ${currentSession.name} (agent mode)`,
1216
+ `Backend: ${backendLabel}`,
1138
1217
  `Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}`,
1139
1218
  `Vault: ${vault.isUnlocked() ? "unlocked" : "locked"} | Crons: ${activeCrons.size}`,
1140
1219
  ];
@@ -1281,6 +1360,16 @@ bot.on("callback_query", async (q) => {
1281
1360
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
1282
1361
  if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
1283
1362
  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; }
1363
+ if (d.startsWith("be:")) {
1364
+ const be = d.slice(3);
1365
+ if (be === "cursor" && !resolvedCursorPath) { await send("Cursor Agent CLI not found."); return; }
1366
+ settings.backend = be;
1367
+ settings.model = null;
1368
+ saveState();
1369
+ const label = be === "cursor" ? "Cursor Agent" : "Claude Code";
1370
+ await send(`Switched to ${label}.`);
1371
+ return;
1372
+ }
1284
1373
 
1285
1374
  // Mode switching
1286
1375
  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
 
@@ -1111,10 +1153,17 @@ bot.onText(/\/sessions$/, (msg) => {
1111
1153
 
1112
1154
  bot.onText(/\/model$/, (msg) => {
1113
1155
  if (!isAuthorized(msg)) return;
1114
- send(`Model: ${settings.model || "default"}`, { keyboard: { inline_keyboard: [
1156
+ const keyboard = settings.backend === "cursor" ? [
1157
+ [{ text: "Composer 2", callback_data: "m:composer-2" }, { text: "Composer 2 Fast", callback_data: "m:composer-2-fast" }],
1158
+ [{ 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" }],
1159
+ [{ text: "GPT-5.4", callback_data: "m:gpt-5.4-medium" }, { text: "GPT-5.4 High", callback_data: "m:gpt-5.4-high" }],
1160
+ [{ text: "Auto", callback_data: "m:auto" }, { text: "Default", callback_data: "m:default" }],
1161
+ ] : [
1115
1162
  [{ text: "Opus", callback_data: "m:opus" }, { text: "Sonnet", callback_data: "m:sonnet" }, { text: "Haiku", callback_data: "m:haiku" }],
1116
1163
  [{ text: "Default", callback_data: "m:default" }],
1117
- ] } });
1164
+ ];
1165
+ const label = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1166
+ send(`${label} model: ${settings.model || "default"}\n\nOr type /model <name> for any model.`, { keyboard: { inline_keyboard: keyboard } });
1118
1167
  });
1119
1168
  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"}`); });
1120
1169
 
@@ -1137,10 +1186,38 @@ bot.onText(/\/budget$/, (msg) => {
1137
1186
  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
1187
 
1139
1188
  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); });
1189
+ 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
1190
  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
1191
  bot.onText(/\/worktree$/, (msg) => { if (!isAuthorized(msg)) return; settings.worktree = !settings.worktree; send(settings.worktree ? "Worktree on." : "Worktree off."); });
1143
1192
 
1193
+ bot.onText(/\/cursor$/, async (msg) => {
1194
+ if (!isAuthorized(msg)) return;
1195
+ if (!resolvedCursorPath) return send("Cursor Agent CLI not found.\nSet CURSOR_PATH in .env or install: https://docs.cursor.com/agent");
1196
+ settings.backend = "cursor";
1197
+ settings.model = null;
1198
+ saveState();
1199
+ const sid = cursorSessionId ? `\nSession: ${cursorSessionId.slice(0, 8)}...` : "\nNew session.";
1200
+ send(`Switched to Cursor Agent.${sid}\n\n/claude — switch back`);
1201
+ });
1202
+
1203
+ bot.onText(/\/claude$/, async (msg) => {
1204
+ if (!isAuthorized(msg)) return;
1205
+ settings.backend = "claude";
1206
+ settings.model = null;
1207
+ saveState();
1208
+ const sid = lastSessionId ? `\nSession: ${lastSessionId.slice(0, 8)}...` : "\nNew session.";
1209
+ send(`Switched to Claude Code.${sid}\n\n/cursor — switch to Cursor`);
1210
+ });
1211
+
1212
+ bot.onText(/\/backend$/, async (msg) => {
1213
+ if (!isAuthorized(msg)) return;
1214
+ const label = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1215
+ const cursorAvail = resolvedCursorPath ? "available" : "not found";
1216
+ send(`Backend: ${label}\n\nClaude Code: available\nCursor Agent: ${cursorAvail}`, { keyboard: { inline_keyboard: [
1217
+ [{ text: "Claude Code", callback_data: "be:claude" }, { text: "Cursor Agent", callback_data: "be:cursor" }],
1218
+ ] } });
1219
+ });
1220
+
1144
1221
  bot.onText(/\/mode$/, async (msg) => {
1145
1222
  if (!isAuthorized(msg)) return;
1146
1223
  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 +1253,10 @@ bot.onText(/\/stop/, async (msg) => {
1176
1253
  bot.onText(/\/status/, (msg) => {
1177
1254
  if (!isAuthorized(msg)) return;
1178
1255
  if (!currentSession) return send("No session.", { keyboard: { inline_keyboard: [[{ text: "Pick", callback_data: "show:projects" }]] } });
1256
+ const backendLabel = settings.backend === "cursor" ? "Cursor Agent" : "Claude Code";
1179
1257
  send([
1180
1258
  `Project: ${currentSession.name}`,
1259
+ `Backend: ${backendLabel}`,
1181
1260
  `Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}`,
1182
1261
  `Vault: ${vault.isUnlocked() ? "unlocked" : "locked"} | Crons: ${activeCrons.size}`,
1183
1262
  runningProcess ? "Working..." : "Ready.",
@@ -1317,6 +1396,16 @@ bot.on("callback_query", async (q) => {
1317
1396
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
1318
1397
  if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
1319
1398
  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; }
1399
+ if (d.startsWith("be:")) {
1400
+ const be = d.slice(3);
1401
+ if (be === "cursor" && !resolvedCursorPath) { await send("Cursor Agent CLI not found."); return; }
1402
+ settings.backend = be;
1403
+ settings.model = null;
1404
+ saveState();
1405
+ const label = be === "cursor" ? "Cursor Agent" : "Claude Code";
1406
+ await send(`Switched to ${label}.`);
1407
+ return;
1408
+ }
1320
1409
 
1321
1410
  // Mode switching — writes mode file and restarts bot
1322
1411
  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.1",
4
4
  "description": "Your always-on AI coding assistant — Claude Code via Telegram",
5
5
  "main": "bot.js",
6
6
  "bin": {