@inetafrica/open-claudia 1.9.1 → 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 ADDED
@@ -0,0 +1,45 @@
1
+ # Changelog
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
+
10
+ ## v1.9.2
11
+ - Fix: show what's new after upgrade
12
+ - Startup message shows version
13
+
14
+ ## v1.9.1
15
+ - Fix: duplicate messages — progress message now edited instead of sending a second copy
16
+
17
+ ## v1.9.0
18
+ - Force password change on first web UI login
19
+ - Password complexity requirements (12+ chars, uppercase, lowercase, number, symbol)
20
+
21
+ ## v1.8.1
22
+ - Web UI accepts WEB_PASSWORD env var for managed deployments
23
+ - Config API whitelist (only safe keys editable)
24
+ - Stronger password entropy (32 chars)
25
+
26
+ ## v1.7.4
27
+ - Run as non-root user in Docker (Claude Code security requirement)
28
+ - Numeric UID 1001 for K8s compatibility
29
+
30
+ ## v1.6.0
31
+ - Agent mode: non-blocking side conversations while tasks run
32
+ - /mode command to switch between direct and agent modes
33
+
34
+ ## v1.5.0
35
+ - Robust message delivery with retry on replyTo failure
36
+ - Adaptive rate limiting (2s -> 5s) to avoid Telegram 429 errors
37
+ - Global error handling with Telegram notification on crash
38
+ - editMessage handles rate limits gracefully
39
+
40
+ ## v1.4.5
41
+ - Streaming progress with tool names and elapsed time
42
+ - Voice note transcription via whisper.cpp
43
+ - File and image handling
44
+ - Cron jobs for scheduled tasks
45
+ - Encrypted vault for credentials
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
  });
@@ -1018,10 +1060,23 @@ bot.onText(/\/upgrade$/, async (msg) => {
1018
1060
  cwd: process.env.HOME || require("os").homedir(),
1019
1061
  env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() },
1020
1062
  });
1021
- // Read version from newly installed package
1063
+ // Read version and changelog from newly installed package
1022
1064
  const root = execSync("npm root -g", { encoding: "utf-8", cwd: process.env.HOME || require("os").homedir(), env: { ...process.env, PATH: FULL_PATH } }).trim();
1023
1065
  const newPkg = JSON.parse(fs.readFileSync(path.join(root, "@inetafrica", "open-claudia", "package.json"), "utf-8"));
1024
- await send(`Installed v${newPkg.version}. Going offline to restart...`);
1066
+ let whatsNew = "";
1067
+ try {
1068
+ const changelog = fs.readFileSync(path.join(root, "@inetafrica", "open-claudia", "CHANGELOG.md"), "utf-8");
1069
+ const versionHeader = `## v${newPkg.version}`;
1070
+ const start = changelog.indexOf(versionHeader);
1071
+ if (start >= 0) {
1072
+ const afterHeader = changelog.slice(start + versionHeader.length);
1073
+ const nextVersion = afterHeader.indexOf("\n## ");
1074
+ const section = nextVersion >= 0 ? afterHeader.slice(0, nextVersion) : afterHeader;
1075
+ whatsNew = section.trim();
1076
+ }
1077
+ } catch (e) { /* no changelog */ }
1078
+ const msg = `Installed v${newPkg.version}.${whatsNew ? `\n\nWhat's new:\n${whatsNew}` : ""}\n\nGoing offline to restart...`;
1079
+ await send(msg);
1025
1080
  } catch (e) {
1026
1081
  const errOutput = (e.stdout || e.stderr || e.message || "").slice(-500);
1027
1082
  await send(`Upgrade failed:\n${errOutput}`);
@@ -1081,10 +1136,38 @@ bot.onText(/\/budget$/, (msg) => {
1081
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"}`); });
1082
1137
 
1083
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."); });
1084
- 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); });
1085
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 }); });
1086
1141
  bot.onText(/\/worktree$/, (msg) => { if (!isAuthorized(msg)) return; settings.worktree = !settings.worktree; send(settings.worktree ? "Worktree on." : "Worktree off."); });
