import type { CharlesBody, CharlesEntry, SimplifiedEntry } from "./types.js"; /** Shannon entropy in bits/char. */ export function calcEntropy(s: string): number { if (!s || s.length < 5) return 0; const n = s.length; const counts = new Map(); for (const ch of s) { counts.set(ch, (counts.get(ch) ?? 0) + 1); } let entropy = 0; for (const c of counts.values()) { const p = c / n; entropy -= p * Math.log2(p); } return entropy; } /** Where a keyword hits inside an entry. */ export function getHitLocations( entry: CharlesEntry, keyword: string, ): string[] { const k = keyword.toLowerCase(); const req = entry.request ?? {}; const res = entry.response ?? {}; const hits: string[] = []; if ((entry.path ?? "").toLowerCase().includes(k)) hits.push("url_path"); const reqHeaders = req.header?.headers ?? []; if (JSON.stringify(reqHeaders).toLowerCase().includes(k)) hits.push("req_header"); if ((req.body?.text ?? "").toLowerCase().includes(k)) hits.push("req_body"); const resHeaders = res.header?.headers ?? []; if (JSON.stringify(resHeaders).toLowerCase().includes(k)) hits.push("res_header"); if ((res.body?.text ?? "").toLowerCase().includes(k)) hits.push("res_body"); return hits; } const RESOURCE_MIME_PREFIXES = [ "image/", "video/", "audio/", "font/", "application/javascript", "text/javascript", "text/css", "application/octet-stream", "application/wasm", ] as const; function fmtSize(n: number): string { if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)}MB`; if (n >= 1024) return `${(n / 1024).toFixed(1)}KB`; return `${n}B`; } function bodyPreview( body: CharlesBody | undefined, mimeType: string, bodySize: number, previewChars: number, ): string | null { if (!body) return null; const encoded = body.encoded ?? false; const text = body.text ?? ""; const mime = (mimeType || "").toLowerCase().split(";")[0]?.trim() ?? ""; const isResource = encoded || RESOURCE_MIME_PREFIXES.some((p) => mime.startsWith(p)); if (isResource) { const size = bodySize || text.length; const label = mime || "binary"; return `[${label} ${fmtSize(size)}]`; } return text.slice(0, previewChars) || null; } /** Compact view for agent decision-making. */ export function simplifyEntry( entry: CharlesEntry, previewChars = 300, ): SimplifiedEntry { const req = entry.request ?? {}; const res = entry.response ?? {}; const reqBody = req.body ?? {}; const resBody = res.body ?? {}; return { id: entry._mcp_id ?? entry.id, method: entry.method, host: entry.host, path: entry.path, status: res.status, req_preview: bodyPreview( reqBody, req.mimeType ?? "", req.sizes?.body ?? 0, previewChars, ), res_preview: bodyPreview( resBody, res.mimeType ?? "", res.sizes?.body ?? 0, previewChars, ), }; } export function entryStartTime(entry: CharlesEntry): string { return entry.times?.start ?? ""; } /** Parse Charles times.start (ISO 8601) to epoch millis. */ export function entryMillis(entry: CharlesEntry): number { const iso = entryStartTime(entry); if (!iso) return 0; const ms = Date.parse(iso); return Number.isFinite(ms) ? ms : 0; }