claude-mission-control 1.13.0 → 1.14.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 CHANGED
@@ -35,6 +35,10 @@ 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.14.0**
39
+
40
+ - Cost per commit in each project's slide-over: every commit made during a Claude session shows its share of that session's cost, with a summary of cost per commit and spend in sessions that committed nothing.
41
+
38
42
  **New in 1.13.0**
39
43
 
40
44
  - Background sessions from `claude --bg` and Agent View carry a `BG` chip, the name Agent View gives them, and its one-line summary of what they did.
@@ -194,6 +198,7 @@ If something looks off, the log is at `~/Library/Logs/claude-dashboard.log`. To
194
198
  ![Stats view: activity heatmap, busiest hours, weekly rhythm, 90-day spend, per-model costs, the most expensive sessions, and models by project](docs/screenshots/stats.png)
195
199
 
196
200
  - **Project details** — click any project's name for a slide-over with its full session list, recent git commits, rendered CLAUDE.md, per-project memory files, skills/agents/commands from `.claude/`, and settings (permissions, MCP servers, allowed tools), plus the resolved **Toolset** below. Read-only; also a quick audit of which projects are missing instructions or memory. Esc closes.
201
+ - **Cost per commit** — in each project's slide-over, every commit made during a Claude session carries a `≈$` figure: that session's cost split evenly over the commits it made (±5 minutes of its activity). Hover for which session it came from. The line above the list sums it up: cost per commit, how many of the recent commits came from a Claude session, and what was spent in sessions that committed nothing. It's an even split, so a one-line version bump counts the same as a feature.
197
202
  - **Toolset** — inside the project slide-over: what Claude Code would actually use in that project. Every installed plugin, every agent and skill from the project, your user dir, and enabled plugins, and every MCP server that applies, each marked on or off with where it came from and which settings scope decided it, resolved with Claude Code's own precedence (managed › local › project › user, deny beats allow, project agents shadow user agents shadow plugin agents). An amber list at the top flags things worth a look: a plugin enabled but not installed or gone stale, a deny rule naming an agent that no longer exists, a server that needs auth, a settings file that doesn't parse. The `⧉` on a row copies the exact snippet to flip it, aimed at `.claude/settings.local.json`; MCP toggles live in Claude's own registry, so that recipe tells you to run `/mcp` in a session instead (servers from a project's `.mcp.json` also honour the settings keys `enabledMcpjsonServers`, `disabledMcpjsonServers` and `enableAllProjectMcpServers`, and their recipes use those). Buttons at the foot reveal the project's settings files in Finder. Read-only — the dashboard never edits Claude configuration.
198
203
 
199
204
  ![Toolset in the project slide-over: plugins, agents, skills and MCP servers marked on or off with their source and deciding settings scope, an amber warning for a plugin enabled but not installed, and buttons for the project's settings files](docs/screenshots/toolset.png)
package/lib/demo.js CHANGED
@@ -463,5 +463,29 @@ function demoToolset() {
463
463
  };
464
464
  }
465
465
 
466
- module.exports = { demoState, demoStats, demoSession, demoChanges, demoInventory, demoToolset };
466
+ // Project slide-over for the demo: commits with their cost split from the
467
+ // sessions that made them (see commitCosts in lib/gitlog.js).
468
+ function demoProject() {
469
+ const now = Date.now();
470
+ const c = (sha, ago, subject, stat, cost, title, n) => ({
471
+ sha, ts: now - ago, subject, author: 'Demo', claude: cost != null, stat,
472
+ ...(cost != null ? { cost, costFrom: [{ sessionId: S1, title, commits: n }], sessionId: S1, sessionTitle: title } : {}),
473
+ });
474
+ return {
475
+ path: '/demo/acme-storefront', sessions: [], muted: false,
476
+ claudeMd: [], memory: [], skills: [], agents: [], commands: [], settings: {},
477
+ toolset: demoToolset(),
478
+ commits: [
479
+ c('9f3c2a1', 6 * MIN, 'Checkout: create the payment intent server-side', '3 files changed, 64 insertions(+), 9 deletions(-)', 8.2, 'Wire Stripe checkout into the cart', 3),
480
+ c('41be07d', 22 * MIN, 'Cart: call the checkout endpoint and redirect', '2 files changed, 31 insertions(+), 4 deletions(-)', 8.2, 'Wire Stripe checkout into the cart', 3),
481
+ c('c0d9e55', 41 * MIN, 'Remove the legacy checkout page', '1 file changed, 0 insertions(+), 212 deletions(-)', 8.2, 'Wire Stripe checkout into the cart', 3),
482
+ c('77a1f30', 3 * HOUR, 'Onboarding: account and store screens', '6 files changed, 240 insertions(+), 12 deletions(-)', 9.1, 'Design the onboarding flow', 2),
483
+ c('e52b8c4', 3 * HOUR + 40 * MIN, 'Onboarding: first-product screen', '3 files changed, 118 insertions(+), 3 deletions(-)', 9.1, 'Design the onboarding flow', 2),
484
+ c('1a6d9f2', 26 * HOUR, 'Bump dependencies', '2 files changed, 14 insertions(+), 14 deletions(-)', null),
485
+ ],
486
+ commitSummary: { total: 6, withSession: 5, perCommit: 8.56, noCommitSpend: 3.4 },
487
+ };
488
+ }
489
+
490
+ module.exports = { demoState, demoStats, demoSession, demoChanges, demoInventory, demoToolset, demoProject };
467
491
 
