claude-mission-control 1.17.1 → 1.19.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 +6 -0
- package/lib/collector.js +27 -0
- package/lib/demo.js +33 -1
- package/lib/fame.js +102 -0
- package/package.json +1 -1
- package/public/index.html +410 -238
- package/server.js +22 -1
package/README.md
CHANGED
|
@@ -35,6 +35,12 @@ Claude Code runs in a terminal. Once you have two or three sessions going, you l
|
|
|
35
35
|
|
|
36
36
|
*Every screenshot here is demo data (`CLAUDE_DASH_DEMO=1`). The projects are invented.*
|
|
37
37
|
|
|
38
|
+
**New in 1.18.0**
|
|
39
|
+
|
|
40
|
+
- Hall of Fame (⌘K → Hall of Fame, or from Hive): your best sessions over a week, 30 days or all time, across seven awards from most commits shipped and best value per commit to the grinder and the delegator. Each award makes a share card.
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
38
44
|
**New in 1.17.1**
|
|
39
45
|
|
|
40
46
|
- Burn rate is now your pace over the last 30 minutes. It used to divide lifetime cost by process age, which read in the thousands per hour right after resuming an old session.
|
package/lib/collector.js
CHANGED
|
@@ -711,6 +711,33 @@ class Collector {
|
|
|
711
711
|
};
|
|
712
712
|
}
|
|
713
713
|
|
|
714
|
+
// Every scanned session, summarised for the Hall of Fame.
|
|
715
|
+
fameSessions() {
|
|
716
|
+
const pricing = readPricing();
|
|
717
|
+
const out = [];
|
|
718
|
+
for (const g of this.raw.transcriptGroups.values()) {
|
|
719
|
+
if (isIgnored(g.path)) continue;
|
|
720
|
+
for (const m of g.sessions) {
|
|
721
|
+
const ts = toolSummary(m.tools);
|
|
722
|
+
out.push({
|
|
723
|
+
sessionId: m.sessionId,
|
|
724
|
+
title: this.titleOf(m).title,
|
|
725
|
+
projectName: friendlyName(g.path),
|
|
726
|
+
projectPath: g.path,
|
|
727
|
+
model: m.model || null,
|
|
728
|
+
cost: Math.round(costReport(combinedUsage(m), pricing).usd * 100) / 100,
|
|
729
|
+
startedAt: m.startedAt || null,
|
|
730
|
+
lastActivityAt: m.lastActivityAt,
|
|
731
|
+
tools: ts ? ts.total : 0,
|
|
732
|
+
errors: ts ? ts.errors : 0,
|
|
733
|
+
subagents: m.subagentCount || 0,
|
|
734
|
+
files: m.changeCount || 0,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return out;
|
|
739
|
+
}
|
|
740
|
+
|
|
714
741
|
// Transcript file + title for one session, for the transcript viewer.
|
|
715
742
|
findSessionFile(sessionId) {
|
|
716
743
|
for (const g of this.raw.transcriptGroups.values()) {
|
package/lib/demo.js
CHANGED
|
@@ -503,5 +503,37 @@ function demoProject() {
|
|
|
503
503
|
};
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
-
|
|
506
|
+
|
|
507
|
+
// Hall of Fame: the same shape lib/fame.js returns, over invented sessions.
|
|
508
|
+
function demoFame() {
|
|
509
|
+
const now = Date.now();
|
|
510
|
+
const e = (sessionId, title, projectName, model, label, ago) => ({ sessionId, title, projectName, model, label, lastActivityAt: now - ago });
|
|
511
|
+
const mig = ['demo2222-2222-4222-8222-222222222222', 'Migrate posts to the new block format', 'Blog Engine', 'claude-fable-5'];
|
|
512
|
+
const cart = ['demo6666-6666-4666-8666-666666666666', 'Cart totals rounding bug', 'Acme Storefront', 'claude-fable-5'];
|
|
513
|
+
const split = ['demo7777-7777-4777-8777-777777777777', 'Split the product page into blocks', 'Acme Storefront', 'claude-opus-5'];
|
|
514
|
+
const rss = ['demo8888-8888-4888-8888-888888888888', 'Fix the RSS feed date format', 'Blog Engine', 'claude-sonnet-5'];
|
|
515
|
+
const deps = ['demo9999-9999-4999-8999-999999999999', 'Bump dependencies and re-run the suite', 'Acme Storefront', 'claude-haiku-4-5'];
|
|
516
|
+
const onb = ['demo3333-3333-4333-8333-333333333333', 'Design the onboarding flow', 'Acme Storefront', 'claude-opus-5'];
|
|
517
|
+
return {
|
|
518
|
+
days: 30, sessions: 23, commits: 41,
|
|
519
|
+
awards: [
|
|
520
|
+
{ key: 'shipped', title: 'Most commits shipped', blurb: 'commits made while the session was working', entries: [
|
|
521
|
+
e(...split, '11 commits', 26 * HOUR), e(...mig, '8 commits', MIN), e(...cart, '6 commits', 30 * HOUR)] },
|
|
522
|
+
{ key: 'value', title: 'Best value', blurb: 'lowest cost per commit, three commits or more', entries: [
|
|
523
|
+
e(...deps, '$0.82 per commit', 3 * DAY), e(...cart, '$1.84 per commit', 30 * HOUR), e(...rss, '$2.23 per commit', 2 * DAY)] },
|
|
524
|
+
{ key: 'grinder', title: 'The grinder', blurb: 'most tool calls in one session', entries: [
|
|
525
|
+
e(...mig, '388 tool calls', MIN), e(...split, '301 tool calls', 26 * HOUR), e(...onb, '164 tool calls', 3 * HOUR)] },
|
|
526
|
+
{ key: 'clean', title: 'Cleanest run', blurb: 'fewest failed tool calls, fifty calls or more', entries: [
|
|
527
|
+
e(...mig, '388 calls, 0 failed', MIN), e(...rss, '92 calls, 0 failed', 2 * DAY), e(...split, '301 calls, 2 failed', 26 * HOUR)] },
|
|
528
|
+
{ key: 'delegator', title: 'The delegator', blurb: 'most subagents handed work', entries: [
|
|
529
|
+
e(...mig, '4 subagents', MIN), e(...split, '3 subagents', 26 * HOUR)] },
|
|
530
|
+
{ key: 'reach', title: 'Widest reach', blurb: 'most files edited', entries: [
|
|
531
|
+
e(...split, '27 files edited', 26 * HOUR), e(...mig, '19 files edited', MIN), e(...onb, '9 files edited', 3 * HOUR)] },
|
|
532
|
+
{ key: 'spender', title: 'Big spender', blurb: 'highest list-price value', entries: [
|
|
533
|
+
e(...mig, '$61.20', MIN), e(...split, '$31.40', 26 * HOUR), e(...onb, '$18.20', 3 * HOUR)] },
|
|
534
|
+
],
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
module.exports = { demoState, demoStats, demoSession, demoChanges, demoInventory, demoToolset, demoProject, demoFame };
|
|
507
539
|
|
package/lib/fame.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Hall of Fame: your best sessions over a period, by award. Pure; the server
|
|
3
|
+
// feeds it session summaries plus `git log` commits per project.
|
|
4
|
+
|
|
5
|
+
const PAD = 5 * 60000; // same padded activity window as cost per commit
|
|
6
|
+
|
|
7
|
+
// sessionId -> commits made inside its window. A commit inside several
|
|
8
|
+
// sessions' windows goes to the one that started most recently, so a long
|
|
9
|
+
// resumed session doesn't claim every commit made while it was open.
|
|
10
|
+
function commitsBySession(sessions, commitsByProject) {
|
|
11
|
+
const out = new Map();
|
|
12
|
+
const byProject = new Map();
|
|
13
|
+
for (const s of sessions) {
|
|
14
|
+
if (!byProject.has(s.projectPath)) byProject.set(s.projectPath, []);
|
|
15
|
+
byProject.get(s.projectPath).push(s);
|
|
16
|
+
}
|
|
17
|
+
for (const [projectPath, list] of byProject) {
|
|
18
|
+
for (const c of commitsByProject[projectPath] || []) {
|
|
19
|
+
let best = null;
|
|
20
|
+
for (const s of list) {
|
|
21
|
+
if (!s.lastActivityAt) continue;
|
|
22
|
+
const start = (s.startedAt || s.lastActivityAt) - PAD;
|
|
23
|
+
if (c.ts < start || c.ts > s.lastActivityAt + PAD) continue;
|
|
24
|
+
if (!best || (s.startedAt || 0) > (best.startedAt || 0)) best = s;
|
|
25
|
+
}
|
|
26
|
+
if (best) out.set(best.sessionId, (out.get(best.sessionId) || 0) + 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const money = (n) => (n >= 100 ? `$${Math.round(n)}` : `$${n.toFixed(2)}`);
|
|
33
|
+
const plural = (n, one, many = one + 's') => `${n} ${n === 1 ? one : many}`;
|
|
34
|
+
|
|
35
|
+
// Each award: who qualifies, the number it ranks by, and how it reads.
|
|
36
|
+
const AWARDS = [
|
|
37
|
+
{
|
|
38
|
+
key: 'shipped', title: 'Most commits shipped', blurb: 'commits made while the session was working',
|
|
39
|
+
ok: (s) => s.commits >= 1, score: (s) => s.commits, label: (s) => plural(s.commits, 'commit'),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: 'value', title: 'Best value', blurb: 'lowest cost per commit, three commits or more',
|
|
43
|
+
ok: (s) => s.commits >= 3 && s.cost >= 0.5, score: (s) => -s.cost / s.commits,
|
|
44
|
+
label: (s) => `${money(s.cost / s.commits)} per commit`,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'grinder', title: 'The grinder', blurb: 'most tool calls in one session',
|
|
48
|
+
ok: (s) => s.tools >= 1, score: (s) => s.tools, label: (s) => plural(s.tools, 'tool call'),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: 'clean', title: 'Cleanest run', blurb: 'fewest failed tool calls, fifty calls or more',
|
|
52
|
+
ok: (s) => s.tools >= 50, score: (s) => -(s.errors / s.tools) + s.tools / 1e7,
|
|
53
|
+
label: (s) => `${s.tools} calls, ${s.errors} failed`,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: 'delegator', title: 'The delegator', blurb: 'most subagents handed work',
|
|
57
|
+
ok: (s) => s.subagents >= 1, score: (s) => s.subagents, label: (s) => plural(s.subagents, 'subagent'),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: 'reach', title: 'Widest reach', blurb: 'most files edited',
|
|
61
|
+
ok: (s) => s.files >= 1, score: (s) => s.files, label: (s) => plural(s.files, 'file') + ' edited',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: 'spender', title: 'Big spender', blurb: 'highest list-price value',
|
|
65
|
+
ok: (s) => s.cost >= 0.01, score: (s) => s.cost, label: (s) => money(s.cost),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// sessions: [{ sessionId, title, projectName, projectPath, model, cost,
|
|
70
|
+
// startedAt, lastActivityAt, tools, errors, subagents, files }]
|
|
71
|
+
function hallOfFame(sessions, commitsByProject, { days = 30, now = Date.now(), podium = 3 } = {}) {
|
|
72
|
+
const cut = days ? now - days * 86400000 : 0;
|
|
73
|
+
const inPeriod = (sessions || []).filter((s) => (s.lastActivityAt || 0) >= cut);
|
|
74
|
+
const counts = commitsBySession(inPeriod, commitsByProject || {});
|
|
75
|
+
const rows = inPeriod.map((s) => ({ ...s, commits: counts.get(s.sessionId) || 0 }));
|
|
76
|
+
const awards = [];
|
|
77
|
+
for (const a of AWARDS) {
|
|
78
|
+
const ranked = rows
|
|
79
|
+
.filter(a.ok)
|
|
80
|
+
.sort((x, y) => a.score(y) - a.score(x) || (y.lastActivityAt || 0) - (x.lastActivityAt || 0))
|
|
81
|
+
.slice(0, podium);
|
|
82
|
+
if (!ranked.length) continue;
|
|
83
|
+
awards.push({
|
|
84
|
+
key: a.key,
|
|
85
|
+
title: a.title,
|
|
86
|
+
blurb: a.blurb,
|
|
87
|
+
entries: ranked.map((s) => ({
|
|
88
|
+
sessionId: s.sessionId,
|
|
89
|
+
title: s.title,
|
|
90
|
+
projectName: s.projectName,
|
|
91
|
+
model: s.model || null,
|
|
92
|
+
label: a.label(s),
|
|
93
|
+
lastActivityAt: s.lastActivityAt,
|
|
94
|
+
})),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
let commits = 0;
|
|
98
|
+
for (const n of counts.values()) commits += n;
|
|
99
|
+
return { days, sessions: rows.length, commits, awards };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { hallOfFame, commitsBySession, AWARDS };
|
package/package.json
CHANGED