@inetafrica/open-claudia 1.5.0 → 1.6.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/bot.js +195 -16
- package/package.json +1 -1
package/bot.js
CHANGED
|
@@ -170,6 +170,7 @@ bot.setMyCommands([
|
|
|
170
170
|
{ command: "compact", description: "Summarize conversation context" },
|
|
171
171
|
{ command: "continue", description: "Resume last conversation" },
|
|
172
172
|
{ command: "worktree", description: "Toggle isolated git branch" },
|
|
173
|
+
{ command: "agents", description: "Toggle direct/parallel agent mode" },
|
|
173
174
|
{ command: "cron", description: "Manage scheduled tasks" },
|
|
174
175
|
{ command: "vault", description: "Manage credentials (password required)" },
|
|
175
176
|
{ command: "soul", description: "View/edit assistant identity" },
|
|
@@ -267,7 +268,8 @@ const savedState = loadState();
|
|
|
267
268
|
|
|
268
269
|
// ── State ───────────────────────────────────────────────────────────
|
|
269
270
|
let currentSession = savedState.currentSession || null;
|
|
270
|
-
let runningProcess = null;
|
|
271
|
+
let runningProcess = null; // Primary process (direct mode)
|
|
272
|
+
let runningTasks = new Map(); // taskId -> { proc, statusMessageId, streamInterval, startTime } (parallel mode)
|
|
271
273
|
let statusMessageId = null;
|
|
272
274
|
let streamBuffer = "";
|
|
273
275
|
let streamInterval = null;
|
|
@@ -277,13 +279,14 @@ let activeCrons = new Map();
|
|
|
277
279
|
let pendingVaultUnlock = false; // Waiting for password
|
|
278
280
|
let pendingVaultAction = null; // What to do after unlock
|
|
279
281
|
let isFirstMessage = !lastSessionId; // Track if this is first message in session
|
|
282
|
+
let taskCounter = 0;
|
|
280
283
|
|
|
281
284
|
let settings = savedState.settings || {
|
|
282
|
-
model: null, effort: null, budget: null, permissionMode: null, worktree: false,
|
|
285
|
+
model: null, effort: null, budget: null, permissionMode: null, worktree: false, agentMode: "direct",
|
|
283
286
|
};
|
|
284
287
|
|
|
285
288
|
function resetSettings() {
|
|
286
|
-
settings = { model: null, effort: null, budget: null, permissionMode: null, worktree: false };
|
|
289
|
+
settings = { model: null, effort: null, budget: null, permissionMode: null, worktree: false, agentMode: settings.agentMode };
|
|
287
290
|
}
|
|
288
291
|
|
|
289
292
|
function isAuthorized(msg) {
|
|
@@ -653,6 +656,19 @@ function parseStreamEvents(data) {
|
|
|
653
656
|
return events;
|
|
654
657
|
}
|
|
655
658
|
|
|
659
|
+
/**
|
|
660
|
+
* Route to the appropriate runner based on agent mode setting.
|
|
661
|
+
* In direct mode: serial execution with shared session.
|
|
662
|
+
* In parallel mode: concurrent independent processes.
|
|
663
|
+
* Commands like /compact and /continue always use direct mode (they need session context).
|
|
664
|
+
*/
|
|
665
|
+
async function runClaudeRouted(prompt, cwd, replyToMsgId, opts = {}) {
|
|
666
|
+
if (opts.forceDirect || settings.agentMode !== "parallel") {
|
|
667
|
+
return runClaude(prompt, cwd, replyToMsgId, opts);
|
|
668
|
+
}
|
|
669
|
+
return runClaudeParallel(prompt, cwd, replyToMsgId, opts);
|
|
670
|
+
}
|
|
671
|
+
|
|
656
672
|
function buildClaudeArgs(prompt, opts = {}) {
|
|
657
673
|
const args = ["-p", "--verbose", "--output-format", "stream-json",
|
|
658
674
|
"--append-system-prompt", buildSystemPrompt()];
|
|
@@ -800,6 +816,126 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
800
816
|
});
|
|
801
817
|
}
|
|
802
818
|
|
|
819
|
+
/**
|
|
820
|
+
* Run Claude in parallel mode — each call gets its own independent process.
|
|
821
|
+
* No queueing, no shared session. Multiple tasks can run simultaneously.
|
|
822
|
+
*/
|
|
823
|
+
async function runClaudeParallel(prompt, cwd, replyToMsgId, opts = {}) {
|
|
824
|
+
bot.sendChatAction(CHAT_ID, "typing").catch(() => {});
|
|
825
|
+
|
|
826
|
+
const taskId = `task-${++taskCounter}`;
|
|
827
|
+
let taskStatusMsgId = null;
|
|
828
|
+
let taskStreamBuf = "";
|
|
829
|
+
let taskText = "";
|
|
830
|
+
let taskToolUses = [];
|
|
831
|
+
let taskCurrentTool = null;
|
|
832
|
+
let taskCurrentToolDetail = "";
|
|
833
|
+
let taskSessionId = null;
|
|
834
|
+
|
|
835
|
+
// Build args — always fresh session in parallel mode (no --resume)
|
|
836
|
+
const args = ["-p", "--verbose", "--output-format", "stream-json",
|
|
837
|
+
"--append-system-prompt", buildSystemPrompt()];
|
|
838
|
+
if (settings.model) args.push("--model", settings.model);
|
|
839
|
+
if (settings.effort) args.push("--effort", settings.effort);
|
|
840
|
+
if (settings.budget) args.push("--max-budget-usd", String(settings.budget));
|
|
841
|
+
if (settings.permissionMode) args.push("--permission-mode", settings.permissionMode);
|
|
842
|
+
else args.push("--dangerously-skip-permissions");
|
|
843
|
+
if (settings.worktree) args.push("--worktree");
|
|
844
|
+
args.push(prompt);
|
|
845
|
+
|
|
846
|
+
const proc = spawn(CLAUDE_PATH, args, {
|
|
847
|
+
cwd,
|
|
848
|
+
env: { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME },
|
|
849
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
850
|
+
detached: process.platform !== "win32",
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const startTime = Date.now();
|
|
854
|
+
let taskInterval = null;
|
|
855
|
+
let lastUpdate = "";
|
|
856
|
+
|
|
857
|
+
// Adaptive progress updates
|
|
858
|
+
const scheduleTaskUpdate = () => {
|
|
859
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
860
|
+
const interval = elapsed > 120 ? 5000 : 2000;
|
|
861
|
+
taskInterval = setTimeout(doTaskUpdate, interval);
|
|
862
|
+
};
|
|
863
|
+
const doTaskUpdate = async () => {
|
|
864
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
865
|
+
try {
|
|
866
|
+
bot.sendChatAction(CHAT_ID, "typing").catch(() => {});
|
|
867
|
+
const display = formatProgress(taskText, taskToolUses, taskCurrentTool, elapsed, taskCurrentToolDetail);
|
|
868
|
+
if (display && display !== lastUpdate) {
|
|
869
|
+
if (!taskStatusMsgId && taskText) {
|
|
870
|
+
taskStatusMsgId = await send(display.length > 4000 ? display.slice(-4000) : display, { replyTo: replyToMsgId });
|
|
871
|
+
} else if (taskStatusMsgId) {
|
|
872
|
+
await editMessage(taskStatusMsgId, display.length > 4000 ? display.slice(-4000) : display);
|
|
873
|
+
}
|
|
874
|
+
lastUpdate = display;
|
|
875
|
+
}
|
|
876
|
+
} catch (e) {
|
|
877
|
+
console.error(`Task ${taskId} progress error:`, e.message);
|
|
878
|
+
}
|
|
879
|
+
if (runningTasks.has(taskId)) scheduleTaskUpdate();
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
runningTasks.set(taskId, { proc, startTime, prompt: prompt.slice(0, 50) });
|
|
883
|
+
scheduleTaskUpdate();
|
|
884
|
+
|
|
885
|
+
proc.stdout.on("data", (data) => {
|
|
886
|
+
taskStreamBuf += data.toString();
|
|
887
|
+
const events = parseStreamEvents(taskStreamBuf);
|
|
888
|
+
const lastNewline = taskStreamBuf.lastIndexOf("\n");
|
|
889
|
+
taskStreamBuf = lastNewline >= 0 ? taskStreamBuf.slice(lastNewline + 1) : taskStreamBuf;
|
|
890
|
+
for (const evt of events) {
|
|
891
|
+
if (evt.type === "assistant" && evt.message?.content) {
|
|
892
|
+
for (const block of evt.message.content) {
|
|
893
|
+
if (block.type === "text") taskText += block.text;
|
|
894
|
+
else if (block.type === "tool_use") {
|
|
895
|
+
taskCurrentTool = block.name;
|
|
896
|
+
taskToolUses.push(block.name);
|
|
897
|
+
const input = block.input || {};
|
|
898
|
+
if (block.name === "Bash" && input.command) taskCurrentToolDetail = input.command.slice(0, 80);
|
|
899
|
+
else if ((block.name === "Read" || block.name === "Edit" || block.name === "Write") && input.file_path)
|
|
900
|
+
taskCurrentToolDetail = input.file_path.split("/").slice(-2).join("/");
|
|
901
|
+
else if (block.name === "Grep" && input.pattern) taskCurrentToolDetail = input.pattern.slice(0, 40);
|
|
902
|
+
else if (block.name === "Glob" && input.pattern) taskCurrentToolDetail = input.pattern;
|
|
903
|
+
else taskCurrentToolDetail = "";
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (evt.type === "result" && evt.session_id) taskSessionId = evt.session_id;
|
|
908
|
+
if (evt.type === "result" && evt.result) taskText = evt.result;
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
proc.stderr.on("data", (d) => console.error(`Task ${taskId} STDERR:`, d.toString()));
|
|
913
|
+
|
|
914
|
+
proc.on("close", async (code) => {
|
|
915
|
+
runningTasks.delete(taskId);
|
|
916
|
+
clearTimeout(taskInterval);
|
|
917
|
+
try {
|
|
918
|
+
const finalText = taskText || "(no output)";
|
|
919
|
+
const chunks = splitMessage(finalText);
|
|
920
|
+
const sent = await send(chunks[0], { replyTo: replyToMsgId });
|
|
921
|
+
if (!sent) await send(chunks[0]);
|
|
922
|
+
for (let i = 1; i < chunks.length; i++) await send(chunks[i]);
|
|
923
|
+
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
924
|
+
} catch (e) {
|
|
925
|
+
console.error(`Task ${taskId} final delivery failed:`, e.message);
|
|
926
|
+
await send("Task completed but failed to deliver the response.");
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
proc.on("error", async (err) => {
|
|
931
|
+
runningTasks.delete(taskId);
|
|
932
|
+
clearTimeout(taskInterval);
|
|
933
|
+
await send(`Task error: ${err.message}`, { replyTo: replyToMsgId });
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
return taskId;
|
|
937
|
+
}
|
|
938
|
+
|
|
803
939
|
async function runClaudeSilent(prompt, cwd, label) {
|
|
804
940
|
return new Promise((resolve) => {
|
|
805
941
|
const args = ["-p", "--output-format", "text", "--verbose",
|
|
@@ -910,7 +1046,7 @@ bot.onText(/\/help/, (msg) => {
|
|
|
910
1046
|
if (!isAuthorized(msg)) return;
|
|
911
1047
|
send([
|
|
912
1048
|
"Session: /session /sessions /projects /continue /status /stop /end",
|
|
913
|
-
"Settings: /model /effort /budget /plan /compact /worktree",
|
|
1049
|
+
"Settings: /model /effort /budget /plan /compact /worktree /agents",
|
|
914
1050
|
"Automation: /cron /vault /soul",
|
|
915
1051
|
"System: /restart /upgrade",
|
|
916
1052
|
"",
|
|
@@ -1018,17 +1154,45 @@ bot.onText(/\/compact/, async (msg) => { if (!isAuthorized(msg)) return; if (!re
|
|
|
1018
1154
|
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 }); });
|
|
1019
1155
|
bot.onText(/\/worktree$/, (msg) => { if (!isAuthorized(msg)) return; settings.worktree = !settings.worktree; send(settings.worktree ? "Worktree on." : "Worktree off."); });
|
|
1020
1156
|
|
|
1157
|
+
bot.onText(/\/agents$/, (msg) => {
|
|
1158
|
+
if (!isAuthorized(msg)) return;
|
|
1159
|
+
const mode = settings.agentMode || "direct";
|
|
1160
|
+
send(`Agent mode: *${mode}*\n\n` +
|
|
1161
|
+
`*direct* — one task at a time, shared session context, queues messages\n` +
|
|
1162
|
+
`*parallel* — concurrent tasks, independent sessions, no queue`, {
|
|
1163
|
+
parseMode: "Markdown",
|
|
1164
|
+
keyboard: { inline_keyboard: [
|
|
1165
|
+
[{ text: mode === "direct" ? "Direct (active)" : "Switch to Direct", callback_data: "am:direct" },
|
|
1166
|
+
{ text: mode === "parallel" ? "Parallel (active)" : "Switch to Parallel", callback_data: "am:parallel" }],
|
|
1167
|
+
] },
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1021
1171
|
bot.onText(/\/stop/, async (msg) => {
|
|
1022
1172
|
if (!isAuthorized(msg)) return;
|
|
1173
|
+
|
|
1174
|
+
// Stop parallel tasks
|
|
1175
|
+
if (runningTasks.size > 0) {
|
|
1176
|
+
for (const [taskId, task] of runningTasks) {
|
|
1177
|
+
try { process.kill(-task.proc.pid, "SIGTERM"); } catch (e) {
|
|
1178
|
+
try { task.proc.kill("SIGTERM"); } catch (e2) {}
|
|
1179
|
+
}
|
|
1180
|
+
setTimeout(() => { try { process.kill(-task.proc.pid, "SIGKILL"); } catch (e) {} }, 3000);
|
|
1181
|
+
}
|
|
1182
|
+
const count = runningTasks.size;
|
|
1183
|
+
runningTasks.clear();
|
|
1184
|
+
await send(`Cancelled ${count} task${count > 1 ? "s" : ""}.`);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Stop direct mode process
|
|
1023
1189
|
if (runningProcess) {
|
|
1024
1190
|
const pid = runningProcess.pid;
|
|
1025
1191
|
try {
|
|
1026
|
-
// Kill entire process group to catch child processes (dev servers, etc.)
|
|
1027
1192
|
process.kill(-pid, "SIGTERM");
|
|
1028
1193
|
} catch (e) {
|
|
1029
1194
|
try { runningProcess.kill("SIGTERM"); } catch (e2) {}
|
|
1030
1195
|
}
|
|
1031
|
-
// Force kill after 3 seconds if still alive
|
|
1032
1196
|
setTimeout(() => {
|
|
1033
1197
|
try { process.kill(-pid, "SIGKILL"); } catch (e) {}
|
|
1034
1198
|
}, 3000);
|
|
@@ -1036,19 +1200,33 @@ bot.onText(/\/stop/, async (msg) => {
|
|
|
1036
1200
|
if (streamInterval) clearTimeout(streamInterval);
|
|
1037
1201
|
messageQueue = [];
|
|
1038
1202
|
await send("Cancelled.");
|
|
1203
|
+
return;
|
|
1039
1204
|
}
|
|
1040
|
-
|
|
1205
|
+
|
|
1206
|
+
await send("Nothing running.");
|
|
1041
1207
|
});
|
|
1042
1208
|
|
|
1043
1209
|
bot.onText(/\/status/, (msg) => {
|
|
1044
1210
|
if (!isAuthorized(msg)) return;
|
|
1045
1211
|
if (!currentSession) return send("No session.", { keyboard: { inline_keyboard: [[{ text: "Pick", callback_data: "show:projects" }]] } });
|
|
1046
|
-
|
|
1212
|
+
const mode = settings.agentMode || "direct";
|
|
1213
|
+
const lines = [
|
|
1047
1214
|
`Project: ${currentSession.name}`,
|
|
1048
|
-
`Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}`,
|
|
1215
|
+
`Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"} | Mode: ${mode}`,
|
|
1049
1216
|
`Vault: ${vault.isUnlocked() ? "unlocked" : "locked"} | Crons: ${activeCrons.size}`,
|
|
1050
|
-
|
|
1051
|
-
|
|
1217
|
+
];
|
|
1218
|
+
if (mode === "parallel" && runningTasks.size > 0) {
|
|
1219
|
+
lines.push(`Running: ${runningTasks.size} task${runningTasks.size > 1 ? "s" : ""}`);
|
|
1220
|
+
for (const [id, task] of runningTasks) {
|
|
1221
|
+
const elapsed = Math.floor((Date.now() - task.startTime) / 1000);
|
|
1222
|
+
lines.push(` ${id}: ${task.prompt}... (${elapsed}s)`);
|
|
1223
|
+
}
|
|
1224
|
+
} else if (runningProcess) {
|
|
1225
|
+
lines.push("Working...");
|
|
1226
|
+
} else {
|
|
1227
|
+
lines.push("Ready.");
|
|
1228
|
+
}
|
|
1229
|
+
send(lines.join("\n"));
|
|
1052
1230
|
});
|
|
1053
1231
|
|
|
1054
1232
|
bot.onText(/\/end/, (msg) => {
|
|
@@ -1184,6 +1362,7 @@ bot.on("callback_query", async (q) => {
|
|
|
1184
1362
|
if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
|
|
1185
1363
|
if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
|
|
1186
1364
|
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; }
|
|
1365
|
+
if (d.startsWith("am:")) { settings.agentMode = d.slice(3); saveState(); await send(`Agent mode: ${settings.agentMode}`); return; }
|
|
1187
1366
|
|
|
1188
1367
|
// Cron presets
|
|
1189
1368
|
if (d.startsWith("cp:") && d !== "cp:clear") {
|
|
@@ -1217,7 +1396,7 @@ bot.on("voice", async (msg) => {
|
|
|
1217
1396
|
try { fs.unlinkSync(oggPath); } catch (e) {}
|
|
1218
1397
|
if (!transcript) return send("Couldn't transcribe. Try typing it.");
|
|
1219
1398
|
await send(`Heard: "${transcript}"`, { replyTo: msg.message_id });
|
|
1220
|
-
await
|
|
1399
|
+
await runClaudeRouted(transcript, currentSession.dir, msg.message_id);
|
|
1221
1400
|
} catch (err) { await send(`Voice failed: ${err.message}`); }
|
|
1222
1401
|
});
|
|
1223
1402
|
|
|
@@ -1232,7 +1411,7 @@ bot.on("audio", async (msg) => {
|
|
|
1232
1411
|
try { fs.unlinkSync(p); } catch (e) {}
|
|
1233
1412
|
if (!t) return send("Couldn't transcribe.");
|
|
1234
1413
|
await send(`Heard: "${t}"`, { replyTo: msg.message_id });
|
|
1235
|
-
await
|
|
1414
|
+
await runClaudeRouted(t, currentSession.dir, msg.message_id);
|
|
1236
1415
|
} catch (err) { await send(`Audio failed: ${err.message}`); }
|
|
1237
1416
|
});
|
|
1238
1417
|
|
|
@@ -1243,7 +1422,7 @@ bot.on("photo", async (msg) => {
|
|
|
1243
1422
|
try {
|
|
1244
1423
|
const p = await downloadFile(msg.photo[msg.photo.length - 1].file_id, ".jpg");
|
|
1245
1424
|
const caption = msg.caption || "Describe this image. If code/UI/error — explain and fix.";
|
|
1246
|
-
await
|
|
1425
|
+
await runClaudeRouted(`Image at ${p}\n\nView it, then: ${caption}`, currentSession.dir, msg.message_id);
|
|
1247
1426
|
} catch (err) { await send(`Image failed: ${err.message}`); }
|
|
1248
1427
|
});
|
|
1249
1428
|
|
|
@@ -1271,7 +1450,7 @@ bot.on("document", async (msg) => {
|
|
|
1271
1450
|
prompt = `File received: ${fileName} (${mime})\nSaved at: ${savePath}\n\nRead this file and ${caption || "summarize its contents. If it's code, explain what it does. If it's a document, give key points."}`;
|
|
1272
1451
|
}
|
|
1273
1452
|
await send(`File saved: ${fileName}`, { replyTo: msg.message_id });
|
|
1274
|
-
await
|
|
1453
|
+
await runClaudeRouted(prompt, currentSession.dir, msg.message_id);
|
|
1275
1454
|
} catch (err) { await send(`Failed: ${err.message}`); }
|
|
1276
1455
|
});
|
|
1277
1456
|
|
|
@@ -1376,7 +1555,7 @@ bot.on("message", async (msg) => {
|
|
|
1376
1555
|
}
|
|
1377
1556
|
}
|
|
1378
1557
|
|
|
1379
|
-
await
|
|
1558
|
+
await runClaudeRouted(prompt, currentSession.dir, msg.message_id);
|
|
1380
1559
|
});
|
|
1381
1560
|
|
|
1382
1561
|
// ── Startup ─────────────────────────────────────────────────────────
|