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 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
+ }
@@ -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 };