@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/README.md CHANGED
@@ -1,2 +1,341 @@
1
- # burn0
2
- Lightweight cost observability for every API call in your stack. Track LLM tokens, database ops, and external services from your terminal.
1
+ <div align="center">
2
+
3
+ <h1>๐Ÿ”ฅ burn0</h1>
4
+
5
+ <h3>Know what your code costs</h3>
6
+
7
+ <p>One import tracks every API call in your stack.<br>
8
+ LLMs, SaaS, infrastructure. See per-request costs in real time.<br><br>
9
+ <strong>The cost observability layer your codebase is missing.</strong></p>
10
+
11
+ <br>
12
+
13
+ <img src="https://img.shields.io/badge/๐Ÿ”ฅ_One_Import-black?style=for-the-badge" alt="One import">&nbsp;
14
+ <img src="https://img.shields.io/badge/๐Ÿ“Š_50+_Services-blue?style=for-the-badge" alt="50+ services">&nbsp;
15
+ <img src="https://img.shields.io/badge/โšก_Sub--ms_Overhead-yellow?style=for-the-badge" alt="Sub-ms overhead">&nbsp;
16
+ <img src="https://img.shields.io/badge/๐Ÿ”“_MIT_Licensed-green?style=for-the-badge" alt="MIT licensed">
17
+
18
+ [![npm version](https://img.shields.io/npm/v/@burn0/burn0.svg?style=flat-square&color=cb3837)](https://npmjs.com/package/@burn0/burn0)
19
+ [![npm downloads](https://img.shields.io/npm/dm/@burn0/burn0.svg?style=flat-square&color=blue)](https://npmjs.com/package/@burn0/burn0)
20
+ [![GitHub stars](https://img.shields.io/github/stars/burn0-dev/burn0?style=flat-square)](https://github.com/burn0-dev/burn0)
21
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178c6?style=flat-square&logo=typescript&logoColor=white)](https://typescriptlang.org)
22
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green?style=flat-square)](LICENSE)
23
+
24
+ <br>
25
+
26
+ [Website](https://burn0.dev) ยท [Docs](https://docs.burn0.dev) ยท [Dashboard](https://burn0.dev/dashboard) ยท [Twitter](https://twitter.com/burn0dev)
27
+
28
+ </div>
29
+
30
+ ---
31
+
32
+ ## The Problem
33
+
34
+ You're running OpenAI, Anthropic, Stripe, Supabase, SendGrid, and a dozen other APIs. Your monthly bill is $2,847 and climbing 340% month-over-month.
35
+
36
+ **You have no idea which feature is burning money.**
37
+
38
+ Observability platforms charge $199/mo. API monitoring tools charge $149/mo. Cost management SaaS charges $99/mo.
39
+
40
+ **burn0 is free. One import. That's it.**
41
+
42
+ ```
43
+ burn0 โ–ธ $4.32 today (47 calls) โ”€โ”€ openai: $3.80 ยท anthropic: $0.52
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Quick Start
49
+
50
+ ```bash
51
+ npm i @burn0/burn0
52
+ ```
53
+
54
+ Add one line to your entry file:
55
+
56
+ ```typescript
57
+ import '@burn0/burn0' // Must be first import
58
+
59
+ import express from 'express'
60
+ import OpenAI from 'openai'
61
+ // ... your app runs exactly the same
62
+ ```
63
+
64
+ That's it. Costs appear in your terminal:
65
+
66
+ ```
67
+ burn0 โ–ธ $0.47 today (12 calls) โ”€โ”€ openai: $0.41 ยท stripe: $0.06
68
+ ```
69
+
70
+ On exit:
71
+
72
+ ```
73
+ burn0 โ–ธ session: $0.47 (12 calls, 4m 22s) โ”€โ”€ today: $14.32 โ”€โ”€ ~$430/mo
74
+ ```
75
+
76
+ ---
77
+
78
+ ## How It Compares
79
+
80
+ | | Observability Platform | API Monitoring | Cost Management SaaS | **burn0** |
81
+ | ------------------- | ---------------------- | -------------- | -------------------- | ---------------------- |
82
+ | **Price** | $199/mo | $149/mo | $99/mo | **Free forever** |
83
+ | **Setup** | SDK + dashboard config | Proxy setup | Manual tagging | **One import** |
84
+ | **Latency** | 5-50ms | 10-100ms | Async | **<1ms** |
85
+ | **Per-feature** | Manual instrumentation | No | Manual | **`burn0.track()`** |
86
+ | **Works locally** | No | No | No | **Yes** |
87
+ | **Open source** | No | No | No | **MIT Licensed** |
88
+ | **Data leaves app** | Always | Always | Always | **Only if you opt in** |
89
+
90
+ **You're spending $526/mo on tools that burn0 replaces for $0.**
91
+
92
+ ---
93
+
94
+ ## CLI
95
+
96
+ ```bash
97
+ # Interactive setup wizard
98
+ npx burn0 init
99
+
100
+ # Cost report (last 7 days)
101
+ npx burn0 report
102
+
103
+ # Today only
104
+ npx burn0 report --today
105
+
106
+ # Run any app with tracking (zero code changes)
107
+ npx burn0 dev -- node app.js
108
+
109
+ # Connect to cloud dashboard
110
+ npx burn0 connect
111
+ ```
112
+
113
+ ### `burn0 report` output
114
+
115
+ ```
116
+ burn0 report โ”€โ”€ last 7 days
117
+
118
+ Total: $12.47 (342 calls)
119
+
120
+ openai $8.32 โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 67%
121
+ anthropic $3.15 โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 25%
122
+ google-gemini $0.85 โ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 7%
123
+ resend $0.15 โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 1%
124
+
125
+ โ”€โ”€ projection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
126
+ ~$53/mo estimated (based on last 7 days)
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Feature Attribution
132
+
133
+ Know exactly which feature burns money:
134
+
135
+ ```typescript
136
+ import { track } from '@burn0/burn0'
137
+
138
+ await track('onboarding', async () => {
139
+ const profile = await ai.generateProfile(user)
140
+ await stripe.createSubscription(user.id)
141
+ await sendWelcomeEmail(user.email)
142
+ })
143
+ ```
144
+
145
+ ```
146
+ burn0 โ–ธ feature "onboarding" โ”€โ”€ $0.47/user
147
+ โ””โ”€ openai $0.39 (83%)
148
+ โ””โ”€ stripe $0.0001 (2%)
149
+ โ””โ”€ sendgrid $0.08 (17%)
150
+ ```
151
+
152
+ Track per user, per feature, per request. No manual tagging. No dashboards to configure.
153
+
154
+ ---
155
+
156
+ ## 50+ Services Supported
157
+
158
+ burn0 auto-detects services from hostnames. Zero configuration.
159
+
160
+ ### AI / LLMs
161
+
162
+ | Service | Detection | Pricing Model |
163
+ |---|---|---|
164
+ | OpenAI | `api.openai.com` | Per-token (exact) |
165
+ | Anthropic | `api.anthropic.com` | Per-token (exact) |
166
+ | Google Gemini | `generativelanguage.googleapis.com` | Per-token (exact) |
167
+ | Mistral | `api.mistral.ai` | Per-token (exact) |
168
+ | Cohere | `api.cohere.ai` | Per-token |
169
+ | Groq | `api.groq.com` | Per-token |
170
+ | Together AI | `api.together.xyz` | Per-token |
171
+ | Perplexity | `api.perplexity.ai` | Per-token |
172
+ | DeepSeek | `api.deepseek.com` | Per-token |
173
+ | Replicate | `api.replicate.com` | Per-second |
174
+ | Fireworks AI | `api.fireworks.ai` | Per-token |
175
+ | AI21 Labs | `api.ai21.com` | Per-token |
176
+ | Pinecone | `*.pinecone.io` | Per-request |
177
+
178
+ ### Pay-per-use APIs
179
+
180
+ | Service | Detection | Pricing Model |
181
+ |---|---|---|
182
+ | Stripe | `api.stripe.com` | Per-transaction |
183
+ | PayPal | `api.paypal.com` | Per-transaction |
184
+ | Plaid | `*.plaid.com` | Per-request |
185
+ | SendGrid | `api.sendgrid.com` | Per-email |
186
+ | Resend | `api.resend.com` | Per-email |
187
+ | Twilio | `api.twilio.com` | Per-message |
188
+ | Vonage | `api.nexmo.com` | Per-message |
189
+ | Algolia | `*.algolia.net` | Per-search |
190
+ | Google Maps | `maps.googleapis.com` | Per-request |
191
+ | Mapbox | `api.mapbox.com` | Per-request |
192
+ | Cloudinary | `api.cloudinary.com` | Per-transform |
193
+ | Sentry | `sentry.io` | Per-event |
194
+ | Segment | `api.segment.io` | Per-event |
195
+ | Mixpanel | `api.mixpanel.com` | Per-event |
196
+
197
+ ### Databases & Infrastructure
198
+
199
+ | Service | Detection | Pricing Model |
200
+ |---|---|---|
201
+ | Supabase | `*.supabase.co` | Per-request |
202
+ | PlanetScale | `*.psdb.cloud` | Per-request |
203
+ | MongoDB Atlas | `*.mongodb.net` | Per-request |
204
+ | Upstash | `*.upstash.io` | Per-request |
205
+ | Neon | `*.neon.tech` | Per-request |
206
+ | Turso | `*.turso.io` | Per-request |
207
+ | Firebase | `*.firebaseio.com` | Per-request |
208
+ | AWS S3 | `*.s3.amazonaws.com` | Per-request |
209
+ | AWS Lambda | `lambda.*.amazonaws.com` | Per-invocation |
210
+ | Vercel | `api.vercel.com` | Per-request |
211
+
212
+ **Unknown APIs are auto-tracked by request count.** Nothing slips through.
213
+
214
+ ---
215
+
216
+ ## How It Works
217
+
218
+ ```
219
+ Your app starts
220
+ โ”‚
221
+ โ”œโ”€ import '@burn0/burn0' patches globalThis.fetch + node:http
222
+ โ”‚
223
+ โ”œโ”€ Every outbound HTTP call is intercepted (zero behavior change)
224
+ โ”‚
225
+ โ”œโ”€ Service identified from hostname (api.openai.com โ†’ OpenAI)
226
+ โ”‚
227
+ โ”œโ”€ Token counts + costs extracted from response metadata
228
+ โ”‚
229
+ โ””โ”€ Costs displayed in terminal + stored in local ledger
230
+ ```
231
+
232
+ 1. **Interception is synchronous** โ€” your request goes out immediately
233
+ 2. **Cost extraction is async** โ€” happens after the response, never blocks
234
+ 3. **Sub-millisecond overhead** โ€” benchmarked, not estimated
235
+ 4. **Never reads content** โ€” only extracts metadata: service, model, tokens, status, latency
236
+ 5. **Never throws** โ€” graceful degradation if anything fails internally
237
+ 6. **ยฑ2% accuracy** โ€” exact token counts from LLM APIs, bundled pricing for SaaS
238
+
239
+ ---
240
+
241
+ ## Two Modes
242
+
243
+ | Mode | API Key | What happens |
244
+ |---|---|---|
245
+ | **Local** (default) | No | Costs in terminal + local ledger. Zero network calls to burn0. |
246
+ | **Cloud** (opt-in) | Yes | Same as local + events sync to dashboard for team visibility. |
247
+
248
+ ### Cloud Dashboard
249
+
250
+ Connect an API key to see costs in the browser:
251
+
252
+ - **Live event feed** โ€” every API call in real-time via SSE
253
+ - **Cost breakdown** โ€” per service, per model, per day
254
+ - **Monthly projection** โ€” estimated monthly spend based on trends
255
+ - **API key management** โ€” create, list, revoke keys
256
+
257
+ ```bash
258
+ # Sign in with GitHub at burn0.dev
259
+ # Create an API key, then:
260
+ npx burn0 connect
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Configuration
266
+
267
+ | Env Variable | Default | Description |
268
+ |---|---|---|
269
+ | `BURN0_API_KEY` | โ€” | API key for cloud mode |
270
+ | `BURN0_API_URL` | `https://api.burn0.dev` | Backend URL |
271
+ | `BURN0_DEBUG` | `false` | Enable debug logging |
272
+ | `BURN0_ENABLE_TEST` | โ€” | Set to `1` to enable in `NODE_ENV=test` |
273
+
274
+ ---
275
+
276
+ ## Works With Everything
277
+
278
+ burn0 works with any Node.js framework. If it makes HTTP calls, burn0 tracks the costs.
279
+
280
+ ```
281
+ Express ยท Next.js ยท Fastify ยท Hono ยท Koa ยท NestJS ยท Remix ยท Nuxt
282
+ ```
283
+
284
+ **Requirements:** Node.js >= 18
285
+
286
+ ---
287
+
288
+ ## Frequently Asked Questions
289
+
290
+ ### Does it slow down my API calls?
291
+
292
+ No. Interception is synchronous but event processing is fully async. burn0 adds sub-millisecond overhead to your API calls.
293
+
294
+ ### Does it send my data anywhere?
295
+
296
+ By default, no. In local mode, costs are logged to your terminal and stored in a local file. Cloud mode (opt-in) ships only metadata โ€” never request/response bodies.
297
+
298
+ ### How accurate are the cost estimates?
299
+
300
+ burn0 extracts exact token counts from LLM API responses. For pay-per-use APIs, it uses bundled pricing data. Accuracy is within ยฑ2%.
301
+
302
+ ### Can I use it in production?
303
+
304
+ Yes. burn0 is designed for production use. It never throws, never adds latency, and gracefully degrades if anything fails internally.
305
+
306
+ ### Is it really free?
307
+
308
+ Yes. burn0 is MIT licensed and free forever. No API key required for local mode. Cloud features (dashboard, team analytics) are available as a paid tier.
309
+
310
+ ---
311
+
312
+ ## Development
313
+
314
+ ```bash
315
+ git clone https://github.com/burn0-dev/burn0.git
316
+ cd burn0
317
+ npm install
318
+ npm run build
319
+ npm test
320
+ ```
321
+
322
+ ---
323
+
324
+ ## Community
325
+
326
+ | Channel | Link |
327
+ |---|---|
328
+ | ๐ŸŒ Website | [burn0.dev](https://burn0.dev) |
329
+ | ๐Ÿ“– Docs | [docs.burn0.dev](https://docs.burn0.dev) |
330
+ | ๐Ÿฆ Twitter | [@burn0dev](https://twitter.com/burn0dev) |
331
+ | ๐Ÿ’ป GitHub | [burn0-dev/burn0](https://github.com/burn0-dev/burn0) |
332
+
333
+ ---
334
+
335
+ <div align="center">
336
+
337
+ **MIT License** ยท Built by the [burn0](https://burn0.dev) team
338
+
339
+ โญ If burn0 saves you money, consider starring the repo.
340
+
341
+ </div>
@@ -404,13 +404,16 @@ function createDispatcher(mode2, deps) {
404
404
  break;
405
405
  case "dev-cloud":
406
406
  deps.logEvent?.(event);
407
+ deps.writeLedger?.(event);
407
408
  deps.addToBatch?.(event);
408
409
  break;
409
410
  case "prod-cloud":
411
+ deps.logEvent?.(event);
412
+ deps.writeLedger?.(event);
410
413
  deps.addToBatch?.(event);
411
414
  break;
412
415
  case "prod-local":
413
- deps.accumulate?.(event);
416
+ deps.logEvent?.(event);
414
417
  break;
415
418
  case "test-enabled":
416
419
  deps.logEvent?.(event);
@@ -641,114 +644,114 @@ function estimateLocalCost(event) {
641
644
  }
642
645
 
643
646
  // src/transport/logger.ts
644
- var DIM = "\x1B[2m";
645
647
  var RESET = "\x1B[0m";
646
- var CYAN = "\x1B[36m";
647
648
  var GREEN = "\x1B[32m";
648
- var YELLOW = "\x1B[33m";
649
- var WHITE = "\x1B[37m";
650
649
  var BOLD = "\x1B[1m";
651
650
  var ORANGE = "\x1B[38;2;250;93;25m";
652
651
  var GRAY = "\x1B[90m";
653
- var headerPrinted = false;
654
- var sessionTotal = 0;
655
- var eventCount = 0;
656
- function formatTokens(count) {
657
- if (count >= 1e6) return `${(count / 1e6).toFixed(1)}M`;
658
- if (count >= 1e3) return `${(count / 1e3).toFixed(1)}K`;
659
- return count.toString();
660
- }
661
652
  function formatCost(cost) {
662
653
  if (cost >= 1) return `$${cost.toFixed(2)}`;
663
654
  if (cost >= 0.01) return `$${cost.toFixed(4)}`;
664
655
  return `$${cost.toFixed(6)}`;
665
656
  }
666
- function formatCostEstimate(estimate) {
667
- switch (estimate.type) {
668
- case "priced":
669
- return `${GREEN}${formatCost(estimate.cost)}${RESET}`;
670
- case "free":
671
- return `${GRAY}free${RESET}`;
672
- case "no-tokens":
673
- return `${YELLOW}no usage${RESET}`;
674
- case "fixed-tier":
675
- return `${YELLOW}plan?${RESET}`;
676
- case "unknown":
677
- return `${GRAY}untracked${RESET}`;
678
- case "loading":
679
- return `${GRAY}...${RESET}`;
680
- }
681
- }
682
- function printHeader() {
683
- if (headerPrinted) return;
684
- headerPrinted = true;
685
- process.stdout.write(`
686
- `);
687
- process.stdout.write(` ${ORANGE}${BOLD} burn0 ${RESET} ${DIM}live cost tracking${RESET}
688
- `);
689
- process.stdout.write(`
690
- `);
691
- process.stdout.write(` ${GRAY}SERVICE ENDPOINT / MODEL USAGE COST${RESET}
692
- `);
693
- process.stdout.write(` ${GRAY}${"\u2500".repeat(68)}${RESET}
694
- `);
695
- }
696
- function printSessionTotal() {
697
- process.stdout.write(` ${GRAY}${"\u2500".repeat(68)}${RESET}
698
- `);
699
- if (sessionTotal > 0) {
700
- process.stdout.write(` ${GRAY}${eventCount} calls${RESET} ${ORANGE}${BOLD}${formatCost(sessionTotal)}${RESET}
701
- `);
702
- } else {
703
- process.stdout.write(` ${GRAY}${eventCount} calls${RESET} ${GRAY}$0${RESET}
704
- `);
705
- }
706
- process.stdout.write(` ${GRAY}${"\u2500".repeat(68)}${RESET}
707
- `);
708
- }
709
- function formatEventLine(event) {
710
- const service = event.service.length > 15 ? event.service.substring(0, 14) + "." : event.service;
711
- 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;
712
- let usage = "";
713
- if (event.tokens_in !== void 0 && event.tokens_out !== void 0) {
714
- usage = `${formatTokens(event.tokens_in)} \u2192 ${formatTokens(event.tokens_out)}`;
715
- }
716
- const estimate = estimateLocalCost(event);
717
- const costStr = formatCostEstimate(estimate);
718
- return ` ${CYAN}${service.padEnd(16)}${RESET} ${WHITE}${modelOrEndpoint.padEnd(30)}${RESET}${GRAY}${usage.padEnd(15)}${RESET}${costStr}`;
719
- }
720
- function formatProcessSummary(events, uptimeSeconds) {
721
- const services = {};
722
- for (const event of events) {
723
- if (!services[event.service]) services[event.service] = { calls: 0 };
724
- services[event.service].calls++;
725
- if (event.tokens_in !== void 0) services[event.service].tokens_in = (services[event.service].tokens_in ?? 0) + event.tokens_in;
726
- if (event.tokens_out !== void 0) services[event.service].tokens_out = (services[event.service].tokens_out ?? 0) + event.tokens_out;
727
- }
728
- for (const svc of Object.values(services)) {
729
- if (svc.tokens_in === void 0) delete svc.tokens_in;
730
- if (svc.tokens_out === void 0) delete svc.tokens_out;
731
- }
732
- return JSON.stringify({
733
- burn0: "process-summary",
734
- uptime_hours: +(uptimeSeconds / 3600).toFixed(1),
735
- total_calls: events.length,
736
- services,
737
- message: "Add BURN0_API_KEY to see cost breakdowns \u2192 burn0.dev"
738
- });
739
- }
740
- function logEvent(event) {
741
- printHeader();
742
- const estimate = estimateLocalCost(event);
743
- if (estimate.type === "priced" && estimate.cost > 0) {
744
- sessionTotal += estimate.cost;
745
- }
746
- eventCount++;
747
- process.stdout.write(`${formatEventLine(event)}
748
- `);
749
- if (eventCount % 5 === 0) {
750
- printSessionTotal();
657
+ function formatDuration(ms) {
658
+ const seconds = Math.floor(ms / 1e3);
659
+ if (seconds < 60) return `${seconds}s`;
660
+ const minutes = Math.floor(seconds / 60);
661
+ const remainingSeconds = seconds % 60;
662
+ if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
663
+ const hours = Math.floor(minutes / 60);
664
+ const remainingMinutes = minutes % 60;
665
+ return `${hours}h ${remainingMinutes}m`;
666
+ }
667
+ function formatServiceBreakdown(perServiceCosts2, maxWidth) {
668
+ const sorted = Object.entries(perServiceCosts2).filter(([, cost]) => cost > 0).sort((a, b) => b[1] - a[1]);
669
+ if (sorted.length === 0) return "";
670
+ const parts = [];
671
+ let currentWidth = 0;
672
+ let shown = 0;
673
+ for (let i = 0; i < sorted.length && shown < 3; i++) {
674
+ const [name, cost] = sorted[i];
675
+ const part = `${name}: ${formatCost(cost)}`;
676
+ if (currentWidth + part.length + 3 > maxWidth && shown > 0) {
677
+ break;
678
+ }
679
+ parts.push(part);
680
+ currentWidth += part.length + 3;
681
+ shown++;
682
+ }
683
+ const remaining = sorted.length - shown;
684
+ if (remaining > 0) {
685
+ parts.push(`+${remaining} more`);
686
+ }
687
+ return parts.join(" \xB7 ");
688
+ }
689
+ function createTicker(init) {
690
+ let sessionCost = 0;
691
+ let sessionCalls = 0;
692
+ const sessionStartTime = Date.now();
693
+ let todayCost2 = init.todayCost;
694
+ let todayCalls2 = init.todayCalls;
695
+ const perServiceCosts2 = { ...init.perServiceCosts };
696
+ let exitPrinted = false;
697
+ let pricedCalls = 0;
698
+ let lastLineLen = 0;
699
+ function render() {
700
+ if (!process.stderr.isTTY) return;
701
+ if (todayCalls2 === 0) return;
702
+ let content;
703
+ if (pricedCalls === 0 && todayCost2 === 0) {
704
+ content = ` burn0 \u25B8 ${todayCalls2} calls today`;
705
+ } else {
706
+ const breakdown = formatServiceBreakdown(perServiceCosts2, 40);
707
+ const breakdownPart = breakdown ? ` \u2500\u2500 ${breakdown}` : "";
708
+ content = ` burn0 \u25B8 ${formatCost(todayCost2)} today (${todayCalls2} calls)${breakdownPart}`;
709
+ }
710
+ const pad = lastLineLen > content.length ? " ".repeat(lastLineLen - content.length) : "";
711
+ lastLineLen = content.length;
712
+ let colored;
713
+ if (pricedCalls === 0 && todayCost2 === 0) {
714
+ colored = ` ${ORANGE}${BOLD}burn0 \u25B8${RESET} ${GRAY}${todayCalls2} calls today${RESET}`;
715
+ } else {
716
+ const breakdown = formatServiceBreakdown(perServiceCosts2, 40);
717
+ const breakdownPart = breakdown ? ` ${GRAY}\u2500\u2500${RESET} ${breakdown}` : "";
718
+ colored = ` ${ORANGE}${BOLD}burn0 \u25B8${RESET} ${GREEN}${formatCost(todayCost2)}${RESET} ${GRAY}today (${todayCalls2} calls)${RESET}${breakdownPart}`;
719
+ }
720
+ process.stderr.write(`\r${colored}${pad}`);
721
+ }
722
+ function tick(event) {
723
+ const estimate = estimateLocalCost(event);
724
+ todayCalls2++;
725
+ sessionCalls++;
726
+ if (estimate.type === "priced" && estimate.cost > 0) {
727
+ todayCost2 += estimate.cost;
728
+ sessionCost += estimate.cost;
729
+ pricedCalls++;
730
+ perServiceCosts2[event.service] = (perServiceCosts2[event.service] ?? 0) + estimate.cost;
731
+ }
732
+ render();
733
+ }
734
+ function printExitSummary() {
735
+ if (!process.stderr.isTTY) return;
736
+ if (sessionCalls === 0) return;
737
+ if (exitPrinted) return;
738
+ exitPrinted = true;
739
+ const duration = formatDuration(Date.now() - sessionStartTime);
740
+ let line;
741
+ if (pricedCalls === 0 && sessionCost === 0) {
742
+ line = `
743
+ ${ORANGE}${BOLD}burn0 \u25B8${RESET} ${GRAY}session: ${sessionCalls} calls (${duration})${RESET} ${GRAY}\u2500\u2500${RESET} ${GRAY}today: ${todayCalls2} calls${RESET}
744
+ `;
745
+ } else {
746
+ const monthlyEst = todayCost2 > 0 ? formatCost(todayCost2 * 30) : null;
747
+ const projPart = monthlyEst ? ` ${GRAY}\u2500\u2500${RESET} ${GRAY}~${GREEN}${monthlyEst}${RESET}${GRAY}/mo${RESET}` : "";
748
+ line = `
749
+ ${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}
750
+ `;
751
+ }
752
+ process.stderr.write(line);
751
753
  }
754
+ return { tick, printExitSummary };
752
755
  }
753
756
 
754
757
  // src/index.ts
@@ -757,12 +760,36 @@ var apiKey = getApiKey();
757
760
  var mode = detectMode({ isTTY: isTTY(), apiKey });
758
761
  var { track, startSpan, enrichEvent } = createTracker();
759
762
  var originalFetch2 = globalThis.fetch;
760
- if (mode !== "test-disabled") {
763
+ if (mode !== "test-disabled" && mode !== "prod-local") {
761
764
  fetchPricing(BURN0_API_URL, originalFetch2).catch(() => {
762
765
  });
763
766
  }
764
- var accumulatedEvents = [];
765
- var ledger = mode === "dev-local" || mode === "test-enabled" ? new LocalLedger(process.cwd()) : null;
767
+ var ledger = new LocalLedger(process.cwd());
768
+ function getTodayDateStr() {
769
+ const d = /* @__PURE__ */ new Date();
770
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
771
+ }
772
+ var todayCost = 0;
773
+ var todayCalls = 0;
774
+ var perServiceCosts = {};
775
+ try {
776
+ const todayStr = getTodayDateStr();
777
+ const allEvents = ledger.read();
778
+ for (const event of allEvents) {
779
+ const eventDate = new Date(event.timestamp);
780
+ const eventDateStr = `${eventDate.getFullYear()}-${String(eventDate.getMonth() + 1).padStart(2, "0")}-${String(eventDate.getDate()).padStart(2, "0")}`;
781
+ if (eventDateStr === todayStr) {
782
+ todayCalls++;
783
+ const estimate = estimateLocalCost(event);
784
+ if (estimate.type === "priced" && estimate.cost > 0) {
785
+ todayCost += estimate.cost;
786
+ perServiceCosts[event.service] = (perServiceCosts[event.service] ?? 0) + estimate.cost;
787
+ }
788
+ }
789
+ }
790
+ } catch {
791
+ }
792
+ var ticker = createTicker({ todayCost, todayCalls, perServiceCosts });
766
793
  var batch = null;
767
794
  if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
768
795
  batch = new BatchBuffer({
@@ -775,17 +802,20 @@ if ((mode === "dev-cloud" || mode === "prod-cloud") && apiKey) {
775
802
  }
776
803
  });
777
804
  }
805
+ var shouldWriteLedger = mode !== "test-disabled" && mode !== "prod-local";
778
806
  var dispatch = createDispatcher(mode, {
779
- logEvent,
780
- writeLedger: ledger ? (e) => ledger.write(e) : void 0,
781
- addToBatch: batch ? (e) => batch.add(e) : void 0,
782
- accumulate: (e) => accumulatedEvents.push(e)
807
+ logEvent: (e) => ticker.tick(e),
808
+ writeLedger: shouldWriteLedger ? (e) => ledger.write(e) : void 0,
809
+ addToBatch: batch ? (e) => batch.add(e) : void 0
783
810
  });
784
811
  var preloaded = checkImportOrder();
785
812
  if (preloaded.length > 0) {
786
- 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.`);
813
+ 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.`);
787
814
  }
788
- if (canPatch() && mode !== "test-disabled") {
815
+ if (mode === "prod-local") {
816
+ console.warn("[burn0] No API key \u2014 costs not tracked. Get one free at burn0.dev/api");
817
+ }
818
+ if (canPatch() && mode !== "test-disabled" && mode !== "prod-local") {
789
819
  const onEvent = (event) => {
790
820
  const enriched = enrichEvent(event);
791
821
  dispatch(enriched);
@@ -794,25 +824,16 @@ if (canPatch() && mode !== "test-disabled") {
794
824
  patchHttp(onEvent);
795
825
  markPatched();
796
826
  }
797
- if (mode === "prod-local") {
798
- const startTime = Date.now();
799
- process.on("beforeExit", () => {
800
- if (accumulatedEvents.length > 0) {
801
- const uptimeSeconds = (Date.now() - startTime) / 1e3;
802
- console.log(formatProcessSummary(accumulatedEvents, uptimeSeconds));
803
- }
804
- });
805
- }
806
- if (batch) {
807
- const exitFlush = () => {
827
+ var exitHandled = false;
828
+ process.on("exit", () => {
829
+ if (exitHandled) return;
830
+ exitHandled = true;
831
+ if (batch) {
808
832
  batch.flush();
809
833
  batch.destroy();
810
- };
811
- process.on("beforeExit", exitFlush);
812
- process.on("SIGTERM", exitFlush);
813
- process.on("SIGINT", exitFlush);
814
- process.on("SIGHUP", exitFlush);
815
- }
834
+ }
835
+ ticker.printExitSummary();
836
+ });
816
837
  var restore = createRestorer({ unpatchFetch, unpatchHttp, resetGuard });
817
838
 
818
839
  export {
@@ -820,4 +841,4 @@ export {
820
841
  startSpan,
821
842
  restore
822
843
  };
823
- //# sourceMappingURL=chunk-ZHAS7BCI.mjs.map
844
+ //# sourceMappingURL=chunk-KKYHE4ZV.mjs.map