@inetafrica/open-claudia 2.6.27 → 2.6.29

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.
@@ -12,6 +12,7 @@ const { TEMP_DIR, FILES_DIR } = require("../../core/config");
12
12
  const { canonicalForChannel } = require("../../core/identity");
13
13
  const { inlineKeyboardToPortable } = require("../types");
14
14
  const { createKazeeSocket } = require("./socket");
15
+ const { normalize } = require("./format");
15
16
 
16
17
  class KazeeAdapter {
17
18
  constructor({ id = "kazee", url, token, ownerUserId, botUserId }) {
@@ -98,7 +99,7 @@ class KazeeAdapter {
98
99
  });
99
100
  }
100
101
 
101
- _handleInboundMessage(msg) {
102
+ async _handleInboundMessage(msg) {
102
103
  if (!msg) return;
103
104
  const senderId = String(msg.sender?._id || msg.sender?.id || msg.senderId || "");
104
105
  if (this.botUserId && senderId === String(this.botUserId)) return; // ignore our own echoes
@@ -169,6 +170,37 @@ class KazeeAdapter {
169
170
  }
170
171
  }
171
172
 
173
+ // Voice/audio distinction: chat-central has no "voice" type, so voice
174
+ // notes arrive as "audio". Detect them by mimeType (ogg/opus) and
175
+ // promote to "voice" so the router can transcribe them correctly.
176
+ if (envelope.type === "audio" && envelope.media?.length) {
177
+ const mime = (envelope.media[0].mimeType || "").toLowerCase();
178
+ if (mime.includes("ogg") || mime.includes("opus")) {
179
+ envelope.type = "voice";
180
+ envelope.media[0].type = "voice";
181
+ }
182
+ }
183
+
184
+ // Reply-to context: fetch the replied-to message content so the router
185
+ // has the same context Telegram provides. Best-effort — failure is silent.
186
+ if (msg.replyTo) {
187
+ try {
188
+ const data = await this._request("GET", `/message/${encodeURIComponent(String(msg.replyTo))}`);
189
+ const m = data?.message || data;
190
+ if (m && (m._id || m.id)) {
191
+ envelope.reply = {
192
+ text: m.content || "",
193
+ from: {
194
+ id: String(m.sender?._id || m.sender?.id || ""),
195
+ name: m.sender?.name || m.sender?.displayName || "",
196
+ username: m.sender?.username || "",
197
+ },
198
+ messageId: m._id || m.id,
199
+ };
200
+ }
201
+ } catch (e) { /* best-effort — envelope is still valid without reply context */ }
202
+ }
203
+
172
204
  this._emit("message", envelope);
173
205
  }
174
206
 
