llm-usage-metrics 0.4.0 → 0.4.2

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
@@ -2459,6 +2459,214 @@ function createDefaultAdapters(options) {
2459
2459
  return sourceRegistrations.map((source) => source.create(options, sourceDirectoryOverrides));
2460
2460
  }
2461
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}">${escapeSvg(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
+
2462
2670
  // src/utils/time-buckets.ts
2463
2671
  var formatterCache = /* @__PURE__ */ new Map();
2464
2672
  function getDateFormatter(timezone) {
@@ -2783,8 +2991,7 @@ function computeDerivedMetrics(usage, outcomes) {
2783
2991
  return {
2784
2992
  usdPerCommit: costUsd !== void 0 && outcomes.commitCount > 0 ? costUsd / outcomes.commitCount : void 0,
2785
2993
  usdPer1kLinesChanged: costUsd !== void 0 && outcomes.linesChanged > 0 ? costUsd / (outcomes.linesChanged / 1e3) : void 0,
2786
- tokensPerCommit: outcomes.commitCount > 0 ? usage.totalTokens / outcomes.commitCount : void 0,
2787
- nonCacheTokensPerCommit: outcomes.commitCount > 0 ? nonCacheTotalTokens / outcomes.commitCount : void 0,
2994
+ tokensPerCommit: outcomes.commitCount > 0 ? nonCacheTotalTokens / outcomes.commitCount : void 0,
2788
2995
  commitsPerUsd: costUsd !== void 0 && costUsd > 0 ? outcomes.commitCount / costUsd : void 0
2789
2996
  };
2790
2997
  }
@@ -5129,6 +5336,15 @@ function emitEnvVarOverrides(activeEnvOverrides, diagnosticsLogger) {
5129
5336
  }
5130
5337
  }
5131
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
+
5132
5348
  // src/render/table-text-layout.ts
5133
5349
  var ansiEscapePattern = new RegExp(String.raw`\u001B\[[0-9;]*m`, "gu");
5134
5350
  var combiningMarkPattern = /\p{Mark}/u;
@@ -5379,8 +5595,7 @@ var efficiencyTableHeaders = [
5379
5595
  "Cost",
5380
5596
  "$/Commit",
5381
5597
  "$/1k Lines",
5382
- "All Tokens/Commit",
5383
- "Non-Cache/Commit",
5598
+ "Tokens/Commit",
5384
5599
  "Commits/$"
5385
5600
  ];
5386
5601
  var integerFormatter = new Intl.NumberFormat("en-US");
@@ -5400,10 +5615,10 @@ var usdRateFormatter = new Intl.NumberFormat("en-US", {
5400
5615
  minimumFractionDigits: 4,
5401
5616
  maximumFractionDigits: 4
5402
5617
  });
