opencode-token-tracker 1.3.2 → 1.4.0

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
@@ -7,6 +7,7 @@ Real-time token usage and cost tracking plugin for [OpenCode](https://opencode.a
7
7
  ## Features
8
8
 
9
9
  - **Real-time Toast notifications** - See token usage and cost after each AI response
10
+ - **Budget control** - Set daily/weekly/monthly spending limits with warnings
10
11
  - **Session statistics** - Track cumulative usage across your entire session
11
12
  - **CLI statistics tool** - Query usage by day/week/month with breakdowns by model/agent
12
13
  - **Cost calculation** - Automatic cost estimation based on model pricing
@@ -37,6 +38,13 @@ Once installed, you'll see Toast notifications after each AI response:
37
38
  $0.023 | Session: $0.156
38
39
  ```
39
40
 
41
+ When budget limits are configured, you'll see warnings:
42
+
43
+ ```
44
+ ⚠️ Budget exceeded!
45
+ Daily: $5.50/$5.00 (110%)
46
+ ```
47
+
40
48
  When a session becomes idle, you'll see a summary:
41
49
 
42
50
  ```
@@ -44,6 +52,49 @@ Session: 45.2K tokens
44
52
  $0.156 | 8 msgs | 5min
45
53
  ```
46
54
 
55
+ ### Budget Control
56
+
57
+ Set spending limits to avoid unexpected costs:
58
+
59
+ ```bash
60
+ # Check current budget status
61
+ opencode-tokens budget
62
+ ```
63
+
64
+ Example output:
65
+
66
+ ```
67
+ Budget Status
68
+ ══════════════════════════════════════════════════════════════════
69
+
70
+ 🟢 Daily
71
+ $3.50 / $10.00 [███████░░░░░░░░░░░░░] 35%
72
+ Remaining: $6.50
73
+
74
+ 🟡 Weekly
75
+ $42.00 / $50.00 [████████████████░░░░] 84%
76
+ Remaining: $8.00
77
+
78
+ 🟢 Monthly
79
+ $120.00 / $200.00 [████████████░░░░░░░░] 60%
80
+ Remaining: $80.00
81
+
82
+ Legend: 🟢 OK 🟡 Warning (>80%) 🔴 Exceeded
83
+ ```
84
+
85
+ Configure budget in `~/.config/opencode/token-tracker.json`:
86
+
87
+ ```json
88
+ {
89
+ "budget": {
90
+ "daily": 10, // $10 per day
91
+ "weekly": 50, // $50 per week
92
+ "monthly": 200, // $200 per month
93
+ "warnAt": 0.8 // Warn at 80% usage
94
+ }
95
+ }
96
+ ```
97
+
47
98
  ### CLI Statistics
48
99
 
49
100
  Query your token usage from the command line:
@@ -97,6 +148,9 @@ Breakdown options (`--by`):
97
148
  ### Pricing & Config Commands
98
149
 
99
150
  ```bash
151
+ # Check budget status
152
+ opencode-tokens budget
153
+
100
154
  # Show built-in pricing table
101
155
  opencode-tokens pricing
102
156
 
@@ -179,10 +233,30 @@ Create a config file at `~/.config/opencode/token-tracker.json`:
179
233
  "enabled": true,
180
234
  "duration": 3000,
181
235
  "showOnIdle": true
236
+ },
237
+ "budget": {
238
+ "daily": 10,
239
+ "weekly": 50,
240
+ "monthly": 200,
241
+ "warnAt": 0.8
182
242
  }
183
243
  }
184
244
  ```
185
245
 
246
+ ### Budget Settings
247
+
248
+ | Option | Type | Default | Description |
249
+ |--------|------|---------|-------------|
250
+ | `daily` | number | - | Maximum daily spend in USD |
251
+ | `weekly` | number | - | Maximum weekly spend in USD |
252
+ | `monthly` | number | - | Maximum monthly spend in USD |
253
+ | `warnAt` | number | `0.8` | Warning threshold (0-1), e.g., 0.8 = warn at 80% |
254
+
255
+ When you exceed a budget limit:
256
+ - Toast notifications change to warning/error style
257
+ - Use `opencode-tokens budget` to check detailed status
258
+ - Budgets reset at midnight (daily), Monday (weekly), or 1st of month (monthly)
259
+
186
260
  ### Pricing Fields Explained
187
261
 
188
262
  All prices are in **USD per 1 million tokens**:
@@ -404,6 +404,12 @@ function cmdConfig(action) {
404
404
  duration: 3000,
405
405
  showOnIdle: true,
406
406
  },
407
+ budget: {
408
+ daily: 5,
409
+ weekly: 25,
410
+ monthly: 100,
411
+ warnAt: 0.8,
412
+ },
407
413
  };
408
414
  // Add providers as comments/examples
409
415
  for (const provider of providers) {
@@ -418,11 +424,11 @@ function cmdConfig(action) {
418
424
  }
419
425
  // Print explanation first
420
426
  console.log(`
