@routerlab/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +171 -0
  3. package/dist/commands/eval.d.ts +21 -0
  4. package/dist/commands/eval.d.ts.map +1 -0
  5. package/dist/commands/eval.js +104 -0
  6. package/dist/commands/eval.js.map +1 -0
  7. package/dist/commands/frontier.d.ts +29 -0
  8. package/dist/commands/frontier.d.ts.map +1 -0
  9. package/dist/commands/frontier.js +212 -0
  10. package/dist/commands/frontier.js.map +1 -0
  11. package/dist/commands/help.d.ts +3 -0
  12. package/dist/commands/help.d.ts.map +1 -0
  13. package/dist/commands/help.js +55 -0
  14. package/dist/commands/help.js.map +1 -0
  15. package/dist/commands/models.d.ts +7 -0
  16. package/dist/commands/models.d.ts.map +1 -0
  17. package/dist/commands/models.js +71 -0
  18. package/dist/commands/models.js.map +1 -0
  19. package/dist/commands/route.d.ts +7 -0
  20. package/dist/commands/route.d.ts.map +1 -0
  21. package/dist/commands/route.js +173 -0
  22. package/dist/commands/route.js.map +1 -0
  23. package/dist/commands/version.d.ts +8 -0
  24. package/dist/commands/version.d.ts.map +1 -0
  25. package/dist/commands/version.js +21 -0
  26. package/dist/commands/version.js.map +1 -0
  27. package/dist/errors.d.ts +15 -0
  28. package/dist/errors.d.ts.map +1 -0
  29. package/dist/errors.js +32 -0
  30. package/dist/errors.js.map +1 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +21 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/io.d.ts +33 -0
  36. package/dist/io.d.ts.map +1 -0
  37. package/dist/io.js +47 -0
  38. package/dist/io.js.map +1 -0
  39. package/dist/main.d.ts +14 -0
  40. package/dist/main.d.ts.map +1 -0
  41. package/dist/main.js +64 -0
  42. package/dist/main.js.map +1 -0
  43. package/dist/parse.d.ts +44 -0
  44. package/dist/parse.d.ts.map +1 -0
  45. package/dist/parse.js +125 -0
  46. package/dist/parse.js.map +1 -0
  47. package/package.json +30 -0
