@gonzih/cc-tg 0.2.17 → 0.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/dist/bot.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare class CcTgBot {
13
13
  private sessions;
14
14
  private opts;
15
15
  private cron;
16
+ private costStore;
16
17
  constructor(opts: BotOptions);
17
18
  private registerBotCommands;
18
19
  private isAllowed;
package/dist/bot.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * One ClaudeProcess per chat_id — sessions are isolated per user.
4
4
  */
5
5
  import TelegramBot from "node-telegram-bot-api";
6
- import { existsSync, createWriteStream, mkdirSync, statSync, readdirSync } from "fs";
6
+ import { existsSync, createWriteStream, mkdirSync, statSync, readdirSync, readFileSync, writeFileSync } from "fs";
7
7
  import { resolve, basename, join } from "path";
8
8
  import os from "os";
9
9
  import { execSync } from "child_process";
@@ -24,14 +24,112 @@ const BOT_COMMANDS = [
24
24
  { command: "clear_npx_cache", description: "Clear npx cache and restart MCP to pick up latest version" },
25
25
  { command: "restart", description: "Restart the bot process in-place" },
26
26
  { command: "get_file", description: "Send a file from the server to this chat" },
27
+ { command: "cost", description: "Show session token usage and cost" },
27
28
  ];
28
29
  const FLUSH_DELAY_MS = 800; // debounce streaming chunks into one Telegram message
29
30
  const TYPING_INTERVAL_MS = 4000; // re-send typing action before Telegram's 5s expiry
31
+ // Claude Sonnet 4.6 pricing (per 1M tokens)
32
+ const PRICING = {
33
+ inputPerM: 3.00,
34
+ outputPerM: 15.00,
35
+ cacheReadPerM: 0.30,
36
+ cacheWritePerM: 3.75,
37
+ };
38
+ function computeCostUsd(usage) {
39
+ return (usage.inputTokens * PRICING.inputPerM / 1_000_000 +
40
+ usage.outputTokens * PRICING.outputPerM / 1_000_000 +
41
+ usage.cacheReadTokens * PRICING.cacheReadPerM / 1_000_000 +
42
+ usage.cacheWriteTokens * PRICING.cacheWritePerM / 1_000_000);
43
+ }
44
+ function formatTokens(n) {
45
+ if (n >= 1000)
46
+ return `${(n / 1000).toFixed(1)}k`;
47
+ return String(n);
48
+ }
49
+ function formatCostReport(cost) {
50
+ const inputCost = cost.totalInputTokens * PRICING.inputPerM / 1_000_000;
51
+ const outputCost = cost.totalOutputTokens * PRICING.outputPerM / 1_000_000;
52
+ const cacheReadCost = cost.totalCacheReadTokens * PRICING.cacheReadPerM / 1_000_000;
53
+ const cacheWriteCost = cost.totalCacheWriteTokens * PRICING.cacheWritePerM / 1_000_000;
54
+ return [
55
+ "šŸ“Š Session cost",
56
+ `Messages: ${cost.messageCount}`,
57
+ `Total: $${cost.totalCostUsd.toFixed(3)}`,
58
+ ` Input: ${formatTokens(cost.totalInputTokens)} tokens ($${inputCost.toFixed(3)})`,
59
+ ` Output: ${formatTokens(cost.totalOutputTokens)} tokens ($${outputCost.toFixed(3)})`,
60
+ ` Cache read: ${formatTokens(cost.totalCacheReadTokens)} tokens ($${cacheReadCost.toFixed(3)})`,
61
+ ` Cache write: ${formatTokens(cost.totalCacheWriteTokens)} tokens ($${cacheWriteCost.toFixed(3)})`,
62
+ ].join("\n");
63
+ }
64
+ function formatCronCostFooter(usage) {
65
+ const cost = computeCostUsd(usage);
66
+ return `\nšŸ’° Cron cost: $${cost.toFixed(4)} (${formatTokens(usage.inputTokens)} in / ${formatTokens(usage.outputTokens)} out tokens)`;
67
+ }
68
+ class CostStore {
69
+ costs = new Map();
70
+ storePath;
71
+ constructor(cwd) {
72
+ this.storePath = join(cwd, ".cc-tg", "costs.json");
73
+ this.load();
74
+ }
75
+ get(chatId) {
76
+ let cost = this.costs.get(chatId);
77
+ if (!cost) {
78
+ cost = { totalInputTokens: 0, totalOutputTokens: 0, totalCacheReadTokens: 0, totalCacheWriteTokens: 0, totalCostUsd: 0, messageCount: 0 };
79
+ this.costs.set(chatId, cost);
80
+ }
81
+ return cost;
82
+ }
83
+ addUsage(chatId, usage) {
84
+ const cost = this.get(chatId);
85
+ cost.totalInputTokens += usage.inputTokens;
86
+ cost.totalOutputTokens += usage.outputTokens;
87
+ cost.totalCacheReadTokens += usage.cacheReadTokens;
88
+ cost.totalCacheWriteTokens += usage.cacheWriteTokens;
89
+ cost.totalCostUsd += computeCostUsd(usage);
90
+ this.persist();
91
+ }
92
+ incrementMessages(chatId) {
93
+ const cost = this.get(chatId);
94
+ cost.messageCount++;
95
+ this.persist();
96
+ }
97
+ persist() {
98
+ try {
99
+ const dir = join(this.storePath, "..");
100
+ if (!existsSync(dir))
101
+ mkdirSync(dir, { recursive: true });
102
+ const data = {};
103
+ for (const [chatId, cost] of this.costs) {
104
+ data[String(chatId)] = cost;
105
+ }
106
+ writeFileSync(this.storePath, JSON.stringify(data, null, 2));
107
+ }
108
+ catch (err) {
109
+ console.error("[costs] persist error:", err.message);
110
+ }
111
+ }
112
+ load() {
113
+ if (!existsSync(this.storePath))
114
+ return;
115
+ try {
116
+ const data = JSON.parse(readFileSync(this.storePath, "utf8"));
117
+ for (const [key, cost] of Object.entries(data)) {
118
+ this.costs.set(Number(key), cost);
119
+ }
120
+ console.log(`[costs] loaded ${this.costs.size} session costs from disk`);
121
+ }
122
+ catch (err) {
123
+ console.error("[costs] load error:", err.message);
124
+ }
125
+ }
126
+ }
30
127
  export class CcTgBot {
31
128
  bot;
32
129
  sessions = new Map();
33
130
  opts;
34
131
  cron;
132
+ costStore;
35
133
  constructor(opts) {
36
134
  this.opts = opts;
37
135
  this.bot = new TelegramBot(opts.telegramToken, { polling: true });
@@ -41,6 +139,7 @@ export class CcTgBot {
41
139
  this.cron = new CronManager(opts.cwd ?? process.cwd(), (chatId, prompt) => {
42
140
  this.runCronTask(chatId, prompt);
43
141
  });
142
+ this.costStore = new CostStore(opts.cwd ?? process.cwd());
44
143
  this.registerBotCommands();
45
144
  console.log("cc-tg bot started");
46
145
  console.log(`[voice] whisper available: ${isVoiceAvailable()}`);
@@ -135,6 +234,12 @@ export class CcTgBot {
135
234
  await this.handleGetFile(chatId, text);
136
235
  return;
137
236
  }
237
+ // /cost — show session token usage and cost
238
+ if (text === "/cost") {
239
+ const cost = this.costStore.get(chatId);
240
+ await this.bot.sendMessage(chatId, formatCostReport(cost));
241
+ return;
242
+ }
138
243
  const session = this.getOrCreateSession(chatId);
139
244
  try {
140
245
  session.claude.sendPrompt(buildPromptWithReplyContext(text, msg));
@@ -235,6 +340,9 @@ export class CcTgBot {
235
340
  typingTimer: null,
236
341
  writtenFiles: new Set(),
237
342
  };
343
+ claude.on("usage", (usage) => {
344
+ this.costStore.addUsage(chatId, usage);
345
+ });
238
346
  claude.on("message", (msg) => {
239
347
  // Verbose logging — log every message type and subtype
240
348
  const subtype = msg.payload.subtype ?? "";
@@ -274,6 +382,7 @@ export class CcTgBot {
274
382
  if (msg.type !== "result")
275
383
  return;
276
384
  this.stopTyping(session);
385
+ this.costStore.incrementMessages(chatId);
277
386
  const text = extractText(msg);
278
387
  if (!text)
279
388
  return;
@@ -505,6 +614,13 @@ export class CcTgBot {
505
614
  `SCHEDULED TASK: ${prompt}`,
506
615
  ].join("\n");
507
616
  let output = "";
617
+ const cronUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
618
+ cronProcess.on("usage", (usage) => {
619
+ cronUsage.inputTokens += usage.inputTokens;
620
+ cronUsage.outputTokens += usage.outputTokens;
621
+ cronUsage.cacheReadTokens += usage.cacheReadTokens;
622
+ cronUsage.cacheWriteTokens += usage.cacheWriteTokens;
623
+ });
508
624
  cronProcess.on("message", (msg) => {
509
625
  if (msg.type === "result") {
510
626
  const text = extractText(msg);
@@ -512,7 +628,8 @@ export class CcTgBot {
512
628
  output += text;
513
629
  const result = output.trim();
514
630
  if (result) {
515
- const chunks = splitMessage(`šŸ• ${result}`);
631
+ const footer = formatCronCostFooter(cronUsage);
632
+ const chunks = splitMessage(`šŸ• ${result}${footer}`);
516
633
  (async () => {
517
634
  for (const chunk of chunks) {
518
635
  try {
package/dist/claude.d.ts CHANGED
@@ -18,8 +18,15 @@ export interface ClaudeOptions {
18
18
  /** OAuth token (sk-ant-oat01-...) or API key (sk-ant-api03-...) */
19
19
  token?: string;
20
20
  }
21
+ export interface UsageEvent {
22
+ inputTokens: number;
23
+ outputTokens: number;
24
+ cacheReadTokens: number;
25
+ cacheWriteTokens: number;
26
+ }
21
27
  export declare interface ClaudeProcess {
22
28
  on(event: "message", listener: (msg: ClaudeMessage) => void): this;
29
+ on(event: "usage", listener: (usage: UsageEvent) => void): this;
23
30
  on(event: "error", listener: (err: Error) => void): this;
24
31
  on(event: "exit", listener: (code: number | null) => void): this;
25
32
  on(event: "stderr", listener: (data: string) => void): this;
package/dist/claude.js CHANGED
@@ -109,6 +109,29 @@ export class ClaudeProcess extends EventEmitter {
109
109
  continue;
110
110
  try {
111
111
  const raw = JSON.parse(line);
112
+ // Emit usage events from Anthropic API stream events passed through by Claude CLI
113
+ if (raw.type === "message_start") {
114
+ const usage = (raw.message?.usage);
115
+ if (usage) {
116
+ this.emit("usage", {
117
+ inputTokens: usage.input_tokens ?? 0,
118
+ outputTokens: 0, // output_tokens at message_start is always 0
119
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0,
120
+ cacheWriteTokens: usage.cache_creation_input_tokens ?? 0,
121
+ });
122
+ }
123
+ }
124
+ else if (raw.type === "message_delta") {
125
+ const usage = raw.usage;
126
+ if (usage?.output_tokens) {
127
+ this.emit("usage", {
128
+ inputTokens: 0,
129
+ outputTokens: usage.output_tokens,
130
+ cacheReadTokens: 0,
131
+ cacheWriteTokens: 0,
132
+ });
133
+ }
134
+ }
112
135
  const msg = this.parseMessage(raw);
113
136
  if (msg)
114
137
  this.emit("message", msg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {