@tokenzero/cli 0.1.5 → 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 +218 -59
  2. package/package.json +1 -1
package/bin/tokenzero.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  // cli/src/main.ts
5
5
  import fs2 from "node:fs";
6
+ import os from "node:os";
6
7
  import path2 from "node:path";
7
8
  import crypto2 from "node:crypto";
8
9
  import readline from "node:readline/promises";
@@ -14,14 +15,14 @@ import crypto from "node:crypto";
14
15
  var P = (provider, tier, input, output, cachedInput, cacheWrite, extra = {}) => ({ provider, tier, input, output, cachedInput, cacheWrite, ...extra });
15
16
  var PRICES = {
16
17
  // ---- OpenAI (standard tier, ≤272K; cache write = 30m TTL rate) ----
17
- "gpt-6-astra": P("openai", "frontier", 10, 50, 1, 12.5, { longCtxAt: 272e3, longCtx: { input: 20, output: 75, cachedInput: 2, cacheWrite: 25 }, minCacheTokens: 1024 }),
18
- "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 }),
19
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 }),
20
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 }),
21
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 }),
22
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 }),
23
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 }),
24
- "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 }),
25
26
  "gpt-5.5-pro": P("openai", "frontier", 30, 180),
26
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 }),
27
28
  "gpt-5.4-pro": P("openai", "frontier", 30, 180),
@@ -39,10 +40,10 @@ var PRICES = {
39
40
  "o3-mini": P("openai", "fast", 1.1, 4.4, 0.55, void 0, { minCacheTokens: 1024 }),
40
41
  "o4-mini": P("openai", "fast", 1.1, 4.4, 0.275, void 0, { minCacheTokens: 1024 }),
41
42
  "o1": P("openai", "frontier", 15, 60, 7.5),
42
- "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 }),
43
44
  "gpt-4.1-mini": P("openai", "fast", 0.4, 1.6, 0.1, void 0, { minCacheTokens: 1024 }),
44
45
  "gpt-4.1-nano": P("openai", "fast", 0.1, 0.4, 0.025, void 0, { minCacheTokens: 1024 }),
45
- "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 }),
46
47
  "gpt-4o-mini": P("openai", "fast", 0.15, 0.6, 0.075, void 0, { minCacheTokens: 1024 }),
47
48
  // ---- Anthropic (cache write = 5-minute TTL, 1.25x; 1h TTL is 2x) ----
48
49
  "claude-fable-5-1": P("anthropic", "frontier", 10, 50, 0.25, 12.5, { minCacheTokens: 512 }),
@@ -53,7 +54,8 @@ var PRICES = {
53
54
  "claude-opus-4-7": P("anthropic", "frontier", 5, 25, 0.5, 6.25, { minCacheTokens: 2048 }),
54
55
  "claude-opus-4-6": P("anthropic", "frontier", 5, 25, 0.5, 6.25, { minCacheTokens: 4096 }),
55
56
  "claude-opus-4-5": P("anthropic", "frontier", 5, 25, 0.5, 6.25, { minCacheTokens: 4096 }),
56
- "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
57
59
  "claude-sonnet-5": P("anthropic", "mid", 2, 10, 0.2, 2.5, { minCacheTokens: 1024 }),
58
60
  "claude-sonnet-4-6": P("anthropic", "mid", 3, 15, 0.3, 3.75, { minCacheTokens: 1024 }),
59
61
  "claude-sonnet-4-5": P("anthropic", "mid", 3, 15, 0.3, 3.75, { minCacheTokens: 1024 }),
@@ -70,18 +72,18 @@ var PRICES = {
70
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 } }),
71
73
  "gemini-2.5-flash": P("gemini", "fast", 0.3, 2.5, 0.03),
72
74
  "gemini-2.5-flash-lite": P("gemini", "fast", 0.1, 0.4, 0.01),
