code-dash 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/bin/claudeview.js +91 -0
- package/package.json +37 -0
- package/src/analyzer.js +389 -0
- package/src/dashboard.html +846 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# ClaudeView
|
|
2
|
+
|
|
3
|
+
A local analytics dashboard for your [Claude Code](https://claude.ai/code) sessions.
|
|
4
|
+
|
|
5
|
+
Reads `~/.claude/` and shows you token usage, costs, tool activity, session history, and more — all in a clean browser dashboard.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx claudeview
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then open http://localhost:3000 (it opens automatically).
|
|
14
|
+
|
|
15
|
+
## Options
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
claudeview --port=3001 # use a different port
|
|
19
|
+
claudeview --no-open # don't auto-open the browser
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What it shows
|
|
23
|
+
|
|
24
|
+
- **Token usage** — input, output, cache creation & reads over time
|
|
25
|
+
- **Estimated cost** — per session and total, by model
|
|
26
|
+
- **Model distribution** — which Claude models you use most
|
|
27
|
+
- **Activity heatmap** — GitHub-style calendar of your activity
|
|
28
|
+
- **Tool usage** — which tools (Bash, Read, Write, etc.) you call most
|
|
29
|
+
- **Hourly distribution** — when in the day you're most active
|
|
30
|
+
- **Cache efficiency** — your prompt cache hit rate
|
|
31
|
+
- **Sessions table** — every session with duration, message counts, tokens, cost
|
|
32
|
+
- **Projects table** — per-project rollup of all activity
|
|
33
|
+
|
|
34
|
+
## Data sources
|
|
35
|
+
|
|
36
|
+
All data is read locally from `~/.claude/`:
|
|
37
|
+
|
|
38
|
+
| Source | Data |
|
|
39
|
+
|--------|------|
|
|
40
|
+
| `~/.claude/projects/**/*.jsonl` | Session conversations, token usage, tool calls |
|
|
41
|
+
| `~/.claude.json` | Account info, project metadata |
|
|
42
|
+
| `~/.claude/history.jsonl` | Command/input history |
|
|
43
|
+
|
|
44
|
+
No data leaves your machine.
|
|
45
|
+
|
|
46
|
+
## Install globally
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install -g claudeview
|
|
50
|
+
claudeview
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const { analyze } = require('../src/analyzer');
|
|
9
|
+
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const portArg = args.find(a => a.startsWith('--port=') || a.startsWith('-p='));
|
|
12
|
+
const PORT = portArg ? parseInt(portArg.split('=')[1]) : 3000;
|
|
13
|
+
const noOpen = args.includes('--no-open');
|
|
14
|
+
const DASHBOARD_PATH = path.join(__dirname, '../src/dashboard.html');
|
|
15
|
+
|
|
16
|
+
const MIME = {
|
|
17
|
+
'.html': 'text/html',
|
|
18
|
+
'.json': 'application/json',
|
|
19
|
+
'.js': 'text/javascript',
|
|
20
|
+
'.css': 'text/css',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function openBrowser(url) {
|
|
24
|
+
try {
|
|
25
|
+
const platform = process.platform;
|
|
26
|
+
if (platform === 'darwin') execSync(`open "${url}"`, { stdio: 'ignore' });
|
|
27
|
+
else if (platform === 'win32') execSync(`start "" "${url}"`, { stdio: 'ignore', shell: true });
|
|
28
|
+
else execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
|
|
29
|
+
} catch (_) {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const server = http.createServer((req, res) => {
|
|
33
|
+
const url = req.url.split('?')[0];
|
|
34
|
+
|
|
35
|
+
// CORS for dev
|
|
36
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
37
|
+
|
|
38
|
+
if (url === '/api/data') {
|
|
39
|
+
try {
|
|
40
|
+
const data = analyze();
|
|
41
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
42
|
+
res.end(JSON.stringify(data));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
45
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Serve dashboard
|
|
51
|
+
if (url === '/' || url === '/index.html') {
|
|
52
|
+
try {
|
|
53
|
+
const html = fs.readFileSync(DASHBOARD_PATH, 'utf8');
|
|
54
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
55
|
+
res.end(html);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
res.writeHead(500);
|
|
58
|
+
res.end(`<pre>Error loading dashboard: ${err.message}</pre>`);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
res.writeHead(404);
|
|
64
|
+
res.end('Not found');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
68
|
+
const url = `http://localhost:${PORT}`;
|
|
69
|
+
console.log('\n ClaudeView ');
|
|
70
|
+
console.log(' ─────────────────────────────────────');
|
|
71
|
+
console.log(` Dashboard → ${url}`);
|
|
72
|
+
console.log(` API data → ${url}/api/data`);
|
|
73
|
+
console.log(' ─────────────────────────────────────');
|
|
74
|
+
console.log(' Press Ctrl+C to stop\n');
|
|
75
|
+
|
|
76
|
+
if (!noOpen) openBrowser(url);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
server.on('error', (err) => {
|
|
80
|
+
if (err.code === 'EADDRINUSE') {
|
|
81
|
+
console.error(`\n Port ${PORT} is already in use. Try: claudeview --port=3001\n`);
|
|
82
|
+
} else {
|
|
83
|
+
console.error('\n Server error:', err.message, '\n');
|
|
84
|
+
}
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
process.on('SIGINT', () => {
|
|
89
|
+
console.log('\n Goodbye!\n');
|
|
90
|
+
process.exit(0);
|
|
91
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "code-dash",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Analytics dashboard for your local Claude Code sessions — token usage, activity, and costs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"code-dash": "bin/claudeview.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/analyzer.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/claudeview.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"anthropic",
|
|
17
|
+
"ai",
|
|
18
|
+
"analytics",
|
|
19
|
+
"dashboard",
|
|
20
|
+
"npx"
|
|
21
|
+
],
|
|
22
|
+
"author": "Hari Govind <harigovind2102@gmail.com>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"preferGlobal": true,
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=16.0.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/harigovind511/claudeview.git"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin/",
|
|
34
|
+
"src/",
|
|
35
|
+
"README.md"
|
|
36
|
+
]
|
|
37
|
+
}
|
package/src/analyzer.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
8
|
+
const CLAUDE_CONFIG = path.join(os.homedir(), '.claude.json');
|
|
9
|
+
|
|
10
|
+
// Approximate cost per million tokens (USD) by model
|
|
11
|
+
const MODEL_COSTS = {
|
|
12
|
+
'claude-opus-4': { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.50 },
|
|
13
|
+
'claude-opus-4-5': { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.50 },
|
|
14
|
+
'claude-sonnet-4-5': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
15
|
+
'claude-sonnet-4-6': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
16
|
+
'claude-haiku-4-5': { input: 0.80, output: 4, cacheWrite: 1.00, cacheRead: 0.08 },
|
|
17
|
+
'claude-haiku-4-5-20251001': { input: 0.80, output: 4, cacheWrite: 1.00, cacheRead: 0.08 },
|
|
18
|
+
'claude-sonnet-3-5': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
19
|
+
'claude-haiku-3': { input: 0.25, output: 1.25, cacheWrite: 0.30, cacheRead: 0.03 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getModelCost(model) {
|
|
23
|
+
if (!model) return { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 };
|
|
24
|
+
// Exact match first
|
|
25
|
+
if (MODEL_COSTS[model]) return MODEL_COSTS[model];
|
|
26
|
+
// Prefix match
|
|
27
|
+
for (const key of Object.keys(MODEL_COSTS)) {
|
|
28
|
+
if (model.startsWith(key) || key.startsWith(model)) return MODEL_COSTS[key];
|
|
29
|
+
}
|
|
30
|
+
return { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function calcCost(usage, model) {
|
|
34
|
+
const rates = getModelCost(model);
|
|
35
|
+
const M = 1_000_000;
|
|
36
|
+
return (
|
|
37
|
+
((usage.input_tokens || 0) * rates.input) / M +
|
|
38
|
+
((usage.output_tokens || 0) * rates.output) / M +
|
|
39
|
+
((usage.cache_creation_input_tokens || 0) * rates.cacheWrite) / M +
|
|
40
|
+
((usage.cache_read_input_tokens || 0) * rates.cacheRead) / M
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseJSONL(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
47
|
+
const results = [];
|
|
48
|
+
for (const line of content.split('\n')) {
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
if (!trimmed) continue;
|
|
51
|
+
try {
|
|
52
|
+
results.push(JSON.parse(trimmed));
|
|
53
|
+
} catch (_) {
|
|
54
|
+
// skip malformed lines
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
} catch (_) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readConfig() {
|
|
64
|
+
try {
|
|
65
|
+
const raw = fs.readFileSync(CLAUDE_CONFIG, 'utf8');
|
|
66
|
+
return JSON.parse(raw);
|
|
67
|
+
} catch (_) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getProjectName(projectPath) {
|
|
73
|
+
if (!projectPath) return 'Unknown';
|
|
74
|
+
const parts = projectPath.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
75
|
+
return parts.slice(-2).join('/') || parts[parts.length - 1] || projectPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatDirName(dirName) {
|
|
79
|
+
// "-Users-hari-Code-Base-Open-Source-claudeview" -> "/Users/hari/Code Base/Open Source/claudeview"
|
|
80
|
+
return '/' + dirName.replace(/-+/g, (m, offset, str) => {
|
|
81
|
+
// heuristic: keep hyphen if it looks like a word-hyphen (e.g. "Open-Source")
|
|
82
|
+
// but replace leading/separator hyphens with /
|
|
83
|
+
return '/';
|
|
84
|
+
}).replace(/^\/+/, '/');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveProjectPath(dirName) {
|
|
88
|
+
// Best-effort: replace - with / to recover original path
|
|
89
|
+
// The dir names use - as separator: "-Users-hari-Foo" -> "/Users/hari/Foo"
|
|
90
|
+
// But paths with spaces become "-Users-hari-Code-Base-Foo" which loses space info
|
|
91
|
+
return dirName.replace(/^-/, '/').replace(/-/g, '/');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function analyze() {
|
|
95
|
+
const config = readConfig();
|
|
96
|
+
const projectsDir = path.join(CLAUDE_DIR, 'projects');
|
|
97
|
+
|
|
98
|
+
// ── Account info ─────────────────────────────────────────────────────────
|
|
99
|
+
let account = { email: null, organization: null, subscriptionType: null };
|
|
100
|
+
if (config?.oauthAccount) {
|
|
101
|
+
account = {
|
|
102
|
+
email: config.oauthAccount.emailAddress || null,
|
|
103
|
+
organization: config.oauthAccount.organizationName || null,
|
|
104
|
+
subscriptionType: config.oauthAccount.accountType || null,
|
|
105
|
+
userId: config.oauthAccount.userId || null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Session files ─────────────────────────────────────────────────────────
|
|
110
|
+
const sessions = [];
|
|
111
|
+
const toolUsage = {};
|
|
112
|
+
const modelUsage = {};
|
|
113
|
+
const dailyMap = {}; // date -> { messages, tokens, cost, sessions }
|
|
114
|
+
const hourlyDist = new Array(24).fill(0);
|
|
115
|
+
|
|
116
|
+
let totalInputTokens = 0;
|
|
117
|
+
let totalOutputTokens = 0;
|
|
118
|
+
let totalCacheCreation = 0;
|
|
119
|
+
let totalCacheRead = 0;
|
|
120
|
+
let totalCostUSD = 0;
|
|
121
|
+
let totalUserMessages = 0;
|
|
122
|
+
let totalAssistantMessages = 0;
|
|
123
|
+
let totalToolCalls = 0;
|
|
124
|
+
|
|
125
|
+
if (fs.existsSync(projectsDir)) {
|
|
126
|
+
const projectDirs = fs.readdirSync(projectsDir).filter(d => {
|
|
127
|
+
return fs.statSync(path.join(projectsDir, d)).isDirectory();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
for (const projDir of projectDirs) {
|
|
131
|
+
const projPath = path.join(projectsDir, projDir);
|
|
132
|
+
const projectFullPath = resolveProjectPath(projDir);
|
|
133
|
+
|
|
134
|
+
const sessionFiles = fs.readdirSync(projPath)
|
|
135
|
+
.filter(f => f.endsWith('.jsonl') && !fs.statSync(path.join(projPath, f)).isDirectory());
|
|
136
|
+
|
|
137
|
+
for (const sessionFile of sessionFiles) {
|
|
138
|
+
const sessionId = sessionFile.replace('.jsonl', '');
|
|
139
|
+
const lines = parseJSONL(path.join(projPath, sessionFile));
|
|
140
|
+
|
|
141
|
+
if (lines.length === 0) continue;
|
|
142
|
+
|
|
143
|
+
// per-session aggregates
|
|
144
|
+
let sInputTokens = 0, sOutputTokens = 0, sCacheCreation = 0, sCacheRead = 0;
|
|
145
|
+
let sUserMessages = 0, sAssistantMessages = 0, sToolCalls = 0;
|
|
146
|
+
let sModel = null;
|
|
147
|
+
let sStartTime = null, sEndTime = null;
|
|
148
|
+
const sTools = {};
|
|
149
|
+
|
|
150
|
+
for (const line of lines) {
|
|
151
|
+
if (!line.timestamp) continue;
|
|
152
|
+
const ts = new Date(line.timestamp);
|
|
153
|
+
if (isNaN(ts.getTime())) continue;
|
|
154
|
+
|
|
155
|
+
if (!sStartTime || ts < sStartTime) sStartTime = ts;
|
|
156
|
+
if (!sEndTime || ts > sEndTime) sEndTime = ts;
|
|
157
|
+
|
|
158
|
+
if (line.type === 'user') {
|
|
159
|
+
sUserMessages++;
|
|
160
|
+
// Count hour distribution
|
|
161
|
+
hourlyDist[ts.getHours()]++;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (line.type === 'assistant' && line.message) {
|
|
165
|
+
sAssistantMessages++;
|
|
166
|
+
const msg = line.message;
|
|
167
|
+
const model = msg.model || line.model || null;
|
|
168
|
+
if (model && !sModel) sModel = model;
|
|
169
|
+
|
|
170
|
+
// Token usage
|
|
171
|
+
const usage = msg.usage || {};
|
|
172
|
+
const inp = usage.input_tokens || 0;
|
|
173
|
+
const out = usage.output_tokens || 0;
|
|
174
|
+
const cc = usage.cache_creation_input_tokens || 0;
|
|
175
|
+
const cr = usage.cache_read_input_tokens || 0;
|
|
176
|
+
|
|
177
|
+
sInputTokens += inp;
|
|
178
|
+
sOutputTokens += out;
|
|
179
|
+
sCacheCreation += cc;
|
|
180
|
+
sCacheRead += cr;
|
|
181
|
+
|
|
182
|
+
const cost = calcCost(usage, model);
|
|
183
|
+
totalCostUSD += cost;
|
|
184
|
+
|
|
185
|
+
// Model usage
|
|
186
|
+
if (model) {
|
|
187
|
+
if (!modelUsage[model]) modelUsage[model] = { messages: 0, inputTokens: 0, outputTokens: 0, cost: 0 };
|
|
188
|
+
modelUsage[model].messages++;
|
|
189
|
+
modelUsage[model].inputTokens += inp;
|
|
190
|
+
modelUsage[model].outputTokens += out;
|
|
191
|
+
modelUsage[model].cost += cost;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Tool calls in content
|
|
195
|
+
if (Array.isArray(msg.content)) {
|
|
196
|
+
for (const block of msg.content) {
|
|
197
|
+
if (block.type === 'tool_use') {
|
|
198
|
+
sToolCalls++;
|
|
199
|
+
const tName = block.name || 'unknown';
|
|
200
|
+
sTools[tName] = (sTools[tName] || 0) + 1;
|
|
201
|
+
toolUsage[tName] = (toolUsage[tName] || 0) + 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Daily map
|
|
207
|
+
const dateKey = ts.toISOString().slice(0, 10);
|
|
208
|
+
if (!dailyMap[dateKey]) dailyMap[dateKey] = { messages: 0, tokens: 0, cost: 0, sessions: new Set() };
|
|
209
|
+
dailyMap[dateKey].messages++;
|
|
210
|
+
dailyMap[dateKey].tokens += inp + out;
|
|
211
|
+
dailyMap[dateKey].cost += cost;
|
|
212
|
+
dailyMap[dateKey].sessions.add(sessionId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Accumulate totals
|
|
217
|
+
totalInputTokens += sInputTokens;
|
|
218
|
+
totalOutputTokens += sOutputTokens;
|
|
219
|
+
totalCacheCreation += sCacheCreation;
|
|
220
|
+
totalCacheRead += sCacheRead;
|
|
221
|
+
totalUserMessages += sUserMessages;
|
|
222
|
+
totalAssistantMessages += sAssistantMessages;
|
|
223
|
+
totalToolCalls += sToolCalls;
|
|
224
|
+
|
|
225
|
+
const durationMs = sStartTime && sEndTime ? sEndTime - sStartTime : 0;
|
|
226
|
+
const sessionCost = calcCost({
|
|
227
|
+
input_tokens: sInputTokens,
|
|
228
|
+
output_tokens: sOutputTokens,
|
|
229
|
+
cache_creation_input_tokens: sCacheCreation,
|
|
230
|
+
cache_read_input_tokens: sCacheRead,
|
|
231
|
+
}, sModel);
|
|
232
|
+
|
|
233
|
+
sessions.push({
|
|
234
|
+
id: sessionId,
|
|
235
|
+
project: projectFullPath,
|
|
236
|
+
projectName: getProjectName(projectFullPath),
|
|
237
|
+
startTime: sStartTime ? sStartTime.toISOString() : null,
|
|
238
|
+
endTime: sEndTime ? sEndTime.toISOString() : null,
|
|
239
|
+
durationMinutes: Math.round(durationMs / 60000),
|
|
240
|
+
messages: sUserMessages + sAssistantMessages,
|
|
241
|
+
userMessages: sUserMessages,
|
|
242
|
+
assistantMessages: sAssistantMessages,
|
|
243
|
+
tokens: {
|
|
244
|
+
input: sInputTokens,
|
|
245
|
+
output: sOutputTokens,
|
|
246
|
+
cacheCreation: sCacheCreation,
|
|
247
|
+
cacheRead: sCacheRead,
|
|
248
|
+
total: sInputTokens + sOutputTokens + sCacheCreation + sCacheRead,
|
|
249
|
+
},
|
|
250
|
+
costUSD: sessionCost,
|
|
251
|
+
model: sModel,
|
|
252
|
+
toolCalls: sToolCalls,
|
|
253
|
+
topTools: Object.entries(sTools)
|
|
254
|
+
.sort((a, b) => b[1] - a[1])
|
|
255
|
+
.slice(0, 5)
|
|
256
|
+
.map(([name, count]) => ({ name, count })),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Sort sessions by startTime desc
|
|
263
|
+
sessions.sort((a, b) => {
|
|
264
|
+
if (!a.startTime) return 1;
|
|
265
|
+
if (!b.startTime) return -1;
|
|
266
|
+
return b.startTime.localeCompare(a.startTime);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── Projects rollup ───────────────────────────────────────────────────────
|
|
270
|
+
const projectMap = {};
|
|
271
|
+
for (const s of sessions) {
|
|
272
|
+
const key = s.project;
|
|
273
|
+
if (!projectMap[key]) {
|
|
274
|
+
projectMap[key] = {
|
|
275
|
+
path: s.project,
|
|
276
|
+
name: s.projectName,
|
|
277
|
+
sessions: 0,
|
|
278
|
+
messages: 0,
|
|
279
|
+
tokens: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, total: 0 },
|
|
280
|
+
costUSD: 0,
|
|
281
|
+
lastActive: null,
|
|
282
|
+
tools: {},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const p = projectMap[key];
|
|
286
|
+
p.sessions++;
|
|
287
|
+
p.messages += s.messages;
|
|
288
|
+
p.tokens.input += s.tokens.input;
|
|
289
|
+
p.tokens.output += s.tokens.output;
|
|
290
|
+
p.tokens.cacheCreation += s.tokens.cacheCreation;
|
|
291
|
+
p.tokens.cacheRead += s.tokens.cacheRead;
|
|
292
|
+
p.tokens.total += s.tokens.total;
|
|
293
|
+
p.costUSD += s.costUSD;
|
|
294
|
+
if (!p.lastActive || (s.startTime && s.startTime > p.lastActive)) {
|
|
295
|
+
p.lastActive = s.startTime;
|
|
296
|
+
}
|
|
297
|
+
for (const { name, count } of s.topTools) {
|
|
298
|
+
p.tools[name] = (p.tools[name] || 0) + count;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Also pull cost data from config's project metadata (more accurate)
|
|
303
|
+
if (config?.projects) {
|
|
304
|
+
for (const [projPath, meta] of Object.entries(config.projects)) {
|
|
305
|
+
const key = projPath;
|
|
306
|
+
if (projectMap[key] && meta.lastCost) {
|
|
307
|
+
// Use config cost if we have it (it's the authoritative source)
|
|
308
|
+
// but only if our calculated cost is zero (no session data)
|
|
309
|
+
if (projectMap[key].costUSD === 0 && meta.lastCost) {
|
|
310
|
+
projectMap[key].costUSD = meta.lastCost;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const projects = Object.values(projectMap)
|
|
317
|
+
.sort((a, b) => (b.lastActive || '').localeCompare(a.lastActive || ''));
|
|
318
|
+
|
|
319
|
+
// ── Daily activity ────────────────────────────────────────────────────────
|
|
320
|
+
const dailyActivity = Object.entries(dailyMap)
|
|
321
|
+
.map(([date, d]) => ({
|
|
322
|
+
date,
|
|
323
|
+
messages: d.messages,
|
|
324
|
+
tokens: d.tokens,
|
|
325
|
+
cost: d.cost,
|
|
326
|
+
sessions: d.sessions.size,
|
|
327
|
+
}))
|
|
328
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
329
|
+
|
|
330
|
+
// ── Date range ────────────────────────────────────────────────────────────
|
|
331
|
+
const allDates = sessions.filter(s => s.startTime).map(s => s.startTime);
|
|
332
|
+
const firstDate = allDates.length ? allDates[allDates.length - 1] : null;
|
|
333
|
+
const lastDate = allDates.length ? allDates[0] : null;
|
|
334
|
+
|
|
335
|
+
const totalTokens = totalInputTokens + totalOutputTokens + totalCacheCreation + totalCacheRead;
|
|
336
|
+
const cacheHitRate = totalTokens > 0
|
|
337
|
+
? Math.round((totalCacheRead / totalTokens) * 100)
|
|
338
|
+
: 0;
|
|
339
|
+
|
|
340
|
+
// ── Command history ───────────────────────────────────────────────────────
|
|
341
|
+
const historyPath = path.join(CLAUDE_DIR, 'history.jsonl');
|
|
342
|
+
const history = parseJSONL(historyPath)
|
|
343
|
+
.filter(h => h.display)
|
|
344
|
+
.slice(-100)
|
|
345
|
+
.map(h => ({
|
|
346
|
+
display: h.display,
|
|
347
|
+
timestamp: h.timestamp,
|
|
348
|
+
project: h.project || null,
|
|
349
|
+
sessionId: h.sessionId || null,
|
|
350
|
+
}));
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
meta: {
|
|
354
|
+
analyzedAt: new Date().toISOString(),
|
|
355
|
+
claudeDir: CLAUDE_DIR,
|
|
356
|
+
version: '1.0.0',
|
|
357
|
+
},
|
|
358
|
+
account,
|
|
359
|
+
overview: {
|
|
360
|
+
totalSessions: sessions.length,
|
|
361
|
+
totalProjects: projects.length,
|
|
362
|
+
totalMessages: { user: totalUserMessages, assistant: totalAssistantMessages, total: totalUserMessages + totalAssistantMessages },
|
|
363
|
+
tokens: {
|
|
364
|
+
input: totalInputTokens,
|
|
365
|
+
output: totalOutputTokens,
|
|
366
|
+
cacheCreation: totalCacheCreation,
|
|
367
|
+
cacheRead: totalCacheRead,
|
|
368
|
+
total: totalTokens,
|
|
369
|
+
cacheHitRate,
|
|
370
|
+
},
|
|
371
|
+
costUSD: totalCostUSD,
|
|
372
|
+
toolCalls: totalToolCalls,
|
|
373
|
+
dateRange: {
|
|
374
|
+
first: firstDate,
|
|
375
|
+
last: lastDate,
|
|
376
|
+
activeDays: dailyActivity.length,
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
models: modelUsage,
|
|
380
|
+
projects,
|
|
381
|
+
sessions: sessions.slice(0, 200), // cap for JSON size
|
|
382
|
+
tools: toolUsage,
|
|
383
|
+
dailyActivity,
|
|
384
|
+
hourlyDistribution: hourlyDist,
|
|
385
|
+
recentHistory: history,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = { analyze };
|