/** * KHAELOR * File: src/cli/args.ts * Description: Dependency-free argv parsing for the khaelor entry point (--model, --print, --debug, --version, --help). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; /** Version read from package.json (dist/cli/ and src/cli/ are both two levels below it). */ function readPackageVersion(): string { try { const here = dirname(fileURLToPath(import.meta.url)); const raw = readFileSync(join(here, "..", "..", "package.json"), "utf8"); const pkg = JSON.parse(raw) as { version?: string }; return pkg.version ?? "0.0.0"; } catch { return "0.0.0"; } } export const KHAELOR_VERSION = readPackageVersion(); /** Parsed CLI arguments. `errors` is non-empty when parsing failed. */ export interface ParsedArgs { help: boolean; version: boolean; debug: boolean; /** Non-interactive mode: run one agent turn, print the final text, exit. */ print: string | null; /** Resume an existing session by id (interactive mode). */ resume: string | null; model: string | null; auxModel: string | null; thinking: string | null; maxOutputTokens: number | null; /** Phase-gate rigor: strict | auto | off (v2 §1). */ gate: string | null; /** Print the detected terminal capability report and exit (TUI v2 §7). */ doctorTui: boolean; /** Show the full splash even on repeat launches (TUI v2 §2). */ splash: boolean; errors: string[]; } const HELP_TEXT = `khaelor — a terminal-native autonomous engineering agent Usage khaelor start the interactive terminal agent khaelor --print "" run one agent turn without the TUI, print the answer, exit khaelor --resume resume a previous session Options --model Anthropic model id for this run --aux-model cheaper model used for context compaction --thinking off | adaptive | always --max-output-tokens output token ceiling per model response --gate phase-gate rigor: strict | auto (default) | off -p, --print non-interactive single turn (mirrors claude -p) --resume resume a session by id --splash show the full splash on launch --doctor-tui print the detected terminal capabilities and exit --debug write developer logs to ~/.khaelor/logs/ -v, --version print the version and exit -h, --help show this help Autonomy khaelord the long-running daemon: goals, heartbeat, budget (khaelord --help) Environment ANTHROPIC_API_KEY required — your Anthropic API key `; export function helpText(): string { return HELP_TEXT; } /** * The actionable message shown when no API key is configured. Never echoes * any secret material — there is nothing to echo. */ export function missingApiKeyMessage(): string { return [ "khaelor needs an Anthropic API key.", "", "Set the ANTHROPIC_API_KEY environment variable and try again:", "", " export ANTHROPIC_API_KEY=", " khaelor", "", "Keys are created at https://console.anthropic.com/ — KHAELOR never", "stores or logs the key; it is read from the environment on each start.", ].join("\n"); } /** Hand-rolled, zero-dependency argv parsing (ARCHITECTURE.md §12.1). */ export function parseArgs(argv: readonly string[]): ParsedArgs { const parsed: ParsedArgs = { help: false, version: false, debug: false, print: null, resume: null, model: null, auxModel: null, thinking: null, maxOutputTokens: null, gate: null, doctorTui: false, splash: false, errors: [], }; const takeValue = (flag: string, index: number): string | null => { const value = argv[index + 1]; if (value === undefined || value.startsWith("--")) { parsed.errors.push(`${flag} requires a value`); return null; } return value; }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i] as string; switch (arg) { case "-h": case "--help": parsed.help = true; break; case "-v": case "--version": parsed.version = true; break; case "--debug": parsed.debug = true; break; case "-p": case "--print": { const value = takeValue("--print", i); if (value !== null) { parsed.print = value; i += 1; } break; } case "--resume": { const value = takeValue("--resume", i); if (value !== null) { parsed.resume = value; i += 1; } break; } case "--model": { const value = takeValue("--model", i); if (value !== null) { parsed.model = value; i += 1; } break; } case "--aux-model": { const value = takeValue("--aux-model", i); if (value !== null) { parsed.auxModel = value; i += 1; } break; } case "--thinking": { const value = takeValue("--thinking", i); if (value !== null) { parsed.thinking = value; i += 1; } break; } case "--gate": { const value = takeValue("--gate", i); if (value !== null) { if (!["strict", "auto", "off"].includes(value)) { parsed.errors.push("--gate must be strict, auto, or off"); } else { parsed.gate = value; } i += 1; } break; } case "--doctor-tui": parsed.doctorTui = true; break; case "--splash": parsed.splash = true; break; case "--max-output-tokens": { const value = takeValue("--max-output-tokens", i); if (value !== null) { const n = Number.parseInt(value, 10); if (!Number.isInteger(n) || n <= 0) { parsed.errors.push("--max-output-tokens must be a positive integer"); } else { parsed.maxOutputTokens = n; } i += 1; } break; } default: parsed.errors.push(`unknown option: ${arg}`); break; } } return parsed; }