73
- // ---- xAI Grok ----
74
- "grok-4": P("xai", "frontier", 3, 15, 0.75),
75
- "grok-4-fast": P("xai", "fast", 0.2, 0.8, 0.05),
76
- "grok-4-mini": P("xai", "fast", 0.2, 0.8, 0.05),
77
- "grok-3": P("xai", "mid", 3, 15, 0.75),
78
- "grok-3-mini": P("xai", "fast", 0.3, 0.5, 0.075),
79
- // ---- DeepSeek ----
80
- "deepseek-v3": P("deepseek", "fast", 0.27, 1.1, 0.07),
81
- "deepseek-chat": P("deepseek", "fast", 0.27, 1.1, 0.07),
82
- "deepseek-r1": P("deepseek", "mid", 0.55, 2.19, 0.14),
83
- "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)
84
85
  };
86
+ var MODEL_ALIASES = { "deepseek-v4-flash": "deepseek-flash", "deepseek-v4-flash-vision-exp": "deepseek-flash" };
85
87
  var CASCADE_TARGET = {
86
88
  "gpt-6-astra": "gpt-6.1-sol",
87
89
  "gpt-6.1-sol": "gpt-6-luna",
@@ -110,20 +112,21 @@ var CASCADE_TARGET = {
110
112
  "gemini-4-argon": "gemini-3.8-flash",
111
113
  "gemini-3.1-pro": "gemini-3.8-flash",
112
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",
113
119
  "gemini-2.5-pro": "gemini-2.5-flash",
114
- "gemini-2.5-flash": "gemini-2.5-flash-lite",
115
- "grok-4": "grok-4-fast",
116
- "grok-3": "grok-3-mini",
117
- "deepseek-r1": "deepseek-v3",
118
- "deepseek-reasoner": "deepseek-chat"
120
+ "gemini-2.5-flash": "gemini-2.5-flash-lite"
119
121
  };
120
122
  var KEYS_BY_LENGTH = Object.keys(PRICES).sort((a, b) => b.length - a.length);
121
123
  function normalizeModel(model) {
122
124
  if (typeof model !== "string" || !model) return null;
123
125
  let m = model.toLowerCase().trim();
124
- m = m.replace(/^(openai|anthropic|google|gemini|x-ai|xai|deepseek|bedrock|models|vertex_ai)\//, "");
125
- m = m.replace(/^(?:us\.|eu\.|apac\.)?anthropic\./, "");
126
- 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];
127
130
  m = m.replace(/@\d{8}$/, "");
128
131
  m = m.replace(/-(\d{8}|\d{4}-\d{2}-\d{2})$/, "");
129
132
  m = m.replace(/-(latest|preview|customtools|thinking)$/, "");
@@ -145,6 +148,33 @@ function costOf(model, u, factor = 1) {
145
148
  const M = 1e6;
146
149
  return factor * (fresh * tier.input / M + cached * (tier.cachedInput ?? tier.input) / M + written * (tier.cacheWrite ?? tier.input) / M + u.output * tier.output / M);
147
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
+ }
148
178
 
149
179
  // cli/src/tokens-lite.ts
150
180
  function countTokens(text, _provider = "openai") {
@@ -155,7 +185,7 @@ function countTokens(text, _provider = "openai") {
155
185
  }
156
186
 
157
187
  // src/audit-core.ts
158
- 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;
159
189
  var CALL_RE = [
160
190
  [/\.chat\.completions\.(create|parse)\s*\(/, "openai-chat"],
161
191
  [/\.responses\.(create|parse|stream)\s*\(/, "openai-responses"],
@@ -366,6 +396,7 @@ function resolveScopes(loc, masked, from, depth = 2) {
366
396
  }
367
397
  return out2;
368
398
  }
399
+ var AUDIT_AT = new Date(process.env.TOKENZERO_AUDIT_AT || Date.now());
369
400
  function analyzeFiles(files, repo = null, manifests2 = []) {
370
401
  const findings = [];
371
402
  const callSites = [];
@@ -508,6 +539,19 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
508
539
  est_savings_pct: 8
509
540
  });
510
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
+ }
511
555
  if (model && !key && !unknownModels.has(model)) {
512
556
  unknownModels.add(model);
513
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 });
@@ -666,7 +710,7 @@ function canonical(obj) {
666
710
  }
667
711
 
668
712
  // cli/src/main.ts
669
- var VERSION = "0.1.5";
713
+ var VERSION = "0.1.7";
670
714
  var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
671
715
  function parseArgs(argv) {
672
716
  const a = { _: [], flags: {} };
@@ -840,6 +884,7 @@ async function cmdInit(a) {
840
884
  key = r.body.key;
841
885
  wsId = r.body.workspace.id;
842
886
  out(`${bold("Workspace")} ${wsId} ${dim("created")}`);
887
+ saveCreds({ workspace_id: wsId, api_key: key, url: base });
843
888
  } catch (e) {
844
889
  err(yellow(`! ${e.message}`));
845
890
  err(dim(" Continuing without a workspace key: requests will be grouped by an anonymous workspace derived from your provider key."));
@@ -873,75 +918,181 @@ ${bold("Your code")}`);
873
918
  if (sn) out(` ${s.padEnd(10)} ${sn}`);
874
919
  }
875
920
  }
876
- if (a.flags["github-action"]) await setupGithubAction(root, a);
877
921
  if (a.flags.mcp) await setupMcp(root, base, a);
922
+ if (a.flags["agent-rules"]) await setupAgentRules(root, base, a);
923
+ if (!a.flags["no-github-action"] && (a.flags["github-action"] || fs2.existsSync(path2.join(root, ".git")))) await setupGithubAction(root, a);
878
924
  out(`
879
925
  ${bold("Next")}`);
880
- if (wsId) out(` dashboard ${base}/dashboard?workspace=${wsId}`);
926
+ if (wsId) out(` dashboard ${base}/dashboard?workspace=${wsId} ${dim("(one-click unlock link: " + selfCmd(base) + " dashboard)")}`);
881
927
  out(` usage ${selfCmd(base)} usage`);
882
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})`);
883
- if (!a.flags["github-action"]) out(` PR checks ${selfCmd(base)} init --github-action`);
884
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)")}`);
931
+ }
932
+ var credsPath = () => path2.join(process.env.TOKENZERO_CONFIG_DIR || path2.join(os.homedir(), ".config", "tokenzero"), "credentials.json");
933
+ function saveCreds(c2) {
934
+ try {
935
+ const p = credsPath();
936
+ fs2.mkdirSync(path2.dirname(p), { recursive: true, mode: 448 });
937
+ fs2.writeFileSync(p, JSON.stringify({ ...c2, updated_at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n", { mode: 384 });
938
+ } catch {
939
+ }
940
+ }
941
+ function loadCreds() {
942
+ try {
943
+ return JSON.parse(fs2.readFileSync(credsPath(), "utf8"));
944
+ } catch {
945
+ return null;
946
+ }
947
+ }
948
+ async function cmdSignup(a) {
949
+ const base = baseUrl(a);
950
+ if (!base) {
951
+ err(red("No gateway URL. Pass --url https://\u2026 or set TOKENZERO_URL."));
952
+ process.exitCode = 2;
953
+ return;
954
+ }
955
+ const name = a.flags.name || path2.basename(process.cwd()) || "cli";
956
+ const r = await api(base, "/v1/workspaces", { method: "POST", body: JSON.stringify({ name }) });
957
+ if (r.status !== 201) {
958
+ err(red(`signup failed (${r.status}): ${JSON.stringify(r.body).slice(0, 200)}`));
959
+ process.exitCode = 1;
960
+ return;
961
+ }
962
+ const wsId = r.body.workspace.id, key = r.body.key;
963
+ saveCreds({ workspace_id: wsId, api_key: key, url: base });
964
+ const payload = {
965
+ workspace_id: wsId,
966
+ api_key: key,
967
+ url: base,
968
+ dashboard_url: `${base}/dashboard?workspace=${wsId}`,
969
+ one_click_dashboard_url: `${base}/dashboard?workspace=${wsId}#key=${key}`,
970
+ openai_base_url: `${base}/w/${key}/v1`,
971
+ anthropic_base_url: `${base}/w/${key}`,
972
+ gemini_native_base_url: `${base}/w/${key}`,
973
+ mcp_url: `${base}/mcp`,
974
+ badge_svg_url: `${base}/v1/badge/${wsId}.svg`,
975
+ credentials_file: credsPath()
976
+ };
977
+ if (a.flags.json) {
978
+ out(JSON.stringify(payload, null, 2));
979
+ return;
980
+ }
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`));
987
+ }
988
+ async function cmdDashboard(a) {
989
+ const saved = loadCreds();
990
+ const base = baseUrl(a) || saved?.url || "";
991
+ const key = a.flags.key || process.env.TOKENZERO_KEY || saved?.api_key || "";
992
+ if (!base || !key) {
993
+ err(red("No workspace key: run `" + selfCmd(base) + " signup` or `init` first, or pass --url and --key."));
994
+ process.exitCode = 2;
995
+ return;
996
+ }
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}`;
1008
+ if (a.flags.json) {
1009
+ out(JSON.stringify({ workspace_id: wsId, dashboard_url: url }, null, 2));
1010
+ return;
1011
+ }
1012
+ out(url);
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)}`));
885
1035
  }
886
1036
  var selfCmd = (_base) => "npx -y @tokenzero/cli";
887
- var redactKey = (s, key) => key ? s.replace(key, key.slice(0, 12) + "\u2026" + key.slice(-4)) : s;
888
- var GH_ACTION_YAML = `name: TokenZero LLM Cost Check
889
- on: [pull_request]
1037
+ var WORKFLOW = `name: TokenZero cost check
1038
+ on: pull_request
890
1039
  permissions:
891
1040
  contents: read
892
1041
  pull-requests: write
893
1042
  jobs:
894
- cost-audit:
1043
+ cost-check:
895
1044
  runs-on: ubuntu-latest
896
1045
  steps:
897
1046
  - uses: actions/checkout@v4
898
1047
  with:
899
1048
  fetch-depth: 0
900
1049
  - uses: tokenzero-ai/tokenzero/action@v0
901
- with:
902
- github-token: \${{ secrets.GITHUB_TOKEN }}
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
903
1053
  `;
904
1054
  async function setupGithubAction(root, a) {
1055
+ const file = path2.join(root, ".github", "workflows", "tokenzero.yml");
1056
+ const rel = path2.relative(root, file);
905
1057
  out(`
906
- ${bold("GitHub Action")} ${dim("(PR token-waste & model-arbitrage check)")}`);
907
- const wfDir = path2.join(root, ".github", "workflows");
908
- const file = path2.join(wfDir, "tokenzero.yml");
1058
+ ${bold("GitHub Action")} ${dim("(audits each PR on your runner; one comment kept up to date; no account, nothing uploaded)")}`);
909
1059
  if (fs2.existsSync(file)) {
910
- out(dim(` ${path2.relative(root, file)} already exists`));
1060
+ out(dim(` ${rel} already exists; left unchanged`));
911
1061
  return;
912
1062
  }
913
- if (await confirm(` Add ${path2.relative(root, file)} to audit every PR?`, a)) {
914
- fs2.mkdirSync(wfDir, { recursive: true });
915
- fs2.writeFileSync(file, GH_ACTION_YAML);
916
- out(green(` \u2713 wrote ${path2.relative(root, file)}`));
1063
+ if (!await confirm(` Write ${rel}?`, a)) {
1064
+ out(dim(" skipped (--github-action to add later, --no-github-action to stop asking)"));
1065
+ return;
917
1066
  }
1067
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
1068
+ fs2.writeFileSync(file, WORKFLOW);
1069
+ out(green(` \u2713 wrote ${rel}`));
918
1070
  }
1071
+ var redactKey = (s, key) => key ? s.replace(key, key.slice(0, 12) + "\u2026" + key.slice(-4)) : s;
919
1072
  async function setupMcp(root, base, a) {
920
1073
  out(`
921
1074
  ${bold("MCP")} ${dim("(audit_repo_token_waste, optimize_prompt_payload, calculate_model_arbitrage)")}`);
922
- out(` Claude Code: claude mcp add --transport http tokenzero ${base}/mcp`);
923
- out(` Local stdio: npx -y @tokenzero/mcp`);
924
- for (const rel of [path2.join(".cursor", "mcp.json"), ".mcp.json"]) {
925
- const file = path2.join(root, rel);
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")]]) {
926
1078
  let cfg = {};
927
1079
  if (fs2.existsSync(file)) {
928
1080
  try {
929
1081
  cfg = JSON.parse(fs2.readFileSync(file, "utf8"));
930
1082
  } catch {
931
- out(yellow(` ! ${file} is not valid JSON; not touching it`));
1083
+ out(yellow(` ! ${path2.relative(root, file)} is not valid JSON; not touching it`));
932
1084
  continue;
933
1085
  }
934
1086
  }
935
1087
  if (cfg?.mcpServers?.tokenzero?.url === `${base}/mcp`) {
936
- out(dim(` ${rel}: already configured`));
1088
+ out(dim(` ${label}: already configured`));
937
1089
  continue;
938
1090
  }
939
- if (await confirm(` Add TokenZero to ${rel}?`, a)) {
940
- cfg.mcpServers = { ...cfg.mcpServers ?? {}, tokenzero: { url: `${base}/mcp` } };
941
- fs2.mkdirSync(path2.dirname(file), { recursive: true });
942
- fs2.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
943
- out(green(` \u2713 wrote ${rel}`));
944
- }
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)}`));
945
1096
  }
946
1097
  }
