@burn0/burn0 0.1.0 → 0.2.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
@@ -465,13 +465,16 @@ function createDispatcher(mode2, deps) {
465
465
  break;
466
466
  case "dev-cloud":
467
467
  deps.logEvent?.(event);
468
+ deps.writeLedger?.(event);
468
469
  deps.addToBatch?.(event);
469
470
  break;
470
471
  case "prod-cloud":
472
+ deps.logEvent?.(event);
473
+ deps.writeLedger?.(event);
471
474
  deps.addToBatch?.(event);
472
475
  break;
473
476
  case "prod-local":
474
- deps.accumulate?.(event);
477
+ deps.logEvent?.(event);
475
478
  break;
476
479
  case "test-enabled":
477
480
  deps.logEvent?.(event);
@@ -702,114 +705,114 @@ function estimateLocalCost(event) {
702
705
  }
703
706
 
704
707
  // src/transport/logger.ts
705
- var DIM = "\x1B[2m";
706
708
  var RESET = "\x1B[0m";
707
- var CYAN = "\x1B[36m";
708
709
  var GREEN = "\x1B[32m";
709
- var YELLOW = "\x1B[33m";
710
- var WHITE = "\x1B[37m";
711
710
  var BOLD = "\x1B[1m";
712
711
  var ORANGE = "\x1B[38;2;250;93;25m";
713
712
  var GRAY = "\x1B[90m";
714
- var headerPrinted = false;
715
- var sessionTotal = 0;
716
- var eventCount = 0;
717
- function formatTokens(count) {
718
- if (count >= 1e6) return `${(count / 1e6).toFixed(1)}M`;
719
- if (count >= 1e3) return `${(count / 1e3).toFixed(1)}K`;
720
- return count.toString();
721
- }
722
713
  function formatCost(cost) {
723
714
  if (cost >= 1) return `$${cost.toFixed(2)}`;
724
715
  if (cost >= 0.01) return `$${cost.toFixed(4)}`;
725
716
  return `$${cost.toFixed(6)}`;
726
717
  }
727
- function formatCostEstimate(estimate) {
728
- switch (estimate.type) {
729
- case "priced":
730
- return `${GREEN}${formatCost(estimate.cost)}${RESET}`;
731
- case "free":
732
- return `${GRAY}free${RESET}`;
733
- case "no-tokens":
734
- return `${YELLOW}no usage${RESET}`;
735
- case "fixed-tier":
736
- return `${YELLOW}plan?${RESET}`;
737
- case "unknown":
738
- return `${GRAY}untracked${RESET}`;
739
- case "loading":
740
- return `${GRAY}...${RESET}`;
741
- }
742
- }
743
- function printHeader() {
744
- if (headerPrinted) return;
745
- headerPrinted = true;
746
- process.stdout.write(`
747
- `);
748
- process.stdout.write(` ${ORANGE}${BOLD} burn0 ${RESET} ${DIM}live cost tracking${RESET}
749
- `);
750
- process.stdout.write(`
751
- `);
752
- process.stdout.write(` ${GRAY}SERVICE ENDPOINT / MODEL USAGE COST${RESET}
753
- `);
754
- process.stdout.write(` ${GRAY}${"\u2500".repeat(68)}${RESET}
755
- `);
756
- }
757
- function printSessionTotal() {
758
- process.stdout.write(` ${GRAY}${"\u2500".repeat(68)}${RESET}
759
- `);
760
- if (sessionTotal > 0) {
761
- process.stdout.write(` ${GRAY}${eventCount} calls${RESET} ${ORANGE}${BOLD}${formatCost(sessionTotal)}${RESET}
762
- `);
763
- } else {
764
- process.stdout.write(` ${GRAY}${eventCount} calls${RESET} ${GRAY}$0${RESET}
765
- `);
766
- }
767
- process.stdout.write(` ${GRAY}${"\u2500".repeat(68)}${RESET}
768
- `);
769
- }
770
- function formatEventLine(event) {
771
- const service = event.service.length > 15 ? event.service.substring(0, 14) + "." : event.service;
772
- const modelOrEndpoint = event.model ? event.model.length > 29 ? event.model.substring(0, 28) + "." : event.model : event.endpoint.length > 29 ? event.endpoint.substring(0, 28) + "." : event.endpoint;
773
- let usage = "";
774
- if (event.tokens_in !== void 0 && event.tokens_out !== void 0) {
775
- usage = `${formatTokens(event.tokens_in)} \u2192 ${formatTokens(event.tokens_out)}`;
776
- }
777
- const estimate = estimateLocalCost(event);
778
- const costStr = formatCostEstimate(estimate);
779
- return ` ${CYAN}${service.padEnd(16)}${RESET} ${WHITE}${modelOrEndpoint.padEnd(30)}${RESET}${GRAY}${usage.padEnd(15)}${RESET}${costStr}`;
780
- }
781
- function formatProcessSummary(events, uptimeSeconds) {
782
- const services = {};
783
- for (const event of events) {
784
- if (!services[event.service]) services[event.service] = { calls: 0 };
785
- services[event.service].calls++;
786
- if (event.tokens_in !== void 0) services[event.service].tokens_in = (services[event.service].tokens_in ?? 0) + event.tokens_in;
787
- if (event.tokens_out !== void 0) services[event.service].tokens_out = (services[event.service].tokens_out ?? 0) + event.tokens_out;
788
- }
789
- for (const svc of Object.values(services)) {
790
- if (svc.tokens_in === void 0) delete svc.tokens_in;
791
- if (svc.tokens_out === void 0) delete svc.tokens_out;
792
- }
793
- return JSON.stringify({
794
- burn0: "process-summary",
795
- uptime_hours: +(uptimeSeconds / 3600).toFixed(1),
796
- total_calls: events.length,
797
- services,
798
- message: "Add BURN0_API_KEY to see cost breakdowns \u2192 burn0.dev"
799
- });
800
- }
801
- function logEvent(event) {
802
- printHeader();
803
- const estimate = estimateLocalCost(event);
804
- if (estimate.type === "priced" && estimate.cost > 0) {
805
- sessionTotal += estimate.cost;
806
- }
807
- eventCount++;
808
- process.stdout.write(`${formatEventLine(event)}
809
- `);
810
- if (eventCount % 5 === 0) {
811
- printSessionTotal();
718
+ function formatDuration(ms) {
719
+ const seconds = Math.floor(ms / 1e3);
720
+ if (seconds < 60) return `${seconds}s`;
721
+ const minutes = Math.floor(seconds / 60);
722
+ const remainingSeconds = seconds % 60;
723
+ if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
724
+ const hours = Math.floor(minutes / 60);
725
+ const remainingMinutes = minutes % 60;
726
+ return `${hours}h ${remainingMinutes}m`;
727
+ }
728
+ function formatServiceBreakdown(perServiceCosts2, maxWidth) {
729
+ const sorted = Object.entries(perServiceCosts2).filter(([, cost]) => cost > 0).sort((a, b) => b[1] - a[1]);
730
+ if (sorted.length === 0) return "";
731
+ const parts = [];
732
+ let currentWidth = 0;
733
+ let shown = 0;
734
+ for (let i = 0; i < sorted.length && shown < 3; i++) {
735
+ const [name, cost] = sorted[i];
736
+ const part = `${name}: ${formatCost(cost)}`;
737
+ if (currentWidth + part.length + 3 > maxWidth && shown > 0) {
738
+ break;
739
+ }
740
+ parts.push(part);
741
+ currentWidth += part.length + 3;
742
+ shown++;
743
+ }
744
+ const remaining = sorted.length - shown;
745
+ if (remaining > 0) {
746
+ parts.push(`+${remaining} more`);
747
+ }
748
+ return parts.join(" \xB7 ");
749
+ }
750
+ function createTicker(init) {
751
+ let sessionCost = 0;
752
+ let sessionCalls = 0;
753
+ const sessionStartTime = Date.now();
754
+ let todayCost2 = init.todayCost;
755
+ let todayCalls2 = init.todayCalls;
756
+ const perServiceCosts2 = { ...init.perServiceCosts };
757
+ let exitPrinted = false;
758
+ let pricedCalls = 0;
759
+ let lastLineLen = 0;
760
+ function render() {
761
+ if (!process.stderr.isTTY) return;
762
+ if (todayCalls2 === 0) return;
763
+ let content;
764
+ if (pricedCalls === 0 && todayCost2 === 0) {
765
+ content = ` burn0 \u25B8 ${todayCalls2} calls today`;
766
+ } else {
767
+ const breakdown = formatServiceBreakdown(perServiceCosts2, 40);
768
+ const breakdownPart = breakdown ? ` \u2500\u2500 ${breakdown}` : "";
769
+ content = ` burn0 \u25B8 ${formatCost(todayCost2)} today (${todayCalls2} calls)${breakdownPart}`;
770
+ }
771
+ const pad = lastLineLen > content.length ? " ".repeat(lastLineLen - content.length) : "";
772
+ lastLineLen = content.length;
773
+ let colored;
774
+ if (pricedCalls === 0 && todayCost2 === 0) {
775
+ colored = ` ${ORANGE}${BOLD}burn0 \u25B8${RESET} ${GRAY}${todayCalls2} calls today${RESET}`;
776
+ } else {
777
+ const breakdown = formatServiceBreakdown(perServiceCosts2, 40);
778
+ const breakdownPart = breakdown ? ` ${GRAY}\u2500\u2500${RESET} ${breakdown}` : "";
779
+ colored = ` ${ORANGE}${BOLD}burn0 \u25B8${RESET} ${GREEN}${formatCost(todayCost2)}${RESET} ${GRAY}today (${todayCalls2} calls)${RESET}${breakdownPart}`;
780
+ }
781
+ process.stderr.write(`\r${colored}${pad}`);
782
+ }
783
+ function tick(event) {
784
+ const estimate = estimateLocalCost(event);
785
+ todayCalls2++;
786
+ sessionCalls++;
787
+ if (estimate.type === "priced" && estimate.cost > 0) {
788
+ todayCost2 += estimate.cost;
789
+ sessionCost += estimate.cost;
790
+ pricedCalls++;
791
+ perServiceCosts2[event.service] = (perServiceCosts2[event.service] ?? 0) + estimate.cost;
792
+ }
793
+ render();
794
+ }
795
+ function printExitSummary() {
796
+ if (!process.stderr.isTTY) return;
797
+ if (sessionCalls === 0) return;
798
+ if (exitPrinted) return;
799
+ exitPrinted = true;
800
+ const duration = formatDuration(Date.now() - sessionStartTime);
801
+ let line;
802
+ if (pricedCalls === 0 && sessionCost === 0) {
803
+ line = `
804
+ ${ORANGE}${BOLD}burn0 \u25B8${RESET} ${GRAY}session: ${sessionCalls} calls (${duration})${RESET} ${GRAY}\u2500\u2500${RESET} ${GRAY}today: ${todayCalls2} calls${RESET}
805
+ `;
806
+ } else {
807
+ const monthlyEst = todayCost2 > 0 ? formatCost(todayCost2 * 30) : null;
808
+ const projPart = monthlyEst ? ` ${GRAY}\u2500\u2500${RESET} ${GRAY}~${GREEN}${monthlyEst}${RESET}${GRAY}/mo${RESET}` : "";
809
+ line = `
810
+ ${ORANGE}${BOLD}burn0 \u25B8${RESET} ${GRAY}session:${RESET} ${GREEN}${formatCost(sessionCost)}${RESET} ${GRAY}(${sessionCalls} calls, ${duration})${RESET} ${GRAY}\u2500\u2500${RESET} ${GRAY}today:${RESET} ${GREEN}${formatCost(todayCost2)}${RESET}${projPart}
811
+ `;
812
+ }
813
+ process.stderr.write(line);
812
814
  }
815
+ return { tick, printExitSummary };
813
816
  }
814
817
 
815
818
  // src/index.ts
@@ -818,12 +821,36 @@ var apiKey = getApiKey();
818
821
  var mode = detectMode({ isTTY: isTTY(), apiKey });
819
822
  var { track, startSpan, enrichEvent } = createTracker();
820
823
  var originalFetch2 = globalThis.fetch;
821
- if (mode !== "test-disabled") {
824
+ if (mode !== "test-disabled" && mode !== "prod-local") {
822
825
  fetchPricing(BURN0_API_URL, originalFetch2).catch(() => {
823
826
  });
824
827
  }
825
- var accumulatedEvents = [];
826
- var ledger = mode === "dev-local" || mode === "test-enabled" ? new LocalLedger(process.cwd()) : null;
828
+ var ledger = new LocalLedger(process.cwd());
829
+ function getTodayDateStr() {
830
+ const d = /* @__PURE__ */ new Date();
831
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
832
+ }
833
+ var todayCost = 0;
834
+ var todayCalls = 0;
835
+ var perServiceCosts = {};
836
+ try {
837
+ const todayStr = getTodayDateStr();
838
+ const allEvents = ledger.read();
839
+ for (const event of allEvents) {
840
+ const eventDate = new Date(event.timestamp);
841
+ const eventDateStr = `${eventDate.getFullYear()}-${String(eventDate.getMonth() + 1).padStart(2, "0")}-${String(eventDate.getDate()).padStart(2, "0")}`;
842
+ if (eventDateStr === todayStr) {
843
+ todayCalls++;
844
+ const estimate = estimateLocalCost(event);
845
+ if (estimate.type === "priced" && estimate.cost > 0) {
846
+ todayCost += estimate.cost;
847
+ perServiceCosts[event.service] = (perServiceCosts[event.service] ?? 0) + estimate.cost;
848
+ }
849
+ }
850
+ }
851
+ } catch {
852
+ }
853
+ var ticker = createTicker({ todayCost, todayCalls, perServiceCosts });
827
854
  var batch = null;
828
855
  if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
829
856
  batch = new BatchBuffer({
@@ -836,17 +863,20 @@ if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
836
863
  }
837
864
  });
838
865
  }
866
+ var shouldWriteLedger = mode !== "test-disabled" && mode !== "prod-local";
839
867
  var dispatch = createDispatcher(mode, {
840
- logEvent,
841
- writeLedger: ledger ? (e) => ledger.write(e) : void 0,
842
- addToBatch: batch ? (e) => batch.add(e) : void 0,
843
- accumulate: (e) => accumulatedEvents.push(e)
868
+ logEvent: (e) => ticker.tick(e),
869
+ writeLedger: shouldWriteLedger ? (e) => ledger.write(e) : void 0,
870
+ addToBatch: batch ? (e) => batch.add(e) : void 0
844
871
  });
845
872
  var preloaded = checkImportOrder();
846
873
  if (preloaded.length > 0) {
847
- console.warn(`[burn0] Warning: These SDKs were imported before burn0 and may not be tracked: ${preloaded.join(", ")}. Move \`import 'burn0'\` to the top of your entry file.`);
874
+ console.warn(`[burn0] Warning: These SDKs were imported before burn0 and may not be tracked: ${preloaded.join(", ")}. Move \`import '@burn0/burn0'\` to the top of your entry file.`);
848
875
  }
849
- if (canPatch() && mode !== "test-disabled") {
876
+ if (mode === "prod-local") {
877
+ console.warn("[burn0] No API key \u2014 costs not tracked. Get one free at burn0.dev/api");
878
+ }
879
+ if (canPatch() && mode !== "test-disabled" && mode !== "prod-local") {
850
880
  const onEvent = (event) => {
851
881
  const enriched = enrichEvent(event);
852
882
  dispatch(enriched);
@@ -855,25 +885,16 @@ if (canPatch() && mode !== "test-disabled") {
855
885
  patchHttp(onEvent);
856
886
  markPatched();
857
887
  }
858
- if (mode === "prod-local") {
859
- const startTime = Date.now();
860
- process.on("beforeExit", () => {
861
- if (accumulatedEvents.length > 0) {
862
- const uptimeSeconds = (Date.now() - startTime) / 1e3;
863
- console.log(formatProcessSummary(accumulatedEvents, uptimeSeconds));
864
- }
865
- });
866
- }
867
- if (batch) {
868
- const exitFlush = () => {
888
+ var exitHandled = false;
889
+ process.on("exit", () => {
890
+ if (exitHandled) return;
891
+ exitHandled = true;
892
+ if (batch) {
869
893
  batch.flush();
870
894
  batch.destroy();
871
- };
872
- process.on("beforeExit", exitFlush);
873
- process.on("SIGTERM", exitFlush);
874
- process.on("SIGINT", exitFlush);
875
- process.on("SIGHUP", exitFlush);
876
- }
895
+ }
896
+ ticker.printExitSummary();
897
+ });
877
898
  var restore = createRestorer({ unpatchFetch, unpatchHttp, resetGuard });
878
899
  // Annotate the CommonJS export names for ESM import in node:
879
900
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  restore,
3
3
  startSpan,
4
4
  track
5
- } from "./chunk-ZHAS7BCI.mjs";
5
+ } from "./chunk-KKYHE4ZV.mjs";
6
6
  import "./chunk-DJ72YN4C.mjs";
7
7
  export {
8
8
  restore,