opencode-token-tracker 1.7.0 → 1.7.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 +46 -5
- package/README.zh-CN.md +44 -5
- package/dist/bin/opencode-tokens.js +437 -63
- package/dist/index.js +2 -3
- package/dist/lib/shared.d.ts +10 -2
- package/dist/lib/shared.js +65 -26
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ If you are building AI-assisted engineering workflows, we strongly recommend ado
|
|
|
33
33
|
|
|
34
34
|
## Installation
|
|
35
35
|
|
|
36
|
+
### Plugin
|
|
37
|
+
|
|
36
38
|
Add to your OpenCode config file (`~/.config/opencode/opencode.json`):
|
|
37
39
|
|
|
38
40
|
```json
|
|
@@ -44,6 +46,36 @@ Add to your OpenCode config file (`~/.config/opencode/opencode.json`):
|
|
|
44
46
|
|
|
45
47
|
Restart OpenCode and the plugin will be automatically installed.
|
|
46
48
|
|
|
49
|
+
This installs the package for OpenCode's plugin runtime. It does not add the
|
|
50
|
+
`opencode-tokens` CLI command to your shell `PATH`.
|
|
51
|
+
|
|
52
|
+
### CLI
|
|
53
|
+
|
|
54
|
+
If you only need to run the CLI occasionally, use it through npm without a
|
|
55
|
+
persistent install:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx -y --package opencode-token-tracker opencode-tokens today
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Equivalent `npm exec` form:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm exec --yes --package opencode-token-tracker -- opencode-tokens today
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If you want `opencode-tokens` to be available as a normal shell command, install
|
|
68
|
+
the package with npm's global bin linking:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install -g opencode-token-tracker
|
|
72
|
+
opencode-tokens today
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If `opencode-tokens: command not found` appears after configuring the plugin,
|
|
76
|
+
the plugin is still installed for OpenCode, but the CLI command has not been
|
|
77
|
+
installed into your shell `PATH`. Use one of the CLI options above.
|
|
78
|
+
|
|
47
79
|
## Usage
|
|
48
80
|
|
|
49
81
|
For an end-to-end setup and verification path, see [walkthrough.md](./walkthrough.md).
|
|
@@ -238,6 +270,9 @@ Config changes are automatically backed up to `token-tracker.json.bak`.
|
|
|
238
270
|
# Check budget status
|
|
239
271
|
opencode-tokens budget
|
|
240
272
|
|
|
273
|
+
# Diagnose plugin config, logs, and pricing fallbacks
|
|
274
|
+
opencode-tokens doctor
|
|
275
|
+
|
|
241
276
|
# Show built-in pricing table
|
|
242
277
|
opencode-tokens pricing
|
|
243
278
|
|
|
@@ -269,7 +304,11 @@ This helps you understand:
|
|
|
269
304
|
- Whether pricing is from built-in table, your config, or default fallback
|
|
270
305
|
- What to add to your config file
|
|
271
306
|
|
|
272
|
-
`config init` is safe for piping because stdout contains only valid JSON and no file is written. `config generate` is the file-writing path: stdout stays empty, guides and status messages go to stderr, the parent directory is created when needed, and an existing config is backed up before overwrite.
|
|
307
|
+
`config init` is safe for piping because stdout contains only valid JSON and no file is written. `config generate` is the file-writing path: stdout stays empty, guides and status messages go to stderr, the parent directory is created when needed, and an existing config is backed up before overwrite. Both commands inspect local logs and pre-fill likely zero-cost providers such as GitHub Copilot, Cursor, and Ollama.
|
|
308
|
+
|
|
309
|
+
`doctor` is a read-only setup check. It reports whether the OpenCode plugin
|
|
310
|
+
entry is present, whether the tracker config and token log exist, the latest log
|
|
311
|
+
record, default-priced models, and the next command to run.
|
|
273
312
|
|
|
274
313
|
## Log Files
|
|
275
314
|
|
|
@@ -375,8 +414,9 @@ All prices are in **USD per 1 million tokens**:
|
|
|
375
414
|
|
|
376
415
|
| Scenario | Config |
|
|
377
416
|
|----------|--------|
|
|
378
|
-
| Subscription service (GitHub Copilot, Cursor) | `{ "input": 0, "output": 0 }` |
|
|
379
|
-
| Free/local
|
|
417
|
+
| Subscription service (GitHub Copilot, Cursor) | Provider override: `{ "input": 0, "output": 0 }` |
|
|
418
|
+
| Free/local provider (Ollama, LM Studio, localhost) | Provider override: `{ "input": 0, "output": 0 }` |
|
|
419
|
+
| Free/local model under a paid provider | Model override: `{ "input": 0, "output": 0 }` |
|
|
380
420
|
| Custom API with known pricing | Look up provider's pricing page |
|
|
381
421
|
|
|
382
422
|
### Pricing Override
|
|
@@ -394,13 +434,14 @@ Exact user config is intentionally checked before built-ins, while broad partial
|
|
|
394
434
|
|
|
395
435
|
#### Example: Free providers
|
|
396
436
|
|
|
397
|
-
If you're using GitHub Copilot or other subscription
|
|
437
|
+
If you're using GitHub Copilot, Ollama, LM Studio, or other subscription/local providers, set their provider cost to $0:
|
|
398
438
|
|
|
399
439
|
```json
|
|
400
440
|
{
|
|
401
441
|
"providers": {
|
|
402
442
|
"github-copilot": { "input": 0, "output": 0 },
|
|
403
|
-
"cursor": { "input": 0, "output": 0 }
|
|
443
|
+
"cursor": { "input": 0, "output": 0 },
|
|
444
|
+
"ollama": { "input": 0, "output": 0 }
|
|
404
445
|
}
|
|
405
446
|
}
|
|
406
447
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
|
|
34
34
|
## 安装
|
|
35
35
|
|
|
36
|
+
### 插件
|
|
37
|
+
|
|
36
38
|
在 OpenCode 配置文件 `~/.config/opencode/opencode.json` 中添加插件:
|
|
37
39
|
|
|
38
40
|
```json
|
|
@@ -44,6 +46,35 @@
|
|
|
44
46
|
|
|
45
47
|
重启 OpenCode 后会自动安装插件。
|
|
46
48
|
|
|
49
|
+
这一步只会让 OpenCode 的插件运行时安装并加载该包,不会把
|
|
50
|
+
`opencode-tokens` CLI 命令加入你的 shell `PATH`。
|
|
51
|
+
|
|
52
|
+
### CLI
|
|
53
|
+
|
|
54
|
+
如果只是偶尔查看统计,可以不做持久安装,直接通过 npm 运行:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx -y --package opencode-token-tracker opencode-tokens today
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
等价的 `npm exec` 写法:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm exec --yes --package opencode-token-tracker -- opencode-tokens today
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
如果希望 `opencode-tokens` 成为普通 shell 命令,需要让 npm 建立全局
|
|
67
|
+
bin 链接:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install -g opencode-token-tracker
|
|
71
|
+
opencode-tokens today
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
如果你已经在 OpenCode 配置里添加了插件,但执行 `opencode-tokens today`
|
|
75
|
+
提示 `opencode-tokens: command not found`,说明插件已供 OpenCode 加载,
|
|
76
|
+
但 CLI 命令还没有安装到 shell `PATH`。请使用上面的任一 CLI 运行方式。
|
|
77
|
+
|
|
47
78
|
## 使用
|
|
48
79
|
|
|
49
80
|
端到端安装、验证与 dogfood 路径见 [walkthrough.md](./walkthrough.md)。
|
|
@@ -243,6 +274,9 @@ opencode-tokens config unset budget.daily
|
|
|
243
274
|
# 查看预算状态
|
|
244
275
|
opencode-tokens budget
|
|
245
276
|
|
|
277
|
+
# 诊断插件配置、日志与默认定价回退
|
|
278
|
+
opencode-tokens doctor
|
|
279
|
+
|
|
246
280
|
# 查看内置定价表
|
|
247
281
|
opencode-tokens pricing
|
|
248
282
|
|
|
@@ -275,7 +309,10 @@ opencode-tokens config generate
|
|
|
275
309
|
- 定价来源是内置表、用户配置还是默认回退
|
|
276
310
|
- 需要在配置文件中补充哪些模型定价
|
|
277
311
|
|
|
278
|
-
`config init` 适合管道重定向,因为 stdout 只包含合法 JSON,且不会写文件。`config generate` 是写文件路径:stdout 保持为空,说明和状态信息输出到 stderr
|
|
312
|
+
`config init` 适合管道重定向,因为 stdout 只包含合法 JSON,且不会写文件。`config generate` 是写文件路径:stdout 保持为空,说明和状态信息输出到 stderr;父目录不存在时会自动创建,覆盖已有配置前会先备份。两个命令都会读取本地日志,并为 GitHub Copilot、Cursor、Ollama 这类疑似零成本 provider 预填覆盖配置。
|
|
313
|
+
|
|
314
|
+
`doctor` 是只读诊断命令,会检查 OpenCode 插件入口、tracker 配置、token log、
|
|
315
|
+
最新记录、默认定价回退模型,并给出下一步应该运行的命令。
|
|
279
316
|
|
|
280
317
|
## 日志文件
|
|
281
318
|
|
|
@@ -367,8 +404,9 @@ token 记录保存在:
|
|
|
367
404
|
|
|
368
405
|
| 场景 | 配置 |
|
|
369
406
|
| --- | --- |
|
|
370
|
-
| 订阅制服务(GitHub Copilot、Cursor) |
|
|
371
|
-
|
|
|
407
|
+
| 订阅制服务(GitHub Copilot、Cursor) | Provider 覆盖:`{ "input": 0, "output": 0 }` |
|
|
408
|
+
| 免费/本地 provider(Ollama、LM Studio、localhost) | Provider 覆盖:`{ "input": 0, "output": 0 }` |
|
|
409
|
+
| 付费 provider 下的免费/本地模型 | Model 覆盖:`{ "input": 0, "output": 0 }` |
|
|
372
410
|
| 自定义 API(已知定价) | 查看 provider 官方定价页 |
|
|
373
411
|
|
|
374
412
|
### 定价优先级
|
|
@@ -386,13 +424,14 @@ token 记录保存在:
|
|
|
386
424
|
|
|
387
425
|
#### 示例:免费 provider
|
|
388
426
|
|
|
389
|
-
使用 GitHub Copilot
|
|
427
|
+
使用 GitHub Copilot、Ollama、LM Studio 等订阅制或本地 provider 时,将 provider 成本设为 $0:
|
|
390
428
|
|
|
391
429
|
```json
|
|
392
430
|
{
|
|
393
431
|
"providers": {
|
|
394
432
|
"github-copilot": { "input": 0, "output": 0 },
|
|
395
|
-
"cursor": { "input": 0, "output": 0 }
|
|
433
|
+
"cursor": { "input": 0, "output": 0 },
|
|
434
|
+
"ollama": { "input": 0, "output": 0 }
|
|
396
435
|
}
|
|
397
436
|
}
|
|
398
437
|
```
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import {
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { BUILTIN_PRICING, BUILTIN_PRICING_META, DEFAULT_CONFIG, formatCost, formatLocalDateKey, formatTokens, getStartOfDay, getStartOfMonth, getStartOfWeek, hasBillableTokenUsage, resolvePricingStatus, round2, validateConfig, } from "../lib/shared.js";
|
|
6
7
|
const CONFIG_DIR = join(homedir(), ".config", "opencode");
|
|
7
8
|
const CONFIG_FILE = join(CONFIG_DIR, "token-tracker.json");
|
|
8
9
|
const LOG_FILE = join(CONFIG_DIR, "logs", "token-tracker", "tokens.jsonl");
|
|
10
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
11
|
+
const OPENCODE_CONFIG_FILES = [
|
|
12
|
+
join(CONFIG_DIR, "opencode.json"),
|
|
13
|
+
join(CONFIG_DIR, "opencode.jsonc"),
|
|
14
|
+
];
|
|
9
15
|
// ============================================================================
|
|
10
16
|
// Helpers
|
|
11
17
|
// ============================================================================
|
|
@@ -20,6 +26,41 @@ function truncateSessionId(sessionId) {
|
|
|
20
26
|
return "unknown";
|
|
21
27
|
return sessionId.length > 16 ? `${sessionId.slice(0, 14)}…` : sessionId;
|
|
22
28
|
}
|
|
29
|
+
function formatAge(ts) {
|
|
30
|
+
const diffMs = Date.now() - ts;
|
|
31
|
+
if (diffMs < 60_000)
|
|
32
|
+
return "less than 1m ago";
|
|
33
|
+
const minutes = Math.floor(diffMs / 60_000);
|
|
34
|
+
if (minutes < 60)
|
|
35
|
+
return `${minutes}m ago`;
|
|
36
|
+
const hours = Math.floor(minutes / 60);
|
|
37
|
+
if (hours < 24)
|
|
38
|
+
return `${hours}h ago`;
|
|
39
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
40
|
+
}
|
|
41
|
+
const ZERO_COST_PROVIDER_HINTS = [
|
|
42
|
+
{ match: "copilot", reason: "subscription/bundled provider" },
|
|
43
|
+
{ match: "cursor", reason: "subscription/bundled provider" },
|
|
44
|
+
{ match: "free", reason: "free provider name" },
|
|
45
|
+
{ match: "ollama", reason: "local/self-hosted provider" },
|
|
46
|
+
{ match: "lmstudio", reason: "local/self-hosted provider" },
|
|
47
|
+
{ match: "lm-studio", reason: "local/self-hosted provider" },
|
|
48
|
+
{ match: "llamacpp", reason: "local/self-hosted provider" },
|
|
49
|
+
{ match: "llama.cpp", reason: "local/self-hosted provider" },
|
|
50
|
+
{ match: "local", reason: "local/self-hosted provider" },
|
|
51
|
+
{ match: "localhost", reason: "local/self-hosted provider" },
|
|
52
|
+
{ match: "self-hosted", reason: "local/self-hosted provider" },
|
|
53
|
+
{ match: "selfhosted", reason: "local/self-hosted provider" },
|
|
54
|
+
];
|
|
55
|
+
function getZeroCostProviderMatch(provider) {
|
|
56
|
+
const normalized = provider.toLowerCase().replace(/\s+/g, "");
|
|
57
|
+
for (const hint of ZERO_COST_PROVIDER_HINTS) {
|
|
58
|
+
if (normalized.includes(hint.match)) {
|
|
59
|
+
return { provider, reason: hint.reason };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
23
64
|
function parseArgs(args) {
|
|
24
65
|
const positional = [];
|
|
25
66
|
const flags = new Map();
|
|
@@ -49,7 +90,7 @@ function parseArgs(args) {
|
|
|
49
90
|
i++;
|
|
50
91
|
continue;
|
|
51
92
|
}
|
|
52
|
-
if (
|
|
93
|
+
if (/^-[A-Za-z]$/.test(arg)) {
|
|
53
94
|
const next = args[i + 1];
|
|
54
95
|
if (next && !next.startsWith("-")) {
|
|
55
96
|
flags.set(arg.slice(1), next);
|
|
@@ -109,7 +150,7 @@ function loadEntries(since) {
|
|
|
109
150
|
shouldStop = true;
|
|
110
151
|
break;
|
|
111
152
|
}
|
|
112
|
-
if (!entry
|
|
153
|
+
if (!hasBillableTokenUsage(entry))
|
|
113
154
|
continue;
|
|
114
155
|
entries.push(entry);
|
|
115
156
|
}
|
|
@@ -122,7 +163,7 @@ function loadEntries(since) {
|
|
|
122
163
|
if (!shouldStop && leftover.trim()) {
|
|
123
164
|
try {
|
|
124
165
|
const entry = JSON.parse(leftover.trim());
|
|
125
|
-
if (entry.type === "tokens" && (!since || entry._ts >= since) && (entry
|
|
166
|
+
if (entry.type === "tokens" && (!since || entry._ts >= since) && hasBillableTokenUsage(entry)) {
|
|
126
167
|
entries.push(entry);
|
|
127
168
|
}
|
|
128
169
|
}
|
|
@@ -233,7 +274,7 @@ function printTable(title, groups, labelHeader) {
|
|
|
233
274
|
function printDailyBreakdown(entries) {
|
|
234
275
|
const byDay = groupBy(entries, (e) => {
|
|
235
276
|
const date = new Date(e._ts);
|
|
236
|
-
return date
|
|
277
|
+
return formatLocalDateKey(date);
|
|
237
278
|
});
|
|
238
279
|
const sorted = Array.from(byDay.entries()).sort((a, b) => b[0].localeCompare(a[0]));
|
|
239
280
|
if (sorted.length === 0) {
|
|
@@ -258,6 +299,38 @@ function printDailyBreakdown(entries) {
|
|
|
258
299
|
// ============================================================================
|
|
259
300
|
// Commands
|
|
260
301
|
// ============================================================================
|
|
302
|
+
const STATS_BREAKDOWNS = ["model", "agent", "provider", "day", "daily", "session", "all"];
|
|
303
|
+
const STATS_PERIODS = ["today", "week", "month", "all"];
|
|
304
|
+
const TOP_LEVEL_COMMANDS = ["budget", "doctor", "pricing", "models", "config", "export", "trend"];
|
|
305
|
+
function isStatsBreakdown(value) {
|
|
306
|
+
return STATS_BREAKDOWNS.includes(value);
|
|
307
|
+
}
|
|
308
|
+
function isStatsPeriod(value) {
|
|
309
|
+
return STATS_PERIODS.includes(value);
|
|
310
|
+
}
|
|
311
|
+
function isTopLevelCommand(value) {
|
|
312
|
+
return TOP_LEVEL_COMMANDS.includes(value);
|
|
313
|
+
}
|
|
314
|
+
function getStatsBreakdown(flags) {
|
|
315
|
+
const longValue = flagValue(flags, "by");
|
|
316
|
+
const shortValue = flagValue(flags, "b");
|
|
317
|
+
if (flags.has("by") && !longValue) {
|
|
318
|
+
failCli("Missing value for --by", "Usage: opencode-tokens [today|week|month|all] --by model|agent|provider|daily|session|all");
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
if (flags.has("b") && !shortValue) {
|
|
322
|
+
failCli("Missing value for -b", "Usage: opencode-tokens [today|week|month|all] -b model|agent|provider|daily|session|all");
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
const breakdown = longValue ?? shortValue;
|
|
326
|
+
if (!breakdown)
|
|
327
|
+
return undefined;
|
|
328
|
+
if (!isStatsBreakdown(breakdown)) {
|
|
329
|
+
failCli(`Unsupported stats breakdown: ${breakdown}`, "Allowed breakdowns: model, agent, provider, daily, day, session, all");
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
return breakdown;
|
|
333
|
+
}
|
|
261
334
|
function cmdStats(period, breakdown) {
|
|
262
335
|
const now = new Date();
|
|
263
336
|
let since;
|
|
@@ -322,12 +395,12 @@ function cmdPricing() {
|
|
|
322
395
|
`);
|
|
323
396
|
// Group by provider
|
|
324
397
|
const groups = {
|
|
325
|
-
"Anthropic Claude": ["claude-opus-4.6", "claude-opus-4.5", "claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-4.5", "claude-haiku-4", "claude-opus-4.1", "claude-opus-4", "claude-haiku-3"],
|
|
326
|
-
"OpenAI": ["gpt-5.2", "gpt-5.2-pro", "gpt-5-mini", "gpt-5.1", "gpt-5", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "gpt-4o", "gpt-4o-mini", "o3", "o3-mini", "o4-mini", "o1", "o1-mini"],
|
|
327
|
-
"DeepSeek": ["deepseek-chat", "deepseek-reasoner"],
|
|
328
|
-
"Google Gemini": ["gemini-3-pro", "gemini-3-pro-preview", "gemini-3-flash", "gemini-3-flash-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.0-flash", "gemini-2.0-flash-lite"],
|
|
398
|
+
"Anthropic Claude": ["claude-opus-4.8", "claude-opus-4.7", "claude-opus-4.6", "claude-opus-4.5", "claude-sonnet-4.6", "claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-4.5", "claude-haiku-4", "claude-opus-4.1", "claude-opus-4", "claude-haiku-3"],
|
|
399
|
+
"OpenAI": ["gpt-5.5", "gpt-5.5-pro", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.4-pro", "gpt-5.3-codex", "gpt-5.3-chat-latest", "gpt-5.2", "gpt-5.2-pro", "gpt-5-mini", "gpt-5-nano", "gpt-5.1", "gpt-5.1-chat-latest", "gpt-5.1-codex-max", "gpt-5.1-codex", "gpt-5.1-codex-mini", "gpt-5", "gpt-5-chat-latest", "gpt-5-codex", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "gpt-4o", "gpt-4o-mini", "o3", "o3-mini", "o4-mini", "o1", "o1-mini"],
|
|
400
|
+
"DeepSeek": ["deepseek-chat", "deepseek-reasoner", "deepseek-v4-pro"],
|
|
401
|
+
"Google Gemini": ["gemini-3.1-pro-preview", "gemini-3-pro", "gemini-3-pro-preview", "gemini-3.5-flash", "gemini-3-flash", "gemini-3-flash-preview", "gemini-3.1-flash-lite", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.0-flash", "gemini-2.0-flash-lite"],
|
|
329
402
|
};
|
|
330
|
-
const modelWidth =
|
|
403
|
+
const modelWidth = 24;
|
|
331
404
|
const priceWidth = 10;
|
|
332
405
|
for (const [group, models] of Object.entries(groups)) {
|
|
333
406
|
console.log(` ${group}`);
|
|
@@ -392,8 +465,19 @@ function cmdModels() {
|
|
|
392
465
|
const statusWidth = 12;
|
|
393
466
|
console.log(` ${padRight("Model", modelWidth)} ${padRight("Provider", providerWidth)} ${padLeft("Msgs", countWidth)} ${padRight("Pricing", statusWidth)}`);
|
|
394
467
|
console.log(` ${"-".repeat(modelWidth)} ${"-".repeat(providerWidth)} ${"-".repeat(countWidth)} ${"-".repeat(statusWidth)}`);
|
|
468
|
+
const defaultModels = [];
|
|
469
|
+
const zeroCostProviderModels = [];
|
|
395
470
|
for (const { model, provider, count } of sorted) {
|
|
396
471
|
const status = resolvePricingStatus(config, model, provider);
|
|
472
|
+
if (status === "default") {
|
|
473
|
+
const zeroCostMatch = getZeroCostProviderMatch(provider);
|
|
474
|
+
if (zeroCostMatch) {
|
|
475
|
+
zeroCostProviderModels.push({ model, provider, count, reason: zeroCostMatch.reason });
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
defaultModels.push({ model, provider, count });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
397
481
|
console.log(` ${padRight(model, modelWidth)} ${padRight(provider, providerWidth)} ${padLeft(count.toString(), countWidth)} ${padRight(status, statusWidth)}`);
|
|
398
482
|
}
|
|
399
483
|
console.log();
|
|
@@ -403,6 +487,194 @@ function cmdModels() {
|
|
|
403
487
|
console.log(` model cfg = overridden by models config`);
|
|
404
488
|
console.log(` default = unknown model, using $1/$4 per 1M tokens`);
|
|
405
489
|
console.log();
|
|
490
|
+
if (zeroCostProviderModels.length > 0 || defaultModels.length > 0) {
|
|
491
|
+
console.log(` Next steps for default pricing:`);
|
|
492
|
+
const total = zeroCostProviderModels.length + defaultModels.length;
|
|
493
|
+
console.log(` ${total} model/provider ${total === 1 ? "pair uses" : "pairs use"} the default $1/$4 estimate.`);
|
|
494
|
+
console.log(` Run: opencode-tokens config init`);
|
|
495
|
+
console.log(` Or: opencode-tokens config generate`);
|
|
496
|
+
console.log();
|
|
497
|
+
if (zeroCostProviderModels.length > 0) {
|
|
498
|
+
console.log(` Likely zero-cost provider overrides to review:`);
|
|
499
|
+
for (const { provider, reason } of dedupeZeroCostProviderMatches(zeroCostProviderModels).slice(0, 5)) {
|
|
500
|
+
console.log(` - ${provider} (${reason}): { "input": 0, "output": 0 }`);
|
|
501
|
+
}
|
|
502
|
+
if (zeroCostProviderModels.length > 5) {
|
|
503
|
+
console.log(` - ...and ${zeroCostProviderModels.length - 5} more model/provider pairs`);
|
|
504
|
+
}
|
|
505
|
+
console.log();
|
|
506
|
+
}
|
|
507
|
+
if (defaultModels.length > 0) {
|
|
508
|
+
console.log(` Model overrides to review before saving:`);
|
|
509
|
+
for (const { model, provider, count } of defaultModels.slice(0, 5)) {
|
|
510
|
+
console.log(` - ${model} (${provider}, ${count} msgs): { "input": 1, "output": 4 }`);
|
|
511
|
+
}
|
|
512
|
+
if (defaultModels.length > 5) {
|
|
513
|
+
console.log(` - ...and ${defaultModels.length - 5} more`);
|
|
514
|
+
}
|
|
515
|
+
console.log();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function cmdDoctor() {
|
|
520
|
+
const entries = loadEntries();
|
|
521
|
+
const configDiagnostics = inspectTrackerConfig();
|
|
522
|
+
const pluginDiagnostics = inspectOpenCodePluginConfig();
|
|
523
|
+
console.log(`
|
|
524
|
+
OpenCode Token Tracker Doctor
|
|
525
|
+
══════════════════════════════════════════════════════════════════
|
|
526
|
+
`);
|
|
527
|
+
console.log(` CLI`);
|
|
528
|
+
console.log(` Entry: ${process.argv[1] ?? "unknown"}`);
|
|
529
|
+
console.log(` Package version: ${readPackageJsonVersion()}`);
|
|
530
|
+
console.log();
|
|
531
|
+
console.log(` OpenCode plugin config`);
|
|
532
|
+
if (pluginDiagnostics.found) {
|
|
533
|
+
console.log(` File: ${pluginDiagnostics.path}`);
|
|
534
|
+
console.log(` Plugin entry: ${pluginDiagnostics.hasPlugin ? "found" : "missing"}`);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
console.log(` File: not found (${OPENCODE_CONFIG_FILES.map((p) => p.replace(`${homedir()}/`, "~/")).join(" or ")})`);
|
|
538
|
+
console.log(` Plugin entry: unknown`);
|
|
539
|
+
}
|
|
540
|
+
console.log();
|
|
541
|
+
console.log(` Tracker config`);
|
|
542
|
+
console.log(` File: ${CONFIG_FILE}`);
|
|
543
|
+
console.log(` Status: ${configDiagnostics.exists ? "exists" : "using defaults"}`);
|
|
544
|
+
if (configDiagnostics.parseError) {
|
|
545
|
+
console.log(` Warning: ${configDiagnostics.parseError}`);
|
|
546
|
+
}
|
|
547
|
+
for (const warning of configDiagnostics.warnings) {
|
|
548
|
+
console.log(` Warning: ${warning}`);
|
|
549
|
+
}
|
|
550
|
+
const budget = configDiagnostics.config.budget;
|
|
551
|
+
const hasBudget = Boolean(budget.daily || budget.weekly || budget.monthly);
|
|
552
|
+
console.log(` Budget: ${hasBudget ? "configured" : "not configured"}`);
|
|
553
|
+
console.log();
|
|
554
|
+
console.log(` Token log`);
|
|
555
|
+
if (!existsSync(LOG_FILE)) {
|
|
556
|
+
console.log(` File: not found (${LOG_FILE})`);
|
|
557
|
+
console.log(` Entries: 0`);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
const latest = entries[entries.length - 1];
|
|
561
|
+
console.log(` File: ${LOG_FILE}`);
|
|
562
|
+
console.log(` Entries: ${entries.length}`);
|
|
563
|
+
console.log(` Latest: ${latest ? `${new Date(latest._ts).toISOString()} (${formatAge(latest._ts)})` : "none"}`);
|
|
564
|
+
}
|
|
565
|
+
console.log();
|
|
566
|
+
const defaultModels = getDefaultModelProviders(entries, configDiagnostics.config);
|
|
567
|
+
const zeroCostProviders = dedupeZeroCostProviderMatches(defaultModels.flatMap(({ model, provider, count }) => {
|
|
568
|
+
const match = getZeroCostProviderMatch(provider);
|
|
569
|
+
return match ? [{ model, provider, count, reason: match.reason }] : [];
|
|
570
|
+
}));
|
|
571
|
+
console.log(` Pricing`);
|
|
572
|
+
console.log(` Built-in pricing updated: ${BUILTIN_PRICING_META.pricingLastUpdated}`);
|
|
573
|
+
console.log(` Default-priced model/provider pairs: ${defaultModels.length}`);
|
|
574
|
+
for (const { model, provider, count } of defaultModels.slice(0, 5)) {
|
|
575
|
+
console.log(` - ${model} (${provider}, ${count} msgs)`);
|
|
576
|
+
}
|
|
577
|
+
if (defaultModels.length > 5) {
|
|
578
|
+
console.log(` - ...and ${defaultModels.length - 5} more`);
|
|
579
|
+
}
|
|
580
|
+
if (zeroCostProviders.length > 0) {
|
|
581
|
+
console.log(` Likely zero-cost providers:`);
|
|
582
|
+
for (const { provider, reason } of zeroCostProviders.slice(0, 5)) {
|
|
583
|
+
console.log(` - ${provider} (${reason})`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
console.log();
|
|
587
|
+
const nextSteps = [];
|
|
588
|
+
if (!pluginDiagnostics.found || !pluginDiagnostics.hasPlugin) {
|
|
589
|
+
nextSteps.push(`Add "opencode-token-tracker" to your OpenCode plugin config.`);
|
|
590
|
+
}
|
|
591
|
+
if (!existsSync(LOG_FILE) || entries.length === 0) {
|
|
592
|
+
nextSteps.push(`Run an OpenCode request, then check ${LOG_FILE}.`);
|
|
593
|
+
}
|
|
594
|
+
if (configDiagnostics.parseError || configDiagnostics.warnings.length > 0) {
|
|
595
|
+
nextSteps.push(`Fix ${CONFIG_FILE} or regenerate it with: opencode-tokens config generate`);
|
|
596
|
+
}
|
|
597
|
+
if (defaultModels.length > 0) {
|
|
598
|
+
nextSteps.push(`Review default pricing with: opencode-tokens models`);
|
|
599
|
+
nextSteps.push(`Generate suggested overrides with: opencode-tokens config init`);
|
|
600
|
+
}
|
|
601
|
+
if (!hasBudget) {
|
|
602
|
+
nextSteps.push(`Configure optional budgets with: opencode-tokens config init`);
|
|
603
|
+
}
|
|
604
|
+
console.log(` Next steps`);
|
|
605
|
+
if (nextSteps.length === 0) {
|
|
606
|
+
console.log(` No obvious issues found.`);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
for (const step of nextSteps) {
|
|
610
|
+
console.log(` - ${step}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
console.log();
|
|
614
|
+
}
|
|
615
|
+
function readPackageJsonVersion() {
|
|
616
|
+
try {
|
|
617
|
+
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
618
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
return "unknown";
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function inspectOpenCodePluginConfig() {
|
|
625
|
+
for (const path of OPENCODE_CONFIG_FILES) {
|
|
626
|
+
if (!existsSync(path))
|
|
627
|
+
continue;
|
|
628
|
+
const raw = readFileSync(path, "utf-8");
|
|
629
|
+
return {
|
|
630
|
+
found: true,
|
|
631
|
+
path,
|
|
632
|
+
hasPlugin: raw.includes("opencode-token-tracker"),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
return { found: false, hasPlugin: false };
|
|
636
|
+
}
|
|
637
|
+
function inspectTrackerConfig() {
|
|
638
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
639
|
+
return { exists: false, config: DEFAULT_CONFIG, warnings: [] };
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
643
|
+
const result = validateConfig(raw);
|
|
644
|
+
return { exists: true, config: result.config, warnings: result.warnings };
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
return { exists: true, config: DEFAULT_CONFIG, warnings: [], parseError: "Config file is not valid JSON, using defaults" };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function getDefaultModelProviders(entries, config) {
|
|
651
|
+
const counts = new Map();
|
|
652
|
+
for (const e of entries) {
|
|
653
|
+
const model = e.model ?? "unknown";
|
|
654
|
+
const provider = e.provider ?? "unknown";
|
|
655
|
+
if (model === "unknown" || provider === "unknown")
|
|
656
|
+
continue;
|
|
657
|
+
const key = `${model}|${provider}`;
|
|
658
|
+
let item = counts.get(key);
|
|
659
|
+
if (!item) {
|
|
660
|
+
item = { model, provider, count: 0, lastUsed: 0 };
|
|
661
|
+
counts.set(key, item);
|
|
662
|
+
}
|
|
663
|
+
item.count++;
|
|
664
|
+
item.lastUsed = Math.max(item.lastUsed, e._ts);
|
|
665
|
+
}
|
|
666
|
+
return Array.from(counts.values())
|
|
667
|
+
.filter(({ model, provider }) => resolvePricingStatus(config, model, provider) === "default")
|
|
668
|
+
.sort((a, b) => b.lastUsed - a.lastUsed);
|
|
669
|
+
}
|
|
670
|
+
function dedupeZeroCostProviderMatches(items) {
|
|
671
|
+
const providers = new Map();
|
|
672
|
+
for (const item of items) {
|
|
673
|
+
if (!providers.has(item.provider)) {
|
|
674
|
+
providers.set(item.provider, { provider: item.provider, reason: item.reason });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return Array.from(providers.values());
|
|
406
678
|
}
|
|
407
679
|
function cmdConfig(positional) {
|
|
408
680
|
const action = positional[1];
|
|
@@ -444,12 +716,14 @@ function cmdConfig(positional) {
|
|
|
444
716
|
},
|
|
445
717
|
};
|
|
446
718
|
const suggestedProviders = [];
|
|
447
|
-
|
|
719
|
+
const suggestedProviderNames = new Set();
|
|
720
|
+
// Add likely zero-cost providers detected from logs.
|
|
448
721
|
for (const provider of providers) {
|
|
449
|
-
const
|
|
450
|
-
if (
|
|
722
|
+
const zeroCostMatch = getZeroCostProviderMatch(provider);
|
|
723
|
+
if (zeroCostMatch) {
|
|
451
724
|
exampleConfig.providers[provider] = { input: 0, output: 0 };
|
|
452
|
-
suggestedProviders.push(
|
|
725
|
+
suggestedProviders.push(zeroCostMatch);
|
|
726
|
+
suggestedProviderNames.add(provider);
|
|
453
727
|
}
|
|
454
728
|
}
|
|
455
729
|
const suggestedModels = [];
|
|
@@ -458,6 +732,8 @@ function cmdConfig(positional) {
|
|
|
458
732
|
const [model, provider] = mp.split("|");
|
|
459
733
|
if (model === "unknown" || provider === "unknown")
|
|
460
734
|
continue;
|
|
735
|
+
if (suggestedProviderNames.has(provider))
|
|
736
|
+
continue;
|
|
461
737
|
const status = resolvePricingStatus(config, model, provider);
|
|
462
738
|
if (status === "default") {
|
|
463
739
|
exampleConfig.models[model] = { input: 1, output: 4 };
|
|
@@ -488,10 +764,10 @@ function cmdConfig(positional) {
|
|
|
488
764
|
}
|
|
489
765
|
if (suggestedProviders.length > 0) {
|
|
490
766
|
suggestionsSummary += `
|
|
491
|
-
Detected zero-cost
|
|
767
|
+
Detected likely zero-cost providers:
|
|
492
768
|
`;
|
|
493
|
-
for (const
|
|
494
|
-
suggestionsSummary += ` • ${
|
|
769
|
+
for (const { provider, reason } of suggestedProviders) {
|
|
770
|
+
suggestionsSummary += ` • ${provider} (${reason}, automatically pre-configured to $0.00)\n`;
|
|
495
771
|
}
|
|
496
772
|
}
|
|
497
773
|
if (suggestedModels.length > 0) {
|
|
@@ -518,11 +794,11 @@ function cmdConfig(positional) {
|
|
|
518
794
|
|
|
519
795
|
Examples:
|
|
520
796
|
{ "input": 15, "output": 75 } = $15 per 1M input, $75 per 1M output
|
|
521
|
-
{ "input": 0, "output": 0 } = Free (subscription or local model)
|
|
797
|
+
{ "input": 0, "output": 0 } = Free (subscription or local provider/model)
|
|
522
798
|
|
|
523
799
|
Common scenarios:
|
|
524
800
|
- GitHub Copilot, Cursor, etc. → Set provider to { input: 0, output: 0 }
|
|
525
|
-
-
|
|
801
|
+
- Ollama, LM Studio, localhost → Set provider to { input: 0, output: 0 }
|
|
526
802
|
- Direct API usage → Look up provider's pricing page
|
|
527
803
|
|
|
528
804
|
Where to find pricing:
|
|
@@ -570,11 +846,11 @@ function cmdConfig(positional) {
|
|
|
570
846
|
if (action === "get") {
|
|
571
847
|
const key = positional[2];
|
|
572
848
|
if (!key) {
|
|
573
|
-
|
|
849
|
+
failCli("Missing config key", "Usage: opencode-tokens config get <key>");
|
|
574
850
|
return;
|
|
575
851
|
}
|
|
576
852
|
if (!SETTABLE_KEYS[key]) {
|
|
577
|
-
|
|
853
|
+
failCli(`Unknown config key: ${key}`, `Available keys: ${Object.keys(SETTABLE_KEYS).join(", ")}`);
|
|
578
854
|
return;
|
|
579
855
|
}
|
|
580
856
|
const value = resolveConfigKey(config, key);
|
|
@@ -584,27 +860,31 @@ function cmdConfig(positional) {
|
|
|
584
860
|
if (action === "set") {
|
|
585
861
|
const key = positional[2];
|
|
586
862
|
const rawValue = positional[3];
|
|
587
|
-
if (!key
|
|
588
|
-
|
|
863
|
+
if (!key) {
|
|
864
|
+
failCli("Missing config key", "Usage: opencode-tokens config set <key> <value>");
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (!rawValue) {
|
|
868
|
+
failCli("Missing config value", "Usage: opencode-tokens config set <key> <value>");
|
|
589
869
|
return;
|
|
590
870
|
}
|
|
591
871
|
const spec = SETTABLE_KEYS[key];
|
|
592
872
|
if (!spec) {
|
|
593
|
-
|
|
873
|
+
failCli(`Unknown config key: ${key}`, `Available keys: ${Object.keys(SETTABLE_KEYS).join(", ")}`);
|
|
594
874
|
return;
|
|
595
875
|
}
|
|
596
876
|
const value = parseConfigValue(rawValue);
|
|
597
877
|
if (typeof value !== spec.type) {
|
|
598
|
-
|
|
878
|
+
failCli(`Invalid type for ${key}: expected ${spec.type}, got ${typeof value}`);
|
|
599
879
|
return;
|
|
600
880
|
}
|
|
601
881
|
if (typeof value === "number") {
|
|
602
882
|
if (value < 0) {
|
|
603
|
-
|
|
883
|
+
failCli(`Invalid value for ${key}: must be >= 0`);
|
|
604
884
|
return;
|
|
605
885
|
}
|
|
606
886
|
if (spec.max !== undefined && value > spec.max) {
|
|
607
|
-
|
|
887
|
+
failCli(`Invalid value for ${key}: must be <= ${spec.max}`);
|
|
608
888
|
return;
|
|
609
889
|
}
|
|
610
890
|
}
|
|
@@ -615,12 +895,12 @@ function cmdConfig(positional) {
|
|
|
615
895
|
if (action === "unset") {
|
|
616
896
|
const key = positional[2];
|
|
617
897
|
if (!key) {
|
|
618
|
-
|
|
898
|
+
failCli("Missing config key", "Usage: opencode-tokens config unset <key>");
|
|
619
899
|
return;
|
|
620
900
|
}
|
|
621
901
|
const spec = SETTABLE_KEYS[key];
|
|
622
902
|
if (!spec) {
|
|
623
|
-
|
|
903
|
+
failCli(`Unknown config key: ${key}`, `Available keys: ${Object.keys(SETTABLE_KEYS).join(", ")}`);
|
|
624
904
|
return;
|
|
625
905
|
}
|
|
626
906
|
applyConfigUnset(key);
|
|
@@ -628,8 +908,7 @@ function cmdConfig(positional) {
|
|
|
628
908
|
return;
|
|
629
909
|
}
|
|
630
910
|
if (action && action !== "show") {
|
|
631
|
-
|
|
632
|
-
console.log(` Usage: opencode-tokens config [show|init|generate|get|set|unset]\n`);
|
|
911
|
+
failCli(`Unknown config action: ${action}`, "Usage: opencode-tokens config [show|init|generate|get|set|unset]");
|
|
633
912
|
return;
|
|
634
913
|
}
|
|
635
914
|
// Show current config
|
|
@@ -733,10 +1012,48 @@ function saveConfig(raw) {
|
|
|
733
1012
|
// ============================================================================
|
|
734
1013
|
// Export
|
|
735
1014
|
// ============================================================================
|
|
1015
|
+
const EXPORT_FORMATS = ["csv", "json"];
|
|
1016
|
+
const EXPORT_PERIODS = ["today", "week", "month", "all"];
|
|
1017
|
+
function failCli(message, usage) {
|
|
1018
|
+
process.stderr.write(`\n Error: ${message}\n`);
|
|
1019
|
+
if (usage) {
|
|
1020
|
+
process.stderr.write(` ${usage}\n`);
|
|
1021
|
+
}
|
|
1022
|
+
process.stderr.write("\n");
|
|
1023
|
+
process.exitCode = 1;
|
|
1024
|
+
}
|
|
1025
|
+
function isExportFormat(format) {
|
|
1026
|
+
return EXPORT_FORMATS.includes(format);
|
|
1027
|
+
}
|
|
1028
|
+
function isExportPeriod(period) {
|
|
1029
|
+
return EXPORT_PERIODS.includes(period);
|
|
1030
|
+
}
|
|
736
1031
|
function cmdExport(flags) {
|
|
737
|
-
const
|
|
738
|
-
const
|
|
1032
|
+
const formatValue = flagValue(flags, "format");
|
|
1033
|
+
const periodValue = flagValue(flags, "period");
|
|
739
1034
|
const outputFile = flagValue(flags, "output");
|
|
1035
|
+
if (flags.has("format") && !formatValue) {
|
|
1036
|
+
failCli("Missing value for --format", "Usage: opencode-tokens export --format csv|json");
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (flags.has("period") && !periodValue) {
|
|
1040
|
+
failCli("Missing value for --period", "Usage: opencode-tokens export --period today|week|month|all");
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (flags.has("output") && !outputFile) {
|
|
1044
|
+
failCli("Missing value for --output", "Usage: opencode-tokens export --output <file>");
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const format = formatValue ?? "csv";
|
|
1048
|
+
const period = periodValue ?? "all";
|
|
1049
|
+
if (!isExportFormat(format)) {
|
|
1050
|
+
failCli(`Unsupported export format: ${format}`, "Allowed formats: csv, json");
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (!isExportPeriod(period)) {
|
|
1054
|
+
failCli(`Unsupported export period: ${period}`, "Allowed periods: today, week, month, all");
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
740
1057
|
const now = new Date();
|
|
741
1058
|
let since;
|
|
742
1059
|
switch (period) {
|
|
@@ -780,7 +1097,14 @@ function cmdExport(flags) {
|
|
|
780
1097
|
output = `${[headers.join(","), ...rows].join("\n")}\n`;
|
|
781
1098
|
}
|
|
782
1099
|
if (outputFile) {
|
|
783
|
-
|
|
1100
|
+
try {
|
|
1101
|
+
writeFileSync(outputFile, output);
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1105
|
+
failCli(`Failed to write export file: ${detail}`);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
784
1108
|
console.log(`\n Exported ${entries.length} entries to ${outputFile}\n`);
|
|
785
1109
|
}
|
|
786
1110
|
else {
|
|
@@ -897,6 +1221,7 @@ function cmdHelp() {
|
|
|
897
1221
|
Commands:
|
|
898
1222
|
(default) Show usage statistics
|
|
899
1223
|
budget Show budget status (daily/weekly/monthly)
|
|
1224
|
+
doctor Diagnose plugin config, logs, and pricing fallbacks
|
|
900
1225
|
pricing Show built-in pricing table
|
|
901
1226
|
models Show your used models and their pricing status
|
|
902
1227
|
config Show/generate/modify configuration
|
|
@@ -935,6 +1260,7 @@ function cmdHelp() {
|
|
|
935
1260
|
|
|
936
1261
|
Examples:
|
|
937
1262
|
opencode-tokens # All-time summary
|
|
1263
|
+
opencode-tokens doctor # Diagnose setup and logs
|
|
938
1264
|
opencode-tokens today --by model # Today by model
|
|
939
1265
|
opencode-tokens week --by session # This week by session
|
|
940
1266
|
opencode-tokens trend --days 7 # 7-day cost trend
|
|
@@ -945,6 +1271,10 @@ function cmdHelp() {
|
|
|
945
1271
|
Costs are estimates from local logs; budgets are warnings, not enforcement.
|
|
946
1272
|
`);
|
|
947
1273
|
}
|
|
1274
|
+
const TREND_METRICS = ["cost", "tokens", "messages"];
|
|
1275
|
+
function isTrendMetric(metric) {
|
|
1276
|
+
return TREND_METRICS.includes(metric);
|
|
1277
|
+
}
|
|
948
1278
|
function getTrendValue(point, metric) {
|
|
949
1279
|
return metric === "tokens" ? point.tokens : metric === "messages" ? point.messages : point.cost;
|
|
950
1280
|
}
|
|
@@ -958,6 +1288,25 @@ function formatSignedTrendValue(value, metric) {
|
|
|
958
1288
|
function metricLabel(metric) {
|
|
959
1289
|
return metric === "tokens" ? "Token Trend" : metric === "messages" ? "Message Trend" : "Cost Trend";
|
|
960
1290
|
}
|
|
1291
|
+
function parsePositiveIntegerOption(flags, name, defaultValue, usage) {
|
|
1292
|
+
const value = flagValue(flags, name);
|
|
1293
|
+
if (flags.has(name) && !value) {
|
|
1294
|
+
failCli(`Missing value for --${name}`, usage);
|
|
1295
|
+
return undefined;
|
|
1296
|
+
}
|
|
1297
|
+
if (!value)
|
|
1298
|
+
return defaultValue;
|
|
1299
|
+
if (!/^\d+$/.test(value)) {
|
|
1300
|
+
failCli(`Invalid value for --${name}: ${value}`, usage);
|
|
1301
|
+
return undefined;
|
|
1302
|
+
}
|
|
1303
|
+
const parsed = Number.parseInt(value, 10);
|
|
1304
|
+
if (parsed <= 0) {
|
|
1305
|
+
failCli(`Invalid value for --${name}: ${value}`, usage);
|
|
1306
|
+
return undefined;
|
|
1307
|
+
}
|
|
1308
|
+
return parsed;
|
|
1309
|
+
}
|
|
961
1310
|
function buildLineRows(points, width, height) {
|
|
962
1311
|
const grid = Array.from({ length: height }, () => Array.from({ length: width }, () => " "));
|
|
963
1312
|
// 1. Calculate the exact integer row r[x] for every column x from 0 to width - 1
|
|
@@ -1064,9 +1413,22 @@ function buildTrendXAxis(points, chartPoints, chartWidth) {
|
|
|
1064
1413
|
};
|
|
1065
1414
|
}
|
|
1066
1415
|
function cmdTrend(flags) {
|
|
1067
|
-
const days =
|
|
1068
|
-
|
|
1069
|
-
|
|
1416
|
+
const days = parsePositiveIntegerOption(flags, "days", 30, "Usage: opencode-tokens trend --days <positive-integer>");
|
|
1417
|
+
if (days === undefined)
|
|
1418
|
+
return;
|
|
1419
|
+
const metricValue = flagValue(flags, "metric");
|
|
1420
|
+
if (flags.has("metric") && !metricValue) {
|
|
1421
|
+
failCli("Missing value for --metric", "Usage: opencode-tokens trend --metric cost|tokens|messages");
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
const metric = metricValue ?? "cost";
|
|
1425
|
+
if (!isTrendMetric(metric)) {
|
|
1426
|
+
failCli(`Unsupported trend metric: ${metric}`, "Allowed metrics: cost, tokens, messages");
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const width = parsePositiveIntegerOption(flags, "width", 60, "Usage: opencode-tokens trend --width <positive-integer>");
|
|
1430
|
+
if (width === undefined)
|
|
1431
|
+
return;
|
|
1070
1432
|
const since = getStartOfDay(new Date(Date.now() - days * 86400000));
|
|
1071
1433
|
const entries = loadEntries(since);
|
|
1072
1434
|
if (entries.length === 0) {
|
|
@@ -1173,33 +1535,45 @@ function main() {
|
|
|
1173
1535
|
return;
|
|
1174
1536
|
}
|
|
1175
1537
|
const { command } = parsed;
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1538
|
+
if (isTopLevelCommand(command))
|
|
1539
|
+
switch (command) {
|
|
1540
|
+
case "budget":
|
|
1541
|
+
cmdBudget();
|
|
1542
|
+
return;
|
|
1543
|
+
case "doctor":
|
|
1544
|
+
cmdDoctor();
|
|
1545
|
+
return;
|
|
1546
|
+
case "pricing":
|
|
1547
|
+
cmdPricing();
|
|
1548
|
+
return;
|
|
1549
|
+
case "models":
|
|
1550
|
+
cmdModels();
|
|
1551
|
+
return;
|
|
1552
|
+
case "config":
|
|
1553
|
+
cmdConfig(parsed.positional);
|
|
1554
|
+
return;
|
|
1555
|
+
case "export":
|
|
1556
|
+
cmdExport(parsed.flags);
|
|
1557
|
+
return;
|
|
1558
|
+
case "trend":
|
|
1559
|
+
cmdTrend(parsed.flags);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1196
1562
|
// Default: stats
|
|
1197
1563
|
let period = "all";
|
|
1198
|
-
const breakdown =
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1564
|
+
const breakdown = getStatsBreakdown(parsed.flags);
|
|
1565
|
+
if (process.exitCode)
|
|
1566
|
+
return;
|
|
1567
|
+
if (command) {
|
|
1568
|
+
if (!isStatsPeriod(command)) {
|
|
1569
|
+
failCli(`Unknown command or stats period: ${command}`, "Usage: opencode-tokens [today|week|month|all] [--by model|agent|provider|daily|session|all]");
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
period = command;
|
|
1573
|
+
const extraArgs = parsed.positional.slice(1);
|
|
1574
|
+
if (extraArgs.length > 0) {
|
|
1575
|
+
failCli(`Unexpected argument for stats command: ${extraArgs[0]}`, "Usage: opencode-tokens [today|week|month|all] [--by model|agent|provider|daily|session|all]");
|
|
1576
|
+
return;
|
|
1203
1577
|
}
|
|
1204
1578
|
}
|
|
1205
1579
|
cmdStats(period, breakdown);
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_CONFIG, formatCost, formatTokens, getStartOfDay, getStartOfWeek,
|
|
1
|
+
import { DEFAULT_CONFIG, calculateCost, evaluateBudgetStatus, formatCost, formatTokens, getStartOfDay, getStartOfMonth, getStartOfWeek, hasBillableTokenUsage, validateConfig, } from "./lib/shared.js";
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, openSync, readSync, closeSync } from "fs";
|
|
3
3
|
import { open } from "fs/promises";
|
|
4
4
|
import { join } from "path";
|
|
@@ -366,8 +366,7 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
|
366
366
|
const reasoning = info.tokens.reasoning ?? 0;
|
|
367
367
|
const cacheRead = info.tokens.cache?.read ?? 0;
|
|
368
368
|
const cacheWrite = info.tokens.cache?.write ?? 0;
|
|
369
|
-
|
|
370
|
-
if (!hasTokens)
|
|
369
|
+
if (!hasBillableTokenUsage({ input, output, cacheRead, cacheWrite }))
|
|
371
370
|
return;
|
|
372
371
|
const dedupeKey = `${messageId}-${input}-${output}-${cacheRead}-${cacheWrite}`;
|
|
373
372
|
if (isDuplicate(dedupeKey))
|
package/dist/lib/shared.d.ts
CHANGED
|
@@ -4,12 +4,18 @@ export interface ModelPricing {
|
|
|
4
4
|
cacheRead?: number;
|
|
5
5
|
cacheWrite?: number;
|
|
6
6
|
}
|
|
7
|
+
export interface BillableTokenUsage {
|
|
8
|
+
input?: number;
|
|
9
|
+
output?: number;
|
|
10
|
+
cacheRead?: number;
|
|
11
|
+
cacheWrite?: number;
|
|
12
|
+
}
|
|
7
13
|
export interface ProviderModelPricingMap {
|
|
8
14
|
[provider: string]: ModelPricing;
|
|
9
15
|
}
|
|
10
16
|
export declare const BUILTIN_PRICING_META: {
|
|
11
|
-
readonly pricingLastUpdated: "2026-
|
|
12
|
-
readonly metadataLastUpdated: "2026-05-
|
|
17
|
+
readonly pricingLastUpdated: "2026-05-29";
|
|
18
|
+
readonly metadataLastUpdated: "2026-05-29";
|
|
13
19
|
readonly source: "Provider official pricing pages";
|
|
14
20
|
readonly notes: "Manually maintained. Report stale prices: https://github.com/tongsh6/opencode-token-tracker/issues/new";
|
|
15
21
|
};
|
|
@@ -21,7 +27,9 @@ export declare function formatCost(cost: number): string;
|
|
|
21
27
|
* @param millionDecimals - decimal places for M-level values (default 1)
|
|
22
28
|
*/
|
|
23
29
|
export declare function formatTokens(tokens: number, millionDecimals?: number): string;
|
|
30
|
+
export declare function hasBillableTokenUsage(usage: BillableTokenUsage): boolean;
|
|
24
31
|
export declare function getStartOfDay(date?: Date): number;
|
|
32
|
+
export declare function formatLocalDateKey(date: Date): string;
|
|
25
33
|
export declare function getStartOfWeek(date?: Date): number;
|
|
26
34
|
export declare function getStartOfMonth(date?: Date): number;
|
|
27
35
|
export interface ToastConfig {
|
package/dist/lib/shared.js
CHANGED
|
@@ -2,28 +2,30 @@
|
|
|
2
2
|
// Shared types, pricing data, and utilities for opencode-token-tracker
|
|
3
3
|
// ============================================================================
|
|
4
4
|
// ============================================================================
|
|
5
|
-
// Built-in Pricing (USD per 1M tokens) - Updated 2026-
|
|
5
|
+
// Built-in Pricing (USD per 1M tokens) - Updated 2026-05-29
|
|
6
6
|
// Sources:
|
|
7
|
-
// - Anthropic: https://
|
|
8
|
-
// - OpenAI: https://openai.com/api/pricing
|
|
7
|
+
// - Anthropic: https://platform.claude.com/docs/en/about-claude/pricing
|
|
8
|
+
// - OpenAI: https://developers.openai.com/api/docs/pricing
|
|
9
9
|
// - DeepSeek: https://api-docs.deepseek.com/quick_start/pricing
|
|
10
10
|
// - Google: https://cloud.google.com/vertex-ai/generative-ai/pricing
|
|
11
11
|
// ============================================================================
|
|
12
12
|
export const BUILTIN_PRICING_META = {
|
|
13
|
-
pricingLastUpdated: "2026-
|
|
14
|
-
metadataLastUpdated: "2026-05-
|
|
13
|
+
pricingLastUpdated: "2026-05-29",
|
|
14
|
+
metadataLastUpdated: "2026-05-29",
|
|
15
15
|
source: "Provider official pricing pages",
|
|
16
16
|
notes: "Manually maintained. Report stale prices: https://github.com/tongsh6/opencode-token-tracker/issues/new",
|
|
17
17
|
};
|
|
18
18
|
export const BUILTIN_PRICING = {
|
|
19
|
-
// Anthropic Claude (https://
|
|
20
|
-
// Opus 4.6: $5 input, $25 output
|
|
19
|
+
// Anthropic Claude (https://platform.claude.com/docs/en/about-claude/pricing)
|
|
20
|
+
// Opus 4.8/4.7/4.6/4.5: $5 input, $25 output, cache write $6.25, cache read $0.50
|
|
21
|
+
"claude-opus-4.8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
22
|
+
"claude-opus-4.7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
21
23
|
"claude-opus-4.6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
22
|
-
// Opus 4.5 (legacy): same pricing as Opus 4.6
|
|
23
24
|
"claude-opus-4.5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
24
|
-
// Sonnet 4.5: $3 input, $15 output
|
|
25
|
+
// Sonnet 4.6/4.5: $3 input, $15 output, cache write $3.75, cache read $0.30
|
|
26
|
+
"claude-sonnet-4.6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
25
27
|
"claude-sonnet-4.5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
26
|
-
// Sonnet 4: $3 input, $15 output
|
|
28
|
+
// Sonnet 4 (deprecated): $3 input, $15 output
|
|
27
29
|
"claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
28
30
|
// Haiku 4.5: $1 input, $5 output, cache write $1.25, cache read $0.10
|
|
29
31
|
"claude-haiku-4.5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
@@ -32,38 +34,63 @@ export const BUILTIN_PRICING = {
|
|
|
32
34
|
"claude-opus-4.1": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
33
35
|
"claude-opus-4": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
34
36
|
"claude-haiku-3": { input: 0.25, output: 1.25, cacheRead: 0.03, cacheWrite: 0.3 },
|
|
35
|
-
// OpenAI GPT (https://openai.com/api/pricing
|
|
36
|
-
// GPT-5.
|
|
37
|
+
// OpenAI GPT (https://developers.openai.com/api/docs/pricing)
|
|
38
|
+
// GPT-5.5/5.4 standard short-context text pricing.
|
|
39
|
+
"gpt-5.5": { input: 5, output: 30, cacheRead: 0.5 },
|
|
40
|
+
"gpt-5.5-pro": { input: 30, output: 180 },
|
|
41
|
+
"gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25 },
|
|
42
|
+
"gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075 },
|
|
43
|
+
"gpt-5.4-nano": { input: 0.2, output: 1.25, cacheRead: 0.02 },
|
|
44
|
+
"gpt-5.4-pro": { input: 30, output: 180 },
|
|
45
|
+
"gpt-5.3-codex": { input: 1.75, output: 14, cacheRead: 0.175 },
|
|
46
|
+
"gpt-5.3-chat-latest": { input: 1.75, output: 14, cacheRead: 0.175 },
|
|
47
|
+
// GPT-5.2 and earlier GPT-5 family.
|
|
37
48
|
"gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175 },
|
|
38
49
|
"gpt-5.2-pro": { input: 21, output: 168 },
|
|
39
50
|
"gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025 },
|
|
40
|
-
"gpt-5
|
|
41
|
-
"gpt-5": { input:
|
|
42
|
-
|
|
43
|
-
"gpt-
|
|
44
|
-
"gpt-
|
|
45
|
-
"gpt-
|
|
51
|
+
"gpt-5-nano": { input: 0.05, output: 0.4, cacheRead: 0.005 },
|
|
52
|
+
"gpt-5.1": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
53
|
+
"gpt-5.1-chat-latest": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
54
|
+
"gpt-5.1-codex-max": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
55
|
+
"gpt-5.1-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
56
|
+
"gpt-5.1-codex-mini": { input: 0.25, output: 2, cacheRead: 0.025 },
|
|
57
|
+
"gpt-5": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
58
|
+
"gpt-5-chat-latest": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
59
|
+
"gpt-5-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
60
|
+
// GPT-4.1 series
|
|
61
|
+
"gpt-4.1": { input: 2, output: 8, cacheRead: 0.5 },
|
|
62
|
+
"gpt-4.1-mini": { input: 0.4, output: 1.6, cacheRead: 0.1 },
|
|
63
|
+
"gpt-4.1-nano": { input: 0.1, output: 0.4, cacheRead: 0.025 },
|
|
46
64
|
// GPT-4o series (may be deprecated)
|
|
47
|
-
"gpt-4o": { input: 2.5, output: 10 },
|
|
48
|
-
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
65
|
+
"gpt-4o": { input: 2.5, output: 10, cacheRead: 1.25 },
|
|
66
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6, cacheRead: 0.075 },
|
|
49
67
|
// Reasoning models
|
|
50
|
-
"o3": { input:
|
|
51
|
-
"o3-mini": { input: 1.1, output: 4.4 },
|
|
52
|
-
"o4-mini": { input:
|
|
53
|
-
"o1": { input: 15, output: 60 },
|
|
54
|
-
"o1-mini": { input: 1.1, output: 4.4 },
|
|
68
|
+
"o3": { input: 2, output: 8, cacheRead: 0.5 },
|
|
69
|
+
"o3-mini": { input: 1.1, output: 4.4, cacheRead: 0.55 },
|
|
70
|
+
"o4-mini": { input: 1.1, output: 4.4, cacheRead: 0.275 },
|
|
71
|
+
"o1": { input: 15, output: 60, cacheRead: 7.5 },
|
|
72
|
+
"o1-mini": { input: 1.1, output: 4.4, cacheRead: 0.55 },
|
|
55
73
|
// DeepSeek (https://api-docs.deepseek.com/quick_start/pricing)
|
|
56
74
|
// DeepSeek-V4 Flash compatibility pricing for deepseek-chat / deepseek-reasoner.
|
|
57
75
|
// $0.14 input (cache miss), $0.0028 input (cache hit), $0.28 output
|
|
58
76
|
"deepseek-chat": { input: 0.14, output: 0.28, cacheRead: 0.0028 },
|
|
59
77
|
"deepseek-reasoner": { input: 0.14, output: 0.28, cacheRead: 0.0028 },
|
|
78
|
+
// DeepSeek-V4-Pro discounted pricing, current until 2026-05-31 15:59 UTC.
|
|
79
|
+
// $0.435 input (cache miss), $0.003625 input (cache hit), $0.87 output
|
|
80
|
+
"deepseek-v4-pro": { input: 0.435, output: 0.87, cacheRead: 0.003625 },
|
|
60
81
|
// Google Gemini (https://cloud.google.com/vertex-ai/generative-ai/pricing)
|
|
61
|
-
//
|
|
82
|
+
// Standard global text pricing at <=200K input tokens where tiered pricing applies.
|
|
83
|
+
// Gemini 3.1 Pro Preview: $2 input, $12 output, cache read $0.20
|
|
84
|
+
"gemini-3.1-pro-preview": { input: 2, output: 12, cacheRead: 0.2 },
|
|
62
85
|
"gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2 },
|
|
63
86
|
"gemini-3-pro-preview": { input: 2, output: 12, cacheRead: 0.2 },
|
|
87
|
+
// Gemini 3.5 Flash: $1.50 input, $9 output, cache read $0.15
|
|
88
|
+
"gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15 },
|
|
64
89
|
// Gemini 3 Flash Preview: $0.5 input, $3 output
|
|
65
90
|
"gemini-3-flash": { input: 0.5, output: 3, cacheRead: 0.05 },
|
|
66
91
|
"gemini-3-flash-preview": { input: 0.5, output: 3, cacheRead: 0.05 },
|
|
92
|
+
// Gemini 3.1 Flash-Lite: $0.25 input, $1.50 output, cache read $0.025
|
|
93
|
+
"gemini-3.1-flash-lite": { input: 0.25, output: 1.5, cacheRead: 0.025 },
|
|
67
94
|
// Gemini 2.5 Pro: $1.25 input, $10 output (≤200K)
|
|
68
95
|
"gemini-2.5-pro": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
69
96
|
// Gemini 2.5 Flash: $0.3 input, $2.5 output
|
|
@@ -99,6 +126,12 @@ export function formatTokens(tokens, millionDecimals = 1) {
|
|
|
99
126
|
return `${(tokens / 1_000).toFixed(1)}K`;
|
|
100
127
|
return tokens.toString();
|
|
101
128
|
}
|
|
129
|
+
export function hasBillableTokenUsage(usage) {
|
|
130
|
+
return (usage.input ?? 0) > 0
|
|
131
|
+
|| (usage.output ?? 0) > 0
|
|
132
|
+
|| (usage.cacheRead ?? 0) > 0
|
|
133
|
+
|| (usage.cacheWrite ?? 0) > 0;
|
|
134
|
+
}
|
|
102
135
|
// ============================================================================
|
|
103
136
|
// Time Utilities
|
|
104
137
|
// ============================================================================
|
|
@@ -107,6 +140,12 @@ export function getStartOfDay(date = new Date()) {
|
|
|
107
140
|
d.setHours(0, 0, 0, 0);
|
|
108
141
|
return d.getTime();
|
|
109
142
|
}
|
|
143
|
+
export function formatLocalDateKey(date) {
|
|
144
|
+
const year = date.getFullYear();
|
|
145
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
146
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
147
|
+
return `${year}-${month}-${day}`;
|
|
148
|
+
}
|
|
110
149
|
export function getStartOfWeek(date = new Date()) {
|
|
111
150
|
const d = new Date(date);
|
|
112
151
|
const day = d.getDay();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-token-tracker",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.1",
|
|
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",
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"test": "tsc && node --test dist/test/*.test.js",
|
|
14
|
-
"prepublishOnly": "npm run build"
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
15
|
+
"release:check": "node scripts/release.js check",
|
|
16
|
+
"release:prepare": "node scripts/release.js prepare",
|
|
17
|
+
"release:tag": "node scripts/release.js tag"
|
|
15
18
|
},
|
|
16
19
|
"keywords": [
|
|
17
20
|
"opencode",
|