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
package/dist/index.js
ADDED
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
|
|
29
|
+
// ../../packages/shared/dist/types.js
|
|
30
|
+
var require_types = __commonJS({
|
|
31
|
+
"../../packages/shared/dist/types.js"(exports2) {
|
|
32
|
+
"use strict";
|
|
33
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ../../packages/shared/dist/pricing.js
|
|
38
|
+
var require_pricing = __commonJS({
|
|
39
|
+
"../../packages/shared/dist/pricing.js"(exports2) {
|
|
40
|
+
"use strict";
|
|
41
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
42
|
+
exports2.DEFAULT_PRICING = exports2.MODEL_PRICING = void 0;
|
|
43
|
+
exports2.calculateCost = calculateCost2;
|
|
44
|
+
exports2.formatModelName = formatModelName;
|
|
45
|
+
exports2.MODEL_PRICING = {
|
|
46
|
+
"claude-opus-4-5-20251101": { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
|
|
47
|
+
"claude-opus-4-5-thinking": { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
|
|
48
|
+
"claude-sonnet-4-5-20250929": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
49
|
+
"claude-sonnet-4-5-thinking": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
50
|
+
"claude-haiku-4-5": { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }
|
|
51
|
+
};
|
|
52
|
+
exports2.DEFAULT_PRICING = {
|
|
53
|
+
input: 3,
|
|
54
|
+
output: 15,
|
|
55
|
+
cache_read: 0.3,
|
|
56
|
+
cache_write: 3.75
|
|
57
|
+
};
|
|
58
|
+
function calculateCost2(model, usage) {
|
|
59
|
+
const pricing = exports2.MODEL_PRICING[model] || exports2.DEFAULT_PRICING;
|
|
60
|
+
const inputTokens = usage.inputTokens ?? usage.input_tokens ?? 0;
|
|
61
|
+
const outputTokens = usage.outputTokens ?? usage.output_tokens ?? 0;
|
|
62
|
+
const cacheRead = usage.cacheReadInputTokens ?? usage.cache_read_input_tokens ?? 0;
|
|
63
|
+
const cacheWrite = usage.cacheCreationInputTokens ?? usage.cache_creation_input_tokens ?? 0;
|
|
64
|
+
const cost = inputTokens / 1e6 * pricing.input + outputTokens / 1e6 * pricing.output + cacheRead / 1e6 * pricing.cache_read + cacheWrite / 1e6 * pricing.cache_write;
|
|
65
|
+
return Math.round(cost * 1e4) / 1e4;
|
|
66
|
+
}
|
|
67
|
+
function formatModelName(model) {
|
|
68
|
+
return model.replace("claude-", "").replace("-20251101", "").replace("-20250929", "").replace("-thinking", " (think)").replace("gemini-", "").replace("-high", "");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ../../packages/shared/dist/index.js
|
|
74
|
+
var require_dist = __commonJS({
|
|
75
|
+
"../../packages/shared/dist/index.js"(exports2) {
|
|
76
|
+
"use strict";
|
|
77
|
+
var __createBinding = exports2 && exports2.__createBinding || (Object.create ? function(o, m, k, k2) {
|
|
78
|
+
if (k2 === void 0) k2 = k;
|
|
79
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
80
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
81
|
+
desc = { enumerable: true, get: function() {
|
|
82
|
+
return m[k];
|
|
83
|
+
} };
|
|
84
|
+
}
|
|
85
|
+
Object.defineProperty(o, k2, desc);
|
|
86
|
+
} : function(o, m, k, k2) {
|
|
87
|
+
if (k2 === void 0) k2 = k;
|
|
88
|
+
o[k2] = m[k];
|
|
89
|
+
});
|
|
90
|
+
var __exportStar = exports2 && exports2.__exportStar || function(m, exports3) {
|
|
91
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p)) __createBinding(exports3, m, p);
|
|
92
|
+
};
|
|
93
|
+
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
94
|
+
__exportStar(require_types(), exports2);
|
|
95
|
+
__exportStar(require_pricing(), exports2);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// src/index.ts
|
|
100
|
+
var import_commander = require("commander");
|
|
101
|
+
var readline = __toESM(require("readline"));
|
|
102
|
+
|
|
103
|
+
// src/streamer.ts
|
|
104
|
+
var os = __toESM(require("os"));
|
|
105
|
+
var crypto = __toESM(require("crypto"));
|
|
106
|
+
function getMachineId() {
|
|
107
|
+
const hostname2 = os.hostname();
|
|
108
|
+
const username = os.userInfo().username;
|
|
109
|
+
const combined = `${hostname2}-${username}`;
|
|
110
|
+
return crypto.createHash("md5").update(combined).digest("hex").slice(0, 12);
|
|
111
|
+
}
|
|
112
|
+
var Streamer = class {
|
|
113
|
+
constructor(options) {
|
|
114
|
+
this.isRunning = false;
|
|
115
|
+
this.intervalId = null;
|
|
116
|
+
this.consecutiveErrors = 0;
|
|
117
|
+
this.maxRetries = 5;
|
|
118
|
+
this.lastClientCount = 0;
|
|
119
|
+
this.server = options.server.replace(/\/$/, "");
|
|
120
|
+
this.token = options.token;
|
|
121
|
+
this.machineId = getMachineId();
|
|
122
|
+
this.machineName = options.name || os.hostname();
|
|
123
|
+
this.interval = options.interval || 1e4;
|
|
124
|
+
this.getAccountId = options.getAccountId;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check with server if any clients are viewing the dashboard
|
|
128
|
+
*/
|
|
129
|
+
async checkHeartbeat() {
|
|
130
|
+
try {
|
|
131
|
+
const response = await fetch(`${this.server}/api/heartbeat`, {
|
|
132
|
+
method: "GET",
|
|
133
|
+
headers: {
|
|
134
|
+
Authorization: `Bearer ${this.token}`
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
return { needsData: true, clientCount: 0 };
|
|
139
|
+
}
|
|
140
|
+
return await response.json();
|
|
141
|
+
} catch {
|
|
142
|
+
return { needsData: true, clientCount: 0 };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async send(data) {
|
|
146
|
+
const payload = {
|
|
147
|
+
machine_id: this.machineId,
|
|
148
|
+
machine_name: this.machineName,
|
|
149
|
+
account_id: this.getAccountId(),
|
|
150
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151
|
+
data
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(`${this.server}/api/stream`, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: {
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
Authorization: `Bearer ${this.token}`
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify(payload)
|
|
161
|
+
});
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
const text = await response.text();
|
|
164
|
+
console.error(`Server error: ${response.status} - ${text}`);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`Network error: ${error instanceof Error ? error.message : error}`);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async streamOnce(collectFn) {
|
|
174
|
+
try {
|
|
175
|
+
const data = collectFn();
|
|
176
|
+
const success = await this.send(data);
|
|
177
|
+
if (success) {
|
|
178
|
+
this.consecutiveErrors = 0;
|
|
179
|
+
} else {
|
|
180
|
+
this.consecutiveErrors++;
|
|
181
|
+
}
|
|
182
|
+
return success;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error(`Collection error: ${error instanceof Error ? error.message : error}`);
|
|
185
|
+
this.consecutiveErrors++;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
start(collectFn) {
|
|
190
|
+
if (this.isRunning) {
|
|
191
|
+
console.log("Streamer is already running");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.isRunning = true;
|
|
195
|
+
console.log(`Starting streamer (smart polling)...`);
|
|
196
|
+
console.log(` Machine ID: ${this.machineId}`);
|
|
197
|
+
console.log(` Machine Name: ${this.machineName}`);
|
|
198
|
+
console.log(` Account ID: ${this.getAccountId()}`);
|
|
199
|
+
console.log(` Server: ${this.server}`);
|
|
200
|
+
console.log(` Interval: ${this.interval}ms`);
|
|
201
|
+
console.log("");
|
|
202
|
+
console.log("Data will only be sent when someone is viewing the dashboard.");
|
|
203
|
+
console.log("");
|
|
204
|
+
this.streamOnce(collectFn).then((success) => {
|
|
205
|
+
if (success) {
|
|
206
|
+
console.log(`\u2713 Connected to server`);
|
|
207
|
+
} else {
|
|
208
|
+
console.log(`\u2717 Failed to connect to server`);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
this.intervalId = setInterval(async () => {
|
|
212
|
+
const heartbeat = await this.checkHeartbeat();
|
|
213
|
+
if (heartbeat.clientCount !== this.lastClientCount) {
|
|
214
|
+
if (heartbeat.clientCount > 0) {
|
|
215
|
+
console.log(`
|
|
216
|
+
\u{1F440} ${heartbeat.clientCount} viewer(s) connected - streaming data`);
|
|
217
|
+
} else {
|
|
218
|
+
console.log(`
|
|
219
|
+
\u{1F4A4} No viewers - pausing data stream`);
|
|
220
|
+
}
|
|
221
|
+
this.lastClientCount = heartbeat.clientCount;
|
|
222
|
+
}
|
|
223
|
+
if (!heartbeat.needsData) {
|
|
224
|
+
process.stdout.write("_");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const success = await this.streamOnce(collectFn);
|
|
228
|
+
if (this.consecutiveErrors >= this.maxRetries) {
|
|
229
|
+
console.error(`
|
|
230
|
+
Too many consecutive errors (${this.consecutiveErrors}), will keep trying...`);
|
|
231
|
+
this.consecutiveErrors = 0;
|
|
232
|
+
}
|
|
233
|
+
if (success) {
|
|
234
|
+
process.stdout.write(".");
|
|
235
|
+
} else {
|
|
236
|
+
process.stdout.write("x");
|
|
237
|
+
}
|
|
238
|
+
}, this.interval);
|
|
239
|
+
process.on("SIGINT", () => this.stop());
|
|
240
|
+
process.on("SIGTERM", () => this.stop());
|
|
241
|
+
}
|
|
242
|
+
stop() {
|
|
243
|
+
if (!this.isRunning) return;
|
|
244
|
+
console.log("\nStopping streamer...");
|
|
245
|
+
this.isRunning = false;
|
|
246
|
+
if (this.intervalId) {
|
|
247
|
+
clearInterval(this.intervalId);
|
|
248
|
+
this.intervalId = null;
|
|
249
|
+
}
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// src/collector.ts
|
|
255
|
+
var fs2 = __toESM(require("fs"));
|
|
256
|
+
var path2 = __toESM(require("path"));
|
|
257
|
+
var os3 = __toESM(require("os"));
|
|
258
|
+
var crypto2 = __toESM(require("crypto"));
|
|
259
|
+
var import_child_process2 = require("child_process");
|
|
260
|
+
var import_shared = __toESM(require_dist());
|
|
261
|
+
|
|
262
|
+
// src/usage-limits.ts
|
|
263
|
+
var import_child_process = require("child_process");
|
|
264
|
+
var fs = __toESM(require("fs"));
|
|
265
|
+
var path = __toESM(require("path"));
|
|
266
|
+
var os2 = __toESM(require("os"));
|
|
267
|
+
var cachedLimits = null;
|
|
268
|
+
var lastFetchTime = 0;
|
|
269
|
+
var isFetching = false;
|
|
270
|
+
var CACHE_DURATION = 5 * 60 * 1e3;
|
|
271
|
+
var CACHE_FILE = path.join(os2.homedir(), ".claude", "usage-cache.json");
|
|
272
|
+
var FILE_CACHE_DURATION = 30 * 60 * 1e3;
|
|
273
|
+
var OAUTH_API_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
274
|
+
function loadCacheFromFile() {
|
|
275
|
+
try {
|
|
276
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
277
|
+
const data = JSON.parse(fs.readFileSync(CACHE_FILE, "utf-8"));
|
|
278
|
+
if (data.fetched_at) {
|
|
279
|
+
const age = Date.now() - new Date(data.fetched_at).getTime();
|
|
280
|
+
if (age < FILE_CACHE_DURATION) {
|
|
281
|
+
return data;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
function saveCacheToFile(limits) {
|
|
290
|
+
try {
|
|
291
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(limits, null, 2));
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function getOAuthCredentials() {
|
|
296
|
+
try {
|
|
297
|
+
const result = (0, import_child_process.execSync)(
|
|
298
|
+
'security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
|
|
299
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
300
|
+
);
|
|
301
|
+
const credentials = JSON.parse(result.trim());
|
|
302
|
+
if (credentials.claudeAiOauth?.accessToken) {
|
|
303
|
+
return {
|
|
304
|
+
accessToken: credentials.claudeAiOauth.accessToken,
|
|
305
|
+
refreshToken: credentials.claudeAiOauth.refreshToken,
|
|
306
|
+
expiresAt: credentials.claudeAiOauth.expiresAt,
|
|
307
|
+
scopes: credentials.claudeAiOauth.scopes
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const credFile = path.join(os2.homedir(), ".claude", ".credentials.json");
|
|
314
|
+
if (fs.existsSync(credFile)) {
|
|
315
|
+
const data = JSON.parse(fs.readFileSync(credFile, "utf-8"));
|
|
316
|
+
if (data.claudeAiOauth?.accessToken) {
|
|
317
|
+
return {
|
|
318
|
+
accessToken: data.claudeAiOauth.accessToken,
|
|
319
|
+
refreshToken: data.claudeAiOauth.refreshToken,
|
|
320
|
+
expiresAt: data.claudeAiOauth.expiresAt,
|
|
321
|
+
scopes: data.claudeAiOauth.scopes
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
async function fetchUsageViaOAuthAPI() {
|
|
330
|
+
const credentials = getOAuthCredentials();
|
|
331
|
+
if (!credentials) {
|
|
332
|
+
console.log("[usage-limits] No OAuth credentials found");
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
if (credentials.expiresAt) {
|
|
336
|
+
const expiresAt = new Date(credentials.expiresAt).getTime();
|
|
337
|
+
if (Date.now() > expiresAt) {
|
|
338
|
+
console.log("[usage-limits] OAuth token expired");
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const response = await fetch(OAUTH_API_URL, {
|
|
344
|
+
method: "GET",
|
|
345
|
+
headers: {
|
|
346
|
+
"Authorization": `Bearer ${credentials.accessToken}`,
|
|
347
|
+
"Accept": "application/json",
|
|
348
|
+
"Content-Type": "application/json",
|
|
349
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
350
|
+
"User-Agent": "claude-monitor-cli"
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
const text = await response.text();
|
|
355
|
+
console.log(`[usage-limits] OAuth API error: ${response.status} - ${text}`);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const data = await response.json();
|
|
359
|
+
return parseOAuthResponse(data);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.log(`[usage-limits] OAuth API fetch error: ${error instanceof Error ? error.message : error}`);
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function parseOAuthResponse(data) {
|
|
366
|
+
const limits = {
|
|
367
|
+
session: null,
|
|
368
|
+
weekly: null,
|
|
369
|
+
fetched_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
370
|
+
};
|
|
371
|
+
if (data.five_hour) {
|
|
372
|
+
const window = data.five_hour;
|
|
373
|
+
const percentUsed = window.utilization ?? window.percent_used ?? 100 - (window.percent_remaining ?? 0);
|
|
374
|
+
const resetTime = window.resets_at || window.reset_at || null;
|
|
375
|
+
limits.session = {
|
|
376
|
+
percent_used: Math.round(percentUsed),
|
|
377
|
+
resets_at: resetTime,
|
|
378
|
+
reset_description: resetTime ? formatResetTime(resetTime) : null
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
if (data.seven_day) {
|
|
382
|
+
const window = data.seven_day;
|
|
383
|
+
const percentUsed = window.utilization ?? window.percent_used ?? 100 - (window.percent_remaining ?? 0);
|
|
384
|
+
const resetTime = window.resets_at || window.reset_at || null;
|
|
385
|
+
limits.weekly = {
|
|
386
|
+
percent_used: Math.round(percentUsed),
|
|
387
|
+
resets_at: resetTime,
|
|
388
|
+
reset_description: resetTime ? formatResetTime(resetTime) : null
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (limits.session || limits.weekly) {
|
|
392
|
+
return limits;
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
function formatResetTime(isoTime) {
|
|
397
|
+
try {
|
|
398
|
+
const resetTime = new Date(isoTime).getTime();
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
const diffMs = resetTime - now;
|
|
401
|
+
if (diffMs <= 0) return "Soon";
|
|
402
|
+
const hours = Math.floor(diffMs / (1e3 * 60 * 60));
|
|
403
|
+
const minutes = Math.floor(diffMs % (1e3 * 60 * 60) / (1e3 * 60));
|
|
404
|
+
if (hours > 24) {
|
|
405
|
+
const days = Math.floor(hours / 24);
|
|
406
|
+
return `${days}d ${hours % 24}h`;
|
|
407
|
+
}
|
|
408
|
+
if (hours > 0) {
|
|
409
|
+
return `${hours}h ${minutes}m`;
|
|
410
|
+
}
|
|
411
|
+
return `${minutes}m`;
|
|
412
|
+
} catch {
|
|
413
|
+
return "Unknown";
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function getUsageLimitsSync() {
|
|
417
|
+
if (cachedLimits && Date.now() - lastFetchTime < CACHE_DURATION) {
|
|
418
|
+
return cachedLimits;
|
|
419
|
+
}
|
|
420
|
+
const fileCache = loadCacheFromFile();
|
|
421
|
+
if (fileCache) {
|
|
422
|
+
cachedLimits = fileCache;
|
|
423
|
+
lastFetchTime = Date.now();
|
|
424
|
+
const age = Date.now() - new Date(fileCache.fetched_at).getTime();
|
|
425
|
+
if (age > 10 * 60 * 1e3 && !isFetching) {
|
|
426
|
+
fetchUsageLimitsBackground();
|
|
427
|
+
}
|
|
428
|
+
return cachedLimits;
|
|
429
|
+
}
|
|
430
|
+
if (!isFetching) {
|
|
431
|
+
fetchUsageLimitsBackground();
|
|
432
|
+
}
|
|
433
|
+
return cachedLimits;
|
|
434
|
+
}
|
|
435
|
+
function fetchUsageLimitsBackground() {
|
|
436
|
+
if (isFetching) return;
|
|
437
|
+
isFetching = true;
|
|
438
|
+
fetchUsageViaOAuthAPI().then((limits) => {
|
|
439
|
+
if (limits) {
|
|
440
|
+
cachedLimits = limits;
|
|
441
|
+
lastFetchTime = Date.now();
|
|
442
|
+
saveCacheToFile(limits);
|
|
443
|
+
console.log("[usage-limits] Fetched via OAuth API successfully");
|
|
444
|
+
} else {
|
|
445
|
+
console.log("[usage-limits] OAuth API failed, no fallback available");
|
|
446
|
+
}
|
|
447
|
+
}).catch((err) => {
|
|
448
|
+
console.log("[usage-limits] Error fetching usage:", err?.message || err);
|
|
449
|
+
}).finally(() => {
|
|
450
|
+
isFetching = false;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
async function fetchUsageLimits() {
|
|
454
|
+
const fileCache = loadCacheFromFile();
|
|
455
|
+
if (fileCache) {
|
|
456
|
+
const age = Date.now() - new Date(fileCache.fetched_at).getTime();
|
|
457
|
+
if (age > 10 * 60 * 1e3 && !isFetching) {
|
|
458
|
+
fetchUsageLimitsBackground();
|
|
459
|
+
}
|
|
460
|
+
return fileCache;
|
|
461
|
+
}
|
|
462
|
+
return fetchUsageViaOAuthAPI();
|
|
463
|
+
}
|
|
464
|
+
async function forceRefreshUsageLimits() {
|
|
465
|
+
cachedLimits = null;
|
|
466
|
+
lastFetchTime = 0;
|
|
467
|
+
try {
|
|
468
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
469
|
+
fs.unlinkSync(CACHE_FILE);
|
|
470
|
+
}
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
console.log("[usage-limits] Force refreshing via OAuth API...");
|
|
474
|
+
const limits = await fetchUsageViaOAuthAPI();
|
|
475
|
+
if (limits) {
|
|
476
|
+
cachedLimits = limits;
|
|
477
|
+
lastFetchTime = Date.now();
|
|
478
|
+
saveCacheToFile(limits);
|
|
479
|
+
console.log("[usage-limits] Force refresh successful");
|
|
480
|
+
} else {
|
|
481
|
+
console.log("[usage-limits] Force refresh failed");
|
|
482
|
+
}
|
|
483
|
+
return limits;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/collector.ts
|
|
487
|
+
var CLAUDE_DIR = path2.join(os3.homedir(), ".claude");
|
|
488
|
+
var STATS_FILE = path2.join(CLAUDE_DIR, "stats-cache.json");
|
|
489
|
+
var PROJECTS_DIR = path2.join(CLAUDE_DIR, "projects");
|
|
490
|
+
var cachedAccountId = null;
|
|
491
|
+
function getAccountId() {
|
|
492
|
+
if (cachedAccountId) return cachedAccountId;
|
|
493
|
+
try {
|
|
494
|
+
const stats = getStats();
|
|
495
|
+
const firstSession = stats.firstSessionDate || "";
|
|
496
|
+
let projectsHash = "";
|
|
497
|
+
if (fs2.existsSync(PROJECTS_DIR)) {
|
|
498
|
+
const projects = fs2.readdirSync(PROJECTS_DIR).sort().join(",");
|
|
499
|
+
projectsHash = projects;
|
|
500
|
+
}
|
|
501
|
+
const combined = `${firstSession}:${projectsHash}`;
|
|
502
|
+
cachedAccountId = crypto2.createHash("md5").update(combined).digest("hex").slice(0, 12);
|
|
503
|
+
return cachedAccountId;
|
|
504
|
+
} catch {
|
|
505
|
+
cachedAccountId = crypto2.randomBytes(6).toString("hex");
|
|
506
|
+
return cachedAccountId;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function getStats() {
|
|
510
|
+
try {
|
|
511
|
+
if (fs2.existsSync(STATS_FILE)) {
|
|
512
|
+
const content = fs2.readFileSync(STATS_FILE, "utf-8");
|
|
513
|
+
return JSON.parse(content);
|
|
514
|
+
}
|
|
515
|
+
} catch (e) {
|
|
516
|
+
}
|
|
517
|
+
return {};
|
|
518
|
+
}
|
|
519
|
+
function parseSessionTokens(sessionFile) {
|
|
520
|
+
const result = {
|
|
521
|
+
input_tokens: 0,
|
|
522
|
+
output_tokens: 0,
|
|
523
|
+
cache_read: 0,
|
|
524
|
+
cache_write: 0,
|
|
525
|
+
model: null,
|
|
526
|
+
messages: 0
|
|
527
|
+
};
|
|
528
|
+
try {
|
|
529
|
+
const content = fs2.readFileSync(sessionFile, "utf-8");
|
|
530
|
+
const lines = content.trim().split("\n");
|
|
531
|
+
for (const line of lines) {
|
|
532
|
+
try {
|
|
533
|
+
const msg = JSON.parse(line);
|
|
534
|
+
if (msg.type === "assistant") {
|
|
535
|
+
result.messages++;
|
|
536
|
+
const usage = msg.message?.usage || {};
|
|
537
|
+
result.input_tokens += usage.input_tokens || 0;
|
|
538
|
+
result.output_tokens += usage.output_tokens || 0;
|
|
539
|
+
result.cache_read += usage.cache_read_input_tokens || 0;
|
|
540
|
+
result.cache_write += usage.cache_creation_input_tokens || 0;
|
|
541
|
+
if (!result.model) {
|
|
542
|
+
result.model = msg.message?.model || null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
return result;
|
|
552
|
+
}
|
|
553
|
+
function parseSessionActivity(sessionFile) {
|
|
554
|
+
const activity = {
|
|
555
|
+
status: "idle",
|
|
556
|
+
current_activity: null,
|
|
557
|
+
last_tool: null,
|
|
558
|
+
last_user_message: null,
|
|
559
|
+
subagent_detail: null,
|
|
560
|
+
waiting_reason: null
|
|
561
|
+
};
|
|
562
|
+
try {
|
|
563
|
+
const stats = fs2.statSync(sessionFile);
|
|
564
|
+
const fd = fs2.openSync(sessionFile, "r");
|
|
565
|
+
const bufferSize = Math.min(8e4, stats.size);
|
|
566
|
+
const buffer = Buffer.alloc(bufferSize);
|
|
567
|
+
const startPos = Math.max(0, stats.size - bufferSize);
|
|
568
|
+
fs2.readSync(fd, buffer, 0, bufferSize, startPos);
|
|
569
|
+
fs2.closeSync(fd);
|
|
570
|
+
const content = buffer.toString("utf-8");
|
|
571
|
+
const lines = content.trim().split("\n");
|
|
572
|
+
const lastMessages = [];
|
|
573
|
+
for (const line of lines.slice(-30)) {
|
|
574
|
+
try {
|
|
575
|
+
lastMessages.push(JSON.parse(line));
|
|
576
|
+
} catch {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (lastMessages.length === 0) return activity;
|
|
581
|
+
const lastMsg = lastMessages[lastMessages.length - 1];
|
|
582
|
+
const lastType = lastMsg.type;
|
|
583
|
+
if (lastType === "assistant") {
|
|
584
|
+
const content2 = lastMsg.message?.content || [];
|
|
585
|
+
let hasToolUse = false;
|
|
586
|
+
let lastText = "";
|
|
587
|
+
if (Array.isArray(content2)) {
|
|
588
|
+
for (const block of content2) {
|
|
589
|
+
if (block?.type === "tool_use") hasToolUse = true;
|
|
590
|
+
if (block?.type === "text") lastText = block.text || "";
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (!hasToolUse && lastText) {
|
|
594
|
+
activity.status = "waiting_input";
|
|
595
|
+
const textLines = lastText.trim().split("\n");
|
|
596
|
+
const lastLine = textLines[textLines.length - 1] || "";
|
|
597
|
+
if (lastText.slice(-300).includes("?")) {
|
|
598
|
+
activity.waiting_reason = "Asking a question";
|
|
599
|
+
for (let i = textLines.length - 1; i >= 0; i--) {
|
|
600
|
+
if (textLines[i].includes("?")) {
|
|
601
|
+
const q = textLines[i].slice(0, 80);
|
|
602
|
+
activity.current_activity = `\u2753 ${q}${textLines[i].length > 80 ? "..." : ""}`;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
activity.waiting_reason = "Waiting for your response";
|
|
608
|
+
activity.current_activity = `\u{1F4AC} ${lastLine.slice(0, 60)}${lastLine.length > 60 ? "..." : ""}`;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
for (let i = lastMessages.length - 1; i >= 0; i--) {
|
|
613
|
+
const msg = lastMessages[i];
|
|
614
|
+
const msgType = msg.type;
|
|
615
|
+
if (msgType === "user" && !activity.last_user_message) {
|
|
616
|
+
const content2 = msg.message?.content;
|
|
617
|
+
if (typeof content2 === "string") {
|
|
618
|
+
activity.last_user_message = content2.slice(0, 100) + (content2.length > 100 ? "..." : "");
|
|
619
|
+
} else if (Array.isArray(content2)) {
|
|
620
|
+
for (const c of content2) {
|
|
621
|
+
if (c?.type === "text") {
|
|
622
|
+
const text = (c.text || "").slice(0, 100);
|
|
623
|
+
activity.last_user_message = text + (text.length >= 100 ? "..." : "");
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (msgType === "assistant" && activity.status !== "waiting_input") {
|
|
630
|
+
const content2 = msg.message?.content || [];
|
|
631
|
+
if (Array.isArray(content2)) {
|
|
632
|
+
for (let j = content2.length - 1; j >= 0; j--) {
|
|
633
|
+
const block = content2[j];
|
|
634
|
+
if (block?.type === "tool_use") {
|
|
635
|
+
const toolName = block.name || "unknown";
|
|
636
|
+
activity.last_tool = toolName;
|
|
637
|
+
activity.status = "working";
|
|
638
|
+
const inp = block.input || {};
|
|
639
|
+
if (toolName === "TodoWrite") {
|
|
640
|
+
const todos = inp.todos || [];
|
|
641
|
+
const inProgress = todos.filter((t) => t.status === "in_progress");
|
|
642
|
+
if (inProgress.length > 0) {
|
|
643
|
+
activity.current_activity = `\u{1F4CB} ${(inProgress[0].content || "Working on task").slice(0, 50)}`;
|
|
644
|
+
} else {
|
|
645
|
+
activity.current_activity = "\u{1F4CB} Updating task list";
|
|
646
|
+
}
|
|
647
|
+
} else if (toolName === "Read") {
|
|
648
|
+
const file = (inp.file_path || "").split("/").pop();
|
|
649
|
+
activity.current_activity = `\u{1F4D6} Reading ${file}`;
|
|
650
|
+
} else if (toolName === "Write") {
|
|
651
|
+
const file = (inp.file_path || "").split("/").pop();
|
|
652
|
+
activity.current_activity = `\u270F\uFE0F Writing ${file}`;
|
|
653
|
+
} else if (toolName === "Edit") {
|
|
654
|
+
const file = (inp.file_path || "").split("/").pop();
|
|
655
|
+
activity.current_activity = `\u270F\uFE0F Editing ${file}`;
|
|
656
|
+
} else if (toolName === "Bash") {
|
|
657
|
+
const cmd = inp.command || "";
|
|
658
|
+
const desc = inp.description || "";
|
|
659
|
+
if (desc) {
|
|
660
|
+
activity.current_activity = `\u26A1 ${desc.slice(0, 50)}`;
|
|
661
|
+
} else {
|
|
662
|
+
activity.current_activity = `\u26A1 $ ${cmd.slice(0, 45)}`;
|
|
663
|
+
}
|
|
664
|
+
} else if (toolName === "Task") {
|
|
665
|
+
const desc = inp.description || "task";
|
|
666
|
+
const agentType = inp.subagent_type || "";
|
|
667
|
+
const prompt = inp.prompt || "";
|
|
668
|
+
if (agentType) {
|
|
669
|
+
activity.current_activity = `\u{1F916} ${agentType} agent: ${desc}`;
|
|
670
|
+
} else {
|
|
671
|
+
activity.current_activity = `\u{1F916} Subagent: ${desc}`;
|
|
672
|
+
}
|
|
673
|
+
if (prompt) {
|
|
674
|
+
activity.subagent_detail = prompt.slice(0, 120) + (prompt.length > 120 ? "..." : "");
|
|
675
|
+
}
|
|
676
|
+
} else if (toolName === "Grep") {
|
|
677
|
+
const pattern = inp.pattern || "";
|
|
678
|
+
activity.current_activity = `\u{1F50D} Searching: ${pattern.slice(0, 40)}`;
|
|
679
|
+
} else if (toolName === "Glob") {
|
|
680
|
+
const pattern = inp.pattern || "";
|
|
681
|
+
activity.current_activity = `\u{1F4C1} Finding: ${pattern.slice(0, 40)}`;
|
|
682
|
+
} else if (toolName === "AskUserQuestion") {
|
|
683
|
+
const questions = inp.questions || [];
|
|
684
|
+
if (questions.length > 0) {
|
|
685
|
+
const q = (questions[0].question || "").slice(0, 60);
|
|
686
|
+
activity.current_activity = `\u2753 ${q}`;
|
|
687
|
+
} else {
|
|
688
|
+
activity.current_activity = "\u2753 Asking question";
|
|
689
|
+
}
|
|
690
|
+
activity.status = "waiting_input";
|
|
691
|
+
activity.waiting_reason = "Needs your input";
|
|
692
|
+
} else if (["WebSearch", "WebFetch", "mcp__web-search-prime__webSearchPrime"].includes(toolName)) {
|
|
693
|
+
const query = (inp.query || inp.search_query || "").slice(0, 40);
|
|
694
|
+
activity.current_activity = `\u{1F310} Searching: ${query}`;
|
|
695
|
+
} else if (toolName.startsWith("mcp__")) {
|
|
696
|
+
const shortName = toolName.split("__").pop();
|
|
697
|
+
activity.current_activity = `\u{1F50C} ${shortName}`;
|
|
698
|
+
} else {
|
|
699
|
+
activity.current_activity = `\u{1F527} ${toolName}`;
|
|
700
|
+
}
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (activity.current_activity) break;
|
|
707
|
+
}
|
|
708
|
+
} catch {
|
|
709
|
+
}
|
|
710
|
+
return activity;
|
|
711
|
+
}
|
|
712
|
+
function getCliProcesses() {
|
|
713
|
+
try {
|
|
714
|
+
const result = (0, import_child_process2.execSync)("ps aux", { encoding: "utf-8", timeout: 5e3 });
|
|
715
|
+
const processes = [];
|
|
716
|
+
for (const line of result.trim().split("\n").slice(1)) {
|
|
717
|
+
const parts = line.split(/\s+/);
|
|
718
|
+
if (parts.length < 11) continue;
|
|
719
|
+
const cmd = parts.slice(10).join(" ");
|
|
720
|
+
if (cmd.startsWith("claude") && !cmd.includes("Claude.app")) {
|
|
721
|
+
const pid = parseInt(parts[1], 10);
|
|
722
|
+
const cpu = parseFloat(parts[2]);
|
|
723
|
+
const mem = parseFloat(parts[3]);
|
|
724
|
+
let cwd = null;
|
|
725
|
+
try {
|
|
726
|
+
const lsof = (0, import_child_process2.execSync)(`lsof -p ${pid} -Fn`, { encoding: "utf-8", timeout: 2e3 });
|
|
727
|
+
for (const lline of lsof.split("\n")) {
|
|
728
|
+
if (lline.startsWith("n") && lline.includes("projects") && lline.includes(".jsonl")) {
|
|
729
|
+
const filePath = lline.slice(1);
|
|
730
|
+
const match = filePath.split("/projects/")[1];
|
|
731
|
+
if (match) {
|
|
732
|
+
cwd = match.split("/")[0].replace(/-/g, "/");
|
|
733
|
+
}
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
processes.push({ pid, cpu, mem, cwd });
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return processes;
|
|
743
|
+
} catch {
|
|
744
|
+
return [];
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function getOpenFiles() {
|
|
748
|
+
const openFiles = /* @__PURE__ */ new Set();
|
|
749
|
+
try {
|
|
750
|
+
const result = (0, import_child_process2.execSync)("lsof -c claude", { encoding: "utf-8", timeout: 5e3 });
|
|
751
|
+
for (const line of result.split("\n")) {
|
|
752
|
+
if (line.includes(".jsonl") && line.includes("projects")) {
|
|
753
|
+
const parts = line.split(/\s+/);
|
|
754
|
+
for (const part of parts) {
|
|
755
|
+
if (part.includes(".jsonl")) {
|
|
756
|
+
openFiles.add(part);
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
} catch {
|
|
763
|
+
}
|
|
764
|
+
return openFiles;
|
|
765
|
+
}
|
|
766
|
+
function getTodayUsageFromSessions() {
|
|
767
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
768
|
+
const todayStart = /* @__PURE__ */ new Date();
|
|
769
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
770
|
+
const todayStartTs = todayStart.getTime();
|
|
771
|
+
const modelTokens = {};
|
|
772
|
+
let messages = 0;
|
|
773
|
+
let sessions = 0;
|
|
774
|
+
if (!fs2.existsSync(PROJECTS_DIR)) {
|
|
775
|
+
return {
|
|
776
|
+
date: today,
|
|
777
|
+
tokens: 0,
|
|
778
|
+
cost: 0,
|
|
779
|
+
messages: 0,
|
|
780
|
+
sessions: 0,
|
|
781
|
+
models: [],
|
|
782
|
+
model_breakdown: {}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
const projectDirs = fs2.readdirSync(PROJECTS_DIR);
|
|
786
|
+
for (const projectDir of projectDirs) {
|
|
787
|
+
const projectPath = path2.join(PROJECTS_DIR, projectDir);
|
|
788
|
+
if (!fs2.statSync(projectPath).isDirectory()) continue;
|
|
789
|
+
const sessionFiles = fs2.readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
|
|
790
|
+
for (const sessionFile of sessionFiles) {
|
|
791
|
+
const sessionPath = path2.join(projectPath, sessionFile);
|
|
792
|
+
const stat = fs2.statSync(sessionPath);
|
|
793
|
+
if (stat.mtimeMs < todayStartTs) continue;
|
|
794
|
+
sessions++;
|
|
795
|
+
try {
|
|
796
|
+
const content = fs2.readFileSync(sessionPath, "utf-8");
|
|
797
|
+
for (const line of content.trim().split("\n")) {
|
|
798
|
+
try {
|
|
799
|
+
const msg = JSON.parse(line);
|
|
800
|
+
const ts = msg.timestamp || "";
|
|
801
|
+
if (!ts.startsWith(today)) continue;
|
|
802
|
+
if (msg.type === "assistant") {
|
|
803
|
+
messages++;
|
|
804
|
+
const usage = msg.message?.usage || {};
|
|
805
|
+
const model = msg.message?.model || "unknown";
|
|
806
|
+
if (!modelTokens[model]) {
|
|
807
|
+
modelTokens[model] = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
|
|
808
|
+
}
|
|
809
|
+
modelTokens[model].input += usage.input_tokens || 0;
|
|
810
|
+
modelTokens[model].output += usage.output_tokens || 0;
|
|
811
|
+
modelTokens[model].cache_read += usage.cache_read_input_tokens || 0;
|
|
812
|
+
modelTokens[model].cache_write += usage.cache_creation_input_tokens || 0;
|
|
813
|
+
}
|
|
814
|
+
} catch {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
} catch {
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
let allTokens = 0;
|
|
824
|
+
let totalCost = 0;
|
|
825
|
+
const modelBreakdown = {};
|
|
826
|
+
for (const [model, tokens] of Object.entries(modelTokens)) {
|
|
827
|
+
const modelTotal = tokens.input + tokens.output + tokens.cache_read + tokens.cache_write;
|
|
828
|
+
allTokens += modelTotal;
|
|
829
|
+
const pricing = import_shared.MODEL_PRICING[model] || import_shared.DEFAULT_PRICING;
|
|
830
|
+
const cost = tokens.input / 1e6 * pricing.input + tokens.output / 1e6 * pricing.output + tokens.cache_read / 1e6 * pricing.cache_read + tokens.cache_write / 1e6 * pricing.cache_write;
|
|
831
|
+
totalCost += cost;
|
|
832
|
+
modelBreakdown[model] = {
|
|
833
|
+
inputTokens: tokens.input,
|
|
834
|
+
outputTokens: tokens.output,
|
|
835
|
+
cacheReadInputTokens: tokens.cache_read,
|
|
836
|
+
cacheCreationInputTokens: tokens.cache_write,
|
|
837
|
+
cost: Math.round(cost * 1e4) / 1e4
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
date: today,
|
|
842
|
+
tokens: allTokens,
|
|
843
|
+
cost: Math.round(totalCost * 1e4) / 1e4,
|
|
844
|
+
messages,
|
|
845
|
+
sessions,
|
|
846
|
+
models: Object.keys(modelTokens),
|
|
847
|
+
model_breakdown: modelBreakdown
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function getDailyUsage(stats) {
|
|
851
|
+
const dailyTokens = stats.dailyModelTokens || [];
|
|
852
|
+
const dailyActivity = {};
|
|
853
|
+
for (const d of stats.dailyActivity || []) {
|
|
854
|
+
dailyActivity[d.date] = d;
|
|
855
|
+
}
|
|
856
|
+
const result = [];
|
|
857
|
+
for (const day of dailyTokens.slice(-14)) {
|
|
858
|
+
const date = day.date;
|
|
859
|
+
const tokensByModel = day.tokensByModel || {};
|
|
860
|
+
const totalTokens = Object.values(tokensByModel).reduce((sum, t) => sum + t, 0);
|
|
861
|
+
let dayCost = 0;
|
|
862
|
+
for (const [model, tokens] of Object.entries(tokensByModel)) {
|
|
863
|
+
const pricing = import_shared.MODEL_PRICING[model] || import_shared.DEFAULT_PRICING;
|
|
864
|
+
dayCost += tokens / 1e6 * pricing.input;
|
|
865
|
+
}
|
|
866
|
+
const activity = dailyActivity[date] || { messageCount: 0, sessionCount: 0 };
|
|
867
|
+
result.push({
|
|
868
|
+
date,
|
|
869
|
+
tokens: totalTokens,
|
|
870
|
+
cost: Math.round(dayCost * 1e4) / 1e4,
|
|
871
|
+
messages: activity.messageCount,
|
|
872
|
+
sessions: activity.sessionCount,
|
|
873
|
+
models: Object.keys(tokensByModel)
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
return result;
|
|
877
|
+
}
|
|
878
|
+
function getSessions() {
|
|
879
|
+
const active = [];
|
|
880
|
+
const recent = [];
|
|
881
|
+
if (!fs2.existsSync(PROJECTS_DIR)) {
|
|
882
|
+
return { active, recent };
|
|
883
|
+
}
|
|
884
|
+
const openFiles = getOpenFiles();
|
|
885
|
+
const now = Date.now();
|
|
886
|
+
const projectDirs = fs2.readdirSync(PROJECTS_DIR);
|
|
887
|
+
for (const projectDir of projectDirs) {
|
|
888
|
+
const projectPath = path2.join(PROJECTS_DIR, projectDir);
|
|
889
|
+
if (!fs2.statSync(projectPath).isDirectory()) continue;
|
|
890
|
+
const sessionFiles = fs2.readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
|
|
891
|
+
for (const sessionFile of sessionFiles) {
|
|
892
|
+
const sessionPath = path2.join(projectPath, sessionFile);
|
|
893
|
+
const stat = fs2.statSync(sessionPath);
|
|
894
|
+
const isActive = openFiles.has(sessionPath) || now - stat.mtimeMs < 3e4;
|
|
895
|
+
const projectName = projectDir.replace(/-/g, "/");
|
|
896
|
+
const projectShort = projectName.includes("/") ? projectName.split("/").pop() : projectName;
|
|
897
|
+
let activity = {
|
|
898
|
+
status: "idle",
|
|
899
|
+
current_activity: null,
|
|
900
|
+
last_tool: null,
|
|
901
|
+
last_user_message: null,
|
|
902
|
+
subagent_detail: null,
|
|
903
|
+
waiting_reason: null
|
|
904
|
+
};
|
|
905
|
+
let tokenData = null;
|
|
906
|
+
let sessionCost = null;
|
|
907
|
+
if (isActive) {
|
|
908
|
+
activity = parseSessionActivity(sessionPath);
|
|
909
|
+
tokenData = parseSessionTokens(sessionPath);
|
|
910
|
+
sessionCost = (0, import_shared.calculateCost)(tokenData.model || "claude-opus-4-5-20251101", {
|
|
911
|
+
input_tokens: tokenData.input_tokens,
|
|
912
|
+
output_tokens: tokenData.output_tokens,
|
|
913
|
+
cache_read_input_tokens: tokenData.cache_read,
|
|
914
|
+
cache_creation_input_tokens: tokenData.cache_write
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
const session = {
|
|
918
|
+
session_id: path2.basename(sessionFile, ".jsonl"),
|
|
919
|
+
project: projectName,
|
|
920
|
+
project_short: projectShort,
|
|
921
|
+
last_modified: new Date(stat.mtimeMs).toLocaleTimeString("en-US", { hour12: false }),
|
|
922
|
+
size_kb: Math.round(stat.size / 1024 * 10) / 10,
|
|
923
|
+
is_active: isActive,
|
|
924
|
+
tokens: tokenData,
|
|
925
|
+
cost: sessionCost,
|
|
926
|
+
model: tokenData?.model || null,
|
|
927
|
+
...activity
|
|
928
|
+
};
|
|
929
|
+
if (isActive) {
|
|
930
|
+
active.push(session);
|
|
931
|
+
} else if (now - stat.mtimeMs < 864e5) {
|
|
932
|
+
recent.push(session);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
active.sort((a, b) => b.last_modified.localeCompare(a.last_modified));
|
|
937
|
+
recent.sort((a, b) => b.last_modified.localeCompare(a.last_modified));
|
|
938
|
+
return { active, recent: recent.slice(0, 10) };
|
|
939
|
+
}
|
|
940
|
+
function collectData() {
|
|
941
|
+
const stats = getStats();
|
|
942
|
+
const processes = getCliProcesses();
|
|
943
|
+
const { active: activeSessions, recent: recentSessions } = getSessions();
|
|
944
|
+
const todayUsage = getTodayUsageFromSessions();
|
|
945
|
+
const modelUsage = { ...stats.modelUsage };
|
|
946
|
+
const todayBreakdown = todayUsage.model_breakdown;
|
|
947
|
+
for (const [model, todayData] of Object.entries(todayBreakdown)) {
|
|
948
|
+
if (modelUsage[model]) {
|
|
949
|
+
modelUsage[model] = {
|
|
950
|
+
inputTokens: (modelUsage[model].inputTokens || 0) + (todayData.inputTokens || 0),
|
|
951
|
+
outputTokens: (modelUsage[model].outputTokens || 0) + (todayData.outputTokens || 0),
|
|
952
|
+
cacheReadInputTokens: (modelUsage[model].cacheReadInputTokens || 0) + (todayData.cacheReadInputTokens || 0),
|
|
953
|
+
cacheCreationInputTokens: (modelUsage[model].cacheCreationInputTokens || 0) + (todayData.cacheCreationInputTokens || 0)
|
|
954
|
+
};
|
|
955
|
+
} else {
|
|
956
|
+
modelUsage[model] = {
|
|
957
|
+
inputTokens: todayData.inputTokens,
|
|
958
|
+
outputTokens: todayData.outputTokens,
|
|
959
|
+
cacheReadInputTokens: todayData.cacheReadInputTokens,
|
|
960
|
+
cacheCreationInputTokens: todayData.cacheCreationInputTokens
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const modelCosts = {};
|
|
965
|
+
let totalCost = 0;
|
|
966
|
+
let totalTokens = 0;
|
|
967
|
+
for (const [model, usage] of Object.entries(modelUsage)) {
|
|
968
|
+
const cost = (0, import_shared.calculateCost)(model, usage);
|
|
969
|
+
modelCosts[model] = cost;
|
|
970
|
+
totalCost += cost;
|
|
971
|
+
totalTokens += (usage.inputTokens || 0) + (usage.outputTokens || 0) + (usage.cacheReadInputTokens || 0) + (usage.cacheCreationInputTokens || 0);
|
|
972
|
+
}
|
|
973
|
+
const dailyUsage = getDailyUsage(stats);
|
|
974
|
+
if (todayUsage.tokens > 0) {
|
|
975
|
+
dailyUsage.push({
|
|
976
|
+
date: todayUsage.date,
|
|
977
|
+
tokens: todayUsage.tokens,
|
|
978
|
+
cost: todayUsage.cost,
|
|
979
|
+
messages: todayUsage.messages,
|
|
980
|
+
sessions: todayUsage.sessions,
|
|
981
|
+
models: todayUsage.models
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
const activeSessionsCost = activeSessions.reduce((sum, s) => sum + (s.cost || 0), 0);
|
|
985
|
+
const activeSessionsTokens = activeSessions.reduce((sum, s) => {
|
|
986
|
+
if (!s.tokens) return sum;
|
|
987
|
+
return sum + s.tokens.input_tokens + s.tokens.output_tokens + s.tokens.cache_read + s.tokens.cache_write;
|
|
988
|
+
}, 0);
|
|
989
|
+
const totalSessionsCount = (stats.totalSessions || 0) + todayUsage.sessions;
|
|
990
|
+
const totalMessagesCount = (stats.totalMessages || 0) + todayUsage.messages;
|
|
991
|
+
const avgCostPerMessage = totalMessagesCount > 0 ? totalCost / totalMessagesCount : 0;
|
|
992
|
+
const avgCostPerSession = totalSessionsCount > 0 ? totalCost / totalSessionsCount : 0;
|
|
993
|
+
let daysActive = dailyUsage.length || 1;
|
|
994
|
+
const firstSessionDate = stats.firstSessionDate;
|
|
995
|
+
if (firstSessionDate) {
|
|
996
|
+
try {
|
|
997
|
+
const firstDt = new Date(firstSessionDate);
|
|
998
|
+
daysActive = Math.max(1, Math.floor((Date.now() - firstDt.getTime()) / 864e5) + 1);
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const dailyAvgCost = daysActive > 0 ? totalCost / daysActive : 0;
|
|
1003
|
+
const last7Days = dailyUsage.slice(-7);
|
|
1004
|
+
let weeklyCost = last7Days.reduce((sum, d) => sum + (d.cost || 0), 0);
|
|
1005
|
+
weeklyCost += todayUsage.cost;
|
|
1006
|
+
const currentHour = (/* @__PURE__ */ new Date()).getHours();
|
|
1007
|
+
const hoursElapsed = Math.max(currentHour, 1);
|
|
1008
|
+
const todayHourlyRate = hoursElapsed > 0 ? todayUsage.cost / hoursElapsed : 0;
|
|
1009
|
+
const hourCounts = stats.hourCounts || {};
|
|
1010
|
+
const summary = {
|
|
1011
|
+
is_running: activeSessions.length > 0,
|
|
1012
|
+
active_count: activeSessions.length,
|
|
1013
|
+
waiting_input: activeSessions.filter((s) => s.status === "waiting_input").length,
|
|
1014
|
+
total_sessions: totalSessionsCount,
|
|
1015
|
+
total_messages: totalMessagesCount,
|
|
1016
|
+
total_cost: Math.round(totalCost * 100) / 100,
|
|
1017
|
+
total_tokens: totalTokens,
|
|
1018
|
+
active_sessions_cost: Math.round(activeSessionsCost * 1e4) / 1e4,
|
|
1019
|
+
active_sessions_tokens: activeSessionsTokens,
|
|
1020
|
+
first_session: firstSessionDate || null,
|
|
1021
|
+
days_active: daysActive,
|
|
1022
|
+
avg_cost_per_message: Math.round(avgCostPerMessage * 1e4) / 1e4,
|
|
1023
|
+
avg_cost_per_session: Math.round(avgCostPerSession * 100) / 100,
|
|
1024
|
+
daily_avg_cost: Math.round(dailyAvgCost * 100) / 100,
|
|
1025
|
+
projected_monthly: Math.round(dailyAvgCost * 30 * 100) / 100,
|
|
1026
|
+
weekly_cost: Math.round(weeklyCost * 100) / 100,
|
|
1027
|
+
today_hourly_rate: Math.round(todayHourlyRate * 1e4) / 1e4,
|
|
1028
|
+
est_weekly: Math.round(dailyAvgCost * 7 * 100) / 100
|
|
1029
|
+
};
|
|
1030
|
+
return {
|
|
1031
|
+
stats,
|
|
1032
|
+
model_usage: modelUsage,
|
|
1033
|
+
processes,
|
|
1034
|
+
active_sessions: activeSessions,
|
|
1035
|
+
recent_sessions: recentSessions,
|
|
1036
|
+
model_costs: modelCosts,
|
|
1037
|
+
daily_usage: dailyUsage,
|
|
1038
|
+
today_usage: todayUsage,
|
|
1039
|
+
hour_counts: hourCounts,
|
|
1040
|
+
summary,
|
|
1041
|
+
usage_limits: getUsageLimitsSync()
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/index.ts
|
|
1046
|
+
var DEFAULT_SERVER = "https://cm.cban.top";
|
|
1047
|
+
var program = new import_commander.Command();
|
|
1048
|
+
program.name("claude-stats").description("Monitor and stream Claude Code usage statistics").version("0.1.0");
|
|
1049
|
+
function promptForInput(question, hidden = false) {
|
|
1050
|
+
return new Promise((resolve) => {
|
|
1051
|
+
const rl = readline.createInterface({
|
|
1052
|
+
input: process.stdin,
|
|
1053
|
+
output: process.stdout
|
|
1054
|
+
});
|
|
1055
|
+
if (hidden && process.stdin.isTTY) {
|
|
1056
|
+
process.stdout.write(question);
|
|
1057
|
+
const stdin = process.stdin;
|
|
1058
|
+
stdin.setRawMode(true);
|
|
1059
|
+
stdin.resume();
|
|
1060
|
+
stdin.setEncoding("utf8");
|
|
1061
|
+
let input = "";
|
|
1062
|
+
const onData = (char) => {
|
|
1063
|
+
if (char === "\n" || char === "\r" || char === "") {
|
|
1064
|
+
stdin.setRawMode(false);
|
|
1065
|
+
stdin.removeListener("data", onData);
|
|
1066
|
+
rl.close();
|
|
1067
|
+
console.log("");
|
|
1068
|
+
resolve(input);
|
|
1069
|
+
} else if (char === "") {
|
|
1070
|
+
process.exit();
|
|
1071
|
+
} else if (char === "\x7F" || char === "\b") {
|
|
1072
|
+
input = input.slice(0, -1);
|
|
1073
|
+
} else {
|
|
1074
|
+
input += char;
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
stdin.on("data", onData);
|
|
1078
|
+
} else {
|
|
1079
|
+
rl.question(question, (answer) => {
|
|
1080
|
+
rl.close();
|
|
1081
|
+
resolve(answer);
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
program.command("start", { isDefault: true }).description("Start streaming usage data to the monitor server").option("-s, --server <url>", "Server URL", process.env.MONITOR_SERVER || DEFAULT_SERVER).option("-t, --token <token>", "Authentication token", process.env.MONITOR_TOKEN).option("-n, --name <name>", "Machine name (defaults to hostname)", process.env.MONITOR_NAME).option("-i, --interval <ms>", "Polling interval in milliseconds", "10000").action(async (options) => {
|
|
1087
|
+
let server = options.server;
|
|
1088
|
+
let token = options.token;
|
|
1089
|
+
if (server && !server.startsWith("http://") && !server.startsWith("https://")) {
|
|
1090
|
+
server = `https://${server}`;
|
|
1091
|
+
}
|
|
1092
|
+
if (!token) {
|
|
1093
|
+
console.log("Claude Stats Monitor");
|
|
1094
|
+
console.log("====================");
|
|
1095
|
+
console.log(`Server: ${server}`);
|
|
1096
|
+
console.log("");
|
|
1097
|
+
token = await promptForInput("Enter your monitor token: ", true);
|
|
1098
|
+
if (!token) {
|
|
1099
|
+
console.error("Error: Token is required");
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
console.log("Claude Stats Monitor");
|
|
1104
|
+
console.log("====================");
|
|
1105
|
+
fetchUsageLimits().then((limits) => {
|
|
1106
|
+
if (limits) {
|
|
1107
|
+
console.log(`Usage limits loaded (Session: ${limits.session?.percent_used ?? "?"}% used)`);
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
const streamer = new Streamer({
|
|
1111
|
+
server,
|
|
1112
|
+
token,
|
|
1113
|
+
name: options.name,
|
|
1114
|
+
interval: parseInt(options.interval, 10),
|
|
1115
|
+
getAccountId
|
|
1116
|
+
});
|
|
1117
|
+
streamer.start(collectData);
|
|
1118
|
+
});
|
|
1119
|
+
program.command("test").description("Collect and display current data without streaming").action(() => {
|
|
1120
|
+
console.log("Collecting data...\n");
|
|
1121
|
+
const data = collectData();
|
|
1122
|
+
console.log("Summary:");
|
|
1123
|
+
console.log(` Active sessions: ${data.summary.active_count}`);
|
|
1124
|
+
console.log(` Waiting input: ${data.summary.waiting_input}`);
|
|
1125
|
+
console.log(` Total sessions: ${data.summary.total_sessions}`);
|
|
1126
|
+
console.log(` Total cost: $${data.summary.total_cost}`);
|
|
1127
|
+
console.log(` Today's cost: $${data.today_usage.cost}`);
|
|
1128
|
+
console.log("");
|
|
1129
|
+
if (data.active_sessions.length > 0) {
|
|
1130
|
+
console.log("Active Sessions:");
|
|
1131
|
+
for (const session of data.active_sessions) {
|
|
1132
|
+
console.log(` - ${session.project_short} (${session.status})`);
|
|
1133
|
+
if (session.current_activity) {
|
|
1134
|
+
console.log(` ${session.current_activity}`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
console.log("");
|
|
1138
|
+
}
|
|
1139
|
+
console.log("Model Usage:");
|
|
1140
|
+
for (const [model, cost] of Object.entries(data.model_costs)) {
|
|
1141
|
+
const shortModel = model.replace("claude-", "").replace("-20251101", "").replace("-20250929", "");
|
|
1142
|
+
console.log(` - ${shortModel}: $${cost.toFixed(4)}`);
|
|
1143
|
+
}
|
|
1144
|
+
if (data.usage_limits) {
|
|
1145
|
+
console.log("\nUsage Limits:");
|
|
1146
|
+
if (data.usage_limits.session) {
|
|
1147
|
+
console.log(` Session: ${data.usage_limits.session.percent_used}% used, resets in ${data.usage_limits.session.reset_description}`);
|
|
1148
|
+
}
|
|
1149
|
+
if (data.usage_limits.weekly) {
|
|
1150
|
+
console.log(` Weekly: ${data.usage_limits.weekly.percent_used}% used, resets in ${data.usage_limits.weekly.reset_description}`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
program.command("refresh-usage").description("Force refresh usage limits from Claude API").action(async () => {
|
|
1155
|
+
console.log("Refreshing usage limits...");
|
|
1156
|
+
const limits = await forceRefreshUsageLimits();
|
|
1157
|
+
if (limits) {
|
|
1158
|
+
console.log("Usage limits refreshed:");
|
|
1159
|
+
if (limits.session) {
|
|
1160
|
+
console.log(` Session: ${limits.session.percent_used}% used, resets in ${limits.session.reset_description}`);
|
|
1161
|
+
}
|
|
1162
|
+
if (limits.weekly) {
|
|
1163
|
+
console.log(` Weekly: ${limits.weekly.percent_used}% used, resets in ${limits.weekly.reset_description}`);
|
|
1164
|
+
}
|
|
1165
|
+
} else {
|
|
1166
|
+
console.log("Failed to refresh usage limits. Make sure you are logged into Claude.");
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
program.parse();
|