getaimeter 0.10.0 → 0.11.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.
Files changed (5) hide show
  1. package/cli.js +657 -657
  2. package/config.js +6 -7
  3. package/package.json +1 -1
  4. package/tray.ps1 +208 -208
  5. package/watcher.js +47 -8
package/cli.js CHANGED
@@ -1,657 +1,657 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const { getApiKey, saveApiKey, getWatchPaths, AIMETER_DIR } = require('./config');
5
- const { startWatching } = require('./watcher');
6
- const { install, uninstall, isInstalled, startNow, stopNow } = require('./service');
7
- const { checkForUpdate, getCurrentVersion } = require('./update-check');
8
- const { startTray, stopTray } = require('./tray');
9
-
10
- const command = process.argv[2] || 'help';
11
-
12
- // Version check on interactive commands (non-blocking)
13
- if (['setup', 'status', 'summary', 'help', '--help', '-h'].includes(command)) {
14
- checkForUpdate();
15
- }
16
-
17
- switch (command) {
18
- case 'setup': runSetup(); break;
19
- case 'watch': runWatch(); break;
20
- case 'install': runInstall(); break;
21
- case 'uninstall': runUninstall(); break;
22
- case 'start': runStart(); break;
23
- case 'stop': runStop(); break;
24
- case 'status': runStatus(); break;
25
- case 'logs': runLogs(); break;
26
- case 'key': runKey(); break;
27
- case 'summary': runSummary(); break;
28
- case 'blocks': runBlocks(); break;
29
- case 'version': case '--version': case '-v':
30
- console.log(getCurrentVersion());
31
- break;
32
- case 'mcp': runMcp(); break;
33
- case 'help': case '--help': case '-h':
34
- printHelp();
35
- break;
36
- default:
37
- console.log(`Unknown command: ${command}\n`);
38
- printHelp();
39
- process.exitCode = 1;
40
- break;
41
- }
42
-
43
- // ---------------------------------------------------------------------------
44
- // fetchFromApi — shared helper for API calls (native https, zero deps)
45
- // ---------------------------------------------------------------------------
46
-
47
- function fetchFromApi(urlPath, apiKey) {
48
- return new Promise((resolve, reject) => {
49
- const https = require('https');
50
- const options = {
51
- hostname: 'aimeter-api.fly.dev',
52
- port: 443,
53
- path: urlPath,
54
- method: 'GET',
55
- headers: {
56
- 'X-AIMeter-Key': apiKey,
57
- },
58
- };
59
-
60
- const req = https.request(options, (res) => {
61
- let body = '';
62
- res.on('data', chunk => body += chunk);
63
- res.on('end', () => {
64
- if (res.statusCode >= 200 && res.statusCode < 300) {
65
- try { resolve(JSON.parse(body)); }
66
- catch { reject(new Error('Invalid JSON response')); }
67
- } else {
68
- reject(new Error(`HTTP ${res.statusCode}: ${body.slice(0, 200)}`));
69
- }
70
- });
71
- });
72
- req.on('error', reject);
73
- req.setTimeout(10000, () => { req.destroy(); reject(new Error('Request timeout')); });
74
- req.end();
75
- });
76
- }
77
-
78
- // ---------------------------------------------------------------------------
79
- // setup — full onboarding wizard
80
- // ---------------------------------------------------------------------------
81
-
82
- async function runSetup() {
83
- const readline = require('readline');
84
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
85
- const ask = (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
86
-
87
- console.log('');
88
- console.log(' ╔══════════════════════════════════╗');
89
- console.log(' ║ AIMeter Setup Wizard ║');
90
- console.log(' ╚══════════════════════════════════╝');
91
- console.log('');
92
-
93
- // Step 1: API Key
94
- console.log(' Step 1: API Key');
95
- console.log(' ───────────────');
96
- const existing = getApiKey();
97
- let key = existing;
98
-
99
- if (existing) {
100
- console.log(` Current key: ${existing.slice(0, 8)}...${existing.slice(-4)}`);
101
- const answer = await ask(' Press Enter to keep, or paste a new key: ');
102
- if (answer.length > 0) {
103
- if (!answer.startsWith('aim_')) {
104
- console.log(' ✗ Invalid key (must start with aim_)');
105
- rl.close();
106
- return;
107
- }
108
- saveApiKey(answer);
109
- key = answer;
110
- console.log(' ✓ Key updated.');
111
- } else {
112
- console.log(' ✓ Key unchanged.');
113
- }
114
- } else {
115
- console.log(' Sign up at: https://getaimeter.com');
116
- console.log(' Then copy your API key from the dashboard.\n');
117
- while (!key || !key.startsWith('aim_')) {
118
- key = await ask(' Paste your API key: ');
119
- if (!key.startsWith('aim_')) {
120
- console.log(' ✗ Invalid — keys start with aim_');
121
- }
122
- }
123
- saveApiKey(key);
124
- console.log(' ✓ Key saved.');
125
- }
126
-
127
- // Step 2: Check watch paths
128
- console.log('');
129
- console.log(' Step 2: Detect Claude installations');
130
- console.log(' ────────────────────────────────────');
131
- const paths = getWatchPaths();
132
- if (paths.length === 0) {
133
- console.log(' ⚠ No Claude transcript directories found.');
134
- console.log(' Install Claude Code (CLI, VS Code, or Desktop App) first.');
135
- console.log(' Paths checked:');
136
- console.log(' ~/.claude/projects/');
137
- const platform = process.platform;
138
- if (platform === 'win32') {
139
- console.log(' %APPDATA%/Claude/local-agent-mode-sessions/');
140
- } else if (platform === 'darwin') {
141
- console.log(' ~/Library/Application Support/Claude/local-agent-mode-sessions/');
142
- }
143
- } else {
144
- for (const p of paths) {
145
- console.log(` ✓ ${p}`);
146
- }
147
- }
148
-
149
- // Step 3: Install background service
150
- console.log('');
151
- console.log(' Step 3: Background service');
152
- console.log(' ──────────────────────────');
153
-
154
- if (isInstalled()) {
155
- console.log(' ✓ Already installed as a background service.');
156
- const reinstall = await ask(' Reinstall? (y/N): ');
157
- if (reinstall.toLowerCase() === 'y') {
158
- const result = install();
159
- startNow();
160
- console.log(` ✓ Reinstalled at: ${result.path}`);
161
- }
162
- } else {
163
- const doInstall = await ask(' Install as background service? (Y/n): ');
164
- if (doInstall.toLowerCase() !== 'n') {
165
- const result = install();
166
- startNow();
167
- console.log(` ✓ Installed at: ${result.path}`);
168
- if (result.platform === 'windows') {
169
- console.log(' ✓ Will auto-start on login (Windows Startup folder)');
170
- console.log(' ✓ Desktop shortcut created (double-click "AIMeter" to start)');
171
- console.log(' ✓ Start Menu shortcut created');
172
- } else if (result.platform === 'macos') {
173
- console.log(' ✓ Will auto-start on login (launchd)');
174
- } else {
175
- console.log(' ✓ Will auto-start on login (systemd user service)');
176
- }
177
- } else {
178
- console.log(' Skipped. You can run manually with: aimeter watch');
179
- }
180
- }
181
-
182
- // Done!
183
- console.log('');
184
- console.log(' ╔══════════════════════════════════╗');
185
- console.log(' ║ Setup complete! ║');
186
- console.log(' ╚══════════════════════════════════╝');
187
- console.log('');
188
- console.log(' Your Claude usage is now being tracked.');
189
- console.log(' View your dashboard at: https://getaimeter.com/dashboard');
190
- console.log('');
191
- console.log(' Commands:');
192
- console.log(' aimeter status — check what\'s running');
193
- console.log(' aimeter logs — view watcher logs');
194
- console.log(' aimeter stop — stop the watcher');
195
- console.log('');
196
-
197
- rl.close();
198
- }
199
-
200
- // ---------------------------------------------------------------------------
201
- // watch — foreground mode
202
- // ---------------------------------------------------------------------------
203
-
204
- function runWatch() {
205
- const fs = require('fs');
206
- const path = require('path');
207
- const lockFile = path.join(AIMETER_DIR, 'watcher.lock');
208
-
209
- // Check if another instance is already running
210
- try {
211
- if (fs.existsSync(lockFile)) {
212
- const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
213
- // Check if the PID is still alive
214
- try {
215
- process.kill(lockData.pid, 0); // signal 0 = just check if alive
216
- console.log(`Another watcher is already running (PID ${lockData.pid}). Use 'aimeter stop' first.`);
217
- process.exitCode = 1;
218
- return;
219
- } catch {
220
- // PID is dead — stale lock, remove it
221
- }
222
- }
223
- } catch {}
224
-
225
- // Write lock file
226
- fs.mkdirSync(AIMETER_DIR, { recursive: true });
227
- fs.writeFileSync(lockFile, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
228
-
229
- const cleanup = startWatching();
230
-
231
- // Launch system tray icon (non-blocking, fails silently if systray2 not available)
232
- const showTray = !process.argv.includes('--no-tray');
233
- if (showTray) {
234
- startTray(() => {
235
- cleanup();
236
- try { fs.unlinkSync(lockFile); } catch {}
237
- }).catch(() => {}); // ignore tray errors
238
- }
239
-
240
- const cleanupAll = () => {
241
- cleanup();
242
- stopTray();
243
- try { fs.unlinkSync(lockFile); } catch {}
244
- process.exit(0);
245
- };
246
-
247
- process.on('SIGINT', cleanupAll);
248
- process.on('SIGTERM', cleanupAll);
249
- process.on('exit', () => {
250
- stopTray();
251
- try { fs.unlinkSync(lockFile); } catch {}
252
- });
253
-
254
- process.on('uncaughtException', (err) => {
255
- console.error('[aimeter] Uncaught:', err.message);
256
- });
257
-
258
- process.on('unhandledRejection', (reason) => {
259
- console.error('[aimeter] Unhandled rejection:', reason);
260
- });
261
- }
262
-
263
- // ---------------------------------------------------------------------------
264
- // install / uninstall / start / stop
265
- // ---------------------------------------------------------------------------
266
-
267
- function runInstall() {
268
- if (!getApiKey()) {
269
- console.log('No API key configured. Run: aimeter setup');
270
- process.exitCode = 1;
271
- return;
272
- }
273
-
274
- const result = install();
275
- console.log(`Installed background service (${result.platform})`);
276
- console.log(` File: ${result.path}`);
277
- console.log('Starting...');
278
- startNow();
279
- console.log('Done. AIMeter is now tracking your Claude usage.');
280
- }
281
-
282
- function runUninstall() {
283
- stopNow();
284
- const removed = uninstall();
285
- if (removed) {
286
- console.log('Background service removed.');
287
- } else {
288
- console.log('No background service found to remove.');
289
- }
290
- }
291
-
292
- function runStart() {
293
- if (!isInstalled()) {
294
- console.log('Service not installed. Run: aimeter install');
295
- process.exitCode = 1;
296
- return;
297
- }
298
-
299
- // Clean up stale lock file if the previous process died
300
- const fs = require('fs');
301
- const pathMod = require('path');
302
- const lockFile = pathMod.join(AIMETER_DIR, 'watcher.lock');
303
- try {
304
- if (fs.existsSync(lockFile)) {
305
- const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
306
- try {
307
- process.kill(lockData.pid, 0);
308
- // PID is alive — watcher is already running
309
- console.log(`AIMeter watcher is already running (PID ${lockData.pid}).`);
310
- return;
311
- } catch {
312
- // PID is dead — stale lock, remove it
313
- fs.unlinkSync(lockFile);
314
- }
315
- }
316
- } catch {}
317
-
318
- startNow();
319
- console.log('AIMeter watcher started.');
320
- }
321
-
322
- function runStop() {
323
- stopNow();
324
- console.log('AIMeter watcher stopped.');
325
- }
326
-
327
- // ---------------------------------------------------------------------------
328
- // status
329
- // ---------------------------------------------------------------------------
330
-
331
- function runStatus() {
332
- const fs = require('fs');
333
- const path = require('path');
334
-
335
- console.log('');
336
- console.log(' AIMeter Status');
337
- console.log(' ══════════════');
338
- console.log('');
339
-
340
- // API key
341
- const key = getApiKey();
342
- console.log(` API key: ${key ? key.slice(0, 8) + '...' + key.slice(-4) : '✗ NOT SET (run: aimeter setup)'}`);
343
-
344
- // Service
345
- const installed = isInstalled();
346
- console.log(` Service: ${installed ? '✓ installed' : '✗ not installed'}`);
347
-
348
- // Watch paths
349
- const paths = getWatchPaths();
350
- console.log(` Watch paths: ${paths.length > 0 ? '' : '(none found)'}`);
351
- for (const p of paths) console.log(` → ${p}`);
352
-
353
- // State
354
- const stateFile = path.join(AIMETER_DIR, 'state.json');
355
- try {
356
- const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
357
- const fileCount = Object.keys(state.fileOffsets || {}).length;
358
- console.log(` Files: ${fileCount} tracked`);
359
- console.log(` Last active: ${state.lastSaved || 'never'}`);
360
- } catch {
361
- console.log(' State: no data yet (first run?)');
362
- }
363
-
364
- // Log file
365
- const logFile = path.join(AIMETER_DIR, 'watcher.log');
366
- try {
367
- const stat = fs.statSync(logFile);
368
- const sizeMB = (stat.size / 1024 / 1024).toFixed(1);
369
- console.log(` Log file: ${logFile} (${sizeMB} MB)`);
370
- } catch {
371
- console.log(' Log file: (none)');
372
- }
373
-
374
- console.log('');
375
- }
376
-
377
- // ---------------------------------------------------------------------------
378
- // logs — tail the watcher log
379
- // ---------------------------------------------------------------------------
380
-
381
- function runLogs() {
382
- const fs = require('fs');
383
- const path = require('path');
384
- const logFile = path.join(AIMETER_DIR, 'watcher.log');
385
-
386
- if (!fs.existsSync(logFile)) {
387
- console.log('No log file yet. Start the watcher first.');
388
- return;
389
- }
390
-
391
- // Show last 50 lines, then follow
392
- const lines = process.argv[3] ? parseInt(process.argv[3], 10) : 50;
393
- const content = fs.readFileSync(logFile, 'utf8');
394
- const allLines = content.split('\n');
395
- const tail = allLines.slice(-lines).join('\n');
396
- console.log(tail);
397
-
398
- // Follow mode — poll every second (fs.watch is unreliable on Windows)
399
- console.log('\n--- Watching for new entries (Ctrl+C to stop) ---\n');
400
-
401
- let fileSize = fs.statSync(logFile).size;
402
- setInterval(() => {
403
- try {
404
- const newSize = fs.statSync(logFile).size;
405
- if (newSize > fileSize) {
406
- const fd = fs.openSync(logFile, 'r');
407
- const buf = Buffer.alloc(newSize - fileSize);
408
- fs.readSync(fd, buf, 0, buf.length, fileSize);
409
- fs.closeSync(fd);
410
- process.stdout.write(buf.toString('utf8'));
411
- fileSize = newSize;
412
- }
413
- } catch {}
414
- }, 1000);
415
- }
416
-
417
- // ---------------------------------------------------------------------------
418
- // key — quick key management
419
- // ---------------------------------------------------------------------------
420
-
421
- function runKey() {
422
- const key = getApiKey();
423
- if (key) {
424
- console.log(key);
425
- } else {
426
- console.log('No API key configured. Run: aimeter setup');
427
- process.exitCode = 1;
428
- }
429
- }
430
-
431
- // ---------------------------------------------------------------------------
432
- // mcp — start MCP server for Claude Code/Desktop
433
- // ---------------------------------------------------------------------------
434
-
435
- function runMcp() {
436
- require('./mcp').startMcpServer();
437
- }
438
-
439
- // ---------------------------------------------------------------------------
440
- // summary — show current usage summary from API
441
- // ---------------------------------------------------------------------------
442
-
443
- async function runSummary() {
444
- const https = require('https');
445
- const apiKey = getApiKey();
446
- if (!apiKey) {
447
- console.log('No API key configured. Run: aimeter setup');
448
- process.exitCode = 1;
449
- return;
450
- }
451
-
452
- // Check for --json flag
453
- const jsonMode = process.argv.includes('--json');
454
-
455
- try {
456
- // Fetch current usage from API
457
- const data = await fetchFromApi('/api/usage/current', apiKey);
458
-
459
- if (jsonMode) {
460
- console.log(JSON.stringify(data, null, 2));
461
- return;
462
- }
463
-
464
- // Pretty print
465
- const fmtTokens = (n) => {
466
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
467
- if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
468
- return String(n);
469
- };
470
- const fmtCost = (c) => {
471
- if (c >= 1) return `$${c.toFixed(2)}`;
472
- if (c >= 0.01) return `$${c.toFixed(3)}`;
473
- return `$${c.toFixed(4)}`;
474
- };
475
-
476
- // Calculate total cost from byModel
477
- const totalCost = (data.byModel || []).reduce((sum, m) => sum + (m.estimatedCost || 0), 0);
478
-
479
- // Reset countdown
480
- let resetStr = 'N/A';
481
- if (data.nextReset) {
482
- const resetMs = new Date(data.nextReset) - Date.now();
483
- if (resetMs > 0) {
484
- const h = Math.floor(resetMs / 3600000);
485
- const m = Math.floor((resetMs % 3600000) / 60000);
486
- resetStr = `${h}h ${m}m`;
487
- } else {
488
- resetStr = 'now';
489
- }
490
- }
491
-
492
- console.log('');
493
- console.log(' AIMeter — Current Usage (5-hour window)');
494
- console.log(' ════════════════════════════════════════');
495
- console.log('');
496
- console.log(` Cost: ${fmtCost(totalCost)}`);
497
- console.log(` Input: ${fmtTokens(data.totalInputTokens || 0)}`);
498
- console.log(` Output: ${fmtTokens(data.totalOutputTokens || 0)}`);
499
- console.log(` Thinking: ${fmtTokens(data.totalThinkingTokens || 0)}`);
500
- console.log(` Cache Read: ${fmtTokens(data.totalCacheReadTokens || 0)}`);
501
- console.log(` Requests: ${data.windowRequests || 0}`);
502
- console.log(` Reset in: ${resetStr}`);
503
-
504
- // By source
505
- if (data.bySource && data.bySource.length > 0) {
506
- console.log('');
507
- console.log(' By Source:');
508
- const sourceLabels = {
509
- cli: 'Terminal', vscode: 'VS Code', desktop_app: 'Desktop',
510
- cursor: 'Cursor', codex_cli: 'Codex CLI', codex_vscode: 'Codex VS',
511
- gemini_cli: 'Gemini', copilot_cli: 'Copilot', copilot_vscode: 'Copilot VS',
512
- };
513
- for (const s of data.bySource) {
514
- const label = sourceLabels[s.source] || s.source;
515
- console.log(` ${label.padEnd(14)} ${String(s.requests).padStart(4)} req`);
516
- }
517
- }
518
-
519
- // By model
520
- if (data.byModel && data.byModel.length > 0) {
521
- console.log('');
522
- console.log(' By Model:');
523
- for (const m of data.byModel) {
524
- const label = m.model.replace(/-\d{8}$/, '');
525
- console.log(` ${label.padEnd(22)} ${String(m.requests).padStart(4)} req ${fmtCost(m.estimatedCost || 0)}`);
526
- }
527
- }
528
-
529
- // Previous window comparison
530
- if (data.previous && data.previous.totalCost > 0) {
531
- const prevCost = data.previous.totalCost;
532
- const change = totalCost - prevCost;
533
- const pct = prevCost > 0 ? ((change / prevCost) * 100).toFixed(0) : '∞';
534
- const arrow = change >= 0 ? '↑' : '↓';
535
- console.log('');
536
- console.log(` vs Previous: ${arrow} ${Math.abs(change).toFixed(2)} (${pct}%)`);
537
- }
538
-
539
- console.log('');
540
- } catch (err) {
541
- console.error(`Error fetching usage: ${err.message}`);
542
- process.exitCode = 1;
543
- }
544
- }
545
-
546
- // ---------------------------------------------------------------------------
547
- // blocks — show 5-hour billing blocks
548
- // ---------------------------------------------------------------------------
549
-
550
- async function runBlocks() {
551
- const apiKey = getApiKey();
552
- if (!apiKey) {
553
- console.log('No API key configured. Run: aimeter setup');
554
- process.exitCode = 1;
555
- return;
556
- }
557
-
558
- const jsonMode = process.argv.includes('--json');
559
- const days = parseInt(process.argv[3]) || 3;
560
-
561
- try {
562
- const data = await fetchFromApi(`/api/usage/blocks?days=${days}`, apiKey);
563
-
564
- if (jsonMode) {
565
- console.log(JSON.stringify(data, null, 2));
566
- return;
567
- }
568
-
569
- console.log('');
570
- console.log(` AIMeter — Billing Blocks (last ${days} days)`);
571
- console.log(' ════════════════════════════════════════');
572
- console.log('');
573
-
574
- if (!data.blocks || data.blocks.length === 0) {
575
- console.log(' No billing blocks found.');
576
- console.log('');
577
- return;
578
- }
579
-
580
- for (const block of data.blocks) {
581
- const start = new Date(block.startTime);
582
- const fmtTime = (d) => d.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
583
- const active = block.isActive ? ' ◉ ACTIVE' : '';
584
-
585
- console.log(` ${fmtTime(start)}${active}`);
586
- console.log(` ────────────────────────────`);
587
-
588
- const fmtCost = (c) => c >= 1 ? `$${c.toFixed(2)}` : `$${c.toFixed(3)}`;
589
- const fmtTokens = (n) => {
590
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
591
- if (n >= 1_000) return `${(n / 1_000).toFixed(0)}k`;
592
- return String(n);
593
- };
594
-
595
- console.log(` Cost: ${fmtCost(block.totalCost)} | Requests: ${block.requestCount} | Tokens: ${fmtTokens(block.totalInputTokens + block.totalOutputTokens)}`);
596
-
597
- if (block.burnRate) {
598
- console.log(` Burn: ${fmtTokens(block.burnRate.tokensPerMinute)}/min | ${fmtCost(block.burnRate.costPerHour)}/hr`);
599
- }
600
-
601
- if (block.isActive && block.remainingMinutes > 0) {
602
- const h = Math.floor(block.remainingMinutes / 60);
603
- const m = Math.round(block.remainingMinutes % 60);
604
- console.log(` Remaining: ${h}h ${m}m`);
605
- }
606
-
607
- console.log(` Sources: ${(block.sources || []).join(', ')}`);
608
- console.log(` Models: ${(block.models || []).join(', ')}`);
609
- console.log('');
610
- }
611
-
612
- console.log(` Total: ${data.totalBlocks} blocks, $${data.totalCost.toFixed(2)}`);
613
- console.log('');
614
- } catch (err) {
615
- console.error(`Error fetching blocks: ${err.message}`);
616
- process.exitCode = 1;
617
- }
618
- }
619
-
620
- // ---------------------------------------------------------------------------
621
- // help
622
- // ---------------------------------------------------------------------------
623
-
624
- function printHelp() {
625
- console.log(`
626
- AIMeter — Track your Claude AI usage
627
- ═════════════════════════════════════
628
-
629
- Usage: aimeter <command>
630
-
631
- Getting started:
632
- setup Full onboarding wizard (recommended)
633
-
634
- Service management:
635
- install Install background service
636
- uninstall Remove background service
637
- start Start the background service
638
- stop Stop the background service
639
-
640
- Manual mode:
641
- watch Run watcher in foreground
642
-
643
- Usage insights:
644
- summary Show current usage summary (--json for JSON)
645
- blocks [N] Show 5-hour billing blocks (last N days, default 3)
646
-
647
- Info:
648
- status Show current configuration
649
- logs [N] Tail watcher log (last N lines, default 50)
650
- key Print current API key
651
-
652
- Integration:
653
- mcp Start MCP server (for Claude Code/Desktop)
654
-
655
- https://getaimeter.com
656
- `);
657
- }
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { getApiKey, saveApiKey, getWatchPaths, AIMETER_DIR } = require('./config');
5
+ const { startWatching } = require('./watcher');
6
+ const { install, uninstall, isInstalled, startNow, stopNow } = require('./service');
7
+ const { checkForUpdate, getCurrentVersion } = require('./update-check');
8
+ const { startTray, stopTray } = require('./tray');
9
+
10
+ const command = process.argv[2] || 'help';
11
+
12
+ // Version check on interactive commands (non-blocking)
13
+ if (['setup', 'status', 'summary', 'help', '--help', '-h'].includes(command)) {
14
+ checkForUpdate();
15
+ }
16
+
17
+ switch (command) {
18
+ case 'setup': runSetup(); break;
19
+ case 'watch': runWatch(); break;
20
+ case 'install': runInstall(); break;
21
+ case 'uninstall': runUninstall(); break;
22
+ case 'start': runStart(); break;
23
+ case 'stop': runStop(); break;
24
+ case 'status': runStatus(); break;
25
+ case 'logs': runLogs(); break;
26
+ case 'key': runKey(); break;
27
+ case 'summary': runSummary(); break;
28
+ case 'blocks': runBlocks(); break;
29
+ case 'version': case '--version': case '-v':
30
+ console.log(getCurrentVersion());
31
+ break;
32
+ case 'mcp': runMcp(); break;
33
+ case 'help': case '--help': case '-h':
34
+ printHelp();
35
+ break;
36
+ default:
37
+ console.log(`Unknown command: ${command}\n`);
38
+ printHelp();
39
+ process.exitCode = 1;
40
+ break;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // fetchFromApi — shared helper for API calls (native https, zero deps)
45
+ // ---------------------------------------------------------------------------
46
+
47
+ function fetchFromApi(urlPath, apiKey) {
48
+ return new Promise((resolve, reject) => {
49
+ const https = require('https');
50
+ const options = {
51
+ hostname: 'aimeter-api.fly.dev',
52
+ port: 443,
53
+ path: urlPath,
54
+ method: 'GET',
55
+ headers: {
56
+ 'X-AIMeter-Key': apiKey,
57
+ },
58
+ };
59
+
60
+ const req = https.request(options, (res) => {
61
+ let body = '';
62
+ res.on('data', chunk => body += chunk);
63
+ res.on('end', () => {
64
+ if (res.statusCode >= 200 && res.statusCode < 300) {
65
+ try { resolve(JSON.parse(body)); }
66
+ catch { reject(new Error('Invalid JSON response')); }
67
+ } else {
68
+ reject(new Error(`HTTP ${res.statusCode}: ${body.slice(0, 200)}`));
69
+ }
70
+ });
71
+ });
72
+ req.on('error', reject);
73
+ req.setTimeout(10000, () => { req.destroy(); reject(new Error('Request timeout')); });
74
+ req.end();
75
+ });
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // setup — full onboarding wizard
80
+ // ---------------------------------------------------------------------------
81
+
82
+ async function runSetup() {
83
+ const readline = require('readline');
84
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
85
+ const ask = (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
86
+
87
+ console.log('');
88
+ console.log(' ╔══════════════════════════════════╗');
89
+ console.log(' ║ AIMeter Setup Wizard ║');
90
+ console.log(' ╚══════════════════════════════════╝');
91
+ console.log('');
92
+
93
+ // Step 1: API Key
94
+ console.log(' Step 1: API Key');
95
+ console.log(' ───────────────');
96
+ const existing = getApiKey();
97
+ let key = existing;
98
+
99
+ if (existing) {
100
+ console.log(` Current key: ${existing.slice(0, 8)}...${existing.slice(-4)}`);
101
+ const answer = await ask(' Press Enter to keep, or paste a new key: ');
102
+ if (answer.length > 0) {
103
+ if (!answer.startsWith('aim_')) {
104
+ console.log(' ✗ Invalid key (must start with aim_)');
105
+ rl.close();
106
+ return;
107
+ }
108
+ saveApiKey(answer);
109
+ key = answer;
110
+ console.log(' ✓ Key updated.');
111
+ } else {
112
+ console.log(' ✓ Key unchanged.');
113
+ }
114
+ } else {
115
+ console.log(' Sign up at: https://getaimeter.com');
116
+ console.log(' Then copy your API key from the dashboard.\n');
117
+ while (!key || !key.startsWith('aim_')) {
118
+ key = await ask(' Paste your API key: ');
119
+ if (!key.startsWith('aim_')) {
120
+ console.log(' ✗ Invalid — keys start with aim_');
121
+ }
122
+ }
123
+ saveApiKey(key);
124
+ console.log(' ✓ Key saved.');
125
+ }
126
+
127
+ // Step 2: Check watch paths
128
+ console.log('');
129
+ console.log(' Step 2: Detect Claude installations');
130
+ console.log(' ────────────────────────────────────');
131
+ const paths = getWatchPaths();
132
+ if (paths.length === 0) {
133
+ console.log(' ⚠ No Claude transcript directories found.');
134
+ console.log(' Install Claude Code (CLI, VS Code, or Desktop App) first.');
135
+ console.log(' Paths checked:');
136
+ console.log(' ~/.claude/projects/');
137
+ const platform = process.platform;
138
+ if (platform === 'win32') {
139
+ console.log(' %APPDATA%/Claude/local-agent-mode-sessions/');
140
+ } else if (platform === 'darwin') {
141
+ console.log(' ~/Library/Application Support/Claude/local-agent-mode-sessions/');
142
+ }
143
+ } else {
144
+ for (const p of paths) {
145
+ console.log(` ✓ ${p}`);
146
+ }
147
+ }
148
+
149
+ // Step 3: Install background service
150
+ console.log('');
151
+ console.log(' Step 3: Background service');
152
+ console.log(' ──────────────────────────');
153
+
154
+ if (isInstalled()) {
155
+ console.log(' ✓ Already installed as a background service.');
156
+ const reinstall = await ask(' Reinstall? (y/N): ');
157
+ if (reinstall.toLowerCase() === 'y') {
158
+ const result = install();
159
+ startNow();
160
+ console.log(` ✓ Reinstalled at: ${result.path}`);
161
+ }
162
+ } else {
163
+ const doInstall = await ask(' Install as background service? (Y/n): ');
164
+ if (doInstall.toLowerCase() !== 'n') {
165
+ const result = install();
166
+ startNow();
167
+ console.log(` ✓ Installed at: ${result.path}`);
168
+ if (result.platform === 'windows') {
169
+ console.log(' ✓ Will auto-start on login (Windows Startup folder)');
170
+ console.log(' ✓ Desktop shortcut created (double-click "AIMeter" to start)');
171
+ console.log(' ✓ Start Menu shortcut created');
172
+ } else if (result.platform === 'macos') {
173
+ console.log(' ✓ Will auto-start on login (launchd)');
174
+ } else {
175
+ console.log(' ✓ Will auto-start on login (systemd user service)');
176
+ }
177
+ } else {
178
+ console.log(' Skipped. You can run manually with: aimeter watch');
179
+ }
180
+ }
181
+
182
+ // Done!
183
+ console.log('');
184
+ console.log(' ╔══════════════════════════════════╗');
185
+ console.log(' ║ Setup complete! ║');
186
+ console.log(' ╚══════════════════════════════════╝');
187
+ console.log('');
188
+ console.log(' Your Claude usage is now being tracked.');
189
+ console.log(' View your dashboard at: https://getaimeter.com/dashboard');
190
+ console.log('');
191
+ console.log(' Commands:');
192
+ console.log(' aimeter status — check what\'s running');
193
+ console.log(' aimeter logs — view watcher logs');
194
+ console.log(' aimeter stop — stop the watcher');
195
+ console.log('');
196
+
197
+ rl.close();
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // watch — foreground mode
202
+ // ---------------------------------------------------------------------------
203
+
204
+ function runWatch() {
205
+ const fs = require('fs');
206
+ const path = require('path');
207
+ const lockFile = path.join(AIMETER_DIR, 'watcher.lock');
208
+
209
+ // Check if another instance is already running
210
+ try {
211
+ if (fs.existsSync(lockFile)) {
212
+ const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
213
+ // Check if the PID is still alive
214
+ try {
215
+ process.kill(lockData.pid, 0); // signal 0 = just check if alive
216
+ console.log(`Another watcher is already running (PID ${lockData.pid}). Use 'aimeter stop' first.`);
217
+ process.exitCode = 1;
218
+ return;
219
+ } catch {
220
+ // PID is dead — stale lock, remove it
221
+ }
222
+ }
223
+ } catch {}
224
+
225
+ // Write lock file
226
+ fs.mkdirSync(AIMETER_DIR, { recursive: true });
227
+ fs.writeFileSync(lockFile, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }));
228
+
229
+ const cleanup = startWatching();
230
+
231
+ // Launch system tray icon (non-blocking, fails silently if systray2 not available)
232
+ const showTray = !process.argv.includes('--no-tray');
233
+ if (showTray) {
234
+ startTray(() => {
235
+ cleanup();
236
+ try { fs.unlinkSync(lockFile); } catch {}
237
+ }).catch(() => {}); // ignore tray errors
238
+ }
239
+
240
+ const cleanupAll = () => {
241
+ cleanup();
242
+ stopTray();
243
+ try { fs.unlinkSync(lockFile); } catch {}
244
+ process.exit(0);
245
+ };
246
+
247
+ process.on('SIGINT', cleanupAll);
248
+ process.on('SIGTERM', cleanupAll);
249
+ process.on('exit', () => {
250
+ stopTray();
251
+ try { fs.unlinkSync(lockFile); } catch {}
252
+ });
253
+
254
+ process.on('uncaughtException', (err) => {
255
+ console.error('[aimeter] Uncaught:', err.message);
256
+ });
257
+
258
+ process.on('unhandledRejection', (reason) => {
259
+ console.error('[aimeter] Unhandled rejection:', reason);
260
+ });
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // install / uninstall / start / stop
265
+ // ---------------------------------------------------------------------------
266
+
267
+ function runInstall() {
268
+ if (!getApiKey()) {
269
+ console.log('No API key configured. Run: aimeter setup');
270
+ process.exitCode = 1;
271
+ return;
272
+ }
273
+
274
+ const result = install();
275
+ console.log(`Installed background service (${result.platform})`);
276
+ console.log(` File: ${result.path}`);
277
+ console.log('Starting...');
278
+ startNow();
279
+ console.log('Done. AIMeter is now tracking your Claude usage.');
280
+ }
281
+
282
+ function runUninstall() {
283
+ stopNow();
284
+ const removed = uninstall();
285
+ if (removed) {
286
+ console.log('Background service removed.');
287
+ } else {
288
+ console.log('No background service found to remove.');
289
+ }
290
+ }
291
+
292
+ function runStart() {
293
+ if (!isInstalled()) {
294
+ console.log('Service not installed. Run: aimeter install');
295
+ process.exitCode = 1;
296
+ return;
297
+ }
298
+
299
+ // Clean up stale lock file if the previous process died
300
+ const fs = require('fs');
301
+ const pathMod = require('path');
302
+ const lockFile = pathMod.join(AIMETER_DIR, 'watcher.lock');
303
+ try {
304
+ if (fs.existsSync(lockFile)) {
305
+ const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
306
+ try {
307
+ process.kill(lockData.pid, 0);
308
+ // PID is alive — watcher is already running
309
+ console.log(`AIMeter watcher is already running (PID ${lockData.pid}).`);
310
+ return;
311
+ } catch {
312
+ // PID is dead — stale lock, remove it
313
+ fs.unlinkSync(lockFile);
314
+ }
315
+ }
316
+ } catch {}
317
+
318
+ startNow();
319
+ console.log('AIMeter watcher started.');
320
+ }
321
+
322
+ function runStop() {
323
+ stopNow();
324
+ console.log('AIMeter watcher stopped.');
325
+ }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // status
329
+ // ---------------------------------------------------------------------------
330
+
331
+ function runStatus() {
332
+ const fs = require('fs');
333
+ const path = require('path');
334
+
335
+ console.log('');
336
+ console.log(' AIMeter Status');
337
+ console.log(' ══════════════');
338
+ console.log('');
339
+
340
+ // API key
341
+ const key = getApiKey();
342
+ console.log(` API key: ${key ? key.slice(0, 8) + '...' + key.slice(-4) : '✗ NOT SET (run: aimeter setup)'}`);
343
+
344
+ // Service
345
+ const installed = isInstalled();
346
+ console.log(` Service: ${installed ? '✓ installed' : '✗ not installed'}`);
347
+
348
+ // Watch paths
349
+ const paths = getWatchPaths();
350
+ console.log(` Watch paths: ${paths.length > 0 ? '' : '(none found)'}`);
351
+ for (const p of paths) console.log(` → ${p}`);
352
+
353
+ // State
354
+ const stateFile = path.join(AIMETER_DIR, 'state.json');
355
+ try {
356
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
357
+ const fileCount = Object.keys(state.fileOffsets || {}).length;
358
+ console.log(` Files: ${fileCount} tracked`);
359
+ console.log(` Last active: ${state.lastSaved || 'never'}`);
360
+ } catch {
361
+ console.log(' State: no data yet (first run?)');
362
+ }
363
+
364
+ // Log file
365
+ const logFile = path.join(AIMETER_DIR, 'watcher.log');
366
+ try {
367
+ const stat = fs.statSync(logFile);
368
+ const sizeMB = (stat.size / 1024 / 1024).toFixed(1);
369
+ console.log(` Log file: ${logFile} (${sizeMB} MB)`);
370
+ } catch {
371
+ console.log(' Log file: (none)');
372
+ }
373
+
374
+ console.log('');
375
+ }
376
+
377
+ // ---------------------------------------------------------------------------
378
+ // logs — tail the watcher log
379
+ // ---------------------------------------------------------------------------
380
+
381
+ function runLogs() {
382
+ const fs = require('fs');
383
+ const path = require('path');
384
+ const logFile = path.join(AIMETER_DIR, 'watcher.log');
385
+
386
+ if (!fs.existsSync(logFile)) {
387
+ console.log('No log file yet. Start the watcher first.');
388
+ return;
389
+ }
390
+
391
+ // Show last 50 lines, then follow
392
+ const lines = process.argv[3] ? parseInt(process.argv[3], 10) : 50;
393
+ const content = fs.readFileSync(logFile, 'utf8');
394
+ const allLines = content.split('\n');
395
+ const tail = allLines.slice(-lines).join('\n');
396
+ console.log(tail);
397
+
398
+ // Follow mode — poll every second (fs.watch is unreliable on Windows)
399
+ console.log('\n--- Watching for new entries (Ctrl+C to stop) ---\n');
400
+
401
+ let fileSize = fs.statSync(logFile).size;
402
+ setInterval(() => {
403
+ try {
404
+ const newSize = fs.statSync(logFile).size;
405
+ if (newSize > fileSize) {
406
+ const fd = fs.openSync(logFile, 'r');
407
+ const buf = Buffer.alloc(newSize - fileSize);
408
+ fs.readSync(fd, buf, 0, buf.length, fileSize);
409
+ fs.closeSync(fd);
410
+ process.stdout.write(buf.toString('utf8'));
411
+ fileSize = newSize;
412
+ }
413
+ } catch {}
414
+ }, 1000);
415
+ }
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // key — quick key management
419
+ // ---------------------------------------------------------------------------
420
+
421
+ function runKey() {
422
+ const key = getApiKey();
423
+ if (key) {
424
+ console.log(key);
425
+ } else {
426
+ console.log('No API key configured. Run: aimeter setup');
427
+ process.exitCode = 1;
428
+ }
429
+ }
430
+
431
+ // ---------------------------------------------------------------------------
432
+ // mcp — start MCP server for Claude Code/Desktop
433
+ // ---------------------------------------------------------------------------
434
+
435
+ function runMcp() {
436
+ require('./mcp').startMcpServer();
437
+ }
438
+
439
+ // ---------------------------------------------------------------------------
440
+ // summary — show current usage summary from API
441
+ // ---------------------------------------------------------------------------
442
+
443
+ async function runSummary() {
444
+ const https = require('https');
445
+ const apiKey = getApiKey();
446
+ if (!apiKey) {
447
+ console.log('No API key configured. Run: aimeter setup');
448
+ process.exitCode = 1;
449
+ return;
450
+ }
451
+
452
+ // Check for --json flag
453
+ const jsonMode = process.argv.includes('--json');
454
+
455
+ try {
456
+ // Fetch current usage from API
457
+ const data = await fetchFromApi('/api/usage/current', apiKey);
458
+
459
+ if (jsonMode) {
460
+ console.log(JSON.stringify(data, null, 2));
461
+ return;
462
+ }
463
+
464
+ // Pretty print
465
+ const fmtTokens = (n) => {
466
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
467
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
468
+ return String(n);
469
+ };
470
+ const fmtCost = (c) => {
471
+ if (c >= 1) return `$${c.toFixed(2)}`;
472
+ if (c >= 0.01) return `$${c.toFixed(3)}`;
473
+ return `$${c.toFixed(4)}`;
474
+ };
475
+
476
+ // Calculate total cost from byModel
477
+ const totalCost = (data.byModel || []).reduce((sum, m) => sum + (m.estimatedCost || 0), 0);
478
+
479
+ // Reset countdown
480
+ let resetStr = 'N/A';
481
+ if (data.nextReset) {
482
+ const resetMs = new Date(data.nextReset) - Date.now();
483
+ if (resetMs > 0) {
484
+ const h = Math.floor(resetMs / 3600000);
485
+ const m = Math.floor((resetMs % 3600000) / 60000);
486
+ resetStr = `${h}h ${m}m`;
487
+ } else {
488
+ resetStr = 'now';
489
+ }
490
+ }
491
+
492
+ console.log('');
493
+ console.log(' AIMeter — Current Usage (5-hour window)');
494
+ console.log(' ════════════════════════════════════════');
495
+ console.log('');
496
+ console.log(` Cost: ${fmtCost(totalCost)}`);
497
+ console.log(` Input: ${fmtTokens(data.totalInputTokens || 0)}`);
498
+ console.log(` Output: ${fmtTokens(data.totalOutputTokens || 0)}`);
499
+ console.log(` Thinking: ${fmtTokens(data.totalThinkingTokens || 0)}`);
500
+ console.log(` Cache Read: ${fmtTokens(data.totalCacheReadTokens || 0)}`);
501
+ console.log(` Requests: ${data.windowRequests || 0}`);
502
+ console.log(` Reset in: ${resetStr}`);
503
+
504
+ // By source
505
+ if (data.bySource && data.bySource.length > 0) {
506
+ console.log('');
507
+ console.log(' By Source:');
508
+ const sourceLabels = {
509
+ cli: 'Terminal', vscode: 'VS Code', desktop_app: 'Desktop',
510
+ cursor: 'Cursor', codex_cli: 'Codex CLI', codex_vscode: 'Codex VS',
511
+ gemini_cli: 'Gemini', copilot_cli: 'Copilot', copilot_vscode: 'Copilot VS',
512
+ };
513
+ for (const s of data.bySource) {
514
+ const label = sourceLabels[s.source] || s.source;
515
+ console.log(` ${label.padEnd(14)} ${String(s.requests).padStart(4)} req`);
516
+ }
517
+ }
518
+
519
+ // By model
520
+ if (data.byModel && data.byModel.length > 0) {
521
+ console.log('');
522
+ console.log(' By Model:');
523
+ for (const m of data.byModel) {
524
+ const label = m.model.replace(/-\d{8}$/, '');
525
+ console.log(` ${label.padEnd(22)} ${String(m.requests).padStart(4)} req ${fmtCost(m.estimatedCost || 0)}`);
526
+ }
527
+ }
528
+
529
+ // Previous window comparison
530
+ if (data.previous && data.previous.totalCost > 0) {
531
+ const prevCost = data.previous.totalCost;
532
+ const change = totalCost - prevCost;
533
+ const pct = prevCost > 0 ? ((change / prevCost) * 100).toFixed(0) : '∞';
534
+ const arrow = change >= 0 ? '↑' : '↓';
535
+ console.log('');
536
+ console.log(` vs Previous: ${arrow} ${Math.abs(change).toFixed(2)} (${pct}%)`);
537
+ }
538
+
539
+ console.log('');
540
+ } catch (err) {
541
+ console.error(`Error fetching usage: ${err.message}`);
542
+ process.exitCode = 1;
543
+ }
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // blocks — show 5-hour billing blocks
548
+ // ---------------------------------------------------------------------------
549
+
550
+ async function runBlocks() {
551
+ const apiKey = getApiKey();
552
+ if (!apiKey) {
553
+ console.log('No API key configured. Run: aimeter setup');
554
+ process.exitCode = 1;
555
+ return;
556
+ }
557
+
558
+ const jsonMode = process.argv.includes('--json');
559
+ const days = parseInt(process.argv[3]) || 3;
560
+
561
+ try {
562
+ const data = await fetchFromApi(`/api/usage/blocks?days=${days}`, apiKey);
563
+
564
+ if (jsonMode) {
565
+ console.log(JSON.stringify(data, null, 2));
566
+ return;
567
+ }
568
+
569
+ console.log('');
570
+ console.log(` AIMeter — Billing Blocks (last ${days} days)`);
571
+ console.log(' ════════════════════════════════════════');
572
+ console.log('');
573
+
574
+ if (!data.blocks || data.blocks.length === 0) {
575
+ console.log(' No billing blocks found.');
576
+ console.log('');
577
+ return;
578
+ }
579
+
580
+ for (const block of data.blocks) {
581
+ const start = new Date(block.startTime);
582
+ const fmtTime = (d) => d.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
583
+ const active = block.isActive ? ' ◉ ACTIVE' : '';
584
+
585
+ console.log(` ${fmtTime(start)}${active}`);
586
+ console.log(` ────────────────────────────`);
587
+
588
+ const fmtCost = (c) => c >= 1 ? `$${c.toFixed(2)}` : `$${c.toFixed(3)}`;
589
+ const fmtTokens = (n) => {
590
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
591
+ if (n >= 1_000) return `${(n / 1_000).toFixed(0)}k`;
592
+ return String(n);
593
+ };
594
+
595
+ console.log(` Cost: ${fmtCost(block.totalCost)} | Requests: ${block.requestCount} | Tokens: ${fmtTokens(block.totalInputTokens + block.totalOutputTokens)}`);
596
+
597
+ if (block.burnRate) {
598
+ console.log(` Burn: ${fmtTokens(block.burnRate.tokensPerMinute)}/min | ${fmtCost(block.burnRate.costPerHour)}/hr`);
599
+ }
600
+
601
+ if (block.isActive && block.remainingMinutes > 0) {
602
+ const h = Math.floor(block.remainingMinutes / 60);
603
+ const m = Math.round(block.remainingMinutes % 60);
604
+ console.log(` Remaining: ${h}h ${m}m`);
605
+ }
606
+
607
+ console.log(` Sources: ${(block.sources || []).join(', ')}`);
608
+ console.log(` Models: ${(block.models || []).join(', ')}`);
609
+ console.log('');
610
+ }
611
+
612
+ console.log(` Total: ${data.totalBlocks} blocks, $${data.totalCost.toFixed(2)}`);
613
+ console.log('');
614
+ } catch (err) {
615
+ console.error(`Error fetching blocks: ${err.message}`);
616
+ process.exitCode = 1;
617
+ }
618
+ }
619
+
620
+ // ---------------------------------------------------------------------------
621
+ // help
622
+ // ---------------------------------------------------------------------------
623
+
624
+ function printHelp() {
625
+ console.log(`
626
+ AIMeter — Track your Claude AI usage
627
+ ═════════════════════════════════════
628
+
629
+ Usage: aimeter <command>
630
+
631
+ Getting started:
632
+ setup Full onboarding wizard (recommended)
633
+
634
+ Service management:
635
+ install Install background service
636
+ uninstall Remove background service
637
+ start Start the background service
638
+ stop Stop the background service
639
+
640
+ Manual mode:
641
+ watch Run watcher in foreground
642
+
643
+ Usage insights:
644
+ summary Show current usage summary (--json for JSON)
645
+ blocks [N] Show 5-hour billing blocks (last N days, default 3)
646
+
647
+ Info:
648
+ status Show current configuration
649
+ logs [N] Tail watcher log (last N lines, default 50)
650
+ key Print current API key
651
+
652
+ Integration:
653
+ mcp Start MCP server (for Claude Code/Desktop)
654
+
655
+ https://getaimeter.com
656
+ `);
657
+ }