@inetafrica/open-claudia 2.2.16 → 2.2.18

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.2.18
4
+ - Fix the legacy Telegram send/edit path so replies default to Telegram HTML parse mode and go through the shared formatter, preventing raw `<b>`, `<a>`, and related tags from leaking in chat.
5
+ - Preserve safe Telegram HTML tags before Markdown conversion, so underscores inside link URLs such as Higgsfield `wan2_2_video` are not misread as italics and do not break rendered links.
6
+
7
+ ## v2.2.17
8
+ - Telegram output now uses `parse_mode: "HTML"` instead of legacy Markdown. The Telegram adapter normalizes both model-authored Telegram HTML and ordinary Markdown/CommonMark into Telegram's safe HTML subset before sending, so `<b>...</b>`, `**bold**`, backticks, links, headings, code blocks, strikethrough, and spoilers render cleanly instead of leaking literal markup or being rejected by Telegram. Messages still fall back to plain text if Telegram rejects the markup.
9
+ - Updated the Telegram system prompt to ask agents for short mobile-readable Telegram HTML (`<b>`, `<code>`, `<pre>`, `<a>`) with bullet-style layout. Relay sends and slash-command responses now use the same HTML parse mode as normal assistant replies.
10
+
3
11
  ## v2.2.16
4
12
  - New `/compactwindow` slash command (alias `/autocompact`) lets each user override the auto-compact token threshold from chat instead of editing `AUTO_COMPACT_TOKENS` in `.env` and restarting. Quick-pick buttons for 200k / 300k / 380k / 500k / Off / Default, or free-form `/compactwindow 250k` / `/compactwindow 0.5m` / `/compactwindow off` / `/compactwindow default`. Stored per-user as `settings.compactWindow` (persists across restarts via `state.json`); `null` falls back to the env default, `0` disables auto-compact entirely (manual `/compact` still works). `/status` now reports the effective window, and `/usage`'s "context is large" tip reflects the user's override.
5
13
 
package/bot-agent.js CHANGED
@@ -6,6 +6,7 @@ const https = require("https");
6
6
  const cron = require("node-cron");
7
7
  const Vault = require("./vault");
8
8
  const CONFIG_DIR = require("./config-dir");
9
+ const { telegramHtml } = require("./channels/telegram/format");
9
10
 
10
11
  // ── Graceful shutdown & error handling ─────────────────────────────
11
12
  process.on("SIGINT", () => process.exit(0));
@@ -653,14 +654,17 @@ function projectKeyboard() {
653
654
  }
654
655
 
