@tspappsen/elamax 1.2.3
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/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/api/server.js +297 -0
- package/dist/cli.js +105 -0
- package/dist/config.js +96 -0
- package/dist/copilot/classifier.js +72 -0
- package/dist/copilot/client.js +30 -0
- package/dist/copilot/mcp-config.js +22 -0
- package/dist/copilot/orchestrator.js +459 -0
- package/dist/copilot/router.js +147 -0
- package/dist/copilot/skills.js +125 -0
- package/dist/copilot/system-message.js +185 -0
- package/dist/copilot/tools.js +486 -0
- package/dist/copilot/watchdog-tools.js +312 -0
- package/dist/copilot/workspace-instructions.js +100 -0
- package/dist/daemon.js +237 -0
- package/dist/diagnosis.js +79 -0
- package/dist/discord/bot.js +505 -0
- package/dist/discord/formatter.js +29 -0
- package/dist/paths.js +37 -0
- package/dist/setup.js +476 -0
- package/dist/store/db.js +173 -0
- package/dist/telegram/bot.js +344 -0
- package/dist/telegram/formatter.js +96 -0
- package/dist/tui/index.js +1026 -0
- package/dist/update.js +72 -0
- package/dist/utils/parseJSON.js +71 -0
- package/package.json +61 -0
- package/skills/.gitkeep +0 -0
- package/skills/find-skills/SKILL.md +161 -0
- package/skills/find-skills/_meta.json +4 -0
- package/templates/instructions/AGENTS.md +18 -0
- package/templates/instructions/TOOLS.md +12 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { Bot } from "grammy";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { basename, extname, join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { config, persistModel } from "../config.js";
|
|
8
|
+
import { sendToOrchestrator, cancelCurrentMessage, getWorkers, getLastRouteResult, invalidateSession } from "../copilot/orchestrator.js";
|
|
9
|
+
import { chunkMessage, toTelegramMarkdown } from "./formatter.js";
|
|
10
|
+
import { searchMemories } from "../store/db.js";
|
|
11
|
+
import { listSkills } from "../copilot/skills.js";
|
|
12
|
+
import { restartDaemon } from "../daemon.js";
|
|
13
|
+
import { getRouterConfig, updateRouterConfig } from "../copilot/router.js";
|
|
14
|
+
let bot;
|
|
15
|
+
async function sendTelegramResponse(ctx, text, replyParams, indicator) {
|
|
16
|
+
const formatted = toTelegramMarkdown(text) + (indicator?.markdownSuffix ?? "");
|
|
17
|
+
const chunks = chunkMessage(formatted);
|
|
18
|
+
const fallbackChunks = chunkMessage(text + (indicator?.plainSuffix ?? ""));
|
|
19
|
+
const sendChunk = async (chunk, fallback, isFirst) => {
|
|
20
|
+
const opts = isFirst
|
|
21
|
+
? { parse_mode: "MarkdownV2", reply_parameters: replyParams }
|
|
22
|
+
: { parse_mode: "MarkdownV2" };
|
|
23
|
+
await ctx.reply(chunk, opts).catch(() => ctx.reply(fallback, isFirst ? { reply_parameters: replyParams } : {}));
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
27
|
+
await sendChunk(chunks[i], fallbackChunks[i] ?? chunks[i], i === 0);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
try {
|
|
32
|
+
for (let i = 0; i < fallbackChunks.length; i++) {
|
|
33
|
+
await ctx.reply(fallbackChunks[i], i === 0 ? { reply_parameters: replyParams } : {});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Nothing more we can do
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function startTypingIndicator(ctx) {
|
|
42
|
+
let typingInterval;
|
|
43
|
+
void ctx.replyWithChatAction("typing").catch(() => { });
|
|
44
|
+
typingInterval = setInterval(() => {
|
|
45
|
+
void ctx.replyWithChatAction("typing").catch(() => { });
|
|
46
|
+
}, 4000);
|
|
47
|
+
return () => {
|
|
48
|
+
if (typingInterval) {
|
|
49
|
+
clearInterval(typingInterval);
|
|
50
|
+
typingInterval = undefined;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function handleTelegramTurn(ctx, prompt, options) {
|
|
55
|
+
if (!ctx.chat || !ctx.message) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const chatId = ctx.chat.id;
|
|
59
|
+
const userMessageId = ctx.message.message_id;
|
|
60
|
+
const replyParams = { message_id: userMessageId };
|
|
61
|
+
const stopTyping = startTypingIndicator(ctx);
|
|
62
|
+
try {
|
|
63
|
+
await sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
64
|
+
if (!done) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
stopTyping();
|
|
68
|
+
const routeResult = getLastRouteResult();
|
|
69
|
+
const indicator = routeResult && routeResult.routerMode === "auto"
|
|
70
|
+
? {
|
|
71
|
+
markdownSuffix: `\n\n_⚡ auto · ${routeResult.model}_`,
|
|
72
|
+
plainSuffix: `\n\n⚡ auto · ${routeResult.model}`,
|
|
73
|
+
}
|
|
74
|
+
: undefined;
|
|
75
|
+
void sendTelegramResponse(ctx, text, replyParams, indicator);
|
|
76
|
+
}, options);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
stopTyping();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function activeModelSupportsVision() {
|
|
83
|
+
try {
|
|
84
|
+
const { getClient } = await import("../copilot/client.js");
|
|
85
|
+
const client = await getClient();
|
|
86
|
+
const models = await client.listModels();
|
|
87
|
+
return models.find((model) => model.id === config.copilotModel)?.capabilities.supports.vision;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function sanitizeFileName(fileName) {
|
|
94
|
+
return fileName.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
95
|
+
}
|
|
96
|
+
async function downloadTelegramFile(ctx, fileId, preferredName) {
|
|
97
|
+
const file = await ctx.api.getFile(fileId);
|
|
98
|
+
if (!file.file_path) {
|
|
99
|
+
throw new Error("Telegram did not provide a downloadable file path.");
|
|
100
|
+
}
|
|
101
|
+
const tempDir = await mkdtemp(join(tmpdir(), "max-telegram-"));
|
|
102
|
+
const sourceName = preferredName || basename(file.file_path);
|
|
103
|
+
const safeName = sanitizeFileName(sourceName) || `image${extname(file.file_path) || ".jpg"}`;
|
|
104
|
+
const filePath = join(tempDir, safeName);
|
|
105
|
+
const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${file.file_path}`;
|
|
106
|
+
const response = await fetch(url);
|
|
107
|
+
if (!response.ok || !response.body) {
|
|
108
|
+
throw new Error(`Telegram file download failed (${response.status} ${response.statusText})`);
|
|
109
|
+
}
|
|
110
|
+
await pipeline(response.body, createWriteStream(filePath));
|
|
111
|
+
return { tempDir, filePath };
|
|
112
|
+
}
|
|
113
|
+
async function removeTempDir(tempDir) {
|
|
114
|
+
if (!tempDir)
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Best-effort cleanup
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function handleTelegramImageMessage(ctx, fileId, prompt, preferredName) {
|
|
124
|
+
const supportsVision = await activeModelSupportsVision();
|
|
125
|
+
if (supportsVision === false) {
|
|
126
|
+
await ctx.reply(`The current model (${config.copilotModel}) can't analyze images. Switch to a vision-capable model with /model and try again.`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let tempDir;
|
|
130
|
+
try {
|
|
131
|
+
const downloaded = await downloadTelegramFile(ctx, fileId, preferredName);
|
|
132
|
+
tempDir = downloaded.tempDir;
|
|
133
|
+
await handleTelegramTurn(ctx, prompt, {
|
|
134
|
+
attachments: [{
|
|
135
|
+
type: "file",
|
|
136
|
+
path: downloaded.filePath,
|
|
137
|
+
displayName: preferredName,
|
|
138
|
+
}],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143
|
+
await ctx.reply(`I couldn't process that image: ${msg}`);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
await removeTempDir(tempDir);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function createBot() {
|
|
150
|
+
if (!config.telegramBotToken) {
|
|
151
|
+
throw new Error("Telegram bot token is missing. Run 'max setup' and enter the bot token from @BotFather.");
|
|
152
|
+
}
|
|
153
|
+
if (config.authorizedUserId === undefined) {
|
|
154
|
+
throw new Error("Telegram user ID is missing. Run 'max setup' and enter your Telegram user ID (get it from @userinfobot).");
|
|
155
|
+
}
|
|
156
|
+
bot = new Bot(config.telegramBotToken);
|
|
157
|
+
// Auth middleware — only allow the authorized user
|
|
158
|
+
bot.use(async (ctx, next) => {
|
|
159
|
+
if (config.authorizedUserId !== undefined && ctx.from?.id !== config.authorizedUserId) {
|
|
160
|
+
return; // Silently ignore unauthorized users
|
|
161
|
+
}
|
|
162
|
+
await next();
|
|
163
|
+
});
|
|
164
|
+
// /start and /help
|
|
165
|
+
bot.command("start", (ctx) => ctx.reply("Max is online. Send me anything."));
|
|
166
|
+
bot.command("help", (ctx) => ctx.reply("I'm Max, your AI daemon.\n\n" +
|
|
167
|
+
"Just send me a message and I'll handle it.\n\n" +
|
|
168
|
+
"Commands:\n" +
|
|
169
|
+
"/cancel — Cancel the current message\n" +
|
|
170
|
+
"/model — Show current model\n" +
|
|
171
|
+
"/model <name> — Switch model\n" +
|
|
172
|
+
"/auto — Toggle auto model routing\n" +
|
|
173
|
+
"/memory — Show stored memories\n" +
|
|
174
|
+
"/skills — List installed skills\n" +
|
|
175
|
+
"/workers — List active worker sessions\n" +
|
|
176
|
+
"/restart — Restart Max\n" +
|
|
177
|
+
"/help — Show this help"));
|
|
178
|
+
bot.command("cancel", async (ctx) => {
|
|
179
|
+
const cancelled = await cancelCurrentMessage();
|
|
180
|
+
await ctx.reply(cancelled ? "⛔ Cancelled." : "Nothing to cancel.");
|
|
181
|
+
});
|
|
182
|
+
bot.command("model", async (ctx) => {
|
|
183
|
+
const arg = ctx.match?.trim();
|
|
184
|
+
if (arg) {
|
|
185
|
+
// Validate against available models before persisting
|
|
186
|
+
try {
|
|
187
|
+
const { getClient } = await import("../copilot/client.js");
|
|
188
|
+
const client = await getClient();
|
|
189
|
+
const models = await client.listModels();
|
|
190
|
+
const match = models.find((m) => m.id === arg);
|
|
191
|
+
if (!match) {
|
|
192
|
+
const suggestions = models
|
|
193
|
+
.filter((m) => m.id.includes(arg) || m.id.toLowerCase().includes(arg.toLowerCase()))
|
|
194
|
+
.map((m) => m.id);
|
|
195
|
+
const hint = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
|
|
196
|
+
await ctx.reply(`Model '${arg}' not found.${hint}`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// If validation fails (client not ready), allow the switch — will fail on next message if wrong
|
|
202
|
+
}
|
|
203
|
+
const previous = config.copilotModel;
|
|
204
|
+
config.copilotModel = arg;
|
|
205
|
+
persistModel(arg);
|
|
206
|
+
invalidateSession();
|
|
207
|
+
await ctx.reply(`Model: ${previous} → ${arg}`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
await ctx.reply(`Current model: ${config.copilotModel}`);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
bot.command("memory", async (ctx) => {
|
|
214
|
+
const memories = searchMemories(undefined, undefined, 50);
|
|
215
|
+
if (memories.length === 0) {
|
|
216
|
+
await ctx.reply("No memories stored.");
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const lines = memories.map((m) => `#${m.id} [${m.category}] ${m.content}`);
|
|
220
|
+
await ctx.reply(lines.join("\n") + `\n\n${memories.length} total`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
bot.command("skills", async (ctx) => {
|
|
224
|
+
const skills = listSkills();
|
|
225
|
+
if (skills.length === 0) {
|
|
226
|
+
await ctx.reply("No skills installed.");
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
const lines = skills.map((s) => `• ${s.name} (${s.source}) — ${s.description}`);
|
|
230
|
+
await ctx.reply(lines.join("\n"));
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
bot.command("workers", async (ctx) => {
|
|
234
|
+
const workers = Array.from(getWorkers().values());
|
|
235
|
+
if (workers.length === 0) {
|
|
236
|
+
await ctx.reply("No active worker sessions.");
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
const lines = workers.map((w) => `• ${w.name} (${w.workingDir}) — ${w.status}`);
|
|
240
|
+
await ctx.reply(lines.join("\n"));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
bot.command("restart", async (ctx) => {
|
|
244
|
+
await ctx.reply("⏳ Restarting Max...");
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
restartDaemon().catch((err) => {
|
|
247
|
+
console.error("[max] Restart failed:", err);
|
|
248
|
+
});
|
|
249
|
+
}, 500);
|
|
250
|
+
});
|
|
251
|
+
bot.command("auto", async (ctx) => {
|
|
252
|
+
const current = getRouterConfig();
|
|
253
|
+
const newState = !current.enabled;
|
|
254
|
+
updateRouterConfig({ enabled: newState });
|
|
255
|
+
const label = newState
|
|
256
|
+
? "⚡ Auto mode on"
|
|
257
|
+
: `Auto mode off · using ${config.copilotModel}`;
|
|
258
|
+
await ctx.reply(label);
|
|
259
|
+
});
|
|
260
|
+
// Handle all text messages
|
|
261
|
+
bot.on("message:text", async (ctx) => {
|
|
262
|
+
await handleTelegramTurn(ctx, ctx.message.text);
|
|
263
|
+
});
|
|
264
|
+
bot.on("message:photo", async (ctx) => {
|
|
265
|
+
const photo = ctx.message.photo.at(-1);
|
|
266
|
+
if (!photo) {
|
|
267
|
+
await ctx.reply("I couldn't find a usable photo in that message.");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const prompt = ctx.message.caption?.trim() || "Please analyze the attached image.";
|
|
271
|
+
await handleTelegramImageMessage(ctx, photo.file_id, prompt, `telegram-photo-${photo.file_unique_id}.jpg`);
|
|
272
|
+
});
|
|
273
|
+
bot.on("message:document", async (ctx) => {
|
|
274
|
+
const document = ctx.message.document;
|
|
275
|
+
if (!document.mime_type?.startsWith("image/")) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const extension = extname(document.file_name || "") || ".png";
|
|
279
|
+
const prompt = ctx.message.caption?.trim() || "Please analyze the attached image.";
|
|
280
|
+
await handleTelegramImageMessage(ctx, document.file_id, prompt, document.file_name || `telegram-image-${document.file_unique_id}${extension}`);
|
|
281
|
+
});
|
|
282
|
+
return bot;
|
|
283
|
+
}
|
|
284
|
+
export async function startBot() {
|
|
285
|
+
if (!bot)
|
|
286
|
+
throw new Error("Bot not created");
|
|
287
|
+
console.log("[max] Telegram bot starting...");
|
|
288
|
+
bot.start({
|
|
289
|
+
onStart: () => console.log("[max] Telegram bot connected"),
|
|
290
|
+
}).catch((err) => {
|
|
291
|
+
if (err?.error_code === 401) {
|
|
292
|
+
console.error("[max] ⚠️ Telegram bot token is invalid or expired. Run 'max setup' and re-enter your bot token from @BotFather.");
|
|
293
|
+
}
|
|
294
|
+
else if (err?.error_code === 409) {
|
|
295
|
+
console.error("[max] ⚠️ Another bot instance is already running with this token. Stop the other instance first.");
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
console.error("[max] ❌ Telegram bot failed to start:", err?.message || err);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
export async function stopBot() {
|
|
303
|
+
if (bot) {
|
|
304
|
+
await bot.stop();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Send an unsolicited message to the authorized user (for background task completions). */
|
|
308
|
+
export async function sendProactiveMessage(text) {
|
|
309
|
+
if (!bot || config.authorizedUserId === undefined)
|
|
310
|
+
return;
|
|
311
|
+
const formatted = toTelegramMarkdown(text);
|
|
312
|
+
const chunks = chunkMessage(formatted);
|
|
313
|
+
const fallbackChunks = chunkMessage(text);
|
|
314
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
315
|
+
try {
|
|
316
|
+
await bot.api.sendMessage(config.authorizedUserId, chunks[i], { parse_mode: "MarkdownV2" });
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
try {
|
|
320
|
+
await bot.api.sendMessage(config.authorizedUserId, fallbackChunks[i] ?? chunks[i]);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Bot may not be connected yet
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** Send a photo to the authorized user. Accepts a file path or URL. */
|
|
329
|
+
export async function sendPhoto(photo, caption) {
|
|
330
|
+
if (!bot || config.authorizedUserId === undefined)
|
|
331
|
+
return;
|
|
332
|
+
try {
|
|
333
|
+
const { InputFile } = await import("grammy");
|
|
334
|
+
const input = photo.startsWith("http") ? photo : new InputFile(photo);
|
|
335
|
+
await bot.api.sendPhoto(config.authorizedUserId, input, {
|
|
336
|
+
caption,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
console.error("[max] Failed to send photo:", err instanceof Error ? err.message : err);
|
|
341
|
+
throw err;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
//# sourceMappingURL=bot.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const TELEGRAM_MAX_LENGTH = 4096;
|
|
2
|
+
/**
|
|
3
|
+
* Split a long message into chunks that fit within Telegram's message limit.
|
|
4
|
+
* Tries to split at newlines, then spaces, falling back to hard cuts.
|
|
5
|
+
*/
|
|
6
|
+
export function chunkMessage(text) {
|
|
7
|
+
if (text.length <= TELEGRAM_MAX_LENGTH) {
|
|
8
|
+
return [text];
|
|
9
|
+
}
|
|
10
|
+
const chunks = [];
|
|
11
|
+
let remaining = text;
|
|
12
|
+
while (remaining.length > 0) {
|
|
13
|
+
if (remaining.length <= TELEGRAM_MAX_LENGTH) {
|
|
14
|
+
chunks.push(remaining);
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
let splitAt = remaining.lastIndexOf("\n", TELEGRAM_MAX_LENGTH);
|
|
18
|
+
if (splitAt < TELEGRAM_MAX_LENGTH * 0.3) {
|
|
19
|
+
splitAt = remaining.lastIndexOf(" ", TELEGRAM_MAX_LENGTH);
|
|
20
|
+
}
|
|
21
|
+
if (splitAt < TELEGRAM_MAX_LENGTH * 0.3) {
|
|
22
|
+
splitAt = TELEGRAM_MAX_LENGTH;
|
|
23
|
+
}
|
|
24
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
25
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
26
|
+
}
|
|
27
|
+
return chunks;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Escape special characters for Telegram MarkdownV2 plain text segments.
|
|
31
|
+
*/
|
|
32
|
+
function escapeSegment(text) {
|
|
33
|
+
return text.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Convert a markdown table into a readable mobile-friendly list.
|
|
37
|
+
* Returns already-escaped MarkdownV2 text ready to be stashed.
|
|
38
|
+
* *Casa del Poeta* — $383 · ⭐ 4.89
|
|
39
|
+
*/
|
|
40
|
+
function convertTable(table) {
|
|
41
|
+
const rows = table.trim().split("\n").filter(row => !/^\|[-| :]+\|$/.test(row.trim()));
|
|
42
|
+
const parsed = rows.map(row => row.split("|").map(c => c.trim()).filter(Boolean));
|
|
43
|
+
if (parsed.length === 0)
|
|
44
|
+
return "";
|
|
45
|
+
// Skip header row, format data rows as: *first col* — rest · rest
|
|
46
|
+
const dataRows = parsed.length > 1 ? parsed.slice(1) : parsed;
|
|
47
|
+
return dataRows.map(cols => {
|
|
48
|
+
if (cols.length === 0)
|
|
49
|
+
return "";
|
|
50
|
+
const first = `*${escapeSegment(cols[0])}*`;
|
|
51
|
+
const rest = cols.slice(1).map(c => escapeSegment(c)).join(" · ");
|
|
52
|
+
return rest ? `${first} — ${rest}` : first;
|
|
53
|
+
}).join("\n");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Convert standard markdown from the AI into Telegram MarkdownV2.
|
|
57
|
+
* Handles bold, italic, code blocks, headers, tables, and horizontal rules.
|
|
58
|
+
*/
|
|
59
|
+
export function toTelegramMarkdown(text) {
|
|
60
|
+
// 1. Stash code blocks (protect from processing)
|
|
61
|
+
const stash = [];
|
|
62
|
+
const stashToken = (s) => { stash.push(s); return `\x00STASH${stash.length - 1}\x00`; };
|
|
63
|
+
let out = text;
|
|
64
|
+
// Stash fenced code blocks
|
|
65
|
+
out = out.replace(/```([a-z]*)\n?([\s\S]*?)```/g, (_m, lang, code) => stashToken("```" + (lang || "") + "\n" + code.trim() + "\n```"));
|
|
66
|
+
// Stash inline code
|
|
67
|
+
out = out.replace(/`([^`\n]+)`/g, (_m, code) => stashToken("`" + code + "`"));
|
|
68
|
+
// 2. Convert tables before any escaping — stash result to avoid double-escaping
|
|
69
|
+
out = out.replace(/(?:^\|.+\|[ \t]*$\n?)+/gm, (table) => stashToken(convertTable(table) + "\n"));
|
|
70
|
+
// 3. Convert headers → bold
|
|
71
|
+
out = out.replace(/^#{1,6}\s+(.+)$/gm, (_m, title) => `**${title.trim()}**`);
|
|
72
|
+
// 4. Remove horizontal rules
|
|
73
|
+
out = out.replace(/^[-*_]{3,}\s*$/gm, "");
|
|
74
|
+
// 5. Extract bold/italic markers before escaping
|
|
75
|
+
const boldParts = [];
|
|
76
|
+
out = out.replace(/\*\*(.+?)\*\*/g, (_m, inner) => {
|
|
77
|
+
boldParts.push(inner);
|
|
78
|
+
return `\x00BOLD${boldParts.length - 1}\x00`;
|
|
79
|
+
});
|
|
80
|
+
const italicParts = [];
|
|
81
|
+
out = out.replace(/\*(.+?)\*/g, (_m, inner) => {
|
|
82
|
+
italicParts.push(inner);
|
|
83
|
+
return `\x00ITALIC${italicParts.length - 1}\x00`;
|
|
84
|
+
});
|
|
85
|
+
// 6. Escape everything that remains
|
|
86
|
+
out = escapeSegment(out);
|
|
87
|
+
// 7. Restore bold and italic with escaped inner text
|
|
88
|
+
out = out.replace(/\x00BOLD(\d+)\x00/g, (_m, i) => `*${escapeSegment(boldParts[+i])}*`);
|
|
89
|
+
out = out.replace(/\x00ITALIC(\d+)\x00/g, (_m, i) => `_${escapeSegment(italicParts[+i])}_`);
|
|
90
|
+
// 8. Restore stashed code blocks/inline code
|
|
91
|
+
out = out.replace(/\x00STASH(\d+)\x00/g, (_m, i) => stash[+i]);
|
|
92
|
+
// 9. Clean up excessive blank lines
|
|
93
|
+
out = out.replace(/\n{3,}/g, "\n\n");
|
|
94
|
+
return out.trim();
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=formatter.js.map
|