@tokenzero/cli 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tokenzero.mjs +85 -3
- package/package.json +1 -1
package/bin/tokenzero.mjs
CHANGED
|
@@ -710,7 +710,7 @@ function canonical(obj) {
|
|
|
710
710
|
}
|
|
711
711
|
|
|
712
712
|
// cli/src/main.ts
|
|
713
|
-
var VERSION = "0.
|
|
713
|
+
var VERSION = "0.2.0";
|
|
714
714
|
var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
|
|
715
715
|
function parseArgs(argv) {
|
|
716
716
|
const a = { _: [], flags: {} };
|
|
@@ -730,7 +730,7 @@ function parseArgs(argv) {
|
|
|
730
730
|
}
|
|
731
731
|
return a;
|
|
732
732
|
}
|
|
733
|
-
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"]);
|
|
734
734
|
var tty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
735
735
|
var c = (code2) => (s) => tty ? `\x1B[${code2}m${s}\x1B[0m` : s;
|
|
736
736
|
var bold = c("1");
|
|
@@ -1118,6 +1118,34 @@ ${bold(f.title)}
|
|
|
1118
1118
|
process.exitCode = 1;
|
|
1119
1119
|
}
|
|
1120
1120
|
}
|
|
1121
|
+
async function cmdDoctor(a) {
|
|
1122
|
+
const saved = loadCreds();
|
|
1123
|
+
const base = baseUrl(a) || saved?.url || "https://tokenzero.sh";
|
|
1124
|
+
const key = a.flags.key || process.env.TOKENZERO_KEY || saved?.api_key || "";
|
|
1125
|
+
const r = await api(base, "/v1/doctor", { headers: key ? { "x-tokenzero-key": key } : {} });
|
|
1126
|
+
if (r.status !== 200) {
|
|
1127
|
+
err(red(`doctor: ${r.status} ${JSON.stringify(r.body).slice(0, 200)}`));
|
|
1128
|
+
process.exitCode = 1;
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (a.flags.json) {
|
|
1132
|
+
out(JSON.stringify(r.body, null, 2));
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
const d = r.body;
|
|
1136
|
+
out(`${bold("tokenzero doctor")} \xB7 ${base} \xB7 gateway v${d.version} \xB7 ${d.authenticated ? "workspace key accepted" : "no workspace key (pass --key or set TOKENZERO_KEY for quota and settings checks)"}`);
|
|
1137
|
+
for (const c2 of d.checks) {
|
|
1138
|
+
const tag = c2.status === "pass" ? green("[PASS]") : c2.status === "warn" ? yellow("[WARN]") : dim("[INFO]");
|
|
1139
|
+
out(` ${tag} ${c2.label}: ${c2.detail}`);
|
|
1140
|
+
}
|
|
1141
|
+
out(dim(` OPENAI_BASE_URL=${d.snippets.openai_base_url}`));
|
|
1142
|
+
out(dim(` ANTHROPIC_BASE_URL=${d.snippets.anthropic_base_url}`));
|
|
1143
|
+
out(dim(` dashboard: ${d.snippets.dashboard_url}`));
|
|
1144
|
+
if (!d.ok) {
|
|
1145
|
+
out(yellow(` something needs attention; support: ${d.support.support_page} (${d.support.sla_target_response_minutes}-minute response target)`));
|
|
1146
|
+
process.exitCode = 1;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1121
1149
|
async function cmdUsage(a) {
|
|
1122
1150
|
const saved = loadCreds();
|
|
1123
1151
|
const base = baseUrl(a) || saved?.url || "";
|
|
@@ -1183,8 +1211,9 @@ async function cmdRotate(a) {
|
|
|
1183
1211
|
async function cmdVerify(a) {
|
|
1184
1212
|
const base = baseUrl(a);
|
|
1185
1213
|
const id = a._[1];
|
|
1214
|
+
if (base && a.flags.chain) return cmdVerifyChain(a, base);
|
|
1186
1215
|
if (!base || !id) {
|
|
1187
|
-
err(red("usage: tokenzero verify <request_id> [--url \u2026]"));
|
|
1216
|
+
err(red("usage: tokenzero verify <request_id> [--url \u2026] | tokenzero verify --chain [--limit N]"));
|
|
1188
1217
|
process.exitCode = 2;
|
|
1189
1218
|
return;
|
|
1190
1219
|
}
|
|
@@ -1208,6 +1237,55 @@ async function cmdVerify(a) {
|
|
|
1208
1237
|
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}`);
|
|
1209
1238
|
if (!ok) process.exitCode = 1;
|
|
1210
1239
|
}
|
|
1240
|
+
async function cmdVerifyChain(a, base) {
|
|
1241
|
+
const saved = loadCreds();
|
|
1242
|
+
const key = a.flags.key || process.env.TOKENZERO_KEY || saved?.api_key || "";
|
|
1243
|
+
if (!key) {
|
|
1244
|
+
err(red("--chain needs the workspace key (TOKENZERO_KEY, --key, or saved credentials)"));
|
|
1245
|
+
process.exitCode = 2;
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
const limit = Number(a.flags.limit) || 50;
|
|
1249
|
+
const [c2, k] = await Promise.all([api(base, `/v1/ledger/chain?limit=${limit}`, { headers: { "x-tokenzero-key": key } }), api(base, "/.well-known/tokenzero.json")]);
|
|
1250
|
+
if (c2.status !== 200) {
|
|
1251
|
+
err(red(`chain: ${c2.status} ${JSON.stringify(c2.body).slice(0, 200)}`));
|
|
1252
|
+
process.exitCode = 1;
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const items = c2.body.items ?? [];
|
|
1256
|
+
const keys = /* @__PURE__ */ new Map();
|
|
1257
|
+
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" }));
|
|
1258
|
+
const sha = (s) => crypto2.createHash("sha256").update(s).digest("hex");
|
|
1259
|
+
let ok = true, reason = "";
|
|
1260
|
+
for (let i = 0; i < items.length && ok; i++) {
|
|
1261
|
+
const { receipt, signature } = items[i];
|
|
1262
|
+
const pub = keys.get(receipt.key_id);
|
|
1263
|
+
if (!pub || !crypto2.verify(null, Buffer.from(canonical(receipt)), pub, Buffer.from(signature, "base64url"))) {
|
|
1264
|
+
ok = false;
|
|
1265
|
+
reason = `bad signature at seq ${receipt.seq}`;
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
const prev = items[i - 1];
|
|
1269
|
+
if (prev && receipt.seq !== prev.receipt.seq + 1) {
|
|
1270
|
+
ok = false;
|
|
1271
|
+
reason = `seq ${receipt.seq} does not follow ${prev.receipt.seq}`;
|
|
1272
|
+
} else if (prev && receipt.prev_receipt_hash !== sha(prev.signature)) {
|
|
1273
|
+
ok = false;
|
|
1274
|
+
reason = `prev_receipt_hash mismatch at seq ${receipt.seq}`;
|
|
1275
|
+
} else if (!prev && receipt.seq === 1 && receipt.prev_receipt_hash !== "0".repeat(64)) {
|
|
1276
|
+
ok = false;
|
|
1277
|
+
reason = "genesis receipt does not point at the zero hash";
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (a.flags.json) {
|
|
1281
|
+
out(JSON.stringify({ valid: ok, reason: reason || null, checked: items.length, head_seq: c2.body.head_seq, server_verification: c2.body.verification }, null, 2));
|
|
1282
|
+
if (!ok) process.exitCode = 1;
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
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 : ""}`);
|
|
1286
|
+
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}`));
|
|
1287
|
+
if (!ok) process.exitCode = 1;
|
|
1288
|
+
}
|
|
1211
1289
|
function help() {
|
|
1212
1290
|
out(`${bold("tokenzero")} v${VERSION} \u2014 cut your LLM bill with a BYOK gateway
|
|
1213
1291
|
|
|
@@ -1219,7 +1297,9 @@ function help() {
|
|
|
1219
1297
|
${bold("tokenzero audit")} [dir] [--json | --format markdown] [--diff file|-] [--fail-over PCT]
|
|
1220
1298
|
Static token-waste audit. Runs locally; nothing is uploaded. --fail-over makes it a CI gate.
|
|
1221
1299
|
${bold("tokenzero usage")} [--json] Routed spend vs the free tier, savings, fee preview.
|
|
1300
|
+
${bold("tokenzero doctor")} [--key K] [--json] Six-check connection and receipt doctor: gateway, signer, pricing, key, quota, settings.
|
|
1222
1301
|
${bold("tokenzero verify")} <request_id> Verify a savings receipt's signature offline.
|
|
1302
|
+
${bold("tokenzero verify --chain")} [--limit N] Verify the workspace's append-only receipt chain (signatures, seq, prev hashes).
|
|
1223
1303
|
${bold("tokenzero rotate-key")} [--yes] Issue a new workspace key; rewrites it in .env. Old key dies immediately.
|
|
1224
1304
|
|
|
1225
1305
|
Env: TOKENZERO_URL, TOKENZERO_KEY, NO_COLOR`);
|
|
@@ -1243,6 +1323,8 @@ async function main() {
|
|
|
1243
1323
|
return cmdAudit(a);
|
|
1244
1324
|
case "usage":
|
|
1245
1325
|
return cmdUsage(a);
|
|
1326
|
+
case "doctor":
|
|
1327
|
+
return cmdDoctor(a);
|
|
1246
1328
|
case "verify":
|
|
1247
1329
|
return cmdVerify(a);
|
|
1248
1330
|
case "rotate-key":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenzero/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|