947
1098
  async function cmdAudit(a) {
@@ -968,8 +1119,9 @@ ${bold(f.title)}
968
1119
  }
969
1120
  }
970
1121
  async function cmdUsage(a) {
971
- const base = baseUrl(a);
972
- 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;
973
1125
  if (!base || !key) {
974
1126
  err(red("Need TOKENZERO_URL and TOKENZERO_KEY (run tokenzero init), or --url and --key."));
975
1127
  process.exitCode = 2;
@@ -1059,8 +1211,11 @@ async function cmdVerify(a) {
1059
1211
  function help() {
1060
1212
  out(`${bold("tokenzero")} v${VERSION} \u2014 cut your LLM bill with a BYOK gateway
1061
1213
 
1062
- ${bold("tokenzero init")} [dir] [--url URL] [--name NAME] [--yes] [--no-env] [--mcp] [--github-action]
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]
1063
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.
1064
1219
  ${bold("tokenzero audit")} [dir] [--json | --format markdown] [--diff file|-] [--fail-over PCT]
1065
1220
  Static token-waste audit. Runs locally; nothing is uploaded. --fail-over makes it a CI gate.
1066
1221
  ${bold("tokenzero usage")} [--json] Routed spend vs the free tier, savings, fee preview.
@@ -1080,6 +1235,10 @@ async function main() {
1080
1235
  switch (cmd) {
1081
1236
  case "init":
1082
1237
  return cmdInit(a);
1238
+ case "signup":
1239
+ return cmdSignup(a);
1240
+ case "dashboard":
1241
+ return cmdDashboard(a);
1083
1242
  case "audit":
1084
1243
  return cmdAudit(a);
1085
1244
  case "usage":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenzero/cli",
3
- "version": "0.1.5",
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",