655
656
  async function send(text, opts = {}) {
657
+ const parseMode = opts.parseMode || "HTML";
658
+ const body = parseMode === "HTML" ? telegramHtml(text) : text;
659
+ const fallbackBody = String(text ?? "");
656
660
  const o = {};
657
- if (opts.parseMode) o.parse_mode = opts.parseMode;
661
+ if (parseMode) o.parse_mode = parseMode;
658
662
  if (opts.keyboard) o.reply_markup = opts.keyboard;
659
663
  if (opts.replyTo) o.reply_to_message_id = opts.replyTo;
660
664
 
661
665
  for (let attempt = 0; attempt < 3; attempt++) {
662
666
  try {
663
- const msg = await bot.sendMessage(CHAT_ID, text, o);
667
+ const msg = await bot.sendMessage(CHAT_ID, body, o);
664
668
  return msg.message_id;
665
669
  } catch (e) {
666
670
  const errMsg = e.message || "";
@@ -680,10 +684,17 @@ async function send(text, opts = {}) {
680
684
  continue;
681
685
  }
682
686
 
683
- // Parse mode failed — retry without it
684
- if (opts.parseMode && o.parse_mode) {
687
+ // Parse mode failed — retry once as plain text, but do not leak raw HTML tags.
688
+ if (parseMode && o.parse_mode) {
689
+ console.error("Send parse-mode failed, retrying plain text:", errMsg);
685
690
  delete o.parse_mode;
686
- continue;
691
+ try {
692
+ const msg = await bot.sendMessage(CHAT_ID, fallbackBody, o);
693
+ return msg.message_id;
694
+ } catch (plainErr) {
695
+ console.error("Send plain-text fallback error:", plainErr.message || plainErr);
696
+ return null;
697
+ }
687
698
  }
688
699
 
689
700
  console.error("Send error:", errMsg);
@@ -695,16 +706,28 @@ async function send(text, opts = {}) {
695
706
  }
696
707
 
697
708
  async function editMessage(messageId, text, opts = {}) {
709
+ const parseMode = opts.parseMode || "HTML";
710
+ const body = parseMode === "HTML" ? telegramHtml(text) : text;
711
+ const fallbackBody = String(text ?? "");
698
712
  try {
699
713
  const o = { chat_id: CHAT_ID, message_id: messageId };
714
+ if (parseMode) o.parse_mode = parseMode;
700
715
  if (opts.keyboard) o.reply_markup = opts.keyboard;
701
- await bot.editMessageText(text, o);
716
+ await bot.editMessageText(body, o);
702
717
  } catch (e) {
703
718
  const errMsg = e.message || "";
704
719
  // Rate limited — skip this update (next interval will catch up)
705
720
  if (errMsg.includes("retry after")) return;
706
721
  // Message unchanged — ignore
707
722
  if (errMsg.includes("message is not modified")) return;
723
+ if (parseMode) {
724
+ try {
725
+ const fallback = { chat_id: CHAT_ID, message_id: messageId };
726
+ if (opts.keyboard) fallback.reply_markup = opts.keyboard;
727
+ await bot.editMessageText(fallbackBody, fallback);
728
+ return;
729
+ } catch (e2) {}
730
+ }
708
731
  // Log anything unexpected
709
732
  if (!errMsg.includes("message to edit not found")) {
710
733
  console.error("Edit error:", errMsg);
@@ -10,6 +10,7 @@ const TelegramBot = require("node-telegram-bot-api");
10
10
  const { TEMP_DIR, FILES_DIR, CONFIG_DIR } = require("../../core/config");
11
11
  const { canonicalForChannel } = require("../../core/identity");
12
12
  const { portableToInlineKeyboard } = require("../types");
13
+ const { telegramHtml } = require("./format");
13
14
 
14
15
  class TelegramAdapter {
15
16
  constructor({ id = "telegram", token, ownerChatId, chatIds }) {
@@ -204,6 +205,7 @@ class TelegramAdapter {
204
205
 
205
206
  async send(channelId, text, opts = {}) {
206
207
  const o = {};
208
+ const body = opts.parseMode === "HTML" ? telegramHtml(text) : text;
207
209
  if (opts.parseMode) o.parse_mode = opts.parseMode;
208
210
  const kb = this._normalizeKeyboard(opts.keyboard);
209
211
  if (kb) o.reply_markup = kb;
@@ -211,7 +213,7 @@ class TelegramAdapter {
211
213
 
212
214
  for (let attempt = 0; attempt < 3; attempt++) {
213
215
  try {
214
- const msg = await this.bot.sendMessage(channelId, text, o);
216
+ const msg = await this.bot.sendMessage(channelId, body, o);
215
217
  return msg.message_id;
216
218
  } catch (e) {
217
219
  const errMsg = e.message || "";
@@ -239,13 +241,14 @@ class TelegramAdapter {
239
241
  }
240
242
 
241
243
  async edit(channelId, messageId, text, opts = {}) {
244
+ const body = opts.parseMode === "HTML" ? telegramHtml(text) : text;
242
245
  const o = { chat_id: channelId, message_id: messageId };
243
246
  if (opts.parseMode) o.parse_mode = opts.parseMode;
244
247
  const kb = this._normalizeKeyboard(opts.keyboard);
245
248
  if (kb) o.reply_markup = kb;
246
249
 
247
250
  try {
248
- await this.bot.editMessageText(text, o);
251
+ await this.bot.editMessageText(body, o);
249
252
  } catch (e) {
250
253
  const errMsg = e.message || "";
251
254
  if (errMsg.includes("retry after")) return;
@@ -1,7 +1,18 @@
1
- // Telegram MarkdownV1 quirks: single-asterisk bold, single-underscore
2
- // italic, backticks for code. Most callers send `parseMode: "Markdown"`
3
- // strings already authored for Telegram, so this module is mostly a
4
- // pass-through with a couple of helpers.
1
+ // Telegram formatting helpers.
2
+ //
3
+ // Telegram's legacy Markdown parser is too fragile for LLM output:
4
+ // CommonMark `**bold**`, underscores in identifiers, unescaped dots in
5
+ // MarkdownV2, and angle brackets in logs all commonly make Telegram reject
6
+ // the message or render markup literally. The bot now sends Telegram HTML and
7
+ // normalizes both model-authored Telegram HTML and ordinary Markdown into the
8
+ // safe subset Telegram accepts.
9
+
10
+ function escapeHtml(text) {
11
+ return String(text || "")
12
+ .replace(/&/g, "&amp;")
13
+ .replace(/</g, "&lt;")
14
+ .replace(/>/g, "&gt;");
15
+ }
5
16
 
6
17
  function stripMarkdown(text) {
7
18
  return String(text || "")
@@ -9,4 +20,94 @@ function stripMarkdown(text) {
9
20
  .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
10
21
  }
11
22
 
12
- module.exports = { stripMarkdown };
23
+ function stashCode(text) {
24
+ const blocks = [];
25
+ const stash = (html) => {
26
+ const token = `\u0000OCTGCODE${blocks.length}\u0000`;
27
+ blocks.push(html);
28
+ return token;
29
+ };
30
+
31
+ let out = String(text || "").replace(/```(?:[^\n`]*)\n?([\s\S]*?)```/g, (_, code) => {
32
+ return stash(`<pre>${escapeHtml(code.trimEnd())}</pre>`);
33
+ });
34
+
35
+ out = out.replace(/`([^`\n]+)`/g, (_, code) => {
36
+ return stash(`<code>${escapeHtml(code)}</code>`);
37
+ });
38
+
39
+ return { text: out, blocks };
40
+ }
41
+
42
+ function stashAllowedHtmlTags(text) {
43
+ const tags = [];
44
+ const stash = (html) => {
45
+ const token = `\u0000OCTGHTML${tags.length}\u0000`;
46
+ tags.push(html);
47
+ return token;
48
+ };
49
+ let out = text;
50
+
51
+ // Hide model-authored Telegram HTML tags from Markdown conversion. This keeps
52
+ // underscores in hrefs such as wan2_2_video from becoming bogus <i> tags.
53
+ for (const tag of ["b", "strong", "i", "em", "u", "s", "strike", "del", "code", "pre", "blockquote"]) {
54
+ const open = new RegExp(`&lt;${tag}&gt;`, "gi");
55
+ const close = new RegExp(`&lt;/${tag}&gt;`, "gi");
56
+ out = out.replace(open, () => stash(`<${tag}>`)).replace(close, () => stash(`</${tag}>`));
57
+ }
58
+
59
+ out = out
60
+ .replace(/&lt;span class="tg-spoiler"&gt;/gi, () => stash('<span class="tg-spoiler">'))
61
+ .replace(/&lt;\/span&gt;/gi, () => stash("</span>"));
62
+
63
+ out = out.replace(/&lt;a href="([^"<>]+)"&gt;/gi, (_, href) => {
64
+ if (!/^(https?:|mailto:|tg:)/i.test(href)) return "";
65
+ return stash(`<a href="${href}">`);
66
+ }).replace(/&lt;\/a&gt;/gi, () => stash("</a>"));
67
+
68
+ return { text: out, tags };
69
+ }
70
+
71
+ function convertMarkdownToHtml(text) {
72
+ let out = text;
73
+
74
+ // Markdown links: [label](https://example.com)
75
+ out = out.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, (_, label, url) => {
76
+ return `<a href="${url}">${label}</a>`;
77
+ });
78
+
79
+ // Headings become compact bold labels instead of literal # noise.
80
+ out = out.replace(/^\s{0,3}#{1,6}\s+(.+)$/gm, (_, title) => `<b>${title}</b>`);
81
+
82
+ // Bold. Handle CommonMark first, then Telegram Markdown v1 style.
83
+ out = out.replace(/\*\*([^*\n][\s\S]*?[^*\n])\*\*/g, "<b>$1</b>");
84
+ out = out.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, "<b>$1</b>");
85
+
86
+ // Italic. Keep conservative to avoid mangling snake_case identifiers.
87
+ out = out.replace(/(?<!_)_([^_\n]+)_(?!_)/g, "<i>$1</i>");
88
+
89
+ // Strikethrough and Telegram spoiler syntax.
90
+ out = out.replace(/~~([^~\n]+)~~/g, "<s>$1</s>");
91
+ out = out.replace(/\|\|([^|\n]+)\|\|/g, '<span class="tg-spoiler">$1</span>');
92
+
93
+ return out;
94
+ }
95
+
96
+ function telegramHtml(text) {
97
+ const { text: withoutCode, blocks } = stashCode(text);
98
+ let out = escapeHtml(withoutCode);
99
+ const { text: withoutHtmlTags, tags } = stashAllowedHtmlTags(out);
100
+ out = convertMarkdownToHtml(withoutHtmlTags);
101
+
102
+ tags.forEach((html, i) => {
103
+ out = out.replace(`\u0000OCTGHTML${i}\u0000`, html);
104
+ });
105
+
106
+ blocks.forEach((html, i) => {
107
+ out = out.replace(`\u0000OCTGCODE${i}\u0000`, html);
108
+ });
109
+
110
+ return out;
111
+ }
112
+
113
+ module.exports = { escapeHtml, stripMarkdown, telegramHtml };
package/core/handlers.js CHANGED
@@ -795,7 +795,7 @@ register({
795
795
  handler: async (env) => {
796
796
  if (!authorized(env)) return;
797
797
  await send("Bot mode: *direct* (default)\n\nSwitch to agent mode for non-blocking execution.\nIn agent mode, heavy tasks run in the background and you can keep chatting.", {
798
- parseMode: "Markdown",
798
+ parseMode: "HTML",
799
799
  keyboard: { inline_keyboard: [[{ text: "Switch to Agent Mode", callback_data: "mode:agent" }]] },
800
800
  });
801
801
  },
@@ -886,7 +886,7 @@ register({
886
886
  lines.push(`\nTip: context is large. The bot auto-compacts after the next reply at ${fmt(threshold)} tokens (/compactwindow); /compact does it now.`);
887
887
  }
888
888
  }
889
- send(lines.join("\n"), { parseMode: "Markdown" });
889
+ send(lines.join("\n"), { parseMode: "HTML" });
890
890
  },
891
891
  });
892
892
 
package/core/relay.js CHANGED
@@ -35,7 +35,7 @@ async function send({ text, target, from = null, kind = "relay" }) {
35
35
  let messageId = null;
36
36
  let errorMsg = null;
37
37
  try {
38
- const sendOpts = adapter.type === "telegram" ? { parseMode: "Markdown" } : {};
38
+ const sendOpts = adapter.type === "telegram" ? { parseMode: "HTML" } : {};
39
39
  const result = await adapter.send(resolved.channelId, String(text), sendOpts);
40
40
  if (typeof result === "object" && result && "messageId" in result) { messageId = result.messageId; ok = !!messageId; }
41
41
  else { messageId = result; ok = !!result; }
package/core/runner.js CHANGED
@@ -22,9 +22,9 @@ const {
22
22
  const { getClaudeOAuthToken, claudeAuthRecoveryMessage, isClaudeAuthErrorText, claudeUsageLimitMessage, isClaudeUsageLimitText, runClaudeAuthStatusDiagnostic, claudeSubprocessEnv } = require("./auth-flow");
23
23
  const loopback = require("./loopback");
24
24
 
25
- function telegramMarkdownOpts(extra = {}) {
25
+ function telegramHtmlOpts(extra = {}) {
26
26
  const adapter = currentAdapter();
27
- return adapter?.type === "telegram" ? { ...extra, parseMode: "Markdown" } : extra;
27
+ return adapter?.type === "telegram" ? { ...extra, parseMode: "HTML" } : extra;
28
28
  }
29
29
 
30
30
  function chatEnvOverlay() {
@@ -476,9 +476,9 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
476
476
  const display = formatProgress(assistantText, toolUses, currentTool, elapsed, currentToolDetail);
477
477
  if (display && display !== lastUpdate) {
478
478
  if (!state.statusMessageId && assistantText) {
479
- state.statusMessageId = await send(display.length > 4000 ? display.slice(-4000) : display, telegramMarkdownOpts({ replyTo: replyToMsgId }));
479
+ state.statusMessageId = await send(display.length > 4000 ? display.slice(-4000) : display, telegramHtmlOpts({ replyTo: replyToMsgId }));
480
480
  } else if (state.statusMessageId) {
481
- await editMessage(state.statusMessageId, display.length > 4000 ? display.slice(-4000) : display, telegramMarkdownOpts());
481
+ await editMessage(state.statusMessageId, display.length > 4000 ? display.slice(-4000) : display, telegramHtmlOpts());
482
482
  }
483
483
  lastUpdate = display;
484
484
  }
@@ -665,12 +665,12 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
665
665
  const firstChunk = chunks[0];
666
666
 
667
667
  if (state.statusMessageId && chunks.length === 1) {
668
- await editMessage(state.statusMessageId, firstChunk, telegramMarkdownOpts());
668
+ await editMessage(state.statusMessageId, firstChunk, telegramHtmlOpts());
669
669
  } else {
670
- const sent = await send(firstChunk, telegramMarkdownOpts({ replyTo: replyToMsgId }));
670
+ const sent = await send(firstChunk, telegramHtmlOpts({ replyTo: replyToMsgId }));
671
671
  if (!sent) await send(firstChunk);
672
672
  for (let i = 1; i < chunks.length; i++) {
673
- await send(chunks[i], telegramMarkdownOpts());
673
+ await send(chunks[i], telegramHtmlOpts());
674
674
  }
675
675
  }
676
676
  if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
@@ -78,28 +78,28 @@ function buildSystemPrompt() {
78
78
  ## Telegram formatting
79
79
  Telegram is mobile-first and narrow. Optimize for readability in a small chat bubble.
80
80
 
81
- Use Telegram Markdown v1 only:
82
- - Bold labels with single asterisks: *Status:* / *Done:* / *Blocked:* / *Next:*
83
- - Italic with single underscores only when genuinely useful.
84
- - Inline code with backticks only for commands, file paths, IDs, short field names, and exact errors.
85
- - Fenced code blocks only for short commands or snippets. Do not paste long logs; save/send a file instead.
81
+ Use Telegram HTML, not raw Markdown:
82
+ - Bold labels with <b>Status:</b> / <b>Done:</b> / <b>Blocked:</b> / <b>Next:</b>
83
+ - Italic with <i>...</i> only when genuinely useful.
84
+ - Inline code with <code>...</code> only for commands, file paths, IDs, short field names, and exact errors.
85
+ - Code blocks with <pre>...</pre> only for short commands or snippets. Do not paste long logs; save/send a file instead.
86
+ - Links may use <a href="https://example.com">label</a>, or just paste the URL.
86
87
 
87
88
  Avoid formatting that renders badly or noisily in Telegram:
88
89
  - No Markdown tables.
89
90
  - No Markdown headings like #, ##, ###.
90
- - No CommonMark bold like **bold**; use *bold*.
91
- - No Markdown links like [text](url); use plain URLs when needed.
92
- - Do not wrap ordinary business words in backticks or quote marks. For example, write Payments completed, not \`Payments\` completed or 'Payments' completed.
91
+ - No raw **bold** / *bold* as your preferred output, even though the bot has a fallback converter.
92
+ - Do not wrap ordinary business words in backticks, <code>, or quote marks. For example, write Payments completed, not <code>Payments</code> completed or 'Payments' completed.
93
93
  - Avoid large paragraphs. Use short sections, blank lines, and 3-7 concise bullets.
94
94
 
95
95
  Good Telegram pattern:
96
- *Status:* CRM sync is still running cleanly.
96
+ <b>Status:</b> CRM sync is still running cleanly.
97
97
 
98
- - Bills and Calls completed.
99
- - Leads completed with 20 rows.
100
- - Payments advanced to 2026-05-17 10:50.
98
+ Bills and Calls completed.
99
+ Leads completed with 20 rows.
100
+ Payments advanced to <code>2026-05-17 10:50</code>.
101
101
 
102
- *Issue:* JSON parse error after Retention. I’m checking that stream next.
102
+ <b>Issue:</b> JSON parse error after Retention. I’m checking that stream next.
103
103
  `
104
104
  : adapter?.type === "kazee"
105
105
  ? `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.2.16",
3
+ "version": "2.2.18",
4
4
  "description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
5
5
  "main": "bot.js",
6
6
  "bin": {