Skip to content

Commit 072998a

Browse files
committed
refactor: extract MEDIA parsing helper and tidy whitespace
1 parent bafaed3 commit 072998a

3 files changed

Lines changed: 70 additions & 42 deletions

File tree

src/auto-reply/reply.ts

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,12 @@ import type { TwilioRequester } from "../twilio/types.js";
2525
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
2626
import { logError } from "../logger.js";
2727
import { ensureMediaHosted } from "../media/host.js";
28+
import { normalizeMediaSource, splitMediaFromOutput } from "../media/parse.js";
2829

2930
type GetReplyOptions = {
3031
onReplyStart?: () => Promise<void> | void;
3132
};
3233

33-
function normalizeMediaSource(src: string) {
34-
if (src.startsWith("file://")) return src.replace("file://", "");
35-
return src;
36-
}
37-
3834
function summarizeClaudeMetadata(payload: unknown): string | undefined {
3935
if (!payload || typeof payload !== "object") return undefined;
4036
const obj = payload as Record<string, unknown>;
@@ -293,43 +289,9 @@ const mediaNote =
293289
},
294290
);
295291
const rawStdout = stdout.trim();
296-
let trimmed = rawStdout;
297-
let mediaFromCommand: string | undefined;
298-
const mediaLine = rawStdout
299-
.split("\n")
300-
.find((line) => /\bMEDIA:/i.test(line));
301-
if (mediaLine) {
302-
let isValidMedia = false;
303-
const mediaMatch = mediaLine.match(/\bMEDIA:\s*([^\s]+)/i);
304-
if (mediaMatch?.[1]) {
305-
const candidate = normalizeMediaSource(mediaMatch[1]);
306-
const looksLikeUrl = /^https?:\/\//i.test(candidate);
307-
const looksLikePath =
308-
candidate.startsWith("/") || candidate.startsWith("./");
309-
const hasWhitespace = /\s/.test(candidate);
310-
isValidMedia =
311-
!hasWhitespace &&
312-
candidate.length <= 1024 &&
313-
(looksLikeUrl || looksLikePath);
314-
if (isValidMedia) mediaFromCommand = candidate;
315-
}
316-
if (isValidMedia && mediaMatch?.[0]) {
317-
trimmed = rawStdout
318-
.replace(mediaMatch[0], "")
319-
.replace(/\s{2,}/g, " ")
320-
.replace(/\s+\n/g, "\n")
321-
.replace(/\n{3,}/g, "\n\n")
322-
.trim();
323-
} else {
324-
trimmed = rawStdout
325-
.split("\n")
326-
.filter((line) => line !== mediaLine)
327-
.join("\n")
328-
.replace(/\n\s+/g, "\n")
329-
.replace(/\n{3,}/g, "\n\n")
330-
.trim();
331-
}
332-
}
292+
const { text: trimmedText, mediaUrl: mediaFromCommand } =
293+
splitMediaFromOutput(rawStdout);
294+
let trimmed = trimmedText;
333295
if (stderr?.trim()) {
334296
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`);
335297
}

src/index.core.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type TwilioFactoryMock = ReturnType<typeof createMockTwilio>["factory"];
1717
const twilioFactory = (await import("twilio")).default as TwilioFactoryMock;
1818

1919
import * as index from "./index.js";
20+
import { splitMediaFromOutput } from "./media/parse.js";
2021

2122
const envBackup = { ...process.env } as Record<string, string | undefined>;
2223

@@ -223,6 +224,14 @@ describe("config and templating", () => {
223224
expect(result?.mediaUrl).toBeUndefined();
224225
});
225226

227+
it("splitMediaFromOutput strips media token and preserves text", () => {
228+
const { text, mediaUrl } = splitMediaFromOutput(
229+
"line1\nMEDIA:https://x/y.png\nline2",
230+
);
231+
expect(mediaUrl).toBe("https://x/y.png");
232+
expect(text).toBe("line1\nline2");
233+
});
234+
226235
it("getReplyFromConfig runs command and manages session store", async () => {
227236
const tmpStore = path.join(os.tmpdir(), `warelay-store-${Date.now()}.json`);
228237
vi.spyOn(crypto, "randomUUID").mockReturnValue("session-123");

src/media/parse.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Shared helpers for parsing MEDIA tokens from command/stdout text.
2+
3+
export const MEDIA_LINE_RE = /\bMEDIA:/i;
4+
export const MEDIA_TOKEN_RE = /\bMEDIA:\s*([^\s]+)/i;
5+
6+
export function normalizeMediaSource(src: string) {
7+
if (src.startsWith("file://")) return src.replace("file://", "");
8+
return src;
9+
}
10+
11+
export function splitMediaFromOutput(raw: string): {
12+
text: string;
13+
mediaUrl?: string;
14+
} {
15+
const trimmedRaw = raw.trim();
16+
let text = trimmedRaw;
17+
let mediaUrl: string | undefined;
18+
19+
const mediaLine = trimmedRaw.split("\n").find((line) => MEDIA_LINE_RE.test(line));
20+
if (!mediaLine) {
21+
return { text: trimmedRaw };
22+
}
23+
24+
let isValidMedia = false;
25+
const mediaMatch = mediaLine.match(MEDIA_TOKEN_RE);
26+
if (mediaMatch?.[1]) {
27+
const candidate = normalizeMediaSource(mediaMatch[1]);
28+
const looksLikeUrl = /^https?:\/\//i.test(candidate);
29+
const looksLikePath = candidate.startsWith("/") || candidate.startsWith("./");
30+
const hasWhitespace = /\s/.test(candidate);
31+
isValidMedia =
32+
!hasWhitespace && candidate.length <= 1024 && (looksLikeUrl || looksLikePath);
33+
if (isValidMedia) {
34+
mediaUrl = candidate;
35+
}
36+
}
37+
38+
if (isValidMedia && mediaMatch?.[0]) {
39+
text = trimmedRaw
40+
.replace(mediaMatch[0], "")
41+
.replace(/[ \t]{2,}/g, " ")
42+
.replace(/[ \t]+\n/g, "\n")
43+
.replace(/\n{2,}/g, "\n")
44+
.trim();
45+
} else {
46+
text = trimmedRaw
47+
.split("\n")
48+
.filter((line) => line !== mediaLine)
49+
.join("\n")
50+
.replace(/[ \t]{2,}/g, " ")
51+
.replace(/[ \t]+\n/g, "\n")
52+
.replace(/\n{2,}/g, "\n")
53+
.trim();
54+
}
55+
56+
return { text, mediaUrl };
57+
}

0 commit comments

Comments
 (0)