opencode-telegram-bot 1.0.1 → 1.0.2
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 +13 -0
- package/dist/index.js +234 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,8 +103,21 @@ These are handled directly by the Telegram bot:
|
|
|
103
103
|
| `/switch <id>` | Switch to a different session (prefix match supported) |
|
|
104
104
|
| `/title <text>` | Rename the current session |
|
|
105
105
|
| `/delete <id>` | Delete a session from the server |
|
|
106
|
+
| `/export` | Export the current session as a markdown file |
|
|
107
|
+
| `/export full` | Export with all details (thinking, costs, steps) |
|
|
106
108
|
| `/help` | Show available commands |
|
|
107
109
|
|
|
110
|
+
### Session Export
|
|
111
|
+
|
|
112
|
+
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.
|
|
113
|
+
|
|
114
|
+
Two modes are available:
|
|
115
|
+
|
|
116
|
+
- **`/export`** -- Default. Includes user messages, assistant text, and tool calls (name, input, output).
|
|
117
|
+
- **`/export full`** (also accepts `detailed` or `all`) -- Includes everything from the default mode plus reasoning/thinking blocks, step boundaries with token counts, costs, subtasks, retries, and compaction markers.
|
|
118
|
+
|
|
119
|
+
The exported file is named `session-<id>.md` (or `session-<id>-detailed.md` for the full export).
|
|
120
|
+
|
|
108
121
|
### OpenCode Commands
|
|
109
122
|
|
|
110
123
|
Any `/` command that isn't a bot command is automatically forwarded to the OpenCode server via its command system. The bot fetches the list of available commands on startup and validates them.
|
package/dist/index.js
CHANGED
|
@@ -18608,7 +18608,7 @@ async function startTelegram(options) {
|
|
|
18608
18608
|
}
|
|
18609
18609
|
// Telegram-only commands that should not be forwarded to OpenCode
|
|
18610
18610
|
const telegramCommands = new Set([
|
|
18611
|
-
"start", "help", "new", "sessions", "switch", "title", "delete",
|
|
18611
|
+
"start", "help", "new", "sessions", "switch", "title", "delete", "export",
|
|
18612
18612
|
]);
|
|
18613
18613
|
// Map of chatId -> sessionId for the active session per chat
|
|
18614
18614
|
const chatSessions = new Map();
|
|
@@ -18845,6 +18845,8 @@ async function startTelegram(options) {
|
|
|
18845
18845
|
"/switch <number> - Switch to a different session\n" +
|
|
18846
18846
|
"/title <text> - Rename the current session\n" +
|
|
18847
18847
|
"/delete <number> - Delete a session\n" +
|
|
18848
|
+
"/export - Export the current session as a markdown file\n" +
|
|
18849
|
+
"/export full - Export with all details (thinking, costs, steps)\n" +
|
|
18848
18850
|
"/help - Show this help message\n";
|
|
18849
18851
|
if (opencodeCommands.size > 0) {
|
|
18850
18852
|
msg +=
|
|
@@ -18862,6 +18864,8 @@ async function startTelegram(options) {
|
|
|
18862
18864
|
"/switch <number> - Switch to a different session\n" +
|
|
18863
18865
|
"/title <text> - Rename the current session\n" +
|
|
18864
18866
|
"/delete <number> - Delete a session\n" +
|
|
18867
|
+
"/export - Export the current session as a markdown file\n" +
|
|
18868
|
+
"/export full - Export with all details (thinking, costs, steps)\n" +
|
|
18865
18869
|
"/help - Show this help message\n";
|
|
18866
18870
|
if (opencodeCommands.size > 0) {
|
|
18867
18871
|
msg +=
|
|
@@ -19021,6 +19025,234 @@ async function startTelegram(options) {
|
|
|
19021
19025
|
await ctx.reply("Failed to delete session.");
|
|
19022
19026
|
}
|
|
19023
19027
|
});
|
|
19028
|
+
/**
|
|
19029
|
+
* Render a message part to markdown.
|
|
19030
|
+
* In default mode: only text and tool calls (name + input/output).
|
|
19031
|
+
* In detailed mode: also includes reasoning, step info, subtasks, costs, etc.
|
|
19032
|
+
*/
|
|
19033
|
+
function renderPart(part, detailed) {
|
|
19034
|
+
let md = "";
|
|
19035
|
+
switch (part.type) {
|
|
19036
|
+
case "text": {
|
|
19037
|
+
const text = part.text;
|
|
19038
|
+
if (text) {
|
|
19039
|
+
md += `${text}\n\n`;
|
|
19040
|
+
}
|
|
19041
|
+
break;
|
|
19042
|
+
}
|
|
19043
|
+
case "tool": {
|
|
19044
|
+
const tool = part.tool;
|
|
19045
|
+
const state = part.state;
|
|
19046
|
+
md += `**Tool: ${tool}**\n\n`;
|
|
19047
|
+
if (state.input) {
|
|
19048
|
+
md += `**Input:**\n\`\`\`json\n${JSON.stringify(state.input, null, 2)}\n\`\`\`\n\n`;
|
|
19049
|
+
}
|
|
19050
|
+
if (state.output) {
|
|
19051
|
+
md += `**Output:**\n\`\`\`\n${state.output}\n\`\`\`\n\n`;
|
|
19052
|
+
}
|
|
19053
|
+
if (state.error) {
|
|
19054
|
+
md += `**Error:**\n\`\`\`\n${state.error}\n\`\`\`\n\n`;
|
|
19055
|
+
}
|
|
19056
|
+
if (detailed && state.time) {
|
|
19057
|
+
const duration = ((state.time.end - state.time.start) / 1000).toFixed(1);
|
|
19058
|
+
md += `*Status: ${state.status} (${duration}s)*\n\n`;
|
|
19059
|
+
}
|
|
19060
|
+
break;
|
|
19061
|
+
}
|
|
19062
|
+
case "reasoning": {
|
|
19063
|
+
if (!detailed)
|
|
19064
|
+
break;
|
|
19065
|
+
const text = part.text;
|
|
19066
|
+
if (text) {
|
|
19067
|
+
md += `<details>\n<summary>Thinking</summary>\n\n${text}\n\n</details>\n\n`;
|
|
19068
|
+
}
|
|
19069
|
+
break;
|
|
19070
|
+
}
|
|
19071
|
+
case "step-start": {
|
|
19072
|
+
if (!detailed)
|
|
19073
|
+
break;
|
|
19074
|
+
md += `---\n*Step started*\n\n`;
|
|
19075
|
+
break;
|
|
19076
|
+
}
|
|
19077
|
+
case "step-finish": {
|
|
19078
|
+
if (!detailed)
|
|
19079
|
+
break;
|
|
19080
|
+
const reason = part.reason;
|
|
19081
|
+
const cost = part.cost;
|
|
19082
|
+
const tokens = part.tokens;
|
|
19083
|
+
let info = `*Step finished`;
|
|
19084
|
+
if (reason)
|
|
19085
|
+
info += ` (${reason})`;
|
|
19086
|
+
info += `*`;
|
|
19087
|
+
if (tokens) {
|
|
19088
|
+
info += `\n*Tokens: ${tokens.input} in / ${tokens.output} out`;
|
|
19089
|
+
if (tokens.reasoning > 0)
|
|
19090
|
+
info += ` / ${tokens.reasoning} reasoning`;
|
|
19091
|
+
if (tokens.cache.read > 0 || tokens.cache.write > 0) {
|
|
19092
|
+
info += ` (cache: ${tokens.cache.read} read, ${tokens.cache.write} write)`;
|
|
19093
|
+
}
|
|
19094
|
+
info += `*`;
|
|
19095
|
+
}
|
|
19096
|
+
if (cost !== undefined && cost > 0) {
|
|
19097
|
+
info += `\n*Cost: $${cost.toFixed(4)}*`;
|
|
19098
|
+
}
|
|
19099
|
+
md += `${info}\n\n---\n\n`;
|
|
19100
|
+
break;
|
|
19101
|
+
}
|
|
19102
|
+
case "subtask": {
|
|
19103
|
+
if (!detailed)
|
|
19104
|
+
break;
|
|
19105
|
+
const description = part.description;
|
|
19106
|
+
const agent = part.agent;
|
|
19107
|
+
const prompt = part.prompt;
|
|
19108
|
+
md += `**Subtask${agent ? ` (${agent})` : ""}**`;
|
|
19109
|
+
if (description)
|
|
19110
|
+
md += `: ${description}`;
|
|
19111
|
+
md += `\n\n`;
|
|
19112
|
+
if (prompt) {
|
|
19113
|
+
md += `> ${prompt.replace(/\n/g, "\n> ")}\n\n`;
|
|
19114
|
+
}
|
|
19115
|
+
break;
|
|
19116
|
+
}
|
|
19117
|
+
case "agent": {
|
|
19118
|
+
if (!detailed)
|
|
19119
|
+
break;
|
|
19120
|
+
const name = part.name;
|
|
19121
|
+
if (name) {
|
|
19122
|
+
md += `*Agent: ${name}*\n\n`;
|
|
19123
|
+
}
|
|
19124
|
+
break;
|
|
19125
|
+
}
|
|
19126
|
+
case "retry": {
|
|
19127
|
+
if (!detailed)
|
|
19128
|
+
break;
|
|
19129
|
+
const attempt = part.attempt;
|
|
19130
|
+
const error = part.error;
|
|
19131
|
+
md += `**Retry (attempt ${attempt || "?"})**`;
|
|
19132
|
+
if (error?.data?.message) {
|
|
19133
|
+
md += `\n\`\`\`\n${error.data.message}\n\`\`\``;
|
|
19134
|
+
}
|
|
19135
|
+
md += `\n\n`;
|
|
19136
|
+
break;
|
|
19137
|
+
}
|
|
19138
|
+
case "compaction": {
|
|
19139
|
+
if (!detailed)
|
|
19140
|
+
break;
|
|
19141
|
+
const auto = part.auto;
|
|
19142
|
+
md += `*Context compacted${auto ? " (auto)" : ""}*\n\n`;
|
|
19143
|
+
break;
|
|
19144
|
+
}
|
|
19145
|
+
default:
|
|
19146
|
+
// snapshot, patch, file, etc. - skip in both modes
|
|
19147
|
+
break;
|
|
19148
|
+
}
|
|
19149
|
+
return md;
|
|
19150
|
+
}
|
|
19151
|
+
// Handle /export command - export the current session to the opencode project directory
|
|
19152
|
+
// Usage: /export - default (text + tool calls only)
|
|
19153
|
+
// /export full - detailed (includes thinking, steps, costs, subtasks, etc.)
|
|
19154
|
+
bot.command("export", async (ctx) => {
|
|
19155
|
+
const chatId = ctx.chat.id.toString();
|
|
19156
|
+
const sessionId = chatSessions.get(chatId);
|
|
19157
|
+
if (!sessionId) {
|
|
19158
|
+
await ctx.reply("No active session. Send a message first to create one.");
|
|
19159
|
+
return;
|
|
19160
|
+
}
|
|
19161
|
+
const args = ctx.message.text.replace(/^\/export\s*/, "").trim().toLowerCase();
|
|
19162
|
+
const detailed = args === "full" || args === "detailed" || args === "all";
|
|
19163
|
+
try {
|
|
19164
|
+
const modeLabel = detailed ? "detailed" : "summary";
|
|
19165
|
+
const processingMsg = await ctx.reply(`Exporting session (${modeLabel})...`);
|
|
19166
|
+
// Fetch session info and messages in parallel
|
|
19167
|
+
const [sessionResult, messagesResult, pathResult] = await Promise.all([
|
|
19168
|
+
client.session.get({ path: { id: sessionId } }),
|
|
19169
|
+
client.session.messages({ path: { id: sessionId } }),
|
|
19170
|
+
client.path.get(),
|
|
19171
|
+
]);
|
|
19172
|
+
if (sessionResult.error || !sessionResult.data) {
|
|
19173
|
+
throw new Error(`Failed to get session: ${JSON.stringify(sessionResult.error)}`);
|
|
19174
|
+
}
|
|
19175
|
+
if (messagesResult.error || !messagesResult.data) {
|
|
19176
|
+
throw new Error(`Failed to get messages: ${JSON.stringify(messagesResult.error)}`);
|
|
19177
|
+
}
|
|
19178
|
+
const session = sessionResult.data;
|
|
19179
|
+
const messages = messagesResult.data;
|
|
19180
|
+
// Build the markdown export
|
|
19181
|
+
let md = `# ${session.title}\n\n`;
|
|
19182
|
+
md += `**Session ID:** ${session.id}\n`;
|
|
19183
|
+
md += `**Created:** ${new Date(session.time.created * 1000).toLocaleString()}\n`;
|
|
19184
|
+
md += `**Updated:** ${new Date(session.time.updated * 1000).toLocaleString()}\n`;
|
|
19185
|
+
if (detailed) {
|
|
19186
|
+
md += `**Export mode:** Detailed\n`;
|
|
19187
|
+
}
|
|
19188
|
+
md += `\n---\n\n`;
|
|
19189
|
+
for (const msg of messages) {
|
|
19190
|
+
const info = msg.info;
|
|
19191
|
+
const parts = msg.parts || [];
|
|
19192
|
+
if (info.role === "user") {
|
|
19193
|
+
md += `## User\n\n`;
|
|
19194
|
+
for (const part of parts) {
|
|
19195
|
+
md += renderPart(part, detailed);
|
|
19196
|
+
}
|
|
19197
|
+
md += `---\n\n`;
|
|
19198
|
+
}
|
|
19199
|
+
else if (info.role === "assistant") {
|
|
19200
|
+
const assistant = info;
|
|
19201
|
+
const duration = assistant.time.completed
|
|
19202
|
+
? ((assistant.time.completed - assistant.time.created) / 1000).toFixed(1) + "s"
|
|
19203
|
+
: "";
|
|
19204
|
+
const modelLabel = assistant.modelID || "unknown";
|
|
19205
|
+
const modeLabel = assistant.mode || "";
|
|
19206
|
+
const header = [modeLabel, modelLabel, duration].filter(Boolean).join(" · ");
|
|
19207
|
+
md += `## Assistant (${header})\n\n`;
|
|
19208
|
+
if (detailed && assistant.tokens) {
|
|
19209
|
+
const t = assistant.tokens;
|
|
19210
|
+
md += `*Tokens: ${t.input} in / ${t.output} out`;
|
|
19211
|
+
if (t.reasoning > 0)
|
|
19212
|
+
md += ` / ${t.reasoning} reasoning`;
|
|
19213
|
+
if (t.cache.read > 0 || t.cache.write > 0) {
|
|
19214
|
+
md += ` (cache: ${t.cache.read} read, ${t.cache.write} write)`;
|
|
19215
|
+
}
|
|
19216
|
+
md += `*\n`;
|
|
19217
|
+
if (assistant.cost && assistant.cost > 0) {
|
|
19218
|
+
md += `*Cost: $${assistant.cost.toFixed(4)}*\n`;
|
|
19219
|
+
}
|
|
19220
|
+
md += `\n`;
|
|
19221
|
+
}
|
|
19222
|
+
for (const part of parts) {
|
|
19223
|
+
md += renderPart(part, detailed);
|
|
19224
|
+
}
|
|
19225
|
+
md += `---\n\n`;
|
|
19226
|
+
}
|
|
19227
|
+
}
|
|
19228
|
+
// Write to the project directory
|
|
19229
|
+
const projectDir = pathResult.data?.directory;
|
|
19230
|
+
const idPrefix = sessionId.substring(0, 8);
|
|
19231
|
+
const suffix = detailed ? "-detailed" : "";
|
|
19232
|
+
const filename = `session-${idPrefix}${suffix}.md`;
|
|
19233
|
+
const exportPath = (0,external_node_path_.resolve)(projectDir || ".", filename);
|
|
19234
|
+
(0,external_node_fs_.writeFileSync)(exportPath, md, "utf-8");
|
|
19235
|
+
console.log(`[Telegram] Exported session ${sessionId} to ${exportPath} (${detailed ? "detailed" : "summary"})`);
|
|
19236
|
+
// Delete the processing message
|
|
19237
|
+
try {
|
|
19238
|
+
await bot.telegram.deleteMessage(ctx.chat.id, processingMsg.message_id);
|
|
19239
|
+
}
|
|
19240
|
+
catch {
|
|
19241
|
+
// Ignore if we can't delete it
|
|
19242
|
+
}
|
|
19243
|
+
// Send the file to the user
|
|
19244
|
+
await bot.telegram.sendDocument(ctx.chat.id, {
|
|
19245
|
+
source: Buffer.from(md, "utf-8"),
|
|
19246
|
+
filename,
|
|
19247
|
+
});
|
|
19248
|
+
await ctx.reply(`Session exported to: ${exportPath}`);
|
|
19249
|
+
}
|
|
19250
|
+
catch (err) {
|
|
19251
|
+
console.error("[Telegram] Error exporting session:", err);
|
|
19252
|
+
await handleSessionError(chatId);
|
|
19253
|
+
await ctx.reply("Sorry, there was an error exporting the session. Try again or use /new to start a fresh session.");
|
|
19254
|
+
}
|
|
19255
|
+
});
|
|
19024
19256
|
// Catch-all for unregistered / commands - forward to OpenCode
|
|
19025
19257
|
bot.on("text", async (ctx) => {
|
|
19026
19258
|
const userText = ctx.message.text;
|
|
@@ -19052,6 +19284,7 @@ async function startTelegram(options) {
|
|
|
19052
19284
|
body: {
|
|
19053
19285
|
command: commandName,
|
|
19054
19286
|
arguments: commandArgs,
|
|
19287
|
+
agent: "default",
|
|
19055
19288
|
},
|
|
19056
19289
|
});
|
|
19057
19290
|
if (result.error) {
|