ultracost 0.2.1 → 0.3.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.
package/bin/cli.js CHANGED
@@ -1,15 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
- import { join } from 'node:path';
3
+ import { join, basename } from 'node:path';
4
4
  import { loadPolicy } from '../src/policy.js';
5
- import { scan, fixFile, collectFiles, auditScripts } from '../src/guard.js';
6
- import { estimateFile } from '../src/estimate.js';
5
+ import { scan, fixFile, collectFiles, auditScripts, stageList, CODES } from '../src/guard.js';
6
+ import { estimateFile, scenarioTotals } from '../src/estimate.js';
7
7
  import { refreshPricing, writePricingToPolicy, DEFAULT_PRICING_URL } from '../src/pricing.js';
8
- import { install, uninstall, readSettings } from '../src/install.js';
8
+ import { install, uninstall } from '../src/install.js';
9
+ import { detectDelivery } from '../src/detect.js';
10
+ import { readTranscripts, locateWorkflowRuns } from '../src/transcript.js';
11
+ import { costFromUsage, modelPrice, totalTokens } from '../src/cost.js';
12
+ import { tierOfModel, classifyPrompt, semanticFindings } from '../src/classify.js';
9
13
  import {
10
- ROOT, CLAUDE_MD, HOOK_PATH, POLICY_PATH, SETTINGS, PROJECTS_DIR, tilde, MARKER_START
14
+ reconcileRun, calibrationFromRuns, writeCalibration, readCalibration, applyCalibration,
15
+ ledgerSync, spentToday
16
+ } from '../src/loop.js';
17
+ import {
18
+ ROOT, CLAUDE_MD, HOOK_PATH, POLICY_PATH, SETTINGS, PROJECTS_DIR, CALIBRATION_PATH, LEDGER_PATH, tilde
11
19
  } from '../src/paths.js';
12
- import { c, log, ok, warn, err, info } from '../src/log.js';
20
+ import { log, ok, warn, err, info } from '../src/log.js';
21
+ import { color, dim, bold, panel, columns, bar, sparkline, gradient, symbols, COLORS } from '../src/render.js';
22
+
23
+ const fmt = (n) => (n >= 1e9 ? (n / 1e9).toFixed(2) + 'B' : n >= 1e6 ? (n / 1e6).toFixed(2) + 'M' : n >= 1e3 ? (n / 1e3).toFixed(1) + 'k' : String(n));
13
24
 
14
25
  const version = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
15
26
  const argv = process.argv.slice(2);
@@ -17,7 +28,14 @@ const cmd = argv[0] || 'help';
17
28
  const has = (flag) => argv.includes(flag);
18
29
  const positional = argv.slice(1).filter((a) => !a.startsWith('-'));
19
30
 
31
+ // When invoked through `npx ultracost ...`, the binary isn't on PATH afterwards, so
32
+ // printed hints must keep the `npx` prefix (npx caches under .../_npx/).
33
+ const NPX = (process.argv[1] || '').includes('/_npx/');
34
+ const SELF = NPX ? 'npx ultracost' : 'ultracost';
35
+
20
36
  const money = (x) => '$' + Number(x).toFixed(4);
37
+ const money6 = (x) => '$' + Number(x);
38
+ const title = (t) => log(gradient(t, COLORS.violet, COLORS.cyan));
21
39
 
