@alexnodeland/claude-telegram 0.3.0 → 0.3.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.
- package/package.json +1 -1
- package/src/html.ts +75 -0
- package/src/streaming.ts +2 -2
package/package.json
CHANGED
package/src/html.ts
CHANGED
|
@@ -23,3 +23,78 @@ export const fmt = {
|
|
|
23
23
|
link: (text: string, url: string) => `<a href="${escapeHtml(url)}">${escapeHtml(text)}</a>`,
|
|
24
24
|
strikethrough: (text: string) => `<s>${escapeHtml(text)}</s>`,
|
|
25
25
|
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert Markdown (Claude's output format) to Telegram-compatible HTML.
|
|
29
|
+
*
|
|
30
|
+
* Handles: fenced code blocks, inline code, bold, italic, strikethrough,
|
|
31
|
+
* links, and headers. Content is HTML-escaped before conversion so the
|
|
32
|
+
* resulting string is safe to send with parse_mode: "HTML".
|
|
33
|
+
*/
|
|
34
|
+
export function markdownToTelegramHtml(md: string): string {
|
|
35
|
+
const PLACEHOLDER_PREFIX = "\u2060CBLK";
|
|
36
|
+
const PLACEHOLDER_SUFFIX = "CBLK\u2060";
|
|
37
|
+
|
|
38
|
+
// 1. Extract fenced code blocks into placeholders
|
|
39
|
+
const codeBlocks: string[] = [];
|
|
40
|
+
const withPlaceholders = md.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang: string, code: string) => {
|
|
41
|
+
const escaped = escapeHtml(code.replace(/\n$/, ""));
|
|
42
|
+
const html = lang
|
|
43
|
+
? `<pre><code class="language-${escapeHtml(lang)}">${escaped}</code></pre>`
|
|
44
|
+
: `<pre>${escaped}</pre>`;
|
|
45
|
+
codeBlocks.push(html);
|
|
46
|
+
return `${PLACEHOLDER_PREFIX}${codeBlocks.length - 1}${PLACEHOLDER_SUFFIX}`;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// 2. Process non-code-block text
|
|
50
|
+
const placeholderRe = new RegExp(`(${PLACEHOLDER_PREFIX}\\d+${PLACEHOLDER_SUFFIX})`, "g");
|
|
51
|
+
const matchRe = new RegExp(`^${PLACEHOLDER_PREFIX}(\\d+)${PLACEHOLDER_SUFFIX}$`);
|
|
52
|
+
|
|
53
|
+
const processed = withPlaceholders
|
|
54
|
+
.split(placeholderRe)
|
|
55
|
+
.map((segment) => {
|
|
56
|
+
const cbMatch = segment.match(matchRe);
|
|
57
|
+
if (cbMatch) return codeBlocks[Number(cbMatch[1])];
|
|
58
|
+
return convertInlineFormatting(segment);
|
|
59
|
+
})
|
|
60
|
+
.join("");
|
|
61
|
+
|
|
62
|
+
return processed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Convert inline Markdown formatting in a text segment (no fenced code blocks). */
|
|
66
|
+
function convertInlineFormatting(text: string): string {
|
|
67
|
+
const parts = text.split(/(`[^`]+`)/g);
|
|
68
|
+
|
|
69
|
+
return parts
|
|
70
|
+
.map((part) => {
|
|
71
|
+
if (part.startsWith("`") && part.endsWith("`")) {
|
|
72
|
+
return `<code>${escapeHtml(part.slice(1, -1))}</code>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let s = escapeHtml(part);
|
|
76
|
+
|
|
77
|
+
// Headers → bold
|
|
78
|
+
s = s.replace(/^(#{1,6})\s+(.+)$/gm, (_m, _hashes: string, content: string) => `<b>${content}</b>`);
|
|
79
|
+
|
|
80
|
+
// Bold: **text** or __text__
|
|
81
|
+
s = s.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
|
|
82
|
+
s = s.replace(/__(.+?)__/g, "<b>$1</b>");
|
|
83
|
+
|
|
84
|
+
// Italic: *text* or _text_
|
|
85
|
+
s = s.replace(/(?<!\w)\*(?!\s)(.+?)(?<!\s)\*(?!\w)/g, "<i>$1</i>");
|
|
86
|
+
s = s.replace(/(?<!\w)_(?!\s)(.+?)(?<!\s)_(?!\w)/g, "<i>$1</i>");
|
|
87
|
+
|
|
88
|
+
// Strikethrough: ~~text~~
|
|
89
|
+
s = s.replace(/~~(.+?)~~/g, "<s>$1</s>");
|
|
90
|
+
|
|
91
|
+
// Links: [text](url)
|
|
92
|
+
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
93
|
+
|
|
94
|
+
// Bullet lists: leading "- " or "* " → "• "
|
|
95
|
+
s = s.replace(/^[\t ]*[-*]\s+/gm, "• ");
|
|
96
|
+
|
|
97
|
+
return s;
|
|
98
|
+
})
|
|
99
|
+
.join("");
|
|
100
|
+
}
|
package/src/streaming.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeHtml, fmt } from "./html.js";
|
|
1
|
+
import { escapeHtml, fmt, markdownToTelegramHtml } from "./html.js";
|
|
2
2
|
import type { TelegramClient } from "./telegram.js";
|
|
3
3
|
|
|
4
4
|
const MAX_TG_LENGTH = 4096;
|
|
@@ -33,7 +33,7 @@ export class StreamingRenderer {
|
|
|
33
33
|
const trimmed = text.trim();
|
|
34
34
|
if (!trimmed) return;
|
|
35
35
|
this.lastToolMessageId = null; // new text block clears tool context
|
|
36
|
-
await sendLongMessage(this.tg, this.chatId,
|
|
36
|
+
await sendLongMessage(this.tg, this.chatId, markdownToTelegramHtml(trimmed));
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
async showToolCall(toolName: string, input: unknown): Promise<void> {
|