chore: format + lint

main
Peter Steinberger 2025-11-26 00:30:30 +01:00
parent 8a01dc7f4c
commit a67f4db5e2
3 changed files with 121 additions and 121 deletions

View File

@ -10,16 +10,16 @@ export type ClaudeOutputFormat = "text" | "json" | "stream-json";
export type SessionScope = "per-sender" | "global"; export type SessionScope = "per-sender" | "global";
export type SessionConfig = { export type SessionConfig = {
scope?: SessionScope; scope?: SessionScope;
resetTriggers?: string[]; resetTriggers?: string[];
idleMinutes?: number; idleMinutes?: number;
store?: string; store?: string;
sessionArgNew?: string[]; sessionArgNew?: string[];
sessionArgResume?: string[]; sessionArgResume?: string[];
sessionArgBeforeBody?: boolean; sessionArgBeforeBody?: boolean;
sendSystemOnce?: boolean; sendSystemOnce?: boolean;
sessionIntro?: string; sessionIntro?: string;
typingIntervalSeconds?: number; typingIntervalSeconds?: number;
}; };
export type LoggingConfig = { export type LoggingConfig = {
@ -28,29 +28,29 @@ export type LoggingConfig = {
}; };
export type WarelayConfig = { export type WarelayConfig = {
logging?: LoggingConfig; logging?: LoggingConfig;
inbound?: { inbound?: {
allowFrom?: string[]; // E.164 numbers allowed to trigger auto-reply (without whatsapp:) allowFrom?: string[]; // E.164 numbers allowed to trigger auto-reply (without whatsapp:)
transcribeAudio?: { transcribeAudio?: {
// Optional CLI to turn inbound audio into text; templated args, must output transcript to stdout. // Optional CLI to turn inbound audio into text; templated args, must output transcript to stdout.
command: string[]; command: string[];
timeoutSeconds?: number; timeoutSeconds?: number;
}; };
reply?: { reply?: {
mode: ReplyMode; mode: ReplyMode;
text?: string; // for mode=text, can contain {{Body}} text?: string; // for mode=text, can contain {{Body}}
command?: string[]; // for mode=command, argv with templates command?: string[]; // for mode=command, argv with templates
cwd?: string; // working directory for command execution cwd?: string; // working directory for command execution
template?: string; // prepend template string when building command/prompt template?: string; // prepend template string when building command/prompt
timeoutSeconds?: number; // optional command timeout; defaults to 600s timeoutSeconds?: number; // optional command timeout; defaults to 600s
bodyPrefix?: string; // optional string prepended to Body before templating bodyPrefix?: string; // optional string prepended to Body before templating
mediaUrl?: string; // optional media attachment (path or URL) mediaUrl?: string; // optional media attachment (path or URL)
session?: SessionConfig; session?: SessionConfig;
claudeOutputFormat?: ClaudeOutputFormat; // when command starts with `claude`, force an output format claudeOutputFormat?: ClaudeOutputFormat; // when command starts with `claude`, force an output format
mediaMaxMb?: number; // optional cap for outbound media (default 5MB) mediaMaxMb?: number; // optional cap for outbound media (default 5MB)
typingIntervalSeconds?: number; // how often to refresh typing indicator while command runs typingIntervalSeconds?: number; // how often to refresh typing indicator while command runs
}; };
}; };
}; };
export const CONFIG_PATH = path.join(os.homedir(), ".warelay", "warelay.json"); export const CONFIG_PATH = path.join(os.homedir(), ".warelay", "warelay.json");

View File

@ -1,6 +1,6 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import net from "node:net";
import fs from "node:fs"; import fs from "node:fs";
import net from "node:net";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js"; import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js";
@ -144,11 +144,11 @@ describe("config and templating", () => {
killed: false, killed: false,
}); });
const result = await index.getReplyFromConfig( const result = await index.getReplyFromConfig(
{ {
Body: "<media:audio>", Body: "<media:audio>",
From: "+1", From: "+1",
To: "+2", To: "+2",
MediaPath: "/tmp/voice.ogg", MediaPath: "/tmp/voice.ogg",
MediaType: "audio/ogg", MediaType: "audio/ogg",
}, },
@ -157,15 +157,15 @@ describe("config and templating", () => {
commandRunner, commandRunner,
); );
expect(runExec).toHaveBeenCalled(); expect(runExec).toHaveBeenCalled();
expect(commandRunner).toHaveBeenCalled(); expect(commandRunner).toHaveBeenCalled();
const argv = commandRunner.mock.calls[0][0]; const argv = commandRunner.mock.calls[0][0];
const prompt = argv[argv.length - 1] as string; const prompt = argv[argv.length - 1] as string;
expect(prompt).toContain("/tmp/voice.ogg"); expect(prompt).toContain("/tmp/voice.ogg");
expect(prompt).toContain("Transcript:"); expect(prompt).toContain("Transcript:");
expect(prompt).toContain("voice transcript"); expect(prompt).toContain("voice transcript");
expect(result?.text).toBe("ok"); expect(result?.text).toBe("ok");
}); });
it("getReplyFromConfig skips transcription when not configured", async () => { it("getReplyFromConfig skips transcription when not configured", async () => {
const cfg = { const cfg = {
@ -640,44 +640,44 @@ describe("config and templating", () => {
expect(onReplyStart.mock.calls.length).toBeGreaterThanOrEqual(3); expect(onReplyStart.mock.calls.length).toBeGreaterThanOrEqual(3);
}); });
it("uses session typing interval override", async () => { it("uses session typing interval override", async () => {
const onReplyStart = vi.fn(); const onReplyStart = vi.fn();
const runSpy = vi.spyOn(index, "runCommandWithTimeout").mockImplementation( const runSpy = vi.spyOn(index, "runCommandWithTimeout").mockImplementation(
() => () =>
new Promise((resolve) => new Promise((resolve) =>
setTimeout( setTimeout(
() => () =>
resolve({ resolve({
stdout: "done\n", stdout: "done\n",
stderr: "", stderr: "",
code: 0, code: 0,
signal: null, signal: null,
killed: false, killed: false,
}), }),
120, 120,
), ),
), ),
); );
const cfg = { const cfg = {
inbound: { inbound: {
reply: { reply: {
mode: "command" as const, mode: "command" as const,
command: ["echo", "{{Body}}"], command: ["echo", "{{Body}}"],
session: { typingIntervalSeconds: 0.02 }, session: { typingIntervalSeconds: 0.02 },
}, },
}, },
}; };
const promise = index.getReplyFromConfig( const promise = index.getReplyFromConfig(
{ Body: "hi", From: "+1", To: "+2" }, { Body: "hi", From: "+1", To: "+2" },
{ onReplyStart }, { onReplyStart },
cfg, cfg,
runSpy, runSpy,
); );
await new Promise((r) => setTimeout(r, 200)); await new Promise((r) => setTimeout(r, 200));
await promise; await promise;
expect(onReplyStart.mock.calls.length).toBeGreaterThanOrEqual(3); expect(onReplyStart.mock.calls.length).toBeGreaterThanOrEqual(3);
}); });
it("injects Claude output format + print flag when configured", async () => { it("injects Claude output format + print flag when configured", async () => {
const runSpy = vi.spyOn(index, "runCommandWithTimeout").mockResolvedValue({ const runSpy = vi.spyOn(index, "runCommandWithTimeout").mockResolvedValue({

View File

@ -501,42 +501,42 @@ describe("provider-web", () => {
fetchMock.mockRestore(); fetchMock.mockRestore();
}); });
it( it(
"compresses common formats to jpeg under the cap", "compresses common formats to jpeg under the cap",
{ timeout: 15_000 }, { timeout: 15_000 },
async () => { async () => {
const formats = [ const formats = [
{ {
name: "png", name: "png",
mime: "image/png", mime: "image/png",
make: (buf: Buffer, opts: { width: number; height: number }) => make: (buf: Buffer, opts: { width: number; height: number }) =>
sharp(buf, { sharp(buf, {
raw: { width: opts.width, height: opts.height, channels: 3 }, raw: { width: opts.width, height: opts.height, channels: 3 },
}) })
.png({ compressionLevel: 0 }) .png({ compressionLevel: 0 })
.toBuffer(), .toBuffer(),
}, },
{ {
name: "jpeg", name: "jpeg",
mime: "image/jpeg", mime: "image/jpeg",
make: (buf: Buffer, opts: { width: number; height: number }) => make: (buf: Buffer, opts: { width: number; height: number }) =>
sharp(buf, { sharp(buf, {
raw: { width: opts.width, height: opts.height, channels: 3 }, raw: { width: opts.width, height: opts.height, channels: 3 },
}) })
.jpeg({ quality: 100, chromaSubsampling: "4:4:4" }) .jpeg({ quality: 100, chromaSubsampling: "4:4:4" })
.toBuffer(), .toBuffer(),
}, },
{ {
name: "webp", name: "webp",
mime: "image/webp", mime: "image/webp",
make: (buf: Buffer, opts: { width: number; height: number }) => make: (buf: Buffer, opts: { width: number; height: number }) =>
sharp(buf, { sharp(buf, {
raw: { width: opts.width, height: opts.height, channels: 3 }, raw: { width: opts.width, height: opts.height, channels: 3 },
}) })
.webp({ quality: 100 }) .webp({ quality: 100 })
.toBuffer(), .toBuffer(),
}, },
] as const; ] as const;
for (const fmt of formats) { for (const fmt of formats) {
// Force a small cap to ensure compression is exercised for every format. // Force a small cap to ensure compression is exercised for every format.