opencode-token-tracker 1.0.0 → 1.2.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
@@ -8,6 +8,7 @@ Real-time token usage and cost tracking plugin for [OpenCode](https://opencode.a
8
8
 
9
9
  - **Real-time Toast notifications** - See token usage and cost after each AI response
10
10
  - **Session statistics** - Track cumulative usage across your entire session
11
+ - **CLI statistics tool** - Query usage by day/week/month with breakdowns by model/agent
11
12
  - **Cost calculation** - Automatic cost estimation based on model pricing
12
13
  - **JSONL logging** - All usage data saved locally for analysis
13
14
  - **Multi-model support** - Claude, GPT, DeepSeek, Gemini, and more
@@ -27,6 +28,8 @@ Restart OpenCode and the plugin will be automatically installed.
27
28
 
28
29
  ## Usage
29
30
 
31
+ ### Toast Notifications
32
+
30
33
  Once installed, you'll see Toast notifications after each AI response:
31
34
 
32
35
  ```
@@ -41,6 +44,56 @@ Session: 45.2K tokens
41
44
  $0.156 | 8 msgs | 5min
42
45
  ```
43
46
 
47
+ ### CLI Statistics
48
+
49
+ Query your token usage from the command line:
50
+
51
+ ```bash
52
+ # All-time summary
53
+ opencode-tokens
54
+
55
+ # Today's usage
56
+ opencode-tokens today
57
+
58
+ # This week's usage with model breakdown
59
+ opencode-tokens week --by model
60
+
61
+ # This month with all breakdowns
62
+ opencode-tokens month --by all
63
+
64
+ # Day-by-day breakdown
65
+ opencode-tokens --by daily
66
+ ```
67
+
68
+ Example output:
69
+
70
+ ```
71
+ Today's Usage
72
+ ──────────────────────────────────────────────────
73
+ Total Tokens: 2.81M
74
+ Input: 2.74M
75
+ Output: 72.9K
76
+ Reasoning: 7.1K
77
+ Cache Read: 12.62M
78
+ Total Cost: $32.93
79
+ Messages: 230
80
+
81
+ By Model
82
+ ─────────────────────────────────────────────────────
83
+ Model Tokens Cost Msgs
84
+ --------------- ---------- ---------- ------
85
+ claude-opus-4.5 2.70M $32.93 206
86
+ deepseek-chat 23.4K $0.0025 6
87
+ gpt-5.2 86.9K $0.0000 18
88
+ ```
89
+
90
+ Breakdown options (`--by`):
91
+ - `model` - Group by model (e.g., claude-opus-4.5)
92
+ - `agent` - Group by agent (e.g., sisyphus, coder)
93
+ - `provider` - Group by provider (e.g., anthropic, openai)
94
+ - `daily` - Show day-by-day breakdown
95
+ - `all` - Show all breakdowns
96
+
44
97
  ## Log Files
45
98
 
46
99
  Token usage is logged to:
@@ -82,12 +135,66 @@ Unknown models use a default pricing estimate.
82
135
 
83
136
  ## Configuration
84
137
 
85
- Currently the plugin works out of the box with no configuration needed.
138
+ Create a config file at `~/.config/opencode/token-tracker.json`:
139
+
140
+ ```json
141
+ {
142
+ "providers": {
143
+ "github-copilot": { "input": 0, "output": 0 }
144
+ },
145
+ "models": {
146
+ "my-custom-model": { "input": 1, "output": 2 }
147
+ },
148
+ "toast": {
149
+ "enabled": true,
150
+ "duration": 3000,
151
+ "showOnIdle": true
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### Pricing Override
157
+
158
+ Pricing is resolved in this order (first match wins):
159
+
160
+ 1. **Provider-level** - Override all models for a provider
161
+ 2. **User model config** - Custom model pricing in config file
162
+ 3. **Built-in pricing** - Default pricing table
163
+ 4. **Fallback** - $1/M input, $4/M output
164
+
165
+ #### Example: Free providers
166
+
167
+ If you're using GitHub Copilot or other subscription-based services, set their cost to $0:
168
+
169
+ ```json
170
+ {
171
+ "providers": {
172
+ "github-copilot": { "input": 0, "output": 0 },
173
+ "cursor": { "input": 0, "output": 0 }
174
+ }
175
+ }
176
+ ```
177
+
178
+ #### Example: Custom model pricing
179
+
180
+ Override or add pricing for specific models:
181
+
182
+ ```json
183
+ {
184
+ "models": {
185
+ "claude-opus-4.5": { "input": 12, "output": 60, "cacheRead": 1.2 },
186
+ "my-local-model": { "input": 0, "output": 0 }
187
+ }
188
+ }
189
+ ```
190
+
191
+ ### Toast Settings
86
192
 
87
- Future versions may support:
88
- - Custom pricing overrides
89
- - Toast display options
90
- - Export formats
193
+ | Option | Type | Default | Description |
194
+ |--------|------|---------|-------------|
195
+ | `enabled` | boolean | `true` | Show toast notifications |
196
+ | `duration` | number | `3000` | Toast display duration (ms) |
197
+ | `showOnIdle` | boolean | `true` | Show session summary on idle |
91
198
 
92
199
  ## Development
93
200
 
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir } from "os";
5
+ const LOG_FILE = join(homedir(), ".config", "opencode", "logs", "token-tracker", "tokens.jsonl");
6
+ // ============================================================================
7
+ // Helpers
8
+ // ============================================================================
9
+ function formatTokens(tokens) {
10
+ if (tokens >= 1_000_000)
11
+ return `${(tokens / 1_000_000).toFixed(2)}M`;
12
+ if (tokens >= 1_000)
13
+ return `${(tokens / 1_000).toFixed(1)}K`;
14
+ return tokens.toString();
15
+ }
16
+ function formatCost(cost) {
17
+ if (cost < 0.01)
18
+ return `$${cost.toFixed(4)}`;
19
+ if (cost < 1)
20
+ return `$${cost.toFixed(3)}`;
21
+ return `$${cost.toFixed(2)}`;
22
+ }
23
+ function padRight(str, len) {
24
+ return str.length >= len ? str : str + " ".repeat(len - str.length);
25
+ }
26
+ function padLeft(str, len) {
27
+ return str.length >= len ? str : " ".repeat(len - str.length) + str;
28
+ }
29
+ function getStartOfDay(date) {
30
+ const d = new Date(date);
31
+ d.setHours(0, 0, 0, 0);
32
+ return d.getTime();
33
+ }
34
+ function getStartOfWeek(date) {
35
+ const d = new Date(date);
36
+ const day = d.getDay();
37
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Monday as start
38
+ d.setDate(diff);
39
+ d.setHours(0, 0, 0, 0);
40
+ return d.getTime();
41
+ }
42
+ function getStartOfMonth(date) {
43
+ const d = new Date(date);
44
+ d.setDate(1);
45
+ d.setHours(0, 0, 0, 0);
46
+ return d.getTime();
47
+ }
48
+ // ============================================================================
49
+ // Data Loading
50
+ // ============================================================================
51
+ function loadEntries(since) {
52
+ if (!existsSync(LOG_FILE)) {
53
+ return [];
54
+ }
55
+ const content = readFileSync(LOG_FILE, "utf-8");
56
+ const lines = content.trim().split("\n").filter(Boolean);
57
+ const entries = [];
58
+ for (const line of lines) {
59
+ try {
60
+ const entry = JSON.parse(line);
61
+ if (entry.type !== "tokens")
62
+ continue;
63
+ if (since && entry._ts < since)
64
+ continue;
65
+ if (!entry.input && !entry.output)
66
+ continue; // Skip empty entries
67
+ entries.push(entry);
68
+ }
69
+ catch {
70
+ // Skip malformed lines
71
+ }
72
+ }
73
+ return entries;
74
+ }
75
+ // ============================================================================
76
+ // Stats Aggregation
77
+ // ============================================================================
78
+ function createEmptyStats() {
79
+ return { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, cost: 0, count: 0 };
80
+ }
81
+ function aggregateStats(entries) {
82
+ const stats = createEmptyStats();
83
+ for (const e of entries) {
84
+ stats.input += e.input ?? 0;
85
+ stats.output += e.output ?? 0;
86
+ stats.reasoning += e.reasoning ?? 0;
87
+ stats.cacheRead += e.cacheRead ?? 0;
88
+ stats.cacheWrite += e.cacheWrite ?? 0;
89
+ stats.cost += e.cost ?? 0;
90
+ stats.count += 1;
91
+ }
92
+ return stats;
93
+ }
94
+ function groupBy(entries, keyFn) {
95
+ const groups = new Map();
96
+ for (const e of entries) {
97
+ const key = keyFn(e);
98
+ if (!groups.has(key)) {
99
+ groups.set(key, createEmptyStats());
100
+ }
101
+ const stats = groups.get(key);
102
+ stats.input += e.input ?? 0;
103
+ stats.output += e.output ?? 0;
104
+ stats.reasoning += e.reasoning ?? 0;
105
+ stats.cacheRead += e.cacheRead ?? 0;
106
+ stats.cacheWrite += e.cacheWrite ?? 0;
107
+ stats.cost += e.cost ?? 0;
108
+ stats.count += 1;
109
+ }
110
+ return groups;
111
+ }
112
+ // ============================================================================
113
+ // Display
114
+ // ============================================================================
115
+ function printSummary(title, stats) {
116
+ const totalTokens = stats.input + stats.output;
117
+ console.log();
118
+ console.log(` ${title}`);
119
+ console.log(` ${"─".repeat(50)}`);
120
+ console.log(` Total Tokens: ${padLeft(formatTokens(totalTokens), 12)}`);
121
+ console.log(` Input: ${padLeft(formatTokens(stats.input), 12)}`);
122
+ console.log(` Output: ${padLeft(formatTokens(stats.output), 12)}`);
123
+ if (stats.reasoning > 0) {
124
+ console.log(` Reasoning: ${padLeft(formatTokens(stats.reasoning), 12)}`);
125
+ }
126
+ console.log(` Cache Read: ${padLeft(formatTokens(stats.cacheRead), 12)}`);
127
+ console.log(` Total Cost: ${padLeft(formatCost(stats.cost), 12)}`);
128
+ console.log(` Messages: ${padLeft(stats.count.toString(), 12)}`);
129
+ console.log();
130
+ }
131
+ function printTable(title, groups, labelHeader) {
132
+ // Sort by cost descending
133
+ const sorted = Array.from(groups.entries()).sort((a, b) => b[1].cost - a[1].cost);
134
+ if (sorted.length === 0) {
135
+ console.log(`\n No data for ${title}\n`);
136
+ return;
137
+ }
138
+ // Calculate column widths
139
+ const labelWidth = Math.max(labelHeader.length, ...sorted.map(([k]) => k.length));
140
+ const tokensWidth = 10;
141
+ const costWidth = 10;
142
+ const countWidth = 6;
143
+ console.log();
144
+ console.log(` ${title}`);
145
+ console.log(` ${"─".repeat(labelWidth + tokensWidth + costWidth + countWidth + 12)}`);
146
+ // Header
147
+ console.log(` ${padRight(labelHeader, labelWidth)} ${padLeft("Tokens", tokensWidth)} ${padLeft("Cost", costWidth)} ${padLeft("Msgs", countWidth)}`);
148
+ console.log(` ${"-".repeat(labelWidth)} ${"-".repeat(tokensWidth)} ${"-".repeat(costWidth)} ${"-".repeat(countWidth)}`);
149
+ // Rows
150
+ for (const [label, stats] of sorted) {
151
+ const totalTokens = stats.input + stats.output;
152
+ console.log(` ${padRight(label, labelWidth)} ${padLeft(formatTokens(totalTokens), tokensWidth)} ${padLeft(formatCost(stats.cost), costWidth)} ${padLeft(stats.count.toString(), countWidth)}`);
153
+ }
154
+ console.log();
155
+ }
156
+ function printDailyBreakdown(entries) {
157
+ const byDay = groupBy(entries, (e) => {
158
+ const date = new Date(e._ts);
159
+ return date.toISOString().slice(0, 10); // YYYY-MM-DD
160
+ });
161
+ // Sort by date descending
162
+ const sorted = Array.from(byDay.entries()).sort((a, b) => b[0].localeCompare(a[0]));
163
+ if (sorted.length === 0) {
164
+ console.log("\n No data\n");
165
+ return;
166
+ }
167
+ const dateWidth = 12;
168
+ const tokensWidth = 10;
169
+ const costWidth = 10;
170
+ const countWidth = 6;
171
+ console.log();
172
+ console.log(` Daily Breakdown`);
173
+ console.log(` ${"─".repeat(dateWidth + tokensWidth + costWidth + countWidth + 12)}`);
174
+ console.log(` ${padRight("Date", dateWidth)} ${padLeft("Tokens", tokensWidth)} ${padLeft("Cost", costWidth)} ${padLeft("Msgs", countWidth)}`);
175
+ console.log(` ${"-".repeat(dateWidth)} ${"-".repeat(tokensWidth)} ${"-".repeat(costWidth)} ${"-".repeat(countWidth)}`);
176
+ for (const [date, stats] of sorted) {
177
+ const totalTokens = stats.input + stats.output;
178
+ console.log(` ${padRight(date, dateWidth)} ${padLeft(formatTokens(totalTokens), tokensWidth)} ${padLeft(formatCost(stats.cost), costWidth)} ${padLeft(stats.count.toString(), countWidth)}`);
179
+ }
180
+ console.log();
181
+ }
182
+ // ============================================================================
183
+ // Commands
184
+ // ============================================================================
185
+ function cmdStats(period, breakdown) {
186
+ const now = new Date();
187
+ let since;
188
+ let title;
189
+ switch (period) {
190
+ case "today":
191
+ since = getStartOfDay(now);
192
+ title = "Today's Usage";
193
+ break;
194
+ case "week":
195
+ since = getStartOfWeek(now);
196
+ title = "This Week's Usage";
197
+ break;
198
+ case "month":
199
+ since = getStartOfMonth(now);
200
+ title = "This Month's Usage";
201
+ break;
202
+ case "all":
203
+ default:
204
+ since = undefined;
205
+ title = "All-Time Usage";
206
+ break;
207
+ }
208
+ const entries = loadEntries(since);
209
+ if (entries.length === 0) {
210
+ console.log(`\n No data for ${title.toLowerCase()}\n`);
211
+ return;
212
+ }
213
+ // Overall summary
214
+ const total = aggregateStats(entries);
215
+ printSummary(title, total);
216
+ // Breakdown
217
+ switch (breakdown) {
218
+ case "model":
219
+ printTable("By Model", groupBy(entries, (e) => e.model ?? "unknown"), "Model");
220
+ break;
221
+ case "agent":
222
+ printTable("By Agent", groupBy(entries, (e) => e.agent ?? "unknown"), "Agent");
223
+ break;
224
+ case "provider":
225
+ printTable("By Provider", groupBy(entries, (e) => e.provider ?? "unknown"), "Provider");
226
+ break;
227
+ case "day":
228
+ case "daily":
229
+ printDailyBreakdown(entries);
230
+ break;
231
+ case "all":
232
+ printTable("By Model", groupBy(entries, (e) => e.model ?? "unknown"), "Model");
233
+ printTable("By Agent", groupBy(entries, (e) => e.agent ?? "unknown"), "Agent");
234
+ printTable("By Provider", groupBy(entries, (e) => e.provider ?? "unknown"), "Provider");
235
+ break;
236
+ }
237
+ }
238
+ function cmdHelp() {
239
+ console.log(`
240
+ opencode-tokens - Token usage statistics CLI
241
+
242
+ Usage:
243
+ opencode-tokens [period] [--by <breakdown>]
244
+
245
+ Period:
246
+ today Show today's usage
247
+ week Show this week's usage
248
+ month Show this month's usage
249
+ all Show all-time usage (default)
250
+
251
+ Breakdown (--by):
252
+ model Group by model (e.g., claude-opus-4.5)
253
+ agent Group by agent (e.g., sisyphus, coder)
254
+ provider Group by provider (e.g., anthropic, openai)
255
+ daily Show day-by-day breakdown
256
+ all Show all breakdowns
257
+
258
+ Examples:
259
+ opencode-tokens # All-time summary
260
+ opencode-tokens today # Today's summary
261
+ opencode-tokens week --by model # This week, grouped by model
262
+ opencode-tokens month --by all # This month, all breakdowns
263
+ opencode-tokens --by daily # All-time, day by day
264
+ `);
265
+ }
266
+ // ============================================================================
267
+ // Main
268
+ // ============================================================================
269
+ function main() {
270
+ const args = process.argv.slice(2);
271
+ if (args.includes("--help") || args.includes("-h")) {
272
+ cmdHelp();
273
+ return;
274
+ }
275
+ // Parse arguments
276
+ let period = "all";
277
+ let breakdown;
278
+ for (let i = 0; i < args.length; i++) {
279
+ const arg = args[i];
280
+ if (arg === "--by" || arg === "-b") {
281
+ breakdown = args[++i];
282
+ }
283
+ else if (["today", "week", "month", "all"].includes(arg)) {
284
+ period = arg;
285
+ }
286
+ }
287
+ cmdStats(period, breakdown);
288
+ }
289
+ main();
package/dist/index.js CHANGED
@@ -1,10 +1,21 @@
1
- import { appendFileSync, existsSync, mkdirSync } from "fs";
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { homedir } from "os";
4
- const LOG_DIR = join(homedir(), ".config", "opencode", "logs", "token-tracker");
4
+ const CONFIG_DIR = join(homedir(), ".config", "opencode");
5
+ const CONFIG_FILE = join(CONFIG_DIR, "token-tracker.json");
6
+ const LOG_DIR = join(CONFIG_DIR, "logs", "token-tracker");
5
7
  const LOG_FILE = join(LOG_DIR, "tokens.jsonl");
6
- // Prices as of 2026-02 (update as needed)
7
- const PRICING = {
8
+ const DEFAULT_CONFIG = {
9
+ providers: {},
10
+ models: {},
11
+ toast: {
12
+ enabled: true,
13
+ duration: 3000,
14
+ showOnIdle: true,
15
+ },
16
+ };
17
+ // Built-in pricing table (USD per 1M tokens) - as of 2026-02
18
+ const BUILTIN_PRICING = {
8
19
  // Anthropic Claude
9
20
  "claude-opus-4.5": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
10
21
  "claude-sonnet-4.5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
@@ -38,21 +49,58 @@ const PRICING = {
38
49
  // Fallback for unknown models
39
50
  "_default": { input: 1, output: 4 },
40
51
  };
41
- function getModelPricing(model) {
42
- // Try exact match first
43
- if (PRICING[model])
44
- return PRICING[model];
45
- // Try partial match (e.g., "claude-opus-4.5" matches "claude-opus-4.5-xxx")
52
+ let config = DEFAULT_CONFIG;
53
+ function loadConfig() {
54
+ try {
55
+ if (existsSync(CONFIG_FILE)) {
56
+ const content = readFileSync(CONFIG_FILE, "utf-8");
57
+ const userConfig = JSON.parse(content);
58
+ return {
59
+ providers: { ...DEFAULT_CONFIG.providers, ...userConfig.providers },
60
+ models: { ...DEFAULT_CONFIG.models, ...userConfig.models },
61
+ toast: { ...DEFAULT_CONFIG.toast, ...userConfig.toast },
62
+ };
63
+ }
64
+ }
65
+ catch (e) {
66
+ // Config parse error - use defaults
67
+ }
68
+ return DEFAULT_CONFIG;
69
+ }
70
+ // ============================================================================
71
+ // Pricing
72
+ // ============================================================================
73
+ function getModelPricing(model, provider) {
74
+ // 1. Check provider-level override first (highest priority)
75
+ if (config.providers[provider]) {
76
+ return config.providers[provider];
77
+ }
78
+ // 2. Check user-defined model pricing
79
+ if (config.models[model]) {
80
+ return config.models[model];
81
+ }
82
+ // 3. Check built-in exact match
83
+ if (BUILTIN_PRICING[model]) {
84
+ return BUILTIN_PRICING[model];
85
+ }
86
+ // 4. Try partial match in user config
46
87
  const modelLower = model.toLowerCase();
47
- for (const [key, pricing] of Object.entries(PRICING)) {
88
+ for (const [key, pricing] of Object.entries(config.models)) {
89
+ if (modelLower.includes(key.toLowerCase())) {
90
+ return pricing;
91
+ }
92
+ }
93
+ // 5. Try partial match in built-in pricing
94
+ for (const [key, pricing] of Object.entries(BUILTIN_PRICING)) {
48
95
  if (key !== "_default" && modelLower.includes(key.toLowerCase())) {
49
96
  return pricing;
50
97
  }
51
98
  }
52
- return PRICING["_default"];
99
+ // 6. Fallback to default
100
+ return BUILTIN_PRICING["_default"];
53
101
  }
54
- function calculateCost(model, input, output, cacheRead = 0, cacheWrite = 0) {
55
- const pricing = getModelPricing(model);
102
+ function calculateCost(model, provider, input, output, cacheRead = 0, cacheWrite = 0) {
103
+ const pricing = getModelPricing(model, provider);
56
104
  // Billable input = total input - cache read (cached tokens are charged at cache rate)
57
105
  const billableInput = Math.max(0, input - cacheRead);
58
106
  const inputCost = (billableInput / 1_000_000) * pricing.input;
@@ -120,7 +168,9 @@ function logJson(data) {
120
168
  appendFileSync(LOG_FILE, entry);
121
169
  }
122
170
  export const TokenTrackerPlugin = async ({ directory, client }) => {
123
- logJson({ type: "init", directory });
171
+ // Load config on plugin init
172
+ config = loadConfig();
173
+ logJson({ type: "init", directory, configLoaded: existsSync(CONFIG_FILE) });
124
174
  return {
125
175
  event: async ({ event }) => {
126
176
  try {
@@ -147,7 +197,7 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
147
197
  return;
148
198
  const model = info.model?.modelID ?? info.modelID ?? "unknown";
149
199
  const provider = info.model?.providerID ?? info.providerID ?? "unknown";
150
- const cost = calculateCost(model, input, output, cacheRead, cacheWrite);
200
+ const cost = calculateCost(model, provider, input, output, cacheRead, cacheWrite);
151
201
  // Update session stats
152
202
  const stats = getOrCreateSessionStats(sessionId);
153
203
  stats.totalInput += input;
@@ -174,21 +224,25 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
174
224
  cost,
175
225
  });
176
226
  // Show toast for this message
177
- const totalTokens = input + output;
178
- try {
179
- await client.tui.showToast({
180
- body: {
181
- title: `${formatTokens(totalTokens)} tokens`,
182
- message: `${formatCost(cost)} | Session: ${formatCost(stats.totalCost)}`,
183
- variant: "info",
184
- duration: 3000,
185
- },
186
- });
227
+ if (config.toast.enabled) {
228
+ const totalTokens = input + output;
229
+ try {
230
+ await client.tui.showToast({
231
+ body: {
232
+ title: `${formatTokens(totalTokens)} tokens`,
233
+ message: `${formatCost(cost)} | Session: ${formatCost(stats.totalCost)}`,
234
+ variant: "info",
235
+ duration: config.toast.duration,
236
+ },
237
+ });
238
+ }
239
+ catch { }
187
240
  }
188
- catch { }
189
241
  }
190
242
  // Handle session idle (show summary)
191
243
  if (event.type === "session.idle") {
244
+ if (!config.toast.enabled || !config.toast.showOnIdle)
245
+ return;
192
246
  const props = event.properties;
193
247
  const sessionId = props?.sessionID;
194
248
  if (!sessionId)
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "opencode-token-tracker",
3
- "version": "1.0.0",
4
- "description": "Real-time token usage and cost tracking plugin for OpenCode with Toast notifications",
3
+ "version": "1.2.0",
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",
7
7
  "type": "module",
8
+ "bin": {
9
+ "opencode-tokens": "dist/bin/opencode-tokens.js"
10
+ },
8
11
  "scripts": {
9
12
  "build": "tsc",
10
13
  "prepublishOnly": "npm run build"