package/lib/gitlog.js CHANGED
@@ -44,6 +44,47 @@ function linkCommitsToSessions(commits, sessions) {
44
44
  });
45
45
  }
46
46
 
47
+ // Cost per commit: each session's cost is split evenly across the commits
48
+ // made inside its activity window (same padded window as above). A commit
49
+ // inside two overlapping sessions gets a share of both. Sessions that cost
50
+ // something but committed nothing, since the oldest listed commit, are
51
+ // reported separately rather than spread over unrelated commits.
52
+ const round2 = (n) => Math.round(n * 100) / 100;
53
+ function commitCosts(commits, sessions) {
54
+ const cost = new Map(); // sha -> usd
55
+ const from = new Map(); // sha -> [{ sessionId, title, commits }]
56
+ const oldest = commits.length ? Math.min(...commits.map((c) => c.ts)) : Infinity;
57
+ let attributed = 0;
58
+ let noCommit = 0;
59
+ for (const s of sessions || []) {
60
+ const usd = s.estCost || 0;
61
+ if (usd < 0.005 || !s.lastActivityAt) continue;
62
+ const start = (s.startedAt || s.lastActivityAt) - PAD;
63
+ const end = s.lastActivityAt + PAD;
64
+ const inside = commits.filter((c) => c.ts >= start && c.ts <= end);
65
+ if (!inside.length) {
66
+ if (s.lastActivityAt >= oldest) noCommit += usd;
67
+ continue;
68
+ }
69
+ attributed += usd;
70
+ for (const c of inside) {
71
+ cost.set(c.sha, (cost.get(c.sha) || 0) + usd / inside.length);
72
+ from.set(c.sha, [...(from.get(c.sha) || []), { sessionId: s.sessionId, title: s.title || null, commits: inside.length }]);
73
+ }
74
+ }
75
+ const out = commits.map((c) => (cost.has(c.sha) ? { ...c, cost: round2(cost.get(c.sha)), costFrom: from.get(c.sha) } : c));
76
+ const withSession = cost.size;
77
+ return {
78
+ commits: out,
79
+ summary: {
80
+ total: commits.length,
81
+ withSession,
82
+ perCommit: withSession ? round2(attributed / withSession) : null,
83
+ noCommitSpend: round2(noCommit),
84
+ },
85
+ };
86
+ }
87
+
47
88
  function recentCommits(projectPath, limit = 30) {
48
89
  return new Promise((resolve) => {
49
90
  execFile(
@@ -55,4 +96,4 @@ function recentCommits(projectPath, limit = 30) {
55
96
  });
56
97
  }
57
98
 
58
- module.exports = { parseGitLog, linkCommitsToSessions, recentCommits };
99
+ module.exports = { parseGitLog, linkCommitsToSessions, commitCosts, recentCommits };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mission-control",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Local dashboard for Claude Code: live sessions, transcripts, costs, git status — all your projects in one place.",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -1607,6 +1607,18 @@ function renderMd(raw) {
1607
1607
  return `<div class="md">${out.join('')}</div>`;
1608
1608
  }
1609
1609
 
1610
+ // "≈$3.40 per commit · 18 of 30 made during a Claude session · ≈$12 in sessions with no commit"
1611
+ function commitSummaryLine(s) {
1612
+ if (!s || !s.withSession) return '';
1613
+ const w = costWording();
1614
+ const parts = [
1615
+ `${costChip(s.perCommit, { cls: '' })} per commit`,
1616
+ `${s.withSession} of ${s.total} made during a Claude session`,
1617
+ fmtCost(s.noCommitSpend) ? `${costChip(s.noCommitSpend, { cls: '' })} in sessions with no commit` : '',
1618
+ ].filter(Boolean);
1619
+ return `<div class="set-sub" style="margin:0 0 8px" title="${esc(`Each session's ${w.noun} is split evenly over the commits made while it was active (±5 minutes). Commits outside any session are left out.`)}">${parts.join(' · ')}</div>`;
1620
+ }
1621
+
1610
1622
  function dSection(title, inner, empty) {
1611
1623
  return `<details class="d-section" open><summary>${esc(title)}</summary>${
1612
1624
  inner || `<div class="d-empty">${esc(empty)}</div>`}</details>`;
@@ -1697,6 +1709,8 @@ function renderDetail(d, name) {
1697
1709
  ${esc(c.subject)}
1698
1710
  </span>
1699
1711
  ${c.sessionId ? `<button class="copy" data-session="${esc(c.sessionId)}" title="${esc(c.sessionTitle || '')}">session →</button>` : ''}
1712
+ ${c.cost != null ? `<span title="${esc((c.costFrom || []).map((f) =>
1713
+ `${f.title || 'session'}: split over ${f.commits} commit${f.commits === 1 ? '' : 's'}`).join('\n'))}">${costChip(c.cost)}</span>` : ''}
1700
1714
  <span class="s-when">${esc(statCompact(c.stat))}</span>
1701
1715
  <span class="s-when">${relTime(c.ts)}</span>
1702
1716
  </li>`).join('');
@@ -1708,7 +1722,7 @@ function renderDetail(d, name) {
1708
1722
  $('#d-body').innerHTML =
1709
1723
  muteBtn +
1710
1724
  dSection(`Sessions — ${d.sessions.length}`, sessions, 'No sessions recorded.') +
1711
- dSection(`What changed — last ${(d.commits || []).length} commits`, commitRows ? `<ul class="d-sessions">${commitRows}</ul>` : null, 'No git history here.') +
1725
+ dSection(`What changed — last ${(d.commits || []).length} commits`, commitRows ? commitSummaryLine(d.commitSummary) + `<ul class="d-sessions">${commitRows}</ul>` : null, 'No git history here.') +
1712
1726
  dSection('CLAUDE.md', d.claudeMd.length ? dFiles(d.claudeMd, 'name') : null, 'No CLAUDE.md — sessions here start without project instructions.') +
1713
1727
  dSection(`Memory — ${d.memory.length} file${d.memory.length === 1 ? '' : 's'}`, d.memory.length ? dFiles(d.memory, 'name') : null, 'No memory yet.') +
1714
1728
  dSection(`Skills, agents & commands — ${caps.length}`, caps.length ? dFiles(caps, 'name') : null, 'None in .claude/skills, .claude/agents, or .claude/commands.') +
package/server.js CHANGED
@@ -15,9 +15,9 @@ const { sessionTitle } = require('./lib/transcripts');
15
15
  const { friendlyName } = require('./lib/names');
16
16
  const cfg = require('./lib/config');
17
17
  const { isProjectMuted } = require('./lib/notify');
18
- const { recentCommits, linkCommitsToSessions } = require('./lib/gitlog');
18
+ const { recentCommits, linkCommitsToSessions, commitCosts } = require('./lib/gitlog');
19
19
  const { sessionChangeList, sessionFileDiff } = require('./lib/changes');
20
- const { demoState, demoStats, demoSession, demoChanges, demoInventory, demoToolset } = require('./lib/demo');
20
+ const { demoState, demoStats, demoSession, demoChanges, demoInventory, demoProject } = require('./lib/demo');
21
21
  const { runSelfUpdate } = require('./lib/update');
22
22
  const { readInventory } = require('./lib/inventory');
23
23
  const { readToolset } = require('./lib/toolset');
@@ -133,13 +133,7 @@ const server = http.createServer((req, res) => {
133
133
  }
134
134
  if (url === '/api/search') return json(res, 200, { q: '', prompts: [], titles: [], notes: [], transcripts: null });
135
135
  if (url === '/api/reveal-file') return json(res, 200, { ok: true });
136
- if (url === '/api/project') {
137
- return json(res, 200, {
138
- path: '/demo/acme-storefront', sessions: [], commits: [], muted: false,
139
- claudeMd: [], memory: [], skills: [], agents: [], commands: [], settings: {},
140
- toolset: demoToolset(),
141
- });
142
- }
136
+ if (url === '/api/project') return json(res, 200, demoProject());
143
137
  if (url === '/api/config' && req.method === 'GET') {
144
138
  return json(res, 200, { demo: true, notifications: true, usageApi: false, weeklyBudget: 200, idleNagMinutes: 60, terminals: [], resolvedTerminal: { id: 'terminal', label: 'Terminal' }, claudeApp: false, names: {}, ignores: [], themes: cfg.THEMES, version: VERSION, errors: [], pricingSource: null, unpricedModels: [] });
145
139
  }
@@ -258,7 +252,9 @@ const server = http.createServer((req, res) => {
258
252
  Promise.all([projectDetail(known), recentCommits(known), readInventory(collector.projectPaths())])
259
253
  .then(async ([detail, commits, inventory]) => {
260
254
  detail.sessions = collector.allSessions(known);
261
- detail.commits = linkCommitsToSessions(commits, detail.sessions);
255
+ const costed = commitCosts(linkCommitsToSessions(commits, detail.sessions), detail.sessions);
256
+ detail.commits = costed.commits;
257
+ detail.commitSummary = costed.summary;
262
258
  detail.muted = isProjectMuted(known, cfg.readConfig().mutedProjects);
263
259
  detail.toolset = await readToolset(known, inventory);
264
260
  res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });