@grinev/opencode-telegram-bot 0.15.0 → 0.16.1

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 (50) hide show
  1. package/.env.example +16 -0
  2. package/README.md +22 -1
  3. package/dist/app/start-bot-app.js +82 -7
  4. package/dist/bot/assistant-run-state.js +60 -0
  5. package/dist/bot/commands/abort.js +2 -0
  6. package/dist/bot/commands/commands.js +10 -0
  7. package/dist/bot/commands/definitions.js +1 -0
  8. package/dist/bot/commands/open.js +314 -0
  9. package/dist/bot/commands/projects.js +5 -38
  10. package/dist/bot/commands/start.js +2 -0
  11. package/dist/bot/handlers/inline-menu.js +9 -1
  12. package/dist/bot/handlers/prompt.js +11 -0
  13. package/dist/bot/index.js +187 -100
  14. package/dist/bot/streaming/response-streamer.js +26 -17
  15. package/dist/bot/utils/assistant-rendering.js +117 -0
  16. package/dist/bot/utils/assistant-run-footer.js +9 -0
  17. package/dist/bot/utils/browser-roots.js +140 -0
  18. package/dist/bot/utils/file-tree.js +92 -0
  19. package/dist/bot/utils/finalize-assistant-response.js +18 -24
  20. package/dist/bot/utils/send-with-markdown-fallback.js +3 -3
  21. package/dist/bot/utils/switch-project.js +48 -0
  22. package/dist/bot/utils/telegram-text.js +95 -1
  23. package/dist/cli/args.js +36 -7
  24. package/dist/cli.js +133 -15
  25. package/dist/config.js +4 -0
  26. package/dist/i18n/de.js +15 -10
  27. package/dist/i18n/en.js +15 -10
  28. package/dist/i18n/es.js +15 -10
  29. package/dist/i18n/fr.js +15 -10
  30. package/dist/i18n/ru.js +15 -10
  31. package/dist/i18n/zh.js +15 -10
  32. package/dist/index.js +2 -0
  33. package/dist/project/manager.js +2 -1
  34. package/dist/scheduled-task/runtime.js +8 -0
  35. package/dist/service/manager.js +244 -0
  36. package/dist/service/runtime.js +19 -0
  37. package/dist/service/types.js +1 -0
  38. package/dist/summary/aggregator.js +17 -1
  39. package/dist/summary/formatter.js +2 -88
  40. package/dist/telegram/render/block-fallback.js +28 -0
  41. package/dist/telegram/render/block-parser.js +295 -0
  42. package/dist/telegram/render/block-renderer.js +457 -0
  43. package/dist/telegram/render/chunker.js +281 -0
  44. package/dist/telegram/render/inline-renderer.js +128 -0
  45. package/dist/telegram/render/markdown-normalizer.js +94 -0
  46. package/dist/telegram/render/pipeline.js +9 -0
  47. package/dist/telegram/render/types.js +1 -0
  48. package/dist/telegram/render/validator.js +160 -0
  49. package/dist/utils/logger.js +200 -73
  50. package/package.json +6 -2
