SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
2.6 KB · 94 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: src/daemon/channels.ts4 * Description: Channel Router — webhook + command adapters behind one ChannelAdapter seam (v2 design §7.7).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { spawn } from "node:child_process";1112/** One outbound daemon notification. */13export interface ChannelNotification {14  kind: "run-completed" | "goal-escalated" | "approval-requested" | "budget-exhausted" | "daemon-status";15  goalId?: string;16  runId?: string;17  title: string;18  body: string;19  ts: number;20}2122/**23 * The single channel seam — same philosophy as the isolated ModelClient:24 * rich channels (Telegram, Slack) arrive later as plugins behind this25 * interface, never inside the daemon core (v2 §7.7).26 */27export interface ChannelAdapter {28  readonly name: string;29  send(notification: ChannelNotification): Promise<void>;30}3132/** POST the notification as JSON to a webhook URL. */33export class WebhookChannel implements ChannelAdapter {34  readonly name = "webhook";35  readonly #url: string;3637  constructor(url: string) {38    this.#url = url;39  }4041  async send(notification: ChannelNotification): Promise<void> {42    await fetch(this.#url, {43      method: "POST",44      headers: { "content-type": "application/json" },45      body: JSON.stringify(notification),46    });47  }48}4950/** Pipe the notification JSON to a user script's stdin (mail/Slack/ntfy — user's choice). */51export class CommandChannel implements ChannelAdapter {52  readonly name = "command";53  readonly #command: string;5455  constructor(command: string) {56    this.#command = command;57  }5859  send(notification: ChannelNotification): Promise<void> {60    return new Promise((resolve) => {61      const child = spawn(this.#command, { shell: true, stdio: ["pipe", "ignore", "ignore"] });62      child.on("error", () => resolve());63      child.on("exit", () => resolve());64      child.stdin.end(`${JSON.stringify(notification)}\n`);65    });66  }67}6869/** Fan a notification out to every configured channel; failures never crash the daemon. */70export class ChannelRouter {71  readonly #channels: ChannelAdapter[];72  readonly #onError: (channel: string, error: unknown) => void;7374  constructor(75    channels: ChannelAdapter[],76    onError: (channel: string, error: unknown) => void = () => undefined,77  ) {78    this.#channels = channels;79    this.#onError = onError;80  }8182  async send(notification: ChannelNotification): Promise<void> {83    await Promise.all(84      this.#channels.map(async (channel) => {85        try {86          await channel.send(notification);87        } catch (error) {88          this.#onError(channel.name, error);89        }90      }),91    );92  }93}94