From 072998a6ab7241d130585398a26224c3c04ca486 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 25 Nov 2025 05:49:18 +0100 Subject: [PATCH] refactor: extract MEDIA parsing helper and tidy whitespace --- src/auto-reply/reply.ts | 46 +++------------------------------ src/index.core.test.ts | 9 +++++++ src/media/parse.ts | 57 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 42 deletions(-) create mode 100644 src/media/parse.ts diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index b9c28b58a..ab4ce1e35 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -25,16 +25,12 @@ import type { TwilioRequester } from "../twilio/types.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { logError } from "../logger.js"; import { ensureMediaHosted } from "../media/host.js"; +import { normalizeMediaSource, splitMediaFromOutput } from "../media/parse.js"; type GetReplyOptions = { onReplyStart?: () => Promise | void; }; -function normalizeMediaSource(src: string) { - if (src.startsWith("file://")) return src.replace("file://", ""); - return src; -} - function summarizeClaudeMetadata(payload: unknown): string | undefined { if (!payload || typeof payload !== "object") return undefined; const obj = payload as Record; @@ -293,43 +289,9 @@ const mediaNote = }, ); const rawStdout = stdout.trim(); - let trimmed = rawStdout; - let mediaFromCommand: string | undefined; - const mediaLine = rawStdout - .split("\n") - .find((line) => /\bMEDIA:/i.test(line)); - if (mediaLine) { - let isValidMedia = false; - const mediaMatch = mediaLine.match(/\bMEDIA:\s*([^\s]+)/i); - if (mediaMatch?.[1]) { - const candidate = normalizeMediaSource(mediaMatch[1]); - const looksLikeUrl = /^https?:\/\//i.test(candidate); - const looksLikePath = - candidate.startsWith("/") || candidate.startsWith("./"); - const hasWhitespace = /\s/.test(candidate); - isValidMedia = - !hasWhitespace && - candidate.length <= 1024 && - (looksLikeUrl || looksLikePath); - if (isValidMedia) mediaFromCommand = candidate; - } - if (isValidMedia && mediaMatch?.[0]) { - trimmed = rawStdout - .replace(mediaMatch[0], "") - .replace(/\s{2,}/g, " ") - .replace(/\s+\n/g, "\n") - .replace(/\n{3,}/g, "\n\n") - .trim(); - } else { - trimmed = rawStdout - .split("\n") - .filter((line) => line !== mediaLine) - .join("\n") - .replace(/\n\s+/g, "\n") - .replace(/\n{3,}/g, "\n\n") - .trim(); - } - } + const { text: trimmedText, mediaUrl: mediaFromCommand } = + splitMediaFromOutput(rawStdout); + let trimmed = trimmedText; if (stderr?.trim()) { logVerbose(`Command auto-reply stderr: ${stderr.trim()}`); } diff --git a/src/index.core.test.ts b/src/index.core.test.ts index 841a1e8f7..f68b86155 100644 --- a/src/index.core.test.ts +++ b/src/index.core.test.ts @@ -17,6 +17,7 @@ type TwilioFactoryMock = ReturnType["factory"]; const twilioFactory = (await import("twilio")).default as TwilioFactoryMock; import * as index from "./index.js"; +import { splitMediaFromOutput } from "./media/parse.js"; const envBackup = { ...process.env } as Record; @@ -223,6 +224,14 @@ describe("config and templating", () => { expect(result?.mediaUrl).toBeUndefined(); }); + it("splitMediaFromOutput strips media token and preserves text", () => { + const { text, mediaUrl } = splitMediaFromOutput( + "line1\nMEDIA:https://x/y.png\nline2", + ); + expect(mediaUrl).toBe("https://x/y.png"); + expect(text).toBe("line1\nline2"); + }); + it("getReplyFromConfig runs command and manages session store", async () => { const tmpStore = path.join(os.tmpdir(), `warelay-store-${Date.now()}.json`); vi.spyOn(crypto, "randomUUID").mockReturnValue("session-123"); diff --git a/src/media/parse.ts b/src/media/parse.ts new file mode 100644 index 000000000..d2bfe8664 --- /dev/null +++ b/src/media/parse.ts @@ -0,0 +1,57 @@ +// Shared helpers for parsing MEDIA tokens from command/stdout text. + +export const MEDIA_LINE_RE = /\bMEDIA:/i; +export const MEDIA_TOKEN_RE = /\bMEDIA:\s*([^\s]+)/i; + +export function normalizeMediaSource(src: string) { + if (src.startsWith("file://")) return src.replace("file://", ""); + return src; +} + +export function splitMediaFromOutput(raw: string): { + text: string; + mediaUrl?: string; +} { + const trimmedRaw = raw.trim(); + let text = trimmedRaw; + let mediaUrl: string | undefined; + + const mediaLine = trimmedRaw.split("\n").find((line) => MEDIA_LINE_RE.test(line)); + if (!mediaLine) { + return { text: trimmedRaw }; + } + + let isValidMedia = false; + const mediaMatch = mediaLine.match(MEDIA_TOKEN_RE); + if (mediaMatch?.[1]) { + const candidate = normalizeMediaSource(mediaMatch[1]); + const looksLikeUrl = /^https?:\/\//i.test(candidate); + const looksLikePath = candidate.startsWith("/") || candidate.startsWith("./"); + const hasWhitespace = /\s/.test(candidate); + isValidMedia = + !hasWhitespace && candidate.length <= 1024 && (looksLikeUrl || looksLikePath); + if (isValidMedia) { + mediaUrl = candidate; + } + } + + if (isValidMedia && mediaMatch?.[0]) { + text = trimmedRaw + .replace(mediaMatch[0], "") + .replace(/[ \t]{2,}/g, " ") + .replace(/[ \t]+\n/g, "\n") + .replace(/\n{2,}/g, "\n") + .trim(); + } else { + text = trimmedRaw + .split("\n") + .filter((line) => line !== mediaLine) + .join("\n") + .replace(/[ \t]{2,}/g, " ") + .replace(/[ \t]+\n/g, "\n") + .replace(/\n{2,}/g, "\n") + .trim(); + } + + return { text, mediaUrl }; +}