import { execFile } from "node:child_process"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); export type NotifyUrgency = "low" | "normal" | "high"; export interface NotifyBackend { name: string; available(): Promise; send(title: string, body: string, urgency: NotifyUrgency): Promise; } async function isOnPath(binary: string): Promise { try { await execFileAsync("which", [binary]); return true; } catch { return false; } } export class OpenclawBackend implements NotifyBackend { readonly name = "openclaw"; async available(): Promise { return isOnPath("openclaw"); } async send(title: string, body: string, _urgency: NotifyUrgency): Promise { try { await execFileAsync("openclaw", ["wake", "--title", title, "--body", body]); return true; } catch { return false; } } } export class MacOsNotifyBackend implements NotifyBackend { readonly name = "macos-osascript"; async available(): Promise { return process.platform === "darwin"; } async send(title: string, body: string, _urgency: NotifyUrgency): Promise { try { const escaped_body = body.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); const escaped_title = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); const script = `display notification "${escaped_body}" with title "${escaped_title}"`; await execFileAsync("osascript", ["-e", script]); return true; } catch { return false; } } } export class LinuxNotifySendBackend implements NotifyBackend { readonly name = "linux-notify-send"; async available(): Promise { return isOnPath("notify-send"); } async send(title: string, body: string, urgency: NotifyUrgency): Promise { const urgencyMap: Record = { low: "low", normal: "normal", high: "critical", }; try { await execFileAsync("notify-send", ["-u", urgencyMap[urgency], title, body]); return true; } catch { return false; } } } export class TerminalBellBackend implements NotifyBackend { readonly name = "terminal"; async available(): Promise { return true; } async send(title: string, body: string, _urgency: NotifyUrgency): Promise { try { process.stderr.write("\x07"); process.stderr.write(`[${title}] ${body}\n`); return true; } catch { return false; } } } const backendFactories: Record NotifyBackend> = { "openclaw": () => new OpenclawBackend(), "macos-osascript": () => new MacOsNotifyBackend(), "linux-notify-send": () => new LinuxNotifySendBackend(), "terminal": () => new TerminalBellBackend(), }; export async function createBackends(configOrder: string[]): Promise { const backends: NotifyBackend[] = []; for (const name of configOrder) { const factory = backendFactories[name]; if (!factory) { continue; } const backend = factory(); if (await backend.available()) { backends.push(backend); } } return backends; }