@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.
Files changed (2) hide show
  1. package/bot.js +122 -39
  2. 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
- try {
528
- const msg = await bot.sendMessage(CHAT_ID, text, o);
529
- return msg.message_id;
530
- } catch (e) {
531
- if (opts.parseMode) {
532
- try {
533
- const f = {}; if (opts.keyboard) f.reply_markup = opts.keyboard;
534
- return (await bot.sendMessage(CHAT_ID, text, f)).message_id;
535
- } catch (e2) { /* ignore */ }
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) { /* ignore */ }
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
- streamInterval = setInterval(async () => {
638
- bot.sendChatAction(CHAT_ID, "typing");
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 display = formatProgress(assistantText, toolUses, currentTool, elapsed, currentToolDetail);
641
- if (display && display !== lastUpdate) {
642
- if (!statusMessageId && assistantText) {
643
- statusMessageId = await send(display.length > 4000 ? display.slice(-4000) : display, { replyTo: replyToMsgId });
644
- } else if (statusMessageId) {
645
- await editMessage(statusMessageId, display.length > 4000 ? display.slice(-4000) : display);
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
- lastUpdate = display;
648
- }
649
- // Notify after 5 minutes that it's still running
650
- if (elapsed > 300 && !longRunningNotified) {
651
- longRunningNotified = true;
652
- await send(`Still working (${Math.floor(elapsed / 60)}min)... Send /stop to cancel.`);
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
- }, 1500);
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
- clearInterval(streamInterval); streamInterval = null;
690
- const finalText = assistantText || "(no output)";
691
- const chunks = splitMessage(finalText);
692
- // Keep the streaming progress message and send final result as a new message
693
- // This ensures the user gets a notification for the final answer
694
- await send(chunks[0], { replyTo: replyToMsgId });
695
- for (let i = 1; i < chunks.length; i++) {
696
- await send(chunks[i]);
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; clearInterval(streamInterval);
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) clearInterval(streamInterval);
1036
+ if (streamInterval) clearTimeout(streamInterval);
954
1037
  messageQueue = [];
955
1038
  await send("Cancelled.");
956
1039
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.4.5",
3
+ "version": "1.5.0",
4
4
  "description": "Your always-on AI coding assistant — Claude Code via Telegram",
5
5
  "main": "bot.js",
6
6
  "bin": {