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,676 @@
|
|
|
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.getAccountId = getAccountId;
|
|
37
|
+
exports.collectData = collectData;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const crypto = __importStar(require("crypto"));
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const shared_1 = require("@claude-monitor/shared");
|
|
44
|
+
const usage_limits_1 = require("./usage-limits");
|
|
45
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
46
|
+
const STATS_FILE = path.join(CLAUDE_DIR, 'stats-cache.json');
|
|
47
|
+
const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|
48
|
+
// Generate a unique account ID based on Claude account data
|
|
49
|
+
// This will be the same across machines using the same Claude account
|
|
50
|
+
let cachedAccountId = null;
|
|
51
|
+
function getAccountId() {
|
|
52
|
+
if (cachedAccountId)
|
|
53
|
+
return cachedAccountId;
|
|
54
|
+
try {
|
|
55
|
+
const stats = getStats();
|
|
56
|
+
// Use firstSessionDate as the primary identifier - it's unique per account
|
|
57
|
+
// and won't change over time
|
|
58
|
+
const firstSession = stats.firstSessionDate || '';
|
|
59
|
+
// Also include a hash of the projects directory structure for extra uniqueness
|
|
60
|
+
let projectsHash = '';
|
|
61
|
+
if (fs.existsSync(PROJECTS_DIR)) {
|
|
62
|
+
const projects = fs.readdirSync(PROJECTS_DIR).sort().join(',');
|
|
63
|
+
projectsHash = projects;
|
|
64
|
+
}
|
|
65
|
+
const combined = `${firstSession}:${projectsHash}`;
|
|
66
|
+
cachedAccountId = crypto.createHash('md5').update(combined).digest('hex').slice(0, 12);
|
|
67
|
+
return cachedAccountId;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Fallback to a random ID if we can't generate one
|
|
71
|
+
cachedAccountId = crypto.randomBytes(6).toString('hex');
|
|
72
|
+
return cachedAccountId;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function getStats() {
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(STATS_FILE)) {
|
|
78
|
+
const content = fs.readFileSync(STATS_FILE, 'utf-8');
|
|
79
|
+
return JSON.parse(content);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
// Ignore errors
|
|
84
|
+
}
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
function parseSessionTokens(sessionFile) {
|
|
88
|
+
const result = {
|
|
89
|
+
input_tokens: 0,
|
|
90
|
+
output_tokens: 0,
|
|
91
|
+
cache_read: 0,
|
|
92
|
+
cache_write: 0,
|
|
93
|
+
model: null,
|
|
94
|
+
messages: 0,
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
const content = fs.readFileSync(sessionFile, 'utf-8');
|
|
98
|
+
const lines = content.trim().split('\n');
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
try {
|
|
101
|
+
const msg = JSON.parse(line);
|
|
102
|
+
if (msg.type === 'assistant') {
|
|
103
|
+
result.messages++;
|
|
104
|
+
const usage = msg.message?.usage || {};
|
|
105
|
+
result.input_tokens += usage.input_tokens || 0;
|
|
106
|
+
result.output_tokens += usage.output_tokens || 0;
|
|
107
|
+
result.cache_read += usage.cache_read_input_tokens || 0;
|
|
108
|
+
result.cache_write += usage.cache_creation_input_tokens || 0;
|
|
109
|
+
if (!result.model) {
|
|
110
|
+
result.model = msg.message?.model || null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Ignore errors
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
function parseSessionActivity(sessionFile) {
|
|
125
|
+
const activity = {
|
|
126
|
+
status: 'idle',
|
|
127
|
+
current_activity: null,
|
|
128
|
+
last_tool: null,
|
|
129
|
+
last_user_message: null,
|
|
130
|
+
subagent_detail: null,
|
|
131
|
+
waiting_reason: null,
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
const stats = fs.statSync(sessionFile);
|
|
135
|
+
const fd = fs.openSync(sessionFile, 'r');
|
|
136
|
+
const bufferSize = Math.min(80000, stats.size);
|
|
137
|
+
const buffer = Buffer.alloc(bufferSize);
|
|
138
|
+
const startPos = Math.max(0, stats.size - bufferSize);
|
|
139
|
+
fs.readSync(fd, buffer, 0, bufferSize, startPos);
|
|
140
|
+
fs.closeSync(fd);
|
|
141
|
+
const content = buffer.toString('utf-8');
|
|
142
|
+
const lines = content.trim().split('\n');
|
|
143
|
+
const lastMessages = [];
|
|
144
|
+
for (const line of lines.slice(-30)) {
|
|
145
|
+
try {
|
|
146
|
+
lastMessages.push(JSON.parse(line));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (lastMessages.length === 0)
|
|
153
|
+
return activity;
|
|
154
|
+
const lastMsg = lastMessages[lastMessages.length - 1];
|
|
155
|
+
const lastType = lastMsg.type;
|
|
156
|
+
// Check if waiting for user input
|
|
157
|
+
if (lastType === 'assistant') {
|
|
158
|
+
const content = lastMsg.message?.content || [];
|
|
159
|
+
let hasToolUse = false;
|
|
160
|
+
let lastText = '';
|
|
161
|
+
if (Array.isArray(content)) {
|
|
162
|
+
for (const block of content) {
|
|
163
|
+
if (block?.type === 'tool_use')
|
|
164
|
+
hasToolUse = true;
|
|
165
|
+
if (block?.type === 'text')
|
|
166
|
+
lastText = block.text || '';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!hasToolUse && lastText) {
|
|
170
|
+
activity.status = 'waiting_input';
|
|
171
|
+
const textLines = lastText.trim().split('\n');
|
|
172
|
+
const lastLine = textLines[textLines.length - 1] || '';
|
|
173
|
+
if (lastText.slice(-300).includes('?')) {
|
|
174
|
+
activity.waiting_reason = 'Asking a question';
|
|
175
|
+
for (let i = textLines.length - 1; i >= 0; i--) {
|
|
176
|
+
if (textLines[i].includes('?')) {
|
|
177
|
+
const q = textLines[i].slice(0, 80);
|
|
178
|
+
activity.current_activity = `❓ ${q}${textLines[i].length > 80 ? '...' : ''}`;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
activity.waiting_reason = 'Waiting for your response';
|
|
185
|
+
activity.current_activity = `💬 ${lastLine.slice(0, 60)}${lastLine.length > 60 ? '...' : ''}`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Analyze messages for tool activity
|
|
190
|
+
for (let i = lastMessages.length - 1; i >= 0; i--) {
|
|
191
|
+
const msg = lastMessages[i];
|
|
192
|
+
const msgType = msg.type;
|
|
193
|
+
// Check for user message
|
|
194
|
+
if (msgType === 'user' && !activity.last_user_message) {
|
|
195
|
+
const content = msg.message?.content;
|
|
196
|
+
if (typeof content === 'string') {
|
|
197
|
+
activity.last_user_message = content.slice(0, 100) + (content.length > 100 ? '...' : '');
|
|
198
|
+
}
|
|
199
|
+
else if (Array.isArray(content)) {
|
|
200
|
+
for (const c of content) {
|
|
201
|
+
if (c?.type === 'text') {
|
|
202
|
+
const text = (c.text || '').slice(0, 100);
|
|
203
|
+
activity.last_user_message = text + (text.length >= 100 ? '...' : '');
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Check for assistant activity
|
|
210
|
+
if (msgType === 'assistant' && activity.status !== 'waiting_input') {
|
|
211
|
+
const content = msg.message?.content || [];
|
|
212
|
+
if (Array.isArray(content)) {
|
|
213
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
214
|
+
const block = content[j];
|
|
215
|
+
if (block?.type === 'tool_use') {
|
|
216
|
+
const toolName = block.name || 'unknown';
|
|
217
|
+
activity.last_tool = toolName;
|
|
218
|
+
activity.status = 'working';
|
|
219
|
+
const inp = block.input || {};
|
|
220
|
+
// Detailed activity based on tool
|
|
221
|
+
if (toolName === 'TodoWrite') {
|
|
222
|
+
const todos = inp.todos || [];
|
|
223
|
+
const inProgress = todos.filter((t) => t.status === 'in_progress');
|
|
224
|
+
if (inProgress.length > 0) {
|
|
225
|
+
activity.current_activity = `📋 ${(inProgress[0].content || 'Working on task').slice(0, 50)}`;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
activity.current_activity = '📋 Updating task list';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (toolName === 'Read') {
|
|
232
|
+
const file = (inp.file_path || '').split('/').pop();
|
|
233
|
+
activity.current_activity = `📖 Reading ${file}`;
|
|
234
|
+
}
|
|
235
|
+
else if (toolName === 'Write') {
|
|
236
|
+
const file = (inp.file_path || '').split('/').pop();
|
|
237
|
+
activity.current_activity = `✏️ Writing ${file}`;
|
|
238
|
+
}
|
|
239
|
+
else if (toolName === 'Edit') {
|
|
240
|
+
const file = (inp.file_path || '').split('/').pop();
|
|
241
|
+
activity.current_activity = `✏️ Editing ${file}`;
|
|
242
|
+
}
|
|
243
|
+
else if (toolName === 'Bash') {
|
|
244
|
+
const cmd = inp.command || '';
|
|
245
|
+
const desc = inp.description || '';
|
|
246
|
+
if (desc) {
|
|
247
|
+
activity.current_activity = `⚡ ${desc.slice(0, 50)}`;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
activity.current_activity = `⚡ $ ${cmd.slice(0, 45)}`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else if (toolName === 'Task') {
|
|
254
|
+
const desc = inp.description || 'task';
|
|
255
|
+
const agentType = inp.subagent_type || '';
|
|
256
|
+
const prompt = inp.prompt || '';
|
|
257
|
+
if (agentType) {
|
|
258
|
+
activity.current_activity = `🤖 ${agentType} agent: ${desc}`;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
activity.current_activity = `🤖 Subagent: ${desc}`;
|
|
262
|
+
}
|
|
263
|
+
if (prompt) {
|
|
264
|
+
activity.subagent_detail = prompt.slice(0, 120) + (prompt.length > 120 ? '...' : '');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (toolName === 'Grep') {
|
|
268
|
+
const pattern = inp.pattern || '';
|
|
269
|
+
activity.current_activity = `🔍 Searching: ${pattern.slice(0, 40)}`;
|
|
270
|
+
}
|
|
271
|
+
else if (toolName === 'Glob') {
|
|
272
|
+
const pattern = inp.pattern || '';
|
|
273
|
+
activity.current_activity = `📁 Finding: ${pattern.slice(0, 40)}`;
|
|
274
|
+
}
|
|
275
|
+
else if (toolName === 'AskUserQuestion') {
|
|
276
|
+
const questions = inp.questions || [];
|
|
277
|
+
if (questions.length > 0) {
|
|
278
|
+
const q = (questions[0].question || '').slice(0, 60);
|
|
279
|
+
activity.current_activity = `❓ ${q}`;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
activity.current_activity = '❓ Asking question';
|
|
283
|
+
}
|
|
284
|
+
activity.status = 'waiting_input';
|
|
285
|
+
activity.waiting_reason = 'Needs your input';
|
|
286
|
+
}
|
|
287
|
+
else if (['WebSearch', 'WebFetch', 'mcp__web-search-prime__webSearchPrime'].includes(toolName)) {
|
|
288
|
+
const query = (inp.query || inp.search_query || '').slice(0, 40);
|
|
289
|
+
activity.current_activity = `🌐 Searching: ${query}`;
|
|
290
|
+
}
|
|
291
|
+
else if (toolName.startsWith('mcp__')) {
|
|
292
|
+
const shortName = toolName.split('__').pop();
|
|
293
|
+
activity.current_activity = `🔌 ${shortName}`;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
activity.current_activity = `🔧 ${toolName}`;
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (activity.current_activity)
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// Ignore errors
|
|
309
|
+
}
|
|
310
|
+
return activity;
|
|
311
|
+
}
|
|
312
|
+
function getCliProcesses() {
|
|
313
|
+
try {
|
|
314
|
+
const result = (0, child_process_1.execSync)('ps aux', { encoding: 'utf-8', timeout: 5000 });
|
|
315
|
+
const processes = [];
|
|
316
|
+
for (const line of result.trim().split('\n').slice(1)) {
|
|
317
|
+
const parts = line.split(/\s+/);
|
|
318
|
+
if (parts.length < 11)
|
|
319
|
+
continue;
|
|
320
|
+
const cmd = parts.slice(10).join(' ');
|
|
321
|
+
if (cmd.startsWith('claude') && !cmd.includes('Claude.app')) {
|
|
322
|
+
const pid = parseInt(parts[1], 10);
|
|
323
|
+
const cpu = parseFloat(parts[2]);
|
|
324
|
+
const mem = parseFloat(parts[3]);
|
|
325
|
+
let cwd = null;
|
|
326
|
+
try {
|
|
327
|
+
const lsof = (0, child_process_1.execSync)(`lsof -p ${pid} -Fn`, { encoding: 'utf-8', timeout: 2000 });
|
|
328
|
+
for (const lline of lsof.split('\n')) {
|
|
329
|
+
if (lline.startsWith('n') && lline.includes('projects') && lline.includes('.jsonl')) {
|
|
330
|
+
const filePath = lline.slice(1);
|
|
331
|
+
const match = filePath.split('/projects/')[1];
|
|
332
|
+
if (match) {
|
|
333
|
+
cwd = match.split('/')[0].replace(/-/g, '/');
|
|
334
|
+
}
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Ignore
|
|
341
|
+
}
|
|
342
|
+
processes.push({ pid, cpu, mem, cwd });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return processes;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function getOpenFiles() {
|
|
352
|
+
const openFiles = new Set();
|
|
353
|
+
try {
|
|
354
|
+
const result = (0, child_process_1.execSync)('lsof -c claude', { encoding: 'utf-8', timeout: 5000 });
|
|
355
|
+
for (const line of result.split('\n')) {
|
|
356
|
+
if (line.includes('.jsonl') && line.includes('projects')) {
|
|
357
|
+
const parts = line.split(/\s+/);
|
|
358
|
+
for (const part of parts) {
|
|
359
|
+
if (part.includes('.jsonl')) {
|
|
360
|
+
openFiles.add(part);
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Ignore
|
|
369
|
+
}
|
|
370
|
+
return openFiles;
|
|
371
|
+
}
|
|
372
|
+
function getTodayUsageFromSessions() {
|
|
373
|
+
const today = new Date().toISOString().split('T')[0];
|
|
374
|
+
const todayStart = new Date();
|
|
375
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
376
|
+
const todayStartTs = todayStart.getTime();
|
|
377
|
+
const modelTokens = {};
|
|
378
|
+
let messages = 0;
|
|
379
|
+
let sessions = 0;
|
|
380
|
+
if (!fs.existsSync(PROJECTS_DIR)) {
|
|
381
|
+
return {
|
|
382
|
+
date: today,
|
|
383
|
+
tokens: 0,
|
|
384
|
+
cost: 0,
|
|
385
|
+
messages: 0,
|
|
386
|
+
sessions: 0,
|
|
387
|
+
models: [],
|
|
388
|
+
model_breakdown: {},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const projectDirs = fs.readdirSync(PROJECTS_DIR);
|
|
392
|
+
for (const projectDir of projectDirs) {
|
|
393
|
+
const projectPath = path.join(PROJECTS_DIR, projectDir);
|
|
394
|
+
if (!fs.statSync(projectPath).isDirectory())
|
|
395
|
+
continue;
|
|
396
|
+
const sessionFiles = fs.readdirSync(projectPath).filter((f) => f.endsWith('.jsonl'));
|
|
397
|
+
for (const sessionFile of sessionFiles) {
|
|
398
|
+
const sessionPath = path.join(projectPath, sessionFile);
|
|
399
|
+
const stat = fs.statSync(sessionPath);
|
|
400
|
+
if (stat.mtimeMs < todayStartTs)
|
|
401
|
+
continue;
|
|
402
|
+
sessions++;
|
|
403
|
+
try {
|
|
404
|
+
const content = fs.readFileSync(sessionPath, 'utf-8');
|
|
405
|
+
for (const line of content.trim().split('\n')) {
|
|
406
|
+
try {
|
|
407
|
+
const msg = JSON.parse(line);
|
|
408
|
+
const ts = msg.timestamp || '';
|
|
409
|
+
if (!ts.startsWith(today))
|
|
410
|
+
continue;
|
|
411
|
+
if (msg.type === 'assistant') {
|
|
412
|
+
messages++;
|
|
413
|
+
const usage = msg.message?.usage || {};
|
|
414
|
+
const model = msg.message?.model || 'unknown';
|
|
415
|
+
if (!modelTokens[model]) {
|
|
416
|
+
modelTokens[model] = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
|
|
417
|
+
}
|
|
418
|
+
modelTokens[model].input += usage.input_tokens || 0;
|
|
419
|
+
modelTokens[model].output += usage.output_tokens || 0;
|
|
420
|
+
modelTokens[model].cache_read += usage.cache_read_input_tokens || 0;
|
|
421
|
+
modelTokens[model].cache_write += usage.cache_creation_input_tokens || 0;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
let allTokens = 0;
|
|
435
|
+
let totalCost = 0;
|
|
436
|
+
const modelBreakdown = {};
|
|
437
|
+
for (const [model, tokens] of Object.entries(modelTokens)) {
|
|
438
|
+
const modelTotal = tokens.input + tokens.output + tokens.cache_read + tokens.cache_write;
|
|
439
|
+
allTokens += modelTotal;
|
|
440
|
+
const pricing = shared_1.MODEL_PRICING[model] || shared_1.DEFAULT_PRICING;
|
|
441
|
+
const cost = (tokens.input / 1000000) * pricing.input +
|
|
442
|
+
(tokens.output / 1000000) * pricing.output +
|
|
443
|
+
(tokens.cache_read / 1000000) * pricing.cache_read +
|
|
444
|
+
(tokens.cache_write / 1000000) * pricing.cache_write;
|
|
445
|
+
totalCost += cost;
|
|
446
|
+
modelBreakdown[model] = {
|
|
447
|
+
inputTokens: tokens.input,
|
|
448
|
+
outputTokens: tokens.output,
|
|
449
|
+
cacheReadInputTokens: tokens.cache_read,
|
|
450
|
+
cacheCreationInputTokens: tokens.cache_write,
|
|
451
|
+
cost: Math.round(cost * 10000) / 10000,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
date: today,
|
|
456
|
+
tokens: allTokens,
|
|
457
|
+
cost: Math.round(totalCost * 10000) / 10000,
|
|
458
|
+
messages,
|
|
459
|
+
sessions,
|
|
460
|
+
models: Object.keys(modelTokens),
|
|
461
|
+
model_breakdown: modelBreakdown,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function getDailyUsage(stats) {
|
|
465
|
+
const dailyTokens = stats.dailyModelTokens || [];
|
|
466
|
+
const dailyActivity = {};
|
|
467
|
+
for (const d of stats.dailyActivity || []) {
|
|
468
|
+
dailyActivity[d.date] = d;
|
|
469
|
+
}
|
|
470
|
+
const result = [];
|
|
471
|
+
for (const day of dailyTokens.slice(-14)) {
|
|
472
|
+
const date = day.date;
|
|
473
|
+
const tokensByModel = day.tokensByModel || {};
|
|
474
|
+
const totalTokens = Object.values(tokensByModel).reduce((sum, t) => sum + t, 0);
|
|
475
|
+
let dayCost = 0;
|
|
476
|
+
for (const [model, tokens] of Object.entries(tokensByModel)) {
|
|
477
|
+
const pricing = shared_1.MODEL_PRICING[model] || shared_1.DEFAULT_PRICING;
|
|
478
|
+
dayCost += (tokens / 1000000) * pricing.input;
|
|
479
|
+
}
|
|
480
|
+
const activity = dailyActivity[date] || { messageCount: 0, sessionCount: 0 };
|
|
481
|
+
result.push({
|
|
482
|
+
date,
|
|
483
|
+
tokens: totalTokens,
|
|
484
|
+
cost: Math.round(dayCost * 10000) / 10000,
|
|
485
|
+
messages: activity.messageCount,
|
|
486
|
+
sessions: activity.sessionCount,
|
|
487
|
+
models: Object.keys(tokensByModel),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
function getSessions() {
|
|
493
|
+
const active = [];
|
|
494
|
+
const recent = [];
|
|
495
|
+
if (!fs.existsSync(PROJECTS_DIR)) {
|
|
496
|
+
return { active, recent };
|
|
497
|
+
}
|
|
498
|
+
const openFiles = getOpenFiles();
|
|
499
|
+
const now = Date.now();
|
|
500
|
+
const projectDirs = fs.readdirSync(PROJECTS_DIR);
|
|
501
|
+
for (const projectDir of projectDirs) {
|
|
502
|
+
const projectPath = path.join(PROJECTS_DIR, projectDir);
|
|
503
|
+
if (!fs.statSync(projectPath).isDirectory())
|
|
504
|
+
continue;
|
|
505
|
+
const sessionFiles = fs.readdirSync(projectPath).filter((f) => f.endsWith('.jsonl'));
|
|
506
|
+
for (const sessionFile of sessionFiles) {
|
|
507
|
+
const sessionPath = path.join(projectPath, sessionFile);
|
|
508
|
+
const stat = fs.statSync(sessionPath);
|
|
509
|
+
const isActive = openFiles.has(sessionPath) || now - stat.mtimeMs < 30000;
|
|
510
|
+
const projectName = projectDir.replace(/-/g, '/');
|
|
511
|
+
const projectShort = projectName.includes('/') ? projectName.split('/').pop() : projectName;
|
|
512
|
+
let activity = {
|
|
513
|
+
status: 'idle',
|
|
514
|
+
current_activity: null,
|
|
515
|
+
last_tool: null,
|
|
516
|
+
last_user_message: null,
|
|
517
|
+
subagent_detail: null,
|
|
518
|
+
waiting_reason: null,
|
|
519
|
+
};
|
|
520
|
+
let tokenData = null;
|
|
521
|
+
let sessionCost = null;
|
|
522
|
+
if (isActive) {
|
|
523
|
+
activity = parseSessionActivity(sessionPath);
|
|
524
|
+
tokenData = parseSessionTokens(sessionPath);
|
|
525
|
+
sessionCost = (0, shared_1.calculateCost)(tokenData.model || 'claude-opus-4-5-20251101', {
|
|
526
|
+
input_tokens: tokenData.input_tokens,
|
|
527
|
+
output_tokens: tokenData.output_tokens,
|
|
528
|
+
cache_read_input_tokens: tokenData.cache_read,
|
|
529
|
+
cache_creation_input_tokens: tokenData.cache_write,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
const session = {
|
|
533
|
+
session_id: path.basename(sessionFile, '.jsonl'),
|
|
534
|
+
project: projectName,
|
|
535
|
+
project_short: projectShort,
|
|
536
|
+
last_modified: new Date(stat.mtimeMs).toLocaleTimeString('en-US', { hour12: false }),
|
|
537
|
+
size_kb: Math.round((stat.size / 1024) * 10) / 10,
|
|
538
|
+
is_active: isActive,
|
|
539
|
+
tokens: tokenData,
|
|
540
|
+
cost: sessionCost,
|
|
541
|
+
model: tokenData?.model || null,
|
|
542
|
+
...activity,
|
|
543
|
+
};
|
|
544
|
+
if (isActive) {
|
|
545
|
+
active.push(session);
|
|
546
|
+
}
|
|
547
|
+
else if (now - stat.mtimeMs < 86400000) {
|
|
548
|
+
recent.push(session);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
active.sort((a, b) => b.last_modified.localeCompare(a.last_modified));
|
|
553
|
+
recent.sort((a, b) => b.last_modified.localeCompare(a.last_modified));
|
|
554
|
+
return { active, recent: recent.slice(0, 10) };
|
|
555
|
+
}
|
|
556
|
+
function collectData() {
|
|
557
|
+
const stats = getStats();
|
|
558
|
+
const processes = getCliProcesses();
|
|
559
|
+
const { active: activeSessions, recent: recentSessions } = getSessions();
|
|
560
|
+
// Today's usage - compute from session files in real-time
|
|
561
|
+
const todayUsage = getTodayUsageFromSessions();
|
|
562
|
+
// Merge historical model usage with today's
|
|
563
|
+
const modelUsage = { ...stats.modelUsage };
|
|
564
|
+
const todayBreakdown = todayUsage.model_breakdown;
|
|
565
|
+
for (const [model, todayData] of Object.entries(todayBreakdown)) {
|
|
566
|
+
if (modelUsage[model]) {
|
|
567
|
+
modelUsage[model] = {
|
|
568
|
+
inputTokens: (modelUsage[model].inputTokens || 0) + (todayData.inputTokens || 0),
|
|
569
|
+
outputTokens: (modelUsage[model].outputTokens || 0) + (todayData.outputTokens || 0),
|
|
570
|
+
cacheReadInputTokens: (modelUsage[model].cacheReadInputTokens || 0) + (todayData.cacheReadInputTokens || 0),
|
|
571
|
+
cacheCreationInputTokens: (modelUsage[model].cacheCreationInputTokens || 0) + (todayData.cacheCreationInputTokens || 0),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
modelUsage[model] = {
|
|
576
|
+
inputTokens: todayData.inputTokens,
|
|
577
|
+
outputTokens: todayData.outputTokens,
|
|
578
|
+
cacheReadInputTokens: todayData.cacheReadInputTokens,
|
|
579
|
+
cacheCreationInputTokens: todayData.cacheCreationInputTokens,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// Calculate costs for each model
|
|
584
|
+
const modelCosts = {};
|
|
585
|
+
let totalCost = 0;
|
|
586
|
+
let totalTokens = 0;
|
|
587
|
+
for (const [model, usage] of Object.entries(modelUsage)) {
|
|
588
|
+
const cost = (0, shared_1.calculateCost)(model, usage);
|
|
589
|
+
modelCosts[model] = cost;
|
|
590
|
+
totalCost += cost;
|
|
591
|
+
totalTokens +=
|
|
592
|
+
(usage.inputTokens || 0) +
|
|
593
|
+
(usage.outputTokens || 0) +
|
|
594
|
+
(usage.cacheReadInputTokens || 0) +
|
|
595
|
+
(usage.cacheCreationInputTokens || 0);
|
|
596
|
+
}
|
|
597
|
+
// Get daily usage and add today
|
|
598
|
+
const dailyUsage = getDailyUsage(stats);
|
|
599
|
+
if (todayUsage.tokens > 0) {
|
|
600
|
+
dailyUsage.push({
|
|
601
|
+
date: todayUsage.date,
|
|
602
|
+
tokens: todayUsage.tokens,
|
|
603
|
+
cost: todayUsage.cost,
|
|
604
|
+
messages: todayUsage.messages,
|
|
605
|
+
sessions: todayUsage.sessions,
|
|
606
|
+
models: todayUsage.models,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
// Active sessions total cost
|
|
610
|
+
const activeSessionsCost = activeSessions.reduce((sum, s) => sum + (s.cost || 0), 0);
|
|
611
|
+
const activeSessionsTokens = activeSessions.reduce((sum, s) => {
|
|
612
|
+
if (!s.tokens)
|
|
613
|
+
return sum;
|
|
614
|
+
return sum + s.tokens.input_tokens + s.tokens.output_tokens + s.tokens.cache_read + s.tokens.cache_write;
|
|
615
|
+
}, 0);
|
|
616
|
+
// Calculate averages and projections
|
|
617
|
+
const totalSessionsCount = (stats.totalSessions || 0) + todayUsage.sessions;
|
|
618
|
+
const totalMessagesCount = (stats.totalMessages || 0) + todayUsage.messages;
|
|
619
|
+
const avgCostPerMessage = totalMessagesCount > 0 ? totalCost / totalMessagesCount : 0;
|
|
620
|
+
const avgCostPerSession = totalSessionsCount > 0 ? totalCost / totalSessionsCount : 0;
|
|
621
|
+
// Days since first session for projection
|
|
622
|
+
let daysActive = dailyUsage.length || 1;
|
|
623
|
+
const firstSessionDate = stats.firstSessionDate;
|
|
624
|
+
if (firstSessionDate) {
|
|
625
|
+
try {
|
|
626
|
+
const firstDt = new Date(firstSessionDate);
|
|
627
|
+
daysActive = Math.max(1, Math.floor((Date.now() - firstDt.getTime()) / 86400000) + 1);
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// Use default
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const dailyAvgCost = daysActive > 0 ? totalCost / daysActive : 0;
|
|
634
|
+
// Weekly cost calculation (last 7 days from daily_usage)
|
|
635
|
+
const last7Days = dailyUsage.slice(-7);
|
|
636
|
+
let weeklyCost = last7Days.reduce((sum, d) => sum + (d.cost || 0), 0);
|
|
637
|
+
weeklyCost += todayUsage.cost;
|
|
638
|
+
// Today's hourly rate
|
|
639
|
+
const currentHour = new Date().getHours();
|
|
640
|
+
const hoursElapsed = Math.max(currentHour, 1);
|
|
641
|
+
const todayHourlyRate = hoursElapsed > 0 ? todayUsage.cost / hoursElapsed : 0;
|
|
642
|
+
const hourCounts = stats.hourCounts || {};
|
|
643
|
+
const summary = {
|
|
644
|
+
is_running: activeSessions.length > 0,
|
|
645
|
+
active_count: activeSessions.length,
|
|
646
|
+
waiting_input: activeSessions.filter((s) => s.status === 'waiting_input').length,
|
|
647
|
+
total_sessions: totalSessionsCount,
|
|
648
|
+
total_messages: totalMessagesCount,
|
|
649
|
+
total_cost: Math.round(totalCost * 100) / 100,
|
|
650
|
+
total_tokens: totalTokens,
|
|
651
|
+
active_sessions_cost: Math.round(activeSessionsCost * 10000) / 10000,
|
|
652
|
+
active_sessions_tokens: activeSessionsTokens,
|
|
653
|
+
first_session: firstSessionDate || null,
|
|
654
|
+
days_active: daysActive,
|
|
655
|
+
avg_cost_per_message: Math.round(avgCostPerMessage * 10000) / 10000,
|
|
656
|
+
avg_cost_per_session: Math.round(avgCostPerSession * 100) / 100,
|
|
657
|
+
daily_avg_cost: Math.round(dailyAvgCost * 100) / 100,
|
|
658
|
+
projected_monthly: Math.round(dailyAvgCost * 30 * 100) / 100,
|
|
659
|
+
weekly_cost: Math.round(weeklyCost * 100) / 100,
|
|
660
|
+
today_hourly_rate: Math.round(todayHourlyRate * 10000) / 10000,
|
|
661
|
+
est_weekly: Math.round(dailyAvgCost * 7 * 100) / 100,
|
|
662
|
+
};
|
|
663
|
+
return {
|
|
664
|
+
stats,
|
|
665
|
+
model_usage: modelUsage,
|
|
666
|
+
processes,
|
|
667
|
+
active_sessions: activeSessions,
|
|
668
|
+
recent_sessions: recentSessions,
|
|
669
|
+
model_costs: modelCosts,
|
|
670
|
+
daily_usage: dailyUsage,
|
|
671
|
+
today_usage: todayUsage,
|
|
672
|
+
hour_counts: hourCounts,
|
|
673
|
+
summary,
|
|
674
|
+
usage_limits: (0, usage_limits_1.getUsageLimitsSync)(),
|
|
675
|
+
};
|
|
676
|
+
}
|
package/dist/index.d.ts
ADDED