/** * M83 — the command routing MECHANISM, and only that. * * ## Scope, stated before the code * * A consumer's terminal surface is 8 639 LOC. Most of it is chrome and widgets and belongs to * `@theokit/ui` / `@theokit/tui`. What has no counterpart anywhere is this: a registry, longest-prefix * routing, and the interpreter shape. * * What is deliberately NOT here (Top-risk 1 of the milestone): help rendering, aliases, completions. * Each of those is a mini CLI framework growing inside the agents package, and each needs its own ADR * with demand behind it. The ~50 command NAMES stay in the product; only the routing crosses. * * ## Longest prefix, not first match * * A terminal accepts `/model` and `/model-list`. Under first-match, whichever was registered first * swallows the other — `/model-list` routes to `model` with the argument `-list` — and the report * reads "my command stopped working", long after the registration order changed. Length is a property * of the names; position is an accident of the array. */ /** How a command treats the text after its name. */ type CommandArgument = 'none' | 'optional' | 'required'; /** One routable command. The NAME is the contract: it is what the user types and what logs record. */ interface CommandDefinition { readonly name: string; readonly description: string; /** Omitted ⇒ `'optional'`, the shape that refuses nothing. */ readonly arg: CommandArgument; } /** Declare a command. A function rather than an object literal so the default is stated once. */ declare function defineCommand(input: { name: string; description: string; arg?: CommandArgument; }): CommandDefinition; /** Why an input could not be routed. */ type RouteFailure = 'unknown-command' | 'missing-argument' | 'unexpected-argument' | 'ambiguous-command'; /** What the input turned out to be. */ type RoutedInput = { readonly kind: 'command'; readonly command: CommandDefinition; readonly arg: string; } | { readonly kind: 'message'; readonly text: string; } | { readonly kind: 'error'; readonly reason: RouteFailure; readonly input: string; }; /** * Route one line of terminal input. * * Plain text is a MESSAGE, not an unknown command: a terminal that answered "unknown command" to * ordinary prose would be unusable. A leading `/` is an explicit claim to be a command, so an * unrecognised one is an ERROR rather than prose — sending `/moddel gpt-5` to the model as text * hides a typo behind a plausible answer. */ declare function routeCommand(input: string, commands: readonly CommandDefinition[]): RoutedInput; /** * M83 — orderly shutdown, with a watchdog. * * ## Why the exit codes must stay distinguishable * * Three things end a terminal agent, and they are not the same event: * * | outcome | code | what an operator does | * |---|---|---| * | clean Ctrl-C | `130` | nothing — the user meant it | * | a cleanup threw | `1` | read the error; something did not release | * | the watchdog fired | `2` | a cleanup HUNG; find which one | * * Collapsing them into one code is what makes a hung cleanup indistinguishable from a user pressing * Ctrl-C — and the hang then reads as normal operation forever. `130` is the shell convention for * SIGINT (`128 + 2`), so a CI job that treats it as "user cancelled" keeps working. * * ## Why the deps are injected * * `signals`, `exit` and the timer come from outside. A shutdown module that calls `process.exit` * directly cannot be tested — the first assertion kills the test runner — so the version that ships * is the version nobody ran. */ /** One thing to release, in the order registered. */ interface CleanupTask { /** Named so a watchdog timeout can say WHICH cleanup hung, rather than that one did. */ readonly name: string; readonly run: () => void | Promise; } /** How the process ended. */ type ShutdownOutcome = 'clean' | 'cleanup-failed' | 'watchdog-timeout'; /** Exit codes, distinguishable on purpose. See the module docblock. */ declare const SHUTDOWN_EXIT_CODES: Readonly>; /** The watchdog budget. Three seconds is the consumer's measured default. */ declare const DEFAULT_WATCHDOG_MS = 3000; interface ShutdownDeps { /** Registers a signal handler. Injected so the suite never installs one on the runner. */ readonly onSignal?: (signal: 'SIGINT' | 'SIGTERM', handler: () => void) => void; /** Ends the process. Injected for the same reason — a real `process.exit` kills the test. */ readonly exit?: (code: number) => void; /** Where a cleanup failure or a watchdog timeout is reported. */ readonly onWarn?: (message: string) => void; /** Watchdog budget in ms. */ readonly watchdogMs?: number; } interface Shutdown { /** Register a cleanup. Runs in registration order. */ register(task: CleanupTask): void; /** Run every cleanup, then exit. Idempotent: a second Ctrl-C does not start a second teardown. */ run(): Promise; } /** * Build a shutdown. * * The watchdog races the whole cleanup sequence rather than each task: a budget per task lets N slow * tasks add up to N × budget, which is the shape that makes "it eventually exits" true and useless. */ declare function createShutdown(deps?: ShutdownDeps): Shutdown; export { type CleanupTask, type CommandArgument, type CommandDefinition, DEFAULT_WATCHDOG_MS, type RouteFailure, type RoutedInput, SHUTDOWN_EXIT_CODES, type Shutdown, type ShutdownDeps, type ShutdownOutcome, createShutdown, defineCommand, routeCommand };