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.
- package/README.md +12 -1
- package/dist/index.js +178 -12
- 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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
19012
|
-
|
|
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
|
-
|
|
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
|
|
19034
|
-
|
|
19035
|
-
if (
|
|
19036
|
-
|
|
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"));
|