opencode-token-tracker 1.6.5 → 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 +77 -14
- package/README.zh-CN.md +149 -13
- package/dist/bin/opencode-tokens.js +531 -111
- package/dist/index.d.ts +0 -3
- package/dist/index.js +9 -133
- package/dist/lib/shared.d.ts +33 -0
- package/dist/lib/shared.js +219 -28
- package/package.json +11 -4
- package/dist/test/cli.test.d.ts +0 -1
- package/dist/test/cli.test.js +0 -55
- package/dist/test/shared.test.d.ts +0 -1
- package/dist/test/shared.test.js +0 -536
|
@@ -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;
|
|
@@ -314,17 +387,20 @@ function cmdStats(period, breakdown) {
|
|
|
314
387
|
function cmdPricing() {
|
|
315
388
|
const config = loadConfig();
|
|
316
389
|
console.log(`
|
|
317
|
-
Built-in Pricing Table (USD per 1M tokens)
|
|
390
|
+
Built-in Pricing Table (USD per 1M tokens)
|
|
318
391
|
══════════════════════════════════════════════════════════════════
|
|
392
|
+
Pricing last updated: ${BUILTIN_PRICING_META.pricingLastUpdated}
|
|
393
|
+
Metadata last updated: ${BUILTIN_PRICING_META.metadataLastUpdated}
|
|
394
|
+
Source: ${BUILTIN_PRICING_META.source}
|
|
319
395
|
`);
|
|
320
396
|
// Group by provider
|
|
321
397
|
const groups = {
|
|
322
|
-
"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"],
|
|
323
|
-
"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"],
|
|
324
|
-
"DeepSeek": ["deepseek-chat", "deepseek-reasoner"],
|
|
325
|
-
"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"],
|
|
326
402
|
};
|
|
327
|
-
const modelWidth =
|
|
403
|
+
const modelWidth = 24;
|
|
328
404
|
const priceWidth = 10;
|
|
329
405
|
for (const [group, models] of Object.entries(groups)) {
|
|
330
406
|
console.log(` ${group}`);
|
|
@@ -347,6 +423,12 @@ function cmdPricing() {
|
|
|
347
423
|
if (Object.keys(config.models || {}).length > 0) {
|
|
348
424
|
console.log(` * = overridden in config`);
|
|
349
425
|
}
|
|
426
|
+
console.log(` Fallback Pricing Notice:`);
|
|
427
|
+
console.log(` When a model is not matched in the built-in pricing table or user configuration,`);
|
|
428
|
+
console.log(` it falls back to the default rate ($1.0 / $4.0 per 1M tokens).`);
|
|
429
|
+
console.log(` You can easily override it in your configuration.`);
|
|
430
|
+
console.log(` ${BUILTIN_PRICING_META.notes}`);
|
|
431
|
+
console.log();
|
|
350
432
|
}
|
|
351
433
|
function cmdModels() {
|
|
352
434
|
const entries = loadEntries();
|
|
@@ -383,29 +465,17 @@ function cmdModels() {
|
|
|
383
465
|
const statusWidth = 12;
|
|
384
466
|
console.log(` ${padRight("Model", modelWidth)} ${padRight("Provider", providerWidth)} ${padLeft("Msgs", countWidth)} ${padRight("Pricing", statusWidth)}`);
|
|
385
467
|
console.log(` ${"-".repeat(modelWidth)} ${"-".repeat(providerWidth)} ${"-".repeat(countWidth)} ${"-".repeat(statusWidth)}`);
|
|
468
|
+
const defaultModels = [];
|
|
469
|
+
const zeroCostProviderModels = [];
|
|
386
470
|
for (const { model, provider, count } of sorted) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
else if (findModelConfigPricing(config.models, model, provider, false)) {
|
|
393
|
-
status = "model cfg";
|
|
394
|
-
}
|
|
395
|
-
else if (BUILTIN_PRICING[model]) {
|
|
396
|
-
status = "built-in";
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
const modelLower = model.toLowerCase();
|
|
400
|
-
const hasBuiltinPartial = Object.keys(BUILTIN_PRICING).some(k => k !== "_default" && modelLower.includes(k.toLowerCase()));
|
|
401
|
-
if (hasBuiltinPartial) {
|
|
402
|
-
status = "built-in";
|
|
403
|
-
}
|
|
404
|
-
else if (findModelConfigPricing(config.models, model, provider, true)) {
|
|
405
|
-
status = "model cfg";
|
|
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 });
|
|
406
476
|
}
|
|
407
477
|
else {
|
|
408
|
-
|
|
478
|
+
defaultModels.push({ model, provider, count });
|
|
409
479
|
}
|
|
410
480
|
}
|
|
411
481
|
console.log(` ${padRight(model, modelWidth)} ${padRight(provider, providerWidth)} ${padLeft(count.toString(), countWidth)} ${padRight(status, statusWidth)}`);
|
|
@@ -417,28 +487,219 @@ function cmdModels() {
|
|
|
417
487
|
console.log(` model cfg = overridden by models config`);
|
|
418
488
|
console.log(` default = unknown model, using $1/$4 per 1M tokens`);
|
|
419
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());
|
|
420
678
|
}
|
|
421
679
|
function cmdConfig(positional) {
|
|
422
680
|
const action = positional[1];
|
|
423
681
|
const config = loadConfig();
|
|
424
682
|
const entries = loadEntries();
|
|
425
683
|
if (action === "init" || action === "generate") {
|
|
426
|
-
// Get unique providers from logs
|
|
684
|
+
// Get unique providers and model+provider combinations from logs
|
|
427
685
|
const providers = new Set();
|
|
428
|
-
const
|
|
686
|
+
const modelProviders = new Set();
|
|
429
687
|
for (const e of entries) {
|
|
430
688
|
if (e.provider)
|
|
431
689
|
providers.add(e.provider);
|
|
432
|
-
|
|
433
|
-
|
|
690
|
+
const model = e.model ?? "unknown";
|
|
691
|
+
const provider = e.provider ?? "unknown";
|
|
692
|
+
modelProviders.add(`${model}|${provider}`);
|
|
434
693
|
}
|
|
435
|
-
//
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
694
|
+
// Estimate daily avg from last 7 days
|
|
695
|
+
const now = Date.now();
|
|
696
|
+
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
|
|
697
|
+
const recentEntries = entries.filter(e => e._ts >= sevenDaysAgo);
|
|
698
|
+
const totalSpent = recentEntries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
|
|
699
|
+
const dailyAvg = totalSpent / 7;
|
|
700
|
+
const dailyLimit = dailyAvg > 0 ? Math.max(0.5, round2(dailyAvg * 1.5)) : 5;
|
|
701
|
+
const weeklyLimit = dailyAvg > 0 ? Math.max(0.5, round2(dailyAvg * 7 * 1.3)) : 25;
|
|
702
|
+
const monthlyLimit = dailyAvg > 0 ? Math.max(0.5, round2(dailyAvg * 30 * 1.2)) : 100;
|
|
442
703
|
const exampleConfig = {
|
|
443
704
|
providers: {},
|
|
444
705
|
models: {},
|
|
@@ -448,25 +709,78 @@ function cmdConfig(positional) {
|
|
|
448
709
|
showOnIdle: true,
|
|
449
710
|
},
|
|
450
711
|
budget: {
|
|
451
|
-
daily:
|
|
452
|
-
weekly:
|
|
453
|
-
monthly:
|
|
712
|
+
daily: dailyLimit,
|
|
713
|
+
weekly: weeklyLimit,
|
|
714
|
+
monthly: monthlyLimit,
|
|
454
715
|
warnAt: 0.8,
|
|
455
716
|
},
|
|
456
717
|
};
|
|
457
|
-
|
|
718
|
+
const suggestedProviders = [];
|
|
719
|
+
const suggestedProviderNames = new Set();
|
|
720
|
+
// Add likely zero-cost providers detected from logs.
|
|
458
721
|
for (const provider of providers) {
|
|
459
|
-
|
|
460
|
-
if (
|
|
722
|
+
const zeroCostMatch = getZeroCostProviderMatch(provider);
|
|
723
|
+
if (zeroCostMatch) {
|
|
461
724
|
exampleConfig.providers[provider] = { input: 0, output: 0 };
|
|
725
|
+
suggestedProviders.push(zeroCostMatch);
|
|
726
|
+
suggestedProviderNames.add(provider);
|
|
462
727
|
}
|
|
463
728
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
729
|
+
const suggestedModels = [];
|
|
730
|
+
// Add unknown/fallback models where status is "default"
|
|
731
|
+
for (const mp of modelProviders) {
|
|
732
|
+
const [model, provider] = mp.split("|");
|
|
733
|
+
if (model === "unknown" || provider === "unknown")
|
|
734
|
+
continue;
|
|
735
|
+
if (suggestedProviderNames.has(provider))
|
|
736
|
+
continue;
|
|
737
|
+
const status = resolvePricingStatus(config, model, provider);
|
|
738
|
+
if (status === "default") {
|
|
739
|
+
exampleConfig.models[model] = { input: 1, output: 4 };
|
|
740
|
+
suggestedModels.push(`${model} (${provider})`);
|
|
741
|
+
}
|
|
467
742
|
}
|
|
468
|
-
//
|
|
469
|
-
|
|
743
|
+
// Construct Dynamic Usage-Aware Suggestions Summary
|
|
744
|
+
let suggestionsSummary = `
|
|
745
|
+
📢 Usage-Aware Suggestions Summary
|
|
746
|
+
══════════════════════════════════════════════════════════════════
|
|
747
|
+
- Analyzed timeframe: Last 7 days
|
|
748
|
+
- Found ${entries.length} historical usage entries in total.
|
|
749
|
+
- Calculated 7-day average daily cost: $${round2(dailyAvg).toFixed(4)}
|
|
750
|
+
`;
|
|
751
|
+
if (dailyAvg > 0) {
|
|
752
|
+
suggestionsSummary += `
|
|
753
|
+
Recommended Budget Limits (derived from daily avg $${round2(dailyAvg).toFixed(4)}):
|
|
754
|
+
- Daily Limit : $${dailyLimit.toFixed(2)} (Avg * 1.5 buffer, clamped min $0.50)
|
|
755
|
+
- Weekly Limit : $${weeklyLimit.toFixed(2)} (Avg * 7 * 1.3 buffer, clamped min $0.50)
|
|
756
|
+
- Monthly Limit : $${monthlyLimit.toFixed(2)} (Avg * 30 * 1.2 buffer, clamped min $0.50)
|
|
757
|
+
`;
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
suggestionsSummary += `
|
|
761
|
+
No active usage logs detected in the last 7 days.
|
|
762
|
+
- Applying fallback default budgets: Daily $5.00, Weekly $25.00, Monthly $100.00.
|
|
763
|
+
`;
|
|
764
|
+
}
|
|
765
|
+
if (suggestedProviders.length > 0) {
|
|
766
|
+
suggestionsSummary += `
|
|
767
|
+
Detected likely zero-cost providers:
|
|
768
|
+
`;
|
|
769
|
+
for (const { provider, reason } of suggestedProviders) {
|
|
770
|
+
suggestionsSummary += ` • ${provider} (${reason}, automatically pre-configured to $0.00)\n`;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (suggestedModels.length > 0) {
|
|
774
|
+
suggestionsSummary += `
|
|
775
|
+
Detected unrecognized fallback models:
|
|
776
|
+
`;
|
|
777
|
+
for (const m of suggestedModels) {
|
|
778
|
+
suggestionsSummary += ` • ${m} (automatically pre-configured with fallback $1/$4 rate)\n`;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
suggestionsSummary += ` ────────────────────────────────────────────────────────────────\n`;
|
|
782
|
+
// Print explanation first to stderr
|
|
783
|
+
const guideText = `
|
|
470
784
|
Configuration Guide
|
|
471
785
|
══════════════════════════════════════════════════════════════════
|
|
472
786
|
|
|
@@ -480,11 +794,11 @@ function cmdConfig(positional) {
|
|
|
480
794
|
|
|
481
795
|
Examples:
|
|
482
796
|
{ "input": 15, "output": 75 } = $15 per 1M input, $75 per 1M output
|
|
483
|
-
{ "input": 0, "output": 0 } = Free (subscription or local model)
|
|
797
|
+
{ "input": 0, "output": 0 } = Free (subscription or local provider/model)
|
|
484
798
|
|
|
485
799
|
Common scenarios:
|
|
486
800
|
- GitHub Copilot, Cursor, etc. → Set provider to { input: 0, output: 0 }
|
|
487
|
-
-
|
|
801
|
+
- Ollama, LM Studio, localhost → Set provider to { input: 0, output: 0 }
|
|
488
802
|
- Direct API usage → Look up provider's pricing page
|
|
489
803
|
|
|
490
804
|
Where to find pricing:
|
|
@@ -507,33 +821,36 @@ function cmdConfig(positional) {
|
|
|
507
821
|
|
|
508
822
|
────────────────────────────────────────────────────────────────
|
|
509
823
|
Example config based on your usage:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
`);
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
console.log(`
|
|
824
|
+
`;
|
|
825
|
+
process.stderr.write(suggestionsSummary + "\n");
|
|
826
|
+
process.stderr.write(guideText + "\n");
|
|
827
|
+
if (action === "init") {
|
|
828
|
+
// Print clean config to stdout ONLY on init
|
|
829
|
+
process.stdout.write(JSON.stringify(exampleConfig, null, 2) + "\n");
|
|
830
|
+
process.stderr.write(`
|
|
521
831
|
To create this config file, run:
|
|
522
832
|
opencode-tokens config generate
|
|
523
833
|
|
|
524
834
|
Or manually create: ${CONFIG_FILE}
|
|
525
|
-
`);
|
|
835
|
+
\n`);
|
|
836
|
+
}
|
|
837
|
+
else if (action === "generate") {
|
|
838
|
+
// Generate has completely empty stdout! Writes to config file, alerts to stderr
|
|
839
|
+
saveConfig(exampleConfig);
|
|
840
|
+
process.stderr.write(`
|
|
841
|
+
Config file created: ${CONFIG_FILE}
|
|
842
|
+
\n`);
|
|
526
843
|
}
|
|
527
844
|
return;
|
|
528
845
|
}
|
|
529
846
|
if (action === "get") {
|
|
530
847
|
const key = positional[2];
|
|
531
848
|
if (!key) {
|
|
532
|
-
|
|
849
|
+
failCli("Missing config key", "Usage: opencode-tokens config get <key>");
|
|
533
850
|
return;
|
|
534
851
|
}
|
|
535
852
|
if (!SETTABLE_KEYS[key]) {
|
|
536
|
-
|
|
853
|
+
failCli(`Unknown config key: ${key}`, `Available keys: ${Object.keys(SETTABLE_KEYS).join(", ")}`);
|
|
537
854
|
return;
|
|
538
855
|
}
|
|
539
856
|
const value = resolveConfigKey(config, key);
|
|
@@ -543,27 +860,31 @@ function cmdConfig(positional) {
|
|
|
543
860
|
if (action === "set") {
|
|
544
861
|
const key = positional[2];
|
|
545
862
|
const rawValue = positional[3];
|
|
546
|
-
if (!key
|
|
547
|
-
|
|
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>");
|
|
548
869
|
return;
|
|
549
870
|
}
|
|
550
871
|
const spec = SETTABLE_KEYS[key];
|
|
551
872
|
if (!spec) {
|
|
552
|
-
|
|
873
|
+
failCli(`Unknown config key: ${key}`, `Available keys: ${Object.keys(SETTABLE_KEYS).join(", ")}`);
|
|
553
874
|
return;
|
|
554
875
|
}
|
|
555
876
|
const value = parseConfigValue(rawValue);
|
|
556
877
|
if (typeof value !== spec.type) {
|
|
557
|
-
|
|
878
|
+
failCli(`Invalid type for ${key}: expected ${spec.type}, got ${typeof value}`);
|
|
558
879
|
return;
|
|
559
880
|
}
|
|
560
881
|
if (typeof value === "number") {
|
|
561
882
|
if (value < 0) {
|
|
562
|
-
|
|
883
|
+
failCli(`Invalid value for ${key}: must be >= 0`);
|
|
563
884
|
return;
|
|
564
885
|
}
|
|
565
886
|
if (spec.max !== undefined && value > spec.max) {
|
|
566
|
-
|
|
887
|
+
failCli(`Invalid value for ${key}: must be <= ${spec.max}`);
|
|
567
888
|
return;
|
|
568
889
|
}
|
|
569
890
|
}
|
|
@@ -574,12 +895,12 @@ function cmdConfig(positional) {
|
|
|
574
895
|
if (action === "unset") {
|
|
575
896
|
const key = positional[2];
|
|
576
897
|
if (!key) {
|
|
577
|
-
|
|
898
|
+
failCli("Missing config key", "Usage: opencode-tokens config unset <key>");
|
|
578
899
|
return;
|
|
579
900
|
}
|
|
580
901
|
const spec = SETTABLE_KEYS[key];
|
|
581
902
|
if (!spec) {
|
|
582
|
-
|
|
903
|
+
failCli(`Unknown config key: ${key}`, `Available keys: ${Object.keys(SETTABLE_KEYS).join(", ")}`);
|
|
583
904
|
return;
|
|
584
905
|
}
|
|
585
906
|
applyConfigUnset(key);
|
|
@@ -587,8 +908,7 @@ function cmdConfig(positional) {
|
|
|
587
908
|
return;
|
|
588
909
|
}
|
|
589
910
|
if (action && action !== "show") {
|
|
590
|
-
|
|
591
|
-
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]");
|
|
592
912
|
return;
|
|
593
913
|
}
|
|
594
914
|
// Show current config
|
|
@@ -692,10 +1012,48 @@ function saveConfig(raw) {
|
|
|
692
1012
|
// ============================================================================
|
|
693
1013
|
// Export
|
|
694
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
|
+
}
|
|
695
1031
|
function cmdExport(flags) {
|
|
696
|
-
const
|
|
697
|
-
const
|
|
1032
|
+
const formatValue = flagValue(flags, "format");
|
|
1033
|
+
const periodValue = flagValue(flags, "period");
|
|
698
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
|
+
}
|
|
699
1057
|
const now = new Date();
|
|
700
1058
|
let since;
|
|
701
1059
|
switch (period) {
|
|
@@ -739,7 +1097,14 @@ function cmdExport(flags) {
|
|
|
739
1097
|
output = `${[headers.join(","), ...rows].join("\n")}\n`;
|
|
740
1098
|
}
|
|
741
1099
|
if (outputFile) {
|
|
742
|
-
|
|
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
|
+
}
|
|
743
1108
|
console.log(`\n Exported ${entries.length} entries to ${outputFile}\n`);
|
|
744
1109
|
}
|
|
745
1110
|
else {
|
|
@@ -777,6 +1142,8 @@ function cmdBudget() {
|
|
|
777
1142
|
}
|
|
778
1143
|
|
|
779
1144
|
Run: opencode-tokens config init for more details.
|
|
1145
|
+
|
|
1146
|
+
Costs are estimates from local logs; budgets are warnings, not enforcement.
|
|
780
1147
|
`);
|
|
781
1148
|
return;
|
|
782
1149
|
}
|
|
@@ -841,6 +1208,7 @@ function cmdBudget() {
|
|
|
841
1208
|
console.log();
|
|
842
1209
|
}
|
|
843
1210
|
console.log(` Legend: 🟢 OK 🟡 Warning (>${Math.round(warnAt * 100)}%) 🔴 Exceeded`);
|
|
1211
|
+
console.log(` Costs are estimates from local logs; budgets are warnings, not enforcement.`);
|
|
844
1212
|
console.log();
|
|
845
1213
|
}
|
|
846
1214
|
function cmdHelp() {
|
|
@@ -853,6 +1221,7 @@ function cmdHelp() {
|
|
|
853
1221
|
Commands:
|
|
854
1222
|
(default) Show usage statistics
|
|
855
1223
|
budget Show budget status (daily/weekly/monthly)
|
|
1224
|
+
doctor Diagnose plugin config, logs, and pricing fallbacks
|
|
856
1225
|
pricing Show built-in pricing table
|
|
857
1226
|
models Show your used models and their pricing status
|
|
858
1227
|
config Show/generate/modify configuration
|
|
@@ -891,14 +1260,21 @@ function cmdHelp() {
|
|
|
891
1260
|
|
|
892
1261
|
Examples:
|
|
893
1262
|
opencode-tokens # All-time summary
|
|
1263
|
+
opencode-tokens doctor # Diagnose setup and logs
|
|
894
1264
|
opencode-tokens today --by model # Today by model
|
|
895
1265
|
opencode-tokens week --by session # This week by session
|
|
896
1266
|
opencode-tokens trend --days 7 # 7-day cost trend
|
|
897
1267
|
opencode-tokens export --format csv # Export all data as CSV
|
|
898
1268
|
opencode-tokens config set budget.daily 10 # Set daily budget to $10
|
|
899
1269
|
opencode-tokens config get toast.enabled # Check if toast is enabled
|
|
1270
|
+
|
|
1271
|
+
Costs are estimates from local logs; budgets are warnings, not enforcement.
|
|
900
1272
|
`);
|
|
901
1273
|
}
|
|
1274
|
+
const TREND_METRICS = ["cost", "tokens", "messages"];
|
|
1275
|
+
function isTrendMetric(metric) {
|
|
1276
|
+
return TREND_METRICS.includes(metric);
|
|
1277
|
+
}
|
|
902
1278
|
function getTrendValue(point, metric) {
|
|
903
1279
|
return metric === "tokens" ? point.tokens : metric === "messages" ? point.messages : point.cost;
|
|
904
1280
|
}
|
|
@@ -912,6 +1288,25 @@ function formatSignedTrendValue(value, metric) {
|
|
|
912
1288
|
function metricLabel(metric) {
|
|
913
1289
|
return metric === "tokens" ? "Token Trend" : metric === "messages" ? "Message Trend" : "Cost Trend";
|
|
914
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
|
+
}
|
|
915
1310
|
function buildLineRows(points, width, height) {
|
|
916
1311
|
const grid = Array.from({ length: height }, () => Array.from({ length: width }, () => " "));
|
|
917
1312
|
// 1. Calculate the exact integer row r[x] for every column x from 0 to width - 1
|
|
@@ -1018,9 +1413,22 @@ function buildTrendXAxis(points, chartPoints, chartWidth) {
|
|
|
1018
1413
|
};
|
|
1019
1414
|
}
|
|
1020
1415
|
function cmdTrend(flags) {
|
|
1021
|
-
const days =
|
|
1022
|
-
|
|
1023
|
-
|
|
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;
|
|
1024
1432
|
const since = getStartOfDay(new Date(Date.now() - days * 86400000));
|
|
1025
1433
|
const entries = loadEntries(since);
|
|
1026
1434
|
if (entries.length === 0) {
|
|
@@ -1127,33 +1535,45 @@ function main() {
|
|
|
1127
1535
|
return;
|
|
1128
1536
|
}
|
|
1129
1537
|
const { command } = parsed;
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
+
}
|
|
1150
1562
|
// Default: stats
|
|
1151
1563
|
let period = "all";
|
|
1152
|
-
const breakdown =
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
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;
|
|
1157
1577
|
}
|
|
1158
1578
|
}
|
|
1159
1579
|
cmdStats(period, breakdown);
|