@inetafrica/open-claudia 2.4.0 → 2.4.2

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.4.2
4
+ - Fix replies losing everything written before tool calls. The backend's final `result` event carries only the LAST text segment of a turn, but the stream parser assigned it over the accumulated `assistantText` — so a turn shaped "long explanation → tool calls → short closing line" delivered only the closing line, which read as nonsense without its context. `evt.result` is now a fallback used only when nothing was accumulated (some Cursor turns). Fixed in both `runClaude` and the auxiliary runner.
5
+
6
+ ## v2.4.1
7
+ - Fix Telegram messages occasionally arriving as raw HTML (`<b>`, `<code>` shown literally). Root cause: the Markdown→HTML pass could inject `<i>` tags *inside* model-authored `<code>` spans (two snake_case identifiers on one line pair their underscores as italics), producing unbalanced HTML that Telegram rejects — and the parse-failure fallback then resent the converted body verbatim, tags and all.
8
+ - `<code>`/`<pre>` spans are now stashed whole (content included) before Markdown conversion, so nothing can be injected inside them.
9
+ - The italic `_..._` rule only fires at word boundaries, so snake_case identifiers anywhere in the text can no longer pair up.
10
+ - Model-authored entities (`&lt;`, `&amp;`, `&#…;`) are preserved instead of double-escaped — `<code>&lt;name&gt;</code>` now renders as `<name>` instead of `&lt;name&gt;`.
11
+ - Last-resort fallback (send + edit) now strips tags and decodes entities (`htmlToPlain`) so a rejected message degrades to clean plain text, never raw markup.
12
+
3
13
  ## v2.4.0
4
14
  - **FTS5 transcript index (cross-session recall).** Project transcripts are now indexed in SQLite FTS5 via Node's built-in `node:sqlite` (no new dependency), turning "did we discuss X last week?" into a ~50ms ranked lookup instead of a linear grep over JSONL.
5
15
  - `core/transcript-index.js`: WAL-mode DB at `~/.open-claudia/transcripts/index.db` (0600), FTS5 table with porter tokenizer, per-file byte offsets for idempotent incremental indexing. Partial trailing lines wait for the next pass; a replaced/truncated transcript drops its stale rows and reindexes. Fail-soft: without `node:sqlite` every call no-ops and transcript-window remains the path.
@@ -10,7 +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
+ const { telegramHtml, htmlToPlain } = require("./format");
14
14
 
