opencode-telegram-bot 1.0.10 → 1.1.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/README.md CHANGED
@@ -112,6 +112,8 @@ These are handled directly by the Telegram bot:
112
112
  | `/model` | Show current model and usage hints |
113
113
  | `/model <keyword>` | Search models by keyword |
114
114
  | `/model default` | Reset to the default model |
115
+ | `/agent` | Show current agent and switch (plan, build, ...) |
116
+ | `/agent <name>` | Switch directly to a named agent |
115
117
  | `/usage` | Show token and cost usage for this session |
116
118
  | `/help` | Show available commands |
117
119
 
@@ -177,6 +179,33 @@ Other commands:
177
179
  - `/model` shows the current model and usage hints
178
180
  - `/model default` resets to the server default
179
181
 
182
+ ### Agent Switching
183
+
184
+ OpenCode supports multiple agents: **build** (default, full edit access), **plan** (read-only analysis), and others. Use `/agent` to see and switch between them:
185
+
186
+ ```
187
+ You: /agent
188
+ Bot: Current agent: build
189
+
190
+ Available agents:
191
+ - build (active) -- The default agent
192
+ - plan -- Plan mode. Disallows all edit tools.
193
+ - general -- General-purpose agent
194
+ - explore -- Codebase explorer
195
+
196
+ [build] [plan] [general] [explore]
197
+
198
+ You: [tap "plan"]
199
+ (button message is deleted)
200
+ Bot: Agent: plan | Model: claude-opus-4 | Verbose: off [pinned]
201
+ ```
202
+
203
+ ### Pinned Status Message
204
+
205
+ A pinned message at the top of the chat shows the current agent, model, and verbose mode. It updates automatically when any of these change — via `/agent`, `/model`, or `/verbose`. The pinned message is the only confirmation; no separate reply is sent.
206
+
207
+ Agent, model, and verbose selections are per-chat and persist across bot restarts.
208
+
180
209
  ## Usage
181
210
 
182
211
  Use `/usage` to see the current session's token counts and estimated cost:
@@ -251,7 +280,7 @@ You: [tap "Auth refactoring"]
251
280
  Bot: Switched to session: Auth refactoring