22
40
  try {
23
41
  await dispatch();
@@ -32,7 +50,14 @@ async function dispatch() {
32
50
  case 'check': case 'guard': cmdCheck(); break;
33
51
  case 'audit': cmdAudit(); break;
34
52
  case 'estimate': cmdEstimate(); break;
53
+ case 'explain': cmdExplain(); break;
54
+ case 'simulate': cmdSimulate(); break;
55
+ case 'diff': cmdDiff(); break;
35
56
  case 'pricing': await cmdPricing(); break;
57
+ case 'usage': cmdUsage(); break;
58
+ case 'reconcile': cmdReconcile(); break;
59
+ case 'calibrate': cmdCalibrate(); break;
60
+ case 'ledger': case 'savings': cmdLedger(); break;
36
61
  case 'status': cmdStatus(); break;
37
62
  case 'doctor': cmdDoctor(); break;
38
63
  case 'uninstall': cmdUninstall(); break;
@@ -46,47 +71,78 @@ async function dispatch() {
46
71
  }
47
72
 
48
73
  function cmdHelp() {
49
- log(`
50
- ${c.bold('ultracost')} ${c.dim('v' + version)} — per-stage model routing for Claude Code workflows
51
-
52
- ${c.bold('Usage')}
53
- ultracost init Install routing rules, hook, and default policy
54
- ultracost check [path] Scan workflow scripts for unpinned agent() stages
55
- ultracost audit [dir] Report pin stats across your real workflow scripts
56
- ultracost estimate <script> Estimate agents, model mix, and cost vs all-Opus baseline
57
- ultracost pricing [refresh] Show pricing, or refresh it from Anthropic's official page
58
- ultracost status Show active policy and install state
59
- ultracost doctor Diagnose the installation
60
- ultracost uninstall Remove everything ultracost installed
61
-
62
- ${c.bold('check flags')}
63
- --json Machine-readable output
64
- --fix Insert the default model on unpinned stages
65
- --quiet Only print problems
66
-
67
- ${c.bold('estimate / audit flags')}
68
- --json Machine-readable output
69
- ${c.dim('audit default dir:')} ${tilde(PROJECTS_DIR)}/**/workflows/scripts/*.js
70
-
71
- ${c.bold('pricing flags')}
72
- refresh Fetch official prices and update the installed policy
73
- --url <url> Override the pricing page URL
74
-
75
- ${c.bold('Policy')}
76
- Edit ${tilde(POLICY_PATH)} to change tiers/rules/effort/pricing, then re-run ${c.cyan('ultracost init')}.
77
- `);
74
+ log('');
75
+ title(' ultracost');
76
+ log(' ' + dim('v' + version + ' — per-stage model routing for Claude Code workflows'));
77
+ log('');
78
+ log(bold(' Routing & guard'));
79
+ log(columns([
80
+ ['init', 'Install routing rules, hook, and default policy'],
81
+ ['check [path]', 'Flag agent() stages that would inherit the session model'],
82
+ ['audit [dir]', 'Pin stats across your real workflow scripts'],
83
+ ['explain <script>', 'Per-stage rationale: tier, effort, tokens, cost, warnings']
84
+ ], { indent: 2, gap: 3 }));
85
+ log('');
86
+ log(bold(' Cost'));
87
+ log(columns([
88
+ ['estimate <script>', 'Agents, model mix, and cost vs an all-opus baseline'],
89
+ ['simulate <script>', 'Cost under alternative policies, side by side'],
90
+ ['diff <a> <b>', 'Cost delta between two workflow versions (--ci for PRs)'],
91
+ ['usage [dir]', 'Real token cost from local transcripts (main vs subagents)'],
92
+ ['reconcile [--last]', 'Estimate vs actual for a real workflow run'],
93
+ ['ledger', 'Cumulative savings vs all-opus across recorded runs'],
94
+ ['calibrate', 'Tune the estimator from your real token usage'],
95
+ ['pricing [refresh]', "Show pricing, or refresh from Anthropic's official page"]
96
+ ], { indent: 2, gap: 3 }));
97
+ log('');
98
+ log(bold(' State'));
99
+ log(columns([
100
+ ['status', 'Active policy + how ultracost is delivered (plugin/cli)'],
101
+ ['doctor', 'Diagnose the installation'],
102
+ ['uninstall', 'Remove everything the CLI installed']
103
+ ], { indent: 2, gap: 3 }));
104
+ log('');
105
+ info(` policy: ${tilde(POLICY_PATH)} · flags: --json --fix --quiet`);
106
+ log('');
78
107
  }
79
108
 
80
109
  function cmdInit() {
110
+ const d = detectDelivery();
111
+ if (d.verdict === 'plugin' && !has('--force')) {
112
+ log(panel([
113
+ `${color.green('●')} ultracost is already delivered by the plugin ${dim('(enabled + hooks active' + (d.plugin.version ? ', v' + d.plugin.version : '') + ')')}`,
114
+ '',
115
+ dim('Running init would write duplicate routing rules into ~/.claude that conflict'),
116
+ dim('with plugin delivery. Use the plugin as-is, or:'),
117
+ ` ${color.cyan(SELF + ' init --force')} ${dim('install the CLI path too (advanced)')}`
118
+ ], { title: 'init skipped', hex: COLORS.amber }));
119
+ return;
120
+ }
121
+
81
122
  const { policy, source } = loadPolicy();
82
123
  const r = install(policy, { force: has('--force') });
83
- log(`${c.bold('ultracost init')}\n`);
84
- ok(`policy: ${r.policy} (${tilde(POLICY_PATH)})`);
85
- ok(`rules: ${r.rules} (${tilde(CLAUDE_MD)})`);
86
- ok(`hook: ${r.hook} (${tilde(HOOK_PATH)})`);
87
- if (r.register === 'invalid') warn(`settings.json is invalid JSON — register the hook manually`);
88
- else ok(`hook ${r.register} in ${tilde(SETTINGS)}`);
89
- info(`\nactive policy from ${tilde(source)} new sessions pick this up immediately.`);
124
+ log('');
125
+ title(' ultracost init');
126
+ log('');
127
+ ok(`policy: ${r.policy} ${dim('(' + tilde(POLICY_PATH) + ')')}`);
128
+ ok(`rules: ${r.rules} ${dim('(' + tilde(CLAUDE_MD) + ')')}`);
129
+ ok(`hook: ${r.hook} ${dim('(' + tilde(HOOK_PATH) + ')')}`);
130
+ if (r.register === 'invalid') warn('settings.json is invalid JSON register the hook manually');
131
+ else ok(`hook ${r.register} ${dim('in ' + tilde(SETTINGS))}`);
132
+ if (d.verdict === 'both' || d.plugin.enabled) {
133
+ log('');
134
+ warn('the plugin is also active — you now have dual delivery; rules may be injected twice.');
135
+ info(`remove one: /plugin uninstall ultracost@ultracost (or) ${SELF} uninstall`);
136
+ }
137
+ log('');
138
+ info(`active policy from ${tilde(source)} — new sessions pick this up immediately.`);
139
+ log('');
140
+ }
141
+
142
+ function severityGlyph(sev) {
143
+ if (sev === 'error') return color.red(symbols.err);
144
+ if (sev === 'warn') return color.amber(symbols.warn);
145
+ return color.green(symbols.ok);
90
146
  }
91
147
 
92
148
  function cmdCheck() {
@@ -109,21 +165,39 @@ function cmdCheck() {
109
165
  process.exit(errors.length ? 1 : 0);
110
166
  }
111
167
 
168
+ log('');
112
169
  if (!findings.length) {
113
- ok(`${files.length} file(s) scanned — every agent() stage pins a model.`);
170
+ log(panel([`${color.green(symbols.ok)} every agent() stage pins a model`], { title: `check · ${files.length} file(s)`, hex: COLORS.green }));
171
+ log('');
114
172
  return;
115
173
  }
174
+
175
+ // group findings by file
176
+ const byFile = new Map();
116
177
  for (const f of findings) {
117
- const tag = f.severity === 'error' ? c.red(f.code) : c.yellow(f.code);
118
- log(`${c.dim(tilde(f.file) + ':' + f.line + ':' + f.column)} ${tag} ${f.message}`);
119
- if (!has('--quiet')) log(` ${c.dim(f.snippet)}`);
178
+ if (!byFile.has(f.file)) byFile.set(f.file, []);
179
+ byFile.get(f.file).push(f);
120
180
  }
121
- log('');
122
- log(`${errors.length} error(s), ${warns.length} warning(s) in ${files.length} file(s).`);
181
+ for (const [file, fs] of byFile) {
182
+ const e = fs.filter((x) => x.severity === 'error').length;
183
+ const w = fs.filter((x) => x.severity === 'warn').length;
184
+ const head = `${tilde(file)} ${e ? color.red(e + ' error' + (e > 1 ? 's' : '')) : ''}${e && w ? dim(' · ') : ''}${w ? color.amber(w + ' warning' + (w > 1 ? 's' : '')) : ''}`;
185
+ log(' ' + bold(head));
186
+ for (const f of fs) {
187
+ const tag = f.severity === 'error' ? color.red(f.code) : color.amber(f.code);
188
+ log(` ${severityGlyph(f.severity)} ${dim(f.line + ':' + f.column)} ${tag} ${f.message}`);
189
+ if (!has('--quiet')) log(` ${dim(f.snippet)}`);
190
+ }
191
+ log('');
192
+ }
193
+ const summary = `${errors.length ? color.red(errors.length + ' error(s)') : color.green('0 errors')} · ${warns.length ? color.amber(warns.length + ' warning(s)') : dim('0 warnings')} ${dim('in ' + files.length + ' file(s)')}`;
194
+ log(' ' + summary);
123
195
  if (errors.length) {
124
- info(`Fix with: ultracost check ${positional[0] || '.'} --fix`);
196
+ info(` fix the unambiguous ones: ${SELF} check ${positional[0] || '.'} --fix`);
197
+ log('');
125
198
  process.exit(1);
126
199
  }
200
+ log('');
127
201
  }
128
202
 
129
203
  function cmdAudit() {
@@ -136,49 +210,324 @@ function cmdAudit() {
136
210
  return;
137
211
  }
138
212
 
139
- log(`${c.bold('ultracost audit')}\n`);
213
+ log('');
214
+ title(' ultracost audit');
215
+ log('');
140
216
  if (!files.length) {
141
217
  warn(`no workflow scripts found under ${tilde(base)}`);
142
218
  info(`looked for ${tilde(base)}/**/workflows/scripts/*.js`);
143
219
  return;
144
220
  }
145
- info(`scanned ${totals.scripts} script(s) under ${tilde(base)}\n`);
146
- const pct = (totals.unpinnedRatio * 100).toFixed(1);
147
- log(` agent() stages ${totals.stages}`);
148
- log(` pinned ${c.green(totals.pinned)}`);
149
- log(` ${c.red('unpinned')} ${c.red(totals.unpinned)} ${c.dim('(UC001/UC002 — inherit the session model)')}`);
150
- log(` banned ${totals.banned} ${c.dim('(UC003)')}`);
151
- log(` inherit ${totals.inherit} ${c.dim('(UC004)')}`);
152
- log(` dynamic ${totals.dynamic} ${c.dim('(UC005options is a variable)')}`);
153
- log('');
154
- log(` ${c.bold('unpinned ratio')} ${pct}%`);
221
+ info(` scanned ${totals.scripts} script(s) under ${tilde(base)}`);
222
+ log('');
223
+ const pinnedPct = totals.stages ? (totals.pinned / totals.stages) * 100 : 0;
224
+ const unpinnedPct = totals.unpinnedRatio * 100;
225
+ log(columns([
226
+ ['agent() stages', String(totals.stages)],
227
+ ['pinned', color.green(String(totals.pinned))],
228
+ ['unpinned', color.red(String(totals.unpinned)), dim('UC001/UC002inherit the session model')],
229
+ ['banned', String(totals.banned), dim('UC003')],
230
+ ['inherit', String(totals.inherit), dim('UC004')],
231
+ ['dynamic', String(totals.dynamic), dim('UC005')],
232
+ ['wrong-tier', String(totals.wrongTier ?? 0), dim('UC006/UC008')],
233
+ ['over-effort', String(totals.overEffort ?? 0), dim('UC007')]
234
+ ], { indent: 2, gap: 2, align: ['left', 'right', 'left'] }));
235
+ log('');
236
+ log(' ' + bold('pinned ') + bar(totals.pinned, totals.stages || 1, 30, COLORS.green) + ` ${pinnedPct.toFixed(1)}%`);
237
+ log(' ' + bold('unpinned ') + bar(totals.unpinned, totals.stages || 1, 30, COLORS.red) + ` ${unpinnedPct.toFixed(1)}%`);
238
+ log('');
155
239
  }
156
240
 
157
241
  function cmdEstimate() {
158
242
  const target = positional[0];
159
- if (!target) { err('usage: ultracost estimate <workflow-script.js> [--json]'); process.exit(1); }
243
+ if (!target) { err(`usage: ${SELF} estimate <workflow-script.js> [--json]`); process.exit(1); }
160
244
  if (!existsSync(target)) { err(`not found: ${target}`); process.exit(1); }
161
245
  const { policy } = loadPolicy();
162
- const est = estimateFile(target, policy);
246
+ const cal = readCalibration();
247
+ const est = estimateFile(target, applyCalibration(policy, cal));
163
248
 
164
249
  if (has('--json')) {
165
- log(JSON.stringify({ target, ...est }, null, 2));
250
+ log(JSON.stringify({ target, calibrated: !!cal, ...est }, null, 2));
166
251
  return;
167
252
  }
168
253
 
169
254
  const a = est.agents;
170
- const fan = a.fanoutGroups ? ` + ${a.fanoutGroups} fan-out group(s) x ~${a.assumedPerFanout} = ~${a.assumedTotal}` : '';
171
- const mix = Object.entries(est.modelMix).map(([k, v]) => `${v}x ${k}`).join(', ') || 'none';
172
- log(`${c.bold('ultracost estimate')} ${c.dim(tilde(target))}\n`);
173
- log(` agents ${a.known} fixed${fan}`);
174
- log(` model mix ${mix}`);
175
- log('');
176
- log(` baseline (all ${est.assumptions.sessionModel}) ${money(est.cost.baseline)}`);
177
- log(` tiered (ultracost) ${money(est.cost.tiered)}`);
178
- log(` ${c.green('savings')} ${c.green(money(est.cost.savings))} ${c.green('(' + est.cost.savingsPct + '%)')}`);
179
- log('');
180
- info(`estimate; pricing as of ${est.assumptions.pricingAsOf || 'n/a'}; fan-out assumes ~${a.assumedPerFanout} items/group; unpinned stages inherit ${est.assumptions.sessionModel} (no saving).`);
181
- if (est.cost.savingsPct === 0 && est.stages.length) info('tip: pin cheaper tiers (sonnet) on mechanical stages to cut cost.');
255
+ const fan = a.fanoutGroups ? `${a.known} fixed + ${a.fanoutGroups} fan-out x ~${a.assumedPerFanout} = ~${a.assumedTotal}` : `${a.known}`;
256
+ const mix = Object.entries(est.modelMix).map(([k, v]) => color[mixKey(k)](`${v}x ${k}`)).join(' ') || 'none';
257
+
258
+ log('');
259
+ title(' ultracost estimate');
260
+ log(' ' + dim(tilde(target)));
261
+ log('');
262
+ log(columns([
263
+ ['agents', fan],
264
+ ['model mix', mix]
265
+ ], { indent: 2, gap: 3 }));
266
+ log('');
267
+ const pct = est.cost.savingsPct;
268
+ log(columns([
269
+ [dim('baseline'), dim('all ' + est.assumptions.sessionModel), money(est.cost.baseline)],
270
+ ['tiered', dim('ultracost'), money(est.cost.tiered)],
271
+ [color.green('savings'), color.green(pct + '%'), color.green(money(est.cost.savings))]
272
+ ], { indent: 2, gap: 3, align: ['left', 'left', 'right'] }));
273
+ log('');
274
+ log(' ' + bar(est.cost.savings, est.cost.baseline || 1, 30, COLORS.green) + ` ${pct}% saved`);
275
+ log('');
276
+ info(` estimate; pricing as of ${est.assumptions.pricingAsOf || 'n/a'}; fan-out assumes ~${a.assumedPerFanout} items/group; unpinned stages inherit ${est.assumptions.sessionModel}.`);
277
+ if (cal) info(` token prior calibrated from your real runs (${SELF} calibrate; ${cal.samples} samples).`);
278
+ if (pct === 0 && est.stages.length) info(' tip: pin cheaper tiers (sonnet) on mechanical stages to cut cost.');
279
+ log('');
280
+ }
281
+
282
+ function cmdUsage() {
283
+ const { policy } = loadPolicy();
284
+ const records = readTranscripts({ root: positional[0] });
285
+ if (has('--json')) {
286
+ const rows = records.map((r) => ({ kind: r.kind, model: r.model, project: r.project, cost: costFromUsage(r.usage, modelPrice(r.model, policy), policy), tokens: totalTokens(r.usage) }));
287
+ log(JSON.stringify({ records: rows.length, rows }, null, 2));
288
+ return;
289
+ }
290
+ log('');
291
+ title(' ultracost usage');
292
+ log(' ' + dim('real token cost from local transcripts'));
293
+ log('');
294
+ if (!records.length) { warn('no transcripts found under your Claude Code projects dir'); log(''); return; }
295
+
296
+ const byKind = {};
297
+ const byModel = {};
298
+ let total = 0;
299
+ let tokens = 0;
300
+ for (const r of records) {
301
+ const cost = costFromUsage(r.usage, modelPrice(r.model, policy), policy);
302
+ const tk = totalTokens(r.usage);
303
+ total += cost; tokens += tk;
304
+ byKind[r.kind] = (byKind[r.kind] || 0) + cost;
305
+ const mk = tierOfModel(r.model);
306
+ byModel[mk] = (byModel[mk] || 0) + cost;
307
+ }
308
+ log(panel([
309
+ `${bold(money(total))} ${dim('across ' + records.length + ' assistant turns · ' + fmt(tokens) + ' tokens')}`
310
+ ], { title: 'total cost', hex: COLORS.violet }));
311
+ log('');
312
+ const kindRows = ['main', 'subagent', 'workflow-stage'].filter((k) => byKind[k]).map((k) => [k, money(byKind[k])]);
313
+ log(columns(kindRows, { indent: 2, gap: 3, align: ['left', 'right'] }));
314
+ log('');
315
+ const maxModel = Math.max(1, ...Object.values(byModel));
316
+ for (const [k, v] of Object.entries(byModel).sort((a, b) => b[1] - a[1])) {
317
+ log(' ' + color[mixKey(k)](pad9(k)) + ' ' + bar(v, maxModel, 26, COLORS[mixKey(k)]) + ' ' + money(v));
318
+ }
319
+ log('');
320
+ }
321
+ function pad9(s) { return (s + ' ').slice(0, 9); }
322
+
323
+ function pickRun(runs) {
324
+ if (has('--last')) return runs[0];
325
+ if (positional[0]) return runs.find((r) => r.wfId.includes(positional[0])) || null;
326
+ return runs[0];
327
+ }
328
+
329
+ function cmdReconcile() {
330
+ const { policy } = loadPolicy();
331
+ const runs = locateWorkflowRuns();
332
+ if (!runs.length) { warn('no dynamic-workflow runs found in your transcripts yet'); return; }
333
+ const run = pickRun(runs);
334
+ if (!run) { err(`no workflow run matching "${positional[0]}"`); process.exit(1); }
335
+ const rec = reconcileRun(run, policy);
336
+
337
+ if (has('--json')) { log(JSON.stringify(rec, null, 2)); return; }
338
+
339
+ log('');
340
+ title(' ultracost reconcile');
341
+ log(' ' + dim(rec.wfId + ' · ' + (rec.ts ? rec.ts.slice(0, 10) : '') + ' · ' + rec.stages.length + ' stages'));
342
+ log('');
343
+ const rows = rec.stages.map((s, i) => [
344
+ dim('#' + (i + 1)),
345
+ color[mixKey(s.tier)](s.tier),
346
+ fmt(s.tokens),
347
+ money(s.actualCost),
348
+ dim(money(s.opusCost))
349
+ ]);
350
+ log(columns(rows, { indent: 2, gap: 3, align: ['right', 'left', 'right', 'right', 'right'], head: [dim('#'), 'tier', 'tokens', 'actual', 'all-opus'] }));
351
+ log('');
352
+ const t = rec.totals;
353
+ log(columns([
354
+ [dim('actual'), money(t.actual)],
355
+ [dim('all-opus baseline'), money(t.allOpus)],
356
+ [color.green('saved'), color.green(money(t.saved) + ' (' + t.savedPct + '%)')]
357
+ ], { indent: 2, gap: 3, align: ['left', 'right'] }));
358
+ log(' ' + bar(t.saved, t.allOpus || 1, 30, COLORS.green) + ` ${t.savedPct}% saved`);
359
+ log('');
360
+ info(' reconciled from real per-stage token usage (subagents/workflows/wf_*/agent-*.jsonl).');
361
+ log('');
362
+ }
363
+
364
+ function cmdCalibrate() {
365
+ const { policy } = loadPolicy();
366
+ const runs = locateWorkflowRuns();
367
+ const cal = calibrationFromRuns(runs, policy);
368
+ if (!cal) { warn('not enough real workflow-stage data to calibrate yet'); return; }
369
+ const path = writeCalibration(cal);
370
+
371
+ if (has('--json')) { log(JSON.stringify(cal, null, 2)); return; }
372
+ log('');
373
+ title(' ultracost calibrate');
374
+ log('');
375
+ const lines = [
376
+ `samples ${cal.samples} stages from ${cal.runs} run(s) ${dim('(' + cal.droppedOutliers + ' outliers dropped)')}`,
377
+ `tokens/stage ${fmt(cal.tokensPerStage.input)} in / ${fmt(cal.tokensPerStage.output)} out`
378
+ ];
379
+ for (const [k, v] of Object.entries(cal.perModel)) lines.push(` ${color[mixKey(k)](k)} ${fmt(v.input)} in / ${fmt(v.output)} out ${dim('(' + v.samples + ')')}`);
380
+ log(panel(lines, { title: 'calibrated token prior', hex: COLORS.cyan }));
381
+ log('');
382
+ ok(`written to ${tilde(path)} — ${SELF} estimate now uses your real token sizes.`);
383
+ log('');
384
+ }
385
+
386
+ function cmdLedger() {
387
+ const { policy } = loadPolicy();
388
+ const runs = locateWorkflowRuns();
389
+ const entries = ledgerSync(runs, policy);
390
+
391
+ if (has('--json')) { log(JSON.stringify({ entries }, null, 2)); return; }
392
+ log('');
393
+ title(' ultracost ledger');
394
+ log(' ' + dim('cumulative savings vs an all-opus baseline'));
395
+ log('');
396
+ if (!entries.length) { warn('no recorded workflow runs yet — run some ultracode workflows, then re-check'); log(''); return; }
397
+ const saved = entries.reduce((n, e) => n + (e.saved || 0), 0);
398
+ const actual = entries.reduce((n, e) => n + (e.actual || 0), 0);
399
+ const allOpus = entries.reduce((n, e) => n + (e.allOpus || 0), 0);
400
+ const pct = allOpus ? Math.round((1 - actual / allOpus) * 100) : 0;
401
+ log(panel([
402
+ `${color.green(bold(money(saved)))} ${dim('saved across ' + entries.length + ' run(s)')}`,
403
+ `${dim('actual ' + money(actual) + ' · all-opus ' + money(allOpus) + ' · ' + pct + '% saved')}`,
404
+ `${dim('today: ' + money(spentToday(entries)))}`
405
+ ], { title: 'savings ledger', hex: COLORS.green }));
406
+ log('');
407
+ const spark = sparkline(entries.map((e) => e.saved), COLORS.green);
408
+ if (spark) log(' per-run saved ' + spark);
409
+ log(' ' + dim(`ledger at ${tilde(LEDGER_PATH)}`));
410
+ log('');
411
+ }
412
+
413
+ function mixKey(k) {
414
+ return k === 'opus' ? 'violet' : k === 'sonnet' ? 'cyan' : k === 'haiku' ? 'red' : 'slate';
415
+ }
416
+
417
+ function cmdExplain() {
418
+ const target = positional[0];
419
+ if (!target || !existsSync(target)) { err(`usage: ${SELF} explain <workflow-script.js>`); process.exit(1); }
420
+ const { policy } = loadPolicy();
421
+ const pol = applyCalibration(policy);
422
+ const est = estimateFile(target, pol);
423
+ const stages = stageList(readFileSync(target, 'utf8'));
424
+
425
+ if (has('--json')) {
426
+ const out = est.stages.map((s, i) => {
427
+ const prompt = stages[i]?.prompt || null;
428
+ const cls = prompt ? classifyPrompt(prompt, policy) : null;
429
+ return { line: s.line, model: s.model, effort: s.effort, fanout: s.fanout, pinned: s.pinned, tieredCost: s.tieredCost, prompt, classified: cls };
430
+ });
431
+ log(JSON.stringify({ target, stages: out }, null, 2));
432
+ return;
433
+ }
434
+
435
+ log('');
436
+ title(' ultracost explain');
437
+ log(' ' + dim(tilde(target)) + (pol._calibrated ? dim(' · calibrated') : ''));
438
+ log('');
439
+ const rows = est.stages.map((s, i) => {
440
+ const prompt = stages[i]?.prompt;
441
+ const cls = prompt ? classifyPrompt(prompt, policy) : null;
442
+ const reads = cls && cls.tier ? `${cls.tier}${cls.confidence === 'high' ? '' : '?'}` : dim('—');
443
+ const flags = prompt
444
+ ? semanticFindings({ model: s.model, effort: s.effort, prompt }, policy, CODES).map((f) => f.code)
445
+ : [];
446
+ const tierName = tierOfModel(s.model);
447
+ return [
448
+ dim('#' + (i + 1)),
449
+ color[mixKey(tierName)](s.model) + (s.fanout ? dim(' xN') : ''),
450
+ s.effort || dim('—'),
451
+ reads,
452
+ money(s.tieredCost),
453
+ flags.length ? color.amber(flags.join(',')) : color.green('ok')
454
+ ];
455
+ });
456
+ log(columns(rows, {
457
+ indent: 2, gap: 3,
458
+ align: ['right', 'left', 'left', 'left', 'right', 'left'],
459
+ head: [dim('#'), 'model', 'effort', 'reads-like', 'est', 'check']
460
+ }));
461
+ log('');
462
+ log(' ' + dim(`${est.agents.assumedTotal} agents · tiered ${money(est.cost.tiered)} · ${est.cost.savingsPct}% under all-${est.assumptions.sessionModel}`));
463
+ info(` "reads-like" is the tier the prompt looks like; a "?" means low confidence. Flags: UC006 wrong-tier, UC007 over-effort, UC008 alwaysOpus.`);
464
+ log('');
465
+ }
466
+
467
+ function cmdSimulate() {
468
+ const target = positional[0];
469
+ if (!target || !existsSync(target)) { err(`usage: ${SELF} simulate <workflow-script.js>`); process.exit(1); }
470
+ const { policy } = loadPolicy();
471
+ const s = scenarioTotals(readFileSync(target, 'utf8'), applyCalibration(policy));
472
+
473
+ if (has('--json')) { log(JSON.stringify({ target, ...s }, null, 2)); return; }
474
+
475
+ log('');
476
+ title(' ultracost simulate');
477
+ log(' ' + dim(tilde(target) + ' · ' + s.stages + ' stage(s)'));
478
+ log('');
479
+ const max = Math.max(s.allOpus, s.allSonnet, s.tiered, 1e-9);
480
+ const row = (label, val, hex, note) => log(' ' + bold(pad14(label)) + ' ' + bar(val, max, 24, hex) + ' ' + money(val) + (note ? ' ' + dim(note) : ''));
481
+ row('all-opus', s.allOpus, COLORS.violet, 'unguided ultracode default');
482
+ row('tiered (yours)', s.tiered, COLORS.green, `${s.allOpus ? Math.round((1 - s.tiered / s.allOpus) * 100) : 0}% under all-opus`);
483
+ row('all-sonnet', s.allSonnet, COLORS.cyan, 'aggressive cost-first');
484
+ log('');
485
+ info(' relative estimate; tiered is your current per-stage pins. Quality-first keeps reasoning on opus.');
486
+ log('');
487
+ }
488
+ function pad14(s) { return (s + ' ').slice(0, 14); }
489
+
490
+ function cmdDiff() {
491
+ const [a, b] = positional;
492
+ if (!a || !b || !existsSync(a) || !existsSync(b)) { err(`usage: ${SELF} diff <old-script.js> <new-script.js> [--ci]`); process.exit(1); }
493
+ const { policy } = loadPolicy();
494
+ const pol = applyCalibration(policy);
495
+ const ea = estimateFile(a, pol);
496
+ const eb = estimateFile(b, pol);
497
+ const dCost = eb.cost.tiered - ea.cost.tiered;
498
+ const dAgents = eb.agents.assumedTotal - ea.agents.assumedTotal;
499
+ const pct = ea.cost.tiered ? Math.round((dCost / ea.cost.tiered) * 100) : 0;
500
+
501
+ if (has('--json')) {
502
+ log(JSON.stringify({ a, b, old: ea.cost, new: eb.cost, deltaTiered: dCost, deltaAgents: dAgents }, null, 2));
503
+ return;
504
+ }
505
+ if (has('--ci')) {
506
+ const sign = dCost >= 0 ? '+' : '−';
507
+ log('## ultracost cost diff');
508
+ log('');
509
+ log('| version | agents | tiered | vs all-opus |');
510
+ log('|---|---|---|---|');
511
+ log(`| \`${basename(a)}\` | ${ea.agents.assumedTotal} | ${money(ea.cost.tiered)} | ${ea.cost.savingsPct}% |`);
512
+ log(`| \`${basename(b)}\` | ${eb.agents.assumedTotal} | ${money(eb.cost.tiered)} | ${eb.cost.savingsPct}% |`);
513
+ log('');
514
+ log(`**Δ tiered cost: ${sign}${money(Math.abs(dCost))} (${pct >= 0 ? '+' : ''}${pct}%)** · Δ agents: ${dAgents >= 0 ? '+' : ''}${dAgents}`);
515
+ return;
516
+ }
517
+
518
+ log('');
519
+ title(' ultracost diff');
520
+ log(' ' + dim(`${basename(a)} → ${basename(b)}`));
521
+ log('');
522
+ log(columns([
523
+ [dim(basename(a)), `${ea.agents.assumedTotal} agents`, money(ea.cost.tiered)],
524
+ [dim(basename(b)), `${eb.agents.assumedTotal} agents`, money(eb.cost.tiered)]
525
+ ], { indent: 2, gap: 3, align: ['left', 'right', 'right'] }));
526
+ log('');
527
+ const up = dCost > 0;
528
+ const deltaStr = `${up ? '+' : ''}${money(dCost)} (${pct >= 0 ? '+' : ''}${pct}%) · ${dAgents >= 0 ? '+' : ''}${dAgents} agents`;
529
+ log(' ' + bold('Δ ') + (up ? color.red(deltaStr) : color.green(deltaStr)));
530
+ log('');
182
531
  }
183
532
 
184
533
  async function cmdPricing() {
@@ -197,68 +546,116 @@ async function cmdPricing() {
197
546
  }
198
547
 
199
548
  function showPricing(pr) {
200
- log(`${c.bold('ultracost pricing')} ${c.dim('(USD per million tokens)')}`);
201
- if (pr?._source) info(`source: ${pr._source}`);
202
- if (pr?._asOf) info(`as of: ${pr._asOf}`);
203
- for (const k of ['opus', 'sonnet', 'haiku']) {
204
- if (pr?.[k]) log(` ${c.cyan(k)}: $${pr[k].input} in / $${pr[k].output} out`);
205
- }
206
- info('refresh from the official page: ultracost pricing refresh');
549
+ log('');
550
+ title(' ultracost pricing');
551
+ log(' ' + dim('USD per million tokens' + (pr?._asOf ? ' · as of ' + pr._asOf : '')));
552
+ log('');
553
+ const rows = ['opus', 'sonnet', 'haiku'].filter((k) => pr?.[k]).map((k) => [
554
+ color[mixKey(k)](k), money6(pr[k].input) + ' in', money6(pr[k].output) + ' out'
555
+ ]);
556
+ log(columns(rows, { indent: 2, gap: 3, align: ['left', 'right', 'right'] }));
557
+ log('');
558
+ if (pr?._source) info(` source: ${pr._source}`);
559
+ info(` refresh: ${SELF} pricing refresh`);
560
+ log('');
561
+ }
562
+
563
+ function deliveryHex(v) {
564
+ return v === 'none' ? COLORS.red : v === 'both' ? COLORS.amber : COLORS.green;
207
565
  }
208
566
 
209
567
  function cmdStatus() {
210
568
  const { policy, source } = loadPolicy();
211
- log(`${c.bold('ultracost status')}\n`);
212
- info(`policy source: ${tilde(source)}`);
213
- log(`${c.bold('tiers')} (never: ${policy.neverUse.join(', ') || 'none'})`);
214
- for (const [name, t] of Object.entries(policy.tiers)) {
215
- const mark = name === policy.default ? c.green(' (default)') : '';
216
- log(` ${c.cyan(name)}: ${t.model}${t.effort ? ' @ ' + t.effort : ''}${mark}`);
569
+ const d = detectDelivery();
570
+ log('');
571
+ title(' ultracost status');
572
+ log('');
573
+
574
+ const dot = (on) => (on ? color.green('') : dim(''));
575
+ const dl = [];
576
+ const pluginActive = d.verdict === 'plugin' || d.verdict === 'both';
577
+ const cliActive = d.verdict === 'cli' || d.verdict === 'both';
578
+ dl.push(`${dot(pluginActive)} plugin ${pluginActive ? color.green('active') : dim('not enabled')}` +
579
+ (pluginActive ? ' ' + dim('v' + (d.plugin.version || '?') + ' · SessionStart + PreToolUse hooks') : ''));
580
+ dl.push(`${dot(cliActive)} cli ${cliActive ? color.green('active') : dim('not installed')}` +
581
+ (cliActive ? ' ' + dim('~/.claude/CLAUDE.md + SessionStart hook') : ''));
582
+ log(panel(dl, { title: 'delivery · ' + d.verdict, hex: deliveryHex(d.verdict) }));
583
+ log('');
584
+
585
+ const tierRows = Object.entries(policy.tiers).map(([name, t]) => [
586
+ color[mixKey(t.model)] ? color[mixKey(t.model)](name) : name,
587
+ `${t.model}${t.effort ? ' @ ' + t.effort : ''}`,
588
+ name === policy.default ? color.green('default') : ''
589
+ ]);
590
+ log(panel([
591
+ columns(tierRows, { gap: 3, align: ['left', 'left', 'left'] }),
592
+ dim('never: ' + (policy.neverUse.join(', ') || 'none'))
593
+ ].join('\n').split('\n'), { title: 'policy', hex: COLORS.violet }));
594
+ log('');
595
+
596
+ // Caveats that change behavior, surfaced loudly.
597
+ if (d.bypass) {
598
+ warn(`permission mode is ${bold(d.permissionMode || 'bypassPermissions')} — the gate's ask path auto-approves, so clean workflows won't pause.`);
599
+ info('unpinned/banned workflows are still hard-denied; turn off bypass (shift+tab) for the full pre-flight stop.');
217
600
  }
218
- log(`\n${c.bold('install')}`);
219
- state(existsSync(CLAUDE_MD) && readFileSync(CLAUDE_MD, 'utf8').includes(MARKER_START), `rules in ${tilde(CLAUDE_MD)}`);
220
- state(existsSync(HOOK_PATH), `hook at ${tilde(HOOK_PATH)}`);
221
- const settings = readSettings();
222
- const registered = settings && settings.hooks?.SessionStart?.some((h) => h.hooks?.some((hh) => hh.command?.includes('ultracost')));
223
- state(!!registered, `hook registered in settings.json`);
601
+ if (d.verdict === 'both') warn('dual delivery: plugin AND cli both active — rules may be injected twice. Remove one.');
602
+ if (d.verdict === 'none') warn(`ultracost is not active — install the plugin or run ${SELF} init.`);
603
+ if (d.gateEnv) info(`ULTRACOST_GATE=${d.gateEnv}`);
604
+ if (d.settingsInvalid) err('settings.json or settings.local.json is invalid JSON');
605
+ info(`policy source: ${tilde(source)}`);
606
+ log('');
224
607
  }
225
608
 
226
609
  function cmdDoctor() {
227
- log(`${c.bold('ultracost doctor')}\n`);
610
+ const d = detectDelivery();
611
+ const lines = [];
228
612
  let issues = 0;
229
- const need = (cond, msg) => { if (cond) ok(msg); else { warn(msg); issues++; } };
613
+ const add = (good, label, detail) => {
614
+ lines.push(`${good ? color.green(symbols.ok) : color.amber(symbols.warn)} ${label}${detail ? ' ' + dim(detail) : ''}`);
615
+ if (!good) issues++;
616
+ };
230
617
 
231
618
  try {
232
619
  const { policy } = loadPolicy();
233
- ok(`policy is valid (${Object.keys(policy.tiers).length} tiers)`);
620
+ add(true, `policy valid ${dim('(' + Object.keys(policy.tiers).length + ' tiers)')}`);
234
621
  } catch (e) {
235
- err(e.message); issues++;
622
+ add(false, 'policy invalid', e.message);
236
623
  }
237
- need(existsSync(CLAUDE_MD) && readFileSync(CLAUDE_MD, 'utf8').includes(MARKER_START), `routing rules present in ${tilde(CLAUDE_MD)}`);
238
- need(existsSync(HOOK_PATH), `re-inject hook installed`);
239
-
240
- const settings = readSettings();
241
- if (settings === undefined) { err(`${tilde(SETTINGS)} is invalid JSON`); issues++; }
242
- else {
243
- const registered = settings?.hooks?.SessionStart?.some((h) => h.hooks?.some((hh) => hh.command?.includes('ultracost')));
244
- need(!!registered, `hook registered for SessionStart`);
245
- if (settings?.permissions?.defaultMode && settings.permissions.defaultMode !== 'auto') {
246
- info(`tip: permission mode is "${settings.permissions.defaultMode}" workflow subagents may prompt`);
247
- }
624
+
625
+ if (d.verdict === 'plugin' || d.verdict === 'both') {
626
+ add(d.plugin.hooks.sessionStart, 'plugin SessionStart policy injection');
627
+ add(d.plugin.hooks.preToolUse, 'plugin PreToolUse cost gate');
628
+ } else if (d.verdict === 'cli') {
629
+ add(d.cli.rules, 'routing rules in ~/.claude/CLAUDE.md');
630
+ add(d.cli.settingsHook, 'SessionStart hook registered');
631
+ add(d.cli.hook, 're-inject hook installed');
632
+ } else {
633
+ add(false, 'ultracost is not active', `install the plugin (/plugin install ultracost@ultracost) or run ${SELF} init`);
248
634
  }
249
635
 
636
+ if (d.verdict === 'both') { lines.push(`${color.amber(symbols.warn)} dual delivery — plugin AND cli both active; remove one to avoid double-injected rules`); issues++; }
637
+ if (d.settingsInvalid) { lines.push(`${color.red(symbols.err)} settings.json or settings.local.json is invalid JSON`); issues++; }
638
+ if (d.bypass) lines.push(dim(`note: ${d.permissionMode || 'bypass'} mode auto-approves the gate's ask path; unpinned workflows are still hard-denied`));
639
+ lines.push(dim('note: pin per stage via the agent() model param — subagent frontmatter "model:" is ignored on some Claude Code 2.1.x (claude-code#52681)'));
640
+
641
+ log('');
642
+ log(panel(lines, { title: issues ? `doctor · ${issues} issue(s)` : 'doctor · all clear', hex: issues ? COLORS.amber : COLORS.green }));
250
643
  log('');
251
- if (issues === 0) log(c.green('All clear. Routing is configured.'));
252
- else { log(c.red(`${issues} issue(s).`)); info('Run: ultracost init'); process.exit(1); }
644
+ if (issues) { info(`fix: ${SELF} init (cli) or /plugin install ultracost@ultracost (plugin)`); process.exit(1); }
253
645
  }
254
646
 
255
647
  function cmdUninstall() {
256
648
  const r = uninstall();
257
- log(`${c.bold('ultracost uninstall')}\n`);
258
- for (const [k, v] of Object.entries(r)) info(`${k}: ${v}`);
649
+ log('');
650
+ title(' ultracost uninstall');
651
+ log('');
652
+ for (const [k, v] of Object.entries(r)) info(` ${k}: ${v}`);
259
653
  ok('done.');
260
- }
261
-
262
- function state(good, label) {
263
- log(` ${good ? c.green('on ') : c.red('off')} ${label}`);
654
+ const d = detectDelivery();
655
+ if (d.plugin.enabled) {
656
+ log('');
657
+ info('note: the plugin is still installed — remove it in Claude Code with:');
658
+ info(' /plugin uninstall ultracost@ultracost then /plugin marketplace remove ultracost');
659
+ }
660
+ log('');
264
661
  }