@diogonzafe/tokenwatch 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/dist/cli.js +405 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +7 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/prices.json +7 -1
package/README.md
CHANGED
|
@@ -577,12 +577,27 @@ Anomaly webhook payload example:
|
|
|
577
577
|
```bash
|
|
578
578
|
npx tokenwatch sync # force update cached prices from remote
|
|
579
579
|
npx tokenwatch prices # list all models and current prices
|
|
580
|
-
npx tokenwatch report # show usage report
|
|
580
|
+
npx tokenwatch report # show usage report (SQLite default)
|
|
581
|
+
npx tokenwatch report --db postgres://user:pass@host:5432/db
|
|
581
582
|
npx tokenwatch dashboard # open local web dashboard (default port: 4242)
|
|
582
583
|
npx tokenwatch dashboard --port 8080
|
|
584
|
+
npx tokenwatch dashboard --db postgres://user:pass@host:5432/db
|
|
585
|
+
npx tokenwatch dashboard --db mysql://user:pass@host:3306/db
|
|
586
|
+
npx tokenwatch dashboard --db mongodb://user:pass@host:27017/db
|
|
583
587
|
npx tokenwatch help # show help
|
|
584
588
|
```
|
|
585
589
|
|
|
590
|
+
Both `report` and `dashboard` accept a `--db <url>` flag to connect directly to any database — no SQLite required. The URL protocol determines the adapter automatically:
|
|
591
|
+
|
|
592
|
+
| URL prefix | Adapter | Peer dep required |
|
|
593
|
+
|---|---|---|
|
|
594
|
+
| *(none)* | SQLite (`~/.tokenwatch/usage.db`) | `better-sqlite3` |
|
|
595
|
+
| `postgres://` or `postgresql://` | `PostgresStorage` | `pg` |
|
|
596
|
+
| `mysql://` | `MySQLStorage` | `mysql2` |
|
|
597
|
+
| `mongodb://` or `mongodb+srv://` | `MongoStorage` | `mongodb` |
|
|
598
|
+
|
|
599
|
+
If the required peer dep is not installed, a clear error message with the install command is shown.
|
|
600
|
+
|
|
586
601
|
### `tokenwatch report`
|
|
587
602
|
|
|
588
603
|
Reads the local SQLite database and prints:
|
|
@@ -617,7 +632,16 @@ Spins up a local web server and opens a dark-themed dashboard with real-time cos
|
|
|
617
632
|
- **Cost forecast** — projected daily and monthly spend based on recent burn rate
|
|
618
633
|
- **Time filter tabs** — 1h | 24h | 7d | 30d | All; updates chart and tables in real-time via SSE
|
|
619
634
|
|
|
620
|
-
Data updates automatically every 3 seconds without refreshing the page.
|
|
635
|
+
Data updates automatically every 3 seconds without refreshing the page. Zero external dependencies — pure Node.js HTTP server with Chart.js loaded from CDN.
|
|
636
|
+
|
|
637
|
+
Works with **any storage backend** via `--db <url>`:
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
tokenwatch dashboard # SQLite (default)
|
|
641
|
+
tokenwatch dashboard --db postgres://user:pass@host:5432/db # PostgreSQL
|
|
642
|
+
tokenwatch dashboard --db mysql://user:pass@host:3306/db # MySQL
|
|
643
|
+
tokenwatch dashboard --db mongodb://user:pass@host:27017/db # MongoDB
|
|
644
|
+
```
|
|
621
645
|
|
|
622
646
|
---
|
|
623
647
|
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,286 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/adapters/postgres.ts
|
|
13
|
+
var postgres_exports = {};
|
|
14
|
+
__export(postgres_exports, {
|
|
15
|
+
PostgresStorage: () => PostgresStorage
|
|
16
|
+
});
|
|
17
|
+
function rowToEntry(r) {
|
|
18
|
+
const reasoningTokens = r["reasoning_tokens"] ?? 0;
|
|
19
|
+
const cachedTokens = r["cached_tokens"] ?? 0;
|
|
20
|
+
const cacheCreationTokens = r["cache_creation_tokens"] ?? 0;
|
|
21
|
+
return {
|
|
22
|
+
model: r["model"],
|
|
23
|
+
inputTokens: r["input_tokens"],
|
|
24
|
+
outputTokens: r["output_tokens"],
|
|
25
|
+
...reasoningTokens > 0 && { reasoningTokens },
|
|
26
|
+
...cachedTokens > 0 && { cachedTokens },
|
|
27
|
+
...cacheCreationTokens > 0 && { cacheCreationTokens },
|
|
28
|
+
costUSD: Number(r["cost_usd"]),
|
|
29
|
+
...r["session_id"] != null && { sessionId: r["session_id"] },
|
|
30
|
+
...r["user_id"] != null && { userId: r["user_id"] },
|
|
31
|
+
...r["feature"] != null && { feature: r["feature"] },
|
|
32
|
+
timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
var PostgresStorage;
|
|
36
|
+
var init_postgres = __esm({
|
|
37
|
+
"src/adapters/postgres.ts"() {
|
|
38
|
+
"use strict";
|
|
39
|
+
PostgresStorage = class {
|
|
40
|
+
constructor(client) {
|
|
41
|
+
this.client = client;
|
|
42
|
+
}
|
|
43
|
+
client;
|
|
44
|
+
/** Creates the `tokenwatch_usage` table if it does not already exist.
|
|
45
|
+
* Also adds new columns for databases created before v0.2.0 / v0.3.0. */
|
|
46
|
+
async migrate() {
|
|
47
|
+
await this.client.query(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS tokenwatch_usage (
|
|
49
|
+
id BIGSERIAL PRIMARY KEY,
|
|
50
|
+
model TEXT NOT NULL,
|
|
51
|
+
input_tokens INTEGER NOT NULL,
|
|
52
|
+
output_tokens INTEGER NOT NULL,
|
|
53
|
+
reasoning_tokens INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
cached_tokens INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
cost_usd NUMERIC NOT NULL,
|
|
57
|
+
session_id TEXT,
|
|
58
|
+
user_id TEXT,
|
|
59
|
+
feature TEXT,
|
|
60
|
+
timestamp TIMESTAMPTZ NOT NULL
|
|
61
|
+
)
|
|
62
|
+
`);
|
|
63
|
+
for (const col of [
|
|
64
|
+
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS reasoning_tokens INTEGER NOT NULL DEFAULT 0",
|
|
65
|
+
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS feature TEXT",
|
|
66
|
+
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cached_tokens INTEGER NOT NULL DEFAULT 0",
|
|
67
|
+
"ALTER TABLE tokenwatch_usage ADD COLUMN IF NOT EXISTS cache_creation_tokens INTEGER NOT NULL DEFAULT 0"
|
|
68
|
+
]) {
|
|
69
|
+
await this.client.query(col).catch(() => {
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
record(entry) {
|
|
74
|
+
this.client.query(
|
|
75
|
+
`INSERT INTO tokenwatch_usage
|
|
76
|
+
(model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
|
|
77
|
+
cost_usd, session_id, user_id, feature, timestamp)
|
|
78
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
|
79
|
+
[
|
|
80
|
+
entry.model,
|
|
81
|
+
entry.inputTokens,
|
|
82
|
+
entry.outputTokens,
|
|
83
|
+
entry.reasoningTokens ?? 0,
|
|
84
|
+
entry.cachedTokens ?? 0,
|
|
85
|
+
entry.cacheCreationTokens ?? 0,
|
|
86
|
+
entry.costUSD,
|
|
87
|
+
entry.sessionId ?? null,
|
|
88
|
+
entry.userId ?? null,
|
|
89
|
+
entry.feature ?? null,
|
|
90
|
+
entry.timestamp
|
|
91
|
+
]
|
|
92
|
+
).catch((err) => {
|
|
93
|
+
console.warn("[tokenwatch] PostgresStorage.record failed:", err);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async getAll() {
|
|
97
|
+
const result = await this.client.query(
|
|
98
|
+
"SELECT * FROM tokenwatch_usage ORDER BY timestamp ASC"
|
|
99
|
+
);
|
|
100
|
+
return result.rows.map(rowToEntry);
|
|
101
|
+
}
|
|
102
|
+
async clearAll() {
|
|
103
|
+
await this.client.query("DELETE FROM tokenwatch_usage");
|
|
104
|
+
}
|
|
105
|
+
async clearSession(sessionId) {
|
|
106
|
+
await this.client.query(
|
|
107
|
+
"DELETE FROM tokenwatch_usage WHERE session_id = $1",
|
|
108
|
+
[sessionId]
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// src/adapters/mysql.ts
|
|
116
|
+
var mysql_exports = {};
|
|
117
|
+
__export(mysql_exports, {
|
|
118
|
+
MySQLStorage: () => MySQLStorage
|
|
119
|
+
});
|
|
120
|
+
function rowToEntry2(r) {
|
|
121
|
+
const reasoningTokens = r["reasoning_tokens"] ?? 0;
|
|
122
|
+
const cachedTokens = r["cached_tokens"] ?? 0;
|
|
123
|
+
const cacheCreationTokens = r["cache_creation_tokens"] ?? 0;
|
|
124
|
+
return {
|
|
125
|
+
model: r["model"],
|
|
126
|
+
inputTokens: r["input_tokens"],
|
|
127
|
+
outputTokens: r["output_tokens"],
|
|
128
|
+
...reasoningTokens > 0 && { reasoningTokens },
|
|
129
|
+
...cachedTokens > 0 && { cachedTokens },
|
|
130
|
+
...cacheCreationTokens > 0 && { cacheCreationTokens },
|
|
131
|
+
costUSD: Number(r["cost_usd"]),
|
|
132
|
+
...r["session_id"] != null && { sessionId: r["session_id"] },
|
|
133
|
+
...r["user_id"] != null && { userId: r["user_id"] },
|
|
134
|
+
...r["feature"] != null && { feature: r["feature"] },
|
|
135
|
+
timestamp: r["timestamp"] instanceof Date ? r["timestamp"].toISOString() : r["timestamp"]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
var MySQLStorage;
|
|
139
|
+
var init_mysql = __esm({
|
|
140
|
+
"src/adapters/mysql.ts"() {
|
|
141
|
+
"use strict";
|
|
142
|
+
MySQLStorage = class {
|
|
143
|
+
constructor(client) {
|
|
144
|
+
this.client = client;
|
|
145
|
+
}
|
|
146
|
+
client;
|
|
147
|
+
/** Creates the `tokenwatch_usage` table if it does not already exist.
|
|
148
|
+
* Also adds new columns for databases created before v0.2.0 / v0.3.0. */
|
|
149
|
+
async migrate() {
|
|
150
|
+
await this.client.execute(`
|
|
151
|
+
CREATE TABLE IF NOT EXISTS tokenwatch_usage (
|
|
152
|
+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
153
|
+
model VARCHAR(255) NOT NULL,
|
|
154
|
+
input_tokens INT NOT NULL,
|
|
155
|
+
output_tokens INT NOT NULL,
|
|
156
|
+
reasoning_tokens INT NOT NULL DEFAULT 0,
|
|
157
|
+
cached_tokens INT NOT NULL DEFAULT 0,
|
|
158
|
+
cache_creation_tokens INT NOT NULL DEFAULT 0,
|
|
159
|
+
cost_usd DECIMAL(18,8) NOT NULL,
|
|
160
|
+
session_id VARCHAR(255),
|
|
161
|
+
user_id VARCHAR(255),
|
|
162
|
+
feature VARCHAR(255),
|
|
163
|
+
timestamp DATETIME(3) NOT NULL
|
|
164
|
+
)
|
|
165
|
+
`);
|
|
166
|
+
await this.client.execute(`
|
|
167
|
+
ALTER TABLE tokenwatch_usage
|
|
168
|
+
ADD COLUMN IF NOT EXISTS reasoning_tokens INT NOT NULL DEFAULT 0,
|
|
169
|
+
ADD COLUMN IF NOT EXISTS feature VARCHAR(255),
|
|
170
|
+
ADD COLUMN IF NOT EXISTS cached_tokens INT NOT NULL DEFAULT 0,
|
|
171
|
+
ADD COLUMN IF NOT EXISTS cache_creation_tokens INT NOT NULL DEFAULT 0
|
|
172
|
+
`).catch(() => {
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
record(entry) {
|
|
176
|
+
this.client.execute(
|
|
177
|
+
`INSERT INTO tokenwatch_usage
|
|
178
|
+
(model, input_tokens, output_tokens, reasoning_tokens, cached_tokens, cache_creation_tokens,
|
|
179
|
+
cost_usd, session_id, user_id, feature, timestamp)
|
|
180
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
181
|
+
[
|
|
182
|
+
entry.model,
|
|
183
|
+
entry.inputTokens,
|
|
184
|
+
entry.outputTokens,
|
|
185
|
+
entry.reasoningTokens ?? 0,
|
|
186
|
+
entry.cachedTokens ?? 0,
|
|
187
|
+
entry.cacheCreationTokens ?? 0,
|
|
188
|
+
entry.costUSD,
|
|
189
|
+
entry.sessionId ?? null,
|
|
190
|
+
entry.userId ?? null,
|
|
191
|
+
entry.feature ?? null,
|
|
192
|
+
entry.timestamp
|
|
193
|
+
]
|
|
194
|
+
).catch((err) => {
|
|
195
|
+
console.warn("[tokenwatch] MySQLStorage.record failed:", err);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async getAll() {
|
|
199
|
+
const [rows] = await this.client.execute(
|
|
200
|
+
"SELECT * FROM tokenwatch_usage ORDER BY timestamp ASC"
|
|
201
|
+
);
|
|
202
|
+
return rows.map(rowToEntry2);
|
|
203
|
+
}
|
|
204
|
+
async clearAll() {
|
|
205
|
+
await this.client.execute("DELETE FROM tokenwatch_usage");
|
|
206
|
+
}
|
|
207
|
+
async clearSession(sessionId) {
|
|
208
|
+
await this.client.execute(
|
|
209
|
+
"DELETE FROM tokenwatch_usage WHERE session_id = ?",
|
|
210
|
+
[sessionId]
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// src/adapters/mongodb.ts
|
|
218
|
+
var mongodb_exports = {};
|
|
219
|
+
__export(mongodb_exports, {
|
|
220
|
+
MongoStorage: () => MongoStorage
|
|
221
|
+
});
|
|
222
|
+
function docToEntry(doc) {
|
|
223
|
+
return {
|
|
224
|
+
model: doc.model,
|
|
225
|
+
inputTokens: doc.inputTokens,
|
|
226
|
+
outputTokens: doc.outputTokens,
|
|
227
|
+
...doc.reasoningTokens != null && doc.reasoningTokens > 0 && { reasoningTokens: doc.reasoningTokens },
|
|
228
|
+
...doc.cachedTokens != null && doc.cachedTokens > 0 && { cachedTokens: doc.cachedTokens },
|
|
229
|
+
...doc.cacheCreationTokens != null && doc.cacheCreationTokens > 0 && { cacheCreationTokens: doc.cacheCreationTokens },
|
|
230
|
+
costUSD: doc.costUSD,
|
|
231
|
+
...doc.sessionId != null && { sessionId: doc.sessionId },
|
|
232
|
+
...doc.userId != null && { userId: doc.userId },
|
|
233
|
+
...doc.feature != null && { feature: doc.feature },
|
|
234
|
+
timestamp: doc.timestamp
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
var COLLECTION, MongoStorage;
|
|
238
|
+
var init_mongodb = __esm({
|
|
239
|
+
"src/adapters/mongodb.ts"() {
|
|
240
|
+
"use strict";
|
|
241
|
+
COLLECTION = "tokenwatch_usage";
|
|
242
|
+
MongoStorage = class {
|
|
243
|
+
col;
|
|
244
|
+
constructor(db) {
|
|
245
|
+
this.col = db.collection(COLLECTION);
|
|
246
|
+
}
|
|
247
|
+
/** Creates recommended indexes for query performance. Call once at startup. */
|
|
248
|
+
async createIndexes() {
|
|
249
|
+
await this.col.createIndex({ timestamp: 1 });
|
|
250
|
+
await this.col.createIndex({ sessionId: 1 });
|
|
251
|
+
await this.col.createIndex({ userId: 1 });
|
|
252
|
+
await this.col.createIndex({ model: 1 });
|
|
253
|
+
}
|
|
254
|
+
record(entry) {
|
|
255
|
+
this.col.insertOne({
|
|
256
|
+
model: entry.model,
|
|
257
|
+
inputTokens: entry.inputTokens,
|
|
258
|
+
outputTokens: entry.outputTokens,
|
|
259
|
+
...entry.reasoningTokens !== void 0 && { reasoningTokens: entry.reasoningTokens },
|
|
260
|
+
...entry.cachedTokens !== void 0 && { cachedTokens: entry.cachedTokens },
|
|
261
|
+
...entry.cacheCreationTokens !== void 0 && { cacheCreationTokens: entry.cacheCreationTokens },
|
|
262
|
+
costUSD: entry.costUSD,
|
|
263
|
+
sessionId: entry.sessionId ?? null,
|
|
264
|
+
userId: entry.userId ?? null,
|
|
265
|
+
...entry.feature !== void 0 && { feature: entry.feature },
|
|
266
|
+
timestamp: entry.timestamp
|
|
267
|
+
}).catch((err) => {
|
|
268
|
+
console.warn("[tokenwatch] MongoStorage.record failed:", err);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
async getAll() {
|
|
272
|
+
const docs = await this.col.find({}).sort({ timestamp: 1 }).toArray();
|
|
273
|
+
return docs.map(docToEntry);
|
|
274
|
+
}
|
|
275
|
+
async clearAll() {
|
|
276
|
+
await this.col.deleteMany({});
|
|
277
|
+
}
|
|
278
|
+
async clearSession(sessionId) {
|
|
279
|
+
await this.col.deleteMany({ sessionId });
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
});
|
|
2
284
|
|
|
3
285
|
// bin/cli.ts
|
|
4
286
|
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
@@ -245,7 +527,7 @@ function maybeSuggestCheaperModel(model, costUSD, inputTokens, outputTokens, lay
|
|
|
245
527
|
|
|
246
528
|
// prices.json
|
|
247
529
|
var prices_default = {
|
|
248
|
-
updated_at: "2026-04-
|
|
530
|
+
updated_at: "2026-04-24",
|
|
249
531
|
source: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
|
|
250
532
|
models: {
|
|
251
533
|
"gpt-4o": {
|
|
@@ -1562,6 +1844,12 @@ var prices_default = {
|
|
|
1562
1844
|
cachedInput: 0.3,
|
|
1563
1845
|
cacheCreationInput: 3.75,
|
|
1564
1846
|
maxInputTokens: 1e6
|
|
1847
|
+
},
|
|
1848
|
+
"gpt-5.5": {
|
|
1849
|
+
input: 5,
|
|
1850
|
+
output: 30,
|
|
1851
|
+
cachedInput: 0.5,
|
|
1852
|
+
maxInputTokens: 272e3
|
|
1565
1853
|
}
|
|
1566
1854
|
}
|
|
1567
1855
|
};
|
|
@@ -2746,7 +3034,82 @@ function startDashboardServer(storage, port) {
|
|
|
2746
3034
|
|
|
2747
3035
|
// bin/cli.ts
|
|
2748
3036
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2749
|
-
var
|
|
3037
|
+
var DEFAULT_DB_PATH = join3(homedir3(), ".tokenwatch", "usage.db");
|
|
3038
|
+
function getFlag(args, flag) {
|
|
3039
|
+
const idx = args.indexOf(flag);
|
|
3040
|
+
if (idx === -1) return void 0;
|
|
3041
|
+
const value = args[idx + 1];
|
|
3042
|
+
return value !== void 0 && !value.startsWith("--") ? value : void 0;
|
|
3043
|
+
}
|
|
3044
|
+
async function openStorage(dbUrl) {
|
|
3045
|
+
if (!dbUrl) {
|
|
3046
|
+
if (!existsSync2(DEFAULT_DB_PATH)) {
|
|
3047
|
+
console.error(`No SQLite database found at ${DEFAULT_DB_PATH}`);
|
|
3048
|
+
console.error("Start your app with storage: 'sqlite' to begin recording usage.");
|
|
3049
|
+
console.error("Or pass --db <url> to connect to Postgres, MySQL, or MongoDB.");
|
|
3050
|
+
process.exit(1);
|
|
3051
|
+
}
|
|
3052
|
+
let storage;
|
|
3053
|
+
try {
|
|
3054
|
+
storage = new SqliteStorage(DEFAULT_DB_PATH);
|
|
3055
|
+
} catch {
|
|
3056
|
+
console.error("Failed to open SQLite database. Is better-sqlite3 installed?");
|
|
3057
|
+
console.error("Run: npm install better-sqlite3");
|
|
3058
|
+
process.exit(1);
|
|
3059
|
+
}
|
|
3060
|
+
return { storage, close: async () => {
|
|
3061
|
+
} };
|
|
3062
|
+
}
|
|
3063
|
+
if (dbUrl.startsWith("postgres://") || dbUrl.startsWith("postgresql://")) {
|
|
3064
|
+
let pgMod;
|
|
3065
|
+
try {
|
|
3066
|
+
pgMod = (await import("pg")).default;
|
|
3067
|
+
} catch {
|
|
3068
|
+
console.error("[tokenwatch] Postgres requires the pg package.");
|
|
3069
|
+
console.error("Run: npm install pg");
|
|
3070
|
+
process.exit(1);
|
|
3071
|
+
}
|
|
3072
|
+
const { PostgresStorage: PostgresStorage2 } = await Promise.resolve().then(() => (init_postgres(), postgres_exports));
|
|
3073
|
+
const pool = new pgMod.Pool({ connectionString: dbUrl });
|
|
3074
|
+
const storage = new PostgresStorage2(pool);
|
|
3075
|
+
return { storage, close: () => pool.end() };
|
|
3076
|
+
}
|
|
3077
|
+
if (dbUrl.startsWith("mysql://")) {
|
|
3078
|
+
let mysqlMod;
|
|
3079
|
+
try {
|
|
3080
|
+
mysqlMod = await import("mysql2/promise");
|
|
3081
|
+
} catch {
|
|
3082
|
+
console.error("[tokenwatch] MySQL requires the mysql2 package.");
|
|
3083
|
+
console.error("Run: npm install mysql2");
|
|
3084
|
+
process.exit(1);
|
|
3085
|
+
}
|
|
3086
|
+
const { MySQLStorage: MySQLStorage2 } = await Promise.resolve().then(() => (init_mysql(), mysql_exports));
|
|
3087
|
+
const pool = mysqlMod.createPool(dbUrl);
|
|
3088
|
+
const storage = new MySQLStorage2(pool);
|
|
3089
|
+
return { storage, close: () => pool.end() };
|
|
3090
|
+
}
|
|
3091
|
+
if (dbUrl.startsWith("mongodb://") || dbUrl.startsWith("mongodb+srv://")) {
|
|
3092
|
+
let mongoMod;
|
|
3093
|
+
try {
|
|
3094
|
+
mongoMod = await import("mongodb");
|
|
3095
|
+
} catch {
|
|
3096
|
+
console.error("[tokenwatch] MongoDB requires the mongodb package.");
|
|
3097
|
+
console.error("Run: npm install mongodb");
|
|
3098
|
+
process.exit(1);
|
|
3099
|
+
}
|
|
3100
|
+
const { MongoStorage: MongoStorage2 } = await Promise.resolve().then(() => (init_mongodb(), mongodb_exports));
|
|
3101
|
+
const urlObj = new URL(dbUrl);
|
|
3102
|
+
const dbName = urlObj.pathname.replace(/^\//, "") || "tokenwatch";
|
|
3103
|
+
const client = new mongoMod.MongoClient(dbUrl);
|
|
3104
|
+
await client.connect();
|
|
3105
|
+
const db = client.db(dbName);
|
|
3106
|
+
const storage = new MongoStorage2(db);
|
|
3107
|
+
return { storage, close: () => client.close() };
|
|
3108
|
+
}
|
|
3109
|
+
console.error(`[tokenwatch] Unsupported database URL: "${dbUrl}"`);
|
|
3110
|
+
console.error("Supported protocols: postgres://, mysql://, mongodb://, mongodb+srv://");
|
|
3111
|
+
process.exit(1);
|
|
3112
|
+
}
|
|
2750
3113
|
function loadBundledPrices() {
|
|
2751
3114
|
const pricesPath = join3(__dirname, "..", "prices.json");
|
|
2752
3115
|
const raw = readFileSync(pricesPath, "utf8");
|
|
@@ -2779,22 +3142,16 @@ function cmdPrices() {
|
|
|
2779
3142
|
console.log(`${row.model.padEnd(maxName)} ${row.input.padStart(12)} ${row.output.padStart(12)}`);
|
|
2780
3143
|
}
|
|
2781
3144
|
}
|
|
2782
|
-
async function cmdReport() {
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
return;
|
|
2787
|
-
}
|
|
2788
|
-
let storage;
|
|
3145
|
+
async function cmdReport(args) {
|
|
3146
|
+
const dbUrl = getFlag(args, "--db");
|
|
3147
|
+
const { storage, close } = await openStorage(dbUrl);
|
|
3148
|
+
let report;
|
|
2789
3149
|
try {
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
process.exit(1);
|
|
3150
|
+
const tracker = createTracker({ storage, syncPrices: false });
|
|
3151
|
+
report = await tracker.getReport();
|
|
3152
|
+
} finally {
|
|
3153
|
+
await close();
|
|
2795
3154
|
}
|
|
2796
|
-
const tracker = createTracker({ storage, syncPrices: false });
|
|
2797
|
-
const report = await tracker.getReport();
|
|
2798
3155
|
if (report.totalCostUSD === 0 && Object.keys(report.byModel).length === 0) {
|
|
2799
3156
|
console.log("No usage recorded yet.");
|
|
2800
3157
|
return;
|
|
@@ -2832,20 +3189,20 @@ async function cmdReport() {
|
|
|
2832
3189
|
}
|
|
2833
3190
|
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
2834
3191
|
}
|
|
2835
|
-
async function cmdDashboard(
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
}
|
|
2841
|
-
let storage;
|
|
2842
|
-
try {
|
|
2843
|
-
storage = new SqliteStorage(DB_PATH2);
|
|
2844
|
-
} catch {
|
|
2845
|
-
console.error("Failed to open SQLite database. Is better-sqlite3 installed?");
|
|
2846
|
-
console.error("Run: npm install better-sqlite3");
|
|
3192
|
+
async function cmdDashboard(args) {
|
|
3193
|
+
const portFlag = getFlag(args, "--port");
|
|
3194
|
+
const port = portFlag !== void 0 ? parseInt(portFlag, 10) : 4242;
|
|
3195
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
3196
|
+
console.error(`[tokenwatch] Invalid port: "${portFlag}". Must be a number between 1 and 65535.`);
|
|
2847
3197
|
process.exit(1);
|
|
2848
3198
|
}
|
|
3199
|
+
const dbUrl = getFlag(args, "--db");
|
|
3200
|
+
const { storage, close } = await openStorage(dbUrl);
|
|
3201
|
+
const shutdown = () => {
|
|
3202
|
+
void close().then(() => process.exit(0));
|
|
3203
|
+
};
|
|
3204
|
+
process.on("SIGINT", shutdown);
|
|
3205
|
+
process.on("SIGTERM", shutdown);
|
|
2849
3206
|
startDashboardServer(storage, port);
|
|
2850
3207
|
}
|
|
2851
3208
|
function cmdHelp() {
|
|
@@ -2853,11 +3210,23 @@ function cmdHelp() {
|
|
|
2853
3210
|
tokenwatch \u2014 CLI
|
|
2854
3211
|
|
|
2855
3212
|
Commands:
|
|
2856
|
-
sync
|
|
2857
|
-
prices
|
|
2858
|
-
report
|
|
2859
|
-
dashboard [--port N]
|
|
2860
|
-
|
|
3213
|
+
sync Fetch and cache latest model prices from remote
|
|
3214
|
+
prices List all bundled models and their current prices
|
|
3215
|
+
report [--db <url>] Show usage report (default: SQLite at ~/.tokenwatch/usage.db)
|
|
3216
|
+
dashboard [--port N] Open local web dashboard (default port: 4242)
|
|
3217
|
+
[--db <url>] Connect to a database instead of the default SQLite
|
|
3218
|
+
|
|
3219
|
+
Database URL formats:
|
|
3220
|
+
(none) ~/.tokenwatch/usage.db (SQLite, default)
|
|
3221
|
+
postgres://user:pass@host:5432/dbname
|
|
3222
|
+
mysql://user:pass@host:3306/dbname
|
|
3223
|
+
mongodb://user:pass@host:27017/dbname
|
|
3224
|
+
|
|
3225
|
+
Examples:
|
|
3226
|
+
tokenwatch dashboard
|
|
3227
|
+
tokenwatch dashboard --port 8080
|
|
3228
|
+
tokenwatch dashboard --db postgres://user:pass@localhost:5432/myapp
|
|
3229
|
+
tokenwatch report --db mysql://root:pass@localhost:3306/myapp
|
|
2861
3230
|
`.trim());
|
|
2862
3231
|
}
|
|
2863
3232
|
async function main() {
|
|
@@ -2870,14 +3239,11 @@ async function main() {
|
|
|
2870
3239
|
cmdPrices();
|
|
2871
3240
|
break;
|
|
2872
3241
|
case "report":
|
|
2873
|
-
await cmdReport();
|
|
3242
|
+
await cmdReport(args);
|
|
2874
3243
|
break;
|
|
2875
|
-
case "dashboard":
|
|
2876
|
-
|
|
2877
|
-
const port = portFlagIdx !== -1 ? parseInt(args[portFlagIdx + 1] ?? "4242", 10) : 4242;
|
|
2878
|
-
await cmdDashboard(port);
|
|
3244
|
+
case "dashboard":
|
|
3245
|
+
await cmdDashboard(args);
|
|
2879
3246
|
break;
|
|
2880
|
-
}
|
|
2881
3247
|
case "help":
|
|
2882
3248
|
case void 0:
|
|
2883
3249
|
cmdHelp();
|