@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 +1 -0
- package/CHANGELOG.md +45 -0
- package/bot-agent.js +102 -7
- package/bot.js +105 -9
- package/package.json +3 -2
package/.env.example
CHANGED
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
|
|
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
|
});
|
|
@@ -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
|
-
|
|
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 (!
|
|
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
|
|
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
|
|
|
@@ -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
|
-
|
|
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 (!
|
|
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.
|
|
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",
|