5403
- function formatInteger(value) {
5618
+ function formatInteger2(value) {
5404
5619
  return integerFormatter.format(value);
5405
5620
  }
5406
- function formatUsd(value, options = {}) {
5621
+ function formatUsd2(value, options = {}) {
5407
5622
  if (value === void 0) {
5408
5623
  return "-";
5409
5624
  }
@@ -5417,7 +5632,7 @@ function formatUsdRate(value, options = {}) {
5417
5632
  const formatted = usdRateFormatter.format(value);
5418
5633
  return options.approximate ? `~${formatted}` : formatted;
5419
5634
  }
5420
- function formatDecimal(value, options = {}) {
5635
+ function formatDecimal2(value, options = {}) {
5421
5636
  if (value === void 0) {
5422
5637
  return "-";
5423
5638
  }
@@ -5427,22 +5642,21 @@ function formatDecimal(value, options = {}) {
5427
5642
  function toEfficiencyTableCells(rows) {
5428
5643
  return rows.map((row) => [
5429
5644
  row.periodKey,
5430
- formatInteger(row.commitCount),
5431
- formatInteger(row.linesAdded),
5432
- formatInteger(row.linesDeleted),
5433
- formatInteger(row.linesChanged),
5434
- formatInteger(row.inputTokens),
5435
- formatInteger(row.outputTokens),
5436
- formatInteger(row.reasoningTokens),
5437
- formatInteger(row.cacheReadTokens),
5438
- formatInteger(row.cacheWriteTokens),
5439
- formatInteger(row.totalTokens),
5440
- 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 }),
5441
5656
  formatUsdRate(row.usdPerCommit, { approximate: row.costIncomplete }),
5442
5657
  formatUsdRate(row.usdPer1kLinesChanged, { approximate: row.costIncomplete }),
5443
- formatDecimal(row.tokensPerCommit),
5444
- formatDecimal(row.nonCacheTokensPerCommit),
5445
- formatDecimal(row.commitsPerUsd, { approximate: row.costIncomplete })
5658
+ formatDecimal2(row.tokensPerCommit),
5659
+ formatDecimal2(row.commitsPerUsd, { approximate: row.costIncomplete })
5446
5660
  ]);
5447
5661
  }
5448
5662
 
@@ -5553,7 +5767,7 @@ function formatSource(row) {
5553
5767
  function formatTokenCount(value) {
5554
5768
  return integerFormatter2.format(value ?? 0);
5555
5769
  }
5556
- function formatUsd2(value, options = {}) {
5770
+ function formatUsd3(value, options = {}) {
5557
5771
  if (value === void 0) {
5558
5772
  return "-";
5559
5773
  }
@@ -5588,13 +5802,13 @@ function formatModelMetric(row, selector, formatter, layout) {
5588
5802
  }
5589
5803
  function formatModelCostMetric(row, layout) {
5590
5804
  if (layout !== "per_model_columns" || row.modelBreakdown.length === 0) {
5591
- return formatUsd2(row.costUsd, { incomplete: row.costIncomplete });
5805
+ return formatUsd3(row.costUsd, { incomplete: row.costIncomplete });
5592
5806
  }
5593
5807
  const lines = row.modelBreakdown.map(
5594
- (modelUsage) => formatUsd2(modelUsage.costUsd, { incomplete: modelUsage.costIncomplete })
5808
+ (modelUsage) => formatUsd3(modelUsage.costUsd, { incomplete: modelUsage.costIncomplete })
5595
5809
  );
5596
5810
  if (row.modelBreakdown.length > 1) {
5597
- lines.push(formatUsd2(row.costUsd, { incomplete: row.costIncomplete }));
5811
+ lines.push(formatUsd3(row.costUsd, { incomplete: row.costIncomplete }));
5598
5812
  }
5599
5813
  return lines.join("\n");
5600
5814
  }
@@ -6198,13 +6412,23 @@ function resolveReportFormat(options) {
6198
6412
  }
6199
6413
  return "terminal";
6200
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
+ }
6201
6423
  async function prepareEfficiencyReport(granularity, options) {
6202
6424
  validateOutputFormatOptions(options);
6425
+ validateShareOption(granularity, options);
6203
6426
  const efficiencyData = await buildEfficiencyData(granularity, options);
6204
6427
  const format = resolveReportFormat(options);
6205
6428
  return {
6206
6429
  format,
6207
6430
  diagnostics: efficiencyData.diagnostics,
6431
+ shareSvg: options.share ? renderEfficiencyMonthlyShareSvg(efficiencyData) : void 0,
6208
6432
  output: renderEfficiencyReport(efficiencyData, format, {
6209
6433
  granularity
6210
6434
  })
@@ -6229,9 +6453,141 @@ async function runEfficiencyReport(granularity, options) {
6229
6453
  logger.warn(message);
6230
6454
  });
6231
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
+ }
6232
6463
  console.log(preparedReport.output);
6233
6464
  }
6234
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
+
6235
6591
  // src/render/render-optimize-report.ts
6236
6592
  import { markdownTable as markdownTable2 } from "markdown-table";
6237
6593
  import pc6 from "picocolors";
@@ -6268,14 +6624,14 @@ function getReportTitle2(granularity) {
6268
6624
  return "Monthly Optimize Report";
6269
6625
  }
6270
6626
  }
6271
- function formatUsd3(value, options = {}) {
6627
+ function formatUsd4(value, options = {}) {
6272
6628
  if (value === void 0) {
6273
6629
  return "-";
6274
6630
  }
6275
6631
  const formatted = usdFormatter3.format(value);
6276
6632
  return options.approximate ? `~${formatted}` : formatted;
6277
6633
  }
6278
- function formatPercent(value) {
6634
+ function formatPercent2(value) {
6279
6635
  if (value === void 0) {
6280
6636
  return "-";
6281
6637
  }
@@ -6329,7 +6685,7 @@ function resolveTerminalContextLines(optimizeData, options) {
6329
6685
  lines.push(options.useColor ? pc6.cyan(providerLine) : providerLine);
6330
6686
  if (allBaselineRow) {
6331
6687
  lines.push(
6332
- `ALL baseline cost: ${formatUsd3(allBaselineRow.baselineCostUsd, { approximate: allBaselineRow.baselineCostIncomplete })}`
6688
+ `ALL baseline cost: ${formatUsd4(allBaselineRow.baselineCostUsd, { approximate: allBaselineRow.baselineCostIncomplete })}`
6333
6689
  );
6334
6690
  }
6335
6691
  if (allCandidateRows.length > 0) {
@@ -6341,11 +6697,11 @@ function resolveTerminalContextLines(optimizeData, options) {
6341
6697
  lines.push("ALL best candidate: unavailable (missing baseline or candidate pricing)");
6342
6698
  } else if (bestRow.savingsUsd > 0) {
6343
6699
  lines.push(
6344
- `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)})`
6345
6701
  );
6346
6702
  } else if (bestRow.savingsUsd < 0) {
6347
6703
  lines.push(
6348
- `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)})`
6349
6705
  );
6350
6706
  } else {
6351
6707
  lines.push(`ALL best candidate: ${bestRow.candidateModel} matches baseline cost`);
@@ -6371,20 +6727,20 @@ function toTableCells(optimizeData, options) {
6371
6727
  periodCell,
6372
6728
  styleCandidateCell("BASELINE", "baseline", options.useColor),
6373
6729
  "-",
6374
- formatUsd3(row.baselineCostUsd, { approximate: row.baselineCostIncomplete }),
6730
+ formatUsd4(row.baselineCostUsd, { approximate: row.baselineCostIncomplete }),
6375
6731
  "-",
6376
6732
  "-"
6377
6733
  ];
6378
6734
  return options.includeNotesColumn ? [...baselineCells, "-"] : baselineCells;
6379
6735
  }
6380
- const savingsCell = formatUsd3(row.savingsUsd);
6381
- const savingsPctCell = formatPercent(row.savingsPct);
6736
+ const savingsCell = formatUsd4(row.savingsUsd);
6737
+ const savingsPctCell = formatPercent2(row.savingsPct);
6382
6738
  const notesCell = formatNotes(row.notes);
6383
6739
  const candidateCells = [
6384
6740
  periodCell,
6385
6741
  styleCandidateCell(row.candidateModel, "candidate", options.useColor),
6386
- formatUsd3(row.hypotheticalCostUsd, { approximate: row.hypotheticalCostIncomplete }),
6387
- formatUsd3(baselineRow?.baselineCostUsd, {
6742
+ formatUsd4(row.hypotheticalCostUsd, { approximate: row.hypotheticalCostIncomplete }),
6743
+ formatUsd4(baselineRow?.baselineCostUsd, {
6388
6744
  approximate: baselineRow?.baselineCostIncomplete === true
6389
6745
  }),
6390
6746
  styleDeltaCell2(row.savingsUsd, savingsCell, options.useColor),
@@ -6825,8 +7181,17 @@ function resolveReportFormat2(options) {
6825
7181
  }
6826
7182
  return "terminal";
6827
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
+ }
6828
7192
  async function prepareOptimizeReport(granularity, options) {
6829
7193
  validateOutputFormatOptions2(options);
7194
+ validateShareOption2(granularity, options);
6830
7195
  const optimizeData = await buildOptimizeData(granularity, options);
6831
7196
  const format = resolveReportFormat2(options);
6832
7197
  return {
@@ -6835,6 +7200,7 @@ async function prepareOptimizeReport(granularity, options) {
6835
7200
  candidateCount: optimizeData.rows.filter(
6836
7201
  (row) => row.rowType === "candidate" && row.periodKey === "ALL"
6837
7202
  ).length,
7203
+ shareSvg: options.share ? renderOptimizeMonthlyShareSvg(optimizeData) : void 0,
6838
7204
  output: renderOptimizeReport(optimizeData, format, {
6839
7205
  granularity
6840
7206
  })
@@ -6860,6 +7226,13 @@ async function runOptimizeReport(granularity, options) {
6860
7226
  logger.warn(message);
6861
7227
  });
6862
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
+ }
6863
7236
  console.log(preparedReport.output);
6864
7237
  }
6865
7238
 
@@ -6917,6 +7290,255 @@ function renderUsageReport(usageData, format, options) {
6917
7290
  }
6918
7291
  }
6919
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 `<rect width="${W3}" height="${ACCENT_H3}" fill="url(#accent-grad)"/>`;
7337
+ }
7338
+ function renderStatColumn(totalTokens, costUsd, sourceCount) {
7339
+ const x = 60;
7340
+ const baseY = ACCENT_H3 + 48;
7341
+ let svg = "";
7342
+ svg += `<text x="${x}" y="${baseY}" fill="${shareTheme.textPrimary}" font-family="${shareTheme.font}" font-size="52" font-weight="800">${escapeSvg(formatCompact(totalTokens))}</text>
7343
+ `;
7344
+ 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>
7345
+ `;
7346
+ if (costUsd !== void 0) {
7347
+ svg += `<text x="${x}" y="${baseY + 50}" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}" font-size="22" font-weight="600">${escapeSvg(formatUsd(costUsd))}</text>
7348
+ `;
7349
+ }
7350
+ svg += `<text x="${x}" y="${baseY + 74}" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}" font-size="13">${sourceCount} source${sourceCount !== 1 ? "s" : ""}</text>
7351
+ `;
7352
+ return svg;
7353
+ }
7354
+ function renderSourcePills(series) {
7355
+ let svg = "";
7356
+ let cx = pad3.left + 10;
7357
+ const pillY = ACCENT_H3 + 30;
7358
+ for (const s of series) {
7359
+ const label = `${s.source} ${formatCompact(s.total)}`;
7360
+ const textW = label.length * 8.5;
7361
+ const pillW = textW + 28;
7362
+ const pillH = 30;
7363
+ 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"/>
7364
+ `;
7365
+ svg += `<circle cx="${cx + 14}" cy="${pillY + pillH / 2}" r="4" fill="${s.color}"/>
7366
+ `;
7367
+ svg += `<text x="${cx + 24}" y="${pillY + pillH / 2 + 5}" fill="${shareTheme.textSecondary}" font-family="${shareTheme.font}" font-size="14">${escapeSvg(label)}</text>
7368
+ `;
7369
+ cx += pillW + 10;
7370
+ }
7371
+ return svg;
7372
+ }
7373
+ function renderCommandBadge(command) {
7374
+ const textW = command.length * 9;
7375
+ const badgeW = textW + 28;
7376
+ const badgeH = 30;
7377
+ const x = W3 - 60 - badgeW;
7378
+ const y = ACCENT_H3 + 30;
7379
+ return [
7380
+ `<rect x="${x}" y="${y}" width="${badgeW}" height="${badgeH}" rx="${badgeH / 2}" fill="none" stroke="${shareTheme.cardBorder}" stroke-width="1"/>`,
7381
+ `<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>`
7382
+ ].join("\n");
7383
+ }
7384
+ function renderGridLines(chartLeft, chartRight, chartTop, chartH, maxY) {
7385
+ const gridCount = 4;
7386
+ let svg = "";
7387
+ for (let i = 1; i <= gridCount; i++) {
7388
+ const val = maxY / gridCount * i;
7389
+ const y = chartTop + chartH - i / gridCount * chartH;
7390
+ svg += `<line x1="${chartLeft}" y1="${y.toFixed(2)}" x2="${chartRight}" y2="${y.toFixed(2)}" stroke="${shareTheme.gridLine}" stroke-width="1" stroke-dasharray="4 4"/>
7391
+ `;
7392
+ 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>
7393
+ `;
7394
+ }
7395
+ return svg;
7396
+ }
7397
+ function renderGradientDefs(series) {
7398
+ return series.map(
7399
+ (s, i) => `<linearGradient id="area-grad-${i}" x1="0" y1="0" x2="0" y2="1">
7400
+ <stop offset="0%" stop-color="${s.color}" stop-opacity="0.6"/>
7401
+ <stop offset="100%" stop-color="${s.color}" stop-opacity="0.15"/>
7402
+ </linearGradient>`
7403
+ ).join("\n");
7404
+ }
7405
+ function renderStackedAreas(series, stacked, periodCount, toX, toChartY, chartBottom) {
7406
+ if (periodCount < 2 || series.length === 0) return "";
7407
+ let svg = "";
7408
+ for (let s = series.length - 1; s >= 0; s--) {
7409
+ const topPoints = Array.from({ length: periodCount }, (_, p) => ({
7410
+ x: toX(p),
7411
+ y: toChartY(stacked[s][p])
7412
+ }));
7413
+ const topPath = catmullRom(topPoints, 0.3, chartBottom);
7414
+ let botPath;
7415
+ if (s === 0) {
7416
+ botPath = `L${toX(periodCount - 1).toFixed(2)},${chartBottom} L${toX(0).toFixed(2)},${chartBottom}`;
7417
+ } else {
7418
+ const botPoints = Array.from({ length: periodCount }, (_, p) => ({
7419
+ x: toX(p),
7420
+ y: toChartY(stacked[s - 1][p])
7421
+ })).reverse();
7422
+ botPath = catmullRom(botPoints, 0.3, chartBottom).replace("M", "L");
7423
+ }
7424
+ svg += `<path d="${topPath} ${botPath} Z" fill="url(#area-grad-${s})" clip-path="url(#chart-clip)"/>
7425
+ `;
7426
+ }
7427
+ const totalPoints = Array.from({ length: periodCount }, (_, p) => ({
7428
+ x: toX(p),
7429
+ y: toChartY(stacked[stacked.length - 1][p])
7430
+ }));
7431
+ const topLinePath = catmullRom(totalPoints, 0.3, chartBottom);
7432
+ 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)"/>
7433
+ `;
7434
+ for (const pt of totalPoints) {
7435
+ 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)"/>
7436
+ `;
7437
+ }
7438
+ return svg;
7439
+ }
7440
+ function renderSinglePeriodBars(series, stacked, toX, toChartY, chartBottom, chartW) {
7441
+ let svg = "";
7442
+ const barWidth = Math.min(120, chartW * 0.4);
7443
+ const xCenter = toX(0);
7444
+ for (let s = series.length - 1; s >= 0; s--) {
7445
+ const yTop = toChartY(stacked[s][0]);
7446
+ const yBot = s === 0 ? chartBottom : toChartY(stacked[s - 1][0]);
7447
+ if (yBot - yTop > 0) {
7448
+ 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"/>
7449
+ `;
7450
+ }
7451
+ }
7452
+ return svg;
7453
+ }
7454
+ function renderPeriodLabels(periods, toX, chartBottom) {
7455
+ const periodCount = periods.length;
7456
+ const maxLabels = 12;
7457
+ const labelStep = periodCount <= maxLabels ? 1 : Math.ceil(periodCount / maxLabels);
7458
+ let svg = "";
7459
+ for (let p = 0; p < periodCount; p += labelStep) {
7460
+ 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>
7461
+ `;
7462
+ }
7463
+ return svg;
7464
+ }
7465
+ function renderFooter(periods) {
7466
+ const y = H3 - FOOTER_H3;
7467
+ const lineY = y + 1;
7468
+ const textY = y + FOOTER_H3 / 2 + 5;
7469
+ const range = periods.length >= 2 ? `${periods[0]} \u2192 ${periods[periods.length - 1]}` : periods[0] ?? "";
7470
+ return [
7471
+ `<line x1="0" y1="${lineY}" x2="${W3}" y2="${lineY}" stroke="${shareTheme.gridLine}" stroke-width="1"/>`,
7472
+ `<text x="60" y="${textY}" fill="${shareTheme.textMuted}" font-family="${shareTheme.mono}" font-size="13">llm-usage-metrics</text>`,
7473
+ `<text x="${W3 - 60}" y="${textY}" text-anchor="end" fill="${shareTheme.textMuted}" font-family="${shareTheme.font}" font-size="13">${escapeSvg(range)}</text>`
7474
+ ].join("\n");
7475
+ }
7476
+ function renderUsageShareSvg(usageData, granularity) {
7477
+ const sourceRows = extractPeriodSourceRows(usageData.rows);
7478
+ const grandTotal = extractGrandTotal(usageData.rows);
7479
+ const periods = [...new Set(sourceRows.map((r) => r.periodKey))].sort(compareByCodePoint);
7480
+ const sources = [...new Set(sourceRows.map((r) => r.source))].sort(compareByCodePoint);
7481
+ const allSeries = buildSourceSeries(sourceRows, periods, sources);
7482
+ const activeSeries = allSeries.filter((s) => s.total > 0);
7483
+ const totalTokens = grandTotal?.totalTokens ?? 0;
7484
+ const totalCost = grandTotal?.costUsd;
7485
+ const chartLeft = pad3.left;
7486
+ const chartTop = pad3.top;
7487
+ const chartRight = W3 - pad3.right;
7488
+ const chartBottom = H3 - pad3.bottom;
7489
+ const chartW = chartRight - chartLeft;
7490
+ const chartH = chartBottom - chartTop;
7491
+ const periodCount = periods.length;
7492
+ const stacked = buildStackedValues(activeSeries);
7493
+ const maxY = periodCount > 0 && stacked.length > 0 ? Math.max(1, ...stacked[stacked.length - 1]) * 1.08 : 1;
7494
+ const toX = (p) => chartLeft + (periodCount <= 1 ? chartW / 2 : p / (periodCount - 1) * chartW);
7495
+ const toChartY = (val) => scaleY(val, maxY, chartTop, chartBottom);
7496
+ const commandText = `llm-usage ${granularity} --share`;
7497
+ let chartContent;
7498
+ if (periodCount === 0) {
7499
+ 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>`;
7500
+ } else if (periodCount === 1) {
7501
+ chartContent = renderSinglePeriodBars(
7502
+ activeSeries,
7503
+ stacked,
7504
+ toX,
7505
+ toChartY,
7506
+ chartBottom,
7507
+ chartW
7508
+ );
7509
+ } else {
7510
+ chartContent = renderStackedAreas(
7511
+ activeSeries,
7512
+ stacked,
7513
+ periodCount,
7514
+ toX,
7515
+ toChartY,
7516
+ chartBottom
7517
+ );
7518
+ }
7519
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W3}" height="${H3}" viewBox="0 0 ${W3} ${H3}">
7520
+ <defs>
7521
+ <linearGradient id="accent-grad" x1="0" y1="0" x2="1" y2="0">
7522
+ <stop offset="0%" stop-color="#10b981"/>
7523
+ <stop offset="100%" stop-color="#06b6d4"/>
7524
+ </linearGradient>
7525
+ <clipPath id="chart-clip">
7526
+ <rect x="${chartLeft}" y="${chartTop - 4}" width="${chartW}" height="${chartH + 8}"/>
7527
+ </clipPath>
7528
+ ${renderGradientDefs(activeSeries)}
7529
+ </defs>
7530
+ <rect width="${W3}" height="${H3}" fill="${shareTheme.bg}"/>
7531
+ ${renderAccentBar()}
7532
+ ${renderStatColumn(totalTokens, totalCost, activeSeries.length)}
7533
+ ${renderSourcePills(activeSeries)}
7534
+ ${renderCommandBadge(commandText)}
7535
+ ${renderGridLines(chartLeft, chartRight, chartTop, chartH, maxY)}
7536
+ ${chartContent}
7537
+ ${renderPeriodLabels(periods, toX, chartBottom)}
7538
+ ${renderFooter(periods)}
7539
+ </svg>`;
7540
+ }
7541
+
6920
7542
  // src/cli/run-usage-report.ts
6921
7543
  function validateOutputFormatOptions3(options) {
6922
7544
  if (options.markdown && options.json) {
@@ -6935,6 +7557,9 @@ function resolveReportFormat3(options) {
6935
7557
  function resolveTableLayout(options) {
6936
7558
  return options.perModelColumns ? "per_model_columns" : "compact";
6937
7559
  }
7560
+ function resolveShareFileName(granularity) {
7561
+ return `usage-${granularity}-share.svg`;
7562
+ }
6938
7563
  async function prepareUsageReport(granularity, options) {
6939
7564
  validateOutputFormatOptions3(options);
6940
7565
  const usageData = await buildUsageData(granularity, options);
@@ -6942,6 +7567,7 @@ async function prepareUsageReport(granularity, options) {
6942
7567
  return {
6943
7568
  format,
6944
7569
  diagnostics: usageData.diagnostics,
7570
+ shareSvg: options.share ? renderUsageShareSvg(usageData, granularity) : void 0,
6945
7571
  output: renderUsageReport(usageData, format, {
6946
7572
  granularity,
6947
7573
  tableLayout: resolveTableLayout(options)
@@ -6957,6 +7583,13 @@ async function runUsageReport(granularity, options) {
6957
7583
  logger.warn(message);
6958
7584
  });
6959
7585
  }
7586
+ if (preparedReport.shareSvg) {
7587
+ const outputPath = await writeShareSvgFile(
7588
+ resolveShareFileName(granularity),
7589
+ preparedReport.shareSvg
7590
+ );
7591
+ logger.info(`Wrote usage share SVG: ${outputPath}`);
7592
+ }
6960
7593
  console.log(preparedReport.output);
6961
7594
  }
6962
7595
 
@@ -7006,6 +7639,9 @@ function addSharedOptions(command, options = {}) {
7006
7639
  "Render per-model metrics as multiline aligned table columns (terminal/markdown)"
7007
7640
  );
7008
7641
  }
7642
+ function addShareOption(command) {
7643
+ return command.option("--share", "Write a share SVG image to the current directory");
7644
+ }
7009
7645
  function commandDescription(granularity) {
7010
7646
  switch (granularity) {
7011
7647
  case "daily":
@@ -7018,7 +7654,7 @@ function commandDescription(granularity) {
7018
7654
  }
7019
7655
  function createCommand(granularity) {
7020
7656
  const command = new Command(granularity);
7021
- addSharedOptions(command).description(commandDescription(granularity)).action(async (options) => {
7657
+ addShareOption(addSharedOptions(command)).description(commandDescription(granularity)).action(async (options) => {
7022
7658
  await runUsageReport(granularity, options);
7023
7659
  });
7024
7660
  return command;
@@ -7032,14 +7668,14 @@ function parseGranularityArgument(value) {
7032
7668
  }
7033
7669
  function createEfficiencyCommand() {
7034
7670
  const command = new Command("efficiency");
7035
- 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) => {
7671
+ 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) => {
7036
7672
  await runEfficiencyReport(granularity, options);
7037
7673
  });
7038
7674
  return command;
7039
7675
  }
7040
7676
  function createOptimizeCommand() {
7041
7677
  const command = new Command("optimize");
7042
- addSharedOptions(command, { includePerModelColumns: false }).argument("<granularity>", "Granularity: daily | weekly | monthly", parseGranularityArgument).option(
7678
+ addShareOption(addSharedOptions(command, { includePerModelColumns: false })).argument("<granularity>", "Granularity: daily | weekly | monthly", parseGranularityArgument).option(
7043
7679
  "--candidate-model <name>",
7044
7680
  "Candidate model for counterfactual pricing (repeatable or comma-separated)",
7045
7681
  collectRepeatedOption,