@inetafrica/open-claudia 1.14.9 → 1.15.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 +55 -3
  2. package/package.json +1 -1
package/bot.js CHANGED
@@ -274,6 +274,7 @@ bot.setMyCommands([
274
274
  { command: "ask", description: "Toggle ask mode (Cursor only)" },
275
275
  { command: "sessions", description: "List conversations for this project" },
276
276
  { command: "compact", description: "Summarize conversation context" },
277
+ { command: "usage", description: "Show token usage & cost for this session" },
277
278
  { command: "continue", description: "Resume last conversation" },
278
279
  { command: "worktree", description: "Toggle isolated git branch" },
279
280
  { command: "cron", description: "Manage scheduled tasks" },
@@ -325,6 +326,7 @@ function saveState() {
325
326
  lastSessionId,
326
327
  cursorSessionId,
327
328
  settings,
329
+ sessionUsage,
328
330
  };
329
331
  try { fs.writeFileSync(STATE_FILE, JSON.stringify(data)); } catch (e) {}
330
332
  }
@@ -402,6 +404,20 @@ let pendingClaudeAuthLabel = null;
402
404
  let isFirstMessage = !lastSessionId; // Track if this is first message in session
403
405
  let lastInputWasVoice = false; // Reply with voice when input was voice
404
406
 
407
+ // Per-session usage tracking (resets on /end or new session)
408
+ let sessionUsage = savedState.sessionUsage || {
409
+ turns: 0,
410
+ inputTokens: 0,
411
+ outputTokens: 0,
412
+ cacheReadTokens: 0,
413
+ cacheCreationTokens: 0,
414
+ costUsd: 0,
415
+ lastInputTokens: 0, // Last turn's input tokens (for compact suggestion)
416
+ };
417
+ function resetSessionUsage() {
418
+ sessionUsage = { turns: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, costUsd: 0, lastInputTokens: 0 };
419
+ }
420
+
405
421
  let settings = savedState.settings || {
406
422
  model: null, effort: null, budget: null, permissionMode: null, worktree: false, backend: "claude",
407
423
  };
@@ -1324,6 +1340,17 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1324
1340
  if (evt.type === "result" && evt.session_id) {
1325
1341
  if (settings.backend === "cursor") { cursorSessionId = evt.session_id; }
1326
1342
  else { lastSessionId = evt.session_id; }
1343
+ if (evt.usage) {
1344
+ sessionUsage.turns += 1;
1345
+ sessionUsage.inputTokens += evt.usage.input_tokens || 0;
1346
+ sessionUsage.outputTokens += evt.usage.output_tokens || 0;
1347
+ sessionUsage.cacheReadTokens += evt.usage.cache_read_input_tokens || 0;
1348
+ sessionUsage.cacheCreationTokens += evt.usage.cache_creation_input_tokens || 0;
1349
+ sessionUsage.lastInputTokens = (evt.usage.input_tokens || 0) +
1350
+ (evt.usage.cache_read_input_tokens || 0) +
1351
+ (evt.usage.cache_creation_input_tokens || 0);
1352
+ }
1353
+ if (typeof evt.total_cost_usd === "number") sessionUsage.costUsd += evt.total_cost_usd;
1327
1354
  saveState();
1328
1355
  }
1329
1356
  if (evt.type === "result" && evt.result) assistantText = evt.result;
@@ -1384,6 +1411,15 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
1384
1411
  const voicePath = textToVoice(finalText);
1385
1412
  if (voicePath) await sendVoice(voicePath);
1386
1413
  }
