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%
1/**2 * KHAELOR3 * File: src/cli/print.ts4 * Description: Non-interactive --print mode — one real agent turn without the TUI, final text on stdout (mirrors claude -p).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import type { ResolvedConfig } from "../config/index.js";11import { buildUsageTotals } from "../session/index.js";12import type { DurableEvent } from "../session/index.js";13import { assembleEngine, openSessionContext } from "./engine.js";14import type { FileLogger } from "./logger.js";1516/**17 * The final answer of the last completed model response that ended the turn18 * (stopReason ≠ tool_use): its settled text blocks, in block order.19 */20export function finalAnswerText(events: readonly DurableEvent[]): string {21 let finalRequestId: string | null = null;22 for (const event of events) {23 if (event.type === "model.response-completed" && event.payload.stopReason !== "tool_use") {24 finalRequestId = event.payload.requestId;25 }26 }27 if (finalRequestId === null) return "";28 const blocks: { blockIndex: number; text: string }[] = [];29 for (const event of events) {30 if (31 event.type === "model.text-block-completed" &&32 event.payload.requestId === finalRequestId33 ) {34 blocks.push({ blockIndex: event.payload.blockIndex, text: event.payload.text });35 }36 }37 blocks.sort((a, b) => a.blockIndex - b.blockIndex);38 return blocks.map((b) => b.text).join("\n");39}4041export interface PrintRunOptions {42 config: ResolvedConfig;43 cwd: string;44 prompt: string;45 logger: FileLogger;46 sessionsDir?: string;47 stdout?: NodeJS.WriteStream;48 stderr?: NodeJS.WriteStream;49}5051/**52 * Run one agent turn headless. Permission `ask`s resolve to deny (silence is53 * not consent); the session log is written exactly as in interactive mode.54 * Returns the process exit code.55 */56export async function runPrintMode(options: PrintRunOptions): Promise<number> {57 const stdout = options.stdout ?? process.stdout;58 const stderr = options.stderr ?? process.stderr;5960 const context = await openSessionContext({61 cwd: options.cwd,62 ...(options.sessionsDir !== undefined ? { sessionsDir: options.sessionsDir } : {}),63 logger: options.logger,64 });65 const engine = await assembleEngine({66 config: options.config,67 cwd: options.cwd,68 context,69 logger: options.logger,70 // no asker: non-interactive — every permission ask resolves deny (§5.2)71 });7273 let exitCode = 0;74 try {75 await engine.captureBaseline();76 engine.session.publishDurable({77 type: "user.message-created",78 payload: { text: options.prompt, mentions: [] },79 });80 const outcome = await engine.runTurn();81 const events = engine.session.events();82 const answer = finalAnswerText(events);8384 if (outcome.kind === "done") {85 stdout.write(answer.length > 0 ? `${answer}\n` : "");86 } else if (outcome.kind === "failed") {87 if (answer.length > 0) stdout.write(`${answer}\n`);88 stderr.write(`khaelor: turn failed (${outcome.reason}): ${outcome.detail}\n`);89 exitCode = 1;90 } else {91 stderr.write(`khaelor: turn ended without completing (${outcome.kind})\n`);92 exitCode = 1;93 }9495 // Honest run summary on stderr — real usage only, never estimated.96 const usage = buildUsageTotals(events).totals;97 stderr.write(98 `khaelor: session ${engine.session.sessionId} · ${usage.requests} model call(s) · ` +99 `in ${usage.inputTokens} out ${usage.outputTokens} ` +100 `cache-write ${usage.cacheWriteTokens} cache-read ${usage.cacheReadTokens} tokens\n` +101 `khaelor: log ${engine.log.filePath}\n`,102 );103 } catch (error) {104 const message = error instanceof Error ? error.message : String(error);105 stderr.write(`khaelor: ${message}\n`);106 options.logger.error("print mode failed", { error: message });107 exitCode = 1;108 } finally {109 await engine.shutdown();110 }111 return exitCode;112}113