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/CHANGELOG.md +67 -1
- package/NOTICE +16 -3
- package/README.md +101 -14
- package/bin/cli.js +514 -117
- package/docs/ESTIMATES.md +24 -0
- package/docs/PUBLISHING.md +41 -34
- package/docs/architecture.md +19 -1
- package/docs/policy.md +25 -2
- package/package.json +1 -1
- package/src/classify.js +125 -0
- package/src/cost.js +54 -0
- package/src/detect.js +93 -0
- package/src/estimate.js +18 -0
- package/src/guard.js +244 -166
- package/src/index.js +7 -1
- package/src/lexer.js +227 -0
- package/src/log.js +20 -13
- package/src/loop.js +143 -0
- package/src/paths.js +10 -0
- package/src/policy.js +14 -0
- package/src/render.js +211 -0
- package/src/rules.js +17 -5
- package/src/transcript.js +186 -0
- package/templates/hooks/reinject.mjs +21 -18
- package/templates/hooks/workflow-gate.mjs +51 -45
- package/templates/policy.default.json +15 -2
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
ok(`
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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(`
|
|
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(
|
|
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)}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
log(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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/UC002 — inherit 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(
|
|
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
|
|
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 ?
|
|
171
|
-
const mix = Object.entries(est.modelMix).map(([k, v]) => `${v}x ${k}`).join('
|
|
172
|
-
|
|
173
|
-
log(
|
|
174
|
-
|
|
175
|
-
log('');
|
|
176
|
-
log(
|
|
177
|
-
log(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
610
|
+
const d = detectDelivery();
|
|
611
|
+
const lines = [];
|
|
228
612
|
let issues = 0;
|
|
229
|
-
const
|
|
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
|
-
|
|
620
|
+
add(true, `policy valid ${dim('(' + Object.keys(policy.tiers).length + ' tiers)')}`);
|
|
234
621
|
} catch (e) {
|
|
235
|
-
|
|
622
|
+
add(false, 'policy invalid', e.message);
|
|
236
623
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
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(
|
|
258
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
}
|