orcacompass 0.0.0-stage → 0.1.0

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 (42) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +51 -0
  3. package/README.md +171 -3
  4. package/data/modelindex-snapshot.json +1537 -0
  5. package/data/models-snapshot.json +1356 -0
  6. package/dist/cli.js +906 -0
  7. package/dist/config.js +272 -0
  8. package/dist/install.js +56 -0
  9. package/dist/paths.js +30 -0
  10. package/dist/placements/agents/antigravity.js +87 -0
  11. package/dist/placements/agents/claude.js +72 -0
  12. package/dist/placements/agents/codex.js +66 -0
  13. package/dist/placements/agents/cursor.js +102 -0
  14. package/dist/placements/agents/deps.js +19 -0
  15. package/dist/placements/agents/grok.js +92 -0
  16. package/dist/placements/agents/list-cache.js +85 -0
  17. package/dist/placements/agents/muse.js +7 -0
  18. package/dist/placements/agents/opencode.js +77 -0
  19. package/dist/placements/agents/orca-catalog.js +34 -0
  20. package/dist/placements/benchmark.js +70 -0
  21. package/dist/placements/build.js +94 -0
  22. package/dist/placements/score.js +70 -0
  23. package/dist/placements/sources/artificialanalysis.js +133 -0
  24. package/dist/placements/sources/modelindex.js +84 -0
  25. package/dist/placements/sources/modelsdev.js +113 -0
  26. package/dist/placements/table.js +94 -0
  27. package/dist/placements/tiers.js +136 -0
  28. package/dist/routing/effort.js +31 -0
  29. package/dist/routing/environment.js +51 -0
  30. package/dist/routing/orca-settings.js +111 -0
  31. package/dist/routing/orca-usage.js +85 -0
  32. package/dist/routing/pace.js +52 -0
  33. package/dist/routing/rejection.js +58 -0
  34. package/dist/routing/rejections.js +18 -0
  35. package/dist/routing/router.js +552 -0
  36. package/dist/setup.js +358 -0
  37. package/dist/system/run.js +128 -0
  38. package/dist/types.js +8 -0
  39. package/dist/verify.js +106 -0
  40. package/dist/version.js +3 -0
  41. package/package.json +60 -6
  42. package/skills/orcacompass/SKILL.md +278 -0