252
281
  ```
253
282
 
254
- The `/sessions` list only shows sessions created by the Telegram bot, not sessions from other OpenCode clients (like the TUI). You cannot delete the currently active session -- use `/new` first.
283
+ The `/sessions` list shows sessions created by the Telegram bot. At the bottom of the list, a **"Show all sessions"** button lets you discover sessions created by other OpenCode clients (like the TUI via `opencode attach`). Tapping an external session adopts it and switches to it. You cannot delete the currently active session -- use `/new` first.
255
284
 
256
285
  ## Session Persistence
257
286
 
package/dist/app.d.ts CHANGED
@@ -26,6 +26,9 @@ interface OpencodeClientLike {
26
26
  reply: (params: any, options?: any) => Promise<any>;
27
27
  reject: (params: any, options?: any) => Promise<any>;
28
28
  };
29
+ app: {
30
+ agents: (params?: any, options?: any) => Promise<any>;
31
+ };
29
32
  }
30
33
  interface StartOptions {
31
34
  url: string;
@@ -48,7 +51,7 @@ interface TelegramBot {
48
51
  }) => Promise<void>;
49
52
  stop: (reason?: string) => void;
50
53
  telegram: {
51
- sendMessage: (chatId: number, text: string, options?: Record<string, unknown>) => Promise<void>;
54
+ sendMessage: (chatId: number, text: string, options?: Record<string, unknown>) => Promise<any>;
52
55
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
53
56
  sendDocument: (chatId: number, file: {
54
57
  source: Buffer;
@@ -61,6 +64,9 @@ interface TelegramBot {
61
64
  command: string;
62
65
  description: string;
63
66
  }>) => Promise<unknown>;
67
+ pinChatMessage: (chatId: number, messageId: number, extra?: Record<string, unknown>) => Promise<void>;
68
+ unpinChatMessage: (chatId: number, messageId?: number) => Promise<void>;
69
+ editMessageText: (chatId: number | undefined, messageId: number | undefined, inlineMessageId: string | undefined, text: string, extra?: Record<string, unknown>) => Promise<void>;
64
70
  };
65
71
  }
66
72
  export declare function startTelegram(options: StartOptions): Promise<TelegramBot>;
package/dist/index.js CHANGED
@@ -19967,6 +19967,8 @@ async function startTelegram(options) {
19967
19967
  const sessionsFile = options.sessionsFilePath || SESSIONS_FILE;
19968
19968
  // Initialize OpenCode client
19969
19969
  const client = options.client || client_createOpencodeClient({ baseUrl: url });
19970
+ // Available agents fetched from OpenCode on startup
19971
+ let availableAgents = [];
19970
19972
  // Verify connection to the OpenCode server and fetch available commands
19971
19973
  const opencodeCommands = new Set();
19972
19974
  const opencodeCommandMenu = [];
@@ -20004,13 +20006,25 @@ async function startTelegram(options) {
20004
20006
  }
20005
20007
  console.log(`[Telegram] Available OpenCode commands: ${[...opencodeCommands].join(", ")}`);
20006
20008
  }
20009
+ // Fetch available agents
20010
+ const agentsResult = await client.app.agents({});
20011
+ if (agentsResult.data) {
20012
+ availableAgents = agentsResult.data
20013
+ .filter((a) => !a.hidden)
20014
+ .map((a) => ({
20015
+ name: a.name,
20016
+ description: a.description || "",
20017
+ mode: a.mode || "primary",
20018
+ }));
20019
+ console.log(`[Telegram] Available agents: ${availableAgents.map((a) => `${a.name} (${a.mode})`).join(", ")}`);
20020
+ }
20007
20021
  }
20008
20022
  catch (err) {
20009
20023
  throw new Error(`[Telegram] Failed to connect to OpenCode server at ${url}. Make sure it's running (npm run serve). Error: ${err}`);
20010
20024
  }
20011
20025
  // Telegram-only commands that should not be forwarded to OpenCode
20012
20026
  const telegramCommands = new Set([
20013
- "start", "help", "new", "sessions", "switch", "title", "delete", "export", "verbose", "model", "usage",
20027
+ "start", "help", "new", "sessions", "switch", "title", "delete", "export", "verbose", "model", "usage", "agent",
20014
20028
  ]);
