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%
3.8 KB · 113 lines typescript
Raw Blame History
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