@@ -184,24 +216,42 @@ class KazeeAdapter {
184
216
  const body = {
185
217
  chat_id: channelId,
186
218
  sender: this.botUserId,
187
- content: text,
219
+ content: normalize(text),
188
220
  type: buttons ? "interactive" : "text",
189
221
  findMeCode: `oc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
190
222
  };
191
223
  if (opts.replyTo) body.replyTo = opts.replyTo;
192
224
  if (buttons) body.interactive = { buttons };
193
- try {
194
- const res = await this._request("POST", `/chat/${encodeURIComponent(channelId)}/message`, body);
195
- return res?.message?._id || res?.messageId || res?._id || null;
196
- } catch (e) {
197
- console.error("Kazee send error:", e.message);
198
- return null;
225
+
226
+ for (let attempt = 0; attempt < 3; attempt++) {
227
+ try {
228
+ const res = await this._request("POST", `/chat/${encodeURIComponent(channelId)}/message`, body);
229
+ return res?.message?._id || res?.messageId || res?._id || null;
230
+ } catch (e) {
231
+ const errMsg = e.message || "";
232
+ // Don't retry on client errors (4xx) — they won't self-heal.
233
+ const statusMatch = errMsg.match(/→ (\d{3})/);
234
+ const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
235
+ if (status >= 400 && status < 500) {
236
+ console.error("Kazee send error (not retrying):", errMsg);
237
+ return null;
238
+ }
239
+ if (attempt < 2) {
240
+ const delay = (attempt + 1) * 1000;
241
+ console.error(`Kazee send error (attempt ${attempt + 1}/3, retrying in ${delay}ms):`, errMsg);
242
+ await new Promise((r) => setTimeout(r, delay));
243
+ continue;
244
+ }
245
+ console.error("Kazee send error (exhausted retries):", errMsg);
246
+ return null;
247
+ }
199
248
  }
249
+ return null;
200
250
  }
201
251
 
202
252
  async edit(channelId, messageId, text, opts = {}) {
203
253
  const buttons = this._normalizeKeyboard(opts.keyboard);
204
- const body = { content: text };
254
+ const body = { content: normalize(text) };
205
255
  if (buttons) {
206
256
  body.type = "interactive";
207
257
  body.interactive = { buttons };
@@ -1,9 +1,27 @@
1
- // Kazee uses standard markdown end-to-end. Telegram-flavoured asterisks
2
- // and underscores still render fine, so this is mostly a hook for future
3
- // divergence (image embeds, code-block fences, etc.).
1
+ // Kazee renders standard markdown end-to-end. This module normalizes text
2
+ // before sending: converts any stray Telegram HTML tags (which the model may
3
+ // accidentally emit) into their markdown equivalents so they render correctly
4
+ // instead of appearing as raw tag noise.
5
+
6
+ function htmlToMarkdown(text) {
7
+ return String(text || "")
8
+ .replace(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, code) => "```\n" + code + "\n```")
9
+ .replace(/<pre>([\s\S]*?)<\/pre>/gi, (_, code) => "```\n" + code + "\n```")
10
+ .replace(/<code>([\s\S]*?)<\/code>/gi, "`$1`")
11
+ .replace(/<b>([\s\S]*?)<\/b>/gi, "**$1**")
12
+ .replace(/<strong>([\s\S]*?)<\/strong>/gi, "**$1**")
13
+ .replace(/<i>([\s\S]*?)<\/i>/gi, "_$1_")
14
+ .replace(/<em>([\s\S]*?)<\/em>/gi, "_$1_")
15
+ .replace(/<s>([\s\S]*?)<\/s>/gi, "~~$1~~")
16
+ .replace(/<strike>([\s\S]*?)<\/strike>/gi, "~~$1~~")
17
+ .replace(/<del>([\s\S]*?)<\/del>/gi, "~~$1~~")
18
+ .replace(/<a href="([^"]+)">([\s\S]*?)<\/a>/gi, "[$2]($1)")
19
+ .replace(/<[^>]+>/g, "")
20
+ .replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)));
21
+ }
4
22
 
5
23
  function normalize(text) {
6
- return String(text || "");
24
+ return htmlToMarkdown(String(text || ""));
7
25
  }
8
26
 
9
27
  module.exports = { normalize };
package/core/runner.js CHANGED
@@ -130,6 +130,11 @@ function logTurnUsage(state, usage, costUsd, announce, opts = {}) {
130
130
  const backend = state.settings?.backend || "claude";
131
131
  const parts = usageParts(usage, backend);
132
132
  const usageScope = opts.usageScope || "turn_delta";
133
+ // `parts.context` is the per-turn SUM of every API call's prefix, inflated
134
+ // by re-counting the cached prefix once per tool round-trip. When the caller
135
+ // tracked the true peak single-call prefix, report that as contextTokens and
136
+ // keep the inflated sum as billedContextTokens for cost attribution.
137
+ const hasPeak = Number.isFinite(opts.liveContextTokens) && opts.liveContextTokens > 0;
133
138
  const record = {
134
139
  ts: new Date().toISOString(),
135
140
  version: PKG_VERSION,
@@ -142,7 +147,8 @@ function logTurnUsage(state, usage, costUsd, announce, opts = {}) {
142
147
  outputTokens: parts.output,
143
148
  cacheReadTokens: parts.cacheRead,
144
149
  cacheCreationTokens: parts.cacheCreation,
145
- contextTokens: parts.context,
150
+ contextTokens: hasPeak ? opts.liveContextTokens : parts.context,
151
+ billedContextTokens: parts.context,
146
152
  costUsd: typeof costUsd === "number" ? costUsd : 0,
147
153
  };
148
154
  if (opts.rawUsage && opts.rawUsage !== usage) {
@@ -785,6 +791,11 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
785
791
  let toolUses = [];
786
792
  let currentTool = null;
787
793
  let currentToolDetail = "";
794
+ // True context-window occupancy is the largest single API call's prefix
795
+ // (input + cache_read + cache_creation), NOT the per-turn sum the result
796
+ // event reports — that sum re-counts the cached prefix once per tool
797
+ // round-trip and balloons to millions. Track the peak per-call prefix.
798
+ let peakContextTokens = 0;
788
799
 
789
800
  // Hermes-style skill announcements: one chat line when a learned skill
790
801
  // is invoked or its SKILL.md is written/patched. Deduped per turn.
@@ -897,6 +908,10 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
897
908
  const lastNewline = state.streamBuffer.lastIndexOf("\n");
898
909
  state.streamBuffer = lastNewline >= 0 ? state.streamBuffer.slice(lastNewline + 1) : state.streamBuffer;
899
910
  for (const evt of events) {
911
+ if (evt.type === "assistant" && evt.message?.usage) {
912
+ const callPrefix = usageParts(evt.message.usage, settings.backend || "claude").context;
913
+ if (callPrefix > peakContextTokens) peakContextTokens = callPrefix;
914
+ }
900
915
  if (evt.type === "assistant" && evt.message?.content) {
901
916
  for (const block of evt.message.content) {
902
917
  if (block.type === "text") assistantText += block.text;
@@ -1003,8 +1018,10 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1003
1018
  // Codex already reports usage at turn.completed. A result usage block,
1004
1019
  // if present, is the same cumulative total and must not be counted again.
1005
1020
  if (evt.usage && settings.backend !== "codex") {
1006
- applyUsageToState(state, evt.usage, typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : 0, { usageScope: "turn_delta" });
1007
- logTurnUsage(state, evt.usage, typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : 0, (text) => chatContext.run(store, () => send(text)), { usageScope: "turn_delta" });
1021
+ const peakOpts = { usageScope: "turn_delta" };
1022
+ if (peakContextTokens > 0) peakOpts.liveContextTokens = peakContextTokens;
1023
+ applyUsageToState(state, evt.usage, typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : 0, peakOpts);
1024
+ logTurnUsage(state, evt.usage, typeof evt.total_cost_usd === "number" ? evt.total_cost_usd : 0, (text) => chatContext.run(store, () => send(text)), peakOpts);
1008
1025
  }
1009
1026
  if (typeof evt.total_cost_usd === "number" && settings.backend === "codex") state.sessionUsage.costUsd += evt.total_cost_usd;
1010
1027
  saveState();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "2.6.27",
3
+ "version": "2.6.29",
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": {