@@ -0,0 +1,212 @@
1
+ // commands/frontier.ts — read `eval/results/frontier.json` and pretty-print
2
+ // the Pareto frontier for a given task.
3
+ //
4
+ // The frontier file is the public artifact published by the eval harness
5
+ // (`eval/frontier/runner.ts`, landing in this same wave). Schema:
6
+ //
7
+ // {
8
+ // "schema_version": 1,
9
+ // "generated_at": "<iso8601>",
10
+ // "frontiers": {
11
+ // "<taskClass>": [
12
+ // {
13
+ // "model": "claude-haiku-4-5",
14
+ // "provider": "anthropic",
15
+ // "expectedCost": 0.00042,
16
+ // "expectedQuality": 0.87,
17
+ // "qualityLo95"?: 0.82,
18
+ // "qualityHi95"?: 0.91,
19
+ // "n"?: 12
20
+ // },
21
+ // ...
22
+ // ]
23
+ // }
24
+ // }
25
+ //
26
+ // We accept either the absolute path via `ROUTERLAB_FRONTIER_PATH` env var
27
+ // or the default repo-relative location. Both are validated at read time.
28
+ import { readFileSync } from "node:fs";
29
+ import { parseArgs } from "node:util";
30
+ import { resolve as resolvePath, dirname } from "node:path";
31
+ import { fileURLToPath } from "node:url";
32
+ import { CliError, EXIT_DOWNSTREAM, EXIT_INVALID_INPUT } from "../errors.js";
33
+ import { writeLine } from "../io.js";
34
+ import { parseFormat, parseTask } from "../parse.js";
35
+ // Resolve repo-relative path from this module's location. File lives at
36
+ // `packages/cli/src/commands/frontier.ts`; frontier.json is at
37
+ // `eval/results/frontier.json` (4 levels up to repo root, then down).
38
+ const DEFAULT_FRONTIER_PATH = resolvePath(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "eval", "results", "frontier.json");
39
+ const FRONTIER_PATH_ENV_VAR = "ROUTERLAB_FRONTIER_PATH";
40
+ /**
41
+ * Parse and execute the `frontier` subcommand.
42
+ */
43
+ export function runFrontier(ctx) {
44
+ let parsed;
45
+ try {
46
+ parsed = parseArgs({
47
+ args: [...ctx.argv],
48
+ options: {
49
+ task: { type: "string" },
50
+ format: { type: "string" },
51
+ path: { type: "string" },
52
+ },
53
+ strict: true,
54
+ allowPositionals: false,
55
+ });
56
+ }
57
+ catch (e) {
58
+ throw new CliError(EXIT_INVALID_INPUT, `frontier: ${e instanceof Error ? e.message : String(e)}`);
59
+ }
60
+ const task = parseTask(parsed.values["task"]);
61
+ const format = parseFormat(parsed.values["format"], "table");
62
+ const pathOverride = parsed.values["path"];
63
+ const path = resolveFrontierPath(ctx, typeof pathOverride === "string" ? pathOverride : undefined);
64
+ const file = loadFrontierFile(path);
65
+ const rows = file.frontiers[task];
66
+ if (rows === undefined || rows.length === 0) {
67
+ throw new CliError(EXIT_DOWNSTREAM, `frontier: no rows for task "${task}" in ${path}`);
68
+ }
69
+ if (format === "json") {
70
+ writeLine(ctx.stdout, JSON.stringify({
71
+ task,
72
+ generated_at: file.generated_at,
73
+ rows,
74
+ }, null, 2));
75
+ return 0;
76
+ }
77
+ renderTable(task, rows, file, ctx);
78
+ return 0;
79
+ }
80
+ /**
81
+ * Resolve which frontier.json path to read. Precedence:
82
+ * 1. `--path=<p>` flag.
83
+ * 2. `ROUTERLAB_FRONTIER_PATH` env var.
84
+ * 3. The repo-default absolute path.
85
+ */
86
+ function resolveFrontierPath(ctx, pathFlag) {
87
+ if (pathFlag !== undefined && pathFlag.length > 0) {
88
+ return resolvePath(ctx.cwd, pathFlag);
89
+ }
90
+ const fromEnv = ctx.env[FRONTIER_PATH_ENV_VAR];
91
+ if (typeof fromEnv === "string" && fromEnv.length > 0) {
92
+ return fromEnv;
93
+ }
94
+ return DEFAULT_FRONTIER_PATH;
95
+ }
96
+ function loadFrontierFile(path) {
97
+ let raw;
98
+ try {
99
+ raw = readFileSync(path, "utf8");
100
+ }
101
+ catch (e) {
102
+ throw new CliError(EXIT_DOWNSTREAM, `frontier: cannot read ${path}: ${e instanceof Error ? e.message : String(e)}`);
103
+ }
104
+ let parsed;
105
+ try {
106
+ parsed = JSON.parse(raw);
107
+ }
108
+ catch (e) {
109
+ throw new CliError(EXIT_DOWNSTREAM, `frontier: ${path} is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
110
+ }
111
+ if (parsed === null || typeof parsed !== "object") {
112
+ throw new CliError(EXIT_DOWNSTREAM, `frontier: ${path} root is not an object`);
113
+ }
114
+ const obj = parsed;
115
+ const frontiers = obj["frontiers"];
116
+ if (frontiers === null || typeof frontiers !== "object") {
117
+ throw new CliError(EXIT_DOWNSTREAM, `frontier: ${path} missing required "frontiers" object`);
118
+ }
119
+ // Schema validation is intentionally shallow — we just verify the rows
120
+ // have the fields we render so a malformed row produces a clean message.
121
+ const normalized = {};
122
+ for (const [k, v] of Object.entries(frontiers)) {
123
+ if (!Array.isArray(v)) {
124
+ throw new CliError(EXIT_DOWNSTREAM, `frontier: ${path} key "frontiers.${k}" is not an array`);
125
+ }
126
+ normalized[k] = v.map((row, idx) => validateRow(row, k, idx, path));
127
+ }
128
+ return {
129
+ schema_version: typeof obj["schema_version"] === "number" ? obj["schema_version"] : undefined,
130
+ generated_at: typeof obj["generated_at"] === "string" ? obj["generated_at"] : undefined,
131
+ frontiers: normalized,
132
+ };
133
+ }
134
+ function validateRow(row, taskKey, idx, path) {
135
+ if (row === null || typeof row !== "object") {
136
+ throw new CliError(EXIT_DOWNSTREAM, `frontier: ${path} frontiers.${taskKey}[${idx}] is not an object`);
137
+ }
138
+ const r = row;
139
+ const requireString = (key) => {
140
+ const v = r[key];
141
+ if (typeof v !== "string" || v.length === 0) {
142
+ throw new CliError(EXIT_DOWNSTREAM, `frontier: ${path} frontiers.${taskKey}[${idx}].${key} must be a non-empty string`);
143
+ }
144
+ return v;
145
+ };
146
+ const requireNumber = (key) => {
147
+ const v = r[key];
148
+ if (typeof v !== "number" || !Number.isFinite(v)) {
149
+ throw new CliError(EXIT_DOWNSTREAM, `frontier: ${path} frontiers.${taskKey}[${idx}].${key} must be a finite number`);
150
+ }
151
+ return v;
152
+ };
153
+ const optionalNumber = (key) => {
154
+ const v = r[key];
155
+ if (v === undefined)
156
+ return undefined;
157
+ if (typeof v !== "number" || !Number.isFinite(v)) {
158
+ throw new CliError(EXIT_DOWNSTREAM, `frontier: ${path} frontiers.${taskKey}[${idx}].${key} must be a finite number`);
159
+ }
160
+ return v;
161
+ };
162
+ const out = {
163
+ model: requireString("model"),
164
+ provider: requireString("provider"),
165
+ expectedCost: requireNumber("expectedCost"),
166
+ expectedQuality: requireNumber("expectedQuality"),
167
+ };
168
+ const lo = optionalNumber("qualityLo95");
169
+ if (lo !== undefined)
170
+ out.qualityLo95 = lo;
171
+ const hi = optionalNumber("qualityHi95");
172
+ if (hi !== undefined)
173
+ out.qualityHi95 = hi;
174
+ const n = optionalNumber("n");
175
+ if (n !== undefined)
176
+ out.n = n;
177
+ return out;
178
+ }
179
+ function renderTable(task, rows, file, ctx) {
180
+ const header = `Pareto frontier — task: ${task}`;
181
+ writeLine(ctx.stdout, header);
182
+ writeLine(ctx.stdout, "=".repeat(header.length));
183
+ if (typeof file.generated_at === "string") {
184
+ writeLine(ctx.stdout, `generated_at: ${file.generated_at}`);
185
+ }
186
+ writeLine(ctx.stdout, "");
187
+ // Sort by cost ascending so the cheapest comes first; this is the natural
188
+ // reading order for "what should I pick at quality bar X?" workflows.
189
+ const sorted = [...rows].sort((a, b) => a.expectedCost - b.expectedCost);
190
+ const headers = ["model", "provider", "cost", "quality", "CI95", "n"];
191
+ const data = sorted.map((r) => [
192
+ r.model,
193
+ r.provider,
194
+ `$${r.expectedCost.toFixed(6)}`,
195
+ r.expectedQuality.toFixed(3),
196
+ r.qualityLo95 !== undefined && r.qualityHi95 !== undefined
197
+ ? `${r.qualityLo95.toFixed(3)}..${r.qualityHi95.toFixed(3)}`
198
+ : "—",
199
+ r.n !== undefined ? String(r.n) : "—",
200
+ ]);
201
+ const widths = headers.map((h, col) => {
202
+ const cells = data.map((row) => row[col] ?? "");
203
+ return Math.max(h.length, ...cells.map((s) => s.length));
204
+ });
205
+ const fmt = (cols) => cols.map((s, i) => s.padEnd(widths[i] ?? s.length)).join(" ");
206
+ writeLine(ctx.stdout, fmt(headers));
207
+ writeLine(ctx.stdout, fmt(widths.map((w) => "-".repeat(w))));
208
+ for (const row of data) {
209
+ writeLine(ctx.stdout, fmt(row));
210
+ }
211
+ }
212
+ //# sourceMappingURL=frontier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontier.js","sourceRoot":"","sources":["../../src/commands/frontier.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { type CliContext } from "../io.ts";
2
+ export declare function runHelp(ctx: CliContext): number;
3
+ //# sourceMappingURL=help.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../src/commands/help.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAa,MAAM,UAAU,CAAC;AA+CtD,wBAAgB,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAG/C"}
@@ -0,0 +1,55 @@
1
+ // commands/help.ts — top-level usage banner.
2
+ //
3
+ // Renders the multi-subcommand help text. The text is intentionally
4
+ // long-form rather than a terse listing because the CLI is the user-facing
5
+ // surface for the project and the primary entry point for adoption.
6
+ import { writeLine } from "../io.js";
7
+ const HELP_TEXT = `routerlab CLI — cost-quality routing for LLM APIs.
8
+
9
+ USAGE
10
+ route <subcommand> [flags]
11
+
12
+ SUBCOMMANDS
13
+ route Route a single prompt to the cheapest model meeting a quality bar.
14
+ frontier Pretty-print the published Pareto frontier for a task.
15
+ models List candidate models (optionally filtered by provider).
16
+ eval Run eval pipelines. Currently: \`eval frontier --task=<t>\`.
17
+ version Print CLI + core versions.
18
+ help Show this message.
19
+
20
+ EXAMPLES
21
+ # Route a prompt for QA at quality bar 0.85, with a $0.005 budget cap:
22
+ echo "What's the capital of France?" \\
23
+ | route route --task=qa --quality-bar=0.85 --max-cost-usd=0.005
24
+
25
+ # Same, but raw JSON for piping into jq:
26
+ route route --task=qa --quality-bar=0.85 --input=prompt.txt --json
27
+
28
+ # Look up the frontier for codegen at quality bar 0.85:
29
+ route frontier --task=codegen
30
+
31
+ # List candidate models on Anthropic only:
32
+ route models --provider=anthropic
33
+
34
+ # Print versions:
35
+ route version
36
+
37
+ FLAGS (route subcommand)
38
+ --task=<qa|codegen|summarization|classification|reasoning> (required)
39
+ --quality-bar=<float in [0, 1]> (required)
40
+ --input=<path> read prompt from file
41
+ --max-cost-usd=<number> hard budget cap
42
+ --max-latency-ms=<number> hard latency cap
43
+ --json emit raw RouteDecision JSON
44
+
45
+ EXIT CODES
46
+ 0 success
47
+ 1 no candidates pass the filters
48
+ 2 invalid input
49
+ 3 downstream error (calibration / runner / missing artifact)
50
+ `;
51
+ export function runHelp(ctx) {
52
+ writeLine(ctx.stdout, HELP_TEXT.trimEnd());
53
+ return 0;
54
+ }
55
+ //# sourceMappingURL=help.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help.js","sourceRoot":"","sources":["../../src/commands/help.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,EAAE;AACF,oEAAoE;AACpE,2EAA2E;AAC3E,oEAAoE;AAEpE,OAAO,EAAmB,SAAS,EAAE,MAAM,UAAU,CAAC;AAEtD,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CjB,CAAC;AAEF,MAAM,UAAU,OAAO,CAAC,GAAe;IACrC,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3C,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { type CliContext } from "../io.ts";
2
+ /**
3
+ * Parse and execute the `models` subcommand. Prints one model per line in
4
+ * a compact column layout, or JSON when `--json` is set.
5
+ */
6
+ export declare function runModels(ctx: CliContext): number;
7
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/commands/models.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,UAAU,EAAa,MAAM,UAAU,CAAC;AAGtD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAgCjD"}
@@ -0,0 +1,71 @@
1
+ // commands/models.ts — list candidate models from the engine's pool.
2
+ //
3
+ // Useful for answering "what can routerlab even route to?" without
4
+ // digging into `packages/core/src/candidates.json`. Supports an optional
5
+ // `--provider=<p>` filter for narrowing the listing.
6
+ import { parseArgs } from "node:util";
7
+ import { getDefaultCandidates } from "@routerlab/core";
8
+ import { CliError, EXIT_INVALID_INPUT } from "../errors.js";
9
+ import { writeLine } from "../io.js";
10
+ import { parseProviderOptional } from "../parse.js";
11
+ /**
12
+ * Parse and execute the `models` subcommand. Prints one model per line in
13
+ * a compact column layout, or JSON when `--json` is set.
14
+ */
15
+ export function runModels(ctx) {
16
+ let parsed;
17
+ try {
18
+ parsed = parseArgs({
19
+ args: [...ctx.argv],
20
+ options: {
21
+ provider: { type: "string" },
22
+ json: { type: "boolean", default: false },
23
+ },
24
+ strict: true,
25
+ allowPositionals: false,
26
+ });
27
+ }
28
+ catch (e) {
29
+ throw new CliError(EXIT_INVALID_INPUT, `models: ${e instanceof Error ? e.message : String(e)}`);
30
+ }
31
+ const provider = parseProviderOptional(parsed.values["provider"]);
32
+ const json = parsed.values["json"] === true;
33
+ const all = getDefaultCandidates();
34
+ const filtered = provider === undefined ? all : all.filter((c) => c.provider === provider);
35
+ if (json) {
36
+ writeLine(ctx.stdout, JSON.stringify({ candidates: filtered }, null, 2));
37
+ return 0;
38
+ }
39
+ renderTable(filtered, ctx);
40
+ return 0;
41
+ }
42
+ /**
43
+ * Render the candidate list as a fixed-width table on stdout. Columns are
44
+ * width-padded so values line up; this is a deliberate choice over a CSV /
45
+ * TSV form because the primary audience is humans reading at a terminal.
46
+ */
47
+ function renderTable(rows, ctx) {
48
+ if (rows.length === 0) {
49
+ writeLine(ctx.stdout, "(no candidates match filter)");
50
+ return;
51
+ }
52
+ const headers = ["provider", "model", "input $/MTok", "output $/MTok", "context"];
53
+ const data = rows.map((c) => [
54
+ c.provider,
55
+ c.model,
56
+ c.pricing.inputUsdPerMtok.toFixed(2),
57
+ c.pricing.outputUsdPerMtok.toFixed(2),
58
+ c.contextWindow.toLocaleString(),
59
+ ]);
60
+ const widths = headers.map((h, col) => {
61
+ const cells = data.map((row) => row[col] ?? "");
62
+ return Math.max(h.length, ...cells.map((s) => s.length));
63
+ });
64
+ const fmt = (cols) => cols.map((s, i) => s.padEnd(widths[i] ?? s.length)).join(" ");
65
+ writeLine(ctx.stdout, fmt(headers));
66
+ writeLine(ctx.stdout, fmt(widths.map((w) => "-".repeat(w))));
67
+ for (const row of data) {
68
+ writeLine(ctx.stdout, fmt(row));
69
+ }
70
+ }
71
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/commands/models.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { type CliContext } from "../io.ts";
2
+ /**
3
+ * Parse and execute the `route` subcommand against the provided context.
4
+ * Returns the exit code; never throws (all errors are converted).
5
+ */
6
+ export declare function runRoute(ctx: CliContext): Promise<number>;
7
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/commands/route.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAE,KAAK,UAAU,EAAgC,MAAM,UAAU,CAAC;AAQzE;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA2E/D"}
@@ -0,0 +1,173 @@
1
+ // commands/route.ts — the `route` subcommand.
2
+ //
3
+ // Reads a prompt (from `--input <file>` or stdin), calls the routing engine,
4
+ // and prints either a human-friendly table or raw JSON (`--json`).
5
+ //
6
+ // Exit codes (see ../errors.ts):
7
+ // 0 success, 1 no candidates, 2 invalid input, 3 downstream error.
8
+ //
9
+ // This subcommand is the user-facing surface for routerlab — most users
10
+ // will only ever touch this and `frontier`. Output is engineered to be
11
+ // scannable: chosen pick first, then ordered fallbacks, then a collapsed
12
+ // skipped list with reasons.
13
+ import { readFile } from "node:fs/promises";
14
+ import { parseArgs } from "node:util";
15
+ import { resolve as resolvePath } from "node:path";
16
+ import { route, } from "@routerlab/core";
17
+ import { CliError, EXIT_INVALID_INPUT, EXIT_NO_CANDIDATES } from "../errors.js";
18
+ import { readStdinToString, writeLine } from "../io.js";
19
+ import { parseNonNegativeFloatOptional, parseQualityBar, parseTask, } from "../parse.js";
20
+ /**
21
+ * Parse and execute the `route` subcommand against the provided context.
22
+ * Returns the exit code; never throws (all errors are converted).
23
+ */
24
+ export async function runRoute(ctx) {
25
+ let parsed;
26
+ try {
27
+ parsed = parseArgs({
28
+ args: [...ctx.argv],
29
+ options: {
30
+ task: { type: "string" },
31
+ "quality-bar": { type: "string" },
32
+ input: { type: "string" },
33
+ "max-cost-usd": { type: "string" },
34
+ "max-latency-ms": { type: "string" },
35
+ json: { type: "boolean", default: false },
36
+ },
37
+ strict: true,
38
+ allowPositionals: false,
39
+ });
40
+ }
41
+ catch (e) {
42
+ throw new CliError(EXIT_INVALID_INPUT, `route: ${e instanceof Error ? e.message : String(e)}`);
43
+ }
44
+ const task = parseTask(parsed.values["task"]);
45
+ const qualityBar = parseQualityBar(parsed.values["quality-bar"]);
46
+ const maxCostUsd = parseNonNegativeFloatOptional("max-cost-usd", parsed.values["max-cost-usd"]);
47
+ const maxLatencyMs = parseNonNegativeFloatOptional("max-latency-ms", parsed.values["max-latency-ms"]);
48
+ const inputPathRaw = parsed.values["input"];
49
+ const json = parsed.values["json"] === true;
50
+ const prompt = await readPrompt(ctx, inputPathRaw);
51
+ if (prompt.trim().length === 0) {
52
+ throw new CliError(EXIT_INVALID_INPUT, "route: prompt is empty. Pass --input=<file> or pipe text via stdin.");
53
+ }
54
+ const request = {
55
+ task,
56
+ prompt,
57
+ qualityBar,
58
+ ...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
59
+ ...(maxLatencyMs !== undefined ? { maxLatencyMs } : {}),
60
+ };
61
+ let decision;
62
+ try {
63
+ decision = route(request);
64
+ }
65
+ catch (e) {
66
+ const msg = e instanceof Error ? e.message : String(e);
67
+ // The engine throws a Plain `Error` whose message starts with this
68
+ // sentinel when no candidates passed filtering. Map that to exit 1.
69
+ if (msg.includes("no candidates passed filtering")) {
70
+ throw new CliError(EXIT_NO_CANDIDATES, msg);
71
+ }
72
+ // Engine-level validation errors are user input issues.
73
+ if (msg.startsWith("route(): ")) {
74
+ throw new CliError(EXIT_INVALID_INPUT, msg);
75
+ }
76
+ throw e;
77
+ }
78
+ if (json) {
79
+ writeLine(ctx.stdout, JSON.stringify(decision, null, 2));
80
+ }
81
+ else {
82
+ renderDecision(decision, ctx);
83
+ }
84
+ return 0;
85
+ }
86
+ /**
87
+ * Resolve the prompt source: `--input <file>` wins over stdin.
88
+ *
89
+ * - If `--input` is given, the file is read from `ctx.cwd`-relative path.
90
+ * - Else stdin is drained to a string (unless the caller is on a TTY, in
91
+ * which case an empty string is returned and the caller raises a usage
92
+ * error — see `runRoute` above).
93
+ */
94
+ async function readPrompt(ctx, inputPathRaw) {
95
+ if (typeof inputPathRaw === "string" && inputPathRaw.length > 0) {
96
+ const absPath = resolvePath(ctx.cwd, inputPathRaw);
97
+ try {
98
+ return await readFile(absPath, "utf8");
99
+ }
100
+ catch (e) {
101
+ throw new CliError(EXIT_INVALID_INPUT, `route: cannot read --input "${inputPathRaw}": ${e instanceof Error ? e.message : String(e)}`);
102
+ }
103
+ }
104
+ return await readStdinToString(ctx.stdin);
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // Rendering
108
+ // ---------------------------------------------------------------------------
109
+ /**
110
+ * Render a routing decision as a human-friendly multi-line block to stdout.
111
+ *
112
+ * Layout:
113
+ *
114
+ * Decision: <model> (<provider>)
115
+ * expected cost: $0.001234
116
+ * expected quality: 0.870
117
+ * reasoning: <engine reasoning>
118
+ *
119
+ * Fallbacks (n):
120
+ * 1. <model> (<provider>) $cost q=quality
121
+ *
122
+ * Skipped (n):
123
+ * - <model>: <reason>
124
+ */
125
+ function renderDecision(decision, ctx) {
126
+ const out = ctx.stdout;
127
+ renderChosen(decision.chosen, out);
128
+ if (decision.fallbacks.length > 0) {
129
+ writeLine(out, "");
130
+ writeLine(out, "Fallbacks (ordered by next-cheapest):");
131
+ decision.fallbacks.forEach((fb, i) => {
132
+ writeLine(out, formatFallbackLine(fb, i + 1));
133
+ });
134
+ }
135
+ if (decision.skipped.length > 0) {
136
+ writeLine(out, "");
137
+ writeLine(out, `Skipped (${decision.skipped.length}):`);
138
+ for (const s of decision.skipped) {
139
+ writeLine(out, formatSkippedLine(s));
140
+ }
141
+ }
142
+ }
143
+ function renderChosen(chosen, out) {
144
+ writeLine(out, `Decision: ${chosen.model.model} (${chosen.model.provider})`);
145
+ writeLine(out, ` expected cost: ${formatUsd(chosen.expectedCost)}`);
146
+ writeLine(out, ` expected quality: ${formatQuality(chosen.expectedQuality)}`);
147
+ writeLine(out, ` reasoning: ${chosen.reasoning}`);
148
+ }
149
+ function formatFallbackLine(fb, rank) {
150
+ // The engine encodes cost + quality in the reason string. We display the
151
+ // canonical model/provider line and append the reason for traceability.
152
+ const head = ` ${rank}. ${fb.model.model} (${fb.model.provider})`;
153
+ return `${head} ${fb.reason}`;
154
+ }
155
+ function formatSkippedLine(s) {
156
+ return ` - ${s.model.model}: ${s.reason}`;
157
+ }
158
+ /**
159
+ * Format a USD amount with six fractional digits — that's enough to
160
+ * distinguish models at the low end of the pricing pool (Groq llama-3.1-8b
161
+ * at $0.05/MTok input).
162
+ */
163
+ function formatUsd(usd) {
164
+ return `$${usd.toFixed(6)}`;
165
+ }
166
+ /**
167
+ * Format a quality score to 3 decimal places — matches the engine's own
168
+ * reasoning string format so the two are visually aligned.
169
+ */
170
+ function formatQuality(q) {
171
+ return q.toFixed(3);
172
+ }
173
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../../src/commands/route.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { type CliContext } from "../io.ts";
2
+ export declare const CLI_VERSION = "0.0.1";
3
+ /**
4
+ * `version` subcommand: prints two lines, `@routerlab/cli` first then
5
+ * `@routerlab/core`. Returns 0 unconditionally.
6
+ */
7
+ export declare function runVersion(ctx: CliContext): number;
8
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/commands/version.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,UAAU,EAAa,MAAM,UAAU,CAAC;AAItD,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAIlD"}
@@ -0,0 +1,21 @@
1
+ // commands/version.ts — print CLI + core versions.
2
+ //
3
+ // We import the core version from `@routerlab/core` directly so the two
4
+ // numbers can drift independently and the CLI honestly reports both.
5
+ // CLI version is read from its own package.json at build time via the
6
+ // constant below — keep this in lockstep with packages/cli/package.json.
7
+ import { version as coreVersion } from "@routerlab/core";
8
+ import { writeLine } from "../io.js";
9
+ // Pinned to packages/cli/package.json. If you bump `package.json`, bump
10
+ // this too — the repo's CI catches drift between the two via test.
11
+ export const CLI_VERSION = "0.0.1";
12
+ /**
13
+ * `version` subcommand: prints two lines, `@routerlab/cli` first then
14
+ * `@routerlab/core`. Returns 0 unconditionally.
15
+ */
16
+ export function runVersion(ctx) {
17
+ writeLine(ctx.stdout, `@routerlab/cli ${CLI_VERSION}`);
18
+ writeLine(ctx.stdout, `@routerlab/core ${coreVersion}`);
19
+ return 0;
20
+ }
21
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/commands/version.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,EAAE;AACF,wEAAwE;AACxE,qEAAqE;AACrE,sEAAsE;AACtE,yEAAyE;AAEzE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAmB,SAAS,EAAE,MAAM,UAAU,CAAC;AAEtD,wEAAwE;AACxE,mEAAmE;AACnE,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC;AAEnC;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,GAAe;IACxC,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,kBAAkB,WAAW,EAAE,CAAC,CAAC;IACvD,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,mBAAmB,WAAW,EAAE,CAAC,CAAC;IACxD,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,15 @@
1
+ export declare const EXIT_SUCCESS = 0;
2
+ export declare const EXIT_NO_CANDIDATES = 1;
3
+ export declare const EXIT_INVALID_INPUT = 2;
4
+ export declare const EXIT_DOWNSTREAM = 3;
5
+ export type CliExitCode = typeof EXIT_SUCCESS | typeof EXIT_NO_CANDIDATES | typeof EXIT_INVALID_INPUT | typeof EXIT_DOWNSTREAM;
6
+ /**
7
+ * The one error type the CLI throws for expected, user-facing failures.
8
+ * Unknown exceptions still bubble up to `main()` and are mapped to
9
+ * `EXIT_DOWNSTREAM` with the underlying message preserved.
10
+ */
11
+ export declare class CliError extends Error {
12
+ readonly code: CliExitCode;
13
+ constructor(code: CliExitCode, message: string);
14
+ }
15
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC,MAAM,MAAM,WAAW,GACnB,OAAO,YAAY,GACnB,OAAO,kBAAkB,GACzB,OAAO,kBAAkB,GACzB,OAAO,eAAe,CAAC;AAE3B;;;;GAIG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;gBAEf,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM;CAK/C"}