/** * message-filter.ts — outgoing message sanitization * * 1. Brand replace: OpenClaw → Titan, openclaw emoji → 🐶 * 2. Path scrub: strip /home/openclaw/ prefix from tool content * 3. Smart truncation: single truncator * - SQL results: row count summary + first N lines * - Other: first 400 chars + …[N more chars] */ // ── 1. Brand replace ──────────────────────────────────────────────────────── const OPENCLAW_EMOJIS = /🐾|šŸ¦ž|šŸ¤–|šŸ¦…/gu; const OPENCLAW_NAME = /\bOpenClaw\b/g; function brandReplace(str: string): string { return str.replace(OPENCLAW_EMOJIS, '🐶').replace(OPENCLAW_NAME, 'Titan'); } // ── 2. Path scrub ──────────────────────────────────────────────────────────── const PATH_ABS = /\/home\/openclaw\//g; const PATH_TILDE = /~\/\.openclaw\//g; const PATH_DOT_CLAW = /\.openclaw\//g; function scrubPaths(str: string): string { return str.replace(PATH_ABS, '').replace(PATH_TILDE, '').replace(PATH_DOT_CLAW, ''); } // ── 3. Smart truncation ────────────────────────────────────────────────────── const SOFT_LIMIT = 80; const SQL_ROWS = 8; function looksLikeSqlResult(str: string): boolean { const lines = str.split('\n').filter(l => l.trim()); if (lines.length < 2) return false; const tabLines = lines.filter(l => l.includes('\t') || (l.match(/\|/g) || []).length >= 2); return tabLines.length > lines.length * 0.5; } function truncateSql(str: string): string { const lines = str.split('\n').filter(l => l.trim()); const total = lines.length; if (total <= SQL_ROWS + 1) return str; return lines.slice(0, SQL_ROWS).join('\n') + `\n… [${total - SQL_ROWS} more rows]`; } function smartTruncate(str: unknown): string { if (typeof str !== 'string') { try { str = JSON.stringify(str); } catch (_) { return String(str); } } const s = str as string; if (s.length <= SOFT_LIMIT) return s; if (looksLikeSqlResult(s)) return truncateSql(s); return s.slice(0, 40) + '…' + s.slice(-40); } // ── Public API ─────────────────────────────────────────────────────────────── export function filterText(str: string | null | undefined): string | null | undefined { if (str == null) return str; return scrubPaths(brandReplace(String(str))); } function extractSql(cmd: string): string | null { const m = cmd.match(/(?:mysql|mariadb)\b[^"']*-e\s+["']([^"']+)["']/s); if (m) return m[1].trim().replace(/\\`/g, '').replace(/`/g, ''); return null; } function prettifyArgs(val: unknown): string { if (typeof val === 'string') { try { val = JSON.parse(val); } catch (_) { return val; } } if (val && typeof val === 'object') { const obj = val as Record; if (typeof obj.command === 'string') { return extractSql(obj.command) || obj.command; } const keys = Object.keys(obj); if (keys.length === 1 && typeof obj[keys[0]] === 'string') return obj[keys[0]] as string; try { return JSON.stringify(val); } catch (_) { return '[non-serializable]'; } } return String(val); } export function filterValue(val: unknown): string | null | undefined { if (val == null) return val as null | undefined; return smartTruncate(scrubPaths(brandReplace(prettifyArgs(val)))); } export { brandReplace, scrubPaths, smartTruncate };