opencode-telegram-bot 1.0.7 → 1.0.9

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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/dist/index.js +178 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  A Telegram bot that forwards messages to an [OpenCode](https://opencode.ai) agent and returns the responses. Each chat gets a persistent session, so the agent remembers conversation context across messages.
4
4
 
5
+ ## 🎥 Demo
6
+
7
+ https://github.com/user-attachments/assets/e071f536-7036-4e9c-a5fb-c77414626825
8
+
5
9
  ## Prerequisites
6
10
 
7
11
  - [Node.js](https://nodejs.org/) 18+
@@ -118,7 +122,9 @@ The bot also registers these commands in Telegram's command menu (the `/` button
118
122
  By default, the bot only shows the assistant's final text response. Use `/verbose` to toggle verbose mode (or `/verbose on|off` to set it explicitly), which also displays:
119
123
 
120
124
  - **Thinking/reasoning** -- shown as plain text with a 🧠 prefix, truncated to 500 characters
121
- - **Tool calls** -- shown as a compact one-line summary (e.g. `> read -- src/app.ts`)
125
+ - **Tool calls** -- shown as a compact one-line summary (e.g. `⚙️ read -- src/app.ts`)
126
+ - **Delegations** -- shown when work is delegated to a subagent (e.g. `🧩 Delegated: Find relevant docs`)
127
+ - **Subagent details** -- reasoning/tool calls from subagents when available, plus a short `ℹ️ Subagent responded` notice
122
128
 
123
129
  Verbose mode is per-chat and persists across bot restarts. Use `/verbose` again to turn it off.
124
130
 
@@ -130,6 +136,11 @@ Example with verbose mode on:
130
136
  ⚙️ grep -- pattern: "authenticate" in src/
131
137
  ⚙️ read -- src/auth/handler.ts
132
138
 
139
+ 🧩 Delegated: Search auth docs
140
+ 🧠 Thinking (agent: subagent): Scanning docs for auth references...
141
+ ⚙️ read -- README.md (agent: subagent)
142
+ ℹ️ Subagent responded
143
+
133
144
  Here's what I found in the auth module...
134
145
  ```
135
146
 
package/dist/index.js CHANGED
@@ -18613,10 +18613,12 @@ async function startTelegram(options) {
18613
18613
  if (!name)
18614
18614
  continue;
18615
18615
  opencodeCommands.add(name);
18616
- if (!isHiddenOpenCodeCommand(name)) {
18616
+ console.log(`[Telegram] Found OpenCode command: ${name}`);
18617
+ if (!isHiddenOpenCodeCommand(name) && /^[a-z0-9_]{1,32}$/.test(name)) {
18618
+ const desc = cmd.description || "OpenCode command";
18617
18619
  opencodeCommandMenu.push({
18618
18620
  command: name,
18619
- description: cmd.description || "OpenCode command",
18621
+ description: desc.length > 256 ? desc.slice(0, 253) + "..." : desc,
18620
18622
  });
18621
18623
  }
18622
18624
  }
@@ -18974,6 +18976,63 @@ async function startTelegram(options) {
18974
18976
  let processingMsgDeleted = false;
18975
18977
  const sentToolIds = new Set();
18976
18978
  const sentReasoningIds = new Set();
18979
+ const sentSubtaskIds = new Set();
18980
+ let sawDelegation = false;
18981
+ let sentSubagentDetails = false;
18982
+ let sentSubagentResponseNotice = false;
18983
+ function extractTaskId(outputText) {
18984
+ const match = outputText.match(/task[_-]?id:\s*([\w-]+)/i);
18985
+ return match ? match[1] : null;
18986
+ }
18987
+ async function emitSubagentMessages(taskSessionId) {
18988
+ try {
18989
+ const messagesResult = await client.session.messages({
18990
+ path: { id: taskSessionId },
18991
+ });
18992
+ if (messagesResult.error || !messagesResult.data) {
18993
+ console.warn(`[Telegram] Failed to fetch subagent messages for ${taskSessionId}: ${JSON.stringify(messagesResult.error)}`);
18994
+ return;
18995
+ }
18996
+ for (const msg of messagesResult.data) {
18997
+ const info = msg.info;
18998
+ if (info?.role !== "assistant")
18999
+ continue;
19000
+ const parts = msg.parts || [];
19001
+ for (const part of parts) {
19002
+ if (part.type === "reasoning") {
19003
+ const text = part.text;
19004
+ if (!text)
19005
+ continue;
19006
+ const truncated = truncate(text.replace(/\n/g, " "), 500);
19007
+ await sendTelegramMessage(chatId, `\u{1F9E0} Thinking (agent: subagent): ${truncated}`, true);
19008
+ sentSubagentDetails = true;
19009
+ }
19010
+ if (part.type === "tool") {
19011
+ const tool = part.tool;
19012
+ const state = part.state;
19013
+ if (!state)
19014
+ continue;
19015
+ if (state.status !== "completed" && state.status !== "error")
19016
+ continue;
19017
+ const summary = summarizeTool(tool, state.input);
19018
+ let line = `\u{2699}\u{FE0F} ${summary} (agent: subagent)`;
19019
+ if (state.status === "error") {
19020
+ line += ` \u{274C}`;
19021
+ }
19022
+ await sendTelegramMessage(chatId, line, true);
19023
+ sentSubagentDetails = true;
19024
+ }
19025
+ }
19026
+ }
19027
+ if (!sentSubagentResponseNotice) {
19028
+ await sendTelegramMessage(chatId, "ℹ️ Subagent responded", true);
19029
+ sentSubagentResponseNotice = true;
19030
+ }
19031
+ }
19032
+ catch (err) {
19033
+ console.warn("[Telegram] Error fetching subagent messages:", err);
19034
+ }
19035
+ }
18977
19036
  // Buffer the latest reasoning text -- we send it when a non-reasoning part arrives
18978
19037
  // so we get the complete thinking block rather than a partial one
18979
19038
  let pendingReasoning = null;
@@ -18999,7 +19058,10 @@ async function startTelegram(options) {
18999
19058
  sentReasoningIds.add(pendingReasoning.id);
19000
19059
  await deleteProcessingMsg();
19001
19060
  const truncated = truncate(pendingReasoning.text.replace(/\n/g, " "), 500);
19002
- await sendTelegramMessage(chatId, `\u{1F9E0} Thinking: ${truncated}`, true);
19061
+ const agentLabel = pendingReasoning.agent
19062
+ ? ` (agent: ${pendingReasoning.agent})`
19063
+ : "";
19064
+ await sendTelegramMessage(chatId, `\u{1F9E0} Thinking${agentLabel}: ${truncated}`, true);
19003
19065
  }
19004
19066
  pendingReasoning = null;
19005
19067
  }
@@ -19008,14 +19070,26 @@ async function startTelegram(options) {
19008
19070
  const ev = event;
19009
19071
  if (ev.type === "message.part.updated" && ev.properties) {
19010
19072
  const part = ev.properties.part;
19011
- // Only process events for our session
19012
- if (part.sessionID !== sessionId)
19073
+ const partSessionId = part.sessionID;
19074
+ const parentSessionId = part.parentSessionID ||
19075
+ part.parentSessionId;
19076
+ const rootSessionId = part.rootSessionID ||
19077
+ part.rootSessionId;
19078
+ const isPrimarySession = partSessionId === sessionId;
19079
+ const isChildSession = parentSessionId === sessionId || rootSessionId === sessionId;
19080
+ // Only process events for our session (or subagent sessions that link back)
19081
+ if (!isPrimarySession && !isChildSession)
19013
19082
  continue;
19083
+ if (!isPrimarySession && isChildSession && verbose) {
19084
+ // Subagent events are allowed to stream in verbose mode
19085
+ }
19014
19086
  const partText = part.text;
19015
19087
  if (part.type === "reasoning" && part.id) {
19016
19088
  // Buffer reasoning -- keep updating with the latest full text
19017
19089
  if (partText) {
19018
- pendingReasoning = { id: part.id, text: partText };
19090
+ const agent = part.agent ||
19091
+ (!isPrimarySession ? "subagent" : undefined);
19092
+ pendingReasoning = { id: part.id, text: partText, agent };
19019
19093
  }
19020
19094
  }
19021
19095
  else if (part.type === "tool" && part.id) {
@@ -19030,12 +19104,74 @@ async function startTelegram(options) {
19030
19104
  sentToolIds.add(part.id);
19031
19105
  await deleteProcessingMsg();
19032
19106
  const tool = part.tool;
19033
- const summary = summarizeTool(tool, state.input);
19034
- let line = `\u{2699}\u{FE0F} ${summary}`;
19035
- if (state.status === "error") {
19036
- line += ` \u{274C}`;
19107
+ const agent = part.agent ||
19108
+ (!isPrimarySession ? "subagent" : undefined);
19109
+ if (tool === "task") {
19110
+ const input = state.input;
19111
+ const description = input?.description || input?.prompt || summarizeTool(tool, state.input);
19112
+ let line = `\u{1F9E9} Delegated`;
19113
+ if (description) {
19114
+ line += `: ${truncate(description, 120)}`;
19115
+ }
19116
+ if (agent) {
19117
+ line += ` (agent: ${agent})`;
19118
+ }
19119
+ if (state.status === "error") {
19120
+ line += ` \u{274C}`;
19121
+ }
19122
+ await sendTelegramMessage(chatId, line, true);
19123
+ sawDelegation = true;
19124
+ if (state.output && verbose) {
19125
+ const rawOutput = typeof state.output === "string"
19126
+ ? state.output
19127
+ : JSON.stringify(state.output, null, 2);
19128
+ const outputText = rawOutput.trim();
19129
+ if (outputText) {
19130
+ const taskSessionId = extractTaskId(outputText);
19131
+ if (taskSessionId) {
19132
+ await emitSubagentMessages(taskSessionId);
19133
+ }
19134
+ if (!sentSubagentDetails && !sentSubagentResponseNotice) {
19135
+ await sendTelegramMessage(chatId, "ℹ️ Subagent responded (no thought/tool details available)", true);
19136
+ sentSubagentResponseNotice = true;
19137
+ }
19138
+ }
19139
+ }
19140
+ }
19141
+ else {
19142
+ const summary = summarizeTool(tool, state.input);
19143
+ let line = `\u{2699}\u{FE0F} ${summary}`;
19144
+ if (agent) {
19145
+ line += ` (agent: ${agent})`;
19146
+ }
19147
+ if (state.status === "error") {
19148
+ line += ` \u{274C}`;
19149
+ }
19150
+ await sendTelegramMessage(chatId, line, true);
19151
+ }
19152
+ }
19153
+ }
19154
+ }
19155
+ else if (part.type === "subtask") {
19156
+ await flushReasoning();
19157
+ if (verbose) {
19158
+ const id = part.id;
19159
+ if (!id || !sentSubtaskIds.has(id)) {
19160
+ if (id)
19161
+ sentSubtaskIds.add(id);
19162
+ await deleteProcessingMsg();
19163
+ const description = part.description;
19164
+ const agent = part.agent ||
19165
+ (!isPrimarySession ? "subagent" : undefined);
19166
+ let line = "\u{1F9E9} Delegated";
19167
+ if (description) {
19168
+ line += `: ${truncate(description, 120)}`;
19169
+ }
19170
+ if (agent) {
19171
+ line += ` (agent: ${agent})`;
19037
19172
  }
19038
19173
  await sendTelegramMessage(chatId, line, true);
19174
+ sawDelegation = true;
19039
19175
  }
19040
19176
  }
19041
19177
  }
@@ -19076,6 +19212,35 @@ async function startTelegram(options) {
19076
19212
  if (!finalText) {
19077
19213
  finalText = "The agent returned an empty response.";
19078
19214
  }
19215
+ if (verbose && sawDelegation) {
19216
+ const lines = finalText.split("\n");
19217
+ let inTaskResult = false;
19218
+ const filtered = lines.filter((line) => {
19219
+ const trimmed = line.trim();
19220
+ if (/^<task_result>$/i.test(trimmed)) {
19221
+ inTaskResult = true;
19222
+ return false;
19223
+ }
19224
+ if (/^<\/task_result>$/i.test(trimmed)) {
19225
+ inTaskResult = false;
19226
+ return false;
19227
+ }
19228
+ if (inTaskResult)
19229
+ return false;
19230
+ if (/^Subagent response:/i.test(trimmed))
19231
+ return false;
19232
+ if (/^Subagent (ran|returned|reported)/i.test(trimmed))
19233
+ return false;
19234
+ if (/^Ran the subagent/i.test(trimmed))
19235
+ return false;
19236
+ return true;
19237
+ });
19238
+ const cleaned = filtered.join("\n").trim();
19239
+ if (!cleaned) {
19240
+ return;
19241
+ }
19242
+ finalText = cleaned;
19243
+ }
19079
19244
  const chunks = splitMessage(finalText, 4096);
19080
19245
  for (const chunk of chunks) {
19081
19246
  await sendTelegramMessage(chatId, chunk);
@@ -19941,9 +20106,10 @@ async function startTelegram(options) {
19941
20106
  });
19942
20107
  if (options.launch !== false) {
19943
20108
  try {
19944
- // Start the bot
20109
+ // Start the bot — launch() returns a promise that resolves only
20110
+ // when polling stops, so we log before awaiting it.
20111
+ console.log("[Telegram] Bot is running (long-polling started)");
19945
20112
  await bot.launch();
19946
- console.log("[Telegram] Bot is running");
19947
20113
  // Enable graceful stop
19948
20114
  process.once("SIGINT", () => bot.stop("SIGINT"));
19949
20115
  process.once("SIGTERM", () => bot.stop("SIGTERM"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-telegram-bot",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "description": "Telegram bot that forwards messages to an OpenCode agent",
6
6
  "main": "./dist/index.js",