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 +53 -0
- package/dist/bin/opencode-tokens.d.ts +2 -0
- package/dist/bin/opencode-tokens.js +289 -0
- package/package.json +5 -2
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,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.
|
|
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"
|