@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.
Files changed (2) hide show
  1. package/bot.js +195 -16
  2. 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
- else await send("Nothing running.");
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
- send([
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
- runningProcess ? "Working..." : "Ready.",
1051
- ].join("\n"));
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 runClaude(transcript, currentSession.dir, msg.message_id);
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 runClaude(t, currentSession.dir, msg.message_id);
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 runClaude(`Image at ${p}\n\nView it, then: ${caption}`, currentSession.dir, msg.message_id);
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 runClaude(prompt, currentSession.dir, msg.message_id);
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 runClaude(prompt, currentSession.dir, msg.message_id);
1558
+ await runClaudeRouted(prompt, currentSession.dir, msg.message_id);
1380
1559
  });
1381
1560
 
1382
1561
  // ── Startup ─────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Your always-on AI coding assistant — Claude Code via Telegram",
5
5
  "main": "bot.js",
6
6
  "bin": {