20015
20029
  const telegramCommandMenu = [
20016
20030
  { command: "new", description: "Start a new conversation" },
@@ -20019,6 +20033,7 @@ async function startTelegram(options) {
20019
20033
  { command: "export", description: "Export session (/export full for details)" },
20020
20034
  { command: "verbose", description: "Toggle verbose mode" },
20021
20035
  { command: "model", description: "Search models (/model <keyword>)" },
20036
+ { command: "agent", description: "Switch agent (plan, build, ...)" },
20022
20037
  { command: "usage", description: "Show token and cost usage" },
20023
20038
  { command: "help", description: "Show available commands" },
20024
20039
  ];
@@ -20031,6 +20046,10 @@ async function startTelegram(options) {
20031
20046
  const chatVerboseMode = new Set();
20032
20047
  // Map of chatId -> model override (provider/model)
20033
20048
  const chatModelOverride = new Map();
20049
+ // Map of chatId -> agent override (e.g. "plan", "build")
20050
+ const chatAgentOverride = new Map();
20051
+ // Map of chatId -> pinned status message ID
20052
+ const chatPinnedStatusMsg = new Map();
20034
20053
  // Map of chatId -> last search results (in-memory only)
20035
20054
  const chatModelSearchResults = new Map();
20036
20055
  // Map of questionId -> pending question context (for forwarding OpenCode questions to Telegram)
@@ -20045,6 +20064,8 @@ async function startTelegram(options) {
20045
20064
  known: [...knownSessionIds],
20046
20065
  verbose: [...chatVerboseMode],
20047
20066
  models: Object.fromEntries(chatModelOverride),
20067
+ agents: Object.fromEntries(chatAgentOverride),
20068
+ pinnedStatus: Object.fromEntries(chatPinnedStatusMsg),
20048
20069
  };
20049
20070
  (0,external_node_fs_.writeFileSync)(sessionsFile, JSON.stringify(data, null, 2));
20050
20071
  }
@@ -20062,6 +20083,8 @@ async function startTelegram(options) {
20062
20083
  let storedKnown = [];
20063
20084
  let storedVerbose = [];
20064
20085
  let storedModels = {};
20086
+ let storedAgents = {};
20087
+ let storedPinnedStatus = {};
20065
20088
  if ((0,external_node_fs_.existsSync)(sessionsFile)) {
20066
20089
  try {
20067
20090
  const raw = (0,external_node_fs_.readFileSync)(sessionsFile, "utf-8");
@@ -20072,6 +20095,8 @@ async function startTelegram(options) {
20072
20095
  storedKnown = parsed.known || [];
20073
20096
  storedVerbose = parsed.verbose || [];
20074
20097
  storedModels = parsed.models || {};
20098
+ storedAgents = parsed.agents || {};
20099
+ storedPinnedStatus = parsed.pinnedStatus || {};
20075
20100
  }
20076
20101
  else {
20077
20102
  // Old format: flat { chatId: sessionId }
@@ -20096,6 +20121,18 @@ async function startTelegram(options) {
20096
20121
  chatModelOverride.set(chatId, modelId);
20097
20122
  }
20098
20123
  }
20124
+ // Restore agent overrides
20125
+ for (const [chatId, agentName] of Object.entries(storedAgents)) {
20126
+ if (agentName) {
20127
+ chatAgentOverride.set(chatId, agentName);
20128
+ }
20129
+ }
20130
+ // Restore pinned status message IDs
20131
+ for (const [chatId, msgId] of Object.entries(storedPinnedStatus)) {
20132
+ if (msgId) {
20133
+ chatPinnedStatusMsg.set(chatId, msgId);
20134
+ }
20135
+ }
20099
20136
  // Fetch all server sessions once for validation and fallback matching
20100
20137
  let serverSessions = [];
20101
20138
  try {
@@ -20214,6 +20251,10 @@ async function startTelegram(options) {
20214
20251
  const modelID = modelParts.join("/");
20215
20252
  promptBody.model = { providerID, modelID };
20216
20253
  }
20254
+ const agentOverride = chatAgentOverride.get(chatId);
20255
+ if (agentOverride) {
20256
+ promptBody.agent = agentOverride;
20257
+ }
20217
20258
  return promptBody;
20218
20259
  }
20219
20260
  function getChatIdFromContext(ctx) {
@@ -20787,7 +20828,12 @@ async function startTelegram(options) {
20787
20828
  if (!authorizedUserId) {
20788
20829
  return next();
20789
20830
  }
20790
- const userId = ctx.from?.id.toString();
20831
+ // Skip auth for service messages without a sender or from the bot itself
20832
+ // (e.g. pin notifications where ctx.from is the bot)
20833
+ if (!ctx.from || ctx.from.is_bot) {
20834
+ return next();
20835
+ }
20836
+ const userId = ctx.from.id.toString();
20791
20837
  if (userId === authorizedUserId) {
20792
20838
  return next();
20793
20839
  }
@@ -20810,6 +20856,7 @@ async function startTelegram(options) {
20810
20856
  "/verbose - Toggle verbose mode (show thinking and tool calls)\n" +
20811
20857
  "/verbose on|off - Set verbose mode explicitly\n" +
20812
20858
  "/model <keyword> - Search available models\n" +
20859
+ "/agent - Switch agent (plan, build, ...)\n" +
20813
20860
  "/usage - Show token and cost usage for this session\n" +
20814
20861
  "/help - Show this help message\n";
20815
20862
  const visibleOpenCodeCommands = getVisibleOpenCodeCommands();
@@ -20832,6 +20879,7 @@ async function startTelegram(options) {
20832
20879
  "/verbose - Toggle verbose mode (show thinking and tool calls)\n" +
20833
20880
  "/verbose on|off - Set verbose mode explicitly\n" +
20834
20881
  "/model <keyword> - Search available models\n" +
20882
+ "/agent - Switch agent (plan, build, ...)\n" +
20835
20883
  "/usage - Show token and cost usage for this session\n" +
20836
20884
  "/help - Show this help message\n";
20837
20885
  const visibleOpenCodeCommands = getVisibleOpenCodeCommands();
@@ -20875,7 +20923,13 @@ async function startTelegram(options) {
20875
20923
  try {
20876
20924
  const sessions = await getKnownSessions();
20877
20925
  if (sessions.length === 0) {
20878
- await ctx.reply("No sessions found.");
20926
+ await ctx.reply("No sessions found.\n\nYou can discover sessions created outside this bot:", {
20927
+ reply_markup: {
20928
+ inline_keyboard: [
20929
+ [{ text: "Show all sessions", callback_data: "sessions:all" }],
20930
+ ],
20931
+ },
20932
+ });
20879
20933
  return;
20880
20934
  }
20881
20935
  const activeSession = sessions.find((session) => session.id === activeSessionId);
@@ -20884,25 +20938,33 @@ async function startTelegram(options) {
20884
20938
  msgLines.push(`Current session: ${activeSession.title}`);
20885
20939
  }
20886
20940
  const otherSessions = sessions.filter((session) => session.id !== activeSessionId);
20887
- if (otherSessions.length === 0) {
20941
+ const keyboard = [];
20942
+ if (otherSessions.length > 0) {
20943
+ msgLines.push("Tap a session to switch or delete.");
20944
+ otherSessions.forEach((session) => {
20945
+ keyboard.push([
20946
+ {
20947
+ text: session.title,
20948
+ callback_data: `switch:${session.id}`,
20949
+ },
20950
+ {
20951
+ text: "Delete",
20952
+ callback_data: `delete:${session.id}`,
20953
+ },
20954
+ ]);
20955
+ });
20956
+ }
20957
+ else {
20888
20958
  msgLines.push("This is your only session.");
20889
- await ctx.reply(msgLines.join("\n"));
20890
- return;
20891
20959
  }
20892
- msgLines.push("Tap a session to switch or delete.");
20893
- const keyboard = [];
20894
- otherSessions.forEach((session) => {
20895
- keyboard.push([
20896
- {
20897
- text: session.title,
20898
- callback_data: `switch:${session.id}`,
20899
- },
20900
- {
20901
- text: "Delete",
20902
- callback_data: `delete:${session.id}`,
20903
- },
20904
- ]);
20905
- });
20960
+ // Always show "Show all sessions" button to discover sessions
20961
+ // created outside the Telegram bot (e.g. via opencode TUI)
20962
+ keyboard.push([
20963
+ {
20964
+ text: "Show all sessions",
20965
+ callback_data: "sessions:all",
20966
+ },
20967
+ ]);
20906
20968
  await ctx.reply(msgLines.join("\n"), {
20907
20969
  reply_markup: {
20908
20970
  inline_keyboard: keyboard,
@@ -21062,6 +21124,83 @@ async function startTelegram(options) {
21062
21124
  await answerAndEdit(ctx, "Failed to delete session.");
21063
21125
  }
21064
21126
  });
21127
+ // Handle "Show all sessions" button - list sessions not tracked by the bot
21128
+ bot.action("sessions:all", async (ctx) => {
21129
+ const chatId = getChatIdFromContext(ctx);
21130
+ if (!chatId)
21131
+ return;
21132
+ try {
21133
+ // Delete the button message
21134
+ try {
21135
+ await ctx.deleteMessage();
21136
+ }
21137
+ catch {
21138
+ // Ignore if already deleted
21139
+ }
21140
+ await ctx.answerCbQuery();
21141
+ const list = await client.session.list({});
21142
+ const allSessions = (list.data || [])
21143
+ .filter((s) => !knownSessionIds.has(s.id) && !s.parentID)
21144
+ .sort((a, b) => b.time.updated - a.time.updated);
21145
+ if (allSessions.length === 0) {
21146
+ await ctx.reply("No other sessions found.");
21147
+ return;
21148
+ }
21149
+ const keyboard = [];
21150
+ allSessions.forEach((session) => {
21151
+ keyboard.push([
21152
+ {
21153
+ text: session.title,
21154
+ callback_data: `adopt:${session.id}`,
21155
+ },
21156
+ ]);
21157
+ });
21158
+ await ctx.reply("Sessions created outside this bot.\nTap to adopt and switch:", {
21159
+ reply_markup: {
21160
+ inline_keyboard: keyboard,
21161
+ },
21162
+ });
21163
+ }
21164
+ catch (err) {
21165
+ console.error("[Telegram] Error listing all sessions:", err);
21166
+ await ctx.reply("Failed to list sessions.");
21167
+ }
21168
+ });
21169
+ // Handle adopt button - adopt an external session and switch to it
21170
+ bot.action(/^adopt:(.+)$/, async (ctx) => {
21171
+ const chatId = getChatIdFromContext(ctx);
21172
+ const sessionId = ctx.match?.[1];
21173
+ if (!chatId || !sessionId)
21174
+ return;
21175
+ try {
21176
+ // Verify the session still exists on the server
21177
+ const list = await client.session.list({});
21178
+ const allSessions = (list.data || []);
21179
+ const match = allSessions.find((s) => s.id === sessionId);
21180
+ if (!match) {
21181
+ await answerAndEdit(ctx, "Session not found. It may have been deleted.");
21182
+ return;
21183
+ }
21184
+ // Delete the button message
21185
+ try {
21186
+ await ctx.deleteMessage();
21187
+ }
21188
+ catch {
21189
+ // Ignore if already deleted
21190
+ }
21191
+ await ctx.answerCbQuery();
21192
+ // Adopt: add to known sessions and switch to it
21193
+ knownSessionIds.add(match.id);
21194
+ chatSessions.set(chatId, match.id);
21195
+ saveSessions();
21196
+ console.log(`[Telegram] Adopted and switched chat ${chatId} to session ${match.id}`);
21197
+ await ctx.reply(`Adopted and switched to session: ${match.title}`);
21198
+ }
21199
+ catch (err) {
21200
+ console.error("[Telegram] Error adopting session:", err);
21201
+ await answerAndEdit(ctx, "Failed to adopt session.");
21202
+ }
21203
+ });
21065
21204
  // Handle /verbose command - toggle verbose mode for this chat
21066
21205
  // Usage: /verbose, /verbose on, /verbose off
21067
21206
  bot.command("verbose", (ctx) => {
@@ -21084,9 +21223,7 @@ async function startTelegram(options) {
21084
21223
  saveSessions();
21085
21224
  const enabled = chatVerboseMode.has(chatId);
21086
21225
  console.log(`[Telegram] Verbose mode ${enabled ? "enabled" : "disabled"} for chat ${chatId}`);
21087
- ctx.reply(enabled
21088
- ? "Verbose mode enabled. Responses will include thinking and tool calls."
21089
- : "Verbose mode disabled. Responses will only show the assistant's text.");
21226
+ updatePinnedStatus(chatId, ctx.chat.id);
21090
21227
  });
21091
21228
  /**
21092
21229
  * Fetch and search available models from connected providers.
@@ -21186,7 +21323,15 @@ async function startTelegram(options) {
21186
21323
  const value = `${selection.providerID}/${selection.modelID}`;
21187
21324
  chatModelOverride.set(chatId, value);
21188
21325
  saveSessions();
21189
- await answerAndEdit(ctx, `Switched to ${selection.displayName}`);
21326
+ try {
21327
+ await ctx.answerCbQuery();
21328
+ }
21329
+ catch { /* ignore */ }
21330
+ try {
21331
+ await ctx.deleteMessage();
21332
+ }
21333
+ catch { /* ignore */ }
21334
+ updatePinnedStatus(chatId, Number(chatId));
21190
21335
  });
21191
21336
  bot.action("model_default", async (ctx) => {
21192
21337
  const chatId = getChatIdFromContext(ctx);
@@ -21194,7 +21339,144 @@ async function startTelegram(options) {
21194
21339
  return;
21195
21340
  chatModelOverride.delete(chatId);
21196
21341
  saveSessions();
21197
- await answerAndEdit(ctx, "Model reset to the default model.");
21342
+ try {
21343
+ await ctx.answerCbQuery();
21344
+ }
21345
+ catch { /* ignore */ }
21346
+ try {
21347
+ await ctx.deleteMessage();
21348
+ }
21349
+ catch { /* ignore */ }
21350
+ updatePinnedStatus(chatId, Number(chatId));
21351
+ });
21352
+ // --- Agent switching and pinned status message ---
21353
+ function getActiveAgent(chatId) {
21354
+ return chatAgentOverride.get(chatId) || "build";
21355
+ }
21356
+ function getActiveModelDisplay(chatId) {
21357
+ const m = chatModelOverride.get(chatId) || model;
21358
+ if (!m)
21359
+ return "default";
21360
+ const parts = m.split("/");
21361
+ return parts.length > 1 ? parts.slice(1).join("/") : m;
21362
+ }
21363
+ function buildStatusText(chatId) {
21364
+ const agent = getActiveAgent(chatId);
21365
+ const modelDisplay = getActiveModelDisplay(chatId);
21366
+ const verbose = chatVerboseMode.has(chatId) ? "on" : "off";
21367
+ return `Agent: *${agent}* | Model: ${modelDisplay} | Verbose: ${verbose}`;
21368
+ }
21369
+ function buildStatusKeyboard(chatId) {
21370
+ const active = getActiveAgent(chatId);
21371
+ // Show non-hidden agents as buttons, highlight the active one
21372
+ const buttons = availableAgents
21373
+ .filter((a) => a.mode === "primary" || a.mode === "subagent")
21374
+ .map((a) => ({
21375
+ text: a.name === active ? `[${a.name}]` : a.name,
21376
+ callback_data: `agent:${a.name}`,
21377
+ }));
21378
+ // Arrange in rows of 3
21379
+ const keyboard = [];
21380
+ for (let i = 0; i < buttons.length; i += 3) {
21381
+ keyboard.push(buttons.slice(i, i + 3));
21382
+ }
21383
+ return keyboard;
21384
+ }
21385
+ async function updatePinnedStatus(chatId, numericChatId) {
21386
+ const text = buildStatusText(chatId);
21387
+ // Delete the old status message to keep the chat clean
21388
+ const existingMsgId = chatPinnedStatusMsg.get(chatId);
21389
+ if (existingMsgId) {
21390
+ try {
21391
+ await bot.telegram.deleteMessage(numericChatId, existingMsgId);
21392
+ }
21393
+ catch {
21394
+ // Already deleted
21395
+ }
21396
+ chatPinnedStatusMsg.delete(chatId);
21397
+ }
21398
+ // Always send a fresh message and pin it. Editing in place doesn't
21399
+ // refresh the pinned bar on Android, so delete+send+pin is the only
21400
+ // reliable approach. The pinned message itself serves as the
21401
+ // confirmation — no separate reply is needed.
21402
+ try {
21403
+ const msg = await bot.telegram.sendMessage(numericChatId, text, {
21404
+ parse_mode: "Markdown",
21405
+ });
21406
+ const messageId = msg.message_id;
21407
+ if (messageId) {
21408
+ chatPinnedStatusMsg.set(chatId, messageId);
21409
+ saveSessions();
21410
+ try {
21411
+ await bot.telegram.pinChatMessage(numericChatId, messageId, {
21412
+ disable_notification: true,
21413
+ });
21414
+ }
21415
+ catch (pinErr) {
21416
+ console.warn("[Telegram] Failed to pin status message:", pinErr);
21417
+ }
21418
+ }
21419
+ }
21420
+ catch (err) {
21421
+ console.warn("[Telegram] Failed to send status message:", err);
21422
+ }
21423
+ }
21424
+ // Handle /agent command
21425
+ bot.command("agent", async (ctx) => {
21426
+ const chatId = ctx.chat.id.toString();
21427
+ const args = (ctx.message?.text || "").replace(/^\/agent\s*/i, "").trim();
21428
+ if (!args) {
21429
+ // Show current agent and list available ones
21430
+ const current = getActiveAgent(chatId);
21431
+ let msg = `Current agent: *${current}*\n\nAvailable agents:\n`;
21432
+ for (const a of availableAgents) {
21433
+ const marker = a.name === current ? " (active)" : "";
21434
+ const desc = a.description ? ` -- ${truncate(a.description, 80)}` : "";
21435
+ msg += `- *${a.name}*${marker}${desc}\n`;
21436
+ }
21437
+ msg += "\nTap a button or use `/agent <name>` to switch.";
21438
+ const keyboard = buildStatusKeyboard(chatId);
21439
+ await ctx.reply(msg, {
21440
+ parse_mode: "Markdown",
21441
+ reply_markup: { inline_keyboard: keyboard },
21442
+ });
21443
+ return;
21444
+ }
21445
+ // Direct switch by name
21446
+ const target = args.toLowerCase();
21447
+ const match = availableAgents.find((a) => a.name === target);
21448
+ if (!match) {
21449
+ await ctx.reply(`Unknown agent "${args}". Available: ${availableAgents.map((a) => a.name).join(", ")}`);
21450
+ return;
21451
+ }
21452
+ chatAgentOverride.set(chatId, match.name);
21453
+ saveSessions();
21454
+ await updatePinnedStatus(chatId, ctx.chat.id);
21455
+ });
21456
+ // Handle agent switch via inline button
21457
+ bot.action(/^agent:(.+)$/, async (ctx) => {
21458
+ const chatId = getChatIdFromContext(ctx);
21459
+ if (!chatId)
21460
+ return;
21461
+ const agentName = ctx.match?.[1];
21462
+ if (!agentName)
21463
+ return;
21464
+ const match = availableAgents.find((a) => a.name === agentName);
21465
+ if (!match) {
21466
+ await answerAndEdit(ctx, `Unknown agent: ${agentName}`);
21467
+ return;
21468
+ }
21469
+ chatAgentOverride.set(chatId, match.name);
21470
+ saveSessions();
21471
+ try {
21472
+ await ctx.answerCbQuery();
21473
+ }
21474
+ catch { /* ignore */ }
21475
+ try {
21476
+ await ctx.deleteMessage();
21477
+ }
21478
+ catch { /* ignore */ }
21479
+ await updatePinnedStatus(chatId, Number(chatId));
21198
21480
  });
21199
21481
  // Handle question answer callback (from OpenCode question.asked events)
21200
21482
  bot.action(/^qa:([^:]+):(\d+):(\d+)$/, async (ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-telegram-bot",
3
- "version": "1.0.10",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "description": "Telegram bot that forwards messages to an OpenCode agent",
6
6
  "main": "./dist/index.js",