Extract auto-reply helpers into modules

This commit is contained in:
Peter Steinberger
2025-11-25 02:16:54 +01:00
parent ba3b271c39
commit b8b0873c1e
9 changed files with 705 additions and 590 deletions

51
src/config/config.ts Normal file
View File

@@ -0,0 +1,51 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import JSON5 from "json5";
export type ReplyMode = "text" | "command";
export type ClaudeOutputFormat = "text" | "json" | "stream-json";
export type SessionScope = "per-sender" | "global";
export type SessionConfig = {
scope?: SessionScope;
resetTriggers?: string[];
idleMinutes?: number;
store?: string;
sessionArgNew?: string[];
sessionArgResume?: string[];
sessionArgBeforeBody?: boolean;
};
export type WarelayConfig = {
inbound?: {
allowFrom?: string[]; // E.164 numbers allowed to trigger auto-reply (without whatsapp:)
reply?: {
mode: ReplyMode;
text?: string; // for mode=text, can contain {{Body}}
command?: string[]; // for mode=command, argv with templates
template?: string; // prepend template string when building command/prompt
timeoutSeconds?: number; // optional command timeout; defaults to 600s
bodyPrefix?: string; // optional string prepended to Body before templating
session?: SessionConfig;
claudeOutputFormat?: ClaudeOutputFormat; // when command starts with `claude`, force an output format
};
};
};
export const CONFIG_PATH = path.join(os.homedir(), ".warelay", "warelay.json");
export function loadConfig(): WarelayConfig {
// Read ~/.warelay/warelay.json (JSON5) if present.
try {
if (!fs.existsSync(CONFIG_PATH)) return {};
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
const parsed = JSON5.parse(raw);
if (typeof parsed !== "object" || parsed === null) return {};
return parsed as WarelayConfig;
} catch (err) {
console.error(`Failed to read config at ${CONFIG_PATH}`, err);
return {};
}
}

54
src/config/sessions.ts Normal file
View File

@@ -0,0 +1,54 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import JSON5 from "json5";
import { CONFIG_DIR, normalizeE164 } from "../utils.js";
import type { MsgContext } from "../auto-reply/templating.js";
export type SessionScope = "per-sender" | "global";
export type SessionEntry = { sessionId: string; updatedAt: number };
export const SESSION_STORE_DEFAULT = path.join(CONFIG_DIR, "sessions.json");
export const DEFAULT_RESET_TRIGGER = "/new";
export const DEFAULT_IDLE_MINUTES = 60;
export function resolveStorePath(store?: string) {
if (!store) return SESSION_STORE_DEFAULT;
if (store.startsWith("~"))
return path.resolve(store.replace("~", os.homedir()));
return path.resolve(store);
}
export function loadSessionStore(storePath: string): Record<string, SessionEntry> {
try {
const raw = fs.readFileSync(storePath, "utf-8");
const parsed = JSON5.parse(raw);
if (parsed && typeof parsed === "object") {
return parsed as Record<string, SessionEntry>;
}
} catch {
// ignore missing/invalid store; we'll recreate it
}
return {};
}
export async function saveSessionStore(
storePath: string,
store: Record<string, SessionEntry>,
) {
await fs.promises.mkdir(path.dirname(storePath), { recursive: true });
await fs.promises.writeFile(
storePath,
JSON.stringify(store, null, 2),
"utf-8",
);
}
export function deriveSessionKey(scope: SessionScope, ctx: MsgContext) {
if (scope === "global") return "global";
const from = ctx.From ? normalizeE164(ctx.From) : "";
return from || "unknown";
}