1087
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
+
1088
1171
  bot.onText(/\/mode$/, async (msg) => {
1089
1172
  if (!isAuthorized(msg)) return;
1090
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.", {
@@ -1120,8 +1203,10 @@ bot.onText(/\/stop/, async (msg) => {
1120
1203
  bot.onText(/\/status/, (msg) => {
1121
1204
  if (!isAuthorized(msg)) return;
1122
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";
1123
1207
  const lines = [
1124
1208
  `Project: ${currentSession.name} (agent mode)`,
1209
+ `Backend: ${backendLabel}`,
1125
1210
  `Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}`,
1126
1211
  `Vault: ${vault.isUnlocked() ? "unlocked" : "locked"} | Crons: ${activeCrons.size}`,
1127
1212
  ];
@@ -1268,6 +1353,16 @@ bot.on("callback_query", async (q) => {
1268
1353
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
1269
1354
  if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
1270
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
+ }
1271
1366
 
1272
1367
  // Mode switching
1273
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
 
@@ -1060,10 +1102,24 @@ bot.onText(/\/upgrade$/, async (msg) => {
1060
1102
  cwd: process.env.HOME || require("os").homedir(),
1061
1103
  env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() },
1062
1104
  });
1063
- // Read version from newly installed package
1105
+ // Read version and changelog from newly installed package
1064
1106
  const root = execSync("npm root -g", { encoding: "utf-8", cwd: process.env.HOME || require("os").homedir(), env: { ...process.env, PATH: FULL_PATH } }).trim();
1065
1107
  const newPkg = JSON.parse(fs.readFileSync(path.join(root, "@inetafrica", "open-claudia", "package.json"), "utf-8"));
1066
- await send(`Installed v${newPkg.version}. Going offline to restart...`);
1108
+ // Extract current version's changelog entry
1109
+ let whatsNew = "";
1110
+ try {
1111
+ const changelog = fs.readFileSync(path.join(root, "@inetafrica", "open-claudia", "CHANGELOG.md"), "utf-8");
1112
+ const versionHeader = `## v${newPkg.version}`;
1113
+ const start = changelog.indexOf(versionHeader);
1114
+ if (start >= 0) {
1115
+ const afterHeader = changelog.slice(start + versionHeader.length);
1116
+ const nextVersion = afterHeader.indexOf("\n## ");
1117
+ const section = nextVersion >= 0 ? afterHeader.slice(0, nextVersion) : afterHeader;
1118
+ whatsNew = section.trim();
1119
+ }
1120
+ } catch (e) { /* no changelog */ }
1121
+ const msg = `Installed v${newPkg.version}.${whatsNew ? `\n\nWhat's new:\n${whatsNew}` : ""}\n\nGoing offline to restart...`;
1122
+ await send(msg);
1067
1123
  } catch (e) {
1068
1124
  const errOutput = (e.stdout || e.stderr || e.message || "").slice(-500);
1069
1125
  await send(`Upgrade failed:\n${errOutput}`);
@@ -1123,10 +1179,38 @@ bot.onText(/\/budget$/, (msg) => {
1123
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"}`); });
1124
1180
 
1125
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."); });
1126
- 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); });
1127
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 }); });
1128
1184
  bot.onText(/\/worktree$/, (msg) => { if (!isAuthorized(msg)) return; settings.worktree = !settings.worktree; send(settings.worktree ? "Worktree on." : "Worktree off."); });
1129
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
+
1130
1214
  bot.onText(/\/mode$/, async (msg) => {
1131
1215
  if (!isAuthorized(msg)) return;
1132
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.", {
@@ -1162,8 +1246,10 @@ bot.onText(/\/stop/, async (msg) => {
1162
1246
  bot.onText(/\/status/, (msg) => {
1163
1247
  if (!isAuthorized(msg)) return;
1164
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";
1165
1250
  send([
1166
1251
  `Project: ${currentSession.name}`,
1252
+ `Backend: ${backendLabel}`,
1167
1253
  `Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}`,
1168
1254
  `Vault: ${vault.isUnlocked() ? "unlocked" : "locked"} | Crons: ${activeCrons.size}`,
1169
1255
  runningProcess ? "Working..." : "Ready.",
@@ -1303,6 +1389,16 @@ bot.on("callback_query", async (q) => {
1303
1389
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
1304
1390
  if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
1305
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
+ }
1306
1402
 
1307
1403
  // Mode switching — writes mode file and restarts bot
1308
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.1",
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": {
@@ -25,7 +25,8 @@
25
25
  "docker-entrypoint.sh",
26
26
  ".dockerignore",
27
27
  ".env.example",
28
- "README.md"
28
+ "README.md",
29
+ "CHANGELOG.md"
29
30
  ],
30
31
  "keywords": [
31
32
  "claude",