llm-usage-metrics 0.3.7 → 0.4.1

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/dist/index.js CHANGED
@@ -1014,6 +1014,13 @@ function hasUsageSignal(usage) {
1014
1014
  function deriveDeltaUsage(info, previousTotalUsage) {
1015
1015
  const totalUsage = toUsage(info.total_token_usage);
1016
1016
  const lastUsage = toUsage(info.last_token_usage);
1017
+ if (totalUsage && previousTotalUsage) {
1018
+ const deltaFromTotals = subtractUsage(totalUsage, previousTotalUsage);
1019
+ if (hasUsageSignal(deltaFromTotals)) {
1020
+ return { deltaUsage: deltaFromTotals, latestTotalUsage: totalUsage };
1021
+ }
1022
+ return { latestTotalUsage: totalUsage };
1023
+ }
1017
1024
  if (lastUsage) {
1018
1025
  return { deltaUsage: lastUsage, latestTotalUsage: totalUsage };
1019
1026
  }
@@ -2452,6 +2459,214 @@ function createDefaultAdapters(options) {
2452
2459
  return sourceRegistrations.map((source) => source.create(options, sourceDirectoryOverrides));
2453
2460
  }
2454
2461
 
2462
+ // src/render/share-svg-theme.ts
2463
+ var shareTheme = {
2464
+ bg: "#0d1117",
2465
+ cardBg: "#161b22",
2466
+ cardBorder: "#30363d",
2467
+ textPrimary: "#e6edf3",
2468
+ textSecondary: "#8b949e",
2469
+ textMuted: "#484f58",
2470
+ gridLine: "#21262d",
2471
+ font: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
2472
+ mono: "ui-monospace, 'SF Mono', 'Fira Code', monospace"
2473
+ };
2474
+ var knownSourceColors = {
2475
+ pi: "#ec4899",
2476
+ codex: "#22c55e",
2477
+ gemini: "#eab308",
2478
+ droid: "#3b82f6",
2479
+ opencode: "#a855f7"
2480
+ };
2481
+ var fallbackColors = ["#f97316", "#06b6d4", "#ef4444", "#84cc16", "#f43f5e"];
2482
+ function getSourceColor(source, index) {
2483
+ return knownSourceColors[source] ?? fallbackColors[index % fallbackColors.length];
2484
+ }
2485
+ function escapeSvg(value) {
2486
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
2487
+ }
2488
+ function formatCompact(n) {
2489
+ if (n >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, "") + "B";
2490
+ if (n >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
2491
+ if (n >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
2492
+ return String(n);
2493
+ }
2494
+ var intFmt = new Intl.NumberFormat("en-US");
2495
+ var decFmt = new Intl.NumberFormat("en-US", {
2496
+ minimumFractionDigits: 2,
2497
+ maximumFractionDigits: 2
2498
+ });
2499
+ var usdFmt = new Intl.NumberFormat("en-US", {
2500
+ style: "currency",
2501
+ currency: "USD",
2502
+ minimumFractionDigits: 2,
2503
+ maximumFractionDigits: 2
2504
+ });
2505
+ function formatInteger(n) {
2506
+ return intFmt.format(n);
2507
+ }
2508
+ function formatDecimal(n) {
2509
+ return n === void 0 ? "-" : decFmt.format(n);
2510
+ }
2511
+ function formatUsd(n) {
2512
+ return n === void 0 ? "-" : usdFmt.format(n);
2513
+ }
2514
+ function catmullRom(points, tension = 0.3, yFloor) {
2515
+ if (points.length < 2) return "";
2516
+ const clamp = (y) => yFloor !== void 0 ? Math.min(y, yFloor) : y;
2517
+ let d = `M${points[0].x.toFixed(2)},${points[0].y.toFixed(2)}`;
2518
+ for (let i = 0; i < points.length - 1; i++) {
2519
+ const p0 = points[Math.max(0, i - 1)];
2520
+ const p1 = points[i];
2521
+ const p2 = points[i + 1];
2522
+ const p3 = points[Math.min(points.length - 1, i + 2)];
2523
+ const cp1x = p1.x + (p2.x - p0.x) * tension / 3;
2524
+ const cp1y = clamp(p1.y + (p2.y - p0.y) * tension / 3);
2525
+ const cp2x = p2.x - (p3.x - p1.x) * tension / 3;
2526
+ const cp2y = clamp(p2.y - (p3.y - p1.y) * tension / 3);
2527
+ d += ` C${cp1x.toFixed(2)},${cp1y.toFixed(2)} ${cp2x.toFixed(2)},${cp2y.toFixed(2)} ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`;
2528
+ }
2529
+ return d;
2530
+ }
2531
+ function scaleY(value, max, top, bottom) {
2532
+ if (max <= 0) return bottom;
2533
+ return bottom - Math.max(0, value) / max * (bottom - top);
2534
+ }
2535
+
2536
+ // src/render/render-efficiency-share-svg.ts
2537
+ var W = 1500;
2538
+ var H = 640;
2539
+ var ACCENT_H = 4;
2540
+ var FOOTER_H = 36;
2541
+ var pad = { top: 160, right: 130, bottom: 70 + FOOTER_H, left: 110 };
2542
+ var chartColors = {
2543
+ commits: "#8b949e",
2544
+ usdPerCommit: "#f97316"
2545
+ };
2546
+ function toMonthlyRows(rows) {
2547
+ return rows.filter((row) => row.rowType === "period").sort((a, b) => a.periodKey.localeCompare(b.periodKey));
2548
+ }
2549
+ function toAllRow(rows) {
2550
+ return rows.find((row) => row.periodKey === "ALL");
2551
+ }
2552
+ function renderSummaryStats(allRow, vcenter) {
2553
+ const cost = formatUsd(allRow?.costUsd);
2554
+ const commits = formatInteger(allRow?.commitCount ?? 0);
2555
+ const usdPerCommit = formatUsd(allRow?.usdPerCommit);
2556
+ const tokPerCommit = formatDecimal(allRow?.tokensPerCommit);
2557
+ const y = vcenter;
2558
+ const items = [
2559
+ { label: "Total Cost", value: cost, x: pad.left },
2560
+ { label: "Commits", value: commits, x: 280 },
2561
+ { label: "$/Commit", value: usdPerCommit, x: 480 },
2562
+ { label: "Tokens/Commit", value: tokPerCommit, x: 680 }
2563
+ ];
2564
+ return items.map(
2565
+ (item) => `<text x="${item.x}" y="${y}" font-size="14" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">${escapeSvg(item.label)}</text><text x="${item.x}" y="${y + 20}" font-size="18" font-weight="700" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}">${escapeSvg(item.value)}</text>`
2566
+ ).join("\n");
2567
+ }
2568
+ function renderLegend(x, y) {
2569
+ const items = [
2570
+ { label: "Commits", color: chartColors.commits, shape: "rect" },
2571
+ { label: "$ / Commit", color: chartColors.usdPerCommit, shape: "line" }
2572
+ ];
2573
+ return items.map((item, i) => {
2574
+ const ix = x + i * 180;
2575
+ const shape = item.shape === "rect" ? `<rect x="${ix}" y="${y - 6}" width="14" height="14" rx="3" fill="${item.color}" opacity="0.5"/>` : `<line x1="${ix}" y1="${y + 1}" x2="${ix + 14}" y2="${y + 1}" stroke="${item.color}" stroke-width="3" stroke-linecap="round"/>`;
2576
+ return `${shape}<text x="${ix + 20}" y="${y + 5}" font-size="13" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">${escapeSvg(item.label)}</text>`;
2577
+ }).join("\n");
2578
+ }
2579
+ function renderCommitBarLabels(monthlyRows, chartLeft, stepX, maxCommits, chartTop, chartBottom) {
2580
+ return monthlyRows.map((row, i) => {
2581
+ const x = chartLeft + i * stepX;
2582
+ const yTop = scaleY(row.commitCount, maxCommits, chartTop, chartBottom);
2583
+ return `<text x="${x.toFixed(2)}" y="${(yTop - 8).toFixed(0)}" text-anchor="middle" font-size="12" font-weight="600" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">${formatInteger(row.commitCount)}</text>`;
2584
+ }).join("\n");
2585
+ }
2586
+ function renderUsdDataLabels(monthlyRows, usdPoints) {
2587
+ return monthlyRows.map((row, i) => {
2588
+ const p = usdPoints[i];
2589
+ const val = row.usdPerCommit;
2590
+ if (val === void 0) return "";
2591
+ const labelY = p.y - 14;
2592
+ return `<text x="${p.x.toFixed(2)}" y="${labelY.toFixed(0)}" text-anchor="middle" font-size="12" font-weight="600" fill="${chartColors.usdPerCommit}" font-family="${shareTheme.font}">${escapeSvg(formatUsd(val))}</text>`;
2593
+ }).join("\n");
2594
+ }
2595
+ function renderEfficiencyMonthlyShareSvg(efficiencyData) {
2596
+ const monthlyRows = toMonthlyRows(efficiencyData.rows);
2597
+ const allRow = toAllRow(efficiencyData.rows);
2598
+ const chartLeft = pad.left;
2599
+ const chartTop = pad.top;
2600
+ const chartRight = W - pad.right;
2601
+ const chartBottom = H - pad.bottom;
2602
+ const chartW = chartRight - chartLeft;
2603
+ const count = Math.max(1, monthlyRows.length);
2604
+ const stepX = count === 1 ? 0 : chartW / (count - 1);
2605
+ const maxCommits = Math.max(1, ...monthlyRows.map((r) => r.commitCount));
2606
+ const actualMaxUsd = Math.max(0, ...monthlyRows.map((r) => Math.max(0, r.usdPerCommit ?? 0)));
2607
+ const scaleMaxUsd = Math.max(0.01, actualMaxUsd);
2608
+ const barWidth = Math.min(48, Math.max(18, chartW / (count * 2.2)));
2609
+ const commitBars = monthlyRows.map((row, i) => {
2610
+ const x = chartLeft + i * stepX;
2611
+ const yTop = scaleY(row.commitCount, maxCommits, chartTop, chartBottom);
2612
+ return `<rect x="${(x - barWidth / 2).toFixed(2)}" y="${yTop.toFixed(2)}" width="${barWidth.toFixed(2)}" height="${(chartBottom - yTop).toFixed(2)}" rx="4" fill="${chartColors.commits}" fill-opacity="0.35"/>`;
2613
+ }).join("\n");
2614
+ const usdPoints = monthlyRows.map((row, i) => ({
2615
+ x: chartLeft + i * stepX,
2616
+ y: scaleY(row.usdPerCommit ?? 0, scaleMaxUsd, chartTop, chartBottom)
2617
+ }));
2618
+ const usdLine = usdPoints.length >= 2 ? `<path d="${catmullRom(usdPoints, 0.3, chartBottom)}" fill="none" stroke="${chartColors.usdPerCommit}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>` : "";
2619
+ const dotRadius = count <= 4 ? 6 : 4.5;
2620
+ const usdDots = usdPoints.map(
2621
+ (p) => `<circle cx="${p.x.toFixed(2)}" cy="${p.y.toFixed(2)}" r="${dotRadius}" fill="${chartColors.usdPerCommit}"/>`
2622
+ ).join("\n");
2623
+ const monthLabels = monthlyRows.map((row, i) => {
2624
+ const x = chartLeft + i * stepX;
2625
+ return `<text x="${x.toFixed(2)}" y="${(chartBottom + 28).toFixed(0)}" text-anchor="middle" font-size="13" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">${escapeSvg(row.periodKey)}</text>`;
2626
+ }).join("\n");
2627
+ const axisLabels = [
2628
+ `<text x="${(chartLeft - 12).toFixed(0)}" y="${(chartTop - 10).toFixed(0)}" text-anchor="end" font-size="12" font-weight="600" fill="${chartColors.commits}" font-family="${shareTheme.font}">Commits</text>`,
2629
+ `<text x="${(chartLeft - 12).toFixed(0)}" y="${(chartTop + 5).toFixed(0)}" text-anchor="end" font-size="11" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">${escapeSvg(formatInteger(maxCommits))}</text>`,
2630
+ `<text x="${(chartLeft - 12).toFixed(0)}" y="${(chartBottom + 5).toFixed(0)}" text-anchor="end" font-size="11" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">0</text>`,
2631
+ `<text x="${(chartRight + 12).toFixed(0)}" y="${(chartTop - 10).toFixed(0)}" font-size="12" font-weight="600" fill="${chartColors.usdPerCommit}" font-family="${shareTheme.font}">$/Commit</text>`,
2632
+ `<text x="${(chartRight + 12).toFixed(0)}" y="${(chartTop + 5).toFixed(0)}" font-size="11" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">${escapeSvg(formatUsd(actualMaxUsd))}</text>`,
2633
+ `<text x="${(chartRight + 12).toFixed(0)}" y="${(chartBottom + 5).toFixed(0)}" font-size="11" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">$0.00</text>`
2634
+ ].join("\n");
2635
+ const noData = monthlyRows.length === 0 ? `<text x="${(W / 2).toFixed(0)}" y="${(H / 2).toFixed(0)}" text-anchor="middle" font-size="20" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">No monthly efficiency data available</text>` : "";
2636
+ const vcenter = 70;
2637
+ const commandText = "llm-usage efficiency monthly --share";
2638
+ const badgeW = commandText.length * 9.5 + 28;
2639
+ const badgeX = W - pad.right - badgeW;
2640
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
2641
+ <defs>
2642
+ <linearGradient id="accent-grad" x1="0" y1="0" x2="1" y2="0">
2643
+ <stop offset="0%" stop-color="#10b981"/>
2644
+ <stop offset="100%" stop-color="#06b6d4"/>
2645
+ </linearGradient>
2646
+ </defs>
2647
+ <rect width="${W}" height="${H}" fill="${shareTheme.bg}"/>
2648
+ <rect width="${W}" height="${ACCENT_H}" fill="url(#accent-grad)"/>
2649
+ <text x="${pad.left}" y="52" font-size="32" font-weight="700" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}">Monthly Efficiency</text>
2650
+ <rect x="${badgeX.toFixed(0)}" y="30" width="${badgeW.toFixed(0)}" height="34" rx="17" fill="none" stroke="${shareTheme.cardBorder}"/>
2651
+ <text x="${(badgeX + badgeW / 2).toFixed(0)}" y="52" text-anchor="middle" font-size="14" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}">${escapeSvg(commandText)}</text>
2652
+ ${renderSummaryStats(allRow, vcenter)}
2653
+ ${renderLegend(pad.left, vcenter + 50)}
2654
+ <line x1="${chartLeft}" y1="${chartBottom}" x2="${chartRight}" y2="${chartBottom}" stroke="${shareTheme.gridLine}" stroke-width="1"/>
2655
+ <line x1="${chartLeft}" y1="${chartTop}" x2="${chartLeft}" y2="${chartBottom}" stroke="${shareTheme.gridLine}" stroke-width="1"/>
2656
+ <line x1="${chartRight}" y1="${chartTop}" x2="${chartRight}" y2="${chartBottom}" stroke="${shareTheme.gridLine}" stroke-width="1"/>
2657
+ ${axisLabels}
2658
+ ${commitBars}
2659
+ ${renderCommitBarLabels(monthlyRows, chartLeft, stepX, maxCommits, chartTop, chartBottom)}
2660
+ ${usdLine}
2661
+ ${usdDots}
2662
+ ${renderUsdDataLabels(monthlyRows, usdPoints)}
2663
+ ${monthLabels}
2664
+ ${noData}
2665
+ <line x1="0" y1="${H - FOOTER_H + 1}" x2="${W}" y2="${H - FOOTER_H + 1}" stroke="${shareTheme.gridLine}" stroke-width="1"/>
2666
+ <text x="60" y="${H - FOOTER_H / 2 + 5}" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}" font-size="13">llm-usage-metrics</text>
2667
+ </svg>`;
2668
+ }
2669
+
2455
2670
  // src/utils/time-buckets.ts
2456
2671
  var formatterCache = /* @__PURE__ */ new Map();
2457
2672
  function getDateFormatter(timezone) {
@@ -2776,8 +2991,7 @@ function computeDerivedMetrics(usage, outcomes) {
2776
2991
  return {
2777
2992
  usdPerCommit: costUsd !== void 0 && outcomes.commitCount > 0 ? costUsd / outcomes.commitCount : void 0,
2778
2993
  usdPer1kLinesChanged: costUsd !== void 0 && outcomes.linesChanged > 0 ? costUsd / (outcomes.linesChanged / 1e3) : void 0,
2779
- tokensPerCommit: outcomes.commitCount > 0 ? usage.totalTokens / outcomes.commitCount : void 0,
2780
- nonCacheTokensPerCommit: outcomes.commitCount > 0 ? nonCacheTotalTokens / outcomes.commitCount : void 0,
2994
+ tokensPerCommit: outcomes.commitCount > 0 ? nonCacheTotalTokens / outcomes.commitCount : void 0,
2781
2995
  commitsPerUsd: costUsd !== void 0 && costUsd > 0 ? outcomes.commitCount / costUsd : void 0
2782
2996
  };
2783
2997
  }
@@ -3576,7 +3790,7 @@ function normalizeSkippedRowReasons(value) {
3576
3790
  // src/cli/parse-file-cache.ts
3577
3791
  import { mkdir as mkdir2, readFile as readFile4, rename, rm, stat as stat3, writeFile as writeFile2 } from "fs/promises";
3578
3792
  import path11 from "path";
3579
- var PARSE_FILE_CACHE_VERSION = 3;
3793
+ var PARSE_FILE_CACHE_VERSION = 4;
3580
3794
  var CACHE_KEY_SEPARATOR = "\0";
3581
3795
  function createCacheKey(source, filePath) {
3582
3796
  return `${source}${CACHE_KEY_SEPARATOR}${filePath}`;
@@ -5122,6 +5336,15 @@ function emitEnvVarOverrides(activeEnvOverrides, diagnosticsLogger) {
5122
5336
  }
5123
5337
  }
5124
5338
 
5339
+ // src/cli/share-artifact.ts
5340
+ import { writeFile as writeFile4 } from "fs/promises";
5341
+ import path13 from "path";
5342
+ async function writeShareSvgFile(fileName, svgContent) {
5343
+ const outputPath = path13.resolve(process.cwd(), fileName);
5344
+ await writeFile4(outputPath, svgContent, "utf8");
5345
+ return outputPath;
5346
+ }
5347
+
5125
5348
  // src/render/table-text-layout.ts
5126
5349
  var ansiEscapePattern = new RegExp(String.raw`\u001B\[[0-9;]*m`, "gu");
5127
5350
  var combiningMarkPattern = /\p{Mark}/u;
@@ -5372,8 +5595,7 @@ var efficiencyTableHeaders = [
5372
5595
  "Cost",
5373
5596
  "$/Commit",
5374
5597
  "$/1k Lines",
5375
- "All Tokens/Commit",
5376
- "Non-Cache/Commit",
5598
+ "Tokens/Commit",
5377
5599
  "Commits/$"
5378
5600
  ];
5379
5601
  var integerFormatter = new Intl.NumberFormat("en-US");
@@ -5393,10 +5615,10 @@ var usdRateFormatter = new Intl.NumberFormat("en-US", {
5393
5615
  minimumFractionDigits: 4,
5394
5616
  maximumFractionDigits: 4
5395
5617
  });
5396
- function formatInteger(value) {
5618
+ function formatInteger2(value) {
5397
5619
  return integerFormatter.format(value);
5398
5620
  }
5399
- function formatUsd(value, options = {}) {
5621
+ function formatUsd2(value, options = {}) {
5400
5622
  if (value === void 0) {
5401
5623
  return "-";
5402
5624
  }
@@ -5410,7 +5632,7 @@ function formatUsdRate(value, options = {}) {
5410
5632
  const formatted = usdRateFormatter.format(value);
5411
5633
  return options.approximate ? `~${formatted}` : formatted;
5412
5634
  }
5413
- function formatDecimal(value, options = {}) {
5635
+ function formatDecimal2(value, options = {}) {
5414
5636
  if (value === void 0) {
5415
5637
  return "-";
5416
5638
  }
@@ -5420,22 +5642,21 @@ function formatDecimal(value, options = {}) {
5420
5642
  function toEfficiencyTableCells(rows) {
5421
5643
  return rows.map((row) => [
5422
5644
  row.periodKey,
5423
- formatInteger(row.commitCount),
5424
- formatInteger(row.linesAdded),
5425
- formatInteger(row.linesDeleted),
5426
- formatInteger(row.linesChanged),
5427
- formatInteger(row.inputTokens),
5428
- formatInteger(row.outputTokens),
5429
- formatInteger(row.reasoningTokens),
5430
- formatInteger(row.cacheReadTokens),
5431
- formatInteger(row.cacheWriteTokens),
5432
- formatInteger(row.totalTokens),
5433
- formatUsd(row.costUsd, { approximate: row.costIncomplete }),
5645
+ formatInteger2(row.commitCount),
5646
+ formatInteger2(row.linesAdded),
5647
+ formatInteger2(row.linesDeleted),
5648
+ formatInteger2(row.linesChanged),
5649
+ formatInteger2(row.inputTokens),
5650
+ formatInteger2(row.outputTokens),
5651
+ formatInteger2(row.reasoningTokens),
5652
+ formatInteger2(row.cacheReadTokens),
5653
+ formatInteger2(row.cacheWriteTokens),
5654
+ formatInteger2(row.totalTokens),
5655
+ formatUsd2(row.costUsd, { approximate: row.costIncomplete }),
5434
5656
  formatUsdRate(row.usdPerCommit, { approximate: row.costIncomplete }),
5435
5657
  formatUsdRate(row.usdPer1kLinesChanged, { approximate: row.costIncomplete }),
5436
- formatDecimal(row.tokensPerCommit),
5437
- formatDecimal(row.nonCacheTokensPerCommit),
5438
- formatDecimal(row.commitsPerUsd, { approximate: row.costIncomplete })
5658
+ formatDecimal2(row.tokensPerCommit),
5659
+ formatDecimal2(row.commitsPerUsd, { approximate: row.costIncomplete })
5439
5660
  ]);
5440
5661
  }
5441
5662
 
@@ -5546,7 +5767,7 @@ function formatSource(row) {
5546
5767
  function formatTokenCount(value) {
5547
5768
  return integerFormatter2.format(value ?? 0);
5548
5769
  }
5549
- function formatUsd2(value, options = {}) {
5770
+ function formatUsd3(value, options = {}) {
5550
5771
  if (value === void 0) {
5551
5772
  return "-";
5552
5773
  }
@@ -5581,13 +5802,13 @@ function formatModelMetric(row, selector, formatter, layout) {
5581
5802
  }
5582
5803
  function formatModelCostMetric(row, layout) {
5583
5804
  if (layout !== "per_model_columns" || row.modelBreakdown.length === 0) {
5584
- return formatUsd2(row.costUsd, { incomplete: row.costIncomplete });
5805
+ return formatUsd3(row.costUsd, { incomplete: row.costIncomplete });
5585
5806
  }
5586
5807
  const lines = row.modelBreakdown.map(
5587
- (modelUsage) => formatUsd2(modelUsage.costUsd, { incomplete: modelUsage.costIncomplete })
5808
+ (modelUsage) => formatUsd3(modelUsage.costUsd, { incomplete: modelUsage.costIncomplete })
5588
5809
  );
5589
5810
  if (row.modelBreakdown.length > 1) {
5590
- lines.push(formatUsd2(row.costUsd, { incomplete: row.costIncomplete }));
5811
+ lines.push(formatUsd3(row.costUsd, { incomplete: row.costIncomplete }));
5591
5812
  }
5592
5813
  return lines.join("\n");
5593
5814
  }
@@ -6191,13 +6412,23 @@ function resolveReportFormat(options) {
6191
6412
  }
6192
6413
  return "terminal";
6193
6414
  }
6415
+ function validateShareOption(granularity, options) {
6416
+ if (!options.share) {
6417
+ return;
6418
+ }
6419
+ if (granularity !== "monthly") {
6420
+ throw new Error("--share is only supported for efficiency monthly");
6421
+ }
6422
+ }
6194
6423
  async function prepareEfficiencyReport(granularity, options) {
6195
6424
  validateOutputFormatOptions(options);
6425
+ validateShareOption(granularity, options);
6196
6426
  const efficiencyData = await buildEfficiencyData(granularity, options);
6197
6427
  const format = resolveReportFormat(options);
6198
6428
  return {
6199
6429
  format,
6200
6430
  diagnostics: efficiencyData.diagnostics,
6431
+ shareSvg: options.share ? renderEfficiencyMonthlyShareSvg(efficiencyData) : void 0,
6201
6432
  output: renderEfficiencyReport(efficiencyData, format, {
6202
6433
  granularity
6203
6434
  })
@@ -6222,9 +6453,141 @@ async function runEfficiencyReport(granularity, options) {
6222
6453
  logger.warn(message);
6223
6454
  });
6224
6455
  }
6456
+ if (preparedReport.shareSvg) {
6457
+ const outputPath = await writeShareSvgFile(
6458
+ "efficiency-monthly-share.svg",
6459
+ preparedReport.shareSvg
6460
+ );
6461
+ logger.info(`Wrote efficiency share SVG: ${outputPath}`);
6462
+ }
6225
6463
  console.log(preparedReport.output);
6226
6464
  }
6227
6465
 
6466
+ // src/render/render-optimize-share-svg.ts
6467
+ var W2 = 1500;
6468
+ var H2 = 780;
6469
+ var ACCENT_H2 = 4;
6470
+ var FOOTER_H2 = 36;
6471
+ var pad2 = { top: 180, right: 70, bottom: 60 + FOOTER_H2, left: 260 };
6472
+ function formatPercent(value) {
6473
+ if (value === void 0) return "-";
6474
+ return `${(value * 100).toFixed(1)}%`;
6475
+ }
6476
+ function cellFill(value) {
6477
+ if (value === void 0) return "rgba(139,148,158,0.12)";
6478
+ const mag = Math.min(1, Math.abs(value));
6479
+ const alpha = 0.22 + mag * 0.58;
6480
+ if (value > 0) return `rgba(34,197,94,${alpha.toFixed(3)})`;
6481
+ if (value < 0) return `rgba(239,68,68,${alpha.toFixed(3)})`;
6482
+ return "rgba(139,148,158,0.15)";
6483
+ }
6484
+ function cellTextFill(value) {
6485
+ if (value === void 0) return shareTheme.textMuted;
6486
+ if (Math.abs(value) >= 0.35) return "#ffffff";
6487
+ return shareTheme.textPrimary;
6488
+ }
6489
+ function toCandidateRows(data) {
6490
+ return data.rows.filter((r) => r.rowType === "candidate");
6491
+ }
6492
+ function sortPeriodKeys(keys) {
6493
+ return [...keys].sort(compareByCodePoint);
6494
+ }
6495
+ function renderOptimizeMonthlyShareSvg(optimizeData) {
6496
+ const candidateRows = toCandidateRows(optimizeData);
6497
+ const periodKeys = sortPeriodKeys(
6498
+ new Set(candidateRows.map((r) => r.periodKey).filter((k) => k !== "ALL"))
6499
+ );
6500
+ const candidateModels = [...new Set(candidateRows.map((r) => r.candidateModel))].sort(
6501
+ compareByCodePoint
6502
+ );
6503
+ const cellMap = /* @__PURE__ */ new Map();
6504
+ for (const row of candidateRows) {
6505
+ cellMap.set(`${row.candidateModel}__${row.periodKey}`, row);
6506
+ }
6507
+ const allByCandidate = /* @__PURE__ */ new Map();
6508
+ for (const row of candidateRows) {
6509
+ if (row.periodKey === "ALL") allByCandidate.set(row.candidateModel, row);
6510
+ }
6511
+ const chartLeft = pad2.left;
6512
+ const chartTop = pad2.top;
6513
+ const chartRight = W2 - pad2.right;
6514
+ const chartBottom = H2 - pad2.bottom;
6515
+ const chartW = chartRight - chartLeft;
6516
+ const chartH = chartBottom - chartTop;
6517
+ const rowCount = Math.max(1, candidateModels.length);
6518
+ const colCount = Math.max(1, periodKeys.length);
6519
+ const cellW = chartW / colCount;
6520
+ const cellH = chartH / rowCount;
6521
+ const gridCells = [];
6522
+ const colLabels = [];
6523
+ const rowLabels = [];
6524
+ for (let c = 0; c < periodKeys.length; c++) {
6525
+ const x = chartLeft + c * cellW + cellW / 2;
6526
+ colLabels.push(
6527
+ `<text x="${x.toFixed(2)}" y="${(chartTop - 14).toFixed(0)}" text-anchor="middle" font-size="14" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">${escapeSvg(periodKeys[c])}</text>`
6528
+ );
6529
+ }
6530
+ for (let r = 0; r < candidateModels.length; r++) {
6531
+ const model = candidateModels[r];
6532
+ const y = chartTop + r * cellH + cellH / 2 + 5;
6533
+ const allRow = allByCandidate.get(model);
6534
+ const allLabel = allRow?.savingsUsd === void 0 ? "-" : formatUsd(Math.abs(allRow.savingsUsd));
6535
+ const allColor = allRow?.savingsUsd === void 0 ? shareTheme.textMuted : allRow.savingsUsd > 0 ? "#22c55e" : allRow.savingsUsd < 0 ? "#ef4444" : shareTheme.textSecondary;
6536
+ const prefix = allRow?.savingsUsd === void 0 ? "" : allRow.savingsUsd > 0 ? "+" : allRow.savingsUsd < 0 ? "-" : "";
6537
+ rowLabels.push(
6538
+ `<text x="${(chartLeft - 16).toFixed(0)}" y="${y.toFixed(0)}" text-anchor="end" font-size="14" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}">${escapeSvg(model)}</text>`
6539
+ );
6540
+ rowLabels.push(
6541
+ `<text x="${(chartLeft - 16).toFixed(0)}" y="${(y + 16).toFixed(0)}" text-anchor="end" font-size="12" fill="${allColor}" font-family="${shareTheme.font}">ALL: ${escapeSvg(prefix + allLabel)}</text>`
6542
+ );
6543
+ for (let c = 0; c < periodKeys.length; c++) {
6544
+ const row = cellMap.get(`${model}__${periodKeys[c]}`);
6545
+ const x = chartLeft + c * cellW;
6546
+ const yTop = chartTop + r * cellH;
6547
+ const pct = row?.savingsPct;
6548
+ gridCells.push(
6549
+ `<rect x="${(x + 2).toFixed(2)}" y="${(yTop + 2).toFixed(2)}" width="${Math.max(0, cellW - 4).toFixed(2)}" height="${Math.max(0, cellH - 4).toFixed(2)}" fill="${cellFill(pct)}" rx="6"/>`
6550
+ );
6551
+ gridCells.push(
6552
+ `<text x="${(x + cellW / 2).toFixed(2)}" y="${(yTop + cellH / 2 + 5).toFixed(2)}" text-anchor="middle" font-size="13" font-weight="600" fill="${cellTextFill(pct)}" font-family="${shareTheme.font}">${escapeSvg(formatPercent(pct))}</text>`
6553
+ );
6554
+ }
6555
+ }
6556
+ const provider = optimizeData.diagnostics.provider;
6557
+ const missing = optimizeData.diagnostics.candidatesWithMissingPricing;
6558
+ const warning = optimizeData.diagnostics.warning ?? "";
6559
+ const commandText = "llm-usage optimize monthly --share";
6560
+ const badgeW = commandText.length * 9.5 + 28;
6561
+ const badgeX = W2 - pad2.right - badgeW;
6562
+ const noData = candidateModels.length === 0 || periodKeys.length === 0 ? `<text x="${(W2 / 2).toFixed(0)}" y="${(H2 / 2).toFixed(0)}" text-anchor="middle" font-size="20" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">No monthly optimize data available</text>` : "";
6563
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W2}" height="${H2}" viewBox="0 0 ${W2} ${H2}">
6564
+ <defs>
6565
+ <linearGradient id="accent-grad" x1="0" y1="0" x2="1" y2="0">
6566
+ <stop offset="0%" stop-color="#10b981"/>
6567
+ <stop offset="100%" stop-color="#06b6d4"/>
6568
+ </linearGradient>
6569
+ </defs>
6570
+ <rect width="${W2}" height="${H2}" fill="${shareTheme.bg}"/>
6571
+ <rect width="${W2}" height="${ACCENT_H2}" fill="url(#accent-grad)"/>
6572
+ <text x="${pad2.left}" y="52" font-size="32" font-weight="700" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}">Monthly Optimize</text>
6573
+ <text x="${pad2.left}" y="78" font-size="15" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">Savings % heatmap by candidate and month</text>
6574
+ <rect x="${badgeX.toFixed(0)}" y="30" width="${badgeW.toFixed(0)}" height="34" rx="17" fill="none" stroke="${shareTheme.cardBorder}"/>
6575
+ <text x="${(badgeX + badgeW / 2).toFixed(0)}" y="52" text-anchor="middle" font-size="14" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}">${escapeSvg(commandText)}</text>
6576
+ <text x="${pad2.left}" y="112" font-size="15" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}">Provider: <tspan font-weight="700">${escapeSvg(provider)}</tspan></text>
6577
+ <text x="${pad2.left + 280}" y="112" font-size="14" fill="#22c55e" font-family="${shareTheme.font}">\u25CF positive = savings</text>
6578
+ <text x="${pad2.left + 480}" y="112" font-size="14" fill="#ef4444" font-family="${shareTheme.font}">\u25CF negative = higher cost</text>
6579
+ ${missing.length > 0 ? `<text x="${pad2.left}" y="136" font-size="13" fill="#eab308" font-family="${shareTheme.font}">Missing pricing: ${escapeSvg(missing.join(", "))}</text>` : ""}
6580
+ ${warning ? `<text x="${pad2.left}" y="158" font-size="13" fill="#eab308" font-family="${shareTheme.font}">${escapeSvg(warning)}</text>` : ""}
6581
+ <rect x="${chartLeft}" y="${chartTop}" width="${chartW}" height="${chartH}" fill="${shareTheme.cardBg}" rx="10"/>
6582
+ ${gridCells.join("\n")}
6583
+ ${colLabels.join("\n")}
6584
+ ${rowLabels.join("\n")}
6585
+ ${noData}
6586
+ <line x1="0" y1="${H2 - FOOTER_H2 + 1}" x2="${W2}" y2="${H2 - FOOTER_H2 + 1}" stroke="${shareTheme.gridLine}" stroke-width="1"/>
6587
+ <text x="60" y="${H2 - FOOTER_H2 / 2 + 5}" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}" font-size="13">llm-usage-metrics</text>
6588
+ </svg>`;
6589
+ }
6590
+
6228
6591
  // src/render/render-optimize-report.ts
6229
6592
  import { markdownTable as markdownTable2 } from "markdown-table";
6230
6593
  import pc6 from "picocolors";
@@ -6261,14 +6624,14 @@ function getReportTitle2(granularity) {
6261
6624
  return "Monthly Optimize Report";
6262
6625
  }
6263
6626
  }
6264
- function formatUsd3(value, options = {}) {
6627
+ function formatUsd4(value, options = {}) {
6265
6628
  if (value === void 0) {
6266
6629
  return "-";
6267
6630
  }
6268
6631
  const formatted = usdFormatter3.format(value);
6269
6632
  return options.approximate ? `~${formatted}` : formatted;
6270
6633
  }
6271
- function formatPercent(value) {
6634
+ function formatPercent2(value) {
6272
6635
  if (value === void 0) {
6273
6636
  return "-";
6274
6637
  }
@@ -6322,7 +6685,7 @@ function resolveTerminalContextLines(optimizeData, options) {
6322
6685
  lines.push(options.useColor ? pc6.cyan(providerLine) : providerLine);
6323
6686
  if (allBaselineRow) {
6324
6687
  lines.push(
6325
- `ALL baseline cost: ${formatUsd3(allBaselineRow.baselineCostUsd, { approximate: allBaselineRow.baselineCostIncomplete })}`
6688
+ `ALL baseline cost: ${formatUsd4(allBaselineRow.baselineCostUsd, { approximate: allBaselineRow.baselineCostIncomplete })}`
6326
6689
  );
6327
6690
  }
6328
6691
  if (allCandidateRows.length > 0) {
@@ -6334,11 +6697,11 @@ function resolveTerminalContextLines(optimizeData, options) {
6334
6697
  lines.push("ALL best candidate: unavailable (missing baseline or candidate pricing)");
6335
6698
  } else if (bestRow.savingsUsd > 0) {
6336
6699
  lines.push(
6337
- `ALL best candidate: ${bestRow.candidateModel} saves ${formatAbsoluteUsd(bestRow.savingsUsd)} (${formatPercent(bestRow.savingsPct)})`
6700
+ `ALL best candidate: ${bestRow.candidateModel} saves ${formatAbsoluteUsd(bestRow.savingsUsd)} (${formatPercent2(bestRow.savingsPct)})`
6338
6701
  );
6339
6702
  } else if (bestRow.savingsUsd < 0) {
6340
6703
  lines.push(
6341
- `ALL best candidate: ${bestRow.candidateModel} increases cost by ${formatAbsoluteUsd(bestRow.savingsUsd)} (${formatPercent(bestRow.savingsPct)})`
6704
+ `ALL best candidate: ${bestRow.candidateModel} increases cost by ${formatAbsoluteUsd(bestRow.savingsUsd)} (${formatPercent2(bestRow.savingsPct)})`
6342
6705
  );
6343
6706
  } else {
6344
6707
  lines.push(`ALL best candidate: ${bestRow.candidateModel} matches baseline cost`);
@@ -6364,20 +6727,20 @@ function toTableCells(optimizeData, options) {
6364
6727
  periodCell,
6365
6728
  styleCandidateCell("BASELINE", "baseline", options.useColor),
6366
6729
  "-",
6367
- formatUsd3(row.baselineCostUsd, { approximate: row.baselineCostIncomplete }),
6730
+ formatUsd4(row.baselineCostUsd, { approximate: row.baselineCostIncomplete }),
6368
6731
  "-",
6369
6732
  "-"
6370
6733
  ];
6371
6734
  return options.includeNotesColumn ? [...baselineCells, "-"] : baselineCells;
6372
6735
  }
6373
- const savingsCell = formatUsd3(row.savingsUsd);
6374
- const savingsPctCell = formatPercent(row.savingsPct);
6736
+ const savingsCell = formatUsd4(row.savingsUsd);
6737
+ const savingsPctCell = formatPercent2(row.savingsPct);
6375
6738
  const notesCell = formatNotes(row.notes);
6376
6739
  const candidateCells = [
6377
6740
  periodCell,
6378
6741
  styleCandidateCell(row.candidateModel, "candidate", options.useColor),
6379
- formatUsd3(row.hypotheticalCostUsd, { approximate: row.hypotheticalCostIncomplete }),
6380
- formatUsd3(baselineRow?.baselineCostUsd, {
6742
+ formatUsd4(row.hypotheticalCostUsd, { approximate: row.hypotheticalCostIncomplete }),
6743
+ formatUsd4(baselineRow?.baselineCostUsd, {
6381
6744
  approximate: baselineRow?.baselineCostIncomplete === true
6382
6745
  }),
6383
6746
  styleDeltaCell2(row.savingsUsd, savingsCell, options.useColor),
@@ -6818,8 +7181,17 @@ function resolveReportFormat2(options) {
6818
7181
  }
6819
7182
  return "terminal";
6820
7183
  }
7184
+ function validateShareOption2(granularity, options) {
7185
+ if (!options.share) {
7186
+ return;
7187
+ }
7188
+ if (granularity !== "monthly") {
7189
+ throw new Error("--share is only supported for optimize monthly");
7190
+ }
7191
+ }
6821
7192
  async function prepareOptimizeReport(granularity, options) {
6822
7193
  validateOutputFormatOptions2(options);
7194
+ validateShareOption2(granularity, options);
6823
7195
  const optimizeData = await buildOptimizeData(granularity, options);
6824
7196
  const format = resolveReportFormat2(options);
6825
7197
  return {
@@ -6828,6 +7200,7 @@ async function prepareOptimizeReport(granularity, options) {
6828
7200
  candidateCount: optimizeData.rows.filter(
6829
7201
  (row) => row.rowType === "candidate" && row.periodKey === "ALL"
6830
7202
  ).length,
7203
+ shareSvg: options.share ? renderOptimizeMonthlyShareSvg(optimizeData) : void 0,
6831
7204
  output: renderOptimizeReport(optimizeData, format, {
6832
7205
  granularity
6833
7206
  })
@@ -6853,6 +7226,13 @@ async function runOptimizeReport(granularity, options) {
6853
7226
  logger.warn(message);
6854
7227
  });
6855
7228
  }
7229
+ if (preparedReport.shareSvg) {
7230
+ const outputPath = await writeShareSvgFile(
7231
+ "optimize-monthly-share.svg",
7232
+ preparedReport.shareSvg
7233
+ );
7234
+ logger.info(`Wrote optimize share SVG: ${outputPath}`);
7235
+ }
6856
7236
  console.log(preparedReport.output);
6857
7237
  }
6858
7238
 
@@ -6910,6 +7290,257 @@ function renderUsageReport(usageData, format, options) {
6910
7290
  }
6911
7291
  }
6912
7292
 
7293
+ // src/render/render-usage-share-svg.ts
7294
+ var W3 = 1500;
7295
+ var H3 = 560;
7296
+ var ACCENT_H3 = 4;
7297
+ var FOOTER_H3 = 36;
7298
+ var pad3 = { top: 140, right: 80, bottom: 60 + FOOTER_H3, left: 200 };
7299
+ function extractPeriodSourceRows(rows) {
7300
+ return rows.filter((r) => r.rowType === "period_source");
7301
+ }
7302
+ function extractGrandTotal(rows) {
7303
+ return rows.find((r) => r.rowType === "grand_total");
7304
+ }
7305
+ function buildSourceSeries(sourceRows, periods, sources) {
7306
+ const lookup = /* @__PURE__ */ new Map();
7307
+ const sourceTotals = /* @__PURE__ */ new Map();
7308
+ for (const row of sourceRows) {
7309
+ const key = `${row.source}__${row.periodKey}`;
7310
+ lookup.set(key, (lookup.get(key) ?? 0) + row.totalTokens);
7311
+ sourceTotals.set(row.source, (sourceTotals.get(row.source) ?? 0) + row.totalTokens);
7312
+ }
7313
+ return sources.map((source, index) => ({
7314
+ source,
7315
+ color: getSourceColor(source, index),
7316
+ total: sourceTotals.get(source) ?? 0,
7317
+ values: periods.map((period) => lookup.get(`${source}__${period}`) ?? 0)
7318
+ }));
7319
+ }
7320
+ function buildStackedValues(series) {
7321
+ if (series.length === 0) return [];
7322
+ const periodCount = series[0].values.length;
7323
+ const stacked = [];
7324
+ for (let s = 0; s < series.length; s++) {
7325
+ stacked.push(
7326
+ Array.from({ length: periodCount }, (_, p) => {
7327
+ let sum = 0;
7328
+ for (let si = 0; si <= s; si++) sum += series[si].values[p];
7329
+ return sum;
7330
+ })
7331
+ );
7332
+ }
7333
+ return stacked;
7334
+ }
7335
+ function renderAccentBar() {
7336
+ return [
7337
+ `<defs><linearGradient id="accent-grad" x1="0" y1="0" x2="1" y2="0">`,
7338
+ ` <stop offset="0%" stop-color="#10b981"/>`,
7339
+ ` <stop offset="100%" stop-color="#06b6d4"/>`,
7340
+ `</linearGradient></defs>`,
7341
+ `<rect width="${W3}" height="${ACCENT_H3}" fill="url(#accent-grad)"/>`
7342
+ ].join("\n");
7343
+ }
7344
+ function renderStatColumn(totalTokens, costUsd, sourceCount) {
7345
+ const x = 60;
7346
+ const baseY = ACCENT_H3 + 48;
7347
+ let svg = "";
7348
+ svg += `<text x="${x}" y="${baseY}" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}" font-size="52" font-weight="800">${escapeSvg(formatCompact(totalTokens))}</text>
7349
+ `;
7350
+ svg += `<text x="${x}" y="${baseY + 22}" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}" font-size="14" letter-spacing="3" font-weight="600">TOKENS</text>
7351
+ `;
7352
+ if (costUsd !== void 0) {
7353
+ svg += `<text x="${x}" y="${baseY + 50}" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}" font-size="22" font-weight="600">${escapeSvg(formatUsd(costUsd))}</text>
7354
+ `;
7355
+ }
7356
+ svg += `<text x="${x}" y="${baseY + 74}" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}" font-size="13">${sourceCount} source${sourceCount !== 1 ? "s" : ""}</text>
7357
+ `;
7358
+ return svg;
7359
+ }
7360
+ function renderSourcePills(series) {
7361
+ let svg = "";
7362
+ let cx = pad3.left + 10;
7363
+ const pillY = ACCENT_H3 + 30;
7364
+ for (const s of series) {
7365
+ const label = `${s.source} ${formatCompact(s.total)}`;
7366
+ const textW = label.length * 8.5;
7367
+ const pillW = textW + 28;
7368
+ const pillH = 30;
7369
+ svg += `<rect x="${cx}" y="${pillY}" width="${pillW.toFixed(0)}" height="${pillH}" rx="${pillH / 2}" fill="${s.color}" fill-opacity="0.15" stroke="${s.color}" stroke-opacity="0.4" stroke-width="1"/>
7370
+ `;
7371
+ svg += `<circle cx="${cx + 14}" cy="${pillY + pillH / 2}" r="4" fill="${s.color}"/>
7372
+ `;
7373
+ svg += `<text x="${cx + 24}" y="${pillY + pillH / 2 + 5}" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}" font-size="14">${escapeSvg(label)}</text>
7374
+ `;
7375
+ cx += pillW + 10;
7376
+ }
7377
+ return svg;
7378
+ }
7379
+ function renderCommandBadge(command) {
7380
+ const textW = command.length * 9;
7381
+ const badgeW = textW + 28;
7382
+ const badgeH = 30;
7383
+ const x = W3 - 60 - badgeW;
7384
+ const y = ACCENT_H3 + 30;
7385
+ return [
7386
+ `<rect x="${x}" y="${y}" width="${badgeW}" height="${badgeH}" rx="${badgeH / 2}" fill="none" stroke="${shareTheme.cardBorder}" stroke-width="1"/>`,
7387
+ `<text x="${x + badgeW / 2}" y="${y + badgeH / 2 + 5}" text-anchor="middle" font-size="13" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}">${escapeSvg(command)}</text>`
7388
+ ].join("\n");
7389
+ }
7390
+ function renderGridLines(chartLeft, chartRight, chartTop, chartH, maxY) {
7391
+ const gridCount = 4;
7392
+ let svg = "";
7393
+ for (let i = 1; i <= gridCount; i++) {
7394
+ const val = maxY / gridCount * i;
7395
+ const y = chartTop + chartH - i / gridCount * chartH;
7396
+ svg += `<line x1="${chartLeft}" y1="${y.toFixed(2)}" x2="${chartRight}" y2="${y.toFixed(2)}" stroke="${shareTheme.gridLine}" stroke-width="1" stroke-dasharray="4 4"/>
7397
+ `;
7398
+ svg += `<text x="${(chartLeft - 12).toFixed(0)}" y="${(y + 4).toFixed(0)}" text-anchor="end" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}" font-size="11">${escapeSvg(formatCompact(val))}</text>
7399
+ `;
7400
+ }
7401
+ return svg;
7402
+ }
7403
+ function renderGradientDefs(series) {
7404
+ return series.map(
7405
+ (s, i) => `<linearGradient id="area-grad-${i}" x1="0" y1="0" x2="0" y2="1">
7406
+ <stop offset="0%" stop-color="${s.color}" stop-opacity="0.6"/>
7407
+ <stop offset="100%" stop-color="${s.color}" stop-opacity="0.15"/>
7408
+ </linearGradient>`
7409
+ ).join("\n");
7410
+ }
7411
+ function renderStackedAreas(series, stacked, periodCount, toX, toChartY, chartBottom) {
7412
+ if (periodCount < 2 || series.length === 0) return "";
7413
+ let svg = "";
7414
+ for (let s = series.length - 1; s >= 0; s--) {
7415
+ const topPoints = Array.from({ length: periodCount }, (_, p) => ({
7416
+ x: toX(p),
7417
+ y: toChartY(stacked[s][p])
7418
+ }));
7419
+ const topPath = catmullRom(topPoints, 0.3, chartBottom);
7420
+ let botPath;
7421
+ if (s === 0) {
7422
+ botPath = `L${toX(periodCount - 1).toFixed(2)},${chartBottom} L${toX(0).toFixed(2)},${chartBottom}`;
7423
+ } else {
7424
+ const botPoints = Array.from({ length: periodCount }, (_, p) => ({
7425
+ x: toX(p),
7426
+ y: toChartY(stacked[s - 1][p])
7427
+ })).reverse();
7428
+ botPath = catmullRom(botPoints, 0.3, chartBottom).replace("M", "L");
7429
+ }
7430
+ svg += `<path d="${topPath} ${botPath} Z" fill="url(#area-grad-${s})" clip-path="url(#chart-clip)"/>
7431
+ `;
7432
+ }
7433
+ const totalPoints = Array.from({ length: periodCount }, (_, p) => ({
7434
+ x: toX(p),
7435
+ y: toChartY(stacked[stacked.length - 1][p])
7436
+ }));
7437
+ const topLinePath = catmullRom(totalPoints, 0.3, chartBottom);
7438
+ svg += `<path d="${topLinePath}" fill="none" stroke="${shareTheme.textPrimary}" stroke-width="2" stroke-opacity="0.5" stroke-linejoin="round" stroke-linecap="round" clip-path="url(#chart-clip)"/>
7439
+ `;
7440
+ for (const pt of totalPoints) {
7441
+ svg += `<circle cx="${pt.x.toFixed(2)}" cy="${pt.y.toFixed(2)}" r="3" fill="${shareTheme.textPrimary}" fill-opacity="0.6" clip-path="url(#chart-clip)"/>
7442
+ `;
7443
+ }
7444
+ return svg;
7445
+ }
7446
+ function renderSinglePeriodBars(series, stacked, toX, toChartY, chartBottom, chartW) {
7447
+ let svg = "";
7448
+ const barWidth = Math.min(120, chartW * 0.4);
7449
+ const xCenter = toX(0);
7450
+ for (let s = series.length - 1; s >= 0; s--) {
7451
+ const yTop = toChartY(stacked[s][0]);
7452
+ const yBot = s === 0 ? chartBottom : toChartY(stacked[s - 1][0]);
7453
+ if (yBot - yTop > 0) {
7454
+ svg += `<rect x="${(xCenter - barWidth / 2).toFixed(2)}" y="${yTop.toFixed(2)}" width="${barWidth.toFixed(2)}" height="${(yBot - yTop).toFixed(2)}" fill="url(#area-grad-${s})" rx="4"/>
7455
+ `;
7456
+ }
7457
+ }
7458
+ return svg;
7459
+ }
7460
+ function renderPeriodLabels(periods, toX, chartBottom) {
7461
+ const periodCount = periods.length;
7462
+ const maxLabels = 12;
7463
+ const labelStep = periodCount <= maxLabels ? 1 : Math.ceil(periodCount / maxLabels);
7464
+ let svg = "";
7465
+ for (let p = 0; p < periodCount; p += labelStep) {
7466
+ svg += `<text x="${toX(p).toFixed(2)}" y="${(chartBottom + 24).toFixed(0)}" text-anchor="middle" font-size="13" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}">${escapeSvg(periods[p])}</text>
7467
+ `;
7468
+ }
7469
+ return svg;
7470
+ }
7471
+ function renderFooter(periods) {
7472
+ const y = H3 - FOOTER_H3;
7473
+ const lineY = y + 1;
7474
+ const textY = y + FOOTER_H3 / 2 + 5;
7475
+ const range = periods.length >= 2 ? `${periods[0]} \u2192 ${periods[periods.length - 1]}` : periods[0] ?? "";
7476
+ return [
7477
+ `<line x1="0" y1="${lineY}" x2="${W3}" y2="${lineY}" stroke="${shareTheme.gridLine}" stroke-width="1"/>`,
7478
+ `<text x="60" y="${textY}" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}" font-size="13">llm-usage-metrics</text>`,
7479
+ `<text x="${W3 - 60}" y="${textY}" text-anchor="end" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}" font-size="13">${escapeSvg(range)}</text>`
7480
+ ].join("\n");
7481
+ }
7482
+ function renderUsageShareSvg(usageData, granularity) {
7483
+ const sourceRows = extractPeriodSourceRows(usageData.rows);
7484
+ const grandTotal = extractGrandTotal(usageData.rows);
7485
+ const periods = [...new Set(sourceRows.map((r) => r.periodKey))].sort(compareByCodePoint);
7486
+ const sources = [...new Set(sourceRows.map((r) => r.source))].sort(compareByCodePoint);
7487
+ const allSeries = buildSourceSeries(sourceRows, periods, sources);
7488
+ const activeSeries = allSeries.filter((s) => s.total > 0);
7489
+ const totalTokens = grandTotal?.totalTokens ?? 0;
7490
+ const totalCost = grandTotal?.costUsd;
7491
+ const chartLeft = pad3.left;
7492
+ const chartTop = pad3.top;
7493
+ const chartRight = W3 - pad3.right;
7494
+ const chartBottom = H3 - pad3.bottom;
7495
+ const chartW = chartRight - chartLeft;
7496
+ const chartH = chartBottom - chartTop;
7497
+ const periodCount = periods.length;
7498
+ const stacked = buildStackedValues(activeSeries);
7499
+ const maxY = periodCount > 0 && stacked.length > 0 ? Math.max(1, ...stacked[stacked.length - 1]) * 1.08 : 1;
7500
+ const toX = (p) => chartLeft + (periodCount <= 1 ? chartW / 2 : p / (periodCount - 1) * chartW);
7501
+ const toChartY = (val) => scaleY(val, maxY, chartTop, chartBottom);
7502
+ const commandText = `llm-usage ${granularity} --share`;
7503
+ let chartContent;
7504
+ if (periodCount === 0) {
7505
+ chartContent = `<text x="${(W3 / 2).toFixed(0)}" y="${(H3 / 2).toFixed(0)}" text-anchor="middle" font-size="20" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}">No usage data available</text>`;
7506
+ } else if (periodCount === 1) {
7507
+ chartContent = renderSinglePeriodBars(
7508
+ activeSeries,
7509
+ stacked,
7510
+ toX,
7511
+ toChartY,
7512
+ chartBottom,
7513
+ chartW
7514
+ );
7515
+ } else {
7516
+ chartContent = renderStackedAreas(
7517
+ activeSeries,
7518
+ stacked,
7519
+ periodCount,
7520
+ toX,
7521
+ toChartY,
7522
+ chartBottom
7523
+ );
7524
+ }
7525
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W3}" height="${H3}" viewBox="0 0 ${W3} ${H3}">
7526
+ <defs>
7527
+ <clipPath id="chart-clip">
7528
+ <rect x="${chartLeft}" y="${chartTop - 4}" width="${chartW}" height="${chartH + 8}"/>
7529
+ </clipPath>
7530
+ ${renderGradientDefs(activeSeries)}
7531
+ </defs>
7532
+ <rect width="${W3}" height="${H3}" fill="${shareTheme.bg}"/>
7533
+ ${renderAccentBar()}
7534
+ ${renderStatColumn(totalTokens, totalCost, activeSeries.length)}
7535
+ ${renderSourcePills(activeSeries)}
7536
+ ${renderCommandBadge(commandText)}
7537
+ ${renderGridLines(chartLeft, chartRight, chartTop, chartH, maxY)}
7538
+ ${chartContent}
7539
+ ${renderPeriodLabels(periods, toX, chartBottom)}
7540
+ ${renderFooter(periods)}
7541
+ </svg>`;
7542
+ }
7543
+
6913
7544
  // src/cli/run-usage-report.ts
6914
7545
  function validateOutputFormatOptions3(options) {
6915
7546
  if (options.markdown && options.json) {
@@ -6928,6 +7559,9 @@ function resolveReportFormat3(options) {
6928
7559
  function resolveTableLayout(options) {
6929
7560
  return options.perModelColumns ? "per_model_columns" : "compact";
6930
7561
  }
7562
+ function resolveShareFileName(granularity) {
7563
+ return `usage-${granularity}-share.svg`;
7564
+ }
6931
7565
  async function prepareUsageReport(granularity, options) {
6932
7566
  validateOutputFormatOptions3(options);
6933
7567
  const usageData = await buildUsageData(granularity, options);
@@ -6935,6 +7569,7 @@ async function prepareUsageReport(granularity, options) {
6935
7569
  return {
6936
7570
  format,
6937
7571
  diagnostics: usageData.diagnostics,
7572
+ shareSvg: options.share ? renderUsageShareSvg(usageData, granularity) : void 0,
6938
7573
  output: renderUsageReport(usageData, format, {
6939
7574
  granularity,
6940
7575
  tableLayout: resolveTableLayout(options)
@@ -6950,6 +7585,13 @@ async function runUsageReport(granularity, options) {
6950
7585
  logger.warn(message);
6951
7586
  });
6952
7587
  }
7588
+ if (preparedReport.shareSvg) {
7589
+ const outputPath = await writeShareSvgFile(
7590
+ resolveShareFileName(granularity),
7591
+ preparedReport.shareSvg
7592
+ );
7593
+ logger.info(`Wrote usage share SVG: ${outputPath}`);
7594
+ }
6953
7595
  console.log(preparedReport.output);
6954
7596
  }
6955
7597
 
@@ -6999,6 +7641,9 @@ function addSharedOptions(command, options = {}) {
6999
7641
  "Render per-model metrics as multiline aligned table columns (terminal/markdown)"
7000
7642
  );
7001
7643
  }
7644
+ function addShareOption(command) {
7645
+ return command.option("--share", "Write a share SVG image to the current directory");
7646
+ }
7002
7647
  function commandDescription(granularity) {
7003
7648
  switch (granularity) {
7004
7649
  case "daily":
@@ -7011,7 +7656,7 @@ function commandDescription(granularity) {
7011
7656
  }
7012
7657
  function createCommand(granularity) {
7013
7658
  const command = new Command(granularity);
7014
- addSharedOptions(command).description(commandDescription(granularity)).action(async (options) => {
7659
+ addShareOption(addSharedOptions(command)).description(commandDescription(granularity)).action(async (options) => {
7015
7660
  await runUsageReport(granularity, options);
7016
7661
  });
7017
7662
  return command;
@@ -7025,14 +7670,14 @@ function parseGranularityArgument(value) {
7025
7670
  }
7026
7671
  function createEfficiencyCommand() {
7027
7672
  const command = new Command("efficiency");
7028
- addSharedOptions(command, { includePerModelColumns: false }).argument("<granularity>", "Granularity: daily | weekly | monthly", parseGranularityArgument).option("--repo-dir <path>", "Path to repository for Git outcome metrics").option("--include-merge-commits", "Include merge commits in Git outcome metrics").description("Show efficiency report by correlating usage metrics with local Git outcomes").action(async (granularity, options) => {
7673
+ addShareOption(addSharedOptions(command, { includePerModelColumns: false })).argument("<granularity>", "Granularity: daily | weekly | monthly", parseGranularityArgument).option("--repo-dir <path>", "Path to repository for Git outcome metrics").option("--include-merge-commits", "Include merge commits in Git outcome metrics").description("Show efficiency report by correlating usage metrics with local Git outcomes").action(async (granularity, options) => {
7029
7674
  await runEfficiencyReport(granularity, options);
7030
7675
  });
7031
7676
  return command;
7032
7677
  }
7033
7678
  function createOptimizeCommand() {
7034
7679
  const command = new Command("optimize");
7035
- addSharedOptions(command, { includePerModelColumns: false }).argument("<granularity>", "Granularity: daily | weekly | monthly", parseGranularityArgument).option(
7680
+ addShareOption(addSharedOptions(command, { includePerModelColumns: false })).argument("<granularity>", "Granularity: daily | weekly | monthly", parseGranularityArgument).option(
7036
7681
  "--candidate-model <name>",
7037
7682
  "Candidate model for counterfactual pricing (repeatable or comma-separated)",
7038
7683
  collectRepeatedOption,