@tokenzero/cli 0.1.6 → 0.1.8
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/tokenzero.mjs +249 -147
- 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.
|
|
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
|
|
75
|
-
"grok-4": P("xai", "frontier",
|
|
76
|
-
"grok-4
|
|
77
|
-
"grok-4
|
|
78
|
-
"grok-3": P("xai", "mid",
|
|
79
|
-
"grok-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"deepseek-
|
|
83
|
-
"deepseek-
|
|
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|
|
|
126
|
-
m = m.replace(/^(
|
|
127
|
-
m = m.replace(/-v\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
|
|
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.
|
|
713
|
+
var VERSION = "0.1.8";
|
|
671
714
|
var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
|
|
672
715
|
function parseArgs(argv) {
|
|
673
716
|
const a = { _: [], flags: {} };
|
|
@@ -687,7 +730,7 @@ function parseArgs(argv) {
|
|
|
687
730
|
}
|
|
688
731
|
return a;
|
|
689
732
|
}
|
|
690
|
-
var VALUE_FLAGS = /* @__PURE__ */ new Set(["url", "name", "key", "format", "diff", "fail-over", "env-file"]);
|
|
733
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["url", "name", "key", "format", "diff", "fail-over", "env-file", "limit"]);
|
|
691
734
|
var tty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
692
735
|
var c = (code2) => (s) => tty ? `\x1B[${code2}m${s}\x1B[0m` : s;
|
|
693
736
|
var bold = c("1");
|
|
@@ -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 (
|
|
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}${
|
|
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: [](${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
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
1023
|
-
out(`
|
|
1024
|
-
out(`
|
|
1025
|
-
out(`
|
|
1026
|
-
out(`
|
|
1027
|
-
out(`
|
|
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 || "
|
|
990
|
+
const base = baseUrl(a) || saved?.url || "";
|
|
1033
991
|
const key = a.flags.key || process.env.TOKENZERO_KEY || saved?.api_key || "";
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1072
|
-
const
|
|
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;
|
|
@@ -1131,8 +1183,9 @@ async function cmdRotate(a) {
|
|
|
1131
1183
|
async function cmdVerify(a) {
|
|
1132
1184
|
const base = baseUrl(a);
|
|
1133
1185
|
const id = a._[1];
|
|
1186
|
+
if (base && a.flags.chain) return cmdVerifyChain(a, base);
|
|
1134
1187
|
if (!base || !id) {
|
|
1135
|
-
err(red("usage: tokenzero verify <request_id> [--url \u2026]"));
|
|
1188
|
+
err(red("usage: tokenzero verify <request_id> [--url \u2026] | tokenzero verify --chain [--limit N]"));
|
|
1136
1189
|
process.exitCode = 2;
|
|
1137
1190
|
return;
|
|
1138
1191
|
}
|
|
@@ -1156,19 +1209,68 @@ async function cmdVerify(a) {
|
|
|
1156
1209
|
out(` baseline $${rc.baseline_usd} \xB7 actual $${rc.actual_usd} \xB7 saved $${rc.savings_usd} \xB7 engines ${rc.engines.join(",") || "none"} \xB7 pricing ${rc.pricing_version}`);
|
|
1157
1210
|
if (!ok) process.exitCode = 1;
|
|
1158
1211
|
}
|
|
1212
|
+
async function cmdVerifyChain(a, base) {
|
|
1213
|
+
const saved = loadCreds();
|
|
1214
|
+
const key = a.flags.key || process.env.TOKENZERO_KEY || saved?.api_key || "";
|
|
1215
|
+
if (!key) {
|
|
1216
|
+
err(red("--chain needs the workspace key (TOKENZERO_KEY, --key, or saved credentials)"));
|
|
1217
|
+
process.exitCode = 2;
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const limit = Number(a.flags.limit) || 50;
|
|
1221
|
+
const [c2, k] = await Promise.all([api(base, `/v1/ledger/chain?limit=${limit}`, { headers: { "x-tokenzero-key": key } }), api(base, "/.well-known/tokenzero.json")]);
|
|
1222
|
+
if (c2.status !== 200) {
|
|
1223
|
+
err(red(`chain: ${c2.status} ${JSON.stringify(c2.body).slice(0, 200)}`));
|
|
1224
|
+
process.exitCode = 1;
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
const items = c2.body.items ?? [];
|
|
1228
|
+
const keys = /* @__PURE__ */ new Map();
|
|
1229
|
+
for (const jwk of k.body?.keys ?? []) keys.set(jwk.kid, crypto2.createPublicKey({ key: { kty: jwk.kty, crv: jwk.crv, x: jwk.x }, format: "jwk" }));
|
|
1230
|
+
const sha = (s) => crypto2.createHash("sha256").update(s).digest("hex");
|
|
1231
|
+
let ok = true, reason = "";
|
|
1232
|
+
for (let i = 0; i < items.length && ok; i++) {
|
|
1233
|
+
const { receipt, signature } = items[i];
|
|
1234
|
+
const pub = keys.get(receipt.key_id);
|
|
1235
|
+
if (!pub || !crypto2.verify(null, Buffer.from(canonical(receipt)), pub, Buffer.from(signature, "base64url"))) {
|
|
1236
|
+
ok = false;
|
|
1237
|
+
reason = `bad signature at seq ${receipt.seq}`;
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
const prev = items[i - 1];
|
|
1241
|
+
if (prev && receipt.seq !== prev.receipt.seq + 1) {
|
|
1242
|
+
ok = false;
|
|
1243
|
+
reason = `seq ${receipt.seq} does not follow ${prev.receipt.seq}`;
|
|
1244
|
+
} else if (prev && receipt.prev_receipt_hash !== sha(prev.signature)) {
|
|
1245
|
+
ok = false;
|
|
1246
|
+
reason = `prev_receipt_hash mismatch at seq ${receipt.seq}`;
|
|
1247
|
+
} else if (!prev && receipt.seq === 1 && receipt.prev_receipt_hash !== "0".repeat(64)) {
|
|
1248
|
+
ok = false;
|
|
1249
|
+
reason = "genesis receipt does not point at the zero hash";
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (a.flags.json) {
|
|
1253
|
+
out(JSON.stringify({ valid: ok, reason: reason || null, checked: items.length, head_seq: c2.body.head_seq, server_verification: c2.body.verification }, null, 2));
|
|
1254
|
+
if (!ok) process.exitCode = 1;
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
out(`${ok ? green("\u2713 chain intact") : red("\u2717 CHAIN BROKEN")}: ${items.length} receipt(s) checked locally${c2.body.head_seq != null ? `, head seq ${c2.body.head_seq}` : ""}${reason ? " \u2014 " + reason : ""}`);
|
|
1258
|
+
if (items.length) out(dim(` ${items[0].receipt.seq ?? "?"} \u2026 ${items.at(-1).receipt.seq ?? "?"} \xB7 savings in this run $${items.reduce((s, x) => s + (x.receipt.savings_usd ?? 0), 0).toFixed(4)} \xB7 key ${items[0].receipt.key_id}`));
|
|
1259
|
+
if (!ok) process.exitCode = 1;
|
|
1260
|
+
}
|
|
1159
1261
|
function help() {
|
|
1160
1262
|
out(`${bold("tokenzero")} v${VERSION} \u2014 cut your LLM bill with a BYOK gateway
|
|
1161
1263
|
|
|
1162
|
-
${bold("tokenzero signup")} [--name NAME] [--json]
|
|
1163
|
-
|
|
1164
|
-
${bold("tokenzero
|
|
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]
|
|
1264
|
+
${bold("tokenzero signup")} [--name NAME] [--json] One command, no form: workspace + key, saved to ~/.config/tokenzero (0600).
|
|
1265
|
+
${bold("tokenzero dashboard")} [--json] One-click dashboard link for the saved key (key travels in the URL fragment only).
|
|
1266
|
+
${bold("tokenzero init")} [dir] [--url URL] [--name NAME] [--yes] [--no-env] [--mcp] [--agent-rules] [--github-action | --no-github-action]
|
|
1167
1267
|
Audit locally, create a workspace, point OPENAI_BASE_URL / ANTHROPIC_BASE_URL at the gateway.
|
|
1268
|
+
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
1269
|
${bold("tokenzero audit")} [dir] [--json | --format markdown] [--diff file|-] [--fail-over PCT]
|
|
1169
1270
|
Static token-waste audit. Runs locally; nothing is uploaded. --fail-over makes it a CI gate.
|
|
1170
1271
|
${bold("tokenzero usage")} [--json] Routed spend vs the free tier, savings, fee preview.
|
|
1171
1272
|
${bold("tokenzero verify")} <request_id> Verify a savings receipt's signature offline.
|
|
1273
|
+
${bold("tokenzero verify --chain")} [--limit N] Verify the workspace's append-only receipt chain (signatures, seq, prev hashes).
|
|
1172
1274
|
${bold("tokenzero rotate-key")} [--yes] Issue a new workspace key; rewrites it in .env. Old key dies immediately.
|
|
1173
1275
|
|
|
1174
1276
|
Env: TOKENZERO_URL, TOKENZERO_KEY, NO_COLOR`);
|
|
@@ -1182,12 +1284,12 @@ async function main() {
|
|
|
1182
1284
|
const cmd = a._[0];
|
|
1183
1285
|
if (a.flags.version || cmd === "version") return out(VERSION);
|
|
1184
1286
|
switch (cmd) {
|
|
1287
|
+
case "init":
|
|
1288
|
+
return cmdInit(a);
|
|
1185
1289
|
case "signup":
|
|
1186
1290
|
return cmdSignup(a);
|
|
1187
1291
|
case "dashboard":
|
|
1188
1292
|
return cmdDashboard(a);
|
|
1189
|
-
case "init":
|
|
1190
|
-
return cmdInit(a);
|
|
1191
1293
|
case "audit":
|
|
1192
1294
|
return cmdAudit(a);
|
|
1193
1295
|
case "usage":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenzero/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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",
|