1414
+
1415
+ // Suggest /compact when context gets large (once per threshold crossing)
1416
+ const ctx = sessionUsage.lastInputTokens;
1417
+ if (ctx > 150000 && !sessionUsage.warnedHighContext) {
1418
+ sessionUsage.warnedHighContext = true;
1419
+ await send(`Heads up: context is ${(ctx / 1000).toFixed(0)}k tokens. Use /compact to summarize or /end for a fresh start — keeps cost down.`);
1420
+ } else if (ctx < 80000 && sessionUsage.warnedHighContext) {
1421
+ sessionUsage.warnedHighContext = false;
1422
+ }
1387
1423
  } catch (e) {
1388
1424
  console.error("Final message delivery failed:", e.message);
1389
1425
  await send("Task completed but failed to deliver the response. Send /continue to see the result.");
@@ -1760,12 +1796,28 @@ bot.onText(/\/status/, (msg) => {
1760
1796
  bot.onText(/\/end/, (msg) => {
1761
1797
  if (!isAuthorized(msg)) return;
1762
1798
  if (currentSession) {
1763
- const n = currentSession.name; currentSession = null; lastSessionId = null; messageQueue = []; resetSettings();
1799
+ const n = currentSession.name; currentSession = null; lastSessionId = null; messageQueue = []; resetSettings(); resetSessionUsage();
1764
1800
  saveState();
1765
1801
  send(`Ended: ${n}`, { keyboard: { inline_keyboard: [[{ text: "New session", callback_data: "show:projects" }]] } });
1766
1802
  } else send("No session.");
1767
1803
  });
1768
1804
 
1805
+ bot.onText(/\/usage$/, (msg) => {
1806
+ if (!isAuthorized(msg)) return;
1807
+ const u = sessionUsage;
1808
+ if (u.turns === 0) return send("No usage yet in this session.");
1809
+ const fmt = (n) => n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n);
1810
+ const lines = [
1811
+ `*Session usage* (${u.turns} turn${u.turns === 1 ? "" : "s"})`,
1812
+ `Input: ${fmt(u.inputTokens)} · Output: ${fmt(u.outputTokens)}`,
1813
+ `Cache read: ${fmt(u.cacheReadTokens)} · Cache write: ${fmt(u.cacheCreationTokens)}`,
1814
+ `Cost: $${u.costUsd.toFixed(4)}`,
1815
+ `Last turn context: ${fmt(u.lastInputTokens)}`,
1816
+ ];
1817
+ if (u.lastInputTokens > 100000) lines.push("\nTip: context is large. Use /compact to summarize, or /end for a clean slate.");
1818
+ send(lines.join("\n"), { parseMode: "Markdown" });
1819
+ });
1820
+
1769
1821
  bot.onText(/\/soul$/, async (msg) => {
1770
1822
  if (!isAuthorized(msg)) return;
1771
1823
  const soul = loadSoul();
@@ -1965,13 +2017,13 @@ bot.on("callback_query", async (q) => {
1965
2017
  const sessions = loadSessions();
1966
2018
  // Don't delete history, just start fresh
1967
2019
  currentSession = { name: proj === "__workspace__" ? "Workspace" : proj, dir: proj === "__workspace__" ? WORKSPACE : path.join(WORKSPACE, proj) };
1968
- lastSessionId = null; isFirstMessage = true; messageQueue = []; resetSettings();
2020
+ lastSessionId = null; isFirstMessage = true; messageQueue = []; resetSettings(); resetSessionUsage();
1969
2021
  saveState();
1970
2022
  await send(`Session: ${currentSession.name}\nNew conversation\n\nSend text, voice, or images.\n\n/sessions — switch conversation\n/session — switch project`);
1971
2023
  return;
1972
2024
  }
1973
2025
  if (d === "a:continue") { if (currentSession) await runClaude("continue", currentSession.dir); else send("No session."); return; }
1974
- if (d === "a:end") { if (currentSession) { const n = currentSession.name; currentSession = null; lastSessionId = null; messageQueue = []; resetSettings(); saveState(); await send(`Ended: ${n}`, { keyboard: { inline_keyboard: [[{ text: "New", callback_data: "show:projects" }]] } }); } return; }
2026
+ if (d === "a:end") { if (currentSession) { const n = currentSession.name; currentSession = null; lastSessionId = null; messageQueue = []; resetSettings(); resetSessionUsage(); saveState(); await send(`Ended: ${n}`, { keyboard: { inline_keyboard: [[{ text: "New", callback_data: "show:projects" }]] } }); } return; }
1975
2027
  if (d.startsWith("m:")) { settings.model = d.slice(2) === "default" ? null : d.slice(2); await send(`Model: ${settings.model || "default"}`); return; }
1976
2028
  if (d.startsWith("e:")) { const e = d.slice(2); settings.effort = e === "default" ? null : e; await send(`Effort: ${settings.effort || "default"}`); return; }
1977
2029
  if (d.startsWith("b:")) { const b = d.slice(2); settings.budget = b === "none" ? null : parseFloat(b); await send(`Budget: ${settings.budget ? "$"+settings.budget : "none"}`); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "1.14.9",
3
+ "version": "1.15.0",
4
4
  "description": "Your always-on AI coding assistant — Claude Code via Telegram",
5
5
  "main": "bot.js",
6
6
  "bin": {