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%
6.3 KB · 221 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: src/cli/args.ts4 * Description: Dependency-free argv parsing for the khaelor entry point (--model, --print, --debug, --version, --help).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { readFileSync } from "node:fs";11import { dirname, join } from "node:path";12import { fileURLToPath } from "node:url";1314/** Version read from package.json (dist/cli/ and src/cli/ are both two levels below it). */15function readPackageVersion(): string {16  try {17    const here = dirname(fileURLToPath(import.meta.url));18    const raw = readFileSync(join(here, "..", "..", "package.json"), "utf8");19    const pkg = JSON.parse(raw) as { version?: string };20    return pkg.version ?? "0.0.0";21  } catch {22    return "0.0.0";23  }24}2526export const KHAELOR_VERSION = readPackageVersion();2728/** Parsed CLI arguments. `errors` is non-empty when parsing failed. */29export interface ParsedArgs {30  help: boolean;31  version: boolean;32  debug: boolean;33  /** Non-interactive mode: run one agent turn, print the final text, exit. */34  print: string | null;35  /** Resume an existing session by id (interactive mode). */36  resume: string | null;37  model: string | null;38  auxModel: string | null;39  thinking: string | null;40  maxOutputTokens: number | null;41  /** Phase-gate rigor: strict | auto | off (v2 §1). */42  gate: string | null;43  /** Print the detected terminal capability report and exit (TUI v2 §7). */44  doctorTui: boolean;45  /** Show the full splash even on repeat launches (TUI v2 §2). */46  splash: boolean;47  errors: string[];48}4950const HELP_TEXT = `khaelor — a terminal-native autonomous engineering agent5152Usage53  khaelor                       start the interactive terminal agent54  khaelor --print "<prompt>"    run one agent turn without the TUI, print the answer, exit55  khaelor --resume <session-id> resume a previous session5657Options58  --model <id>              Anthropic model id for this run59  --aux-model <id>          cheaper model used for context compaction60  --thinking <mode>         off | adaptive | always61  --max-output-tokens <n>   output token ceiling per model response62  --gate <mode>             phase-gate rigor: strict | auto (default) | off63  -p, --print <prompt>      non-interactive single turn (mirrors claude -p)64  --resume <session-id>     resume a session by id65  --splash                  show the full splash on launch66  --doctor-tui              print the detected terminal capabilities and exit67  --debug                   write developer logs to ~/.khaelor/logs/68  -v, --version             print the version and exit69  -h, --help                show this help7071Autonomy72  khaelord                  the long-running daemon: goals, heartbeat, budget (khaelord --help)7374Environment75  ANTHROPIC_API_KEY         required — your Anthropic API key76`;7778export function helpText(): string {79  return HELP_TEXT;80}8182/**83 * The actionable message shown when no API key is configured. Never echoes84 * any secret material — there is nothing to echo.85 */86export function missingApiKeyMessage(): string {87  return [88    "khaelor needs an Anthropic API key.",89    "",90    "Set the ANTHROPIC_API_KEY environment variable and try again:",91    "",92    "  export ANTHROPIC_API_KEY=<your key>",93    "  khaelor",94    "",95    "Keys are created at https://console.anthropic.com/ — KHAELOR never",96    "stores or logs the key; it is read from the environment on each start.",97  ].join("\n");98}99100/** Hand-rolled, zero-dependency argv parsing (ARCHITECTURE.md §12.1). */101export function parseArgs(argv: readonly string[]): ParsedArgs {102  const parsed: ParsedArgs = {103    help: false,104    version: false,105    debug: false,106    print: null,107    resume: null,108    model: null,109    auxModel: null,110    thinking: null,111    maxOutputTokens: null,112    gate: null,113    doctorTui: false,114    splash: false,115    errors: [],116  };117118  const takeValue = (flag: string, index: number): string | null => {119    const value = argv[index + 1];120    if (value === undefined || value.startsWith("--")) {121      parsed.errors.push(`${flag} requires a value`);122      return null;123    }124    return value;125  };126127  for (let i = 0; i < argv.length; i += 1) {128    const arg = argv[i] as string;129    switch (arg) {130      case "-h":131      case "--help":132        parsed.help = true;133        break;134      case "-v":135      case "--version":136        parsed.version = true;137        break;138      case "--debug":139        parsed.debug = true;140        break;141      case "-p":142      case "--print": {143        const value = takeValue("--print", i);144        if (value !== null) {145          parsed.print = value;146          i += 1;147        }148        break;149      }150      case "--resume": {151        const value = takeValue("--resume", i);152        if (value !== null) {153          parsed.resume = value;154          i += 1;155        }156        break;157      }158      case "--model": {159        const value = takeValue("--model", i);160        if (value !== null) {161          parsed.model = value;162          i += 1;163        }164        break;165      }166      case "--aux-model": {167        const value = takeValue("--aux-model", i);168        if (value !== null) {169          parsed.auxModel = value;170          i += 1;171        }172        break;173      }174      case "--thinking": {175        const value = takeValue("--thinking", i);176        if (value !== null) {177          parsed.thinking = value;178          i += 1;179        }180        break;181      }182      case "--gate": {183        const value = takeValue("--gate", i);184        if (value !== null) {185          if (!["strict", "auto", "off"].includes(value)) {186            parsed.errors.push("--gate must be strict, auto, or off");187          } else {188            parsed.gate = value;189          }190          i += 1;191        }192        break;193      }194      case "--doctor-tui":195        parsed.doctorTui = true;196        break;197      case "--splash":198        parsed.splash = true;199        break;200      case "--max-output-tokens": {201        const value = takeValue("--max-output-tokens", i);202        if (value !== null) {203          const n = Number.parseInt(value, 10);204          if (!Number.isInteger(n) || n <= 0) {205            parsed.errors.push("--max-output-tokens must be a positive integer");206          } else {207            parsed.maxOutputTokens = n;208          }209          i += 1;210        }211        break;212      }213      default:214        parsed.errors.push(`unknown option: ${arg}`);215        break;216    }217  }218219  return parsed;220}221