opencode-telegram-bot 1.0.2 → 1.0.3
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 +22 -0
- package/dist/index.js +233 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,8 +105,30 @@ These are handled directly by the Telegram bot:
|
|
|
105
105
|
| `/delete <id>` | Delete a session from the server |
|
|
106
106
|
| `/export` | Export the current session as a markdown file |
|
|
107
107
|
| `/export full` | Export with all details (thinking, costs, steps) |
|
|
108
|
+
| `/verbose` | Toggle verbose mode (show thinking and tool calls in chat) |
|
|
109
|
+
| `/verbose on|off` | Explicitly enable/disable verbose mode |
|
|
108
110
|
| `/help` | Show available commands |
|
|
109
111
|
|
|
112
|
+
### Verbose Mode
|
|
113
|
+
|
|
114
|
+
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:
|
|
115
|
+
|
|
116
|
+
- **Thinking/reasoning** -- shown as plain text with a 🧠 prefix, truncated to 500 characters
|
|
117
|
+
- **Tool calls** -- shown as a compact one-line summary (e.g. `> read -- src/app.ts`)
|
|
118
|
+
|
|
119
|
+
Verbose mode is per-chat and persists across bot restarts. Use `/verbose` again to turn it off.
|
|
120
|
+
|
|
121
|
+
Example with verbose mode on:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
🧠 Thinking: Let me analyze the authentication flow and check for potential issues...
|
|
125
|
+
|
|
126
|
+
⚙️ grep -- pattern: "authenticate" in src/
|
|
127
|
+
⚙️ read -- src/auth/handler.ts
|
|
128
|
+
|
|
129
|
+
Here's what I found in the auth module...
|
|
130
|
+
```
|
|
131
|
+
|
|
110
132
|
### Session Export
|
|
111
133
|
|
|
112
134
|
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.
|
package/dist/index.js
CHANGED
|
@@ -18588,12 +18588,19 @@ async function startTelegram(options) {
|
|
|
18588
18588
|
const client = client_createOpencodeClient({ baseUrl: url });
|
|
18589
18589
|
// Verify connection to the OpenCode server and fetch available commands
|
|
18590
18590
|
const opencodeCommands = new Set();
|
|
18591
|
+
let projectDirectory = "";
|
|
18591
18592
|
try {
|
|
18592
18593
|
const sessions = await client.session.list();
|
|
18593
18594
|
if (sessions.error) {
|
|
18594
18595
|
throw new Error(`Server returned error: ${JSON.stringify(sessions.error)}`);
|
|
18595
18596
|
}
|
|
18596
18597
|
console.log(`[Telegram] Connected to OpenCode server at ${url}`);
|
|
18598
|
+
// Fetch the project directory (needed for SSE event subscription)
|
|
18599
|
+
const pathResult = await client.path.get();
|
|
18600
|
+
if (pathResult.data?.directory) {
|
|
18601
|
+
projectDirectory = pathResult.data.directory;
|
|
18602
|
+
console.log(`[Telegram] Project directory: ${projectDirectory}`);
|
|
18603
|
+
}
|
|
18597
18604
|
// Fetch available OpenCode commands
|
|
18598
18605
|
const cmds = await client.command.list();
|
|
18599
18606
|
if (cmds.data) {
|
|
@@ -18608,12 +18615,14 @@ async function startTelegram(options) {
|
|
|
18608
18615
|
}
|
|
18609
18616
|
// Telegram-only commands that should not be forwarded to OpenCode
|
|
18610
18617
|
const telegramCommands = new Set([
|
|
18611
|
-
"start", "help", "new", "sessions", "switch", "title", "delete", "export",
|
|
18618
|
+
"start", "help", "new", "sessions", "switch", "title", "delete", "export", "verbose",
|
|
18612
18619
|
]);
|
|
18613
18620
|
// Map of chatId -> sessionId for the active session per chat
|
|
18614
18621
|
const chatSessions = new Map();
|
|
18615
18622
|
// Set of all session IDs ever created/used by this bot (for filtering)
|
|
18616
18623
|
const knownSessionIds = new Set();
|
|
18624
|
+
// Set of chatIds with verbose mode enabled
|
|
18625
|
+
const chatVerboseMode = new Set();
|
|
18617
18626
|
/**
|
|
18618
18627
|
* Save the chat-to-session mapping and known session IDs to disk.
|
|
18619
18628
|
*/
|
|
@@ -18622,6 +18631,7 @@ async function startTelegram(options) {
|
|
|
18622
18631
|
const data = {
|
|
18623
18632
|
active: Object.fromEntries(chatSessions),
|
|
18624
18633
|
known: [...knownSessionIds],
|
|
18634
|
+
verbose: [...chatVerboseMode],
|
|
18625
18635
|
};
|
|
18626
18636
|
(0,external_node_fs_.writeFileSync)(SESSIONS_FILE, JSON.stringify(data, null, 2));
|
|
18627
18637
|
}
|
|
@@ -18637,14 +18647,16 @@ async function startTelegram(options) {
|
|
|
18637
18647
|
// Load from file (supports both old flat format and new {active, known} format)
|
|
18638
18648
|
let storedActive = {};
|
|
18639
18649
|
let storedKnown = [];
|
|
18650
|
+
let storedVerbose = [];
|
|
18640
18651
|
if ((0,external_node_fs_.existsSync)(SESSIONS_FILE)) {
|
|
18641
18652
|
try {
|
|
18642
18653
|
const raw = (0,external_node_fs_.readFileSync)(SESSIONS_FILE, "utf-8");
|
|
18643
18654
|
const parsed = JSON.parse(raw);
|
|
18644
18655
|
if (parsed.active && typeof parsed.active === "object") {
|
|
18645
|
-
// New format: { active: {...}, known: [...] }
|
|
18656
|
+
// New format: { active: {...}, known: [...], verbose: [...] }
|
|
18646
18657
|
storedActive = parsed.active;
|
|
18647
18658
|
storedKnown = parsed.known || [];
|
|
18659
|
+
storedVerbose = parsed.verbose || [];
|
|
18648
18660
|
}
|
|
18649
18661
|
else {
|
|
18650
18662
|
// Old format: flat { chatId: sessionId }
|
|
@@ -18656,6 +18668,13 @@ async function startTelegram(options) {
|
|
|
18656
18668
|
console.warn("[Telegram] Failed to parse sessions file:", err);
|
|
18657
18669
|
}
|
|
18658
18670
|
}
|
|
18671
|
+
// Restore verbose mode preferences
|
|
18672
|
+
for (const chatId of storedVerbose) {
|
|
18673
|
+
chatVerboseMode.add(chatId);
|
|
18674
|
+
}
|
|
18675
|
+
if (storedVerbose.length > 0) {
|
|
18676
|
+
console.log(`[Telegram] Restored verbose mode for ${storedVerbose.length} chat(s)`);
|
|
18677
|
+
}
|
|
18659
18678
|
// Fetch all server sessions once for validation and fallback matching
|
|
18660
18679
|
let serverSessions = [];
|
|
18661
18680
|
try {
|
|
@@ -18758,6 +18777,52 @@ async function startTelegram(options) {
|
|
|
18758
18777
|
console.warn("[Telegram] Failed to auto-title session:", err);
|
|
18759
18778
|
}
|
|
18760
18779
|
}
|
|
18780
|
+
/**
|
|
18781
|
+
* Build a one-line summary for a tool call, picking the most meaningful input field.
|
|
18782
|
+
*/
|
|
18783
|
+
function summarizeTool(tool, input) {
|
|
18784
|
+
if (!input)
|
|
18785
|
+
return tool;
|
|
18786
|
+
// Pick the most descriptive field based on common tool patterns
|
|
18787
|
+
const summaryField = input.filePath || input.path || input.command || input.pattern ||
|
|
18788
|
+
input.url || input.query || input.content || input.prompt ||
|
|
18789
|
+
input.description || input.name;
|
|
18790
|
+
if (summaryField && typeof summaryField === "string") {
|
|
18791
|
+
return `${tool} -- ${truncate(summaryField, 80)}`;
|
|
18792
|
+
}
|
|
18793
|
+
// For tools with a glob/include pattern
|
|
18794
|
+
if (input.include && typeof input.include === "string") {
|
|
18795
|
+
return `${tool} -- ${truncate(input.include, 80)}`;
|
|
18796
|
+
}
|
|
18797
|
+
return tool;
|
|
18798
|
+
}
|
|
18799
|
+
/**
|
|
18800
|
+
* Send a message to Telegram, with Markdown fallback.
|
|
18801
|
+
* When disableLinkPreview is true, link previews are suppressed (useful for verbose messages).
|
|
18802
|
+
*/
|
|
18803
|
+
async function sendTelegramMessage(chatId, text, disableLinkPreview = false) {
|
|
18804
|
+
const options = {
|
|
18805
|
+
parse_mode: "Markdown",
|
|
18806
|
+
};
|
|
18807
|
+
if (disableLinkPreview) {
|
|
18808
|
+
options.link_preview_options = { is_disabled: true };
|
|
18809
|
+
}
|
|
18810
|
+
try {
|
|
18811
|
+
await bot.telegram.sendMessage(chatId, text, options);
|
|
18812
|
+
}
|
|
18813
|
+
catch {
|
|
18814
|
+
try {
|
|
18815
|
+
const fallbackOptions = {};
|
|
18816
|
+
if (disableLinkPreview) {
|
|
18817
|
+
fallbackOptions.link_preview_options = { is_disabled: true };
|
|
18818
|
+
}
|
|
18819
|
+
await bot.telegram.sendMessage(chatId, text, fallbackOptions);
|
|
18820
|
+
}
|
|
18821
|
+
catch (fallbackErr) {
|
|
18822
|
+
console.error("[Telegram] Fallback error:", fallbackErr);
|
|
18823
|
+
}
|
|
18824
|
+
}
|
|
18825
|
+
}
|
|
18761
18826
|
/**
|
|
18762
18827
|
* Extract text from response parts and send to Telegram chat.
|
|
18763
18828
|
*/
|
|
@@ -18781,20 +18846,145 @@ async function startTelegram(options) {
|
|
|
18781
18846
|
// Send the response, splitting if needed (Telegram has a 4096 char limit)
|
|
18782
18847
|
const chunks = splitMessage(responseText, 4096);
|
|
18783
18848
|
for (const chunk of chunks) {
|
|
18784
|
-
|
|
18785
|
-
|
|
18786
|
-
|
|
18787
|
-
|
|
18788
|
-
|
|
18789
|
-
|
|
18849
|
+
await sendTelegramMessage(chatId, chunk);
|
|
18850
|
+
}
|
|
18851
|
+
}
|
|
18852
|
+
/**
|
|
18853
|
+
* Send a prompt with streaming: fires the prompt asynchronously, then listens
|
|
18854
|
+
* to SSE events to stream thinking and tool call updates to Telegram in real-time.
|
|
18855
|
+
* The final text response is sent when the session goes idle.
|
|
18856
|
+
*/
|
|
18857
|
+
async function sendPromptStreaming(chatId, sessionId, promptBody, processingMsgId, verbose = false) {
|
|
18858
|
+
// Subscribe to SSE events before sending the prompt so we don't miss anything
|
|
18859
|
+
const abortController = new AbortController();
|
|
18860
|
+
// Subscribe to SSE events before sending the prompt so we don't miss anything
|
|
18861
|
+
const subscribeOptions = {
|
|
18862
|
+
signal: abortController.signal,
|
|
18863
|
+
};
|
|
18864
|
+
if (projectDirectory) {
|
|
18865
|
+
subscribeOptions.query = { directory: projectDirectory };
|
|
18866
|
+
}
|
|
18867
|
+
const { stream } = await client.event.subscribe(subscribeOptions);
|
|
18868
|
+
// Fire the prompt asynchronously (returns immediately)
|
|
18869
|
+
const asyncResult = await client.session.promptAsync({
|
|
18870
|
+
path: { id: sessionId },
|
|
18871
|
+
body: promptBody,
|
|
18872
|
+
});
|
|
18873
|
+
if (asyncResult.error) {
|
|
18874
|
+
abortController.abort();
|
|
18875
|
+
throw new Error(`Prompt failed: ${JSON.stringify(asyncResult.error)}`);
|
|
18876
|
+
}
|
|
18877
|
+
// Track state as events stream in
|
|
18878
|
+
let finalText = "";
|
|
18879
|
+
let processingMsgDeleted = false;
|
|
18880
|
+
const sentToolIds = new Set();
|
|
18881
|
+
const sentReasoningIds = new Set();
|
|
18882
|
+
// Buffer the latest reasoning text -- we send it when a non-reasoning part arrives
|
|
18883
|
+
// so we get the complete thinking block rather than a partial one
|
|
18884
|
+
let pendingReasoning = null;
|
|
18885
|
+
/**
|
|
18886
|
+
* Delete the "processing" message once, right before sending the first real output.
|
|
18887
|
+
*/
|
|
18888
|
+
async function deleteProcessingMsg() {
|
|
18889
|
+
if (!processingMsgDeleted) {
|
|
18890
|
+
processingMsgDeleted = true;
|
|
18790
18891
|
try {
|
|
18791
|
-
await bot.telegram.
|
|
18892
|
+
await bot.telegram.deleteMessage(chatId, processingMsgId);
|
|
18893
|
+
}
|
|
18894
|
+
catch {
|
|
18895
|
+
// Ignore
|
|
18896
|
+
}
|
|
18897
|
+
}
|
|
18898
|
+
}
|
|
18899
|
+
/**
|
|
18900
|
+
* Flush any buffered reasoning to Telegram as a spoiler message.
|
|
18901
|
+
*/
|
|
18902
|
+
async function flushReasoning() {
|
|
18903
|
+
if (verbose && pendingReasoning && !sentReasoningIds.has(pendingReasoning.id)) {
|
|
18904
|
+
sentReasoningIds.add(pendingReasoning.id);
|
|
18905
|
+
await deleteProcessingMsg();
|
|
18906
|
+
const truncated = truncate(pendingReasoning.text.replace(/\n/g, " "), 500);
|
|
18907
|
+
await sendTelegramMessage(chatId, `\u{1F9E0} Thinking: ${truncated}`, true);
|
|
18908
|
+
}
|
|
18909
|
+
pendingReasoning = null;
|
|
18910
|
+
}
|
|
18911
|
+
try {
|
|
18912
|
+
for await (const event of stream) {
|
|
18913
|
+
const ev = event;
|
|
18914
|
+
if (ev.type === "message.part.updated" && ev.properties) {
|
|
18915
|
+
const part = ev.properties.part;
|
|
18916
|
+
// Only process events for our session
|
|
18917
|
+
if (part.sessionID !== sessionId)
|
|
18918
|
+
continue;
|
|
18919
|
+
const partText = part.text;
|
|
18920
|
+
if (part.type === "reasoning" && part.id) {
|
|
18921
|
+
// Buffer reasoning -- keep updating with the latest full text
|
|
18922
|
+
if (partText) {
|
|
18923
|
+
pendingReasoning = { id: part.id, text: partText };
|
|
18924
|
+
}
|
|
18925
|
+
}
|
|
18926
|
+
else if (part.type === "tool" && part.id) {
|
|
18927
|
+
// A tool part arrived -- flush any pending reasoning first
|
|
18928
|
+
await flushReasoning();
|
|
18929
|
+
if (verbose) {
|
|
18930
|
+
const state = part.state;
|
|
18931
|
+
// Only send tool messages once they have a completed/error status
|
|
18932
|
+
if (state &&
|
|
18933
|
+
(state.status === "completed" || state.status === "error") &&
|
|
18934
|
+
!sentToolIds.has(part.id)) {
|
|
18935
|
+
sentToolIds.add(part.id);
|
|
18936
|
+
await deleteProcessingMsg();
|
|
18937
|
+
const tool = part.tool;
|
|
18938
|
+
const summary = summarizeTool(tool, state.input);
|
|
18939
|
+
let line = `\u{2699}\u{FE0F} ${summary}`;
|
|
18940
|
+
if (state.status === "error") {
|
|
18941
|
+
line += ` \u{274C}`;
|
|
18942
|
+
}
|
|
18943
|
+
await sendTelegramMessage(chatId, line, true);
|
|
18944
|
+
}
|
|
18945
|
+
}
|
|
18946
|
+
}
|
|
18947
|
+
else if (part.type === "text") {
|
|
18948
|
+
// A text part arrived -- flush any pending reasoning first
|
|
18949
|
+
await flushReasoning();
|
|
18950
|
+
// Accumulate text for the final response (text parts carry the full text, not deltas)
|
|
18951
|
+
const text = part.text;
|
|
18952
|
+
if (text) {
|
|
18953
|
+
finalText = text;
|
|
18954
|
+
}
|
|
18955
|
+
}
|
|
18792
18956
|
}
|
|
18793
|
-
|
|
18794
|
-
|
|
18957
|
+
else if (ev.type === "session.idle" && ev.properties) {
|
|
18958
|
+
const idleSessionId = ev.properties.sessionID;
|
|
18959
|
+
if (idleSessionId === sessionId) {
|
|
18960
|
+
// Flush any remaining reasoning before finishing
|
|
18961
|
+
await flushReasoning();
|
|
18962
|
+
break;
|
|
18963
|
+
}
|
|
18964
|
+
}
|
|
18965
|
+
else if (ev.type === "session.error" && ev.properties) {
|
|
18966
|
+
const errorSessionId = ev.properties.sessionID;
|
|
18967
|
+
if (errorSessionId === sessionId) {
|
|
18968
|
+
const error = ev.properties.error;
|
|
18969
|
+
const errorMsg = error?.data?.message || "Unknown error";
|
|
18970
|
+
throw new Error(`Session error: ${errorMsg}`);
|
|
18971
|
+
}
|
|
18795
18972
|
}
|
|
18796
18973
|
}
|
|
18797
18974
|
}
|
|
18975
|
+
finally {
|
|
18976
|
+
abortController.abort();
|
|
18977
|
+
}
|
|
18978
|
+
// Delete the processing message if it hasn't been deleted yet (non-verbose mode)
|
|
18979
|
+
await deleteProcessingMsg();
|
|
18980
|
+
// Send the final text response
|
|
18981
|
+
if (!finalText) {
|
|
18982
|
+
finalText = "The agent returned an empty response.";
|
|
18983
|
+
}
|
|
18984
|
+
const chunks = splitMessage(finalText, 4096);
|
|
18985
|
+
for (const chunk of chunks) {
|
|
18986
|
+
await sendTelegramMessage(chatId, chunk);
|
|
18987
|
+
}
|
|
18798
18988
|
}
|
|
18799
18989
|
/**
|
|
18800
18990
|
* Handle session errors - clear invalid sessions.
|
|
@@ -18847,6 +19037,8 @@ async function startTelegram(options) {
|
|
|
18847
19037
|
"/delete <number> - Delete a session\n" +
|
|
18848
19038
|
"/export - Export the current session as a markdown file\n" +
|
|
18849
19039
|
"/export full - Export with all details (thinking, costs, steps)\n" +
|
|
19040
|
+
"/verbose - Toggle verbose mode (show thinking and tool calls)\n" +
|
|
19041
|
+
"/verbose on|off - Set verbose mode explicitly\n" +
|
|
18850
19042
|
"/help - Show this help message\n";
|
|
18851
19043
|
if (opencodeCommands.size > 0) {
|
|
18852
19044
|
msg +=
|
|
@@ -18866,6 +19058,8 @@ async function startTelegram(options) {
|
|
|
18866
19058
|
"/delete <number> - Delete a session\n" +
|
|
18867
19059
|
"/export - Export the current session as a markdown file\n" +
|
|
18868
19060
|
"/export full - Export with all details (thinking, costs, steps)\n" +
|
|
19061
|
+
"/verbose - Toggle verbose mode (show thinking and tool calls)\n" +
|
|
19062
|
+
"/verbose on|off - Set verbose mode explicitly\n" +
|
|
18869
19063
|
"/help - Show this help message\n";
|
|
18870
19064
|
if (opencodeCommands.size > 0) {
|
|
18871
19065
|
msg +=
|
|
@@ -19025,6 +19219,32 @@ async function startTelegram(options) {
|
|
|
19025
19219
|
await ctx.reply("Failed to delete session.");
|
|
19026
19220
|
}
|
|
19027
19221
|
});
|
|
19222
|
+
// Handle /verbose command - toggle verbose mode for this chat
|
|
19223
|
+
// Usage: /verbose, /verbose on, /verbose off
|
|
19224
|
+
bot.command("verbose", (ctx) => {
|
|
19225
|
+
const chatId = ctx.chat.id.toString();
|
|
19226
|
+
const args = ctx.message.text.replace(/^\/verbose\s*/, "").trim().toLowerCase();
|
|
19227
|
+
if (args === "on") {
|
|
19228
|
+
chatVerboseMode.add(chatId);
|
|
19229
|
+
}
|
|
19230
|
+
else if (args === "off") {
|
|
19231
|
+
chatVerboseMode.delete(chatId);
|
|
19232
|
+
}
|
|
19233
|
+
else {
|
|
19234
|
+
if (chatVerboseMode.has(chatId)) {
|
|
19235
|
+
chatVerboseMode.delete(chatId);
|
|
19236
|
+
}
|
|
19237
|
+
else {
|
|
19238
|
+
chatVerboseMode.add(chatId);
|
|
19239
|
+
}
|
|
19240
|
+
}
|
|
19241
|
+
saveSessions();
|
|
19242
|
+
const enabled = chatVerboseMode.has(chatId);
|
|
19243
|
+
console.log(`[Telegram] Verbose mode ${enabled ? "enabled" : "disabled"} for chat ${chatId}`);
|
|
19244
|
+
ctx.reply(enabled
|
|
19245
|
+
? "Verbose mode enabled. Responses will include thinking and tool calls."
|
|
19246
|
+
: "Verbose mode disabled. Responses will only show the assistant's text.");
|
|
19247
|
+
});
|
|
19028
19248
|
/**
|
|
19029
19249
|
* Render a message part to markdown.
|
|
19030
19250
|
* In default mode: only text and tool calls (name + input/output).
|
|
@@ -19316,15 +19536,8 @@ async function startTelegram(options) {
|
|
|
19316
19536
|
const modelID = modelParts.join("/");
|
|
19317
19537
|
promptBody.model = { providerID, modelID };
|
|
19318
19538
|
}
|
|
19319
|
-
const
|
|
19320
|
-
|
|
19321
|
-
body: promptBody,
|
|
19322
|
-
});
|
|
19323
|
-
if (result.error) {
|
|
19324
|
-
throw new Error(`Prompt failed: ${JSON.stringify(result.error)}`);
|
|
19325
|
-
}
|
|
19326
|
-
const parts = (result.data?.parts || []);
|
|
19327
|
-
await sendResponseToChat(ctx.chat.id, parts, processingMsg.message_id);
|
|
19539
|
+
const verbose = chatVerboseMode.has(chatId);
|
|
19540
|
+
await sendPromptStreaming(ctx.chat.id, sessionId, promptBody, processingMsg.message_id, verbose);
|
|
19328
19541
|
}
|
|
19329
19542
|
catch (err) {
|
|
19330
19543
|
console.error("[Telegram] Error processing message:", err);
|