@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,457 @@
1
+ import { renderInlineNodesValidated } from "./inline-renderer.js";
2
+ import { validateTelegramEntities } from "./validator.js";
3
+ const ENTITY_TYPE_PRIORITY = {
4
+ bold: 1,
5
+ italic: 2,
6
+ underline: 3,
7
+ strikethrough: 4,
8
+ spoiler: 5,
9
+ code: 6,
10
+ pre: 7,
11
+ text_link: 8,
12
+ mention: 100,
13
+ hashtag: 101,
14
+ cashtag: 102,
15
+ bot_command: 103,
16
+ url: 104,
17
+ email: 105,
18
+ phone_number: 106,
19
+ blockquote: 107,
20
+ expandable_blockquote: 108,
21
+ text_mention: 109,
22
+ custom_emoji: 110,
23
+ };
24
+ function compareEntities(left, right) {
25
+ if (left.offset !== right.offset) {
26
+ return left.offset - right.offset;
27
+ }
28
+ if (left.length !== right.length) {
29
+ return right.length - left.length;
30
+ }
31
+ return ENTITY_TYPE_PRIORITY[left.type] - ENTITY_TYPE_PRIORITY[right.type];
32
+ }
33
+ function sortEntities(entities) {
34
+ return [...entities].sort(compareEntities);
35
+ }
36
+ function pushTextNode(target, text) {
37
+ if (!text) {
38
+ return;
39
+ }
40
+ const previous = target.at(-1);
41
+ if (previous?.type === "text") {
42
+ previous.text += text;
43
+ return;
44
+ }
45
+ target.push({ type: "text", text });
46
+ }
47
+ function appendInlineNode(target, node) {
48
+ if (node.type === "text") {
49
+ pushTextNode(target, node.text);
50
+ return;
51
+ }
52
+ target.push(node);
53
+ }
54
+ function appendInlineNodes(target, nodes) {
55
+ for (const node of nodes) {
56
+ appendInlineNode(target, node);
57
+ }
58
+ }
59
+ function rebaseEntities(entities, offset) {
60
+ if (!entities?.length) {
61
+ return [];
62
+ }
63
+ return entities.map((entity) => ({
64
+ ...entity,
65
+ offset: entity.offset + offset,
66
+ }));
67
+ }
68
+ function extractInlinePlainText(nodes) {
69
+ let result = "";
70
+ for (const node of nodes) {
71
+ switch (node.type) {
72
+ case "text":
73
+ result += node.text;
74
+ break;
75
+ case "bold":
76
+ case "italic":
77
+ case "strike":
78
+ case "underline":
79
+ case "spoiler":
80
+ result += extractInlinePlainText(node.children);
81
+ break;
82
+ case "code":
83
+ result += node.text;
84
+ break;
85
+ case "link":
86
+ result += extractInlinePlainText(node.text);
87
+ break;
88
+ default: {
89
+ const exhaustiveCheck = node;
90
+ throw new Error(`Unsupported inline node: ${JSON.stringify(exhaustiveCheck)}`);
91
+ }
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+ function containsCodeNode(nodes) {
97
+ for (const node of nodes) {
98
+ switch (node.type) {
99
+ case "code":
100
+ return true;
101
+ case "bold":
102
+ case "italic":
103
+ case "strike":
104
+ case "underline":
105
+ case "spoiler":
106
+ if (containsCodeNode(node.children)) {
107
+ return true;
108
+ }
109
+ break;
110
+ case "link":
111
+ if (containsCodeNode(node.text)) {
112
+ return true;
113
+ }
114
+ break;
115
+ case "text":
116
+ break;
117
+ default: {
118
+ const exhaustiveCheck = node;
119
+ throw new Error(`Unsupported inline node: ${JSON.stringify(exhaustiveCheck)}`);
120
+ }
121
+ }
122
+ }
123
+ return false;
124
+ }
125
+ function simplifyInlineNodes(nodes, insideLink = false) {
126
+ const result = [];
127
+ for (const node of nodes) {
128
+ switch (node.type) {
129
+ case "text":
130
+ pushTextNode(result, node.text);
131
+ break;
132
+ case "code":
133
+ result.push(node);
134
+ break;
135
+ case "link": {
136
+ const simplifiedChildren = simplifyInlineNodes(node.text, true);
137
+ if (insideLink) {
138
+ appendInlineNodes(result, simplifiedChildren);
139
+ break;
140
+ }
141
+ if (simplifiedChildren.length > 0) {
142
+ result.push({ type: "link", text: simplifiedChildren, url: node.url });
143
+ }
144
+ break;
145
+ }
146
+ case "bold":
147
+ case "italic":
148
+ case "strike":
149
+ case "underline":
150
+ case "spoiler": {
151
+ const simplifiedChildren = simplifyInlineNodes(node.children, insideLink);
152
+ if (simplifiedChildren.length === 0) {
153
+ break;
154
+ }
155
+ if (containsCodeNode(simplifiedChildren)) {
156
+ appendInlineNodes(result, simplifiedChildren);
157
+ break;
158
+ }
159
+ result.push({ ...node, children: simplifiedChildren });
160
+ break;
161
+ }
162
+ default: {
163
+ const exhaustiveCheck = node;
164
+ throw new Error(`Unsupported inline node: ${JSON.stringify(exhaustiveCheck)}`);
165
+ }
166
+ }
167
+ }
168
+ return result;
169
+ }
170
+ function wrapInlineNode(node, children) {
171
+ if (children.length === 0) {
172
+ return null;
173
+ }
174
+ switch (node.type) {
175
+ case "bold":
176
+ case "italic":
177
+ case "strike":
178
+ case "underline":
179
+ case "spoiler":
180
+ return { ...node, children };
181
+ case "link":
182
+ return { ...node, text: children };
183
+ case "text":
184
+ return { type: "text", text: extractInlinePlainText(children) };
185
+ case "code":
186
+ return { type: "code", text: extractInlinePlainText(children) };
187
+ default: {
188
+ const exhaustiveCheck = node;
189
+ throw new Error(`Unsupported inline node: ${JSON.stringify(exhaustiveCheck)}`);
190
+ }
191
+ }
192
+ }
193
+ function splitInlineNodesByNewline(nodes) {
194
+ const lines = [[]];
195
+ const pushLineBreak = () => {
196
+ lines.push([]);
197
+ };
198
+ const appendNodeWithLineSplit = (node) => {
199
+ switch (node.type) {
200
+ case "text": {
201
+ const parts = node.text.split("\n");
202
+ for (let index = 0; index < parts.length; index++) {
203
+ pushTextNode(lines[lines.length - 1], parts[index]);
204
+ if (index < parts.length - 1) {
205
+ pushLineBreak();
206
+ }
207
+ }
208
+ break;
209
+ }
210
+ case "code": {
211
+ const parts = node.text.split("\n");
212
+ for (let index = 0; index < parts.length; index++) {
213
+ if (parts[index]) {
214
+ appendInlineNode(lines[lines.length - 1], { type: "code", text: parts[index] });
215
+ }
216
+ if (index < parts.length - 1) {
217
+ pushLineBreak();
218
+ }
219
+ }
220
+ break;
221
+ }
222
+ case "bold":
223
+ case "italic":
224
+ case "strike":
225
+ case "underline":
226
+ case "spoiler":
227
+ case "link": {
228
+ const childLines = splitInlineNodesByNewline(node.type === "link" ? node.text : node.children);
229
+ for (let index = 0; index < childLines.length; index++) {
230
+ if (index > 0) {
231
+ pushLineBreak();
232
+ }
233
+ const wrapped = wrapInlineNode(node, childLines[index]);
234
+ if (wrapped) {
235
+ appendInlineNode(lines[lines.length - 1], wrapped);
236
+ }
237
+ }
238
+ break;
239
+ }
240
+ default: {
241
+ const exhaustiveCheck = node;
242
+ throw new Error(`Unsupported inline node: ${JSON.stringify(exhaustiveCheck)}`);
243
+ }
244
+ }
245
+ };
246
+ for (const node of nodes) {
247
+ appendNodeWithLineSplit(node);
248
+ }
249
+ return lines;
250
+ }
251
+ function applyWholeLineBold(text, entities) {
252
+ if (!text) {
253
+ return entities;
254
+ }
255
+ const nextEntities = sortEntities([
256
+ ...(entities ?? []),
257
+ { type: "bold", offset: 0, length: text.length },
258
+ ]);
259
+ const validation = validateTelegramEntities(text, nextEntities);
260
+ if (!validation.ok) {
261
+ throw new Error(validation.issues.map((issue) => issue.message).join("; "));
262
+ }
263
+ return nextEntities;
264
+ }
265
+ function createRenderedBlock(blockType, mode, text, fallbackText, entities) {
266
+ const normalizedEntities = entities?.length ? sortEntities(entities) : undefined;
267
+ if (normalizedEntities) {
268
+ const validation = validateTelegramEntities(text, normalizedEntities);
269
+ if (!validation.ok) {
270
+ throw new Error(validation.issues.map((issue) => issue.message).join("; "));
271
+ }
272
+ }
273
+ return {
274
+ blockType,
275
+ mode,
276
+ text,
277
+ entities: normalizedEntities,
278
+ fallbackText,
279
+ source: normalizedEntities?.length ? "entities" : "plain",
280
+ };
281
+ }
282
+ function renderInlineSegment(nodes, simplify, boldWholeLine = false) {
283
+ const effectiveNodes = simplify ? simplifyInlineNodes(nodes) : nodes;
284
+ const rendered = renderInlineNodesValidated(effectiveNodes);
285
+ const entities = boldWholeLine
286
+ ? applyWholeLineBold(rendered.text, rendered.entities)
287
+ : rendered.entities;
288
+ return {
289
+ text: rendered.text,
290
+ fallbackText: extractInlinePlainText(effectiveNodes),
291
+ entities,
292
+ source: entities?.length ? "entities" : "plain",
293
+ };
294
+ }
295
+ function renderInlineSegmentWithLocalFallback(nodes, boldWholeLine = false) {
296
+ try {
297
+ return renderInlineSegment(nodes, false, boldWholeLine);
298
+ }
299
+ catch {
300
+ try {
301
+ return renderInlineSegment(nodes, true, boldWholeLine);
302
+ }
303
+ catch {
304
+ const text = extractInlinePlainText(nodes);
305
+ try {
306
+ const entities = boldWholeLine ? applyWholeLineBold(text, undefined) : undefined;
307
+ return {
308
+ text,
309
+ fallbackText: text,
310
+ entities,
311
+ source: entities?.length ? "entities" : "plain",
312
+ };
313
+ }
314
+ catch {
315
+ return {
316
+ text,
317
+ fallbackText: text,
318
+ source: "plain",
319
+ };
320
+ }
321
+ }
322
+ }
323
+ }
324
+ function renderLineSpecs(blockType, mode, lineSpecs, boldWholeLine = false) {
325
+ const textParts = [];
326
+ const fallbackParts = [];
327
+ const entities = [];
328
+ let currentOffset = 0;
329
+ for (let index = 0; index < lineSpecs.length; index++) {
330
+ const lineSpec = lineSpecs[index];
331
+ const renderedLine = mode === "line-by-line"
332
+ ? renderInlineSegmentWithLocalFallback(lineSpec.nodes, boldWholeLine)
333
+ : renderInlineSegment(lineSpec.nodes, mode === "simplified", boldWholeLine);
334
+ const lineText = `${lineSpec.prefix}${renderedLine.text}`;
335
+ const lineFallbackText = `${lineSpec.prefix}${renderedLine.fallbackText}`;
336
+ if (index > 0) {
337
+ currentOffset += 1;
338
+ }
339
+ textParts.push(lineText);
340
+ fallbackParts.push(lineFallbackText);
341
+ entities.push(...rebaseEntities(renderedLine.entities, currentOffset + lineSpec.prefix.length));
342
+ currentOffset += lineText.length;
343
+ }
344
+ return createRenderedBlock(blockType, mode, textParts.join("\n"), fallbackParts.join("\n"), entities);
345
+ }
346
+ function renderPreformattedBlock(blockType, mode, text, language) {
347
+ const entities = text
348
+ ? [
349
+ {
350
+ type: "pre",
351
+ offset: 0,
352
+ length: text.length,
353
+ ...(language ? { language } : {}),
354
+ },
355
+ ]
356
+ : undefined;
357
+ return createRenderedBlock(blockType, mode, text, text, entities);
358
+ }
359
+ function buildParagraphLineSpecs(inlines) {
360
+ return splitInlineNodesByNewline(inlines).map((nodes) => ({ prefix: "", nodes }));
361
+ }
362
+ function buildBlockquoteLineSpecs(lines) {
363
+ return lines.flatMap((lineNodes) => splitInlineNodesByNewline(lineNodes).map((nodes) => ({
364
+ prefix: "> ",
365
+ nodes,
366
+ })));
367
+ }
368
+ function buildListLineSpecs(ordered, items) {
369
+ return items.flatMap((itemNodes, index) => {
370
+ const marker = ordered ? `${index + 1}. ` : "- ";
371
+ const continuationPrefix = " ".repeat(marker.length);
372
+ return splitInlineNodesByNewline(itemNodes).map((nodes, lineIndex) => ({
373
+ prefix: lineIndex === 0 ? marker : continuationPrefix,
374
+ nodes,
375
+ }));
376
+ });
377
+ }
378
+ function buildAlignedTableText(rows) {
379
+ const columnCount = Math.max(...rows.map((row) => row.length));
380
+ const normalizedRows = rows.map((row) => Array.from({ length: columnCount }, (_, index) => row[index] ?? ""));
381
+ const columnWidths = Array.from({ length: columnCount }, (_, index) => Math.max(...normalizedRows.map((row) => row[index].length)));
382
+ const formatRow = (row) => row.map((cell, index) => cell.padEnd(columnWidths[index], " ")).join(" | ");
383
+ const divider = columnWidths.map((width) => "-".repeat(width)).join("-|-");
384
+ const formattedRows = normalizedRows.map(formatRow);
385
+ if (formattedRows.length <= 1) {
386
+ return formattedRows.join("\n");
387
+ }
388
+ return [formattedRows[0], divider, ...formattedRows.slice(1)].join("\n");
389
+ }
390
+ export function renderTelegramBlock(block, mode = "full") {
391
+ switch (block.type) {
392
+ case "paragraph":
393
+ if (mode === "plain") {
394
+ const text = extractInlinePlainText(block.inlines);
395
+ return createRenderedBlock(block.type, mode, text, text);
396
+ }
397
+ if (mode === "line-by-line") {
398
+ return renderLineSpecs(block.type, mode, buildParagraphLineSpecs(block.inlines));
399
+ }
400
+ {
401
+ const segment = renderInlineSegment(block.inlines, mode === "simplified");
402
+ return createRenderedBlock(block.type, mode, segment.text, segment.fallbackText, segment.entities);
403
+ }
404
+ case "heading":
405
+ if (mode === "plain") {
406
+ const text = extractInlinePlainText(block.inlines);
407
+ return createRenderedBlock(block.type, mode, text, text);
408
+ }
409
+ if (mode === "line-by-line") {
410
+ return renderLineSpecs(block.type, mode, buildParagraphLineSpecs(block.inlines), true);
411
+ }
412
+ {
413
+ const segment = renderInlineSegment(block.inlines, mode === "simplified", true);
414
+ return createRenderedBlock(block.type, mode, segment.text, segment.fallbackText, segment.entities);
415
+ }
416
+ case "blockquote":
417
+ if (mode === "plain") {
418
+ const text = buildBlockquoteLineSpecs(block.lines)
419
+ .map((line) => `${line.prefix}${extractInlinePlainText(line.nodes)}`)
420
+ .join("\n");
421
+ return createRenderedBlock(block.type, mode, text, text);
422
+ }
423
+ return renderLineSpecs(block.type, mode === "line-by-line" ? mode : mode, buildBlockquoteLineSpecs(mode === "simplified"
424
+ ? block.lines.map((line) => simplifyInlineNodes(line))
425
+ : block.lines));
426
+ case "list":
427
+ if (mode === "plain") {
428
+ const text = buildListLineSpecs(block.ordered, block.items)
429
+ .map((line) => `${line.prefix}${extractInlinePlainText(line.nodes)}`)
430
+ .join("\n");
431
+ return createRenderedBlock(block.type, mode, text, text);
432
+ }
433
+ return renderLineSpecs(block.type, mode === "line-by-line" ? mode : mode, buildListLineSpecs(block.ordered, mode === "simplified"
434
+ ? block.items.map((item) => simplifyInlineNodes(item))
435
+ : block.items));
436
+ case "code":
437
+ if (mode === "plain" || mode === "line-by-line") {
438
+ return createRenderedBlock(block.type, mode, block.text, block.text);
439
+ }
440
+ return renderPreformattedBlock(block.type, mode, block.text, block.language);
441
+ case "table": {
442
+ const text = buildAlignedTableText(block.rows);
443
+ if (mode === "plain" || mode === "line-by-line") {
444
+ return createRenderedBlock(block.type, mode, text, text);
445
+ }
446
+ return renderPreformattedBlock(block.type, mode, text);
447
+ }
448
+ case "rule":
449
+ return createRenderedBlock(block.type, mode, "──────────", "──────────");
450
+ case "plain":
451
+ return createRenderedBlock(block.type, mode, block.text, block.text);
452
+ default: {
453
+ const exhaustiveCheck = block;
454
+ throw new Error(`Unsupported Telegram block: ${JSON.stringify(exhaustiveCheck)}`);
455
+ }
456
+ }
457
+ }