@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 +1 -0
- package/CHANGELOG.md +7 -0
- package/README.md +96 -23
- package/bot-agent.js +96 -7
- package/bot.js +98 -9
- package/package.json +1 -1
package/.env.example
CHANGED
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.
|
|
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
|
|
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
|
|
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
|
|
20
|
-
- **Plan mode, effort levels, budgets** — full
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
| `/
|
|
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.
|
|
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
|
|
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
|
|
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 `
|
|
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.
|
|
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
|
-
|
|
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
|
|
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) {
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
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",
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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:")) {
|