chore: format to 2-space and bump changelog
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased] 1.0.5
|
## 1.1.0 — 2025-11-25
|
||||||
|
|
||||||
### Pending
|
### Pending
|
||||||
- Web auto-replies now resize/recompress media and honor `inbound.reply.mediaMaxMb` in `~/.warelay/warelay.json` (default 5 MB) to avoid provider/API limits.
|
- Web auto-replies now resize/recompress media and honor `inbound.reply.mediaMaxMb` in `~/.warelay/warelay.json` (default 5 MB) to avoid provider/API limits.
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"$schema": "https://biomejs.dev/schemas/biome.json",
|
"$schema": "https://biomejs.dev/schemas/biome.json",
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"indentWidth": 2
|
"indentWidth": 2,
|
||||||
|
"indentStyle": "space"
|
||||||
},
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ function summarizeClaudeMetadata(payload: unknown): string | undefined {
|
|||||||
export type ReplyPayload = {
|
export type ReplyPayload = {
|
||||||
text?: string;
|
text?: string;
|
||||||
mediaUrl?: string;
|
mediaUrl?: string;
|
||||||
|
mediaUrls?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getReplyFromConfig(
|
export async function getReplyFromConfig(
|
||||||
@@ -423,10 +424,10 @@ export async function getReplyFromConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Run media extraction once on the final human text (post-JSON parse if available).
|
// Run media extraction once on the final human text (post-JSON parse if available).
|
||||||
const { text: cleanedText, mediaUrl: mediaFound } =
|
const { text: cleanedText, mediaUrls: mediaFound } =
|
||||||
splitMediaFromOutput(trimmed);
|
splitMediaFromOutput(trimmed);
|
||||||
trimmed = cleanedText;
|
trimmed = cleanedText;
|
||||||
if (mediaFound) {
|
if (mediaFound?.length) {
|
||||||
mediaFromCommand = mediaFound;
|
mediaFromCommand = mediaFound;
|
||||||
if (isVerbose()) logVerbose(`MEDIA token extracted: ${mediaFound}`);
|
if (isVerbose()) logVerbose(`MEDIA token extracted: ${mediaFound}`);
|
||||||
} else if (isVerbose()) {
|
} else if (isVerbose()) {
|
||||||
@@ -455,10 +456,15 @@ export async function getReplyFromConfig(
|
|||||||
);
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const mediaUrl = mediaFromCommand ?? reply.mediaUrl;
|
const mediaUrls =
|
||||||
|
mediaFromCommand ?? (reply.mediaUrl ? [reply.mediaUrl] : undefined);
|
||||||
const result =
|
const result =
|
||||||
trimmed || mediaUrl
|
trimmed || mediaUrls?.length
|
||||||
? { text: trimmed || undefined, mediaUrl }
|
? {
|
||||||
|
text: trimmed || undefined,
|
||||||
|
mediaUrl: mediaUrls?.[0],
|
||||||
|
mediaUrls,
|
||||||
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
cleanupTyping();
|
cleanupTyping();
|
||||||
return result;
|
return result;
|
||||||
@@ -559,7 +565,13 @@ export async function autoReplyIfConfigured(
|
|||||||
},
|
},
|
||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
if (!replyResult || (!replyResult.text && !replyResult.mediaUrl)) return;
|
if (
|
||||||
|
!replyResult ||
|
||||||
|
(!replyResult.text &&
|
||||||
|
!replyResult.mediaUrl &&
|
||||||
|
!replyResult.mediaUrls?.length)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
const replyFrom = message.to;
|
const replyFrom = message.to;
|
||||||
const replyTo = message.from;
|
const replyTo = message.from;
|
||||||
@@ -583,17 +595,35 @@ export async function autoReplyIfConfigured(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let mediaUrl = replyResult.mediaUrl;
|
const mediaList = replyResult.mediaUrls?.length
|
||||||
if (mediaUrl && !/^https?:\/\//i.test(mediaUrl)) {
|
? replyResult.mediaUrls
|
||||||
const hosted = await ensureMediaHosted(mediaUrl);
|
: replyResult.mediaUrl
|
||||||
mediaUrl = hosted.url;
|
? [replyResult.mediaUrl]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const sendTwilio = async (body: string, media?: string) => {
|
||||||
|
let resolvedMedia = media;
|
||||||
|
if (resolvedMedia && !/^https?:\/\//i.test(resolvedMedia)) {
|
||||||
|
const hosted = await ensureMediaHosted(resolvedMedia);
|
||||||
|
resolvedMedia = hosted.url;
|
||||||
}
|
}
|
||||||
await client.messages.create({
|
await client.messages.create({
|
||||||
from: replyFrom,
|
from: replyFrom,
|
||||||
to: replyTo,
|
to: replyTo,
|
||||||
body: replyResult.text ?? "",
|
body,
|
||||||
...(mediaUrl ? { mediaUrl: [mediaUrl] } : {}),
|
...(resolvedMedia ? { mediaUrl: [resolvedMedia] } : {}),
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mediaList.length === 0) {
|
||||||
|
await sendTwilio(replyResult.text ?? "");
|
||||||
|
} else {
|
||||||
|
// First media with body (if any), then remaining as separate media-only sends.
|
||||||
|
await sendTwilio(replyResult.text ?? "", mediaList[0]);
|
||||||
|
for (const extra of mediaList.slice(1)) {
|
||||||
|
await sendTwilio("", extra);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isVerbose()) {
|
if (isVerbose()) {
|
||||||
console.log(
|
console.log(
|
||||||
info(
|
info(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Shared helpers for parsing MEDIA tokens from command/stdout text.
|
// Shared helpers for parsing MEDIA tokens from command/stdout text.
|
||||||
|
|
||||||
// Allow optional wrapping backticks and punctuation after the token; capture the core token.
|
// Allow optional wrapping backticks and punctuation after the token; capture the core token.
|
||||||
export const MEDIA_TOKEN_RE = /\bMEDIA:\s*`?([^\s`]+)`?/i;
|
export const MEDIA_TOKEN_RE = /\bMEDIA:\s*`?([^\n]+)`?/gi;
|
||||||
|
|
||||||
export function normalizeMediaSource(src: string) {
|
export function normalizeMediaSource(src: string) {
|
||||||
return src.startsWith("file://") ? src.replace("file://", "") : src;
|
return src.startsWith("file://") ? src.replace("file://", "") : src;
|
||||||
@@ -24,30 +24,78 @@ function isValidMedia(candidate: string) {
|
|||||||
|
|
||||||
export function splitMediaFromOutput(raw: string): {
|
export function splitMediaFromOutput(raw: string): {
|
||||||
text: string;
|
text: string;
|
||||||
mediaUrl?: string;
|
mediaUrls?: string[];
|
||||||
|
mediaUrl?: string; // legacy first item for backward compatibility
|
||||||
} {
|
} {
|
||||||
const trimmedRaw = raw.trim();
|
const trimmedRaw = raw.trim();
|
||||||
const match = MEDIA_TOKEN_RE.exec(trimmedRaw);
|
if (!trimmedRaw) return { text: "" };
|
||||||
if (!match?.[1]) return { text: trimmedRaw };
|
|
||||||
|
|
||||||
const candidate = normalizeMediaSource(cleanCandidate(match[1]));
|
const media: string[] = [];
|
||||||
const mediaUrl = isValidMedia(candidate) ? candidate : undefined;
|
let foundMediaToken = false;
|
||||||
|
|
||||||
const cleanedText = mediaUrl
|
// Collect tokens line by line so we can strip them cleanly.
|
||||||
? trimmedRaw
|
const lines = trimmedRaw.split("\n");
|
||||||
.replace(match[0], "")
|
const keptLines: string[] = [];
|
||||||
.replace(/[ \t]+\n/g, "\n")
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const matches = Array.from(line.matchAll(MEDIA_TOKEN_RE));
|
||||||
|
if (matches.length === 0) {
|
||||||
|
keptLines.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foundMediaToken = true;
|
||||||
|
const pieces: string[] = [];
|
||||||
|
let cursor = 0;
|
||||||
|
let hasValidMedia = false;
|
||||||
|
|
||||||
|
for (const match of matches) {
|
||||||
|
const start = match.index ?? 0;
|
||||||
|
pieces.push(line.slice(cursor, start));
|
||||||
|
|
||||||
|
const payload = match[1];
|
||||||
|
const parts = payload.split(/\s+/).filter(Boolean);
|
||||||
|
const invalidParts: string[] = [];
|
||||||
|
for (const part of parts) {
|
||||||
|
const candidate = normalizeMediaSource(cleanCandidate(part));
|
||||||
|
if (isValidMedia(candidate)) {
|
||||||
|
media.push(candidate);
|
||||||
|
hasValidMedia = true;
|
||||||
|
} else {
|
||||||
|
invalidParts.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasValidMedia && invalidParts.length > 0) {
|
||||||
|
pieces.push(invalidParts.join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = start + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
pieces.push(line.slice(cursor));
|
||||||
|
|
||||||
|
const cleanedLine = pieces
|
||||||
|
.join("")
|
||||||
.replace(/[ \t]{2,}/g, " ")
|
.replace(/[ \t]{2,}/g, " ")
|
||||||
.replace(/\n{2,}/g, "\n")
|
.trim();
|
||||||
.trim()
|
|
||||||
: trimmedRaw
|
// If the line becomes empty, drop it.
|
||||||
.split("\n")
|
if (cleanedLine) {
|
||||||
.filter((line) => !MEDIA_TOKEN_RE.test(line))
|
keptLines.push(cleanedLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedText = keptLines
|
||||||
.join("\n")
|
.join("\n")
|
||||||
.replace(/[ \t]+\n/g, "\n")
|
.replace(/[ \t]+\n/g, "\n")
|
||||||
.replace(/[ \t]{2,}/g, " ")
|
.replace(/[ \t]{2,}/g, " ")
|
||||||
.replace(/\n{2,}/g, "\n")
|
.replace(/\n{2,}/g, "\n")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
return mediaUrl ? { text: cleanedText, mediaUrl } : { text: cleanedText };
|
if (media.length === 0) {
|
||||||
|
return { text: foundMediaToken ? cleanedText : trimmedRaw };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: cleanedText, mediaUrls: media, mediaUrl: media[0] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,14 @@ import sharp from "sharp";
|
|||||||
import { getReplyFromConfig } from "./auto-reply/reply.js";
|
import { getReplyFromConfig } from "./auto-reply/reply.js";
|
||||||
import { waitForever } from "./cli/wait.js";
|
import { waitForever } from "./cli/wait.js";
|
||||||
import { loadConfig } from "./config/config.js";
|
import { loadConfig } from "./config/config.js";
|
||||||
import { danger, info, isVerbose, logVerbose, success } from "./globals.js";
|
import {
|
||||||
|
danger,
|
||||||
|
info,
|
||||||
|
isVerbose,
|
||||||
|
logVerbose,
|
||||||
|
success,
|
||||||
|
warn,
|
||||||
|
} from "./globals.js";
|
||||||
import { logInfo } from "./logger.js";
|
import { logInfo } from "./logger.js";
|
||||||
import { getChildLogger } from "./logging.js";
|
import { getChildLogger } from "./logging.js";
|
||||||
import { maxBytesForKind, mediaKindFromMime } from "./media/constants.js";
|
import { maxBytesForKind, mediaKindFromMime } from "./media/constants.js";
|
||||||
@@ -466,34 +473,45 @@ export async function monitorWebProvider(
|
|||||||
onReplyStart: msg.sendComposing,
|
onReplyStart: msg.sendComposing,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (!replyResult || (!replyResult.text && !replyResult.mediaUrl)) {
|
if (
|
||||||
|
!replyResult ||
|
||||||
|
(!replyResult.text &&
|
||||||
|
!replyResult.mediaUrl &&
|
||||||
|
!replyResult.mediaUrls?.length)
|
||||||
|
) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
"Skipping auto-reply: no text/media returned from resolver",
|
"Skipping auto-reply: no text/media returned from resolver",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (replyResult.mediaUrl) {
|
const mediaList = replyResult.mediaUrls?.length
|
||||||
|
? replyResult.mediaUrls
|
||||||
|
: replyResult.mediaUrl
|
||||||
|
? [replyResult.mediaUrl]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (mediaList.length > 0) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`Web auto-reply media detected: ${replyResult.mediaUrl}`,
|
`Web auto-reply media detected: ${mediaList.filter(Boolean).join(", ")}`,
|
||||||
);
|
);
|
||||||
|
for (const [index, mediaUrl] of mediaList.entries()) {
|
||||||
try {
|
try {
|
||||||
const media = await loadWebMedia(
|
const media = await loadWebMedia(mediaUrl, maxMediaBytes);
|
||||||
replyResult.mediaUrl,
|
|
||||||
maxMediaBytes,
|
|
||||||
);
|
|
||||||
if (isVerbose()) {
|
if (isVerbose()) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`Web auto-reply media size: ${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB`,
|
`Web auto-reply media size: ${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB`,
|
||||||
);
|
);
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`Web auto-reply media source: ${replyResult.mediaUrl} (kind ${media.kind})`,
|
`Web auto-reply media source: ${mediaUrl} (kind ${media.kind})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const caption =
|
||||||
|
index === 0 ? replyResult.text || undefined : undefined;
|
||||||
if (media.kind === "image") {
|
if (media.kind === "image") {
|
||||||
await msg.sendMedia({
|
await msg.sendMedia({
|
||||||
image: media.buffer,
|
image: media.buffer,
|
||||||
caption: replyResult.text || undefined,
|
caption,
|
||||||
mimetype: media.contentType,
|
mimetype: media.contentType,
|
||||||
});
|
});
|
||||||
} else if (media.kind === "audio") {
|
} else if (media.kind === "audio") {
|
||||||
@@ -501,21 +519,20 @@ export async function monitorWebProvider(
|
|||||||
audio: media.buffer,
|
audio: media.buffer,
|
||||||
ptt: true,
|
ptt: true,
|
||||||
mimetype: media.contentType,
|
mimetype: media.contentType,
|
||||||
caption: replyResult.text || undefined,
|
caption,
|
||||||
} as AnyMessageContent);
|
} as AnyMessageContent);
|
||||||
} else if (media.kind === "video") {
|
} else if (media.kind === "video") {
|
||||||
await msg.sendMedia({
|
await msg.sendMedia({
|
||||||
video: media.buffer,
|
video: media.buffer,
|
||||||
caption: replyResult.text || undefined,
|
caption,
|
||||||
mimetype: media.contentType,
|
mimetype: media.contentType,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const fileName =
|
const fileName = mediaUrl.split("/").pop() ?? "file";
|
||||||
replyResult.mediaUrl.split("/").pop() ?? "file";
|
|
||||||
await msg.sendMedia({
|
await msg.sendMedia({
|
||||||
document: media.buffer,
|
document: media.buffer,
|
||||||
fileName,
|
fileName,
|
||||||
caption: replyResult.text || undefined,
|
caption,
|
||||||
mimetype: media.contentType,
|
mimetype: media.contentType,
|
||||||
} as AnyMessageContent);
|
} as AnyMessageContent);
|
||||||
}
|
}
|
||||||
@@ -527,8 +544,8 @@ export async function monitorWebProvider(
|
|||||||
{
|
{
|
||||||
to: msg.from,
|
to: msg.from,
|
||||||
from: msg.to,
|
from: msg.to,
|
||||||
text: replyResult.text ?? null,
|
text: index === 0 ? (replyResult.text ?? null) : null,
|
||||||
mediaUrl: replyResult.mediaUrl,
|
mediaUrl,
|
||||||
mediaSizeBytes: media.buffer.length,
|
mediaSizeBytes: media.buffer.length,
|
||||||
mediaKind: media.kind,
|
mediaKind: media.kind,
|
||||||
durationMs: Date.now() - replyStarted,
|
durationMs: Date.now() - replyStarted,
|
||||||
@@ -541,39 +558,30 @@ export async function monitorWebProvider(
|
|||||||
`Failed sending web media to ${msg.from}: ${String(err)}`,
|
`Failed sending web media to ${msg.from}: ${String(err)}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (replyResult.text) {
|
if (index === 0 && replyResult.text) {
|
||||||
|
console.log(
|
||||||
|
warn(`⚠️ Media skipped; sent text-only to ${msg.from}`),
|
||||||
|
);
|
||||||
|
await msg.reply(replyResult.text || "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (replyResult.text) {
|
||||||
await msg.reply(replyResult.text);
|
await msg.reply(replyResult.text);
|
||||||
logInfo(
|
|
||||||
`⚠️ Media skipped; sent text-only to ${msg.from}`,
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
replyLogger.info(
|
|
||||||
{
|
|
||||||
to: msg.from,
|
|
||||||
from: msg.to,
|
|
||||||
text: replyResult.text,
|
|
||||||
mediaUrl: replyResult.mediaUrl,
|
|
||||||
durationMs: Date.now() - replyStarted,
|
|
||||||
mediaSendFailed: true,
|
|
||||||
},
|
|
||||||
"auto-reply sent (text fallback)",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await msg.reply(replyResult.text ?? "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const durationMs = Date.now() - replyStarted;
|
const durationMs = Date.now() - replyStarted;
|
||||||
|
const hasMedia = mediaList.length > 0;
|
||||||
if (isVerbose()) {
|
if (isVerbose()) {
|
||||||
console.log(
|
console.log(
|
||||||
success(
|
success(
|
||||||
`↩️ Auto-replied to ${msg.from} (web, ${replyResult.text?.length ?? 0} chars${replyResult.mediaUrl ? ", media" : ""}, ${formatDuration(durationMs)})`,
|
`↩️ Auto-replied to ${msg.from} (web, ${replyResult.text?.length ?? 0} chars${hasMedia ? ", media" : ""}, ${formatDuration(durationMs)})`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
success(
|
success(
|
||||||
`↩️ ${replyResult.text ?? "<media>"}${replyResult.mediaUrl ? " (media)" : ""}`,
|
`↩️ ${replyResult.text ?? "<media>"}${hasMedia ? " (media)" : ""}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -582,7 +590,7 @@ export async function monitorWebProvider(
|
|||||||
to: msg.from,
|
to: msg.from,
|
||||||
from: msg.to,
|
from: msg.to,
|
||||||
text: replyResult.text ?? null,
|
text: replyResult.text ?? null,
|
||||||
mediaUrl: replyResult.mediaUrl,
|
mediaUrl: mediaList[0] ?? null,
|
||||||
durationMs,
|
durationMs,
|
||||||
},
|
},
|
||||||
"auto-reply sent",
|
"auto-reply sent",
|
||||||
|
|||||||
Reference in New Issue
Block a user