claude-stats 0.1.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/dist/collector.d.ts +3 -0
- package/dist/collector.js +676 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1169 -0
- package/dist/streamer.d.ts +35 -0
- package/dist/streamer.js +199 -0
- package/dist/usage-limits.d.ts +14 -0
- package/dist/usage-limits.js +325 -0
- package/package.json +47 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { MonitorData } from '@claude-monitor/shared';
|
|
2
|
+
export interface StreamerOptions {
|
|
3
|
+
server: string;
|
|
4
|
+
token: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
interval?: number;
|
|
7
|
+
getAccountId: () => string;
|
|
8
|
+
}
|
|
9
|
+
interface HeartbeatResponse {
|
|
10
|
+
needsData: boolean;
|
|
11
|
+
clientCount: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class Streamer {
|
|
14
|
+
private server;
|
|
15
|
+
private token;
|
|
16
|
+
private machineId;
|
|
17
|
+
private machineName;
|
|
18
|
+
private interval;
|
|
19
|
+
private getAccountId;
|
|
20
|
+
private isRunning;
|
|
21
|
+
private intervalId;
|
|
22
|
+
private consecutiveErrors;
|
|
23
|
+
private maxRetries;
|
|
24
|
+
private lastClientCount;
|
|
25
|
+
constructor(options: StreamerOptions);
|
|
26
|
+
/**
|
|
27
|
+
* Check with server if any clients are viewing the dashboard
|
|
28
|
+
*/
|
|
29
|
+
checkHeartbeat(): Promise<HeartbeatResponse>;
|
|
30
|
+
send(data: MonitorData): Promise<boolean>;
|
|
31
|
+
streamOnce(collectFn: () => MonitorData): Promise<boolean>;
|
|
32
|
+
start(collectFn: () => MonitorData): void;
|
|
33
|
+
stop(): void;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
package/dist/streamer.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Streamer = void 0;
|
|
37
|
+
const os = __importStar(require("os"));
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
// Generate a stable machine ID based on hostname and username
|
|
40
|
+
function getMachineId() {
|
|
41
|
+
const hostname = os.hostname();
|
|
42
|
+
const username = os.userInfo().username;
|
|
43
|
+
const combined = `${hostname}-${username}`;
|
|
44
|
+
return crypto.createHash('md5').update(combined).digest('hex').slice(0, 12);
|
|
45
|
+
}
|
|
46
|
+
class Streamer {
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.isRunning = false;
|
|
49
|
+
this.intervalId = null;
|
|
50
|
+
this.consecutiveErrors = 0;
|
|
51
|
+
this.maxRetries = 5;
|
|
52
|
+
this.lastClientCount = 0;
|
|
53
|
+
this.server = options.server.replace(/\/$/, ''); // Remove trailing slash
|
|
54
|
+
this.token = options.token;
|
|
55
|
+
this.machineId = getMachineId();
|
|
56
|
+
this.machineName = options.name || os.hostname();
|
|
57
|
+
this.interval = options.interval || 10000;
|
|
58
|
+
this.getAccountId = options.getAccountId;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check with server if any clients are viewing the dashboard
|
|
62
|
+
*/
|
|
63
|
+
async checkHeartbeat() {
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(`${this.server}/api/heartbeat`, {
|
|
66
|
+
method: 'GET',
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${this.token}`,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
return { needsData: true, clientCount: 0 }; // Default to sending on error
|
|
73
|
+
}
|
|
74
|
+
return (await response.json());
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return { needsData: true, clientCount: 0 }; // Default to sending on error
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async send(data) {
|
|
81
|
+
const payload = {
|
|
82
|
+
machine_id: this.machineId,
|
|
83
|
+
machine_name: this.machineName,
|
|
84
|
+
account_id: this.getAccountId(),
|
|
85
|
+
timestamp: new Date().toISOString(),
|
|
86
|
+
data,
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(`${this.server}/api/stream`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
Authorization: `Bearer ${this.token}`,
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify(payload),
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const text = await response.text();
|
|
99
|
+
console.error(`Server error: ${response.status} - ${text}`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error(`Network error: ${error instanceof Error ? error.message : error}`);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async streamOnce(collectFn) {
|
|
110
|
+
try {
|
|
111
|
+
const data = collectFn();
|
|
112
|
+
const success = await this.send(data);
|
|
113
|
+
if (success) {
|
|
114
|
+
this.consecutiveErrors = 0;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
this.consecutiveErrors++;
|
|
118
|
+
}
|
|
119
|
+
return success;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
console.error(`Collection error: ${error instanceof Error ? error.message : error}`);
|
|
123
|
+
this.consecutiveErrors++;
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
start(collectFn) {
|
|
128
|
+
if (this.isRunning) {
|
|
129
|
+
console.log('Streamer is already running');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.isRunning = true;
|
|
133
|
+
console.log(`Starting streamer (smart polling)...`);
|
|
134
|
+
console.log(` Machine ID: ${this.machineId}`);
|
|
135
|
+
console.log(` Machine Name: ${this.machineName}`);
|
|
136
|
+
console.log(` Account ID: ${this.getAccountId()}`);
|
|
137
|
+
console.log(` Server: ${this.server}`);
|
|
138
|
+
console.log(` Interval: ${this.interval}ms`);
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log('Data will only be sent when someone is viewing the dashboard.');
|
|
141
|
+
console.log('');
|
|
142
|
+
// Initial send to register the machine
|
|
143
|
+
this.streamOnce(collectFn).then((success) => {
|
|
144
|
+
if (success) {
|
|
145
|
+
console.log(`✓ Connected to server`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.log(`✗ Failed to connect to server`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
// Start interval with smart polling
|
|
152
|
+
this.intervalId = setInterval(async () => {
|
|
153
|
+
// Check if anyone is viewing the dashboard
|
|
154
|
+
const heartbeat = await this.checkHeartbeat();
|
|
155
|
+
// Log when client count changes
|
|
156
|
+
if (heartbeat.clientCount !== this.lastClientCount) {
|
|
157
|
+
if (heartbeat.clientCount > 0) {
|
|
158
|
+
console.log(`\n👀 ${heartbeat.clientCount} viewer(s) connected - streaming data`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
console.log(`\n💤 No viewers - pausing data stream`);
|
|
162
|
+
}
|
|
163
|
+
this.lastClientCount = heartbeat.clientCount;
|
|
164
|
+
}
|
|
165
|
+
if (!heartbeat.needsData) {
|
|
166
|
+
// No one viewing, just send a lightweight heartbeat indicator
|
|
167
|
+
process.stdout.write('_');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Someone is viewing, send full data
|
|
171
|
+
const success = await this.streamOnce(collectFn);
|
|
172
|
+
if (this.consecutiveErrors >= this.maxRetries) {
|
|
173
|
+
console.error(`\nToo many consecutive errors (${this.consecutiveErrors}), will keep trying...`);
|
|
174
|
+
this.consecutiveErrors = 0;
|
|
175
|
+
}
|
|
176
|
+
if (success) {
|
|
177
|
+
process.stdout.write('.');
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
process.stdout.write('x');
|
|
181
|
+
}
|
|
182
|
+
}, this.interval);
|
|
183
|
+
// Handle graceful shutdown
|
|
184
|
+
process.on('SIGINT', () => this.stop());
|
|
185
|
+
process.on('SIGTERM', () => this.stop());
|
|
186
|
+
}
|
|
187
|
+
stop() {
|
|
188
|
+
if (!this.isRunning)
|
|
189
|
+
return;
|
|
190
|
+
console.log('\nStopping streamer...');
|
|
191
|
+
this.isRunning = false;
|
|
192
|
+
if (this.intervalId) {
|
|
193
|
+
clearInterval(this.intervalId);
|
|
194
|
+
this.intervalId = null;
|
|
195
|
+
}
|
|
196
|
+
process.exit(0);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
exports.Streamer = Streamer;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { UsageLimits } from '@claude-monitor/shared';
|
|
2
|
+
/**
|
|
3
|
+
* Get cached usage limits synchronously.
|
|
4
|
+
* Starts a background fetch if cache is stale.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getUsageLimitsSync(): UsageLimits | null;
|
|
7
|
+
/**
|
|
8
|
+
* Async function to fetch usage limits (for initial load)
|
|
9
|
+
*/
|
|
10
|
+
export declare function fetchUsageLimits(): Promise<UsageLimits | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Force refresh usage limits (clears cache and fetches fresh data)
|
|
13
|
+
*/
|
|
14
|
+
export declare function forceRefreshUsageLimits(): Promise<UsageLimits | null>;
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getUsageLimitsSync = getUsageLimitsSync;
|
|
37
|
+
exports.fetchUsageLimits = fetchUsageLimits;
|
|
38
|
+
exports.forceRefreshUsageLimits = forceRefreshUsageLimits;
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const os = __importStar(require("os"));
|
|
43
|
+
// Cache the usage limits
|
|
44
|
+
let cachedLimits = null;
|
|
45
|
+
let lastFetchTime = 0;
|
|
46
|
+
let isFetching = false;
|
|
47
|
+
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes memory cache
|
|
48
|
+
// Cache file for persisting usage data
|
|
49
|
+
const CACHE_FILE = path.join(os.homedir(), '.claude', 'usage-cache.json');
|
|
50
|
+
const FILE_CACHE_DURATION = 30 * 60 * 1000; // 30 minutes file cache
|
|
51
|
+
// OAuth API endpoint (like CodexBar)
|
|
52
|
+
const OAUTH_API_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
53
|
+
/**
|
|
54
|
+
* Load cached limits from file
|
|
55
|
+
*/
|
|
56
|
+
function loadCacheFromFile() {
|
|
57
|
+
try {
|
|
58
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
59
|
+
const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
|
|
60
|
+
// Check if cache is still valid
|
|
61
|
+
if (data.fetched_at) {
|
|
62
|
+
const age = Date.now() - new Date(data.fetched_at).getTime();
|
|
63
|
+
if (age < FILE_CACHE_DURATION) {
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Ignore errors
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Save limits to cache file
|
|
76
|
+
*/
|
|
77
|
+
function saveCacheToFile(limits) {
|
|
78
|
+
try {
|
|
79
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(limits, null, 2));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Ignore errors
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get OAuth credentials from macOS Keychain
|
|
87
|
+
*/
|
|
88
|
+
function getOAuthCredentials() {
|
|
89
|
+
try {
|
|
90
|
+
// Try macOS Keychain first
|
|
91
|
+
const result = (0, child_process_1.execSync)('security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null', { encoding: 'utf-8', timeout: 5000 });
|
|
92
|
+
const credentials = JSON.parse(result.trim());
|
|
93
|
+
if (credentials.claudeAiOauth?.accessToken) {
|
|
94
|
+
return {
|
|
95
|
+
accessToken: credentials.claudeAiOauth.accessToken,
|
|
96
|
+
refreshToken: credentials.claudeAiOauth.refreshToken,
|
|
97
|
+
expiresAt: credentials.claudeAiOauth.expiresAt,
|
|
98
|
+
scopes: credentials.claudeAiOauth.scopes,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Keychain access failed, try file-based credentials
|
|
104
|
+
}
|
|
105
|
+
// Try ~/.claude/.credentials.json
|
|
106
|
+
try {
|
|
107
|
+
const credFile = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
108
|
+
if (fs.existsSync(credFile)) {
|
|
109
|
+
const data = JSON.parse(fs.readFileSync(credFile, 'utf-8'));
|
|
110
|
+
if (data.claudeAiOauth?.accessToken) {
|
|
111
|
+
return {
|
|
112
|
+
accessToken: data.claudeAiOauth.accessToken,
|
|
113
|
+
refreshToken: data.claudeAiOauth.refreshToken,
|
|
114
|
+
expiresAt: data.claudeAiOauth.expiresAt,
|
|
115
|
+
scopes: data.claudeAiOauth.scopes,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// File access failed
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Fetch usage via OAuth API (like CodexBar)
|
|
127
|
+
*/
|
|
128
|
+
async function fetchUsageViaOAuthAPI() {
|
|
129
|
+
const credentials = getOAuthCredentials();
|
|
130
|
+
if (!credentials) {
|
|
131
|
+
console.log('[usage-limits] No OAuth credentials found');
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
// Check if token is expired
|
|
135
|
+
if (credentials.expiresAt) {
|
|
136
|
+
const expiresAt = new Date(credentials.expiresAt).getTime();
|
|
137
|
+
if (Date.now() > expiresAt) {
|
|
138
|
+
console.log('[usage-limits] OAuth token expired');
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const response = await fetch(OAUTH_API_URL, {
|
|
144
|
+
method: 'GET',
|
|
145
|
+
headers: {
|
|
146
|
+
'Authorization': `Bearer ${credentials.accessToken}`,
|
|
147
|
+
'Accept': 'application/json',
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
150
|
+
'User-Agent': 'claude-monitor-cli',
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
const text = await response.text();
|
|
155
|
+
console.log(`[usage-limits] OAuth API error: ${response.status} - ${text}`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const data = (await response.json());
|
|
159
|
+
return parseOAuthResponse(data);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
console.log(`[usage-limits] OAuth API fetch error: ${error instanceof Error ? error.message : error}`);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Parse OAuth API response into UsageLimits
|
|
168
|
+
*/
|
|
169
|
+
function parseOAuthResponse(data) {
|
|
170
|
+
const limits = {
|
|
171
|
+
session: null,
|
|
172
|
+
weekly: null,
|
|
173
|
+
fetched_at: new Date().toISOString(),
|
|
174
|
+
};
|
|
175
|
+
// Parse 5-hour session window
|
|
176
|
+
if (data.five_hour) {
|
|
177
|
+
const window = data.five_hour;
|
|
178
|
+
// API returns 'utilization' as the percentage used
|
|
179
|
+
const percentUsed = window.utilization ?? window.percent_used ?? (100 - (window.percent_remaining ?? 0));
|
|
180
|
+
const resetTime = window.resets_at || window.reset_at || null;
|
|
181
|
+
limits.session = {
|
|
182
|
+
percent_used: Math.round(percentUsed),
|
|
183
|
+
resets_at: resetTime,
|
|
184
|
+
reset_description: resetTime ? formatResetTime(resetTime) : null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// Parse 7-day weekly window
|
|
188
|
+
if (data.seven_day) {
|
|
189
|
+
const window = data.seven_day;
|
|
190
|
+
const percentUsed = window.utilization ?? window.percent_used ?? (100 - (window.percent_remaining ?? 0));
|
|
191
|
+
const resetTime = window.resets_at || window.reset_at || null;
|
|
192
|
+
limits.weekly = {
|
|
193
|
+
percent_used: Math.round(percentUsed),
|
|
194
|
+
resets_at: resetTime,
|
|
195
|
+
reset_description: resetTime ? formatResetTime(resetTime) : null,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (limits.session || limits.weekly) {
|
|
199
|
+
return limits;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Format reset time as relative description
|
|
205
|
+
*/
|
|
206
|
+
function formatResetTime(isoTime) {
|
|
207
|
+
try {
|
|
208
|
+
const resetTime = new Date(isoTime).getTime();
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const diffMs = resetTime - now;
|
|
211
|
+
if (diffMs <= 0)
|
|
212
|
+
return 'Soon';
|
|
213
|
+
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
214
|
+
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
215
|
+
if (hours > 24) {
|
|
216
|
+
const days = Math.floor(hours / 24);
|
|
217
|
+
return `${days}d ${hours % 24}h`;
|
|
218
|
+
}
|
|
219
|
+
if (hours > 0) {
|
|
220
|
+
return `${hours}h ${minutes}m`;
|
|
221
|
+
}
|
|
222
|
+
return `${minutes}m`;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return 'Unknown';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get cached usage limits synchronously.
|
|
230
|
+
* Starts a background fetch if cache is stale.
|
|
231
|
+
*/
|
|
232
|
+
function getUsageLimitsSync() {
|
|
233
|
+
// Try memory cache first
|
|
234
|
+
if (cachedLimits && Date.now() - lastFetchTime < CACHE_DURATION) {
|
|
235
|
+
return cachedLimits;
|
|
236
|
+
}
|
|
237
|
+
// Try file cache
|
|
238
|
+
const fileCache = loadCacheFromFile();
|
|
239
|
+
if (fileCache) {
|
|
240
|
+
cachedLimits = fileCache;
|
|
241
|
+
lastFetchTime = Date.now();
|
|
242
|
+
// Trigger background refresh if cache is getting old
|
|
243
|
+
const age = Date.now() - new Date(fileCache.fetched_at).getTime();
|
|
244
|
+
if (age > 10 * 60 * 1000 && !isFetching) {
|
|
245
|
+
fetchUsageLimitsBackground();
|
|
246
|
+
}
|
|
247
|
+
return cachedLimits;
|
|
248
|
+
}
|
|
249
|
+
// Start background fetch if needed
|
|
250
|
+
if (!isFetching) {
|
|
251
|
+
fetchUsageLimitsBackground();
|
|
252
|
+
}
|
|
253
|
+
return cachedLimits;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Fetch usage limits in the background without blocking
|
|
257
|
+
*/
|
|
258
|
+
function fetchUsageLimitsBackground() {
|
|
259
|
+
if (isFetching)
|
|
260
|
+
return;
|
|
261
|
+
isFetching = true;
|
|
262
|
+
fetchUsageViaOAuthAPI()
|
|
263
|
+
.then((limits) => {
|
|
264
|
+
if (limits) {
|
|
265
|
+
cachedLimits = limits;
|
|
266
|
+
lastFetchTime = Date.now();
|
|
267
|
+
saveCacheToFile(limits);
|
|
268
|
+
console.log('[usage-limits] Fetched via OAuth API successfully');
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
console.log('[usage-limits] OAuth API failed, no fallback available');
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
.catch((err) => {
|
|
275
|
+
console.log('[usage-limits] Error fetching usage:', err?.message || err);
|
|
276
|
+
})
|
|
277
|
+
.finally(() => {
|
|
278
|
+
isFetching = false;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Async function to fetch usage limits (for initial load)
|
|
283
|
+
*/
|
|
284
|
+
async function fetchUsageLimits() {
|
|
285
|
+
// Try file cache first
|
|
286
|
+
const fileCache = loadCacheFromFile();
|
|
287
|
+
if (fileCache) {
|
|
288
|
+
// Start background refresh if cache is getting old (> 10 min)
|
|
289
|
+
const age = Date.now() - new Date(fileCache.fetched_at).getTime();
|
|
290
|
+
if (age > 10 * 60 * 1000 && !isFetching) {
|
|
291
|
+
fetchUsageLimitsBackground();
|
|
292
|
+
}
|
|
293
|
+
return fileCache;
|
|
294
|
+
}
|
|
295
|
+
// Try OAuth API
|
|
296
|
+
return fetchUsageViaOAuthAPI();
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Force refresh usage limits (clears cache and fetches fresh data)
|
|
300
|
+
*/
|
|
301
|
+
async function forceRefreshUsageLimits() {
|
|
302
|
+
cachedLimits = null;
|
|
303
|
+
lastFetchTime = 0;
|
|
304
|
+
// Delete cache file
|
|
305
|
+
try {
|
|
306
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
307
|
+
fs.unlinkSync(CACHE_FILE);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// Ignore
|
|
312
|
+
}
|
|
313
|
+
console.log('[usage-limits] Force refreshing via OAuth API...');
|
|
314
|
+
const limits = await fetchUsageViaOAuthAPI();
|
|
315
|
+
if (limits) {
|
|
316
|
+
cachedLimits = limits;
|
|
317
|
+
lastFetchTime = Date.now();
|
|
318
|
+
saveCacheToFile(limits);
|
|
319
|
+
console.log('[usage-limits] Force refresh successful');
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
console.log('[usage-limits] Force refresh failed');
|
|
323
|
+
}
|
|
324
|
+
return limits;
|
|
325
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-stats",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Monitor and stream Claude Code usage statistics to a remote dashboard",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"anthropic",
|
|
8
|
+
"cli",
|
|
9
|
+
"usage",
|
|
10
|
+
"monitor",
|
|
11
|
+
"stats",
|
|
12
|
+
"tokens"
|
|
13
|
+
],
|
|
14
|
+
"author": "Claude Monitor",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/projectashik/claude-monitor"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"claude-stats": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --external:commander --banner:js='#!/usr/bin/env node'",
|
|
28
|
+
"build:tsc": "tsc",
|
|
29
|
+
"dev": "tsc --watch",
|
|
30
|
+
"stream": "tsx src/index.ts",
|
|
31
|
+
"start": "node dist/index.js",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"commander": "^12.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@claude-monitor/shared": "*",
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"esbuild": "^0.24.0",
|
|
41
|
+
"tsx": "^4.7.0",
|
|
42
|
+
"typescript": "^5.7.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|