@inetafrica/open-claudia 1.4.5 → 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 +317 -55
- package/package.json +1 -1
package/bot.js
CHANGED
|
@@ -7,10 +7,40 @@ const cron = require("node-cron");
|
|
|
7
7
|
const Vault = require("./vault");
|
|
8
8
|
const CONFIG_DIR = require("./config-dir");
|
|
9
9
|
|
|
10
|
-
// ── Graceful shutdown
|
|
10
|
+
// ── Graceful shutdown & error handling ─────────────────────────────
|
|
11
11
|
process.on("SIGINT", () => process.exit(0));
|
|
12
12
|
process.on("SIGTERM", () => process.exit(0));
|
|
13
13
|
|
|
14
|
+
// Notify user of crashes via Telegram before exiting
|
|
15
|
+
function notifyError(label, err) {
|
|
16
|
+
const msg = `${label}: ${err?.message || err}`.slice(0, 1000);
|
|
17
|
+
console.error(msg, err?.stack || "");
|
|
18
|
+
try {
|
|
19
|
+
// Synchronous-style notification using the Telegram API directly
|
|
20
|
+
const token = process.env.TELEGRAM_BOT_TOKEN;
|
|
21
|
+
const chatId = process.env.TELEGRAM_CHAT_ID?.split(",")[0];
|
|
22
|
+
if (token && chatId) {
|
|
23
|
+
const data = JSON.stringify({ chat_id: chatId, text: msg });
|
|
24
|
+
const req = require("https").request({
|
|
25
|
+
hostname: "api.telegram.org", path: `/bot${token}/sendMessage`,
|
|
26
|
+
method: "POST", headers: { "Content-Type": "application/json", "Content-Length": data.length },
|
|
27
|
+
});
|
|
28
|
+
req.write(data);
|
|
29
|
+
req.end();
|
|
30
|
+
}
|
|
31
|
+
} catch (e) { /* last resort — ignore */ }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
process.on("uncaughtException", (err) => {
|
|
35
|
+
notifyError("Uncaught exception", err);
|
|
36
|
+
// Give the notification a moment to send before exiting
|
|
37
|
+
setTimeout(() => process.exit(1), 2000);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
process.on("unhandledRejection", (reason) => {
|
|
41
|
+
notifyError("Unhandled rejection", reason);
|
|
42
|
+
});
|
|
43
|
+
|
|
14
44
|
// ── Load Config from .env ───────────────────────────────────────────
|
|
15
45
|
function loadEnv() {
|
|
16
46
|
const envPath = path.join(CONFIG_DIR, ".env");
|
|
@@ -140,6 +170,7 @@ bot.setMyCommands([
|
|
|
140
170
|
{ command: "compact", description: "Summarize conversation context" },
|
|
141
171
|
{ command: "continue", description: "Resume last conversation" },
|
|
142
172
|
{ command: "worktree", description: "Toggle isolated git branch" },
|
|
173
|
+
{ command: "agents", description: "Toggle direct/parallel agent mode" },
|
|
143
174
|
{ command: "cron", description: "Manage scheduled tasks" },
|
|
144
175
|
{ command: "vault", description: "Manage credentials (password required)" },
|
|
145
176
|
{ command: "soul", description: "View/edit assistant identity" },
|
|
@@ -237,7 +268,8 @@ const savedState = loadState();
|
|
|
237
268
|
|
|
238
269
|
// ── State ───────────────────────────────────────────────────────────
|
|
239
270
|
let currentSession = savedState.currentSession || null;
|
|
240
|
-
let runningProcess = null;
|
|
271
|
+
let runningProcess = null; // Primary process (direct mode)
|
|
272
|
+
let runningTasks = new Map(); // taskId -> { proc, statusMessageId, streamInterval, startTime } (parallel mode)
|
|
241
273
|
let statusMessageId = null;
|
|
242
274
|
let streamBuffer = "";
|
|
243
275
|
let streamInterval = null;
|
|
@@ -247,13 +279,14 @@ let activeCrons = new Map();
|
|
|
247
279
|
let pendingVaultUnlock = false; // Waiting for password
|
|
248
280
|
let pendingVaultAction = null; // What to do after unlock
|
|
249
281
|
let isFirstMessage = !lastSessionId; // Track if this is first message in session
|
|
282
|
+
let taskCounter = 0;
|
|
250
283
|
|
|
251
284
|
let settings = savedState.settings || {
|
|
252
|
-
model: null, effort: null, budget: null, permissionMode: null, worktree: false,
|
|
285
|
+
model: null, effort: null, budget: null, permissionMode: null, worktree: false, agentMode: "direct",
|
|
253
286
|
};
|
|
254
287
|
|
|
255
288
|
function resetSettings() {
|
|
256
|
-
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 };
|
|
257
290
|
}
|
|
258
291
|
|
|
259
292
|
function isAuthorized(msg) {
|
|
@@ -524,19 +557,41 @@ async function send(text, opts = {}) {
|
|
|
524
557
|
if (opts.parseMode) o.parse_mode = opts.parseMode;
|
|
525
558
|
if (opts.keyboard) o.reply_markup = opts.keyboard;
|
|
526
559
|
if (opts.replyTo) o.reply_to_message_id = opts.replyTo;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
560
|
+
|
|
561
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
562
|
+
try {
|
|
563
|
+
const msg = await bot.sendMessage(CHAT_ID, text, o);
|
|
564
|
+
return msg.message_id;
|
|
565
|
+
} catch (e) {
|
|
566
|
+
const errMsg = e.message || "";
|
|
567
|
+
|
|
568
|
+
// replyTo message was deleted or not found — retry without it
|
|
569
|
+
if (o.reply_to_message_id && errMsg.includes("message to be replied not found")) {
|
|
570
|
+
delete o.reply_to_message_id;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Rate limited — wait and retry
|
|
575
|
+
const retryMatch = errMsg.match(/retry after (\d+)/i);
|
|
576
|
+
if (retryMatch) {
|
|
577
|
+
const waitSec = Math.min(parseInt(retryMatch[1], 10), 30);
|
|
578
|
+
console.error(`Send: rate limited, waiting ${waitSec}s`);
|
|
579
|
+
await new Promise((r) => setTimeout(r, waitSec * 1000));
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Parse mode failed — retry without it
|
|
584
|
+
if (opts.parseMode && o.parse_mode) {
|
|
585
|
+
delete o.parse_mode;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
console.error("Send error:", errMsg);
|
|
590
|
+
return null;
|
|
536
591
|
}
|
|
537
|
-
console.error("Send error:", e.message);
|
|
538
|
-
return null;
|
|
539
592
|
}
|
|
593
|
+
console.error("Send: exhausted retries");
|
|
594
|
+
return null;
|
|
540
595
|
}
|
|
541
596
|
|
|
542
597
|
async function editMessage(messageId, text, opts = {}) {
|
|
@@ -544,7 +599,17 @@ async function editMessage(messageId, text, opts = {}) {
|
|
|
544
599
|
const o = { chat_id: CHAT_ID, message_id: messageId };
|
|
545
600
|
if (opts.keyboard) o.reply_markup = opts.keyboard;
|
|
546
601
|
await bot.editMessageText(text, o);
|
|
547
|
-
} catch (e) {
|
|
602
|
+
} catch (e) {
|
|
603
|
+
const errMsg = e.message || "";
|
|
604
|
+
// Rate limited — skip this update (next interval will catch up)
|
|
605
|
+
if (errMsg.includes("retry after")) return;
|
|
606
|
+
// Message unchanged — ignore
|
|
607
|
+
if (errMsg.includes("message is not modified")) return;
|
|
608
|
+
// Log anything unexpected
|
|
609
|
+
if (!errMsg.includes("message to edit not found")) {
|
|
610
|
+
console.error("Edit error:", errMsg);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
548
613
|
}
|
|
549
614
|
|
|
550
615
|
function splitMessage(text, maxLen = 4000) {
|
|
@@ -591,6 +656,19 @@ function parseStreamEvents(data) {
|
|
|
591
656
|
return events;
|
|
592
657
|
}
|
|
593
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
|
+
|
|
594
672
|
function buildClaudeArgs(prompt, opts = {}) {
|
|
595
673
|
const args = ["-p", "--verbose", "--output-format", "stream-json",
|
|
596
674
|
"--append-system-prompt", buildSystemPrompt()];
|
|
@@ -634,24 +712,36 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
634
712
|
let longRunningNotified = false;
|
|
635
713
|
|
|
636
714
|
let lastUpdate = "";
|
|
637
|
-
|
|
638
|
-
|
|
715
|
+
// Adaptive update interval: 2s for first 2min, then 5s to avoid rate limits
|
|
716
|
+
const scheduleUpdate = () => {
|
|
639
717
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
718
|
+
const interval = elapsed > 120 ? 5000 : 2000;
|
|
719
|
+
streamInterval = setTimeout(updateProgress, interval);
|
|
720
|
+
};
|
|
721
|
+
const updateProgress = async () => {
|
|
722
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
723
|
+
try {
|
|
724
|
+
bot.sendChatAction(CHAT_ID, "typing").catch(() => {});
|
|
725
|
+
const display = formatProgress(assistantText, toolUses, currentTool, elapsed, currentToolDetail);
|
|
726
|
+
if (display && display !== lastUpdate) {
|
|
727
|
+
if (!statusMessageId && assistantText) {
|
|
728
|
+
statusMessageId = await send(display.length > 4000 ? display.slice(-4000) : display, { replyTo: replyToMsgId });
|
|
729
|
+
} else if (statusMessageId) {
|
|
730
|
+
await editMessage(statusMessageId, display.length > 4000 ? display.slice(-4000) : display);
|
|
731
|
+
}
|
|
732
|
+
lastUpdate = display;
|
|
646
733
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
734
|
+
// Notify after 5 minutes that it's still running
|
|
735
|
+
if (elapsed > 300 && !longRunningNotified) {
|
|
736
|
+
longRunningNotified = true;
|
|
737
|
+
await send(`Still working (${Math.floor(elapsed / 60)}min)... Send /stop to cancel.`);
|
|
738
|
+
}
|
|
739
|
+
} catch (e) {
|
|
740
|
+
console.error("Progress update error:", e.message);
|
|
653
741
|
}
|
|
654
|
-
|
|
742
|
+
if (runningProcess) scheduleUpdate();
|
|
743
|
+
};
|
|
744
|
+
scheduleUpdate();
|
|
655
745
|
|
|
656
746
|
proc.stdout.on("data", (data) => {
|
|
657
747
|
streamBuffer += data.toString();
|
|
@@ -686,16 +776,25 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
686
776
|
|
|
687
777
|
proc.on("close", async (code) => {
|
|
688
778
|
runningProcess = null;
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
779
|
+
clearTimeout(streamInterval); streamInterval = null;
|
|
780
|
+
try {
|
|
781
|
+
const finalText = assistantText || "(no output)";
|
|
782
|
+
const chunks = splitMessage(finalText);
|
|
783
|
+
// Send final result as a new message (triggers notification)
|
|
784
|
+
const sent = await send(chunks[0], { replyTo: replyToMsgId });
|
|
785
|
+
if (!sent) {
|
|
786
|
+
// Fallback: if the first send failed completely, try without any options
|
|
787
|
+
await send(chunks[0]);
|
|
788
|
+
}
|
|
789
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
790
|
+
await send(chunks[i]);
|
|
791
|
+
}
|
|
792
|
+
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
793
|
+
} catch (e) {
|
|
794
|
+
console.error("Final message delivery failed:", e.message);
|
|
795
|
+
// Last-resort attempt to notify user something went wrong
|
|
796
|
+
await send("Task completed but failed to deliver the response. Send /continue to see the result.");
|
|
697
797
|
}
|
|
698
|
-
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
699
798
|
if (settings.budget) settings.budget = null;
|
|
700
799
|
statusMessageId = null;
|
|
701
800
|
|
|
@@ -712,11 +811,131 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
712
811
|
});
|
|
713
812
|
|
|
714
813
|
proc.on("error", async (err) => {
|
|
715
|
-
runningProcess = null;
|
|
814
|
+
runningProcess = null; clearTimeout(streamInterval);
|
|
716
815
|
await send(`Error: ${err.message}`); statusMessageId = null;
|
|
717
816
|
});
|
|
718
817
|
}
|
|
719
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
|
+
|
|
720
939
|
async function runClaudeSilent(prompt, cwd, label) {
|
|
721
940
|
return new Promise((resolve) => {
|
|
722
941
|
const args = ["-p", "--output-format", "text", "--verbose",
|
|
@@ -827,7 +1046,7 @@ bot.onText(/\/help/, (msg) => {
|
|
|
827
1046
|
if (!isAuthorized(msg)) return;
|
|
828
1047
|
send([
|
|
829
1048
|
"Session: /session /sessions /projects /continue /status /stop /end",
|
|
830
|
-
"Settings: /model /effort /budget /plan /compact /worktree",
|
|
1049
|
+
"Settings: /model /effort /budget /plan /compact /worktree /agents",
|
|
831
1050
|
"Automation: /cron /vault /soul",
|
|
832
1051
|
"System: /restart /upgrade",
|
|
833
1052
|
"",
|
|
@@ -935,37 +1154,79 @@ bot.onText(/\/compact/, async (msg) => { if (!isAuthorized(msg)) return; if (!re
|
|
|
935
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 }); });
|
|
936
1155
|
bot.onText(/\/worktree$/, (msg) => { if (!isAuthorized(msg)) return; settings.worktree = !settings.worktree; send(settings.worktree ? "Worktree on." : "Worktree off."); });
|
|
937
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
|
+
|
|
938
1171
|
bot.onText(/\/stop/, async (msg) => {
|
|
939
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
|
|
940
1189
|
if (runningProcess) {
|
|
941
1190
|
const pid = runningProcess.pid;
|
|
942
1191
|
try {
|
|
943
|
-
// Kill entire process group to catch child processes (dev servers, etc.)
|
|
944
1192
|
process.kill(-pid, "SIGTERM");
|
|
945
1193
|
} catch (e) {
|
|
946
1194
|
try { runningProcess.kill("SIGTERM"); } catch (e2) {}
|
|
947
1195
|
}
|
|
948
|
-
// Force kill after 3 seconds if still alive
|
|
949
1196
|
setTimeout(() => {
|
|
950
1197
|
try { process.kill(-pid, "SIGKILL"); } catch (e) {}
|
|
951
1198
|
}, 3000);
|
|
952
1199
|
runningProcess = null;
|
|
953
|
-
if (streamInterval)
|
|
1200
|
+
if (streamInterval) clearTimeout(streamInterval);
|
|
954
1201
|
messageQueue = [];
|
|
955
1202
|
await send("Cancelled.");
|
|
1203
|
+
return;
|
|
956
1204
|
}
|
|
957
|
-
|
|
1205
|
+
|
|
1206
|
+
await send("Nothing running.");
|
|
958
1207
|
});
|
|
959
1208
|
|
|
960
1209
|
bot.onText(/\/status/, (msg) => {
|
|
961
1210
|
if (!isAuthorized(msg)) return;
|
|
962
1211
|
if (!currentSession) return send("No session.", { keyboard: { inline_keyboard: [[{ text: "Pick", callback_data: "show:projects" }]] } });
|
|
963
|
-
|
|
1212
|
+
const mode = settings.agentMode || "direct";
|
|
1213
|
+
const lines = [
|
|
964
1214
|
`Project: ${currentSession.name}`,
|
|
965
|
-
`Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"}`,
|
|
1215
|
+
`Model: ${settings.model || "default"} | Effort: ${settings.effort || "default"} | Mode: ${mode}`,
|
|
966
1216
|
`Vault: ${vault.isUnlocked() ? "unlocked" : "locked"} | Crons: ${activeCrons.size}`,
|
|
967
|
-
|
|
968
|
-
|
|
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"));
|
|
969
1230
|
});
|
|
970
1231
|
|
|
971
1232
|
bot.onText(/\/end/, (msg) => {
|
|
@@ -1101,6 +1362,7 @@ bot.on("callback_query", async (q) => {
|
|
|
1101
1362
|
if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
|
|
1102
1363
|
if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
|
|
1103
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; }
|
|
1104
1366
|
|
|
1105
1367
|
// Cron presets
|
|
1106
1368
|
if (d.startsWith("cp:") && d !== "cp:clear") {
|
|
@@ -1134,7 +1396,7 @@ bot.on("voice", async (msg) => {
|
|
|
1134
1396
|
try { fs.unlinkSync(oggPath); } catch (e) {}
|
|
1135
1397
|
if (!transcript) return send("Couldn't transcribe. Try typing it.");
|
|
1136
1398
|
await send(`Heard: "${transcript}"`, { replyTo: msg.message_id });
|
|
1137
|
-
await
|
|
1399
|
+
await runClaudeRouted(transcript, currentSession.dir, msg.message_id);
|
|
1138
1400
|
} catch (err) { await send(`Voice failed: ${err.message}`); }
|
|
1139
1401
|
});
|
|
1140
1402
|
|
|
@@ -1149,7 +1411,7 @@ bot.on("audio", async (msg) => {
|
|
|
1149
1411
|
try { fs.unlinkSync(p); } catch (e) {}
|
|
1150
1412
|
if (!t) return send("Couldn't transcribe.");
|
|
1151
1413
|
await send(`Heard: "${t}"`, { replyTo: msg.message_id });
|
|
1152
|
-
await
|
|
1414
|
+
await runClaudeRouted(t, currentSession.dir, msg.message_id);
|
|
1153
1415
|
} catch (err) { await send(`Audio failed: ${err.message}`); }
|
|
1154
1416
|
});
|
|
1155
1417
|
|
|
@@ -1160,7 +1422,7 @@ bot.on("photo", async (msg) => {
|
|
|
1160
1422
|
try {
|
|
1161
1423
|
const p = await downloadFile(msg.photo[msg.photo.length - 1].file_id, ".jpg");
|
|
1162
1424
|
const caption = msg.caption || "Describe this image. If code/UI/error — explain and fix.";
|
|
1163
|
-
await
|
|
1425
|
+
await runClaudeRouted(`Image at ${p}\n\nView it, then: ${caption}`, currentSession.dir, msg.message_id);
|
|
1164
1426
|
} catch (err) { await send(`Image failed: ${err.message}`); }
|
|
1165
1427
|
});
|
|
1166
1428
|
|
|
@@ -1188,7 +1450,7 @@ bot.on("document", async (msg) => {
|
|
|
1188
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."}`;
|
|
1189
1451
|
}
|
|
1190
1452
|
await send(`File saved: ${fileName}`, { replyTo: msg.message_id });
|
|
1191
|
-
await
|
|
1453
|
+
await runClaudeRouted(prompt, currentSession.dir, msg.message_id);
|
|
1192
1454
|
} catch (err) { await send(`Failed: ${err.message}`); }
|
|
1193
1455
|
});
|
|
1194
1456
|
|
|
@@ -1293,7 +1555,7 @@ bot.on("message", async (msg) => {
|
|
|
1293
1555
|
}
|
|
1294
1556
|
}
|
|
1295
1557
|
|
|
1296
|
-
await
|
|
1558
|
+
await runClaudeRouted(prompt, currentSession.dir, msg.message_id);
|
|
1297
1559
|
});
|
|
1298
1560
|
|
|
1299
1561
|
// ── Startup ─────────────────────────────────────────────────────────
|