@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 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 from ~/.tokenwatch/usage.db
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. Requires `storage: 'sqlite'` in your app and `better-sqlite3` installed. Zero external dependencies — pure Node.js HTTP server with Chart.js loaded from CDN.
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-23",
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 DB_PATH2 = join3(homedir3(), ".tokenwatch", "usage.db");
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
- if (!existsSync2(DB_PATH2)) {
2784
- console.log(`No SQLite database found at ${DB_PATH2}`);
2785
- console.log("Start your app with storage: 'sqlite' to begin recording usage.");
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
- storage = new SqliteStorage(DB_PATH2);
2791
- } catch {
2792
- console.error("Failed to open SQLite database. Is better-sqlite3 installed?");
2793
- console.error("Run: npm install better-sqlite3");
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(port) {
2836
- if (!existsSync2(DB_PATH2)) {
2837
- console.log(`No SQLite database found at ${DB_PATH2}`);
2838
- console.log("Start your app with storage: 'sqlite' to begin recording usage.");
2839
- process.exit(1);
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 Fetch and cache latest model prices from remote
2857
- prices List all bundled models and their current prices
2858
- report Show last saved usage report (requires SQLite storage)
2859
- dashboard [--port N] Open local web dashboard (default port: 4242)
2860
- help Show this help message
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
- const portFlagIdx = args.indexOf("--port");
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();