@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.
Files changed (2) hide show
  1. package/bin/tokenzero.mjs +85 -3
  2. 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.1.7";
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.1.7",
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",