@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.
- package/bin/tokenzero.mjs +218 -59
- 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.
|
|
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
|
|
74
|
-
"grok-4": P("xai", "frontier",
|
|
75
|
-
"grok-4
|
|
76
|
-
"grok-4
|
|
77
|
-
"grok-3": P("xai", "mid",
|
|
78
|
-
"grok-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"deepseek-
|
|
82
|
-
"deepseek-
|
|
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|
|
|
125
|
-
m = m.replace(/^(
|
|
126
|
-
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];
|
|
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
|
|
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.
|
|
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: [](${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
|
|
888
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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(` ${
|
|
1060
|
+
out(dim(` ${rel} already exists; left unchanged`));
|
|
911
1061
|
return;
|
|
912
1062
|
}
|
|
913
|
-
if (await confirm(`
|
|
914
|
-
|
|
915
|
-
|
|
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(`
|
|
924
|
-
for (const
|
|
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(` ${
|
|
1088
|
+
out(dim(` ${label}: already configured`));
|
|
937
1089
|
continue;
|
|
938
1090
|
}
|
|
939
|
-
if (await confirm(` Add TokenZero to ${
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
|
972
|
-
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;
|
|
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
|
|
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.
|
|
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",
|