crivo-web / src /cli.ts
dizyann's picture
feat: support the full Crivo model family
6986878
Raw History Blame Contribute Delete
3.86 kB
#!/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<CrivoLoadOptions['device']>
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 <size>', 'Crivo model size (latest upstream checkpoint)')
.choices([...MODEL_SIZES])
.default('150m')
)
.argument('<input>', 'JSON file path, or - to read JSON from stdin')
.addOption(
new Option('--device <device>', 'execution device')
.choices(['auto', 'cpu', 'gpu'])
.default('auto')
)
.addOption(
new Option('--dtype <dtype>', 'model precision (default depends on device)').choices([
'q8',
'fp32'
])
)
.option('--cache-dir <path>', '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)
})
}
}