opencode-token-tracker 1.0.0 → 1.1.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:
@@ -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/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.1.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"