/** * KHAELOR * File: src/daemon/main.ts * Description: khaelord entrypoint — start/stop/status/tick and goal/approval management from the command line (v2 design §7). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { join } from "node:path"; import { loadConfig } from "../config/index.js"; import { defaultLogDir, FileLogger } from "../cli/logger.js"; import { workspaceExec } from "../cli/subtasks.js"; import { LocalWorkspace } from "../workspace/index.js"; import { ApprovalQueue } from "./approvals.js"; import { BudgetGuard } from "./budget.js"; import { ChannelRouter, CommandChannel, WebhookChannel } from "./channels.js"; import type { ChannelAdapter } from "./channels.js"; import { daemonDirFor, loadDaemonConfig } from "./config.js"; import { KhaelorDaemon, readDaemonStatus } from "./daemon.js"; import { daemonRequest, pidAlive } from "./client.js"; import { GoalStore } from "./goals.js"; import type { GoalEscalation, GoalType } from "./goals.js"; import { buildGoalRunner } from "./runner.js"; const HELP = `khaelord — the KHAELOR autonomous daemon (v2 §7) Usage khaelord start run the daemon in the foreground (use nohup/launchd/systemd to detach) khaelord stop stop a running daemon khaelord status show pid, budget, active runs khaelord tick force one scheduler pass on the running daemon's project khaelord goal add "" [--type maintain|achieve|watch] [--schedule ""|heartbeat] [--check ""] [--budget /day] [--runs /day] [--escalation notify|draft-pr|auto-merge-if-verified] khaelord goal list list goals with status khaelord goal pause | resume khaelord approvals list pending approvals khaelord approve | deny resolve an approval Configuration: .khaelor/daemon/config.json (budget, activeHours, heartbeatMinutes, model, channels, pricing). Every decision the daemon takes is an event in .khaelor/daemon/goals/.events.jsonl — replayable, auditable. `; function out(text: string): void { process.stdout.write(`${text}\n`); } function fail(text: string): never { process.stderr.write(`khaelord: ${text}\n`); process.exit(1); } function flagValue(argv: string[], flag: string): string | undefined { const index = argv.indexOf(flag); if (index === -1) return undefined; return argv[index + 1]; } async function cmdStart(cwd: string): Promise { const daemonDir = daemonDirFor(cwd); const existing = await readDaemonStatus(daemonDir); if (existing !== null && pidAlive(existing.pid)) { fail(`already running (pid ${existing.pid})`); } const logger = new FileLogger(join(defaultLogDir(), "khaelord.log"), { debug: true }); const daemonConfig = await loadDaemonConfig(cwd); const sessionConfig = await loadConfig({ cwd, ...(daemonConfig.model.runs !== undefined ? { flags: { model: daemonConfig.model.runs } } : {}), }); if (!sessionConfig.hasApiKey) fail("ANTHROPIC_API_KEY is not set — the daemon cannot run goals."); const workspace = new LocalWorkspace(cwd); const exec = workspaceExec(workspace); const channels: ChannelAdapter[] = []; if (daemonConfig.channels.webhook !== undefined) channels.push(new WebhookChannel(daemonConfig.channels.webhook)); if (daemonConfig.channels.command !== undefined) channels.push(new CommandChannel(daemonConfig.channels.command)); const daemon = new KhaelorDaemon({ daemonDir, config: daemonConfig, goals: new GoalStore(daemonDir), budget: new BudgetGuard(daemonDir, daemonConfig.budget), approvals: new ApprovalQueue(daemonDir), channels: new ChannelRouter(channels, (channel, error) => { logger.log("warn", `channel ${channel} failed`, { error: String(error) }); }), runGoal: buildGoalRunner({ config: sessionConfig, projectRoot: cwd, exec, logger, ...(daemonConfig.pricing !== undefined ? { pricing: daemonConfig.pricing } : {}), }), execCheck: async (cmd) => { const result = await workspace.exec({ cmd, timeoutMs: 120_000 }); return { exitCode: result.exitCode }; }, log: (level, message) => { logger.log(level === "info" ? "info" : level, message); out(`[${level}] ${message}`); }, }); const shutdown = (): void => { void daemon.stop().then(() => process.exit(0)); }; process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); await daemon.start(); out(`khaelord running — project ${cwd}`); out(`control socket: ${daemon.socketPath}`); // Foreground loop; the interval keeps the process alive. } async function cmdGoal(cwd: string, argv: string[]): Promise { const store = new GoalStore(daemonDirFor(cwd)); const sub = argv[0]; if (sub === "add") { const description = argv[1]; if (description === undefined || description.startsWith("--")) fail("goal add needs a description"); const type = (flagValue(argv, "--type") ?? "watch") as GoalType; if (!["maintain", "achieve", "watch"].includes(type)) fail(`invalid --type ${type}`); const schedule = flagValue(argv, "--schedule") ?? "heartbeat"; const check = flagValue(argv, "--check"); const escalation = (flagValue(argv, "--escalation") ?? "notify") as GoalEscalation; if (!["notify", "draft-pr", "auto-merge-if-verified"].includes(escalation)) { fail(`invalid --escalation ${escalation}`); } const budgetRaw = flagValue(argv, "--budget"); const runsRaw = flagValue(argv, "--runs"); const maxUsdPerDay = budgetRaw !== undefined ? Number.parseFloat(budgetRaw) : undefined; const maxRunsPerDay = runsRaw !== undefined ? Number.parseInt(runsRaw, 10) : undefined; const goal = await store.create({ description, type, schedule, ...(check !== undefined ? { check } : {}), escalation, budget: { ...(maxUsdPerDay !== undefined && !Number.isNaN(maxUsdPerDay) ? { maxUsdPerDay } : {}), ...(maxRunsPerDay !== undefined && !Number.isNaN(maxRunsPerDay) ? { maxRunsPerDay } : {}), }, }); out(`goal ${goal.id} created — ${goal.type} · schedule ${goal.schedule} · escalation ${goal.escalation}`); return; } if (sub === "list") { const goals = await store.list(); if (goals.length === 0) { out("no goals — add one with: khaelord goal add \"\""); return; } for (const goal of goals) { const runs = await store.runsToday(goal.id); out( `${goal.id} [${goal.status}] ${goal.type} · ${goal.schedule} · $${goal.budget.maxUsdPerDay}/day · runs today ${runs}/${goal.budget.maxRunsPerDay}\n ${goal.description}`, ); } return; } if (sub === "pause" || sub === "resume") { const id = argv[1]; if (id === undefined) fail(`goal ${sub} needs an id`); await store.setStatus(id, sub === "pause" ? "paused" : "active"); out(`goal ${id} ${sub}d`); return; } fail(`unknown goal subcommand: ${String(sub)}`); } export async function daemonMain(argv: string[] = process.argv.slice(2)): Promise { const cwd = process.cwd(); const daemonDir = daemonDirFor(cwd); const command = argv[0]; switch (command) { case undefined: case "-h": case "--help": case "help": out(HELP); return; case "start": await cmdStart(cwd); return; case "stop": { const response = await daemonRequest(daemonDir, { cmd: "stop" }).catch(() => null); if (response === null) fail("no running daemon (or socket unreachable)"); out("stopping"); return; } case "status": { const status = await readDaemonStatus(daemonDir); if (status === null || !pidAlive(status.pid)) { out("khaelord: not running"); return; } const live = await daemonRequest(daemonDir, { cmd: "status" }).catch(() => null); out(`khaelord: running (pid ${status.pid}, since ${new Date(status.startedAt).toISOString()})`); if (live !== null) { out( ` active runs: ${String(live["activeRuns"])} · spent today: $${Number(live["spentTodayUsd"] ?? 0).toFixed(2)}`, ); } return; } case "goal": await cmdGoal(cwd, argv.slice(1)); return; case "approvals": { const response = await daemonRequest(daemonDir, { cmd: "approvals" }).catch(() => null); if (response === null) fail("no running daemon"); const approvals = (response["approvals"] ?? []) as { id: string; capability: string; context: string }[]; if (approvals.length === 0) out("no pending approvals"); for (const approval of approvals) { out(`${approval.id} ${approval.capability}\n ${approval.context}`); } return; } case "approve": case "deny": { const id = argv[1]; if (id === undefined) fail(`${command} needs an approval id`); const response = await daemonRequest(daemonDir, { cmd: command, id }).catch(() => null); if (response === null) fail("no running daemon"); out(response["ok"] === true ? `${command}d ${id}` : `failed: ${String(response["error"])}`); return; } default: fail(`unknown command: ${command}\n\n${HELP}`); } } // Direct execution (bin entry). const isMain = process.argv[1]?.endsWith("daemon/main.js") === true || process.argv[1]?.endsWith("khaelord") === true; if (isMain) { daemonMain().catch((error: unknown) => { process.stderr.write(`khaelord: ${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); }); }