opencode-telegram-bot 1.0.3 → 1.0.5

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/README.md CHANGED
@@ -106,7 +106,12 @@ These are handled directly by the Telegram bot:
106
106
  | `/export` | Export the current session as a markdown file |
107
107
  | `/export full` | Export with all details (thinking, costs, steps) |
108
108
  | `/verbose` | Toggle verbose mode (show thinking and tool calls in chat) |
109
- | `/verbose on|off` | Explicitly enable/disable verbose mode |
109
+ | `/verbose on\|off` | Explicitly enable/disable verbose mode |
110
+ | `/model` | Show current model and usage hints |
111
+ | `/model <keyword>` | Search models by keyword |
112
+ | `/model <number>` | Switch to a model from the last search |
113
+ | `/model default` | Reset to the default model |
114
+ | `/usage` | Show token and cost usage for this session |
110
115
  | `/help` | Show available commands |
111
116
 
112
117
  ### Verbose Mode
@@ -129,6 +134,39 @@ Example with verbose mode on:
129
134
  Here's what I found in the auth module...
130
135
  ```
131
136
 
137
+ ### Model Switching
138
+
139
+ Use `/model` to search and switch models without typing long names:
140
+
141
+ ```
142
+ You: /model sonnet
143
+ Bot: Models matching "sonnet":
144
+ 1. claude-sonnet-4-5 (google-vertex-anthropic)
145
+ 2. claude-sonnet-4 (anthropic)
146
+
147
+ Use /model <number> to select.
148
+
149
+ You: /model 1
150
+ Bot: Switched to claude-sonnet-4-5 (google-vertex-anthropic)
151
+ ```
152
+
153
+ Other commands:
154
+
155
+ - `/model` shows the current model and usage hints
156
+ - `/model default` resets to the server default
157
+
158
+ ## Usage
159
+
160
+ Use `/usage` to see the current session's token counts and estimated cost:
161
+
162
+ ```
163
+ Session usage:
164
+ - Assistant responses: 4
165
+ - Tokens: 1200 total (input 600, output 500, reasoning 100)
166
+ - Cache: read 1200, write 80
167
+ - Cost: $0.0123
168
+ ```
169
+
132
170
  ### Session Export
133
171
 
134
172
  The `/export` command builds a markdown file from the current session and saves it to the directory where OpenCode is running. The file is also sent back to you as a Telegram document.
@@ -152,6 +190,13 @@ If you type an unknown command, the bot will reply with the list of available co
152
190
 
153
191
  Any other text message is forwarded to the OpenCode agent as a prompt. Follow-up messages go into the same session, so the agent has full conversation context. Use `/new` when you want a fresh conversation.
154
192
 
193
+ ### Files and Images
194
+
195
+ You can send files or images to the bot. They are forwarded to OpenCode as file parts (with optional caption text). Text-based files (including markdown) are sent as text instead of file parts for better model compatibility. Supported types:
196
+
197
+ - Photos sent via Telegram
198
+ - Documents (PDFs, images, etc.)
199
+
155
200
  ## Session Management
156
201
 
157
202
  Sessions are never deleted automatically. You can have multiple sessions and switch between them.
package/dist/app.d.ts CHANGED
@@ -1,6 +1,54 @@
1
+ interface OpencodeClientLike {
2
+ session: {
3
+ list: (options?: any) => Promise<any>;
4
+ create: (options: any) => Promise<any>;
5
+ update: (options: any) => Promise<any>;
6
+ delete: (options: any) => Promise<any>;
7
+ get: (options: any) => Promise<any>;
8
+ messages: (options: any) => Promise<any>;
9
+ command: (options: any) => Promise<any>;
10
+ promptAsync: (options: any) => Promise<any>;
11
+ };
12
+ command: {
13
+ list: (options?: any) => Promise<any>;
14
+ };
15
+ provider: {
16
+ list: (options?: any) => Promise<any>;
17
+ };
18
+ path: {
19
+ get: (options?: any) => Promise<any>;
20
+ };
21
+ event: {
22
+ subscribe: (options?: any) => Promise<any>;
23
+ };
24
+ }
1
25
  interface StartOptions {
2
26
  url: string;
3
27
  model?: string;
28
+ launch?: boolean;
29
+ client?: OpencodeClientLike;
30
+ botFactory?: (token: string) => TelegramBot;
31
+ sessionsFilePath?: string;
32
+ }
33
+ interface TelegramBot {
34
+ use: (fn: (ctx: any, next: () => Promise<void>) => Promise<void> | void) => void;
35
+ start: (fn: (ctx: any) => void | Promise<void>) => void;
36
+ help: (fn: (ctx: any) => void | Promise<void>) => void;
37
+ command: (command: string, fn: (ctx: any) => void | Promise<void>) => void;
38
+ on: (event: string, fn: (ctx: any) => void | Promise<void>) => void;
39
+ launch: () => Promise<void>;
40
+ stop: (reason?: string) => void;
41
+ telegram: {
42
+ sendMessage: (chatId: number, text: string, options?: Record<string, unknown>) => Promise<void>;
43
+ deleteMessage: (chatId: number, messageId: number) => Promise<void>;
44
+ sendDocument: (chatId: number, file: {
45
+ source: Buffer;
46
+ filename: string;
47
+ }) => Promise<void>;
48
+ getFileLink: (fileId: string) => Promise<{
49
+ toString(): string;
50
+ }>;
51
+ };
4
52
  }
5
- export declare function startTelegram(options: StartOptions): Promise<void>;
53
+ export declare function startTelegram(options: StartOptions): Promise<TelegramBot>;
6
54
  export {};
package/dist/index.js CHANGED
@@ -18584,8 +18584,9 @@ async function startTelegram(options) {
18584
18584
  else {
18585
18585
  console.log(`[Telegram] Bot is restricted to user ID: ${authorizedUserId}`);
18586
18586
  }
18587
+ const sessionsFile = options.sessionsFilePath || SESSIONS_FILE;
18587
18588
  // Initialize OpenCode client
18588
- const client = client_createOpencodeClient({ baseUrl: url });
18589
+ const client = options.client || client_createOpencodeClient({ baseUrl: url });
18589
18590
  // Verify connection to the OpenCode server and fetch available commands
18590
18591
  const opencodeCommands = new Set();
18591
18592
  let projectDirectory = "";
@@ -18615,7 +18616,7 @@ async function startTelegram(options) {
18615
18616
  }
18616
18617
  // Telegram-only commands that should not be forwarded to OpenCode
18617
18618
  const telegramCommands = new Set([
18618
- "start", "help", "new", "sessions", "switch", "title", "delete", "export", "verbose",
18619
+ "start", "help", "new", "sessions", "switch", "title", "delete", "export", "verbose", "model", "usage",
18619
18620
  ]);
18620
18621
  // Map of chatId -> sessionId for the active session per chat
18621
18622
  const chatSessions = new Map();
@@ -18623,6 +18624,10 @@ async function startTelegram(options) {
18623
18624
  const knownSessionIds = new Set();
18624
18625
  // Set of chatIds with verbose mode enabled
18625
18626
  const chatVerboseMode = new Set();
18627
+ // Map of chatId -> model override (provider/model)
18628
+ const chatModelOverride = new Map();
18629
+ // Map of chatId -> last search results (in-memory only)
18630
+ const chatModelSearchResults = new Map();
18626
18631
  /**
18627
18632
  * Save the chat-to-session mapping and known session IDs to disk.
18628
18633
  */
@@ -18632,8 +18637,9 @@ async function startTelegram(options) {
18632
18637
  active: Object.fromEntries(chatSessions),
18633
18638
  known: [...knownSessionIds],
18634
18639
  verbose: [...chatVerboseMode],
18640
+ models: Object.fromEntries(chatModelOverride),
18635
18641
  };
18636
- (0,external_node_fs_.writeFileSync)(SESSIONS_FILE, JSON.stringify(data, null, 2));
18642
+ (0,external_node_fs_.writeFileSync)(sessionsFile, JSON.stringify(data, null, 2));
18637
18643
  }
18638
18644
  catch (err) {
18639
18645
  console.error("[Telegram] Failed to save sessions file:", err);
@@ -18648,21 +18654,23 @@ async function startTelegram(options) {
18648
18654
  let storedActive = {};
18649
18655
  let storedKnown = [];
18650
18656
  let storedVerbose = [];
18651
- if ((0,external_node_fs_.existsSync)(SESSIONS_FILE)) {
18657
+ let storedModels = {};
18658
+ if ((0,external_node_fs_.existsSync)(sessionsFile)) {
18652
18659
  try {
18653
- const raw = (0,external_node_fs_.readFileSync)(SESSIONS_FILE, "utf-8");
18660
+ const raw = (0,external_node_fs_.readFileSync)(sessionsFile, "utf-8");
18654
18661
  const parsed = JSON.parse(raw);
18655
18662
  if (parsed.active && typeof parsed.active === "object") {
18656
18663
  // New format: { active: {...}, known: [...], verbose: [...] }
18657
18664
  storedActive = parsed.active;
18658
18665
  storedKnown = parsed.known || [];
18659
18666
  storedVerbose = parsed.verbose || [];
18667
+ storedModels = parsed.models || {};
18660
18668
  }
18661
18669
  else {
18662
18670
  // Old format: flat { chatId: sessionId }
18663
18671
  storedActive = parsed;
18664
18672
  }
18665
- console.log(`[Telegram] Loaded ${Object.keys(storedActive).length} active session(s) and ${storedKnown.length} known session(s) from ${SESSIONS_FILE}`);
18673
+ console.log(`[Telegram] Loaded ${Object.keys(storedActive).length} active session(s) and ${storedKnown.length} known session(s) from ${sessionsFile}`);
18666
18674
  }
18667
18675
  catch (err) {
18668
18676
  console.warn("[Telegram] Failed to parse sessions file:", err);
@@ -18675,6 +18683,12 @@ async function startTelegram(options) {
18675
18683
  if (storedVerbose.length > 0) {
18676
18684
  console.log(`[Telegram] Restored verbose mode for ${storedVerbose.length} chat(s)`);
18677
18685
  }
18686
+ // Restore model overrides
18687
+ for (const [chatId, modelId] of Object.entries(storedModels)) {
18688
+ if (modelId) {
18689
+ chatModelOverride.set(chatId, modelId);
18690
+ }
18691
+ }
18678
18692
  // Fetch all server sessions once for validation and fallback matching
18679
18693
  let serverSessions = [];
18680
18694
  try {
@@ -18777,6 +18791,45 @@ async function startTelegram(options) {
18777
18791
  console.warn("[Telegram] Failed to auto-title session:", err);
18778
18792
  }
18779
18793
  }
18794
+ function buildPromptBody(chatId, parts) {
18795
+ const promptBody = { parts };
18796
+ const modelOverride = chatModelOverride.get(chatId) || model;
18797
+ if (modelOverride) {
18798
+ const [providerID, ...modelParts] = modelOverride.split("/");
18799
+ const modelID = modelParts.join("/");
18800
+ promptBody.model = { providerID, modelID };
18801
+ }
18802
+ return promptBody;
18803
+ }
18804
+ async function getTelegramFileUrl(fileId) {
18805
+ const link = await bot.telegram.getFileLink(fileId);
18806
+ return link.toString();
18807
+ }
18808
+ function isTextMime(mime) {
18809
+ const normalized = mime.toLowerCase();
18810
+ return (normalized.startsWith("text/") ||
18811
+ normalized === "application/json" ||
18812
+ normalized === "application/xml" ||
18813
+ normalized === "application/yaml" ||
18814
+ normalized === "application/x-yaml" ||
18815
+ normalized === "application/markdown");
18816
+ }
18817
+ async function fetchTelegramFileText(fileId, maxBytes = 200_000) {
18818
+ const url = await getTelegramFileUrl(fileId);
18819
+ const response = await fetch(url);
18820
+ if (!response.ok) {
18821
+ throw new Error(`Failed to fetch file: ${response.status}`);
18822
+ }
18823
+ const contentLength = response.headers.get("content-length");
18824
+ if (contentLength && Number(contentLength) > maxBytes) {
18825
+ throw new Error("File is too large to send as text.");
18826
+ }
18827
+ const buffer = await response.arrayBuffer();
18828
+ if (buffer.byteLength > maxBytes) {
18829
+ throw new Error("File is too large to send as text.");
18830
+ }
18831
+ return new TextDecoder("utf-8").decode(buffer);
18832
+ }
18780
18833
  /**
18781
18834
  * Build a one-line summary for a tool call, picking the most meaningful input field.
18782
18835
  */
@@ -18986,6 +19039,40 @@ async function startTelegram(options) {
18986
19039
  await sendTelegramMessage(chatId, chunk);
18987
19040
  }
18988
19041
  }
19042
+ async function handleFileMessage(ctx, fileId, mime, filename, caption) {
19043
+ const chatId = ctx.chat.id.toString();
19044
+ try {
19045
+ const processingMsg = await ctx.reply("Processing your request...");
19046
+ const sessionId = await getOrCreateSession(chatId);
19047
+ const titleText = caption || filename || "File upload";
19048
+ await autoTitleSession(sessionId, titleText);
19049
+ const parts = [];
19050
+ if (caption) {
19051
+ parts.push({ type: "text", text: caption });
19052
+ }
19053
+ const normalizedMime = mime.toLowerCase();
19054
+ if (isTextMime(normalizedMime)) {
19055
+ const textContent = await fetchTelegramFileText(fileId);
19056
+ const header = filename ? `File: ${filename}` : "File";
19057
+ parts.push({
19058
+ type: "text",
19059
+ text: `${header}\n\n${textContent}`,
19060
+ });
19061
+ }
19062
+ else {
19063
+ const url = await getTelegramFileUrl(fileId);
19064
+ parts.push({ type: "file", mime: normalizedMime, filename, url });
19065
+ }
19066
+ const promptBody = buildPromptBody(chatId, parts);
19067
+ const verbose = chatVerboseMode.has(chatId);
19068
+ await sendPromptStreaming(ctx.chat.id, sessionId, promptBody, processingMsg.message_id, verbose);
19069
+ }
19070
+ catch (err) {
19071
+ console.error("[Telegram] Error processing file message:", err);
19072
+ await handleSessionError(chatId);
19073
+ await ctx.reply("Sorry, there was an error processing your request. Try again or use /new to start a fresh session.");
19074
+ }
19075
+ }
18989
19076
  /**
18990
19077
  * Handle session errors - clear invalid sessions.
18991
19078
  */
@@ -19010,7 +19097,9 @@ async function startTelegram(options) {
19010
19097
  }
19011
19098
  }
19012
19099
  // Initialize Telegram bot
19013
- const bot = new lib.Telegraf(token);
19100
+ const bot = options.botFactory
19101
+ ? options.botFactory(token)
19102
+ : new lib.Telegraf(token);
19014
19103
  // Middleware to check if the user is authorized
19015
19104
  bot.use((ctx, next) => {
19016
19105
  if (!authorizedUserId) {
@@ -19039,6 +19128,8 @@ async function startTelegram(options) {
19039
19128
  "/export full - Export with all details (thinking, costs, steps)\n" +
19040
19129
  "/verbose - Toggle verbose mode (show thinking and tool calls)\n" +
19041
19130
  "/verbose on|off - Set verbose mode explicitly\n" +
19131
+ "/model - Show or search available models\n" +
19132
+ "/usage - Show token and cost usage for this session\n" +
19042
19133
  "/help - Show this help message\n";
19043
19134
  if (opencodeCommands.size > 0) {
19044
19135
  msg +=
@@ -19060,6 +19151,8 @@ async function startTelegram(options) {
19060
19151
  "/export full - Export with all details (thinking, costs, steps)\n" +
19061
19152
  "/verbose - Toggle verbose mode (show thinking and tool calls)\n" +
19062
19153
  "/verbose on|off - Set verbose mode explicitly\n" +
19154
+ "/model - Show or search available models\n" +
19155
+ "/usage - Show token and cost usage for this session\n" +
19063
19156
  "/help - Show this help message\n";
19064
19157
  if (opencodeCommands.size > 0) {
19065
19158
  msg +=
@@ -19081,9 +19174,10 @@ async function startTelegram(options) {
19081
19174
  */
19082
19175
  async function getKnownSessions() {
19083
19176
  const list = await client.session.list();
19084
- if (!list.data || list.data.length === 0)
19177
+ const data = (list.data || []);
19178
+ if (data.length === 0)
19085
19179
  return [];
19086
- return list.data
19180
+ return data
19087
19181
  .filter((s) => knownSessionIds.has(s.id))
19088
19182
  .sort((a, b) => b.time.updated - a.time.updated);
19089
19183
  }
@@ -19245,6 +19339,161 @@ async function startTelegram(options) {
19245
19339
  ? "Verbose mode enabled. Responses will include thinking and tool calls."
19246
19340
  : "Verbose mode disabled. Responses will only show the assistant's text.");
19247
19341
  });
19342
+ /**
19343
+ * Fetch and search available models from connected providers.
19344
+ */
19345
+ async function searchModels(keyword) {
19346
+ const list = await client.provider.list();
19347
+ if (list.error || !list.data) {
19348
+ throw new Error(`Failed to list providers: ${JSON.stringify(list.error)}`);
19349
+ }
19350
+ const connected = new Set(list.data.connected || []);
19351
+ const results = [];
19352
+ const query = keyword.toLowerCase();
19353
+ for (const provider of list.data.all || []) {
19354
+ if (!connected.has(provider.id))
19355
+ continue;
19356
+ const providerName = (provider.name || provider.id).toLowerCase();
19357
+ const models = provider.models;
19358
+ for (const [modelID, model] of Object.entries(models || {})) {
19359
+ const modelName = ((model && model.name) || modelID).toLowerCase();
19360
+ if (modelID.toLowerCase().includes(query) ||
19361
+ modelName.includes(query) ||
19362
+ providerName.includes(query)) {
19363
+ const displayName = `${(model && model.name) || modelID} (${provider.id})`;
19364
+ results.push({ providerID: provider.id, modelID, displayName });
19365
+ }
19366
+ }
19367
+ }
19368
+ return results;
19369
+ }
19370
+ // Handle /model command
19371
+ // Usage: /model, /model <keyword>, /model <number>, /model default
19372
+ bot.command("model", async (ctx) => {
19373
+ const chatId = ctx.chat.id.toString();
19374
+ const args = ctx.message.text.replace(/^\/model\s*/, "").trim();
19375
+ if (!args) {
19376
+ const current = chatModelOverride.get(chatId) || model || "server default";
19377
+ await ctx.reply(`Current model: ${current}\n\n` +
19378
+ "Use /model <keyword> to search available models.\n" +
19379
+ "Use /model default to reset to the default model.");
19380
+ return;
19381
+ }
19382
+ if (args.toLowerCase() === "default") {
19383
+ chatModelOverride.delete(chatId);
19384
+ saveSessions();
19385
+ await ctx.reply("Model reset to the default model.");
19386
+ return;
19387
+ }
19388
+ const asNumber = Number.parseInt(args, 10);
19389
+ if (!Number.isNaN(asNumber) && String(asNumber) === args) {
19390
+ const results = chatModelSearchResults.get(chatId) || [];
19391
+ if (results.length === 0) {
19392
+ await ctx.reply("No recent search results. Use /model <keyword> first.");
19393
+ return;
19394
+ }
19395
+ if (asNumber < 1 || asNumber > results.length) {
19396
+ await ctx.reply("Invalid selection. Use /model <number> from the latest search results.");
19397
+ return;
19398
+ }
19399
+ const selection = results[asNumber - 1];
19400
+ const value = `${selection.providerID}/${selection.modelID}`;
19401
+ chatModelOverride.set(chatId, value);
19402
+ saveSessions();
19403
+ await ctx.reply(`Switched to ${selection.displayName}`);
19404
+ return;
19405
+ }
19406
+ try {
19407
+ const results = await searchModels(args);
19408
+ if (results.length === 0) {
19409
+ await ctx.reply(`No models found matching "${args}".`);
19410
+ return;
19411
+ }
19412
+ const limited = results.slice(0, 10);
19413
+ chatModelSearchResults.set(chatId, limited);
19414
+ let msg = `Models matching "${args}":\n\n`;
19415
+ for (const [index, item] of limited.entries()) {
19416
+ msg += `${index + 1}. ${item.displayName}\n`;
19417
+ }
19418
+ if (results.length > limited.length) {
19419
+ msg += `\nFound ${results.length} models. Refine your search to narrow the list.`;
19420
+ }
19421
+ msg += "\nUse /model <number> to select.";
19422
+ await ctx.reply(msg);
19423
+ }
19424
+ catch (err) {
19425
+ console.error("[Telegram] Error searching models:", err);
19426
+ await ctx.reply("Failed to list models. Try again later.");
19427
+ }
19428
+ });
19429
+ // Handle /usage command - show token and cost usage for current session
19430
+ bot.command("usage", async (ctx) => {
19431
+ const chatId = ctx.chat.id.toString();
19432
+ const sessionId = chatSessions.get(chatId);
19433
+ if (!sessionId) {
19434
+ await ctx.reply("No active session. Send a message first to create one.");
19435
+ return;
19436
+ }
19437
+ try {
19438
+ const messagesResult = await client.session.messages({
19439
+ path: { id: sessionId },
19440
+ });
19441
+ if (messagesResult.error || !messagesResult.data) {
19442
+ throw new Error(`Failed to get messages: ${JSON.stringify(messagesResult.error)}`);
19443
+ }
19444
+ let assistantCount = 0;
19445
+ let costTotal = 0;
19446
+ let inputTokens = 0;
19447
+ let outputTokens = 0;
19448
+ let reasoningTokens = 0;
19449
+ let cacheRead = 0;
19450
+ let cacheWrite = 0;
19451
+ for (const msg of messagesResult.data) {
19452
+ const info = msg.info;
19453
+ if (info.role !== "assistant")
19454
+ continue;
19455
+ assistantCount += 1;
19456
+ costTotal += info.cost || 0;
19457
+ if (info.tokens) {
19458
+ inputTokens += info.tokens.input || 0;
19459
+ outputTokens += info.tokens.output || 0;
19460
+ reasoningTokens += info.tokens.reasoning || 0;
19461
+ cacheRead += info.tokens.cache?.read || 0;
19462
+ cacheWrite += info.tokens.cache?.write || 0;
19463
+ }
19464
+ }
19465
+ const totalTokens = inputTokens + outputTokens + reasoningTokens;
19466
+ const lines = [
19467
+ `Session usage:`,
19468
+ `- Assistant responses: ${assistantCount}`,
19469
+ `- Tokens: ${totalTokens} total (input ${inputTokens}, output ${outputTokens}, reasoning ${reasoningTokens})`,
19470
+ `- Cache: read ${cacheRead}, write ${cacheWrite}`,
19471
+ `- Cost: $${costTotal.toFixed(4)}`,
19472
+ ];
19473
+ await ctx.reply(lines.join("\n"));
19474
+ }
19475
+ catch (err) {
19476
+ console.error("[Telegram] Error getting usage:", err);
19477
+ await ctx.reply("Failed to fetch usage. Try again later.");
19478
+ }
19479
+ });
19480
+ // Handle photo messages (images)
19481
+ bot.on("photo", async (ctx) => {
19482
+ const photos = ctx.message.photo;
19483
+ if (!photos || photos.length === 0)
19484
+ return;
19485
+ const largest = photos[photos.length - 1];
19486
+ const caption = ctx.message.caption;
19487
+ await handleFileMessage(ctx, largest.file_id, "image/jpeg", "photo.jpg", caption);
19488
+ });
19489
+ // Handle document messages (files)
19490
+ bot.on("document", async (ctx) => {
19491
+ const doc = ctx.message.document;
19492
+ if (!doc)
19493
+ return;
19494
+ const caption = ctx.message.caption;
19495
+ await handleFileMessage(ctx, doc.file_id, doc.mime_type || "application/octet-stream", doc.file_name, caption);
19496
+ });
19248
19497
  /**
19249
19498
  * Render a message part to markdown.
19250
19499
  * In default mode: only text and tool calls (name + input/output).
@@ -19527,15 +19776,9 @@ async function startTelegram(options) {
19527
19776
  const sessionId = await getOrCreateSession(chatId);
19528
19777
  // Auto-title if this is the first message in a new session
19529
19778
  await autoTitleSession(sessionId, userText);
19530
- // Build the prompt body
19531
- const promptBody = {
19532
- parts: [{ type: "text", text: userText }],
19533
- };
19534
- if (model) {
19535
- const [providerID, ...modelParts] = model.split("/");
19536
- const modelID = modelParts.join("/");
19537
- promptBody.model = { providerID, modelID };
19538
- }
19779
+ const promptBody = buildPromptBody(chatId, [
19780
+ { type: "text", text: userText },
19781
+ ]);
19539
19782
  const verbose = chatVerboseMode.has(chatId);
19540
19783
  await sendPromptStreaming(ctx.chat.id, sessionId, promptBody, processingMsg.message_id, verbose);
19541
19784
  }
@@ -19545,18 +19788,21 @@ async function startTelegram(options) {
19545
19788
  await ctx.reply("Sorry, there was an error processing your request. Try again or use /new to start a fresh session.");
19546
19789
  }
19547
19790
  });
19548
- try {
19549
- // Start the bot
19550
- await bot.launch();
19551
- console.log("[Telegram] Bot is running");
19552
- // Enable graceful stop
19553
- process.once("SIGINT", () => bot.stop("SIGINT"));
19554
- process.once("SIGTERM", () => bot.stop("SIGTERM"));
19555
- }
19556
- catch (error) {
19557
- console.error("Unable to start the Telegram bot:", error);
19558
- throw error;
19791
+ if (options.launch !== false) {
19792
+ try {
19793
+ // Start the bot
19794
+ await bot.launch();
19795
+ console.log("[Telegram] Bot is running");
19796
+ // Enable graceful stop
19797
+ process.once("SIGINT", () => bot.stop("SIGINT"));
19798
+ process.once("SIGTERM", () => bot.stop("SIGTERM"));
19799
+ }
19800
+ catch (error) {
19801
+ console.error("Unable to start the Telegram bot:", error);
19802
+ throw error;
19803
+ }
19559
19804
  }
19805
+ return bot;
19560
19806
  }
19561
19807
  /**
19562
19808
  * Format a Unix timestamp (seconds) into a human-readable relative time.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-telegram-bot",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "description": "Telegram bot that forwards messages to an OpenCode agent",
6
6
  "main": "./dist/index.js",
@@ -14,7 +14,10 @@
14
14
  "start": "tsx src/index.ts",
15
15
  "serve": "opencode serve",
16
16
  "build": "ncc build src/index.ts --out dist",
17
- "prepublishOnly": "npm run build"
17
+ "prepublishOnly": "npm run build",
18
+ "test": "vitest",
19
+ "test:run": "vitest run",
20
+ "test:integration": "vitest run tests/integration.test.ts"
18
21
  },
19
22
  "keywords": [
20
23
  "opencode",
@@ -35,6 +38,7 @@
35
38
  "devDependencies": {
36
39
  "@vercel/ncc": "^0.38.3",
37
40
  "tsx": "^4.0.0",
38
- "@types/node": "^20.0.0"
41
+ "@types/node": "^20.0.0",
42
+ "vitest": "^2.1.5"
39
43
  }
40
44
  }