@grinev/opencode-telegram-bot 0.7.0 → 0.9.0

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.
@@ -1,4 +1,5 @@
1
1
  import * as path from "path";
2
+ import { convert } from "telegram-markdown-v2";
2
3
  import { config } from "../config.js";
3
4
  import { logger } from "../utils/logger.js";
4
5
  import { t } from "../i18n/index.js";
@@ -22,6 +23,87 @@ function splitText(text, maxLength) {
22
23
  }
23
24
  return parts;
24
25
  }
26
+ function isCodeFenceLine(line) {
27
+ return line.trimStart().startsWith("```");
28
+ }
29
+ function isHorizontalRuleLine(line) {
30
+ const normalized = line.trim();
31
+ if (!normalized) {
32
+ return false;
33
+ }
34
+ return /^([-*_])(?:\s*\1){2,}$/.test(normalized);
35
+ }
36
+ function isHeadingLine(line) {
37
+ return /^\s{0,3}#{1,6}\s+\S/.test(line);
38
+ }
39
+ function normalizeHeadingLine(line) {
40
+ const match = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*$/);
41
+ if (!match) {
42
+ return line;
43
+ }
44
+ return `**${match[1]}**`;
45
+ }
46
+ function normalizeChecklistLine(line) {
47
+ const match = line.match(/^(\s*)(?:[-+*]|\d+\.)\s+\[( |x|X)\]\s+(.*)$/);
48
+ if (!match) {
49
+ return null;
50
+ }
51
+ const marker = match[2].toLowerCase() === "x" ? "✅" : "🔲";
52
+ return `${match[1]}${marker} ${match[3]}`;
53
+ }
54
+ function preprocessMarkdownForTelegram(text) {
55
+ const lines = text.split("\n");
56
+ const output = [];
57
+ let inCodeFence = false;
58
+ let inQuote = false;
59
+ for (let index = 0; index < lines.length; index++) {
60
+ const line = lines[index];
61
+ if (isCodeFenceLine(line)) {
62
+ inCodeFence = !inCodeFence;
63
+ inQuote = false;
64
+ output.push(line);
65
+ continue;
66
+ }
67
+ if (inCodeFence) {
68
+ output.push(line);
69
+ continue;
70
+ }
71
+ if (!line.trim()) {
72
+ inQuote = false;
73
+ output.push(line);
74
+ continue;
75
+ }
76
+ if (isHeadingLine(line)) {
77
+ output.push(normalizeHeadingLine(line));
78
+ inQuote = false;
79
+ continue;
80
+ }
81
+ if (isHorizontalRuleLine(line)) {
82
+ output.push("──────────");
83
+ inQuote = false;
84
+ continue;
85
+ }
86
+ const trimmedLeft = line.trimStart();
87
+ if (trimmedLeft.startsWith(">")) {
88
+ inQuote = true;
89
+ const quoteContent = trimmedLeft.replace(/^>\s?/, "");
90
+ const normalizedChecklistInQuote = normalizeChecklistLine(quoteContent);
91
+ output.push(normalizedChecklistInQuote ? `> ${normalizedChecklistInQuote.trimStart()}` : trimmedLeft);
92
+ continue;
93
+ }
94
+ const normalizedChecklist = normalizeChecklistLine(line);
95
+ if (normalizedChecklist) {
96
+ output.push(inQuote ? `> ${normalizedChecklist.trimStart()}` : normalizedChecklist);
97
+ continue;
98
+ }
99
+ if (inQuote) {
100
+ output.push(`> ${trimmedLeft}`);
101
+ continue;
102
+ }
103
+ output.push(line);
104
+ }
105
+ return output.join("\n");
106
+ }
25
107
  export function normalizePathForDisplay(filePath) {
26
108
  const normalizedPath = filePath.replace(/\\/g, "/");
27
109
  const project = getCurrentProject();
@@ -44,6 +126,25 @@ export function normalizePathForDisplay(filePath) {
44
126
  return normalizedPath;
45
127
  }
46
128
  export function formatSummary(text) {
129
+ return formatSummaryWithMode(text, config.bot.messageFormatMode);
130
+ }
131
+ export function getAssistantParseMode() {
132
+ if (config.bot.messageFormatMode === "markdown") {
133
+ return "MarkdownV2";
134
+ }
135
+ return undefined;
136
+ }
137
+ function formatMarkdownForTelegram(text) {
138
+ try {
139
+ const preprocessed = preprocessMarkdownForTelegram(text);
140
+ return convert(preprocessed, "keep");
141
+ }
142
+ catch (error) {
143
+ logger.warn("[Formatter] Failed to convert markdown summary, falling back to raw text", error);
144
+ return text;
145
+ }
146
+ }
147
+ export function formatSummaryWithMode(text, mode) {
47
148
  if (!text || text.trim().length === 0) {
48
149
  return [];
49
150
  }
@@ -54,6 +155,17 @@ export function formatSummary(text) {
54
155
  if (!trimmed) {
55
156
  continue;
56
157
  }
158
+ if (mode === "markdown") {
159
+ const converted = formatMarkdownForTelegram(trimmed);
160
+ const convertedParts = splitText(converted, TELEGRAM_MESSAGE_LIMIT);
161
+ for (const convertedPart of convertedParts) {
162
+ const normalizedPart = convertedPart.trim();
163
+ if (normalizedPart) {
164
+ formattedParts.push(normalizedPart);
165
+ }
166
+ }
167
+ continue;
168
+ }
57
169
  if (parts.length > 1) {
58
170
  formattedParts.push(`\`\`\`\n${trimmed}\n\`\`\``);
59
171
  }
@@ -142,15 +254,15 @@ function getToolIcon(tool) {
142
254
  function formatTodos(todos) {
143
255
  const MAX_TODOS = 20;
144
256
  const statusToMarker = {
145
- completed: "x",
146
- in_progress: "~",
147
- pending: " ",
257
+ completed: "",
258
+ in_progress: "🔄",
259
+ pending: "🔲",
148
260
  };
149
261
  const formattedTodos = [];
150
262
  for (let i = 0; i < Math.min(todos.length, MAX_TODOS); i++) {
151
263
  const todo = todos[i];
152
- const marker = statusToMarker[todo.status] ?? " ";
153
- formattedTodos.push(`[${marker}] ${todo.content}`);
264
+ const marker = statusToMarker[todo.status] ?? "🔲";
265
+ formattedTodos.push(`${marker} ${todo.content}`);
154
266
  }
155
267
  let result = formattedTodos.join("\n");
156
268
  if (todos.length > MAX_TODOS) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grinev/opencode-telegram-bot",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Telegram bot client for OpenCode to run and monitor coding tasks from chat.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -57,7 +57,8 @@
57
57
  "dotenv": "^17.2.3",
58
58
  "grammy": "^1.39.2",
59
59
  "https-proxy-agent": "^7.0.6",
60
- "socks-proxy-agent": "^8.0.5"
60
+ "socks-proxy-agent": "^8.0.5",
61
+ "telegram-markdown-v2": "^0.0.4"
61
62
  },
62
63
  "devDependencies": {
63
64
  "@types/better-sqlite3": "^7.6.13",