421
- Pricing Configuration Guide
427
+ Configuration Guide
422
428
  ══════════════════════════════════════════════════════════════════
423
429
 
424
- All prices are in USD per 1 MILLION tokens.
425
-
430
+ PRICING (prices in USD per 1 MILLION tokens)
431
+ ────────────────────────────────────────────────────────────────
426
432
  Fields:
427
433
  input Cost for input/prompt tokens sent to the model
428
434
  output Cost for output/completion tokens from the model
@@ -434,7 +440,7 @@ function cmdConfig(action) {
434
440
  { "input": 0, "output": 0 } = Free (subscription or local model)
435
441
 
436
442
  Common scenarios:
437
- - GitHub Copilot, Cursor, etc. → Set to 0 (subscription-based)
443
+ - GitHub Copilot, Cursor, etc. → Set provider to { input: 0, output: 0 }
438
444
  - Local/self-hosted models → Set to 0
439
445
  - Direct API usage → Look up provider's pricing page
440
446
 
@@ -445,6 +451,17 @@ function cmdConfig(action) {
445
451
  - Google: https://ai.google.dev/pricing
446
452
  - Or run: opencode-tokens pricing
447
453
 
454
+ BUDGET CONTROL
455
+ ────────────────────────────────────────────────────────────────
456
+ Set spending limits to avoid unexpected costs:
457
+ daily Maximum spend per day (USD)
458
+ weekly Maximum spend per week (USD)
459
+ monthly Maximum spend per month (USD)
460
+ warnAt Warning threshold (0-1), default 0.8 = 80%
461
+
462
+ When budget is exceeded, you'll see a warning toast.
463
+ Check status anytime with: opencode-tokens budget
464
+
448
465
  ────────────────────────────────────────────────────────────────
449
466
  Example config based on your usage:
450
467
  `);
@@ -485,6 +502,92 @@ function cmdConfig(action) {
485
502
  console.log(` opencode-tokens config generate Create config file`);
486
503
  console.log();
487
504
  }
505
+ function cmdBudget() {
506
+ const config = loadConfig();
507
+ const budget = config.budget;
508
+ if (!budget?.daily && !budget?.weekly && !budget?.monthly) {
509
+ console.log(`
510
+ Budget Status
511
+ ══════════════════════════════════════════════════════════════════
512
+
513
+ No budget configured.
514
+
515
+ To set a budget, add to your config file (${CONFIG_FILE}):
516
+
517
+ {
518
+ "budget": {
519
+ "daily": 5, // $5 per day
520
+ "weekly": 25, // $25 per week (optional)
521
+ "monthly": 100, // $100 per month (optional)
522
+ "warnAt": 0.8 // Warn at 80% usage
523
+ }
524
+ }
525
+
526
+ Run: opencode-tokens config init for more details.
527
+ `);
528
+ return;
529
+ }
530
+ const now = new Date();
531
+ const warnAt = budget.warnAt ?? 0.8;
532
+ console.log(`
533
+ Budget Status
534
+ ══════════════════════════════════════════════════════════════════
535
+ `);
536
+ // Helper to create progress bar
537
+ const progressBar = (pct, width = 20) => {
538
+ const filled = Math.min(Math.round(pct * width), width);
539
+ const empty = width - filled;
540
+ const bar = "█".repeat(filled) + "░".repeat(empty);
541
+ return bar;
542
+ };
543
+ // Helper to get color indicator
544
+ const statusIndicator = (pct) => {
545
+ if (pct >= 1)
546
+ return "🔴";
547
+ if (pct >= warnAt)
548
+ return "🟡";
549
+ return "🟢";
550
+ };
551
+ const entries = loadEntries();
552
+ // Daily budget
553
+ if (budget.daily) {
554
+ const dayStart = getStartOfDay(now);
555
+ const dayEntries = entries.filter(e => e._ts >= dayStart);
556
+ const spent = dayEntries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
557
+ const pct = spent / budget.daily;
558
+ const pctDisplay = Math.round(pct * 100);
559
+ console.log(` ${statusIndicator(pct)} Daily`);
560
+ console.log(` ${formatCost(spent)} / ${formatCost(budget.daily)} [${progressBar(pct)}] ${pctDisplay}%`);
561
+ console.log(` Remaining: ${formatCost(Math.max(0, budget.daily - spent))}`);
562
+ console.log();
563
+ }
564
+ // Weekly budget
565
+ if (budget.weekly) {
566
+ const weekStart = getStartOfWeek(now);
567
+ const weekEntries = entries.filter(e => e._ts >= weekStart);
568
+ const spent = weekEntries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
569
+ const pct = spent / budget.weekly;
570
+ const pctDisplay = Math.round(pct * 100);
571
+ console.log(` ${statusIndicator(pct)} Weekly`);
572
+ console.log(` ${formatCost(spent)} / ${formatCost(budget.weekly)} [${progressBar(pct)}] ${pctDisplay}%`);
573
+ console.log(` Remaining: ${formatCost(Math.max(0, budget.weekly - spent))}`);
574
+ console.log();
575
+ }
576
+ // Monthly budget
577
+ if (budget.monthly) {
578
+ const monthStart = getStartOfMonth(now);
579
+ const monthEntries = entries.filter(e => e._ts >= monthStart);
580
+ const spent = monthEntries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
581
+ const pct = spent / budget.monthly;
582
+ const pctDisplay = Math.round(pct * 100);
583
+ console.log(` ${statusIndicator(pct)} Monthly`);
584
+ console.log(` ${formatCost(spent)} / ${formatCost(budget.monthly)} [${progressBar(pct)}] ${pctDisplay}%`);
585
+ console.log(` Remaining: ${formatCost(Math.max(0, budget.monthly - spent))}`);
586
+ console.log();
587
+ }
588
+ console.log(` Legend: 🟢 OK 🟡 Warning (>${Math.round(warnAt * 100)}%) 🔴 Exceeded`);
589
+ console.log();
590
+ }
488
591
  function cmdHelp() {
489
592
  console.log(`
490
593
  opencode-tokens - Token usage statistics CLI
@@ -494,6 +597,7 @@ function cmdHelp() {
494
597
 
495
598
  Commands:
496
599
  (default) Show usage statistics
600
+ budget Show budget status (daily/weekly/monthly)
497
601
  pricing Show built-in pricing table
498
602
  models Show your used models and their pricing status
499
603
  config Show/generate configuration
@@ -508,6 +612,7 @@ function cmdHelp() {
508
612
 
509
613
  Examples:
510
614
  opencode-tokens # All-time summary
615
+ opencode-tokens budget # Check budget status
511
616
  opencode-tokens today # Today's summary
512
617
  opencode-tokens week --by model # This week, by model
513
618
  opencode-tokens pricing # Show pricing table
@@ -526,6 +631,10 @@ function main() {
526
631
  return;
527
632
  }
528
633
  // Handle subcommands
634
+ if (command === "budget") {
635
+ cmdBudget();
636
+ return;
637
+ }
529
638
  if (command === "pricing") {
530
639
  cmdPricing();
531
640
  return;
package/dist/index.js CHANGED
@@ -13,6 +13,9 @@ const DEFAULT_CONFIG = {
13
13
  duration: 3000,
14
14
  showOnIdle: true,
15
15
  },
16
+ budget: {
17
+ warnAt: 0.8,
18
+ },
16
19
  };
17
20
  // Built-in pricing table (USD per 1M tokens) - Updated 2026-02-05
18
21
  // Sources:
@@ -87,6 +90,7 @@ function loadConfig() {
87
90
  providers: { ...DEFAULT_CONFIG.providers, ...userConfig.providers },
88
91
  models: { ...DEFAULT_CONFIG.models, ...userConfig.models },
89
92
  toast: { ...DEFAULT_CONFIG.toast, ...userConfig.toast },
93
+ budget: { ...DEFAULT_CONFIG.budget, ...userConfig.budget },
90
94
  };
91
95
  }
92
96
  }
@@ -195,6 +199,102 @@ function logJson(data) {
195
199
  const entry = JSON.stringify({ ...data, _ts: Date.now() }) + "\n";
196
200
  appendFileSync(LOG_FILE, entry);
197
201
  }
202
+ function getStartOfDay(date = new Date()) {
203
+ const d = new Date(date);
204
+ d.setHours(0, 0, 0, 0);
205
+ return d.getTime();
206
+ }
207
+ function getStartOfWeek(date = new Date()) {
208
+ const d = new Date(date);
209
+ const day = d.getDay();
210
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Monday as first day
211
+ d.setDate(diff);
212
+ d.setHours(0, 0, 0, 0);
213
+ return d.getTime();
214
+ }
215
+ function getStartOfMonth(date = new Date()) {
216
+ const d = new Date(date);
217
+ d.setDate(1);
218
+ d.setHours(0, 0, 0, 0);
219
+ return d.getTime();
220
+ }
221
+ function loadEntriesSince(since) {
222
+ if (!existsSync(LOG_FILE))
223
+ return [];
224
+ try {
225
+ const content = readFileSync(LOG_FILE, "utf-8");
226
+ const lines = content.trim().split("\n").filter(Boolean);
227
+ const entries = [];
228
+ for (const line of lines) {
229
+ try {
230
+ const entry = JSON.parse(line);
231
+ if (entry.type === "tokens" && entry._ts >= since && entry.cost) {
232
+ entries.push(entry);
233
+ }
234
+ }
235
+ catch { }
236
+ }
237
+ return entries;
238
+ }
239
+ catch {
240
+ return [];
241
+ }
242
+ }
243
+ function calculateSpentSince(since) {
244
+ const entries = loadEntriesSince(since);
245
+ return entries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
246
+ }
247
+ function checkBudgetStatus() {
248
+ const budget = config.budget;
249
+ if (!budget.daily && !budget.weekly && !budget.monthly) {
250
+ return null;
251
+ }
252
+ const warnAt = budget.warnAt ?? 0.8;
253
+ const now = new Date();
254
+ // Check in order: daily -> weekly -> monthly (most restrictive first)
255
+ if (budget.daily) {
256
+ const spent = calculateSpentSince(getStartOfDay(now));
257
+ const percentage = spent / budget.daily;
258
+ return {
259
+ period: "daily",
260
+ spent,
261
+ limit: budget.daily,
262
+ percentage,
263
+ exceeded: percentage >= 1,
264
+ warning: percentage >= warnAt && percentage < 1,
265
+ };
266
+ }
267
+ if (budget.weekly) {
268
+ const spent = calculateSpentSince(getStartOfWeek(now));
269
+ const percentage = spent / budget.weekly;
270
+ return {
271
+ period: "weekly",
272
+ spent,
273
+ limit: budget.weekly,
274
+ percentage,
275
+ exceeded: percentage >= 1,
276
+ warning: percentage >= warnAt && percentage < 1,
277
+ };
278
+ }
279
+ if (budget.monthly) {
280
+ const spent = calculateSpentSince(getStartOfMonth(now));
281
+ const percentage = spent / budget.monthly;
282
+ return {
283
+ period: "monthly",
284
+ spent,
285
+ limit: budget.monthly,
286
+ percentage,
287
+ exceeded: percentage >= 1,
288
+ warning: percentage >= warnAt && percentage < 1,
289
+ };
290
+ }
291
+ return null;
292
+ }
293
+ function formatBudgetMessage(status) {
294
+ const pct = Math.round(status.percentage * 100);
295
+ const periodLabel = status.period.charAt(0).toUpperCase() + status.period.slice(1);
296
+ return `${periodLabel}: ${formatCost(status.spent)}/${formatCost(status.limit)} (${pct}%)`;
297
+ }
198
298
  export const TokenTrackerPlugin = async ({ directory, client }) => {
199
299
  // Load config on plugin init
200
300
  config = loadConfig();
@@ -254,13 +354,30 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
254
354
  // Show toast for this message
255
355
  if (config.toast.enabled) {
256
356
  const totalTokens = input + output;
357
+ // Check budget status
358
+ const budgetStatus = checkBudgetStatus();
359
+ let title = `${formatTokens(totalTokens)} tokens`;
360
+ let message = `${formatCost(cost)} | Session: ${formatCost(stats.totalCost)}`;
361
+ let variant = "info";
362
+ // Add budget warning/alert if applicable
363
+ if (budgetStatus) {
364
+ if (budgetStatus.exceeded) {
365
+ title = `⚠️ Budget exceeded!`;
366
+ message = formatBudgetMessage(budgetStatus);
367
+ variant = "error";
368
+ }
369
+ else if (budgetStatus.warning) {
370
+ message = `${formatCost(cost)} | ${formatBudgetMessage(budgetStatus)}`;
371
+ variant = "warning";
372
+ }
373
+ }
257
374
  try {
258
375
  await client.tui.showToast({
259
376
  body: {
260
- title: `${formatTokens(totalTokens)} tokens`,
261
- message: `${formatCost(cost)} | Session: ${formatCost(stats.totalCost)}`,
262
- variant: "info",
263
- duration: config.toast.duration,
377
+ title,
378
+ message,
379
+ variant,
380
+ duration: budgetStatus?.exceeded ? 5000 : config.toast.duration,
264
381
  },
265
382
  });
266
383
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-token-tracker",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Real-time token usage and cost tracking plugin for OpenCode with Toast notifications and CLI stats",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",