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/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.