@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.
Files changed (2) hide show
  1. package/bot.js +140 -43
  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) {
@@ -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
- streamInterval = setInterval(async () => {
637
- bot.sendChatAction(CHAT_ID, "typing");
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 display = formatProgress(assistantText, toolUses, currentTool, elapsed);
640
- if (display && display !== lastUpdate) {
641
- if (!statusMessageId && assistantText) {
642
- statusMessageId = await send(display.length > 4000 ? display.slice(-4000) : display, { replyTo: replyToMsgId });
643
- } else if (statusMessageId) {
644
- 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;
645
717
  }
646
- lastUpdate = display;
647
- }
648
- // Notify after 5 minutes that it's still running
649
- if (elapsed > 300 && !longRunningNotified) {
650
- longRunningNotified = true;
651
- 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);
652
725
  }
653
- }, 1500);
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") { currentTool = block.name; toolUses.push(block.name); }
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
- clearInterval(streamInterval); streamInterval = null;
677
- const finalText = assistantText || "(no output)";
678
- const chunks = splitMessage(finalText);
679
- // Keep the streaming progress message and send final result as a new message
680
- // This ensures the user gets a notification for the final answer
681
- await send(chunks[0], { replyTo: replyToMsgId });
682
- for (let i = 1; i < chunks.length; i++) {
683
- 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.");
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; clearInterval(streamInterval);
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
- const status = currentTool ? `Working: ${currentTool}` : (tools.length > 0 ? "Processing..." : "Thinking...");
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 > 0) parts.push(`Steps: ${[...new Set(tools)].join(" > ")}`);
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) clearInterval(streamInterval);
1036
+ if (streamInterval) clearTimeout(streamInterval);
940
1037
  messageQueue = [];
941
1038
  await send("Cancelled.");
942
1039
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.4.4",
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": {