15
15
  class TelegramAdapter {
16
16
  constructor({ id = "telegram", token, ownerChatId, chatIds }) {
@@ -205,7 +205,7 @@ class TelegramAdapter {
205
205
 
206
206
  async send(channelId, text, opts = {}) {
207
207
  const o = {};
208
- const body = opts.parseMode === "HTML" ? telegramHtml(text) : text;
208
+ let body = opts.parseMode === "HTML" ? telegramHtml(text) : text;
209
209
  if (opts.parseMode) o.parse_mode = opts.parseMode;
210
210
  const kb = this._normalizeKeyboard(opts.keyboard);
211
211
  if (kb) o.reply_markup = kb;
@@ -229,7 +229,9 @@ class TelegramAdapter {
229
229
  continue;
230
230
  }
231
231
  if (opts.parseMode && o.parse_mode) {
232
+ console.error("Send: HTML rejected, falling back to plain:", errMsg);
232
233
  delete o.parse_mode;
234
+ body = htmlToPlain(body);
233
235
  continue;
234
236
  }
235
237
  console.error("Send error:", errMsg);
@@ -255,7 +257,7 @@ class TelegramAdapter {
255
257
  if (errMsg.includes("message is not modified")) return;
256
258
  if (opts.parseMode && o.parse_mode) {
257
259
  delete o.parse_mode;
258
- try { await this.bot.editMessageText(text, o); return; } catch (e2) {}
260
+ try { await this.bot.editMessageText(htmlToPlain(body), o); return; } catch (e2) {}
259
261
  }
260
262
  if (!errMsg.includes("message to edit not found")) console.error("Edit error:", errMsg);
261
263
  }
@@ -14,6 +14,15 @@ function escapeHtml(text) {
14
14
  .replace(/>/g, "&gt;");
15
15
  }
16
16
 
17
+ // Like escapeHtml, but keeps entities the model already wrote (&lt; &amp; &#123;)
18
+ // instead of double-escaping them into visible "&lt;" text.
19
+ function escapeHtmlPreservingEntities(text) {
20
+ return String(text || "")
21
+ .replace(/&(?!(?:amp|lt|gt|quot|#\d+|#x[0-9a-fA-F]+);)/g, "&amp;")
22
+ .replace(/</g, "&lt;")
23
+ .replace(/>/g, "&gt;");
24
+ }
25
+
17
26
  function stripMarkdown(text) {
18
27
  return String(text || "")
19
28
  .replace(/[*_`~]/g, "")
@@ -48,6 +57,13 @@ function stashAllowedHtmlTags(text) {
48
57
  };
49
58
  let out = text;
50
59
 
60
+ // Stash whole model-authored <code>/<pre> spans, content included, so
61
+ // Markdown conversion can never inject tags inside them (e.g. two
62
+ // snake_case identifiers on one line becoming a stray <i> pair).
63
+ out = out.replace(/&lt;(code|pre)&gt;([\s\S]*?)&lt;\/\1&gt;/gi, (_, tag, inner) => {
64
+ return stash(`<${tag.toLowerCase()}>${inner}</${tag.toLowerCase()}>`);
65
+ });
66
+
51
67
  // Hide model-authored Telegram HTML tags from Markdown conversion. This keeps
52
68
  // underscores in hrefs such as wan2_2_video from becoming bogus <i> tags.
53
69
  for (const tag of ["b", "strong", "i", "em", "u", "s", "strike", "del", "code", "pre", "blockquote"]) {
@@ -83,8 +99,9 @@ function convertMarkdownToHtml(text) {
83
99
  out = out.replace(/\*\*([^*\n][\s\S]*?[^*\n])\*\*/g, "<b>$1</b>");
84
100
  out = out.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, "<b>$1</b>");
85
101
 
86
- // Italic. Keep conservative to avoid mangling snake_case identifiers.
87
- out = out.replace(/(?<!_)_([^_\n]+)_(?!_)/g, "<i>$1</i>");
102
+ // Italic. Only fire when the underscores sit at word boundaries, so
103
+ // snake_case identifiers (skill_manage ... skill_manage) never pair up.
104
+ out = out.replace(/(?<![A-Za-z0-9_])_([^_\n]+)_(?![A-Za-z0-9_])/g, "<i>$1</i>");
88
105
 
89
106
  // Strikethrough and Telegram spoiler syntax.
90
107
  out = out.replace(/~~([^~\n]+)~~/g, "<s>$1</s>");
@@ -95,7 +112,7 @@ function convertMarkdownToHtml(text) {
95
112
 
96
113
  function telegramHtml(text) {
97
114
  const { text: withoutCode, blocks } = stashCode(text);
98
- let out = escapeHtml(withoutCode);
115
+ let out = escapeHtmlPreservingEntities(withoutCode);
99
116
  const { text: withoutHtmlTags, tags } = stashAllowedHtmlTags(out);
100
117
  out = convertMarkdownToHtml(withoutHtmlTags);
101
118
 
@@ -110,4 +127,16 @@ function telegramHtml(text) {
110
127
  return out;
111
128
  }
112
129
 
113
- module.exports = { escapeHtml, stripMarkdown, telegramHtml };
130
+ // Last-resort plain text for when Telegram rejects the HTML: drop tags,
131
+ // decode entities, so the user never sees raw <b>/<code> markup.
132
+ function htmlToPlain(html) {
133
+ return String(html || "")
134
+ .replace(/<\/?[a-zA-Z][^>]*>/g, "")
135
+ .replace(/&lt;/g, "<")
136
+ .replace(/&gt;/g, ">")
137
+ .replace(/&quot;/g, '"')
138
+ .replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
139
+ .replace(/&amp;/g, "&");
140
+ }
141
+
142
+ module.exports = { escapeHtml, stripMarkdown, telegramHtml, htmlToPlain };
package/core/runner.js CHANGED
@@ -500,7 +500,10 @@ async function runClaudeCapture(prompt, cwd, opts = {}) {
500
500
  if (typeof evt.total_cost_usd === "number") state.sessionUsage.costUsd += evt.total_cost_usd;
501
501
  saveState();
502
502
  }
503
- if (evt.type === "result" && evt.result) assistantText = evt.result;
503
+ // evt.result only carries the FINAL text segment of the turn. Using it
504
+ // as anything but a fallback would clobber text streamed before tool
505
+ // calls (the "long reply, then tools, then short closing line" shape).
506
+ if (evt.type === "result" && evt.result && !assistantText.trim()) assistantText = evt.result;
504
507
  }
505
508
  });
506
509
  proc.stderr.on("data", (d) => { stderrBuffer += d.toString(); });
@@ -803,7 +806,9 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
803
806
  if (typeof evt.total_cost_usd === "number") state.sessionUsage.costUsd += evt.total_cost_usd;
804
807
  saveState();
805
808
  }
806
- if (evt.type === "result" && evt.result) assistantText = evt.result;
809
+ // Fallback only: evt.result is just the final text segment, and assigning
810
+ // it unconditionally wiped everything the model said before tool calls.
811
+ if (evt.type === "result" && evt.result && !assistantText.trim()) assistantText = evt.result;
807
812
  }
808
813
  });
809
814
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
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": {