@inetafrica/open-claudia 1.4.4 → 1.5.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 +140 -43
- 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");
|
|
@@ -524,19 +554,41 @@ async function send(text, opts = {}) {
|
|
|
524
554
|
if (opts.parseMode) o.parse_mode = opts.parseMode;
|
|
525
555
|
if (opts.keyboard) o.reply_markup = opts.keyboard;
|
|
526
556
|
if (opts.replyTo) o.reply_to_message_id = opts.replyTo;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
557
|
+
|
|
558
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
559
|
+
try {
|
|
560
|
+
const msg = await bot.sendMessage(CHAT_ID, text, o);
|
|
561
|
+
return msg.message_id;
|
|
562
|
+
} catch (e) {
|
|
563
|
+
const errMsg = e.message || "";
|
|
564
|
+
|
|
565
|
+
// replyTo message was deleted or not found — retry without it
|
|
566
|
+
if (o.reply_to_message_id && errMsg.includes("message to be replied not found")) {
|
|
567
|
+
delete o.reply_to_message_id;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Rate limited — wait and retry
|
|
572
|
+
const retryMatch = errMsg.match(/retry after (\d+)/i);
|
|
573
|
+
if (retryMatch) {
|
|
574
|
+
const waitSec = Math.min(parseInt(retryMatch[1], 10), 30);
|
|
575
|
+
console.error(`Send: rate limited, waiting ${waitSec}s`);
|
|
576
|
+
await new Promise((r) => setTimeout(r, waitSec * 1000));
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Parse mode failed — retry without it
|
|
581
|
+
if (opts.parseMode && o.parse_mode) {
|
|
582
|
+
delete o.parse_mode;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
console.error("Send error:", errMsg);
|
|
587
|
+
return null;
|
|
536
588
|
}
|
|
537
|
-
console.error("Send error:", e.message);
|
|
538
|
-
return null;
|
|
539
589
|
}
|
|
590
|
+
console.error("Send: exhausted retries");
|
|
591
|
+
return null;
|
|
540
592
|
}
|
|
541
593
|
|
|
542
594
|
async function editMessage(messageId, text, opts = {}) {
|
|
@@ -544,7 +596,17 @@ async function editMessage(messageId, text, opts = {}) {
|
|
|
544
596
|
const o = { chat_id: CHAT_ID, message_id: messageId };
|
|
545
597
|
if (opts.keyboard) o.reply_markup = opts.keyboard;
|
|
546
598
|
await bot.editMessageText(text, o);
|
|
547
|
-
} catch (e) {
|
|
599
|
+
} catch (e) {
|
|
600
|
+
const errMsg = e.message || "";
|
|
601
|
+
// Rate limited — skip this update (next interval will catch up)
|
|
602
|
+
if (errMsg.includes("retry after")) return;
|
|
603
|
+
// Message unchanged — ignore
|
|
604
|
+
if (errMsg.includes("message is not modified")) return;
|
|
605
|
+
// Log anything unexpected
|
|
606
|
+
if (!errMsg.includes("message to edit not found")) {
|
|
607
|
+
console.error("Edit error:", errMsg);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
548
610
|
}
|
|
549
611
|
|
|
550
612
|
function splitMessage(text, maxLen = 4000) {
|
|
@@ -619,6 +681,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
619
681
|
let assistantText = "";
|
|
620
682
|
let toolUses = [];
|
|
621
683
|
let currentTool = null;
|
|
684
|
+
let currentToolDetail = "";
|
|
622
685
|
|
|
623
686
|
const args = buildClaudeArgs(prompt, opts);
|
|
624
687
|
const proc = spawn(CLAUDE_PATH, args, {
|
|
@@ -633,24 +696,36 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
633
696
|
let longRunningNotified = false;
|
|
634
697
|
|
|
635
698
|
let lastUpdate = "";
|
|
636
|
-
|
|
637
|
-
|
|
699
|
+
// Adaptive update interval: 2s for first 2min, then 5s to avoid rate limits
|
|
700
|
+
const scheduleUpdate = () => {
|
|
638
701
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
702
|
+
const interval = elapsed > 120 ? 5000 : 2000;
|
|
703
|
+
streamInterval = setTimeout(updateProgress, interval);
|
|
704
|
+
};
|
|
705
|
+
const updateProgress = async () => {
|
|
706
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
707
|
+
try {
|
|
708
|
+
bot.sendChatAction(CHAT_ID, "typing").catch(() => {});
|
|
709
|
+
const display = formatProgress(assistantText, toolUses, currentTool, elapsed, currentToolDetail);
|
|
710
|
+
if (display && display !== lastUpdate) {
|
|
711
|
+
if (!statusMessageId && assistantText) {
|
|
712
|
+
statusMessageId = await send(display.length > 4000 ? display.slice(-4000) : display, { replyTo: replyToMsgId });
|
|
713
|
+
} else if (statusMessageId) {
|
|
714
|
+
await editMessage(statusMessageId, display.length > 4000 ? display.slice(-4000) : display);
|
|
715
|
+
}
|
|
716
|
+
lastUpdate = display;
|
|
645
717
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
718
|
+
// Notify after 5 minutes that it's still running
|
|
719
|
+
if (elapsed > 300 && !longRunningNotified) {
|
|
720
|
+
longRunningNotified = true;
|
|
721
|
+
await send(`Still working (${Math.floor(elapsed / 60)}min)... Send /stop to cancel.`);
|
|
722
|
+
}
|
|
723
|
+
} catch (e) {
|
|
724
|
+
console.error("Progress update error:", e.message);
|
|
652
725
|
}
|
|
653
|
-
|
|
726
|
+
if (runningProcess) scheduleUpdate();
|
|
727
|
+
};
|
|
728
|
+
scheduleUpdate();
|
|
654
729
|
|
|
655
730
|
proc.stdout.on("data", (data) => {
|
|
656
731
|
streamBuffer += data.toString();
|
|
@@ -661,7 +736,19 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
661
736
|
if (evt.type === "assistant" && evt.message?.content) {
|
|
662
737
|
for (const block of evt.message.content) {
|
|
663
738
|
if (block.type === "text") assistantText += block.text;
|
|
664
|
-
else if (block.type === "tool_use") {
|
|
739
|
+
else if (block.type === "tool_use") {
|
|
740
|
+
currentTool = block.name;
|
|
741
|
+
toolUses.push(block.name);
|
|
742
|
+
// Extract useful detail from tool input
|
|
743
|
+
const input = block.input || {};
|
|
744
|
+
if (block.name === "Bash" && input.command) currentToolDetail = input.command.slice(0, 80);
|
|
745
|
+
else if (block.name === "Read" && input.file_path) currentToolDetail = input.file_path.split("/").slice(-2).join("/");
|
|
746
|
+
else if (block.name === "Edit" && input.file_path) currentToolDetail = input.file_path.split("/").slice(-2).join("/");
|
|
747
|
+
else if (block.name === "Write" && input.file_path) currentToolDetail = input.file_path.split("/").slice(-2).join("/");
|
|
748
|
+
else if (block.name === "Grep" && input.pattern) currentToolDetail = input.pattern.slice(0, 40);
|
|
749
|
+
else if (block.name === "Glob" && input.pattern) currentToolDetail = input.pattern;
|
|
750
|
+
else currentToolDetail = "";
|
|
751
|
+
}
|
|
665
752
|
}
|
|
666
753
|
}
|
|
667
754
|
if (evt.type === "result" && evt.session_id) { lastSessionId = evt.session_id; saveState(); }
|
|
@@ -673,16 +760,25 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
673
760
|
|
|
674
761
|
proc.on("close", async (code) => {
|
|
675
762
|
runningProcess = null;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
763
|
+
clearTimeout(streamInterval); streamInterval = null;
|
|
764
|
+
try {
|
|
765
|
+
const finalText = assistantText || "(no output)";
|
|
766
|
+
const chunks = splitMessage(finalText);
|
|
767
|
+
// Send final result as a new message (triggers notification)
|
|
768
|
+
const sent = await send(chunks[0], { replyTo: replyToMsgId });
|
|
769
|
+
if (!sent) {
|
|
770
|
+
// Fallback: if the first send failed completely, try without any options
|
|
771
|
+
await send(chunks[0]);
|
|
772
|
+
}
|
|
773
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
774
|
+
await send(chunks[i]);
|
|
775
|
+
}
|
|
776
|
+
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
777
|
+
} catch (e) {
|
|
778
|
+
console.error("Final message delivery failed:", e.message);
|
|
779
|
+
// Last-resort attempt to notify user something went wrong
|
|
780
|
+
await send("Task completed but failed to deliver the response. Send /continue to see the result.");
|
|
684
781
|
}
|
|
685
|
-
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
686
782
|
if (settings.budget) settings.budget = null;
|
|
687
783
|
statusMessageId = null;
|
|
688
784
|
|
|
@@ -699,7 +795,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
699
795
|
});
|
|
700
796
|
|
|
701
797
|
proc.on("error", async (err) => {
|
|
702
|
-
runningProcess = null;
|
|
798
|
+
runningProcess = null; clearTimeout(streamInterval);
|
|
703
799
|
await send(`Error: ${err.message}`); statusMessageId = null;
|
|
704
800
|
});
|
|
705
801
|
}
|
|
@@ -724,15 +820,16 @@ async function runClaudeSilent(prompt, cwd, label) {
|
|
|
724
820
|
});
|
|
725
821
|
}
|
|
726
822
|
|
|
727
|
-
function formatProgress(text, tools, currentTool, elapsed) {
|
|
823
|
+
function formatProgress(text, tools, currentTool, elapsed, toolDetail) {
|
|
728
824
|
const mins = Math.floor(elapsed / 60);
|
|
729
825
|
const secs = elapsed % 60;
|
|
730
826
|
const time = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
731
827
|
|
|
732
828
|
const parts = [];
|
|
733
|
-
|
|
829
|
+
let status = currentTool ? `Working: ${currentTool}` : (tools.length > 0 ? "Processing..." : "Thinking...");
|
|
830
|
+
if (currentTool && toolDetail) status += ` — ${toolDetail}`;
|
|
734
831
|
parts.push(`${status} (${time})`);
|
|
735
|
-
if (tools.length >
|
|
832
|
+
if (tools.length > 1) parts.push(`Steps: ${[...new Set(tools)].join(" > ")}`);
|
|
736
833
|
if (text) parts.push(text.length > 800 ? "..." + text.slice(-800) : text);
|
|
737
834
|
return parts.join("\n\n");
|
|
738
835
|
}
|
|
@@ -936,7 +1033,7 @@ bot.onText(/\/stop/, async (msg) => {
|
|
|
936
1033
|
try { process.kill(-pid, "SIGKILL"); } catch (e) {}
|
|
937
1034
|
}, 3000);
|
|
938
1035
|
runningProcess = null;
|
|
939
|
-
if (streamInterval)
|
|
1036
|
+
if (streamInterval) clearTimeout(streamInterval);
|
|
940
1037
|
messageQueue = [];
|
|
941
1038
|
await send("Cancelled.");
|
|
942
1039
|
}
|