@inetafrica/open-claudia 1.4.5 → 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 +122 -39
- 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) {
|
|
@@ -634,24 +696,36 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
634
696
|
let longRunningNotified = false;
|
|
635
697
|
|
|
636
698
|
let lastUpdate = "";
|
|
637
|
-
|
|
638
|
-
|
|
699
|
+
// Adaptive update interval: 2s for first 2min, then 5s to avoid rate limits
|
|
700
|
+
const scheduleUpdate = () => {
|
|
639
701
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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;
|
|
646
717
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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);
|
|
653
725
|
}
|
|
654
|
-
|
|
726
|
+
if (runningProcess) scheduleUpdate();
|
|
727
|
+
};
|
|
728
|
+
scheduleUpdate();
|
|
655
729
|
|
|
656
730
|
proc.stdout.on("data", (data) => {
|
|
657
731
|
streamBuffer += data.toString();
|
|
@@ -686,16 +760,25 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
686
760
|
|
|
687
761
|
proc.on("close", async (code) => {
|
|
688
762
|
runningProcess = null;
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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.");
|
|
697
781
|
}
|
|
698
|
-
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
699
782
|
if (settings.budget) settings.budget = null;
|
|
700
783
|
statusMessageId = null;
|
|
701
784
|
|
|
@@ -712,7 +795,7 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
712
795
|
});
|
|
713
796
|
|
|
714
797
|
proc.on("error", async (err) => {
|
|
715
|
-
runningProcess = null;
|
|
798
|
+
runningProcess = null; clearTimeout(streamInterval);
|
|
716
799
|
await send(`Error: ${err.message}`); statusMessageId = null;
|
|
717
800
|
});
|
|
718
801
|
}
|
|
@@ -950,7 +1033,7 @@ bot.onText(/\/stop/, async (msg) => {
|
|
|
950
1033
|
try { process.kill(-pid, "SIGKILL"); } catch (e) {}
|
|
951
1034
|
}, 3000);
|
|
952
1035
|
runningProcess = null;
|
|
953
|
-
if (streamInterval)
|
|
1036
|
+
if (streamInterval) clearTimeout(streamInterval);
|
|
954
1037
|
messageQueue = [];
|
|
955
1038
|
await send("Cancelled.");
|
|
956
1039
|
}
|