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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/dist/index.js +234 -1
  3. 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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-telegram-bot",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "description": "Telegram bot that forwards messages to an OpenCode agent",
6
6
  "main": "./dist/index.js",