@@ -0,0 +1,295 @@
1
+ import { toString } from "mdast-util-to-string";
2
+ import { unified } from "unified";
3
+ import remarkGfm from "remark-gfm";
4
+ import remarkParse from "remark-parse";
5
+ import { normalizeMarkdownForTelegramBlockParsing } from "./markdown-normalizer.js";
6
+ const markdownProcessor = unified().use(remarkParse).use(remarkGfm);
7
+ function pushTextNode(nodes, text) {
8
+ if (!text) {
9
+ return;
10
+ }
11
+ const previous = nodes.at(-1);
12
+ if (previous?.type === "text") {
13
+ previous.text += text;
14
+ return;
15
+ }
16
+ nodes.push({ type: "text", text });
17
+ }
18
+ function appendInlineNodes(target, additions) {
19
+ for (const node of additions) {
20
+ if (node.type === "text") {
21
+ pushTextNode(target, node.text);
22
+ continue;
23
+ }
24
+ target.push(node);
25
+ }
26
+ }
27
+ function prefixLines(text, prefix) {
28
+ return text
29
+ .split("\n")
30
+ .map((line) => `${prefix}${line}`)
31
+ .join("\n");
32
+ }
33
+ function createPlainBlock(text) {
34
+ const normalized = text.trim();
35
+ if (!normalized) {
36
+ return [];
37
+ }
38
+ return [{ type: "plain", text: normalized }];
39
+ }
40
+ function extractInlinePlainText(nodes) {
41
+ let result = "";
42
+ for (const node of nodes) {
43
+ switch (node.type) {
44
+ case "text":
45
+ result += node.value;
46
+ break;
47
+ case "strong":
48
+ case "emphasis":
49
+ case "delete":
50
+ case "link":
51
+ result += extractInlinePlainText(node.children);
52
+ break;
53
+ case "inlineCode":
54
+ result += node.value;
55
+ break;
56
+ case "break":
57
+ result += "\n";
58
+ break;
59
+ case "image":
60
+ result += node.alt ?? "";
61
+ break;
62
+ case "imageReference":
63
+ result += node.alt ?? "";
64
+ break;
65
+ case "linkReference":
66
+ result += extractInlinePlainText(node.children);
67
+ break;
68
+ case "html":
69
+ result += node.value;
70
+ break;
71
+ case "footnoteReference":
72
+ result += `[^${node.identifier}]`;
73
+ break;
74
+ default:
75
+ result += toString(node);
76
+ break;
77
+ }
78
+ }
79
+ return result;
80
+ }
81
+ function extractTableCellPlainText(cell) {
82
+ return extractInlinePlainText(cell.children);
83
+ }
84
+ function extractListItemPlainText(item, index, ordered) {
85
+ const prefix = item.checked === true ? "✅ " : item.checked === false ? "🔲 " : "";
86
+ const marker = ordered ? `${index + 1}. ` : "- ";
87
+ const body = item.children.map(extractBlockPlainText).filter(Boolean).join("\n");
88
+ return `${marker}${prefix}${body}`.trimEnd();
89
+ }
90
+ function extractBlockPlainText(node) {
91
+ switch (node.type) {
92
+ case "paragraph":
93
+ case "heading":
94
+ return extractInlinePlainText(node.children);
95
+ case "blockquote":
96
+ return node.children
97
+ .map(extractBlockPlainText)
98
+ .filter(Boolean)
99
+ .map((text) => prefixLines(text, "> "))
100
+ .join("\n");
101
+ case "list":
102
+ return node.children
103
+ .map((item, index) => extractListItemPlainText(item, index, Boolean(node.ordered)))
104
+ .filter(Boolean)
105
+ .join("\n");
106
+ case "listItem":
107
+ return node.children.map(extractBlockPlainText).filter(Boolean).join("\n");
108
+ case "code":
109
+ return node.value;
110
+ case "table":
111
+ return node.children
112
+ .map((row) => row.children.map(extractTableCellPlainText).join(" | "))
113
+ .join("\n");
114
+ case "thematicBreak":
115
+ return "──────────";
116
+ case "html":
117
+ return node.value;
118
+ default:
119
+ return toString(node);
120
+ }
121
+ }
122
+ function parseInlineNodes(nodes) {
123
+ const result = [];
124
+ for (const node of nodes) {
125
+ switch (node.type) {
126
+ case "text":
127
+ pushTextNode(result, node.value);
128
+ break;
129
+ case "strong": {
130
+ const children = parseInlineNodes(node.children);
131
+ if (!children) {
132
+ return null;
133
+ }
134
+ result.push({ type: "bold", children });
135
+ break;
136
+ }
137
+ case "emphasis": {
138
+ const children = parseInlineNodes(node.children);
139
+ if (!children) {
140
+ return null;
141
+ }
142
+ result.push({ type: "italic", children });
143
+ break;
144
+ }
145
+ case "delete": {
146
+ const children = parseInlineNodes(node.children);
147
+ if (!children) {
148
+ return null;
149
+ }
150
+ result.push({ type: "strike", children });
151
+ break;
152
+ }
153
+ case "inlineCode":
154
+ result.push({ type: "code", text: node.value });
155
+ break;
156
+ case "link": {
157
+ const children = parseInlineNodes(node.children);
158
+ if (!children) {
159
+ return null;
160
+ }
161
+ result.push({ type: "link", text: children, url: node.url });
162
+ break;
163
+ }
164
+ case "break":
165
+ pushTextNode(result, "\n");
166
+ break;
167
+ default:
168
+ return null;
169
+ }
170
+ }
171
+ return result;
172
+ }
173
+ function parseParagraphBlock(node) {
174
+ const inlines = parseInlineNodes(node.children);
175
+ if (!inlines) {
176
+ return createPlainBlock(extractBlockPlainText(node));
177
+ }
178
+ return [{ type: "paragraph", inlines }];
179
+ }
180
+ function parseHeadingBlock(node) {
181
+ const inlines = parseInlineNodes(node.children);
182
+ if (!inlines) {
183
+ return createPlainBlock(extractBlockPlainText(node));
184
+ }
185
+ return [
186
+ {
187
+ type: "heading",
188
+ level: Math.min(6, Math.max(1, Math.floor(node.depth))),
189
+ inlines,
190
+ },
191
+ ];
192
+ }
193
+ function parseBlockquoteBlock(node) {
194
+ const lines = [];
195
+ for (const child of node.children) {
196
+ if (child.type !== "paragraph" && child.type !== "heading") {
197
+ return createPlainBlock(extractBlockPlainText(node));
198
+ }
199
+ const inlines = parseInlineNodes(child.children);
200
+ if (!inlines) {
201
+ return createPlainBlock(extractBlockPlainText(node));
202
+ }
203
+ lines.push(inlines);
204
+ }
205
+ if (lines.length === 0) {
206
+ return [];
207
+ }
208
+ return [{ type: "blockquote", lines }];
209
+ }
210
+ function parseListItemInlines(item) {
211
+ const result = [];
212
+ const taskPrefix = item.checked === true ? "✅ " : item.checked === false ? "🔲 " : "";
213
+ if (taskPrefix) {
214
+ pushTextNode(result, taskPrefix);
215
+ }
216
+ for (let index = 0; index < item.children.length; index++) {
217
+ const child = item.children[index];
218
+ if (child.type !== "paragraph") {
219
+ return null;
220
+ }
221
+ const inlines = parseInlineNodes(child.children);
222
+ if (!inlines) {
223
+ return null;
224
+ }
225
+ if (index > 0) {
226
+ pushTextNode(result, "\n");
227
+ }
228
+ appendInlineNodes(result, inlines);
229
+ }
230
+ return result;
231
+ }
232
+ function parseListBlock(node) {
233
+ const items = [];
234
+ for (const item of node.children) {
235
+ const parsedItem = parseListItemInlines(item);
236
+ if (!parsedItem) {
237
+ return createPlainBlock(extractBlockPlainText(node));
238
+ }
239
+ items.push(parsedItem);
240
+ }
241
+ if (items.length === 0) {
242
+ return [];
243
+ }
244
+ return [{ type: "list", ordered: Boolean(node.ordered), items }];
245
+ }
246
+ function parseCodeBlock(node) {
247
+ return [{ type: "code", language: node.lang ?? undefined, text: node.value }];
248
+ }
249
+ function parseTableBlock(node) {
250
+ const rows = node.children.map((row) => row.children.map(extractTableCellPlainText));
251
+ if (rows.length === 0) {
252
+ return [];
253
+ }
254
+ return [{ type: "table", rows }];
255
+ }
256
+ function parseRootContent(node) {
257
+ switch (node.type) {
258
+ case "paragraph":
259
+ return parseParagraphBlock(node);
260
+ case "heading":
261
+ return parseHeadingBlock(node);
262
+ case "blockquote":
263
+ return parseBlockquoteBlock(node);
264
+ case "list":
265
+ return parseListBlock(node);
266
+ case "code":
267
+ return parseCodeBlock(node);
268
+ case "table":
269
+ return parseTableBlock(node);
270
+ case "thematicBreak":
271
+ return [{ type: "rule" }];
272
+ default:
273
+ return createPlainBlock(extractBlockPlainText(node));
274
+ }
275
+ }
276
+ function mergeAdjacentBlocks(blocks) {
277
+ const merged = [];
278
+ for (const block of blocks) {
279
+ const previous = merged.at(-1);
280
+ if (previous?.type === "blockquote" && block.type === "blockquote") {
281
+ previous.lines.push(...block.lines);
282
+ continue;
283
+ }
284
+ merged.push(block);
285
+ }
286
+ return merged;
287
+ }
288
+ export function parseTelegramBlocks(markdown) {
289
+ const normalized = normalizeMarkdownForTelegramBlockParsing(markdown).trim();
290
+ if (!normalized) {
291
+ return [];
292
+ }
293
+ const tree = markdownProcessor.parse(normalized);
294
+ return mergeAdjacentBlocks(tree.children.flatMap(parseRootContent));
295
+ }