#!/usr/bin/env bun import { readFile } from 'node:fs/promises' import { Command, CommanderError, Option } from 'commander' import { TerminalProgress } from './cli/progress.js' import type { CrivoLoadOptions } from './decisions/adapters/crivo.js' import type { RuntimeMetrics } from './decisions/adapters/runtime.js' import { MODEL_SIZES, type ModelSize } from './decisions/models.js' import { InvalidInputError, validateInput } from './decisions/validate.js' import { decide } from './index.js' interface Options { size: ModelSize device: NonNullable dtype?: CrivoLoadOptions['dtype'] cacheDir?: string } function report(error: object): void { process.stderr.write(`${JSON.stringify({ ok: false, error }, null, 2)}\n`) process.exitCode = 1 } const program = new Command() .name('decide') .description('Run typed decisions against a shared state. Input: JSON with state and decisions.') .addOption( new Option('--size ', 'Crivo model size (latest upstream checkpoint)') .choices([...MODEL_SIZES]) .default('150m') ) .argument('', 'JSON file path, or - to read JSON from stdin') .addOption( new Option('--device ', 'execution device') .choices(['auto', 'cpu', 'gpu']) .default('auto') ) .addOption( new Option('--dtype ', 'model precision (default depends on device)').choices([ 'q8', 'fp32' ]) ) .option('--cache-dir ', 'model cache directory', './.cache/crivo') .addHelpText( 'after', '\nExamples:\n bun run decide examples/basic/input.json --device cpu\n cat request.json | bun run decide -\n' ) .exitOverride() .action(async (path: string, options: Options) => { let source: string try { if (path === '-') { if (process.stdin.isTTY) throw new Error('Pipe JSON to stdin when using -.') source = await new Response(Bun.stdin.stream()).text() } else { source = await readFile(path, 'utf8') } } catch (error) { report({ type: 'input-read-error', message: error instanceof Error ? error.message : String(error) }) return } let input: unknown try { input = JSON.parse(source) } catch { report({ type: 'invalid-json', message: 'Input must contain a valid JSON object with state and decisions.' }) return } validateInput(input) const progress = new TerminalProgress() let metrics: RuntimeMetrics | undefined const { createRuntime } = await import('./decisions/adapters/runtime.js') const result = await decide(input, { requirements: { decisionRuntime: createRuntime( { ...options, onLoadStart: (info) => progress.loading(info), onProgress: (event) => progress.progress(event) }, (value) => { metrics = value } ) } }) .onSuccess((answers) => ({ ok: true as const, answers })) .onError({ type: 'invalid-input' }, (error) => ({ ok: false as const, error })) .onError({ type: 'input-too-long' }, (error) => ({ ok: false as const, error })) if (result.ok) { process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) if (metrics) progress.summary(metrics, input.decisions.length) } else report(result.error) }) try { await program.parseAsync() } catch (error) { if (error instanceof CommanderError) { // Commander already printed help or the usage error. process.exitCode = error.exitCode } else if (error instanceof InvalidInputError) { report({ type: 'invalid-input', path: error.path, message: error.message }) } else { report({ type: 'runtime-error', message: error instanceof Error ? error.message : String(error) }) } }