@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 +1 -0
- package/dist/bot.js +119 -2
- package/dist/claude.d.ts +7 -0
- package/dist/claude.js +23 -0
- package/package.json +1 -1
package/dist/bot.d.ts
CHANGED
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
|
|
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);
|