@grinev/opencode-telegram-bot 0.12.0 → 0.12.1

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/index.js CHANGED
@@ -61,6 +61,12 @@ const SESSION_RETRY_PREFIX = "🔁";
61
61
  const __filename = fileURLToPath(import.meta.url);
62
62
  const __dirname = path.dirname(__filename);
63
63
  const TEMP_DIR = path.join(__dirname, "..", ".tmp");
64
+ function getCurrentReplyKeyboard() {
65
+ if (!keyboardManager.isInitialized()) {
66
+ return undefined;
67
+ }
68
+ return keyboardManager.getKeyboard();
69
+ }
64
70
  function prepareDocumentCaption(caption) {
65
71
  const normalizedCaption = caption.trim();
66
72
  if (!normalizedCaption) {
@@ -81,8 +87,10 @@ const toolMessageBatcher = new ToolMessageBatcher({
81
87
  if (!currentSession || currentSession.id !== sessionId) {
82
88
  return;
83
89
  }
90
+ const keyboard = getCurrentReplyKeyboard();
84
91
  await botInstance.api.sendMessage(chatIdInstance, text, {
85
92
  disable_notification: true,
93
+ ...(keyboard ? { reply_markup: keyboard } : {}),
86
94
  });
87
95
  },
88
96
  sendFile: async (sessionId, fileData) => {
@@ -98,9 +106,11 @@ const toolMessageBatcher = new ToolMessageBatcher({
98
106
  logger.debug(`[Bot] Sending code file: ${fileData.filename} (${fileData.buffer.length} bytes, session=${sessionId})`);
99
107
  await fs.mkdir(TEMP_DIR, { recursive: true });
100
108
  await fs.writeFile(tempFilePath, fileData.buffer);
109
+ const keyboard = getCurrentReplyKeyboard();
101
110
  await botInstance.api.sendDocument(chatIdInstance, new InputFile(tempFilePath), {
102
111
  caption: fileData.caption,
103
112
  disable_notification: true,
113
+ ...(keyboard ? { reply_markup: keyboard } : {}),
104
114
  });
105
115
  }
106
116
  finally {
@@ -161,8 +171,7 @@ async function ensureEventSubscription(directory) {
161
171
  const assistantMessageFormat = assistantParseMode === "MarkdownV2" ? "markdown_v2" : "raw";
162
172
  logger.debug(`[Bot] Sending completed message to Telegram (chatId=${chatIdInstance}, parts=${parts.length})`);
163
173
  for (let i = 0; i < parts.length; i++) {
164
- const isLastPart = i === parts.length - 1;
165
- const keyboard = isLastPart && keyboardManager.isInitialized() ? keyboardManager.getKeyboard() : undefined;
174
+ const keyboard = getCurrentReplyKeyboard();
166
175
  const options = keyboard ? { reply_markup: keyboard } : undefined;
167
176
  await sendBotText({
168
177
  api: botInstance.api,
@@ -305,6 +314,18 @@ async function ensureEventSubscription(directory) {
305
314
  logger.error("[Bot] Error updating pinned message with tokens:", err);
306
315
  }
307
316
  });
317
+ summaryAggregator.setOnCost(async (cost) => {
318
+ if (!pinnedMessageManager.isInitialized()) {
319
+ return;
320
+ }
321
+ try {
322
+ logger.debug(`[Bot] Cost update: $${cost.toFixed(2)}`);
323
+ await pinnedMessageManager.onCostUpdate(cost);
324
+ }
325
+ catch (err) {
326
+ logger.error("[Bot] Error updating cost:", err);
327
+ }
328
+ });
308
329
  summaryAggregator.setOnSessionCompacted(async (sessionId, directory) => {
309
330
  if (!pinnedMessageManager.isInitialized()) {
310
331
  return;
package/dist/i18n/de.js CHANGED
@@ -216,6 +216,7 @@ export const de = {
216
216
  "pinned.line.project": "Projekt: {project}",
217
217
  "pinned.line.model": "Modell: {model}",
218
218
  "pinned.line.context": "Kontext: {used} / {limit} ({percent}%)",
219
+ "pinned.line.cost": "Kosten: {cost} ausgegeben",
219
220
  "pinned.files.title": "Dateien ({count}):",
220
221
  "pinned.files.item": " {path}{diff}",
221
222
  "pinned.files.more": " ... und {count} mehr",
package/dist/i18n/en.js CHANGED
@@ -216,6 +216,7 @@ export const en = {
216
216
  "pinned.line.project": "Project: {project}",
217
217
  "pinned.line.model": "Model: {model}",
218
218
  "pinned.line.context": "Context: {used} / {limit} ({percent}%)",
219
+ "pinned.line.cost": "Cost: {cost} spent",
219
220
  "pinned.files.title": "Files ({count}):",
220
221
  "pinned.files.item": " {path}{diff}",
221
222
  "pinned.files.more": " ... and {count} more",
package/dist/i18n/es.js CHANGED
@@ -216,6 +216,7 @@ export const es = {
216
216
  "pinned.line.project": "Proyecto: {project}",
217
217
  "pinned.line.model": "Modelo: {model}",
218
218
  "pinned.line.context": "Contexto: {used} / {limit} ({percent}%)",
219
+ "pinned.line.cost": "Costo: {cost} gastado",
219
220
  "pinned.files.title": "Archivos ({count}):",
220
221
  "pinned.files.item": " {path}{diff}",
221
222
  "pinned.files.more": " ... y {count} más",
package/dist/i18n/fr.js CHANGED
@@ -216,6 +216,7 @@ export const fr = {
216
216
  "pinned.line.project": "Projet : {project}",
217
217
  "pinned.line.model": "Modèle : {model}",
218
218
  "pinned.line.context": "Contexte : {used} / {limit} ({percent}%)",
219
+ "pinned.line.cost": "Coût : {cost} dépensé",
219
220
  "pinned.files.title": "Fichiers ({count}) :",
220
221
  "pinned.files.item": " {path}{diff}",
221
222
  "pinned.files.more": " ... et encore {count}",
package/dist/i18n/ru.js CHANGED
@@ -216,6 +216,7 @@ export const ru = {
216
216
  "pinned.line.project": "Проект: {project}",
217
217
  "pinned.line.model": "Модель: {model}",
218
218
  "pinned.line.context": "Контекст: {used} / {limit} ({percent}%)",
219
+ "pinned.line.cost": "Стоимость: {cost} потрачено",
219
220
  "pinned.files.title": "Файлы ({count}):",
220
221
  "pinned.files.item": " {path}{diff}",
221
222
  "pinned.files.more": " ... и еще {count}",
package/dist/i18n/zh.js CHANGED
@@ -216,6 +216,7 @@ export const zh = {
216
216
  "pinned.line.project": "项目: {project}",
217
217
  "pinned.line.model": "模型: {model}",
218
218
  "pinned.line.context": "上下文: {used} / {limit} ({percent}%)",
219
+ "pinned.line.cost": "费用: {cost}",
219
220
  "pinned.files.title": "文件({count}):",
220
221
  "pinned.files.item": " {path}{diff}",
221
222
  "pinned.files.more": " ... 还有 {count} 个",
@@ -17,6 +17,7 @@ class PinnedMessageManager {
17
17
  tokensLimit: 0,
18
18
  lastUpdated: 0,
19
19
  changedFiles: [],
20
+ cost: 0,
20
21
  };
21
22
  contextLimit = null;
22
23
  onKeyboardUpdateCallback;
@@ -40,6 +41,7 @@ class PinnedMessageManager {
40
41
  logger.info(`[PinnedManager] Session changed: ${sessionId}, title: ${sessionTitle}`);
41
42
  // Reset tokens for new session
42
43
  this.state.tokensUsed = 0;
44
+ this.state.cost = 0;
43
45
  // Update state
44
46
  this.state.sessionId = sessionId;
45
47
  this.state.sessionTitle = sessionTitle || t("pinned.default_session_title");
@@ -84,9 +86,10 @@ class PinnedMessageManager {
84
86
  logger.warn("[PinnedManager] Failed to load session history:", error);
85
87
  return;
86
88
  }
87
- // Get the maximum context size from session history
89
+ // Get the maximum context size and cost from session history
88
90
  // Context = input + cache.read (cache.read contains previously cached context)
89
91
  let maxContextSize = 0;
92
+ let maxCost = 0;
90
93
  logger.debug(`[PinnedManager] Processing ${messagesData.length} messages from history`);
91
94
  messagesData.forEach(({ info }) => {
92
95
  if (info.role === "assistant") {
@@ -99,16 +102,22 @@ class PinnedMessageManager {
99
102
  const input = assistantInfo.tokens?.input || 0;
100
103
  const cacheRead = assistantInfo.tokens?.cache?.read || 0;
101
104
  const contextSize = input + cacheRead;
102
- logger.debug(`[PinnedManager] Assistant message: input=${input}, cache.read=${cacheRead}, total=${contextSize}`);
105
+ const cost = assistantInfo.cost || 0;
106
+ logger.debug(`[PinnedManager] Assistant message: input=${input}, cache.read=${cacheRead}, total=${contextSize}, cost=$${cost.toFixed(2)}`);
103
107
  // Keep track of maximum context size (peak usage in session)
104
108
  if (contextSize > maxContextSize) {
105
109
  maxContextSize = contextSize;
106
110
  }
111
+ // Keep track of maximum cost (cumulative in OpenCode)
112
+ if (cost > maxCost) {
113
+ maxCost = cost;
114
+ }
107
115
  }
108
116
  });
109
117
  this.state.tokensUsed = maxContextSize;
118
+ this.state.cost = maxCost;
110
119
  this.state.sessionId = sessionId;
111
- logger.info(`[PinnedManager] Loaded context from history: ${this.state.tokensUsed} tokens`);
120
+ logger.info(`[PinnedManager] Loaded context from history: ${this.state.tokensUsed} tokens, cost: $${this.state.cost.toFixed(2)}`);
112
121
  await this.updatePinnedMessage();
113
122
  }
114
123
  catch (err) {
@@ -140,6 +149,14 @@ class PinnedMessageManager {
140
149
  await this.refreshSessionTitle();
141
150
  await this.updatePinnedMessage();
142
151
  }
152
+ /**
153
+ * Called when cost info is received from SSE events
154
+ */
155
+ async onCostUpdate(cost) {
156
+ this.state.cost = cost;
157
+ logger.debug(`[PinnedManager] Cost updated: $${cost.toFixed(2)}`);
158
+ await this.updatePinnedMessage();
159
+ }
143
160
  /**
144
161
  * Set callback for keyboard updates when context changes
145
162
  */
@@ -455,6 +472,9 @@ class PinnedMessageManager {
455
472
  percent: percentage,
456
473
  }),
457
474
  ];
475
+ if (this.state.cost !== undefined && this.state.cost !== null) {
476
+ lines.push(t("pinned.line.cost", { cost: `$${this.state.cost.toFixed(2)}` }));
477
+ }
458
478
  if (this.state.changedFiles.length > 0) {
459
479
  const maxFiles = 10;
460
480
  const total = this.state.changedFiles.length;
@@ -38,6 +38,7 @@ class SummaryAggregator {
38
38
  onQuestionErrorCallback = null;
39
39
  onThinkingCallback = null;
40
40
  onTokensCallback = null;
41
+ onCostCallback = null;
41
42
  onSessionCompactedCallback = null;
42
43
  onSessionErrorCallback = null;
43
44
  onSessionRetryCallback = null;
@@ -76,6 +77,9 @@ class SummaryAggregator {
76
77
  setOnTokens(callback) {
77
78
  this.onTokensCallback = callback;
78
79
  }
80
+ setOnCost(callback) {
81
+ this.onCostCallback = callback;
82
+ }
79
83
  setOnSessionCompacted(callback) {
80
84
  this.onSessionCompactedCallback = callback;
81
85
  }
@@ -231,6 +235,11 @@ class SummaryAggregator {
231
235
  // Call synchronously so keyboardManager is updated before onComplete sends the reply
232
236
  this.onTokensCallback(tokens);
233
237
  }
238
+ // Extract and report cost
239
+ if (this.onCostCallback && assistantInfo.cost !== undefined) {
240
+ logger.debug(`[Aggregator] Cost: $${assistantInfo.cost.toFixed(2)}`);
241
+ this.onCostCallback(assistantInfo.cost);
242
+ }
234
243
  if (this.onCompleteCallback && lastPart.length > 0) {
235
244
  this.onCompleteCallback(this.currentSessionId, lastPart);
236
245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grinev/opencode-telegram-bot",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Telegram bot client for OpenCode to run and monitor coding tasks from chat.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",