package/dist/cli.js ADDED
@@ -0,0 +1,906 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, realpathSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { parseArgs } from 'node:util';
5
+ import { applyAgentChoice, ConfigError, isChosen, loadConfig, parsePercent, setBans, setPin, setPreferRiskLimit, updateConfigFile, writeAgentChoice, } from './config.js';
6
+ import { parseEffort } from './routing/effort.js';
7
+ import { poolState } from './routing/pace.js';
8
+ import { activeRejections } from './routing/rejection.js';
9
+ import { agentNotFamily, agentSkipReason, banMatches, enabledInOrca, formatLaunch, headroomText, launchIds, rejectedText, route } from './routing/router.js';
10
+ import { AGENT_IDS, DEMANDS, TIERS, } from './types.js';
11
+ import { gather, gatherEnvironment, loadPlacements } from './routing/environment.js';
12
+ import { AA_KEY_ENV } from './placements/sources/artificialanalysis.js';
13
+ import { runCommand } from './system/run.js';
14
+ import { configPath } from './paths.js';
15
+ import { readRejections, recordRejection, writeRejections } from './routing/rejections.js';
16
+ import { install } from './install.js';
17
+ import { setupInfo, setupWizard } from './setup.js';
18
+ import { verifyWorker } from './verify.js';
19
+ const USAGE = `usage: orcacompass <command> [options]
20
+
21
+ commands:
22
+ install [-g|--global] [-y|--yes] [-a|--agent <name>]... [--skill-only]
23
+ install the Skill, then the CLI if it is missing
24
+ route --tier <light|daily|advanced|frontier> [--no-downgrade] [--agent <id>]...
25
+ [--exclude-agent <id>]... [--demand <low|moderate|high|exceptional>]
26
+ [--effort <effort>] [--tier-reason <reason>] [--json]
27
+ verify --dispatch <id> [--wait <seconds>] [--json]
28
+ did a new worker's agent accept its model
29
+ rejected [--clear] [--json] models agents refused at launch (skipped)
30
+ status [--json]
31
+ tiers [--json] every model's tiers, from the placement table
32
+ agents [--json] which agents are used, and why not
33
+ agents disable|enable|only <id>... [--json]
34
+ agents reset [--json] use every agent again
35
+ prefer [--json] preferred route per tier
36
+ prefer <tier> <agent> [model] [--json]
37
+ prefer clear <tier>|--all [--json]
38
+ prefer --risk-limit <percent> [--json]
39
+ ban [<model-or-family>...] [--json] never route these models
40
+ unban <model-or-family>... [--json]
41
+ refresh [--json] refetch metadata and rebuild the placement table
42
+ setup answer a few questions (needs a terminal)
43
+ setup --json what the setup questions need, for a chat setup
44
+ setup --complete [--json] finish chat setup, including unchanged defaults
45
+ setup --skip [--json] keep the defaults and stop offering setup`;
46
+ class UsageError extends Error {
47
+ }
48
+ /** Runs one command and returns the exit code: 0 answered, 1 usage error, 2 config error. */
49
+ export async function main(argv, io, deps = {}) {
50
+ const [command, ...rest] = argv;
51
+ try {
52
+ if (command === 'help' || command === '--help' || command === '-h') {
53
+ io.out(USAGE);
54
+ return 0;
55
+ }
56
+ if (command === 'route')
57
+ return await routeCommand(rest, io, deps);
58
+ if (command === 'verify')
59
+ return await verifyCommand(rest, io, deps);
60
+ if (command === 'rejected')
61
+ return await rejectedCommand(rest, io, deps);
62
+ if (command === 'status')
63
+ return await statusCommand(rest, io, deps);
64
+ if (command === 'tiers')
65
+ return await tiersCommand(rest, io, deps);
66
+ if (command === 'agents')
67
+ return await agentsCommand(rest, io, deps);
68
+ if (command === 'prefer')
69
+ return await preferCommand(rest, io, deps);
70
+ if (command === 'ban')
71
+ return await banCommand(rest, io, deps);
72
+ if (command === 'unban')
73
+ return await unbanCommand(rest, io, deps);
74
+ if (command === 'refresh')
75
+ return await refreshCommand(rest, io, deps);
76
+ if (command === 'setup')
77
+ return await setupCommand(rest, io, deps);
78
+ if (command === 'install')
79
+ return await installCommand(rest, io, deps);
80
+ throw new UsageError(command ? `unknown command: ${command}` : 'missing command');
81
+ }
82
+ catch (e) {
83
+ if (e instanceof UsageError) {
84
+ io.err(`${e.message}\n\n${USAGE}`);
85
+ return 1;
86
+ }
87
+ if (e instanceof ConfigError) {
88
+ io.err(`config error: ${e.message}`);
89
+ return 2;
90
+ }
91
+ io.err(`error: ${e instanceof Error ? e.message : String(e)}`);
92
+ return 1;
93
+ }
94
+ }
95
+ function parse(args, options) {
96
+ try {
97
+ return parseArgs({ args, options, strict: true, allowPositionals: false }).values;
98
+ }
99
+ catch (e) {
100
+ throw new UsageError(e.message);
101
+ }
102
+ }
103
+ // --- route ----------------------------------------------------------------------------------
104
+ async function routeCommand(args, io, deps) {
105
+ const v = parse(args, {
106
+ tier: { type: 'string' },
107
+ 'tier-reason': { type: 'string' },
108
+ 'no-downgrade': { type: 'boolean' },
109
+ agent: { type: 'string', multiple: true },
110
+ 'exclude-agent': { type: 'string', multiple: true },
111
+ effort: { type: 'string' },
112
+ demand: { type: 'string' },
113
+ json: { type: 'boolean' },
114
+ });
115
+ if (v.tier === undefined)
116
+ throw new UsageError(`--tier is required (${TIERS.join(', ')})`);
117
+ const tier = v.tier.toLowerCase();
118
+ if (!TIERS.includes(tier))
119
+ throw new UsageError(`unknown tier "${v.tier}" (expected ${TIERS.join(', ')})`);
120
+ const tierReason = v['tier-reason']?.trim();
121
+ if (tierReason === '')
122
+ throw new UsageError('--tier-reason must contain a short explanation');
123
+ let effort;
124
+ if (v.effort !== undefined) {
125
+ effort = parseEffort(v.effort);
126
+ if (!effort)
127
+ throw new UsageError(`unknown effort "${v.effort}"`);
128
+ }
129
+ let demand;
130
+ if (v.demand !== undefined) {
131
+ demand = v.demand.toLowerCase();
132
+ if (!DEMANDS.includes(demand))
133
+ throw new UsageError(`unknown demand "${v.demand}" (expected ${DEMANDS.join(', ')})`);
134
+ }
135
+ // An unknown id is not an error: the route goes on and a warning suggests the id that was probably meant.
136
+ const unknownAgents = [];
137
+ const agentIds = (raw, flag, effect) => raw?.map((r) => {
138
+ const id = r.toLowerCase();
139
+ if (!AGENT_IDS.includes(id)) {
140
+ const guess = suggestAgent(id);
141
+ unknownAgents.push(`unknown agent "${r}" in ${flag}: ${effect} (${guess ? `did you mean ${guess}?` : `known: ${AGENT_IDS.join(', ')}`})`);
142
+ }
143
+ return id;
144
+ });
145
+ const agents = agentIds(v.agent, '--agent', 'not routed');
146
+ const excludeAgents = agentIds(v['exclude-agent'], '--exclude-agent', 'nothing excluded for it');
147
+ const req = {
148
+ tier,
149
+ ...(tierReason ? { tierReason } : {}),
150
+ ...(v['no-downgrade'] ? { noDowngrade: true } : {}),
151
+ ...(agents ? { agents } : {}),
152
+ ...(excludeAgents ? { excludeAgents } : {}),
153
+ ...(effort ? { effort } : {}),
154
+ ...(demand ? { demand } : {}),
155
+ };
156
+ const config = loadConfig();
157
+ const env = await gatherEnvironment(config, deps);
158
+ const d = route(req, env, config);
159
+ d.warnings.push(...unknownAgents);
160
+ if (v.json) {
161
+ const sources = {
162
+ catalogSource: env.catalogSource, scoreSource: env.scoreSource ?? 'unavailable', benchmarkSource: env.benchmarkSource ?? 'unavailable',
163
+ placementsBuiltAt: env.placementsBuiltAt,
164
+ };
165
+ io.out(JSON.stringify({ ...d, launch: formatLaunch(d) ?? null, ...sources }, null, 2));
166
+ }
167
+ else
168
+ io.out(decisionText(d, env.now).join('\n'));
169
+ return 0;
170
+ }
171
+ /** Executable names people use for an agent, e.g. Cursor's CLI is `cursor-agent`. */
172
+ const AGENT_ALIASES = { 'cursor-agent': 'cursor', agy: 'antigravity', 'claude-code': 'claude' };
173
+ /** The agent id an unknown id most likely meant: an executable alias, or a known id within two edits. */
174
+ function suggestAgent(id) {
175
+ if (AGENT_ALIASES[id])
176
+ return AGENT_ALIASES[id];
177
+ const distance = (a, b) => {
178
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
179
+ for (let i = 1; i <= a.length; i++) {
180
+ const row = [i];
181
+ for (let j = 1; j <= b.length; j++)
182
+ row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
183
+ prev = row;
184
+ }
185
+ return prev[b.length];
186
+ };
187
+ let best;
188
+ for (const known of AGENT_IDS) {
189
+ const d = distance(id, known);
190
+ if (d <= 2 && (!best || d < best.d))
191
+ best = { id: known, d };
192
+ }
193
+ return best?.id;
194
+ }
195
+ function decisionText(d, now) {
196
+ let head;
197
+ if (d.status === 'ok' && d.route) {
198
+ const { agent, model, effort } = d.route;
199
+ head = `${d.tier} → ${agent} · ${model}${effort ? ` · ${effort}` : ''} (${d.confidence} confidence)`;
200
+ }
201
+ else if (d.status === 'wait' && d.waitUntil !== undefined) {
202
+ head = `${d.requestedTier} → wait until ${new Date(d.waitUntil).toISOString()} (in ${duration(d.waitUntil - now)})`;
203
+ }
204
+ else {
205
+ head = `${d.requestedTier} → no route`;
206
+ }
207
+ const launch = formatLaunch(d);
208
+ return [head, ...d.reasons.map((r) => ` ${r}`), ...d.warnings.map((w) => ` warning: ${w}`), ...(launch ? [launch] : [])];
209
+ }
210
+ // --- verify and rejected --------------------------------------------------------------------
211
+ async function verifyCommand(args, io, deps) {
212
+ const v = parse(args, { dispatch: { type: 'string' }, wait: { type: 'string' }, json: { type: 'boolean' } });
213
+ if (!v.dispatch)
214
+ throw new UsageError('--dispatch <id> is required (the dispatchId worker-start printed)');
215
+ const wait = v.wait === undefined ? 60 : Number(v.wait);
216
+ if (!Number.isFinite(wait) || wait < 0 || wait > 60)
217
+ throw new UsageError(`--wait must be between 0 and 60 seconds (got "${v.wait}")`);
218
+ const config = loadConfig();
219
+ const clock = deps.clock ?? Date.now;
220
+ const verdict = await verifyWorker(v.dispatch, {
221
+ run: deps.run ?? runCommand,
222
+ sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
223
+ clock,
224
+ }, { waitMs: wait * 1000 });
225
+ let skipHours;
226
+ if (verdict.status === 'rejected' && verdict.agent && verdict.model && verdict.message) {
227
+ recordRejection({ agent: verdict.agent, model: verdict.model, message: verdict.message, at: clock() }, config.rejectionTtlHours);
228
+ skipHours = config.rejectionTtlHours;
229
+ }
230
+ const next = verifyNext(verdict, skipHours);
231
+ if (v.json)
232
+ io.out(JSON.stringify({ ...verdict, ...(skipHours !== undefined ? { skippedForHours: skipHours } : {}), next }, null, 2));
233
+ else
234
+ io.out(verifyText(verdict, next).join('\n'));
235
+ return 0;
236
+ }
237
+ /** What the coordinator does next, in order. */
238
+ function verifyNext(d, skipHours) {
239
+ const retry = `start the replacement on the same Task: orca orchestration worker-start --task ${d.taskId ?? '<task_id>'} --retry-of ${d.dispatchId} --worktree <same placement> <new launchArgs> --json`;
240
+ const cleanup = d.cleanup.map((argv) => argv.join(' ')).join(', then ');
241
+ const exclude = `--exclude-agent ${d.agent ?? '<agent>'}`;
242
+ switch (d.status) {
243
+ case 'started':
244
+ case 'reported':
245
+ return ['nothing: wait for worker_done as usual'];
246
+ case 'rejected':
247
+ return [
248
+ `clean up: ${cleanup}`,
249
+ `route again with the same flags; ${d.agent} ${d.model} is skipped${skipHours !== undefined ? ` for ${skipHours}h` : ''}`,
250
+ retry,
251
+ ];
252
+ case 'failed':
253
+ return [
254
+ `clean up: ${cleanup}`,
255
+ `if the tail shows the agent failing to start, route again with ${exclude} for this job`,
256
+ retry,
257
+ 'tell the user which agent failed to start, why, and how they can fix it',
258
+ ];
259
+ case 'stalled':
260
+ return [
261
+ 'read the tail: the agent ended its turn without worker_done',
262
+ `if it reports an error (model, account, limits): orca orchestration worker-stop --dispatch ${d.dispatchId} --json, then route again with ${exclude} and start the replacement with --retry-of ${d.dispatchId}`,
263
+ 'if it asks a question, answer it in its terminal; otherwise keep waiting',
264
+ ];
265
+ default:
266
+ return ['keep waiting as usual; run verify again later or read the terminal'];
267
+ }
268
+ }
269
+ function verifyText(d, next) {
270
+ const who = d.agent && d.model ? `${d.agent} · ${d.model}` : d.dispatchId;
271
+ const lines = [`${d.status}: ${who} — ${d.detail}`];
272
+ if (d.message)
273
+ lines.push(` agent said: ${d.message}`);
274
+ for (const l of d.tail ?? [])
275
+ lines.push(` | ${l}`);
276
+ lines.push('next:', ...next.map((n, i) => ` ${i + 1}. ${n}`));
277
+ return lines;
278
+ }
279
+ async function rejectedCommand(args, io, deps) {
280
+ const v = parse(args, { clear: { type: 'boolean' }, json: { type: 'boolean' } });
281
+ const config = loadConfig();
282
+ const now = deps.now ?? Date.now();
283
+ const all = readRejections();
284
+ if (v.clear)
285
+ writeRejections([]);
286
+ const active = v.clear ? [] : activeRejections(all, now, config.rejectionTtlHours);
287
+ if (v.json) {
288
+ io.out(JSON.stringify({ rejectionTtlHours: config.rejectionTtlHours, cleared: v.clear ? all.length : 0, rejections: active }, null, 2));
289
+ return 0;
290
+ }
291
+ if (v.clear)
292
+ io.out(`forgot ${all.length} refused model${all.length === 1 ? '' : 's'}`);
293
+ else if (active.length === 0)
294
+ io.out('no refused models');
295
+ else {
296
+ io.out([
297
+ `refused at launch, skipped for ${config.rejectionTtlHours}h after:`,
298
+ ...active.map((r) => ` ${r.agent} ${r.model}: ${rejectedText(r, now)}`),
299
+ ].join('\n'));
300
+ }
301
+ return 0;
302
+ }
303
+ function buildStatus(gathered, config) {
304
+ const { env, table } = gathered;
305
+ const skipped = new Map();
306
+ const banned = new Map();
307
+ const tiers = {};
308
+ for (const tier of TIERS) {
309
+ const d = route({ tier, noDowngrade: true }, env, config);
310
+ for (const s of d.skipped) {
311
+ if (s.model) {
312
+ if (s.reason === 'banned by you')
313
+ banned.set(`${s.agent} ${s.model}`, { agent: s.agent, model: s.model });
314
+ }
315
+ else {
316
+ skipped.set(s.agent, s.reason);
317
+ }
318
+ }
319
+ // A structural upgrade belongs to another tier; this view lists only the tier's own candidates.
320
+ const own = d.tier === undefined || d.tier === tier;
321
+ const view = { status: own ? d.status : 'none', alternatives: own ? d.alternatives : [], warnings: own ? d.warnings : [] };
322
+ if (own && d.route) {
323
+ const prefix = `${d.route.agent} ${d.route.model}: `;
324
+ const reason = d.reasons[0] ?? '';
325
+ view.pick = { ...d.route, confidence: d.confidence, reason: reason.startsWith(prefix) ? reason.slice(prefix.length) : reason };
326
+ }
327
+ if (own && d.waitUntil !== undefined)
328
+ view.waitUntil = d.waitUntil;
329
+ tiers[tier] = view;
330
+ }
331
+ const agents = env.discoveries.map((disc) => {
332
+ const u = env.usage[disc.agent];
333
+ const usage = u && {
334
+ status: u.status,
335
+ ...(u.updatedAt !== undefined ? { updatedAt: u.updatedAt } : {}),
336
+ ...(u.error ? { error: u.error } : {}),
337
+ pools: Object.values(u.pools).map((p) => {
338
+ const s = poolState(p, env.now, config.thresholds);
339
+ return { ...p, headroom: Math.round(s.headroom * 100) / 100, expired: s.expired, blocked: s.blocked };
340
+ }),
341
+ };
342
+ const reason = skipped.get(disc.agent);
343
+ return {
344
+ agent: disc.agent,
345
+ installed: disc.installed,
346
+ enabledInOrca: enabledInOrca(env.orcaAgents, disc.agent),
347
+ models: disc.models.length,
348
+ usage: usage ?? null,
349
+ ...(disc.note ? { note: disc.note } : {}),
350
+ ...(reason ? { skipped: reason } : {}),
351
+ };
352
+ });
353
+ const unclassified = [];
354
+ const scored = [];
355
+ for (const disc of env.discoveries) {
356
+ for (const m of disc.models) {
357
+ if (m.placements.length === 0)
358
+ unclassified.push({ agent: disc.agent, model: m.modelId });
359
+ for (const cls of m.placements) {
360
+ if (cls.detail)
361
+ scored.push({ agent: disc.agent, model: m.modelId, tier: cls.tier, detail: cls.detail });
362
+ }
363
+ }
364
+ }
365
+ return {
366
+ now: env.now,
367
+ catalog: table.sources.catalog,
368
+ scores: table.sources.scores,
369
+ benchmarks: table.sources.benchmarks,
370
+ placements: placementsView(gathered, env.now),
371
+ orcaAgents: env.orcaAgents ?? null,
372
+ agents,
373
+ banned: [...banned.values()],
374
+ rejected: env.rejections ?? [],
375
+ tiers,
376
+ scored,
377
+ unclassified,
378
+ };
379
+ }
380
+ function statusText(s) {
381
+ const { catalog, now } = s;
382
+ const lines = [
383
+ `catalog: ${catalog.source}, ${catalog.models} models, fetched ${duration(now - catalog.fetchedAt)} ago`,
384
+ s.scores
385
+ ? `scores: ${s.scores.source}, ${s.scores.models} models (The Model Index, CC BY 4.0), fetched ${duration(now - s.scores.fetchedAt)} ago`
386
+ : 'scores: unavailable (only the family table assigns tiers)',
387
+ s.benchmarks
388
+ ? `benchmarks: ${s.benchmarks.source}, ${s.benchmarks.models} models (Source: Artificial Analysis, artificialanalysis.ai), fetched ${duration(now - s.benchmarks.fetchedAt)} ago`
389
+ : `benchmarks: unavailable (set ${AA_KEY_ENV} to your Artificial Analysis key and run orcacompass refresh)`,
390
+ ...placementsLines(s.placements, now),
391
+ orcaAgentsLine(s.orcaAgents ?? undefined),
392
+ 'agents:',
393
+ ];
394
+ const width = Math.max(...s.agents.map((a) => a.agent.length));
395
+ for (const a of s.agents) {
396
+ const name = a.agent.padEnd(width);
397
+ if (!a.installed) {
398
+ lines.push(` ${name} not installed${a.note ? ` (${a.note})` : ''}`);
399
+ continue;
400
+ }
401
+ const u = a.usage;
402
+ const usage = !u ? 'no usage data' : u.status === 'ok' ? `usage ok${u.updatedAt !== undefined ? `, updated ${duration(now - u.updatedAt)} ago` : ''}` : `usage ${u.status}${u.error ? `: ${u.error}` : ''}`;
403
+ lines.push(` ${name} ${a.models} models, ${usage}${a.skipped ? ` — skipped: ${a.skipped}` : ''}`);
404
+ if (a.note)
405
+ lines.push(` ${' '.repeat(width)} ${a.note}`);
406
+ for (const p of u?.pools ?? []) {
407
+ const state = p.expired ? ' (expired)' : p.blocked ? ' (blocked)' : '';
408
+ lines.push(` ${p.name.replace(/^bucket:/, '')}: ${percent(p.usedPercent)} used, resets in ${duration(p.resetsAt - now)}, headroom ${headroomText(p.headroom)}${state}`);
409
+ }
410
+ }
411
+ lines.push('tiers:');
412
+ for (const tier of TIERS) {
413
+ const t = s.tiers[tier];
414
+ const label = tier.padEnd(8);
415
+ if (t.pick) {
416
+ const { agent, model, effort, confidence, reason } = t.pick;
417
+ lines.push(` ${label} ${agent} · ${model}${effort ? ` · ${effort}` : ''} (${confidence}) — ${reason}`);
418
+ }
419
+ else if (t.status === 'wait' && t.waitUntil !== undefined) {
420
+ lines.push(` ${label} all blocked; wait ${duration(t.waitUntil - now)}`);
421
+ }
422
+ else {
423
+ lines.push(` ${label} no candidates`);
424
+ }
425
+ for (const w of t.warnings)
426
+ lines.push(` ${' '.repeat(8)} warning: ${w}`);
427
+ for (const alt of t.alternatives.slice(0, 3)) {
428
+ lines.push(` ${' '.repeat(8)} alt: ${alt.agent} · ${alt.model}${alt.effort ? ` · ${alt.effort}` : ''}${alt.blocked ? ' (blocked)' : ''} — ${alt.reason}`);
429
+ }
430
+ }
431
+ if (s.banned.length > 0)
432
+ lines.push(`banned by you: ${s.banned.map((b) => `${b.agent} ${b.model}`).join(', ')}`);
433
+ for (const r of s.rejected)
434
+ lines.push(`skipped ${r.agent} ${r.model}, ${rejectedText(r, now)} (orcacompass rejected --clear forgets it)`);
435
+ if (s.scored.length > 0) {
436
+ lines.push(`placements explained (lineup, benchmarks, capability score): ${s.scored.length}`);
437
+ // OpenCode can place a hundred OpenRouter models by score: past 30, an agent shows 15.
438
+ const total = new Map();
439
+ for (const x of s.scored)
440
+ total.set(x.agent, (total.get(x.agent) ?? 0) + 1);
441
+ const shown = new Map();
442
+ for (const x of s.scored) {
443
+ const n = (shown.get(x.agent) ?? 0) + 1;
444
+ shown.set(x.agent, n);
445
+ if (total.get(x.agent) <= 30 || n <= 15)
446
+ lines.push(` ${x.agent} ${x.model}: tier ${x.tier} from ${x.detail}`);
447
+ }
448
+ for (const [agent, n] of total)
449
+ if (n > 30)
450
+ lines.push(` ${agent}: … ${n - 15} more (status --json lists them)`);
451
+ }
452
+ if (s.unclassified.length > 0) {
453
+ lines.push(`unclassified (never routed): ${s.unclassified.length}`);
454
+ const byAgent = new Map();
455
+ for (const u of s.unclassified)
456
+ byAgent.set(u.agent, [...(byAgent.get(u.agent) ?? []), u.model]);
457
+ // OpenCode can list hundreds of OpenRouter models; status --json has them all.
458
+ for (const [agent, models] of byAgent) {
459
+ const more = models.length > 15 ? `, … ${models.length - 15} more` : '';
460
+ lines.push(` ${agent}: ${models.slice(0, 15).join(', ')}${more}`);
461
+ }
462
+ }
463
+ return lines;
464
+ }
465
+ async function statusCommand(args, io, deps) {
466
+ const v = parse(args, { json: { type: 'boolean' } });
467
+ const config = loadConfig();
468
+ const s = buildStatus(await gather(config, deps), config);
469
+ io.out(v.json ? JSON.stringify(s, null, 2) : statusText(s).join('\n'));
470
+ return 0;
471
+ }
472
+ // --- agents ---------------------------------------------------------------------------------
473
+ const ACTIONS = ['disable', 'enable', 'only', 'reset'];
474
+ function orcaAgentsLine(o) {
475
+ if (!o)
476
+ return 'orca agent switches: not read; every installed agent counts as enabled';
477
+ if (!o.known)
478
+ return `orca agent switches: unknown (${o.reason}); every installed agent counts as enabled`;
479
+ return `orca agent switches: read from ${o.source} (experimental); disabled: ${o.disabled.join(', ') || 'none'}`;
480
+ }
481
+ async function agentsCommand(args, io, deps) {
482
+ let parsed;
483
+ try {
484
+ parsed = parseArgs({ args, options: { json: { type: 'boolean' } }, strict: true, allowPositionals: true });
485
+ }
486
+ catch (e) {
487
+ throw new UsageError(e.message);
488
+ }
489
+ const json = parsed.values.json === true;
490
+ const [action, ...rawIds] = parsed.positionals;
491
+ if (action === undefined)
492
+ return listAgents(json, io, deps);
493
+ if (!ACTIONS.includes(action))
494
+ throw new UsageError(`unknown agents action "${action}" (expected ${ACTIONS.join(', ')})`);
495
+ if (action === 'reset' && rawIds.length > 0)
496
+ throw new UsageError('agents reset takes no agent ids');
497
+ if (action !== 'reset' && rawIds.length === 0)
498
+ throw new UsageError(`agents ${action} needs at least one agent id (${AGENT_IDS.join(', ')})`);
499
+ const ids = rawIds.map((id) => id.toLowerCase());
500
+ const bad = rawIds.find((_, i) => !AGENT_IDS.includes(ids[i]));
501
+ if (bad !== undefined)
502
+ throw new UsageError(`unknown agent "${bad}" (known: ${AGENT_IDS.join(', ')})`);
503
+ const next = applyAgentChoice(loadConfig(), action, ids);
504
+ writeAgentChoice(next);
505
+ const chosen = AGENT_IDS.filter((a) => isChosen(a, next));
506
+ const notChosen = AGENT_IDS.filter((a) => !isChosen(a, next));
507
+ if (json) {
508
+ io.out(JSON.stringify({ configPath: configPath(), allowAgents: next.allowAgents ?? null, denyAgents: next.denyAgents, chosen, notChosen }, null, 2));
509
+ }
510
+ else {
511
+ io.out([`wrote ${configPath()}`, `chosen: ${chosen.join(', ') || 'none'}`, `not chosen: ${notChosen.join(', ') || 'none'}`].join('\n'));
512
+ }
513
+ return 0;
514
+ }
515
+ async function listAgents(json, io, deps) {
516
+ const config = loadConfig();
517
+ const env = await gatherEnvironment(config, deps);
518
+ const agents = env.discoveries.map((d) => {
519
+ const usage = env.usage[d.agent];
520
+ const reason = agentSkipReason(d, env, config);
521
+ return {
522
+ agent: d.agent,
523
+ installed: d.installed,
524
+ enabledInOrca: enabledInOrca(env.orcaAgents, d.agent),
525
+ orcaUsage: usage?.status ?? 'unknown',
526
+ ...(usage && usage.status !== 'ok' && usage.error ? { orcaUsageError: usage.error } : {}),
527
+ chosen: isChosen(d.agent, config),
528
+ used: reason === undefined,
529
+ verdict: reason !== undefined ? `not used: ${reason}` : config.lastResortAgents.includes(d.agent) ? 'used as last resort' : 'used',
530
+ };
531
+ });
532
+ if (json) {
533
+ const choice = { configPath: configPath(), allowAgents: config.allowAgents ?? null, denyAgents: config.denyAgents };
534
+ io.out(JSON.stringify({ orcaAgents: env.orcaAgents ?? null, ...choice, agents }, null, 2));
535
+ return 0;
536
+ }
537
+ const yes = (b) => (b ? 'yes' : 'no');
538
+ const rows = [
539
+ ['agent', 'installed', 'enabled in Orca', 'Orca usage', 'chosen', 'verdict'],
540
+ ...agents.map((a) => [a.agent, yes(a.installed), a.enabledInOrca, a.orcaUsage, yes(a.chosen), a.verdict]),
541
+ ];
542
+ const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => r[i].length)));
543
+ const lines = [orcaAgentsLine(env.orcaAgents), `config: ${configPath()}`, ''];
544
+ for (const r of rows)
545
+ lines.push(r.map((cell, i) => (i === r.length - 1 ? cell : cell.padEnd(widths[i]))).join(' '));
546
+ io.out(lines.join('\n'));
547
+ return 0;
548
+ }
549
+ // --- prefer ---------------------------------------------------------------------------------
550
+ const prefLabel = (p) => (!p ? 'none' : p.model ? `${p.agent} ${p.model}` : `${p.agent} (its best model)`);
551
+ function parsePositionals(args, options) {
552
+ try {
553
+ return parseArgs({ args, options, strict: true, allowPositionals: true });
554
+ }
555
+ catch (e) {
556
+ throw new UsageError(e.message);
557
+ }
558
+ }
559
+ function agentId(raw) {
560
+ const id = raw.toLowerCase();
561
+ if (!AGENT_IDS.includes(id))
562
+ throw new UsageError(`unknown agent "${raw}" (known: ${AGENT_IDS.join(', ')})`);
563
+ return id;
564
+ }
565
+ function tierOf(raw) {
566
+ const tier = raw.toLowerCase();
567
+ if (!TIERS.includes(tier))
568
+ throw new UsageError(`unknown tier "${raw}" (expected ${TIERS.join(', ')})`);
569
+ return tier;
570
+ }
571
+ async function preferCommand(args, io, deps) {
572
+ const { values, positionals } = parsePositionals(args, { json: { type: 'boolean' }, all: { type: 'boolean' }, 'risk-limit': { type: 'string' } });
573
+ const json = values.json === true;
574
+ const show = (config) => {
575
+ if (json) {
576
+ io.out(JSON.stringify({ configPath: configPath(), preferRiskLimit: config.preferRiskLimit, pins: config.pins }, null, 2));
577
+ return;
578
+ }
579
+ const lines = [`preferred routes (above ${config.preferRiskLimit}% projected usage, switch only to a known within-limit alternative):`];
580
+ for (const tier of TIERS)
581
+ lines.push(` ${tier.padEnd(8)} ${prefLabel(config.pins[tier])}`);
582
+ io.out(lines.join('\n'));
583
+ };
584
+ const rawLimit = values['risk-limit'];
585
+ if (typeof rawLimit === 'string') {
586
+ if (positionals.length > 0 || values.all)
587
+ throw new UsageError('use "prefer --risk-limit <percent>" on its own');
588
+ const limit = parsePercent(rawLimit);
589
+ if (limit === undefined)
590
+ throw new UsageError(`--risk-limit must be a non-negative percent (got "${rawLimit}")`);
591
+ loadConfig();
592
+ updateConfigFile((raw) => setPreferRiskLimit(raw, limit));
593
+ if (!json)
594
+ io.out(`wrote ${configPath()}`);
595
+ show(loadConfig());
596
+ return 0;
597
+ }
598
+ if (positionals.length === 0) {
599
+ if (values.all)
600
+ throw new UsageError('--all only goes with "prefer clear"');
601
+ show(loadConfig());
602
+ return 0;
603
+ }
604
+ if (positionals[0] === 'clear') {
605
+ const rest = positionals.slice(1);
606
+ if (values.all ? rest.length > 0 : rest.length !== 1)
607
+ throw new UsageError('use "prefer clear <tier>" or "prefer clear --all"');
608
+ const tier = values.all ? undefined : tierOf(rest[0]);
609
+ loadConfig();
610
+ updateConfigFile((raw) => {
611
+ for (const t of tier ? [tier] : TIERS)
612
+ setPin(raw, t, undefined);
613
+ });
614
+ if (!json)
615
+ io.out(`wrote ${configPath()}`);
616
+ show(loadConfig());
617
+ return 0;
618
+ }
619
+ if (values.all)
620
+ throw new UsageError('--all only goes with "prefer clear"');
621
+ if (positionals.length < 2 || positionals.length > 3)
622
+ throw new UsageError('use "prefer <tier> <agent> [model]"');
623
+ const tier = tierOf(positionals[0]);
624
+ const agent = agentId(positionals[1]);
625
+ const model = positionals[2];
626
+ const config = loadConfig();
627
+ const env = await gatherEnvironment(config, deps);
628
+ const disc = env.discoveries.find((d) => d.agent === agent);
629
+ if (!disc?.installed || disc.models.length === 0) {
630
+ const state = disc?.installed ? 'lists no models' : 'is not installed';
631
+ io.err(`warning: ${agent} ${state} right now; saved${model ? ' without checking the model' : ''}`);
632
+ }
633
+ else if (model && !disc.models.some((m) => launchIds(m).includes(model))) {
634
+ const known = disc.models.map((m) => m.modelId);
635
+ throw new UsageError(`unknown model "${model}" for ${agent} (known: ${known.slice(0, 12).join(', ')}${known.length > 12 ? ', …' : ''})`);
636
+ }
637
+ const pin = { agent, ...(model ? { model } : {}) };
638
+ updateConfigFile((raw) => setPin(raw, tier, pin));
639
+ if (json)
640
+ show(loadConfig());
641
+ else
642
+ io.out([`wrote ${configPath()}`, `${tier} → ${prefLabel(pin)}`].join('\n'));
643
+ return 0;
644
+ }
645
+ // --- ban ------------------------------------------------------------------------------------
646
+ async function banCommand(args, io, deps) {
647
+ const { values, positionals } = parsePositionals(args, { json: { type: 'boolean' } });
648
+ const config = loadConfig();
649
+ const env = await gatherEnvironment(config, deps);
650
+ const entries = [...new Set(positionals.map((e) => e.trim().toLowerCase()))];
651
+ const next = [...config.banModels];
652
+ if (entries.length > 0) {
653
+ const matches = banMatches(entries, env);
654
+ // An agent id is refused unless it is also a model family (grok); check them all before saying anything.
655
+ const agent = agentNotFamily(entries, matches);
656
+ if (agent)
657
+ throw new UsageError(`"${agent}" is an agent; to stop using it run: orcacompass agents disable ${agent}`);
658
+ for (const entry of entries) {
659
+ if (AGENT_IDS.includes(entry)) {
660
+ io.err(`note: "${entry}" bans the ${entry} model family; to stop using the ${agentName(entry)} agent run: orcacompass agents disable ${entry}`);
661
+ }
662
+ if (matches[entry].length === 0)
663
+ io.err(`warning: "${entry}" matches no model discovered right now; saved anyway`);
664
+ if (!next.includes(entry))
665
+ next.push(entry);
666
+ }
667
+ writeBans(next);
668
+ }
669
+ showBans(next, env, values.json === true, entries.length > 0, io);
670
+ return 0;
671
+ }
672
+ async function unbanCommand(args, io, deps) {
673
+ const { values, positionals } = parsePositionals(args, { json: { type: 'boolean' } });
674
+ if (positionals.length === 0)
675
+ throw new UsageError('use "unban <model-or-family>..."');
676
+ const entries = positionals.map((e) => e.trim().toLowerCase());
677
+ const config = loadConfig();
678
+ for (const entry of entries)
679
+ if (!config.banModels.includes(entry))
680
+ io.err(`warning: "${entry}" was not banned`);
681
+ const next = config.banModels.filter((b) => !entries.includes(b));
682
+ writeBans(next);
683
+ showBans(next, await gatherEnvironment(config, deps), values.json === true, true, io);
684
+ return 0;
685
+ }
686
+ const agentName = (id) => (id === 'grok' ? 'Grok Build' : id);
687
+ function writeBans(banModels) {
688
+ updateConfigFile((raw) => setBans(raw, banModels));
689
+ }
690
+ function showBans(banModels, env, json, wrote, io) {
691
+ const matches = banMatches(banModels, env);
692
+ if (json) {
693
+ io.out(JSON.stringify({ configPath: configPath(), banModels, matches }, null, 2));
694
+ return;
695
+ }
696
+ const lines = wrote ? [`wrote ${configPath()}`] : [];
697
+ if (banModels.length === 0)
698
+ lines.push('banned by you: none');
699
+ else {
700
+ lines.push('banned by you:');
701
+ for (const entry of banModels) {
702
+ const found = matches[entry].map((m) => `${m.agent} ${m.model}`);
703
+ lines.push(` ${entry} ${found.length ? found.join(', ') : '(matches nothing discovered right now)'}`);
704
+ }
705
+ }
706
+ io.out(lines.join('\n'));
707
+ }
708
+ // --- refresh --------------------------------------------------------------------------------
709
+ async function refreshCommand(args, io, deps) {
710
+ const v = parse(args, { json: { type: 'boolean' } });
711
+ const config = loadConfig();
712
+ const key = (deps.env ?? process.env)[AA_KEY_ENV];
713
+ const { table, saveError } = await loadPlacements(config, { ...deps, refresh: true });
714
+ const { catalog: c, scores: r, benchmarks: b } = table.sources;
715
+ const at = (t) => new Date(t).toISOString();
716
+ if (c.source !== 'network')
717
+ io.err(`could not reach models.dev; still using the ${c.source} from ${at(c.fetchedAt)}`);
718
+ if (r?.source !== 'network') {
719
+ io.err(`could not reach themodelindex.org; ${r ? `still using the ${r.source} from ${at(r.fetchedAt)}` : 'no scores available'}`);
720
+ }
721
+ // No key is the normal opt-out, reported in the output rather than as an error.
722
+ if (key && b?.source !== 'network')
723
+ io.err(`could not reach artificialanalysis.ai; ${b ? `still using the cache from ${at(b.fetchedAt)}` : 'no benchmarks available'}`);
724
+ if (saveError !== undefined)
725
+ io.err(`placements: could not be saved: ${saveError}`);
726
+ const models = table.agents.reduce((n, a) => n + a.models.length, 0);
727
+ const placements = { builtAt: table.builtAt, models, agents: table.agents.length };
728
+ if (v.json) {
729
+ io.out(JSON.stringify({ source: c.source, fetchedAt: c.fetchedAt, models: c.models, scores: r, benchmarks: b, placements }, null, 2));
730
+ }
731
+ else {
732
+ const scoreLine = r ? `${r.source}, ${r.models} rated models (fetched ${at(r.fetchedAt)})` : 'unavailable';
733
+ const benchLine = b
734
+ ? `${b.source}, ${b.models} benchmarked models (fetched ${at(b.fetchedAt)})`
735
+ : `unavailable${key ? '' : ` (set ${AA_KEY_ENV} to your Artificial Analysis key to fetch)`}`;
736
+ io.out([
737
+ `models.dev: ${c.source}, ${c.models} models (fetched ${at(c.fetchedAt)})`,
738
+ `model index: ${scoreLine}`,
739
+ `artificial analysis: ${benchLine}`,
740
+ `placements: ${models} models across ${table.agents.length} agents (built now)`,
741
+ ].join('\n'));
742
+ }
743
+ return 0;
744
+ }
745
+ // --- tiers ----------------------------------------------------------------------------------
746
+ /** Plain words for why the table was rebuilt on this call. */
747
+ const REBUILT_WORDS = {
748
+ missing: 'no table yet',
749
+ invalid: 'table unreadable',
750
+ version: 'older version',
751
+ expired: 'table expired',
752
+ config: 'config changed',
753
+ 'agent-appeared': 'new agent on PATH',
754
+ refresh: 'refresh asked',
755
+ };
756
+ function placementsView(g, now) {
757
+ const { table } = g;
758
+ return {
759
+ builtAt: table.builtAt,
760
+ age: Math.max(0, now - table.builtAt),
761
+ version: table.version,
762
+ rebuilt: g.rebuilt ?? null,
763
+ saveError: g.saveError ?? null,
764
+ benchmarkScale: table.sources.benchmarkScale,
765
+ scoreScale: table.sources.scoreScale,
766
+ };
767
+ }
768
+ function placementsLines(p, now) {
769
+ const built = p.rebuilt ? `built now: ${REBUILT_WORDS[p.rebuilt]}` : `built ${duration(now - p.builtAt)} ago`;
770
+ const lines = [`placements: ${built} (orcacompass ${p.version})`];
771
+ if (p.saveError)
772
+ lines.push(`placements: could not be saved: ${p.saveError}`);
773
+ return lines;
774
+ }
775
+ function tiersText(table, view, now) {
776
+ const { catalog, scores, benchmarks, benchmarkScale } = table.sources;
777
+ const lines = [
778
+ ...placementsLines(view, now),
779
+ `catalog: ${catalog.source}, ${catalog.models} models; scores: ${scores ? `${scores.source}, ${scores.models} models` : 'unavailable'}; benchmarks: ${benchmarks ? `${benchmarks.source}, ${benchmarks.models} models` : 'unavailable'}`,
780
+ ];
781
+ if (benchmarkScale) {
782
+ const median = benchmarkScale.medianValue === null ? 'no prices' : `median ${benchmarkScale.medianValue.toFixed(1)} points per dollar`;
783
+ lines.push(`benchmark scale: top ${benchmarkScale.top.toFixed(1)}, ${median}`);
784
+ }
785
+ const rows = table.agents.flatMap((a) => a.models.flatMap((m) => m.placements.map((c) => ({ agent: a.agent, model: m.modelId, tier: c.tier, detail: c.detail ?? c.source }))));
786
+ rows.sort((x, y) => (x.agent < y.agent ? -1 : x.agent > y.agent ? 1 : x.model < y.model ? -1 : x.model > y.model ? 1 : 0));
787
+ for (const tier of TIERS) {
788
+ lines.push(`${tier}:`);
789
+ for (const r of rows.filter((x) => x.tier === tier))
790
+ lines.push(` ${r.agent} ${r.model} ${r.detail}`);
791
+ }
792
+ const unclassified = table.agents.flatMap((a) => a.models.filter((m) => m.placements.length === 0).map((m) => ({ agent: a.agent, model: m.modelId })));
793
+ lines.push(`unclassified: ${unclassified.length}`);
794
+ const byAgent = new Map();
795
+ for (const u of unclassified)
796
+ byAgent.set(u.agent, [...(byAgent.get(u.agent) ?? []), u.model]);
797
+ // OpenCode can list hundreds of OpenRouter models; tiers --json has them all.
798
+ for (const [agent, models] of byAgent) {
799
+ const more = models.length > 15 ? `, … ${models.length - 15} more` : '';
800
+ lines.push(` ${agent}: ${models.slice(0, 15).join(', ')}${more}`);
801
+ }
802
+ return lines;
803
+ }
804
+ async function tiersCommand(args, io, deps) {
805
+ const v = parse(args, { json: { type: 'boolean' } });
806
+ const config = loadConfig();
807
+ const now = deps.now ?? Date.now();
808
+ const loaded = await loadPlacements(config, { ...deps, now });
809
+ const view = placementsView(loaded, now);
810
+ io.out(v.json ? JSON.stringify({ ...loaded.table, age: view.age }, null, 2) : tiersText(loaded.table, view, now).join('\n'));
811
+ return 0;
812
+ }
813
+ // --- install --------------------------------------------------------------------------------
814
+ async function installCommand(args, io, deps) {
815
+ const v = parse(args, {
816
+ global: { type: 'boolean', short: 'g' },
817
+ yes: { type: 'boolean', short: 'y' },
818
+ agent: { type: 'string', short: 'a', multiple: true },
819
+ 'skill-only': { type: 'boolean' },
820
+ });
821
+ // The installer asks which agents and where; without a terminal nobody can answer.
822
+ if (!deps.terminal && !v.yes) {
823
+ io.err('orcacompass install asks questions in a terminal, and this is not one. Run it in a terminal, or add --yes (with --global and --agent <name> to choose).');
824
+ return 1;
825
+ }
826
+ return install({ global: v.global, yes: v.yes, agents: v.agent, skillOnly: v['skill-only'] }, { log: io.out, ...deps.install });
827
+ }
828
+ // --- setup ----------------------------------------------------------------------------------
829
+ async function setupCommand(args, io, deps) {
830
+ const v = parse(args, { json: { type: 'boolean' }, skip: { type: 'boolean' }, complete: { type: 'boolean' } });
831
+ if (v.skip && v.complete)
832
+ throw new UsageError('choose either --complete or --skip');
833
+ if (v.skip)
834
+ return skipSetup(v.json, io, deps);
835
+ if (!v.json && !v.complete && !deps.terminal) {
836
+ io.err([
837
+ 'orcacompass setup asks its questions in a terminal, and this is not one.',
838
+ 'In chat, ask your orchestrator to set up OrcaCompass: it reads `orcacompass setup --json` and asks the same questions.',
839
+ 'Or set things one at a time: orcacompass agents, orcacompass prefer, orcacompass ban.',
840
+ ].join('\n'));
841
+ return 1;
842
+ }
843
+ if (v.complete && !existsSync(configPath()))
844
+ updateConfigFile(() => { });
845
+ const config = loadConfig();
846
+ const env = await gatherEnvironment(config, deps);
847
+ if (v.json) {
848
+ io.out(JSON.stringify(setupInfo(env, config), null, 2));
849
+ return 0;
850
+ }
851
+ if (v.complete) {
852
+ io.out(`Setup complete. Config: ${configPath()}`);
853
+ return 0;
854
+ }
855
+ return setupWizard(deps.terminal, env, config);
856
+ }
857
+ /** The user declined setup: create an empty config file, if there is none, so setup is not offered again. */
858
+ async function skipSetup(json, io, deps) {
859
+ if (!existsSync(configPath()))
860
+ updateConfigFile(() => { });
861
+ const config = loadConfig();
862
+ const env = await gatherEnvironment(config, deps);
863
+ const used = env.discoveries.filter((d) => !agentSkipReason(d, env, config)).map((d) => d.agent);
864
+ const orca = env.orcaAgents?.known ? 'they follow the agents enabled in Orca' : "Orca's agent switches could not be read";
865
+ if (json) {
866
+ io.out(JSON.stringify({ configPath: configPath(), agentsUsed: used, orcaAgents: env.orcaAgents ?? null }, null, 2));
867
+ }
868
+ else {
869
+ io.out(`Setup skipped; defaults kept in ${configPath()}.
870
+ Agents used now: ${used.join(', ') || 'none'} (${orca}).`);
871
+ }
872
+ return 0;
873
+ }
874
+ // --- formatting -----------------------------------------------------------------------------
875
+ const percent = (n) => `${Number.isInteger(n) ? n : n.toFixed(1)}%`;
876
+ function duration(ms) {
877
+ if (ms <= 0)
878
+ return '0m';
879
+ const days = Math.floor(ms / 86_400_000);
880
+ if (days >= 1)
881
+ return `${days}d`;
882
+ const hours = Math.floor(ms / 3_600_000);
883
+ if (hours >= 1)
884
+ return `${hours}h`;
885
+ return `${Math.floor(ms / 60_000)}m`;
886
+ }
887
+ // --- entry ----------------------------------------------------------------------------------
888
+ // Only run when executed directly (also through npm's bin symlinks/shims), never on import.
889
+ function isEntry() {
890
+ const script = process.argv[1];
891
+ if (!script)
892
+ return false;
893
+ try {
894
+ return realpathSync(script) === realpathSync(fileURLToPath(import.meta.url));
895
+ }
896
+ catch {
897
+ return false;
898
+ }
899
+ }
900
+ if (isEntry()) {
901
+ const io = { out: (s) => process.stdout.write(`${s}\n`), err: (s) => process.stderr.write(`${s}\n`) };
902
+ const terminal = process.stdin.isTTY && process.stdout.isTTY ? { input: process.stdin, output: process.stdout } : undefined;
903
+ void main(process.argv.slice(2), io, terminal ? { terminal } : {}).then((code) => {
904
+ process.exitCode = code;
905
+ });
906
+ }