tokengraph 0.2.1 → 0.2.3

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/bundle/bin.js +114 -93
  2. package/package.json +2 -2
package/bundle/bin.js CHANGED
@@ -984,14 +984,20 @@ function escapeXml(value) {
984
984
  function n(value) {
985
985
  return String(Math.round(value * 100) / 100);
986
986
  }
987
+ function tokenParts(value) {
988
+ if (Math.round(value) < 1e3)
989
+ return { figure: String(Math.round(value)), unit: "" };
990
+ let scaled = value / 1e3;
991
+ let unit = 0;
992
+ while (unit < TOKEN_UNITS.length - 1 && Number(scaled.toFixed(1)) >= 1e3) {
993
+ scaled /= 1e3;
994
+ unit += 1;
995
+ }
996
+ return { figure: scaled.toFixed(1), unit: TOKEN_UNITS[unit] };
997
+ }
987
998
  function formatTokens(value) {
988
- if (value >= 1e9)
989
- return `${(value / 1e9).toFixed(1)}B`;
990
- if (value >= 1e6)
991
- return `${(value / 1e6).toFixed(1)}M`;
992
- if (value >= 1e3)
993
- return `${(value / 1e3).toFixed(1)}K`;
994
- return String(Math.round(value));
999
+ const { figure, unit } = tokenParts(value);
1000
+ return figure + unit;
995
1001
  }
996
1002
  function formatTokensExact(value) {
997
1003
  return String(Math.round(value)).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -1309,7 +1315,7 @@ function renderGraphSvg(days, options) {
1309
1315
  const label = `Token usage graph, ${formatTokens(grid.total)} weighted tokens over ${plural(grid.activeDays, "active day")}` + (grid.activeDays > 0 ? `, current streak ${plural(stats2.currentStreak, "day")}` : "");
1310
1316
  return graphFrame(layout2, height, theme, parts3.join(""), label, options.fontFaceCss);
1311
1317
  }
1312
- var FULL, COMPACT, WEEKS, DAYS, MONTHS, FONT, FALLBACK_ADVANCE, LEGEND_SWATCH, LEGEND_LOGO, LEGEND_ROW_GAP, LEGEND_ITEM_GAP, LEGEND_ROW, FOOTER_MARK_HEIGHT, FLAME_LEFT, FLAME_TOP, FLAME_HEIGHT, FLAME_CENTROID_Y, CHROME, ANIMATION_CSS, STRIP_HEIGHT, STRIP_COLUMNS, STRIP_INSET;
1318
+ var FULL, COMPACT, WEEKS, DAYS, MONTHS, FONT, FALLBACK_ADVANCE, TOKEN_UNITS, LEGEND_SWATCH, LEGEND_LOGO, LEGEND_ROW_GAP, LEGEND_ITEM_GAP, LEGEND_ROW, FOOTER_MARK_HEIGHT, FLAME_LEFT, FLAME_TOP, FLAME_HEIGHT, FLAME_CENTROID_Y, CHROME, ANIMATION_CSS, STRIP_HEIGHT, STRIP_COLUMNS, STRIP_INSET;
1313
1319
  var init_graph = __esm({
1314
1320
  "../core/dist/graph.js"() {
1315
1321
  "use strict";
@@ -1356,6 +1362,7 @@ var init_graph = __esm({
1356
1362
  MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1357
1363
  FONT = "'IBM Plex Sans', -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif";
1358
1364
  FALLBACK_ADVANCE = 0.56;
1365
+ TOKEN_UNITS = ["K", "M", "B", "T", "Q"];
1359
1366
  LEGEND_SWATCH = 11;
1360
1367
  LEGEND_LOGO = 14;
1361
1368
  LEGEND_ROW_GAP = 8;
@@ -1616,8 +1623,16 @@ var init_pricing = __esm({
1616
1623
  GEMINI_LONG_CONTEXT = { ...OPENAI_LONG_CONTEXT, thresholdTokens: 2e5 };
1617
1624
  XAI_LONG_CONTEXT = { input: 2, output: 2, thresholdTokens: 2e5 };
1618
1625
  RATES = {
1619
- // Anthropic, from platform.claude.com/docs/en/about-claude/pricing, checked 2026-10-12. Fast mode
1620
- // (Claude Code's usage.speed) is published only for Opus 5.5, Opus 5 and Opus 4.8, at 2x every rate.
1626
+ // Anthropic, from platform.claude.com/docs/en/about-claude/pricing, checked 2026-10-12, rechecked
1627
+ // 2026-11-09 (Fable 5.2 and Mythos 5.2 launched that day; no other row changed). Fast mode (Claude
1628
+ // Code's usage.speed) is published only for Fable 5.2, Mythos 5.2, Opus 5.5, Opus 5 and Opus 4.8,
1629
+ // at 2x every rate.
1630
+ "claude-fable-5-2": [
1631
+ { effectiveFrom: "2000-01-01", inputPerMillion: 8, outputPerMillion: 40, cacheReadMultiplier: 0.025, fastMultiplier: 2 }
1632
+ ],
1633
+ "claude-mythos-5-2": [
1634
+ { effectiveFrom: "2000-01-01", inputPerMillion: 8, outputPerMillion: 40, cacheReadMultiplier: 0.025, fastMultiplier: 2 }
1635
+ ],
1621
1636
  "claude-fable-5-1": [
1622
1637
  { effectiveFrom: "2000-01-01", inputPerMillion: 10, outputPerMillion: 50, cacheReadMultiplier: 0.025 }
1623
1638
  ],
@@ -2487,19 +2502,7 @@ function periodPhrase(range) {
2487
2502
  }
2488
2503
  }
2489
2504
  function heroNumber(value) {
2490
- const scales = [
2491
- [1e12, "T"],
2492
- [1e9, "B"],
2493
- [1e6, "M"],
2494
- [1e3, "K"]
2495
- ];
2496
- for (const [size, unit] of scales) {
2497
- if (value >= size) {
2498
- const scaled = value / size;
2499
- return { figure: scaled >= 100 ? String(Math.round(scaled)) : scaled.toFixed(1), unit };
2500
- }
2501
- }
2502
- return { figure: String(Math.round(value)), unit: "" };
2505
+ return tokenParts(value);
2503
2506
  }
2504
2507
  function compactDollars(value) {
2505
2508
  if (value < 1e3)
@@ -3865,6 +3868,69 @@ var init_fs_retry = __esm({
3865
3868
  }
3866
3869
  });
3867
3870
 
3871
+ // dist/adapters/model-ids.js
3872
+ function normalizeClaudeModel(raw) {
3873
+ const model = raw.trim().toLowerCase();
3874
+ if (!model.startsWith("claude-") || CANONICAL_CLAUDE.test(model) || LEGACY_CLAUDE.test(model)) {
3875
+ return model.startsWith("claude-") ? model : raw;
3876
+ }
3877
+ const parts3 = model.split("-");
3878
+ if (parts3.some((part) => /[^a-z0-9.]/.test(part)))
3879
+ return model;
3880
+ const family = parts3.find((part) => CLAUDE_FAMILIES.has(part));
3881
+ const version = parts3.filter((part) => /^\d{1,2}(\.\d{1,2})?$/.test(part)).flatMap((part) => part.split("."));
3882
+ if (!family || version.length === 0 || version.length > 2)
3883
+ return model;
3884
+ const major = Number(version[0]);
3885
+ return major < 4 ? `claude-${version.join("-")}-${family}` : `claude-${family}-${version.join("-")}`;
3886
+ }
3887
+ function migrateStoredModelId(model) {
3888
+ return model.startsWith("claude-") && model.split("-").includes("mythos") ? normalizeClaudeModel(model) : model;
3889
+ }
3890
+ function capModelName(base, suffix = "") {
3891
+ const appliedSuffix = suffix && !base.endsWith(suffix) ? suffix : "";
3892
+ const maxBaseLength = MAX_MODEL_NAME_LENGTH - appliedSuffix.length;
3893
+ return `${base.slice(0, maxBaseLength)}${appliedSuffix}`;
3894
+ }
3895
+ function looksLikePathOrArn(model) {
3896
+ return model.startsWith("/") || model.startsWith("~") || model.startsWith("arn:") || WINDOWS_DRIVE_PREFIX.test(model) || model.includes("/Users/") || model.includes("/home/");
3897
+ }
3898
+ function canonicalModelId(providerId, modelId) {
3899
+ let model = (modelId ?? "").trim();
3900
+ if (model.length === 0)
3901
+ return "unknown";
3902
+ if (looksLikePathOrArn(model))
3903
+ return "unknown";
3904
+ const provider = (providerId ?? "").trim().toLowerCase();
3905
+ if (BEDROCK_PROVIDERS.has(provider)) {
3906
+ model = model.replace(BEDROCK_REGION_PREFIX, "").replace(/^anthropic\./, "").replace(BEDROCK_VERSION_SUFFIX, "");
3907
+ } else if (VERTEX_PROVIDERS.has(provider)) {
3908
+ model = model.replace(VERTEX_VERSION_SUFFIX, "");
3909
+ }
3910
+ const slash = model.indexOf("/");
3911
+ if (slash > 0 && STRIPPED_VENDOR_PREFIXES.has(model.slice(0, slash).toLowerCase())) {
3912
+ model = model.slice(slash + 1);
3913
+ }
3914
+ return capModelName(normalizeClaudeModel(model));
3915
+ }
3916
+ var MAX_MODEL_NAME_LENGTH, CLAUDE_FAMILIES, CANONICAL_CLAUDE, LEGACY_CLAUDE, WINDOWS_DRIVE_PREFIX, STRIPPED_VENDOR_PREFIXES, BEDROCK_PROVIDERS, VERTEX_PROVIDERS, BEDROCK_REGION_PREFIX, BEDROCK_VERSION_SUFFIX, VERTEX_VERSION_SUFFIX;
3917
+ var init_model_ids = __esm({
3918
+ "dist/adapters/model-ids.js"() {
3919
+ "use strict";
3920
+ MAX_MODEL_NAME_LENGTH = 120;
3921
+ CLAUDE_FAMILIES = /* @__PURE__ */ new Set(["opus", "sonnet", "haiku", "fable", "mythos"]);
3922
+ CANONICAL_CLAUDE = /^claude-(opus|sonnet|haiku|fable|mythos)-\d+(-\d+)?(-\d{8})?$/;
3923
+ LEGACY_CLAUDE = /^claude-[1-3](-\d+)?-(opus|sonnet|haiku)(-\d{8})?$/;
3924
+ WINDOWS_DRIVE_PREFIX = /^[A-Za-z]:[\\/]/;
3925
+ STRIPPED_VENDOR_PREFIXES = /* @__PURE__ */ new Set(["anthropic", "openai", "google"]);
3926
+ BEDROCK_PROVIDERS = /* @__PURE__ */ new Set(["bedrock", "amazon-bedrock"]);
3927
+ VERTEX_PROVIDERS = /* @__PURE__ */ new Set(["vertex", "google-vertex", "google-vertex-anthropic"]);
3928
+ BEDROCK_REGION_PREFIX = /^(us|eu|apac|global|jp)\./;
3929
+ BEDROCK_VERSION_SUFFIX = /-v\d+(:\d+)?$/;
3930
+ VERTEX_VERSION_SUFFIX = /@[^@]+$/;
3931
+ }
3932
+ });
3933
+
3868
3934
  // dist/ledger.js
3869
3935
  import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
3870
3936
  function emptyLedger() {
@@ -4079,7 +4145,7 @@ function parseAccounts(raw) {
4079
4145
  const { coveredThrough, days } = value;
4080
4146
  if (typeof coveredThrough !== "string" || Number.isNaN(Date.parse(coveredThrough)))
4081
4147
  continue;
4082
- cursor[key] = { coveredThrough: clampInstant(coveredThrough), days: parseDays(days, readTiers) ?? {} };
4148
+ cursor[key] = { coveredThrough: clampInstant(coveredThrough), days: parseDays(days, readTiers, modelKeysFor(CURSOR_SOURCE)) ?? {} };
4083
4149
  }
4084
4150
  return Object.keys(cursor).length > 0 ? { cursor } : void 0;
4085
4151
  }
@@ -4094,7 +4160,7 @@ function parseProjects(raw) {
4094
4160
  continue;
4095
4161
  const folders = {};
4096
4162
  for (const [folder, rawDays] of Object.entries(rawFolders)) {
4097
- const days = isFolderKey(folder) ? parseDays(rawDays, readTiers) : null;
4163
+ const days = isFolderKey(folder) ? parseDays(rawDays, readTiers, modelKeysFor(source)) : null;
4098
4164
  if (days)
4099
4165
  folders[folder] = days;
4100
4166
  }
@@ -4114,25 +4180,38 @@ function parseProjects(raw) {
4114
4180
  function parseLedgerAccounts(raw) {
4115
4181
  return parseAccounts(raw);
4116
4182
  }
4117
- function parseModels(models, readTiers2) {
4183
+ function modelKeysFor(source) {
4184
+ return RAW_MODEL_ID_SOURCES.has(source) ? (model) => model : migrateStoredModelId;
4185
+ }
4186
+ function parseModels(models, readTiers2, modelKey) {
4118
4187
  if (typeof models !== "object" || models === null)
4119
4188
  return null;
4120
4189
  const parsed = {};
4121
- for (const [model, value] of Object.entries(models)) {
4190
+ for (const [stored, value] of Object.entries(models)) {
4122
4191
  const tiers = readTiers2(value);
4123
- if (tiers)
4192
+ if (!tiers)
4193
+ continue;
4194
+ const model = modelKey(stored);
4195
+ const existing = parsed[model];
4196
+ if (!existing) {
4124
4197
  parsed[model] = tiers;
4198
+ continue;
4199
+ }
4200
+ for (const [tier, counts2] of Object.entries(tiers)) {
4201
+ const current = existing[tier];
4202
+ existing[tier] = current ? addSplitCounts(current, counts2) : counts2;
4203
+ }
4125
4204
  }
4126
4205
  return Object.keys(parsed).length > 0 ? parsed : null;
4127
4206
  }
4128
- function parseDays(rawDays, readTiers2) {
4207
+ function parseDays(rawDays, readTiers2, modelKey) {
4129
4208
  if (typeof rawDays !== "object" || rawDays === null)
4130
4209
  return null;
4131
4210
  const days = {};
4132
4211
  for (const [date, models] of Object.entries(rawDays)) {
4133
4212
  if (!isDateKey(date))
4134
4213
  continue;
4135
- const parsed = parseModels(models, readTiers2);
4214
+ const parsed = parseModels(models, readTiers2, modelKey);
4136
4215
  if (parsed)
4137
4216
  days[date] = parsed;
4138
4217
  }
@@ -4163,7 +4242,7 @@ function parseLedger(raw) {
4163
4242
  const fields = raw;
4164
4243
  const version = fields["version"];
4165
4244
  if (version === 1 || version === 2) {
4166
- const days = parseDays(fields["days"], legacyTiers(version === 1 ? readLegacyCounts : readSplitCounts));
4245
+ const days = parseDays(fields["days"], legacyTiers(version === 1 ? readLegacyCounts : readSplitCounts), modelKeysFor(CLAUDE_CODE_SOURCE));
4167
4246
  return days ? { version: LEDGER_VERSION, sources: { [CLAUDE_CODE_SOURCE]: days } } : emptyLedger();
4168
4247
  }
4169
4248
  if (version !== LEDGER_VERSION)
@@ -4175,7 +4254,7 @@ function parseLedger(raw) {
4175
4254
  for (const [source, rawDays] of Object.entries(rawSources)) {
4176
4255
  if (!isKnownSource(source))
4177
4256
  continue;
4178
- const days = parseDays(rawDays, readTiers);
4257
+ const days = parseDays(rawDays, readTiers, modelKeysFor(source));
4179
4258
  if (days)
4180
4259
  sources[source] = days;
4181
4260
  }
@@ -4220,18 +4299,20 @@ async function writeLedger(path, ledger) {
4220
4299
  await writeFile2(temporary, JSON.stringify(ledger, null, 2), { encoding: "utf8", mode: 384 });
4221
4300
  await renameWithRetry(temporary, path);
4222
4301
  }
4223
- var LEDGER_VERSION, ACCOUNT_KEY, MAX_FOLDER_LENGTH, ABSOLUTE_FOLDER, BASE_FIELDS, writeCounter;
4302
+ var LEDGER_VERSION, ACCOUNT_KEY, MAX_FOLDER_LENGTH, ABSOLUTE_FOLDER, BASE_FIELDS, RAW_MODEL_ID_SOURCES, writeCounter;
4224
4303
  var init_ledger = __esm({
4225
4304
  "dist/ledger.js"() {
4226
4305
  "use strict";
4227
4306
  init_clamp_instant();
4228
4307
  init_fs_retry();
4308
+ init_model_ids();
4229
4309
  init_dist();
4230
4310
  LEDGER_VERSION = 3;
4231
4311
  ACCOUNT_KEY = /^[0-9a-f]{16}$/;
4232
4312
  MAX_FOLDER_LENGTH = 1024;
4233
4313
  ABSOLUTE_FOLDER = /^(?:[a-z]:)?\//i;
4234
4314
  BASE_FIELDS = ["inputTokens", "outputTokens", "cacheCreationTokens", "cacheReadTokens"];
4315
+ RAW_MODEL_ID_SOURCES = /* @__PURE__ */ new Set([CLAUDE_CODE_SOURCE, CODEX_CLI_SOURCE]);
4235
4316
  writeCounter = 0;
4236
4317
  }
4237
4318
  });
@@ -4307,66 +4388,6 @@ var init_request_buckets = __esm({
4307
4388
  }
4308
4389
  });
4309
4390
 
4310
- // dist/adapters/model-ids.js
4311
- function normalizeClaudeModel(raw) {
4312
- const model = raw.trim().toLowerCase();
4313
- if (!model.startsWith("claude-") || CANONICAL_CLAUDE.test(model) || LEGACY_CLAUDE.test(model)) {
4314
- return model.startsWith("claude-") ? model : raw;
4315
- }
4316
- const parts3 = model.split("-");
4317
- if (parts3.some((part) => /[^a-z0-9.]/.test(part)))
4318
- return model;
4319
- const family = parts3.find((part) => CLAUDE_FAMILIES.has(part));
4320
- const version = parts3.filter((part) => /^\d{1,2}(\.\d{1,2})?$/.test(part)).flatMap((part) => part.split("."));
4321
- if (!family || version.length === 0 || version.length > 2)
4322
- return model;
4323
- const major = Number(version[0]);
4324
- return major < 4 ? `claude-${version.join("-")}-${family}` : `claude-${family}-${version.join("-")}`;
4325
- }
4326
- function capModelName(base, suffix = "") {
4327
- const appliedSuffix = suffix && !base.endsWith(suffix) ? suffix : "";
4328
- const maxBaseLength = MAX_MODEL_NAME_LENGTH - appliedSuffix.length;
4329
- return `${base.slice(0, maxBaseLength)}${appliedSuffix}`;
4330
- }
4331
- function looksLikePathOrArn(model) {
4332
- return model.startsWith("/") || model.startsWith("~") || model.startsWith("arn:") || WINDOWS_DRIVE_PREFIX.test(model) || model.includes("/Users/") || model.includes("/home/");
4333
- }
4334
- function canonicalModelId(providerId, modelId) {
4335
- let model = (modelId ?? "").trim();
4336
- if (model.length === 0)
4337
- return "unknown";
4338
- if (looksLikePathOrArn(model))
4339
- return "unknown";
4340
- const provider = (providerId ?? "").trim().toLowerCase();
4341
- if (BEDROCK_PROVIDERS.has(provider)) {
4342
- model = model.replace(BEDROCK_REGION_PREFIX, "").replace(/^anthropic\./, "").replace(BEDROCK_VERSION_SUFFIX, "");
4343
- } else if (VERTEX_PROVIDERS.has(provider)) {
4344
- model = model.replace(VERTEX_VERSION_SUFFIX, "");
4345
- }
4346
- const slash = model.indexOf("/");
4347
- if (slash > 0 && STRIPPED_VENDOR_PREFIXES.has(model.slice(0, slash).toLowerCase())) {
4348
- model = model.slice(slash + 1);
4349
- }
4350
- return capModelName(normalizeClaudeModel(model));
4351
- }
4352
- var MAX_MODEL_NAME_LENGTH, CLAUDE_FAMILIES, CANONICAL_CLAUDE, LEGACY_CLAUDE, WINDOWS_DRIVE_PREFIX, STRIPPED_VENDOR_PREFIXES, BEDROCK_PROVIDERS, VERTEX_PROVIDERS, BEDROCK_REGION_PREFIX, BEDROCK_VERSION_SUFFIX, VERTEX_VERSION_SUFFIX;
4353
- var init_model_ids = __esm({
4354
- "dist/adapters/model-ids.js"() {
4355
- "use strict";
4356
- MAX_MODEL_NAME_LENGTH = 120;
4357
- CLAUDE_FAMILIES = /* @__PURE__ */ new Set(["opus", "sonnet", "haiku", "fable"]);
4358
- CANONICAL_CLAUDE = /^claude-(opus|sonnet|haiku|fable)-\d+(-\d+)?(-\d{8})?$/;
4359
- LEGACY_CLAUDE = /^claude-[1-3](-\d+)?-(opus|sonnet|haiku)(-\d{8})?$/;
4360
- WINDOWS_DRIVE_PREFIX = /^[A-Za-z]:[\\/]/;
4361
- STRIPPED_VENDOR_PREFIXES = /* @__PURE__ */ new Set(["anthropic", "openai", "google"]);
4362
- BEDROCK_PROVIDERS = /* @__PURE__ */ new Set(["bedrock", "amazon-bedrock"]);
4363
- VERTEX_PROVIDERS = /* @__PURE__ */ new Set(["vertex", "google-vertex", "google-vertex-anthropic"]);
4364
- BEDROCK_REGION_PREFIX = /^(us|eu|apac|global|jp)\./;
4365
- BEDROCK_VERSION_SUFFIX = /-v\d+(:\d+)?$/;
4366
- VERTEX_VERSION_SUFFIX = /@[^@]+$/;
4367
- }
4368
- });
4369
-
4370
4391
  // dist/adapters/cursor/aggregator.js
4371
4392
  function normalizeCursorModel(raw) {
4372
4393
  const lower = raw.trim().toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokengraph",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "A GitHub-style graph of the tokens your AI coding agents burn each day: Claude Code, Codex CLI, Cursor, Gemini CLI, Cline, OpenCode, Kilo CLI, Copilot CLI, pi, Qwen Code and Goose.",
5
5
  "keywords": [
6
6
  "ai",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@resvg/resvg-wasm": "^2.6.2",
49
- "@tokengraph/core": "0.2.1",
49
+ "@tokengraph/core": "0.2.3",
50
50
  "@types/react": "^19.3.0",
51
51
  "esbuild": "0.28.2",
52
52
  "qrcode-generator": "^2.0.4",