ultracost 0.2.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.
- package/CHANGELOG.md +81 -0
- package/LICENSE +21 -0
- package/NOTICE +18 -0
- package/README.md +306 -0
- package/bin/cli.js +264 -0
- package/docs/ESTIMATES.md +191 -0
- package/docs/PUBLISHING.md +164 -0
- package/docs/TESTING.md +260 -0
- package/docs/architecture.md +166 -0
- package/docs/policy.md +42 -0
- package/docs/ultracode.md +37 -0
- package/package.json +52 -0
- package/src/estimate.js +101 -0
- package/src/guard.js +300 -0
- package/src/index.js +7 -0
- package/src/install.js +113 -0
- package/src/log.js +18 -0
- package/src/paths.js +27 -0
- package/src/policy.js +80 -0
- package/src/pricing.js +82 -0
- package/src/rules.js +84 -0
- package/templates/hooks/reinject.mjs +41 -0
- package/templates/hooks/workflow-gate.mjs +126 -0
- package/templates/policy.default.json +49 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { loadPolicy } from '../src/policy.js';
|
|
5
|
+
import { scan, fixFile, collectFiles, auditScripts } from '../src/guard.js';
|
|
6
|
+
import { estimateFile } from '../src/estimate.js';
|
|
7
|
+
import { refreshPricing, writePricingToPolicy, DEFAULT_PRICING_URL } from '../src/pricing.js';
|
|
8
|
+
import { install, uninstall, readSettings } from '../src/install.js';
|
|
9
|
+
import {
|
|
10
|
+
ROOT, CLAUDE_MD, HOOK_PATH, POLICY_PATH, SETTINGS, PROJECTS_DIR, tilde, MARKER_START
|
|
11
|
+
} from '../src/paths.js';
|
|
12
|
+
import { c, log, ok, warn, err, info } from '../src/log.js';
|
|
13
|
+
|
|
14
|
+
const version = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
|
|
15
|
+
const argv = process.argv.slice(2);
|
|
16
|
+
const cmd = argv[0] || 'help';
|
|
17
|
+
const has = (flag) => argv.includes(flag);
|
|
18
|
+
const positional = argv.slice(1).filter((a) => !a.startsWith('-'));
|
|
19
|
+
|
|
20
|
+
const money = (x) => '$' + Number(x).toFixed(4);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await dispatch();
|
|
24
|
+
} catch (e) {
|
|
25
|
+
err(e.message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function dispatch() {
|
|
30
|
+
switch (cmd) {
|
|
31
|
+
case 'init': case 'install': cmdInit(); break;
|
|
32
|
+
case 'check': case 'guard': cmdCheck(); break;
|
|
33
|
+
case 'audit': cmdAudit(); break;
|
|
34
|
+
case 'estimate': cmdEstimate(); break;
|
|
35
|
+
case 'pricing': await cmdPricing(); break;
|
|
36
|
+
case 'status': cmdStatus(); break;
|
|
37
|
+
case 'doctor': cmdDoctor(); break;
|
|
38
|
+
case 'uninstall': cmdUninstall(); break;
|
|
39
|
+
case '-v': case '--version': case 'version': log(version); break;
|
|
40
|
+
case 'help': case '-h': case '--help': cmdHelp(); break;
|
|
41
|
+
default:
|
|
42
|
+
err(`Unknown command: ${cmd}`);
|
|
43
|
+
cmdHelp();
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
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
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cmdInit() {
|
|
81
|
+
const { policy, source } = loadPolicy();
|
|
82
|
+
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.`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function cmdCheck() {
|
|
93
|
+
const target = positional[0] || process.cwd();
|
|
94
|
+
const { policy } = loadPolicy();
|
|
95
|
+
|
|
96
|
+
if (has('--fix')) {
|
|
97
|
+
const targets = collectFiles(target);
|
|
98
|
+
let fixed = 0;
|
|
99
|
+
for (const f of targets) fixed += fixFile(f, policy);
|
|
100
|
+
ok(`applied ${fixed} fix(es) across ${targets.length} file(s)`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { findings, files } = scan(target, policy);
|
|
104
|
+
const errors = findings.filter((f) => f.severity === 'error');
|
|
105
|
+
const warns = findings.filter((f) => f.severity === 'warn');
|
|
106
|
+
|
|
107
|
+
if (has('--json')) {
|
|
108
|
+
log(JSON.stringify({ target, files: files.length, findings }, null, 2));
|
|
109
|
+
process.exit(errors.length ? 1 : 0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!findings.length) {
|
|
113
|
+
ok(`${files.length} file(s) scanned — every agent() stage pins a model.`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
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)}`);
|
|
120
|
+
}
|
|
121
|
+
log('');
|
|
122
|
+
log(`${errors.length} error(s), ${warns.length} warning(s) in ${files.length} file(s).`);
|
|
123
|
+
if (errors.length) {
|
|
124
|
+
info(`Fix with: ultracost check ${positional[0] || '.'} --fix`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function cmdAudit() {
|
|
130
|
+
const base = positional[0] || PROJECTS_DIR;
|
|
131
|
+
const { policy } = loadPolicy();
|
|
132
|
+
const { files, totals } = auditScripts(base, policy);
|
|
133
|
+
|
|
134
|
+
if (has('--json')) {
|
|
135
|
+
log(JSON.stringify({ base, ...totals }, null, 2));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
log(`${c.bold('ultracost audit')}\n`);
|
|
140
|
+
if (!files.length) {
|
|
141
|
+
warn(`no workflow scripts found under ${tilde(base)}`);
|
|
142
|
+
info(`looked for ${tilde(base)}/**/workflows/scripts/*.js`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
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('(UC005 — options is a variable)')}`);
|
|
153
|
+
log('');
|
|
154
|
+
log(` ${c.bold('unpinned ratio')} ${pct}%`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function cmdEstimate() {
|
|
158
|
+
const target = positional[0];
|
|
159
|
+
if (!target) { err('usage: ultracost estimate <workflow-script.js> [--json]'); process.exit(1); }
|
|
160
|
+
if (!existsSync(target)) { err(`not found: ${target}`); process.exit(1); }
|
|
161
|
+
const { policy } = loadPolicy();
|
|
162
|
+
const est = estimateFile(target, policy);
|
|
163
|
+
|
|
164
|
+
if (has('--json')) {
|
|
165
|
+
log(JSON.stringify({ target, ...est }, null, 2));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
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.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function cmdPricing() {
|
|
185
|
+
const { policy } = loadPolicy();
|
|
186
|
+
if (positional[0] === 'refresh') {
|
|
187
|
+
const urlIdx = argv.indexOf('--url');
|
|
188
|
+
const url = urlIdx !== -1 ? argv[urlIdx + 1] : undefined;
|
|
189
|
+
info(`fetching official pricing from ${url || policy.pricing?._source || DEFAULT_PRICING_URL} ...`);
|
|
190
|
+
const updated = await refreshPricing(policy, { url });
|
|
191
|
+
const path = writePricingToPolicy(updated);
|
|
192
|
+
ok(`pricing updated in ${tilde(path)} (as of ${updated._asOf})`);
|
|
193
|
+
showPricing(updated);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
showPricing(policy.pricing);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
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');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function cmdStatus() {
|
|
210
|
+
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}`);
|
|
217
|
+
}
|
|
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`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function cmdDoctor() {
|
|
227
|
+
log(`${c.bold('ultracost doctor')}\n`);
|
|
228
|
+
let issues = 0;
|
|
229
|
+
const need = (cond, msg) => { if (cond) ok(msg); else { warn(msg); issues++; } };
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const { policy } = loadPolicy();
|
|
233
|
+
ok(`policy is valid (${Object.keys(policy.tiers).length} tiers)`);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
err(e.message); issues++;
|
|
236
|
+
}
|
|
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
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
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); }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function cmdUninstall() {
|
|
256
|
+
const r = uninstall();
|
|
257
|
+
log(`${c.bold('ultracost uninstall')}\n`);
|
|
258
|
+
for (const [k, v] of Object.entries(r)) info(`${k}: ${v}`);
|
|
259
|
+
ok('done.');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function state(good, label) {
|
|
263
|
+
log(` ${good ? c.green('on ') : c.red('off')} ${label}`);
|
|
264
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Cost estimate, dynamic effort, and the pre-flight gate
|
|
2
|
+
|
|
3
|
+
ultracost does three things beyond routing: it **estimates** the cost of a workflow
|
|
4
|
+
before it runs, it has Claude pick a **per-stage effort** level, and it puts a
|
|
5
|
+
**pre-flight gate** in front of a workflow launch so you can approve, cancel, or
|
|
6
|
+
restructure it.
|
|
7
|
+
|
|
8
|
+
## `ultracost estimate <script>`
|
|
9
|
+
|
|
10
|
+
Static analysis (the same parser as `check`) reads every `agent()` stage, its pinned
|
|
11
|
+
`model` and `effort`, and whether it is a fan-out, then prices it.
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
ultracost estimate ./workflow.js
|
|
15
|
+
|
|
16
|
+
agents 4 fixed + 1 fan-out group(s) x ~5 = ~9
|
|
17
|
+
model mix 3x opus, 6x sonnet
|
|
18
|
+
|
|
19
|
+
baseline (all opus) $0.9000
|
|
20
|
+
tiered (ultracost) $0.5304
|
|
21
|
+
savings $0.3696 (41%)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **baseline** = every stage on the session model (opus @ xhigh) — what an unguided
|
|
25
|
+
ultracode run does.
|
|
26
|
+
- **tiered** = the per-stage `model` + `effort` actually pinned. Unpinned stages
|
|
27
|
+
inherit the session model, so they contribute **no** savings (a built-in incentive
|
|
28
|
+
to pin them).
|
|
29
|
+
- `--json` emits the full breakdown for CI or tooling.
|
|
30
|
+
|
|
31
|
+
### The cost model (and its assumptions)
|
|
32
|
+
|
|
33
|
+
`cost(stage) = inputTokens/1e6 * price.input + outputTokens * effortMultiplier / 1e6 * price.output`
|
|
34
|
+
|
|
35
|
+
Defaults (all editable in `policy.json` under `estimation`):
|
|
36
|
+
|
|
37
|
+
- `tokensPerStage`: `{ input: 2000, output: 1200 }`
|
|
38
|
+
- `effortOutputMultiplier`: `low 0.4, medium 1, high 1.8, xhigh 3, max 4`
|
|
39
|
+
- `assumedFanout`: `5` (items per fan-out group, since the real count is a runtime value)
|
|
40
|
+
|
|
41
|
+
These are deliberately simple per-stage assumptions, **not** a measured token count.
|
|
42
|
+
Treat the dollar figures as relative estimates (tiered vs baseline), not a bill. The
|
|
43
|
+
savings **percentage** is robust to the absolute token assumption; the absolute dollars
|
|
44
|
+
scale with it.
|
|
45
|
+
|
|
46
|
+
## Pricing is official-sourced and refreshable
|
|
47
|
+
|
|
48
|
+
Prices live in `policy.json` under `pricing`, with provenance:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
"pricing": {
|
|
52
|
+
"_source": "https://platform.claude.com/docs/en/about-claude/pricing",
|
|
53
|
+
"_asOf": "2026-06-13",
|
|
54
|
+
"_models": { "opus": "Claude Opus 4.8", "sonnet": "Claude Sonnet 4.6", "haiku": "Claude Haiku 4.5" },
|
|
55
|
+
"opus": { "input": 5, "output": 25 },
|
|
56
|
+
"sonnet": { "input": 3, "output": 15 },
|
|
57
|
+
"haiku": { "input": 1, "output": 5 }
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The committed numbers are a snapshot of Anthropic's official pricing page. Refresh them
|
|
62
|
+
any time:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
ultracost pricing # show current prices + source + as-of date
|
|
66
|
+
ultracost pricing refresh # fetch the official page, parse, and update policy.json
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The estimate reads prices from `policy.json` (offline, deterministic) — there is **no**
|
|
70
|
+
network call on the estimate hot path, so it works in CI and offline. `refresh` is the
|
|
71
|
+
only command that reaches the network, and only when you run it. Bump the model version
|
|
72
|
+
strings in `_models` when a new model ships, then `refresh`.
|
|
73
|
+
|
|
74
|
+
## Dynamic per-stage effort
|
|
75
|
+
|
|
76
|
+
Each stage also gets an `effort`, chosen as the **lowest level that fits**, bounded by
|
|
77
|
+
the model (`sonnet` up to `high`, `opus` up to `xhigh`):
|
|
78
|
+
|
|
79
|
+
| effort | use for |
|
|
80
|
+
|--------|---------|
|
|
81
|
+
| `low` | trivial deterministic work: listing/globbing, simple extraction, formatting |
|
|
82
|
+
| `medium` | light judgment on a small surface: a single straightforward edit, summarizing one source |
|
|
83
|
+
| `high` | standard coding/analysis: most refactors, per-file review, non-trivial tests |
|
|
84
|
+
| `xhigh` | hard reasoning: cross-file architecture, adversarial review, planning, final synthesis |
|
|
85
|
+
|
|
86
|
+
Effort feeds the estimate (higher effort = more output tokens = more cost), so dialing a
|
|
87
|
+
mechanical stage down to `low` is visible savings.
|
|
88
|
+
|
|
89
|
+
## The pre-flight gate (approve / cancel / modify)
|
|
90
|
+
|
|
91
|
+
Before launching a workflow, the injected policy has Claude: draft the script, run
|
|
92
|
+
`ultracost estimate` on it, then use the **AskUserQuestion** tool to offer three
|
|
93
|
+
options — **Approve** (launch), **Cancel** (don't), **Modify** (restructure to cut cost,
|
|
94
|
+
re-estimate, ask again).
|
|
95
|
+
|
|
96
|
+
### Two gate mechanisms, and an honest limitation
|
|
97
|
+
|
|
98
|
+
1. **Deterministic `PreToolUse` gate (default, hard).** `templates/hooks/workflow-gate.mjs`
|
|
99
|
+
is registered by the plugin (`hooks/hooks.json`) on the `Workflow` tool. It runs the
|
|
100
|
+
static guard **and** the cost estimate on the drafted script, then returns a permission
|
|
101
|
+
decision so **every** workflow launch pauses with the numbers — independent of whether
|
|
102
|
+
the model decides to ask. This is the enforcement layer.
|
|
103
|
+
- **Where you see the number.** The gate emits the estimate as a top-level
|
|
104
|
+
`systemMessage` (the documented hook→user channel) **and** as the
|
|
105
|
+
`permissionDecisionReason`. The `systemMessage` is what reliably renders: Claude Code
|
|
106
|
+
currently does **not** display the `permissionDecisionReason` for an `"ask"` decision
|
|
107
|
+
in the TUI ([anthropics/claude-code#24059](https://github.com/anthropics/claude-code/issues/24059)),
|
|
108
|
+
so without `systemMessage` the cost would be computed but invisible. For `strict`/`deny`
|
|
109
|
+
the reason renders too. The native "Run a dynamic workflow?" confirmation is Claude
|
|
110
|
+
Code's own dialog — the ultracost line appears as a system message around it, not inside it.
|
|
111
|
+
- **It leads with the problem.** If any stage is unpinned/banned/`inherit`, the message
|
|
112
|
+
opens with `⚠ ultracost: N/M stage(s) NOT pinned -> will inherit <session model>` before
|
|
113
|
+
the estimate — so an accidental all-Opus fan-out is impossible to miss. (This is the
|
|
114
|
+
exact failure a live test caught: a `pipeline(...)` build/verify/fix workflow where the
|
|
115
|
+
model pinned *no* stage.)
|
|
116
|
+
- **Mode-aware by default — hard in every permission mode.** A `PreToolUse` hook runs in
|
|
117
|
+
*all* permission modes; bypass only auto-approves the `ask` path, while a `deny` is
|
|
118
|
+
honored regardless of mode. The gate reads `permission_mode` from the event and adapts:
|
|
119
|
+
|
|
120
|
+
| mode | clean (all pinned) | problem (unpinned/banned/`inherit`) |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `default` / `acceptEdits` / `auto` | ask + estimate | **ask** + ⚠ warning (an ask surfaces here) |
|
|
123
|
+
| `bypassPermissions` / `dontAsk` | ask + estimate | **deny** (an ask wouldn't pause, so block) |
|
|
124
|
+
| `plan` | (a workflow doesn't launch in plan mode) | — |
|
|
125
|
+
|
|
126
|
+
- **Env overrides (`ULTRACOST_GATE`):**
|
|
127
|
+
- *unset* (default) — the mode-aware behavior above.
|
|
128
|
+
- `strict` — **deny** on any problem in *every* mode; `ask` when all pinned.
|
|
129
|
+
- `ask` — never escalate to deny; always `ask` (opt out of the mode-aware deny).
|
|
130
|
+
- `off` — disable entirely, for non-interactive runs (`claude -p`, Auto Mode, CI),
|
|
131
|
+
where an unanswered `ask` is denied (the gate fails closed). Disable it there on
|
|
132
|
+
purpose rather than letting an unpriced fan-out through silently.
|
|
133
|
+
- **Residual limitation:** Claude Code currently skips `PreToolUse` hooks for subagents
|
|
134
|
+
dispatched under `bypassPermissions`
|
|
135
|
+
([anthropics/claude-code#43772](https://github.com/anthropics/claude-code/issues/43772)),
|
|
136
|
+
so a nested agent there can evade the gate. The top-level `Workflow` launch is still gated.
|
|
137
|
+
2. **AskUserQuestion-mediated (nicer UX, model-driven).** Driven by the always-on
|
|
138
|
+
`SessionStart` policy injection. This renders the arrow-key 3-option
|
|
139
|
+
Approve/Cancel/Modify menu, but it is *model-mediated* — Claude runs it because the
|
|
140
|
+
policy is in context. It only appears in an interactive session. The hard hook above
|
|
141
|
+
is what guarantees a stop even when this is skipped.
|
|
142
|
+
|
|
143
|
+
A plugin hook **cannot** render a fully custom arrow-key menu directly — that is an open
|
|
144
|
+
Claude Code feature request
|
|
145
|
+
([anthropics/claude-code#52343](https://github.com/anthropics/claude-code/issues/52343)).
|
|
146
|
+
Hooks can only block, allow, or escalate to the built-in allow/deny prompt. The custom
|
|
147
|
+
3-option Approve/Cancel/Modify menu therefore comes from `AskUserQuestion`, which the
|
|
148
|
+
model invokes, rather than from the kernel. This is documented, not hidden.
|
|
149
|
+
|
|
150
|
+
## Covered cases
|
|
151
|
+
|
|
152
|
+
- Fixed stages, fan-out stages (`.map`/`.flatMap`/`Array.from` **and `pipeline(items, ...stages)`**,
|
|
153
|
+
whose every stage runs once per item), nested `parallel([...])` thunks (fixed count),
|
|
154
|
+
pinned and unpinned stages, banned/`inherit` models, and prompt text that literally
|
|
155
|
+
contains `agent(` (ignored). See `tests/estimate.test.js`.
|
|
156
|
+
|
|
157
|
+
> `pipeline(items, ...stages)` is the Workflow API's per-item fan-out primitive — each
|
|
158
|
+
> stage's `agent()` runs once for every item in `items`. The guard and estimate treat
|
|
159
|
+
> those stages as fan-out (counted as `assumedFanout` each, like `.map`). This is the
|
|
160
|
+
> exact shape an `ultracode` build/verify/fix workflow takes, so without it the agent
|
|
161
|
+
> count and cost are badly under-reported.
|
|
162
|
+
|
|
163
|
+
## Limitations
|
|
164
|
+
|
|
165
|
+
- **Estimates, not bills.** Per-stage token counts are assumptions; the absolute dollars
|
|
166
|
+
scale with them. The tiered-vs-baseline ratio is the trustworthy signal.
|
|
167
|
+
- **Fan-out is a range.** The real item count is a runtime value; the estimate uses
|
|
168
|
+
`assumedFanout` and the total scales linearly with the real count.
|
|
169
|
+
- **Dynamic options** (`agent(task, opts)` where `opts` is a variable) can't be read
|
|
170
|
+
statically — the guard reports `UC005` and the estimate treats the stage as unpinned.
|
|
171
|
+
- **The gate's pause needs a TUI; deny does not.** The hard `PreToolUse` gate is on by
|
|
172
|
+
default. In interactive modes that surface an ask (`default`/`acceptEdits`/`auto`) it
|
|
173
|
+
pauses with the estimate; in `bypassPermissions`/`dontAsk` it auto-escalates an unpinned
|
|
174
|
+
workflow to a `deny` (honored in every mode). In non-interactive runs an unanswered `ask`
|
|
175
|
+
is denied, so set `ULTRACOST_GATE=off` there. The 3-option AskUserQuestion menu needs a
|
|
176
|
+
TUI session.
|
|
177
|
+
|
|
178
|
+
## Validation (live, multi-domain)
|
|
179
|
+
|
|
180
|
+
Drafted by Claude under the plugin across domains; each script guard-clean (every stage
|
|
181
|
+
pinned), with dynamic effort, then measured by `ultracost estimate`:
|
|
182
|
+
|
|
183
|
+
| Domain | stages (model @ effort) | est. baseline (all-opus) | est. tiered | savings |
|
|
184
|
+
|--------|--------------------------|--------------------------|-------------|---------|
|
|
185
|
+
| Code refactor | opus@xhigh, opus@high, opus@xhigh | $0.30 | $0.264 | 12% |
|
|
186
|
+
| Web research | opus@high, sonnet@medium, opus@xhigh | $0.30 | $0.188 | 37% |
|
|
187
|
+
| CSV data | sonnet@low, sonnet@high (fan-out), opus@xhigh | $0.70 | $0.305 | 56% |
|
|
188
|
+
| Docs gen | sonnet@low, opus@high, opus@xhigh | $0.30 | $0.177 | 41% |
|
|
189
|
+
|
|
190
|
+
Savings track how much of the work is mechanical/fan-out (droppable to sonnet + lower
|
|
191
|
+
effort) versus genuine reasoning that stays on opus — exactly as intended.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Publishing & recognition
|
|
2
|
+
|
|
3
|
+
How to ship ultracost and get it found, ordered by impact. Do the pre-publish checklist
|
|
4
|
+
first, then work down the distribution list.
|
|
5
|
+
|
|
6
|
+
> **External-site note.** Anthropic plugin/marketplace facts below were verified against
|
|
7
|
+
> the official docs (`code.claude.com/docs/en/plugins`,
|
|
8
|
+
> `code.claude.com/docs/en/plugin-marketplaces`) on **2026-06-14**. Third-party sites
|
|
9
|
+
> (awesome lists, auto-trackers) come from the project plan — confirm their current
|
|
10
|
+
> submission rules on each site before relying on them, since they change.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Pre-publish checklist
|
|
15
|
+
|
|
16
|
+
The GitHub handle is set to `danielkremen818` across the repo. If you fork or move it,
|
|
17
|
+
update the handle in every file that ships:
|
|
18
|
+
|
|
19
|
+
- [x] `package.json` — `repository.url`, `bugs.url`, `homepage`.
|
|
20
|
+
- [x] `README.md` — install commands (`/plugin marketplace add danielkremen818/ultracost`) and the npm/CI badge URLs.
|
|
21
|
+
- [x] `CHANGELOG.md` — the `[Unreleased]`/release compare links.
|
|
22
|
+
- [x] `.claude-plugin/plugin.json` — `homepage` and `repository`; also confirm `author` and `version`.
|
|
23
|
+
- [ ] `LICENSE` and `NOTICE` — confirm the copyright holder.
|
|
24
|
+
|
|
25
|
+
Names that must stay consistent across the plugin package and the docs (so the documented
|
|
26
|
+
install works):
|
|
27
|
+
|
|
28
|
+
- Marketplace name: **`ultracost`** and plugin name: **`ultracost`** → users install with
|
|
29
|
+
`/plugin install ultracost@ultracost`.
|
|
30
|
+
- Command resolves to **`/ultracost:check`**.
|
|
31
|
+
|
|
32
|
+
Sanity-check before any submission:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
claude plugin validate . # marketplace.json schema + referenced plugin.json
|
|
36
|
+
claude plugin validate ./ # (same, repo root)
|
|
37
|
+
npm test # unit tests
|
|
38
|
+
node bin/cli.js check examples/workflow.good.js
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### GitHub repo "About" (sidebar metadata)
|
|
42
|
+
|
|
43
|
+
When the remote is created, set these on the repo's **About** panel (the gear icon at
|
|
44
|
+
the top-right of the repo page) so the listing reads well and the auto-trackers index it.
|
|
45
|
+
|
|
46
|
+
**Description** (one line):
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
Per-stage model routing for Claude Code ultracode dynamic workflows — keeps a fan-out from silently running every subagent on Opus 4.8.
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Topics:**
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
claude-code, claude-code-plugin, ultracode, dynamic-workflows, subagents, model-routing, cost-optimization, anthropic, claude
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Website:** the npm package page once published (`https://www.npmjs.com/package/ultracost`).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Distribution, ordered by impact
|
|
63
|
+
|
|
64
|
+
### 1. Official community marketplace (highest reach)
|
|
65
|
+
|
|
66
|
+
Anthropic runs a public community marketplace, `anthropics/claude-plugins-community`, that
|
|
67
|
+
users add with `/plugin marketplace add anthropics/claude-plugins-community` and install
|
|
68
|
+
from as `@claude-community`. Approved plugins also surface on `claude.com/plugins`.
|
|
69
|
+
|
|
70
|
+
Submit through the in-app directory form. The project plan points to the short link
|
|
71
|
+
**`clau.de/plugin-directory-submission`**. As of 2026-06-14 the official docs list these
|
|
72
|
+
canonical submission entry points:
|
|
73
|
+
|
|
74
|
+
- **claude.ai:** `claude.ai/admin-settings/directory/submissions/plugins/new` — requires a
|
|
75
|
+
Team or Enterprise org with directory-management access (org Owners have it by default).
|
|
76
|
+
- **Console:** `platform.claude.com/plugins/submit` — for individual authors not in a
|
|
77
|
+
Team/Enterprise org.
|
|
78
|
+
|
|
79
|
+
What to know:
|
|
80
|
+
|
|
81
|
+
- Submissions go through an **automated safety screening** plus the same
|
|
82
|
+
`claude plugin validate` check the pipeline runs — pass it locally first.
|
|
83
|
+
- Approved plugins are pinned to a specific commit SHA in the community catalog; CI bumps the
|
|
84
|
+
pin as you push. The public catalog **syncs nightly**, so expect a delay between approval
|
|
85
|
+
and your plugin appearing.
|
|
86
|
+
- The separate **official** marketplace (`claude-plugins-official`) is curated by Anthropic
|
|
87
|
+
at its discretion — there's no application; the submission form does not add to it.
|
|
88
|
+
|
|
89
|
+
### 2. Your own marketplace repo
|
|
90
|
+
|
|
91
|
+
ultracost ships its own `.claude-plugin/marketplace.json`, so anyone can install immediately
|
|
92
|
+
without waiting on review:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
/plugin marketplace add danielkremen818/ultracost
|
|
96
|
+
/plugin install ultracost@ultracost
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This is the install path the README leads with. It works the moment the repo is public —
|
|
100
|
+
put it in the README and launch posts.
|
|
101
|
+
|
|
102
|
+
### 3. awesome-claude-code (hesreallyhim)
|
|
103
|
+
|
|
104
|
+
A large, high-traffic curated list (the plan notes ~45k stars — verify the current count).
|
|
105
|
+
Submit via the repo's contribution form/PR process. Their bar, which ultracost already meets:
|
|
106
|
+
|
|
107
|
+
- **Evidence-based claims** — lead with the audit finding (most real `ultracode` stages are
|
|
108
|
+
unpinned; even Anthropic's bundled `deep-research` workflow pins zero stages) and a short
|
|
109
|
+
demo (`ultracost audit ~/.claude/projects`).
|
|
110
|
+
- **OSS license** — MIT.
|
|
111
|
+
- **No telemetry, no network calls** — ultracost is a local static analyzer + file installer;
|
|
112
|
+
it makes no outbound requests.
|
|
113
|
+
|
|
114
|
+
### 4. Auto-trackers (passive listings)
|
|
115
|
+
|
|
116
|
+
These sites index public Claude Code plugin/marketplace repos automatically; a public repo
|
|
117
|
+
with a valid `marketplace.json` is usually enough. Per the plan:
|
|
118
|
+
|
|
119
|
+
- `awesomeclaudeplugins.com`
|
|
120
|
+
- `claudecodemarketplace.com`
|
|
121
|
+
- `claudecodeplugins.dev`
|
|
122
|
+
|
|
123
|
+
Confirm each site's current intake (some have a submit form, some scrape) before assuming a
|
|
124
|
+
listing.
|
|
125
|
+
|
|
126
|
+
### 5. npm publish + GitHub release
|
|
127
|
+
|
|
128
|
+
CI is already wired: pushing a `vX.Y.Z` tag runs the tests, creates a GitHub Release with
|
|
129
|
+
generated notes, and publishes to npm when `NPM_TOKEN` is set (see
|
|
130
|
+
`.github/workflows/release.yml`).
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# bump version in package.json (and plugin.json), update CHANGELOG.md, commit, then:
|
|
134
|
+
git tag v0.1.0
|
|
135
|
+
git push origin v0.1.0
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This makes `npx ultracost ...` work for the CLI/CI audience and gives the plugin a citable
|
|
139
|
+
release.
|
|
140
|
+
|
|
141
|
+
### 6. Launch posts
|
|
142
|
+
|
|
143
|
+
Lead every post with the evidence line:
|
|
144
|
+
|
|
145
|
+
> Even Anthropic's bundled `deep-research` workflow runs every stage on your session model —
|
|
146
|
+
> in a scan of ~22 real `ultracode` scripts, almost none pinned a model. ultracost makes the
|
|
147
|
+
> per-stage routing explicit and verifiable.
|
|
148
|
+
|
|
149
|
+
Channels:
|
|
150
|
+
|
|
151
|
+
- **r/ClaudeAI** — problem + the `ultracost audit` screenshot + install.
|
|
152
|
+
- **Anthropic Discord** — relevant plugin/workflow channels.
|
|
153
|
+
- **#claudecode on X** — the evidence line + a short demo clip.
|
|
154
|
+
- **GitHub Discussions** — a longer write-up linking the audit output and `docs/ultracode.md`.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Post-launch
|
|
159
|
+
|
|
160
|
+
- Watch the community catalog for your plugin to appear (nightly sync) and link it once live.
|
|
161
|
+
- Keep `version` bumped on every plugin change, or users won't get updates (Claude Code skips
|
|
162
|
+
re-install when the resolved version is unchanged).
|
|
163
|
+
- Re-run `ultracost audit` periodically as Claude Code evolves — the headline stat is the best
|
|
164
|
+
proof the problem still exists.
|