UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

208 lines • 8.74 kB
const TELEGRAM_SPECIAL = /[_*[\]()~`>#+\-=|{}.!\\]/g; /** Escape text for Telegram's MarkdownV2 — every character it reserves gets a backslash. */ export function escapeTelegramMarkdown(value) { return value.replace(TELEGRAM_SPECIAL, '\\$&'); } /** Undo {@link escapeTelegramMarkdown}: what a message reads as plain text when MarkdownV2 is refused. */ export function unescapeTelegramMarkdown(value) { return value.replace(/\\([_*[\]()~`>#+\-=|{}.!\\])/g, '$1'); } const linkText = (label, url) => label === '' || label === url ? url : `${label} (${url})`; const DIALECTS = { whatsapp: { text: (value) => value, bold: (inner) => `*${inner}*`, italic: (inner) => `_${inner}_`, strike: (inner) => `~${inner}~`, code: (value) => `\`${value}\``, pre: (_language, value) => `\`\`\`${value}\`\`\``, link: linkText, heading: (inner) => `*${inner}*`, bullet: '- ', quote: (inner) => `> ${inner}`, }, telegram: { text: escapeTelegramMarkdown, bold: (inner) => `*${inner}*`, italic: (inner) => `_${inner}_`, strike: (inner) => `~${inner}~`, code: (value) => `\`${value.replace(/[`\\]/g, '\\$&')}\``, pre: (language, value) => `\`\`\`${language}\n${value.replace(/[`\\]/g, '\\$&')}\`\`\``, link: (label, url) => `[${escapeTelegramMarkdown(label === '' ? url : label)}](${url.replace(/[)\\]/g, '\\$&')})`, heading: (inner) => `*${inner}*`, bullet: '• ', quote: (inner) => `>${inner}`, }, none: { text: (value) => value, bold: (inner) => inner, italic: (inner) => inner, strike: (inner) => inner, code: (value) => value, pre: (_language, value) => value, link: linkText, heading: (inner) => inner, bullet: '- ', quote: (inner) => `> ${inner}`, }, }; /** * Inline tokens, earliest first: code, image/link, `**bold**`, `__bold__`, `~~strike~~`, then the * single-character forms. A single marker must hug its text (`*a*`, not `* a *`) and must not sit * inside a word, so `snake_case` and `2*3*4` stay as written. */ const INLINE = /`([^`\n]+)`|!?\[([^\]\n]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|\*\*(?!\s)(.+?)(?<!\s)\*\*|__(?!\s)(.+?)(?<!\s)__|~~(?!\s)(.+?)(?<!\s)~~|(?<![\w*\\])\*(?![\s*])([^*\n]+?)(?<![\s\\])\*(?![\w*])|(?<![\w_\\])_(?![\s_])([^_\n]+?)(?<![\s\\])_(?![\w_])|(?<![\w~\\])~(?![\s~])([^~\n]+?)(?<![\s\\])~(?![\w~])/g; function inline(source, dialect) { let out = ''; let last = 0; for (const match of source.matchAll(INLINE)) { const index = match.index ?? 0; out += dialect.text(source.slice(last, index)); last = index + match[0].length; const [, code, label, url, bold, underscoreBold, strike, starBold, italic, tildeStrike] = match; if (code !== undefined) out += dialect.code(code); else if (url !== undefined) out += dialect.link(label ?? '', url); else if (bold !== undefined) out += dialect.bold(inline(bold, dialect)); else if (underscoreBold !== undefined) out += dialect.bold(inline(underscoreBold, dialect)); else if (strike !== undefined) out += dialect.strike(inline(strike, dialect)); else if (starBold !== undefined) out += dialect.bold(inline(starBold, dialect)); else if (italic !== undefined) out += dialect.italic(inline(italic, dialect)); else if (tildeStrike !== undefined) out += dialect.strike(inline(tildeStrike, dialect)); } return out + dialect.text(source.slice(last)); } function line(source, dialect) { const heading = /^\s{0,3}#{1,6}\s+(.*?)(?:\s+#+)?\s*$/.exec(source); if (heading) { // A heading is bold already; markers inside it would nest bold in bold. return dialect.heading(inline((heading[1] ?? '').replace(/\*\*|__/g, ''), dialect)); } if (/^\s{0,3}([-*_])(\s*\1){2,}\s*$/.test(source)) return ''; const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(source); if (bullet) return `${bullet[1] ?? ''}${dialect.bullet}${inline(bullet[2] ?? '', dialect)}`; const quote = /^\s{0,3}>\s?(.*)$/.exec(source); if (quote) return dialect.quote(inline(quote[1] ?? '', dialect)); return inline(source, dialect); } /** A table row: `| a | b |` (outer pipes optional, at least one inner pipe). */ const TABLE_ROW = /^\s{0,3}\|?.*\|.*\|?\s*$/; /** The row under a table's header: `|---|:--:|`. */ const TABLE_DIVIDER = /^\s{0,3}\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)+\|?\s*$/; function cells(row) { return row .trim() .replace(/^\|/, '') .replace(/(?<!\\)\|$/, '') .split(/(?<!\\)\|/) .map((cell) => cell.trim().replace(/\\\|/g, '|')); } /** * One table row as a bullet — no chat channel draws tables, and the raw pipes arrive as noise. The * first cell leads in bold; with two columns the second follows it, with more each cell is named * by its header (`*Ana* — Idade: 3; Peso: 14 kg`). Empty cells are left out. */ function tableRow(row, header, dialect) { const [first = '', ...rest] = row.map((cell) => inline(cell, dialect)); const lead = first === '' ? '' : dialect.bold(first); const values = rest.flatMap((cell, index) => { if (cell === '') return []; const name = header.length > 2 ? (header[index + 1] ?? '') : ''; return [name === '' ? cell : `${inline(name, dialect)}: ${cell}`]; }); const separator = header.length > 2 ? ' — ' : ': '; const body = values.join('; '); return dialect.bullet + (lead && body ? `${lead}${separator}${body}` : lead || body); } /** * Cut `text` around its GitHub tables (header, divider, rows), outside fenced code. Text pieces * keep their line breaks; the pieces joined back with `\n` are the input. */ export function splitTables(text) { const rows = text.split('\n'); const pieces = []; let prose = []; let fenced = false; const flushProse = () => { if (prose.length > 0) pieces.push({ kind: 'text', text: prose.join('\n') }); prose = []; }; for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; if (/^\s{0,3}```/.test(row)) fenced = !fenced; const divider = rows[index + 1]; if (!fenced && TABLE_ROW.test(row) && divider !== undefined && TABLE_DIVIDER.test(divider)) { const start = index; const body = []; index += 2; while (index < rows.length && TABLE_ROW.test(rows[index])) { body.push(cells(rows[index])); index += 1; } flushProse(); pieces.push({ kind: 'table', table: { header: cells(row), rows: body }, source: rows.slice(start, index).join('\n'), }); index -= 1; continue; } prose.push(row); } flushProse(); return pieces; } /** Every line through {@link line}, except GitHub tables, which become bullet lists. */ function withoutTables(rows, dialect) { const out = []; for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const divider = rows[index + 1]; if (TABLE_ROW.test(row) && divider !== undefined && TABLE_DIVIDER.test(divider)) { const header = cells(row); index += 2; while (index < rows.length && TABLE_ROW.test(rows[index])) { out.push(tableRow(cells(rows[index]), header, dialect)); index += 1; } index -= 1; continue; } out.push(line(row, dialect)); } return out; } /** * Convert a turn's markdown to what `markdown` renders: WhatsApp's `*bold*` `_italic_` `~strike~`, * Telegram's MarkdownV2 (every reserved character escaped), or plain text. Links become * `label (url)` where the channel has no link syntax; headings become bold lines. */ export function toChannelMarkdown(text, markdown) { const dialect = DIALECTS[markdown]; const fence = /```([\w+-]*)[^\S\n]*\n?([\s\S]*?)\n?```/g; let out = ''; let last = 0; const prose = (source) => withoutTables(source.split('\n'), dialect).join('\n'); for (const match of text.matchAll(fence)) { const index = match.index ?? 0; out += prose(text.slice(last, index)); out += dialect.pre(match[1] ?? '', match[2] ?? ''); last = index + match[0].length; } return out + prose(text.slice(last)); } //# sourceMappingURL=markdown.js.map