@tokenzero/cli 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/tokenzero.mjs +196 -145
  2. package/package.json +1 -1
package/bin/tokenzero.mjs CHANGED
@@ -15,14 +15,14 @@ import crypto from "node:crypto";
15
15
  var P = (provider, tier, input, output, cachedInput, cacheWrite, extra = {}) => ({ provider, tier, input, output, cachedInput, cacheWrite, ...extra });
16
16
  var PRICES = {
17
17
  // ---- OpenAI (standard tier, ≤272K; cache write = 30m TTL rate) ----
18
- "gpt-6-astra": P("openai", "frontier", 10, 50, 1, 12.5, { longCtxAt: 272e3, longCtx: { input: 20, output: 75, cachedInput: 2, cacheWrite: 25 }, minCacheTokens: 1024 }),
19
- "gpt-6.1-sol": P("openai", "mid", 2, 10, 0.1, 2.5, { longCtxAt: 272e3, longCtx: { input: 4, output: 15, cachedInput: 0.2, cacheWrite: 5 }, minCacheTokens: 1024 }),
18
+ "gpt-6-astra": P("openai", "frontier", 10, 50, 1, 12.5, { ultrafast: true, longCtxAt: 272e3, longCtx: { input: 20, output: 75, cachedInput: 2, cacheWrite: 25 }, minCacheTokens: 1024 }),
19
+ "gpt-6.1-sol": P("openai", "mid", 2, 10, 0.1, 2.5, { ultrafast: true, longCtxAt: 272e3, longCtx: { input: 4, output: 15, cachedInput: 0.2, cacheWrite: 5 }, minCacheTokens: 1024 }),
20
20
  "gpt-6-sol": P("openai", "mid", 2, 10, 0.2, 2.5, { longCtxAt: 272e3, longCtx: { input: 4, output: 15, cachedInput: 0.4, cacheWrite: 5 }, minCacheTokens: 1024 }),
21
21
  "gpt-6-luna": P("openai", "fast", 0.1, 0.5, 0.01, 0.125, { longCtxAt: 272e3, longCtx: { input: 0.2, output: 0.75, cachedInput: 0.02, cacheWrite: 0.25 }, minCacheTokens: 1024 }),
22
22
  "gpt-5.6-sol": P("openai", "frontier", 4, 20, 0.4, 5, { longCtxAt: 272e3, longCtx: { input: 8, output: 30, cachedInput: 0.8, cacheWrite: 10 }, minCacheTokens: 1024 }),
23
23
  "gpt-5.6-terra": P("openai", "mid", 2, 12, 0.2, 2.5, { longCtxAt: 272e3, longCtx: { input: 4, output: 18, cachedInput: 0.4, cacheWrite: 5 }, minCacheTokens: 1024 }),
24
24
  "gpt-5.6-luna": P("openai", "fast", 0.2, 1.2, 0.02, 0.25, { longCtxAt: 272e3, longCtx: { input: 0.4, output: 1.8, cachedInput: 0.04, cacheWrite: 0.5 }, minCacheTokens: 1024 }),
25
- "gpt-5.5": P("openai", "frontier", 5, 30, 0.5, void 0, { longCtxAt: 272e3, longCtx: { input: 10, output: 45, cachedInput: 1 }, minCacheTokens: 1024 }),
25
+ "gpt-5.5": P("openai", "frontier", 5, 30, 0.5, void 0, { fastFactor: 2.5, longCtxAt: 272e3, longCtx: { input: 10, output: 45, cachedInput: 1 }, minCacheTokens: 1024 }),
26
26
  "gpt-5.5-pro": P("openai", "frontier", 30, 180),
27
27
  "gpt-5.4": P("openai", "frontier", 2.5, 15, 0.25, void 0, { longCtxAt: 272e3, longCtx: { input: 5, output: 22.5, cachedInput: 0.5 }, minCacheTokens: 1024 }),
28
28
  "gpt-5.4-pro": P("openai", "frontier", 30, 180),
@@ -40,10 +40,10 @@ var PRICES = {
40
40
  "o3-mini": P("openai", "fast", 1.1, 4.4, 0.55, void 0, { minCacheTokens: 1024 }),
41
41
  "o4-mini": P("openai", "fast", 1.1, 4.4, 0.275, void 0, { minCacheTokens: 1024 }),
42
42
  "o1": P("openai", "frontier", 15, 60, 7.5),
43
- "gpt-4.1": P("openai", "mid", 2, 8, 0.5, void 0, { minCacheTokens: 1024 }),
43
+ "gpt-4.1": P("openai", "mid", 2, 8, 0.5, void 0, { fastFactor: 1.75, minCacheTokens: 1024 }),
44
44
  "gpt-4.1-mini": P("openai", "fast", 0.4, 1.6, 0.1, void 0, { minCacheTokens: 1024 }),
45
45
  "gpt-4.1-nano": P("openai", "fast", 0.1, 0.4, 0.025, void 0, { minCacheTokens: 1024 }),
46
- "gpt-4o": P("openai", "mid", 2.5, 10, 1.25, void 0, { minCacheTokens: 1024 }),
46
+ "gpt-4o": P("openai", "mid", 2.5, 10, 1.25, void 0, { fastFactor: 1.7, minCacheTokens: 1024 }),
47
47
  "gpt-4o-mini": P("openai", "fast", 0.15, 0.6, 0.075, void 0, { minCacheTokens: 1024 }),
48
48
  // ---- Anthropic (cache write = 5-minute TTL, 1.25x; 1h TTL is 2x) ----
49
49
  "claude-fable-5-1": P("anthropic", "frontier", 10, 50, 0.25, 12.5, { minCacheTokens: 512 }),
@@ -54,7 +54,8 @@ var PRICES = {
54
54
  "claude-opus-4-7": P("anthropic", "frontier", 5, 25, 0.5, 6.25, { minCacheTokens: 2048 }),
55
55
  "claude-opus-4-6": P("anthropic", "frontier", 5, 25, 0.5, 6.25, { minCacheTokens: 4096 }),
56
56
  "claude-opus-4-5": P("anthropic", "frontier", 5, 25, 0.5, 6.25, { minCacheTokens: 4096 }),
57
- "claude-sonnet-5-5": P("anthropic", "mid", 2, 10, 0.2, 2.5, { minCacheTokens: 512 }),
57
+ "claude-sonnet-5-5": P("anthropic", "mid", 2, 10, 0.1, 2.5, { minCacheTokens: 512 }),
58
+ // cache hits 0.05x base on Opus 5.5 and Sonnet 5.5
58
59
  "claude-sonnet-5": P("anthropic", "mid", 2, 10, 0.2, 2.5, { minCacheTokens: 1024 }),
59
60
  "claude-sonnet-4-6": P("anthropic", "mid", 3, 15, 0.3, 3.75, { minCacheTokens: 1024 }),
60
61
  "claude-sonnet-4-5": P("anthropic", "mid", 3, 15, 0.3, 3.75, { minCacheTokens: 1024 }),
@@ -71,18 +72,18 @@ var PRICES = {
71
72
  "gemini-2.5-pro": P("gemini", "frontier", 1.25, 10, 0.125, void 0, { longCtxAt: 2e5, longCtx: { input: 2.5, output: 15, cachedInput: 0.25 } }),
72
73
  "gemini-2.5-flash": P("gemini", "fast", 0.3, 2.5, 0.03),
73
74
  "gemini-2.5-flash-lite": P("gemini", "fast", 0.1, 0.4, 0.01),
74
- // ---- xAI Grok ----
75
- "grok-4": P("xai", "frontier", 3, 15, 0.75),
76
- "grok-4-fast": P("xai", "fast", 0.2, 0.8, 0.05),
77
- "grok-4-mini": P("xai", "fast", 0.2, 0.8, 0.05),
78
- "grok-3": P("xai", "mid", 3, 15, 0.75),
79
- "grok-3-mini": P("xai", "fast", 0.3, 0.5, 0.075),
80
- // ---- DeepSeek ----
81
- "deepseek-v3": P("deepseek", "fast", 0.27, 1.1, 0.07),
82
- "deepseek-chat": P("deepseek", "fast", 0.27, 1.1, 0.07),
83
- "deepseek-r1": P("deepseek", "mid", 0.55, 2.19, 0.14),
84
- "deepseek-reasoner": P("deepseek", "mid", 0.55, 2.19, 0.14)
75
+ // ---- xAI (docs.x.ai/docs/models; prompts over 200K tokens bill the whole request at 2x) ----
76
+ "grok-4.7": P("xai", "frontier", 2, 6, 0.5, void 0, { longCtxAt: 2e5, longCtx: { input: 4, output: 12, cachedInput: 1 } }),
77
+ "grok-4.6": P("xai", "frontier", 2, 6, 0.5, void 0, { longCtxAt: 2e5, longCtx: { input: 4, output: 12, cachedInput: 1 } }),
78
+ "grok-4.5": P("xai", "frontier", 2, 6, 0.3, void 0, { longCtxAt: 2e5, longCtx: { input: 4, output: 12, cachedInput: 0.6 } }),
79
+ "grok-4.3": P("xai", "mid", 1.25, 2.5, 0.2, void 0, { longCtxAt: 2e5, longCtx: { input: 2.5, output: 5, cachedInput: 0.4 } }),
80
+ "grok-4.20": P("xai", "mid", 1.25, 2.5, 0.2, void 0, { longCtxAt: 2e5, longCtx: { input: 2.5, output: 5, cachedInput: 0.4 } }),
81
+ "grok-build-0.1": P("xai", "fast", 1, 2, 0.2, void 0, { longCtxAt: 2e5, longCtx: { input: 2, output: 4, cachedInput: 0.4 } }),
82
+ // ---- DeepSeek (api-docs.deepseek.com; PEAK rates listed, off-peak is half: see priceModifier) ----
83
+ "deepseek-v4-pro": P("deepseek", "mid", 1.32, 3.96, 0.044),
84
+ "deepseek-flash": P("deepseek", "fast", 0.3, 1.2, 6e-3)
85
85
  };
86
+ var MODEL_ALIASES = { "deepseek-v4-flash": "deepseek-flash", "deepseek-v4-flash-vision-exp": "deepseek-flash" };
86
87
  var CASCADE_TARGET = {
87
88
  "gpt-6-astra": "gpt-6.1-sol",
88
89
  "gpt-6.1-sol": "gpt-6-luna",
@@ -111,20 +112,21 @@ var CASCADE_TARGET = {
111
112
  "gemini-4-argon": "gemini-3.8-flash",
112
113
  "gemini-3.1-pro": "gemini-3.8-flash",
113
114
  "gemini-3.8-flash": "gemini-3.5-flash-lite",
115
+ "grok-4.7": "grok-4.3",
116
+ "grok-4.6": "grok-4.3",
117
+ "grok-4.5": "grok-4.3",
118
+ "deepseek-v4-pro": "deepseek-flash",
114
119
  "gemini-2.5-pro": "gemini-2.5-flash",
115
- "gemini-2.5-flash": "gemini-2.5-flash-lite",
116
- "grok-4": "grok-4-fast",
117
- "grok-3": "grok-3-mini",
118
- "deepseek-r1": "deepseek-v3",
119
- "deepseek-reasoner": "deepseek-chat"
120
+ "gemini-2.5-flash": "gemini-2.5-flash-lite"
120
121
  };
121
122
  var KEYS_BY_LENGTH = Object.keys(PRICES).sort((a, b) => b.length - a.length);
122
123
  function normalizeModel(model) {
123
124
  if (typeof model !== "string" || !model) return null;
124
125
  let m = model.toLowerCase().trim();
125
- m = m.replace(/^(openai|anthropic|google|gemini|x-ai|xai|deepseek|bedrock|models|vertex_ai)\//, "");
126
- m = m.replace(/^(?:us\.|eu\.|apac\.)?anthropic\./, "");
127
- m = m.replace(/-v\d+:\d+$/, "");
126
+ m = m.replace(/^(openai|anthropic|google|models|vertex_ai|x-ai|xai|deepseek|bedrock|openrouter)\//, "");
127
+ m = m.replace(/^(us|eu|apac|global)\./, "").replace(/^(anthropic|amazon|meta|mistral)\./, "");
128
+ m = m.replace(/-v\d+(?::\d+)?$/, "");
129
+ if (MODEL_ALIASES[m]) m = MODEL_ALIASES[m];
128
130
  m = m.replace(/@\d{8}$/, "");
129
131
  m = m.replace(/-(\d{8}|\d{4}-\d{2}-\d{2})$/, "");
130
132
  m = m.replace(/-(latest|preview|customtools|thinking)$/, "");
@@ -146,6 +148,33 @@ function costOf(model, u, factor = 1) {
146
148
  const M = 1e6;
147
149
  return factor * (fresh * tier.input / M + cached * (tier.cachedInput ?? tier.input) / M + written * (tier.cacheWrite ?? tier.input) / M + u.output * tier.output / M);
148
150
  }
151
+ var MODEL_SUNSET = {
152
+ "gpt-4o": { retires: "2027-01-14", replacement: "gpt-6-luna", announced: "2026-10-14" },
153
+ "gpt-4o-mini": { retires: "2027-01-14", replacement: "gpt-6-luna", announced: "2026-10-14" },
154
+ "gpt-4.1": { retires: "2027-01-14", replacement: "gpt-6-luna", announced: "2026-10-14" },
155
+ "gpt-4.1-mini": { retires: "2027-01-14", replacement: "gpt-6-luna", announced: "2026-10-14" },
156
+ "gpt-4.1-nano": { retires: "2026-10-23", replacement: "gpt-5.6-luna", announced: "2026-04-22" },
157
+ "gpt-5": { retires: "2026-12-11", replacement: "gpt-5.6-sol", announced: "2026-06-11" },
158
+ "gpt-5-mini": { retires: "2026-12-11", replacement: "gpt-5.6-terra", announced: "2026-06-11" },
159
+ "gpt-5-nano": { retires: "2026-12-11", replacement: "gpt-5.6-luna", announced: "2026-06-11" },
160
+ "gpt-5-pro": { retires: "2026-12-11", replacement: "gpt-5.6-sol", announced: "2026-06-11" },
161
+ "gpt-5.1": { retires: "2027-04-01", replacement: "gpt-6-sol", announced: "2026-10-01" },
162
+ "gpt-5.4-nano": { retires: "2027-04-01", replacement: "gpt-6-luna", announced: "2026-10-01" },
163
+ "o1": { retires: "2026-10-23", replacement: "gpt-5.6-sol", announced: "2026-04-22" },
164
+ "o3": { retires: "2026-12-11", replacement: "gpt-5.6-sol", announced: "2026-06-11" },
165
+ "o3-pro": { retires: "2026-12-11", replacement: "gpt-5.6-sol", announced: "2026-06-11" },
166
+ "o3-mini": { retires: "2026-10-23", replacement: "gpt-5.6-sol", announced: "2026-04-22" },
167
+ "o4-mini": { retires: "2026-10-23", replacement: "gpt-5.6-terra", announced: "2026-04-22" },
168
+ "claude-sonnet-4-5": { retires: "2026-11-30", replacement: "claude-sonnet-5-5", announced: "2026-09-30" }
169
+ };
170
+ function modelSunset(model, at = /* @__PURE__ */ new Date()) {
171
+ const key = normalizeModel(model);
172
+ if (!key) return null;
173
+ const s = MODEL_SUNSET[key];
174
+ if (!s) return null;
175
+ const days = Math.ceil((Date.parse(s.retires + "T00:00:00Z") - at.getTime()) / 864e5);
176
+ return { key, ...s, retired: days <= 0, days_left: Math.max(0, days) };
177
+ }
149
178
 
150
179
  // cli/src/tokens-lite.ts
151
180
  function countTokens(text, _provider = "openai") {
@@ -156,7 +185,7 @@ function countTokens(text, _provider = "openai") {
156
185
  }
157
186
 
158
187
  // src/audit-core.ts
159
- var MODEL_RE = /["'`](?:openai\/|anthropic\/|google\/|models\/)?((?:gpt-[\w.-]+|o[134](?:-mini|-pro)?(?:-[\w-]+)?|claude-[\w.-]+|gemini-[\w.-]+))["'`]/g;
188
+ var MODEL_RE = /["'`](?:(?:openai|anthropic|google|models|x-ai|xai|deepseek|bedrock|openrouter)\/|(?:us|eu|apac|global)\.anthropic\.|anthropic\.)?((?:gpt-[\w.-]+|o[134](?:-mini|-pro)?(?:-[\w-]+)?|claude-[\w.-]+|gemini-[\w.-]+|grok-[\w.-]+|deepseek-[\w.-]+)(?::\d+)?)["'`]/g;
160
189
  var CALL_RE = [
161
190
  [/\.chat\.completions\.(create|parse)\s*\(/, "openai-chat"],
162
191
  [/\.responses\.(create|parse|stream)\s*\(/, "openai-responses"],
@@ -367,6 +396,7 @@ function resolveScopes(loc, masked, from, depth = 2) {
367
396
  }
368
397
  return out2;
369
398
  }
399
+ var AUDIT_AT = new Date(process.env.TOKENZERO_AUDIT_AT || Date.now());
370
400
  function analyzeFiles(files, repo = null, manifests2 = []) {
371
401
  const findings = [];
372
402
  const callSites = [];
@@ -509,6 +539,19 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
509
539
  est_savings_pct: 8
510
540
  });
511
541
  }
542
+ const sunset = model && key ? modelSunset(key, AUDIT_AT) : null;
543
+ if (sunset && !unknownModels.has("sunset:" + key)) {
544
+ unknownModels.add("sunset:" + key);
545
+ push({
546
+ severity: sunset.retired ? "high" : sunset.days_left <= 90 ? "medium" : "low",
547
+ file: f.path,
548
+ line: i + 1,
549
+ title: sunset.retired ? `Retired model "${model}"` : `"${model}" retires on ${sunset.retires}`,
550
+ detail: sunset.retired ? `The provider shut this model down on ${sunset.retires}; requests fail or are remapped. Recommended replacement: ${sunset.replacement}.` : `The provider announced on ${sunset.announced} that this model retires on ${sunset.retires} (${sunset.days_left} days). Recommended replacement: ${sunset.replacement}.`,
551
+ fix: `Move to ${sunset.replacement} (eval first); TokenZero's cascade can route to it, and the gateway warns in X-TokenZero-Warning until you do.`,
552
+ est_savings_pct: 0
553
+ });
554
+ }
512
555
  if (model && !key && !unknownModels.has(model)) {
513
556
  unknownModels.add(model);
514
557
  push({ severity: "info", file: f.path, line: i + 1, title: `Unrecognised model id "${model}"`, detail: "Not in the current pricing table: it may be retired or renamed. Retired models return errors or are silently remapped by some providers.", fix: "Check the provider model list; TokenZero flags unpriced models in `X-TokenZero-Warning`.", est_savings_pct: 0 });
@@ -667,7 +710,7 @@ function canonical(obj) {
667
710
  }
668
711
 
669
712
  // cli/src/main.ts
670
- var VERSION = "0.1.6";
713
+ var VERSION = "0.1.7";
671
714
  var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
672
715
  function parseArgs(argv) {
673
716
  const a = { _: [], flags: {} };
@@ -841,6 +884,7 @@ async function cmdInit(a) {
841
884
  key = r.body.key;
842
885
  wsId = r.body.workspace.id;
843
886
  out(`${bold("Workspace")} ${wsId} ${dim("created")}`);
887
+ saveCreds({ workspace_id: wsId, api_key: key, url: base });
844
888
  } catch (e) {
845
889
  err(yellow(`! ${e.message}`));
846
890
  err(dim(" Continuing without a workspace key: requests will be grouped by an anonymous workspace derived from your provider key."));
@@ -874,83 +918,18 @@ ${bold("Your code")}`);
874
918
  if (sn) out(` ${s.padEnd(10)} ${sn}`);
875
919
  }
876
920
  }
877
- if (a.flags["github-action"]) await setupGithubAction(root, a);
878
921
  if (a.flags.mcp) await setupMcp(root, base, a);
879
922
  if (a.flags["agent-rules"]) await setupAgentRules(root, base, a);
880
- if (key && wsId) saveCreds({ workspace_id: wsId, api_key: key, url: base });
923
+ if (!a.flags["no-github-action"] && (a.flags["github-action"] || fs2.existsSync(path2.join(root, ".git")))) await setupGithubAction(root, a);
881
924
  out(`
882
925
  ${bold("Next")}`);
883
- if (wsId) out(` dashboard ${base}/dashboard?workspace=${wsId}${key ? "#key=" + redactKey(key, key) : ""}`);
926
+ if (wsId) out(` dashboard ${base}/dashboard?workspace=${wsId} ${dim("(one-click unlock link: " + selfCmd(base) + " dashboard)")}`);
884
927
  out(` usage ${selfCmd(base)} usage`);
885
928
  if (wsId) out(` badge turn on public stats in the dashboard, then: [![LLM spend verified by TokenZero](${base}/v1/badge/${wsId}.svg)](${base}/dashboard?workspace=${wsId})`);
886
- if (!a.flags["github-action"]) out(` PR checks ${selfCmd(base)} init --github-action`);
887
929
  if (!a.flags.mcp) out(` coding agents: ${selfCmd(base)} init --mcp`);
930
+ if (!a.flags["agent-rules"]) out(` agent rules: ${selfCmd(base)} init --agent-rules ${dim("(AGENTS.md block for Claude Code / Cursor / Codex)")}`);
888
931
  }
889
- var selfCmd = (_base) => "npx -y @tokenzero/cli";
890
- var redactKey = (s, key) => key ? s.replace(key, key.slice(0, 12) + "\u2026" + key.slice(-4)) : s;
891
- var GH_ACTION_YAML = `name: TokenZero LLM Cost Check
892
- on: [pull_request]
893
- permissions:
894
- contents: read
895
- pull-requests: write
896
- jobs:
897
- cost-audit:
898
- runs-on: ubuntu-latest
899
- steps:
900
- - uses: actions/checkout@v4
901
- with:
902
- fetch-depth: 0
903
- - uses: tokenzero-ai/tokenzero/action@v0
904
- with:
905
- github-token: \${{ secrets.GITHUB_TOKEN }}
906
- `;
907
- async function setupGithubAction(root, a) {
908
- out(`
909
- ${bold("GitHub Action")} ${dim("(PR token-waste & model-arbitrage check)")}`);
910
- const wfDir = path2.join(root, ".github", "workflows");
911
- const file = path2.join(wfDir, "tokenzero.yml");
912
- if (fs2.existsSync(file)) {
913
- out(dim(` ${path2.relative(root, file)} already exists`));
914
- return;
915
- }
916
- if (await confirm(` Add ${path2.relative(root, file)} to audit every PR?`, a)) {
917
- fs2.mkdirSync(wfDir, { recursive: true });
918
- fs2.writeFileSync(file, GH_ACTION_YAML);
919
- out(green(` \u2713 wrote ${path2.relative(root, file)}`));
920
- }
921
- }
922
- async function setupMcp(root, base, a) {
923
- out(`
924
- ${bold("MCP")} ${dim("(audit_repo_token_waste, optimize_prompt_payload, calculate_model_arbitrage)")}`);
925
- out(` Claude Code: claude mcp add --transport http tokenzero ${base}/mcp`);
926
- out(` Local stdio: npx -y @tokenzero/mcp`);
927
- for (const rel of [path2.join(".cursor", "mcp.json"), ".mcp.json"]) {
928
- const file = path2.join(root, rel);
929
- let cfg = {};
930
- if (fs2.existsSync(file)) {
931
- try {
932
- cfg = JSON.parse(fs2.readFileSync(file, "utf8"));
933
- } catch {
934
- out(yellow(` ! ${file} is not valid JSON; not touching it`));
935
- continue;
936
- }
937
- }
938
- if (cfg?.mcpServers?.tokenzero?.url === `${base}/mcp`) {
939
- out(dim(` ${rel}: already configured`));
940
- continue;
941
- }
942
- if (await confirm(` Add TokenZero to ${rel}?`, a)) {
943
- cfg.mcpServers = { ...cfg.mcpServers ?? {}, tokenzero: { url: `${base}/mcp` } };
944
- fs2.mkdirSync(path2.dirname(file), { recursive: true });
945
- fs2.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
946
- out(green(` \u2713 wrote ${rel}`));
947
- }
948
- }
949
- }
950
- var AGENT_RULES_MARKER = "<!-- tokenzero-agent-rules -->";
951
- function credsPath() {
952
- return path2.join(process.env.TOKENZERO_CONFIG_DIR || path2.join(os.homedir(), ".config", "tokenzero"), "credentials.json");
953
- }
932
+ var credsPath = () => path2.join(process.env.TOKENZERO_CONFIG_DIR || path2.join(os.homedir(), ".config", "tokenzero"), "credentials.json");
954
933
  function saveCreds(c2) {
955
934
  try {
956
935
  const p = credsPath();
@@ -966,28 +945,6 @@ function loadCreds() {
966
945
  return null;
967
946
  }
968
947
  }
969
- async function setupAgentRules(root, base, a) {
970
- out(`
971
- ${bold("Agent Rules")} ${dim("(AGENTS.md FinOps & MCP routing rules)")}`);
972
- const file = path2.join(root, "AGENTS.md");
973
- const existing = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8") : "";
974
- if (existing.includes(AGENT_RULES_MARKER)) {
975
- out(dim(" AGENTS.md: already includes TokenZero FinOps rules"));
976
- return;
977
- }
978
- const block = [
979
- AGENT_RULES_MARKER,
980
- "## TokenZero LLM FinOps & MCP Gateway",
981
- `- **1-Line BYOK Gateway:** Point \`OPENAI_BASE_URL=${base}/v1\` and \`ANTHROPIC_BASE_URL=${base}\` (with \`X-TokenZero-Key\` or \`/w/<key>/v1\`). Provider keys pass straight through and are never stored.`,
982
- `- **MCP Tools:** Run \`npx -y @tokenzero/mcp\` or connect HTTP MCP at \`${base}/mcp\` for \`audit_repo_token_waste\`, \`calculate_model_arbitrage\`, \`optimize_prompt_payload\`, and \`provision_workspace\`.`,
983
- `- **Local Audit:** Run \`npx -y @tokenzero/cli audit\` before opening PRs that add or modify LLM calls.`
984
- ].join("\n") + "\n";
985
- if (await confirm(" Add TokenZero FinOps & MCP rules to AGENTS.md?", a)) {
986
- const sep = existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "";
987
- fs2.writeFileSync(file, existing + sep + block);
988
- out(green(" \u2713 wrote AGENTS.md"));
989
- }
990
- }
991
948
  async function cmdSignup(a) {
992
949
  const base = baseUrl(a);
993
950
  if (!base) {
@@ -995,55 +952,149 @@ async function cmdSignup(a) {
995
952
  process.exitCode = 2;
996
953
  return;
997
954
  }
998
- const name = a.flags.name || path2.basename(process.cwd()) || "cli-signup";
955
+ const name = a.flags.name || path2.basename(process.cwd()) || "cli";
999
956
  const r = await api(base, "/v1/workspaces", { method: "POST", body: JSON.stringify({ name }) });
1000
- if (r.status !== 201 && r.status !== 200) {
957
+ if (r.status !== 201) {
1001
958
  err(red(`signup failed (${r.status}): ${JSON.stringify(r.body).slice(0, 200)}`));
1002
959
  process.exitCode = 1;
1003
960
  return;
1004
961
  }
1005
- const wsId = r.body.workspace?.id ?? r.body.workspace_id ?? r.body.id;
1006
- const key = r.body.key ?? r.body.api_key;
962
+ const wsId = r.body.workspace.id, key = r.body.key;
1007
963
  saveCreds({ workspace_id: wsId, api_key: key, url: base });
1008
964
  const payload = {
1009
965
  workspace_id: wsId,
1010
966
  api_key: key,
967
+ url: base,
1011
968
  dashboard_url: `${base}/dashboard?workspace=${wsId}`,
1012
969
  one_click_dashboard_url: `${base}/dashboard?workspace=${wsId}#key=${key}`,
1013
970
  openai_base_url: `${base}/w/${key}/v1`,
1014
971
  anthropic_base_url: `${base}/w/${key}`,
972
+ gemini_native_base_url: `${base}/w/${key}`,
1015
973
  mcp_url: `${base}/mcp`,
1016
- badge_svg_url: `${base}/v1/badge/${wsId}.svg`
974
+ badge_svg_url: `${base}/v1/badge/${wsId}.svg`,
975
+ credentials_file: credsPath()
1017
976
  };
1018
977
  if (a.flags.json) {
1019
978
  out(JSON.stringify(payload, null, 2));
1020
979
  return;
1021
980
  }
1022
- out(`${bold("TokenZero Workspace Provisioned (0-Click / No Form)")}`);
1023
- out(` workspace_id ${cyan(wsId)}`);
1024
- out(` api_key ${green(key)}`);
1025
- out(` 1-click dash ${cyan(payload.one_click_dashboard_url)}`);
1026
- out(` OPENAI_BASE_URL ${payload.openai_base_url}`);
1027
- out(` ANTHROPIC_BASE_URL ${payload.anthropic_base_url}`);
1028
- out(dim(` Saved credentials to ${credsPath()}`));
981
+ out(`${bold("Workspace")} ${wsId} ${dim("created \xB7 key shown once")}`);
982
+ out(` TOKENZERO_KEY ${key}`);
983
+ out(` OPENAI_BASE_URL ${payload.openai_base_url}`);
984
+ out(` ANTHROPIC_BASE_URL ${payload.anthropic_base_url}`);
985
+ out(` dashboard ${payload.one_click_dashboard_url} ${dim("(the #key= part never leaves your browser)")}`);
986
+ out(dim(` saved to ${credsPath()} (0600); \`${selfCmd(base)} init\` writes the .env lines for a project`));
1029
987
  }
1030
988
  async function cmdDashboard(a) {
1031
989
  const saved = loadCreds();
1032
- const base = baseUrl(a) || saved?.url || "https://tokenzero.sh";
990
+ const base = baseUrl(a) || saved?.url || "";
1033
991
  const key = a.flags.key || process.env.TOKENZERO_KEY || saved?.api_key || "";
1034
- const wsId = saved?.workspace_id || "";
1035
- if (!key && !wsId) {
1036
- err(red("No saved workspace credentials. Run `npx -y @tokenzero/cli signup` or `tokenzero init` first."));
992
+ if (!base || !key) {
993
+ err(red("No workspace key: run `" + selfCmd(base) + " signup` or `init` first, or pass --url and --key."));
1037
994
  process.exitCode = 2;
1038
995
  return;
1039
996
  }
1040
- const url = `${base}/dashboard${wsId ? "?workspace=" + wsId : ""}${key ? "#key=" + key : ""}`;
997
+ let wsId = saved?.api_key === key ? saved?.workspace_id ?? "" : "";
998
+ if (!wsId) {
999
+ const r = await api(base, "/v1/workspaces/me", { headers: { "x-tokenzero-key": key } });
1000
+ if (r.status === 200) wsId = r.body.id;
1001
+ }
1002
+ if (!wsId) {
1003
+ err(red("That key does not unlock a workspace at " + base));
1004
+ process.exitCode = 1;
1005
+ return;
1006
+ }
1007
+ const url = `${base}/dashboard?workspace=${wsId}#key=${key}`;
1041
1008
  if (a.flags.json) {
1042
- out(JSON.stringify({ workspace_id: wsId || null, dashboard_url: url }, null, 2));
1009
+ out(JSON.stringify({ workspace_id: wsId, dashboard_url: url }, null, 2));
1043
1010
  return;
1044
1011
  }
1045
1012
  out(url);
1046
1013
  }
1014
+ var AGENT_RULES_MARKER = "<!-- tokenzero-agent-rules -->";
1015
+ async function setupAgentRules(root, base, a) {
1016
+ out(`
1017
+ ${bold("Agent rules")} ${dim("(AGENTS.md: read by Claude Code, Cursor, Codex, Windsurf)")}`);
1018
+ const file = path2.join(root, "AGENTS.md");
1019
+ const existing = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8") : "";
1020
+ if (existing.includes(AGENT_RULES_MARKER)) {
1021
+ out(dim(" AGENTS.md already has the TokenZero section"));
1022
+ return;
1023
+ }
1024
+ const block = [
1025
+ AGENT_RULES_MARKER,
1026
+ "## LLM cost (TokenZero)",
1027
+ `- LLM calls go through the TokenZero gateway: \`OPENAI_BASE_URL=${base}/w/<key>/v1\`, \`ANTHROPIC_BASE_URL=${base}/w/<key>\` (or send \`X-TokenZero-Key\`). Provider keys pass through untouched; never commit them.`,
1028
+ `- Before opening a PR that adds or changes an LLM call, run \`npx -y @tokenzero/cli audit\` (local, nothing uploaded) and fix medium/high findings: cache the static prefix, use the cheaper sibling for extraction/classification, batch calls in loops.`,
1029
+ `- Model choices: prefer ids the audit prices; retiring ids are flagged with their replacement. Price alternatives with the MCP tool \`calculate_model_arbitrage\` (\`npx -y @tokenzero/mcp\` or ${base}/mcp).`,
1030
+ `- Savings are verified: every routed request has a signed receipt (\`npx -y @tokenzero/cli verify <request_id>\`).`
1031
+ ].join("\n") + "\n";
1032
+ if (!await confirm(" Add the TokenZero section to AGENTS.md?", a)) return;
1033
+ fs2.writeFileSync(file, existing + (existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "") + block);
1034
+ out(green(` \u2713 wrote ${path2.relative(root, file)}`));
1035
+ }
1036
+ var selfCmd = (_base) => "npx -y @tokenzero/cli";
1037
+ var WORKFLOW = `name: TokenZero cost check
1038
+ on: pull_request
1039
+ permissions:
1040
+ contents: read
1041
+ pull-requests: write
1042
+ jobs:
1043
+ cost-check:
1044
+ runs-on: ubuntu-latest
1045
+ steps:
1046
+ - uses: actions/checkout@v4
1047
+ with:
1048
+ fetch-depth: 0
1049
+ - uses: tokenzero-ai/tokenzero/action@v0
1050
+ # with:
1051
+ # fail-over: 30 # fail the check above 30% estimated recoverable waste
1052
+ # comment-on-clean: true # also comment when the changes have no findings
1053
+ `;
1054
+ async function setupGithubAction(root, a) {
1055
+ const file = path2.join(root, ".github", "workflows", "tokenzero.yml");
1056
+ const rel = path2.relative(root, file);
1057
+ out(`
1058
+ ${bold("GitHub Action")} ${dim("(audits each PR on your runner; one comment kept up to date; no account, nothing uploaded)")}`);
1059
+ if (fs2.existsSync(file)) {
1060
+ out(dim(` ${rel} already exists; left unchanged`));
1061
+ return;
1062
+ }
1063
+ if (!await confirm(` Write ${rel}?`, a)) {
1064
+ out(dim(" skipped (--github-action to add later, --no-github-action to stop asking)"));
1065
+ return;
1066
+ }
1067
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
1068
+ fs2.writeFileSync(file, WORKFLOW);
1069
+ out(green(` \u2713 wrote ${rel}`));
1070
+ }
1071
+ var redactKey = (s, key) => key ? s.replace(key, key.slice(0, 12) + "\u2026" + key.slice(-4)) : s;
1072
+ async function setupMcp(root, base, a) {
1073
+ out(`
1074
+ ${bold("MCP")} ${dim("(audit_repo_token_waste, optimize_prompt_payload, calculate_model_arbitrage)")}`);
1075
+ out(` Claude Code (hosted): claude mcp add --transport http tokenzero ${base}/mcp`);
1076
+ out(` Claude Code (local): claude mcp add tokenzero -- npx -y @tokenzero/mcp`);
1077
+ for (const [label, file] of [["Claude Code", path2.join(root, ".mcp.json")], ["Cursor", path2.join(root, ".cursor", "mcp.json")]]) {
1078
+ let cfg = {};
1079
+ if (fs2.existsSync(file)) {
1080
+ try {
1081
+ cfg = JSON.parse(fs2.readFileSync(file, "utf8"));
1082
+ } catch {
1083
+ out(yellow(` ! ${path2.relative(root, file)} is not valid JSON; not touching it`));
1084
+ continue;
1085
+ }
1086
+ }
1087
+ if (cfg?.mcpServers?.tokenzero?.url === `${base}/mcp`) {
1088
+ out(dim(` ${label}: already configured`));
1089
+ continue;
1090
+ }
1091
+ if (!await confirm(` Add TokenZero to ${path2.relative(root, file)} for ${label}?`, a)) continue;
1092
+ cfg.mcpServers = { ...cfg.mcpServers ?? {}, tokenzero: label === "Claude Code" ? { type: "http", url: `${base}/mcp` } : { url: `${base}/mcp` } };
1093
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
1094
+ fs2.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
1095
+ out(green(` \u2713 wrote ${path2.relative(root, file)}`));
1096
+ }
1097
+ }
1047
1098
  async function cmdAudit(a) {
1048
1099
  const root = path2.resolve(a._[1] ?? ".");
1049
1100
  let report;
@@ -1068,8 +1119,9 @@ ${bold(f.title)}
1068
1119
  }
1069
1120
  }
1070
1121
  async function cmdUsage(a) {
1071
- const base = baseUrl(a);
1072
- const key = a.flags.key || process.env.TOKENZERO_KEY;
1122
+ const saved = loadCreds();
1123
+ const base = baseUrl(a) || saved?.url || "";
1124
+ const key = a.flags.key || process.env.TOKENZERO_KEY || saved?.api_key;
1073
1125
  if (!base || !key) {
1074
1126
  err(red("Need TOKENZERO_URL and TOKENZERO_KEY (run tokenzero init), or --url and --key."));
1075
1127
  process.exitCode = 2;
@@ -1159,12 +1211,11 @@ async function cmdVerify(a) {
1159
1211
  function help() {
1160
1212
  out(`${bold("tokenzero")} v${VERSION} \u2014 cut your LLM bill with a BYOK gateway
1161
1213
 
1162
- ${bold("tokenzero signup")} [--name NAME] [--json]
1163
- Instant 1-command workspace & API key provisioning (no form, saves to ~/.config/tokenzero).
1164
- ${bold("tokenzero dashboard")} [--json]
1165
- Print your 1-click auto-unlock dashboard URL (#key= fragment, never sent in HTTP headers).
1166
- ${bold("tokenzero init")} [dir] [--url URL] [--name NAME] [--yes] [--no-env] [--mcp] [--github-action] [--agent-rules]
1214
+ ${bold("tokenzero signup")} [--name NAME] [--json] One command, no form: workspace + key, saved to ~/.config/tokenzero (0600).
1215
+ ${bold("tokenzero dashboard")} [--json] One-click dashboard link for the saved key (key travels in the URL fragment only).
1216
+ ${bold("tokenzero init")} [dir] [--url URL] [--name NAME] [--yes] [--no-env] [--mcp] [--agent-rules] [--github-action | --no-github-action]
1167
1217
  Audit locally, create a workspace, point OPENAI_BASE_URL / ANTHROPIC_BASE_URL at the gateway.
1218
+ Git repositories are offered the PR cost-check workflow; --mcp writes .mcp.json and .cursor/mcp.json; --agent-rules adds an AGENTS.md section.
1168
1219
  ${bold("tokenzero audit")} [dir] [--json | --format markdown] [--diff file|-] [--fail-over PCT]
1169
1220
  Static token-waste audit. Runs locally; nothing is uploaded. --fail-over makes it a CI gate.
1170
1221
  ${bold("tokenzero usage")} [--json] Routed spend vs the free tier, savings, fee preview.
@@ -1182,12 +1233,12 @@ async function main() {
1182
1233
  const cmd = a._[0];
1183
1234
  if (a.flags.version || cmd === "version") return out(VERSION);
1184
1235
  switch (cmd) {
1236
+ case "init":
1237
+ return cmdInit(a);
1185
1238
  case "signup":
1186
1239
  return cmdSignup(a);
1187
1240
  case "dashboard":
1188
1241
  return cmdDashboard(a);
1189
- case "init":
1190
- return cmdInit(a);
1191
1242
  case "audit":
1192
1243
  return cmdAudit(a);
1193
1244
  case "usage":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenzero/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "TokenZero CLI: connect a project to the TokenZero BYOK gateway, audit LLM token waste locally, check usage and verify signed savings receipts.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",