claude-mission-control 1.10.0 → 1.11.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
@@ -1,14 +1,52 @@
1
1
  # Claude Dashboard
2
2
 
3
- A local, zero-dependency dashboard for all your [Claude Code](https://claude.com/claude-code) projects. Live sessions pinned at the top — including whether one is waiting on your input — with a card per project below: recent sessions, git status, a 14-day activity sparkline, and one-click resume commands.
3
+ One web page that shows you everything Claude Code is doing on your machine.
4
4
 
5
- Everything is read from local files under `~/.claude`, and it never writes to Claude's data — read-only. No API keys to configure. The only automatic network call is to Anthropic's own usage API for your quota meters, authenticated with the Keychain credentials Claude Code already holds (kept in memory, toggleable in settings). Plain Node.js (which Claude Code already requires), zero npm dependencies. macOS first, Linux supported, Windows experimental.
5
+ Claude Code runs in a terminal. Once you have two or three sessions going, you lose track. Which one is waiting for an answer? What did that session change while you were away? How much has this week cost? Claude Dashboard answers those questions on one screen, updated live, from the files Claude Code already writes to `~/.claude`.
6
6
 
7
- **Requirements:** macOS or Linux (or Windows, experimental — see below), Node.js ≥ 18, Claude Code.
7
+ **What you get**
8
8
 
9
- ![Claude Dashboard, dark mode — live sessions, the pinned strip, unpushed work, and today's digest](docs/screenshots/dashboard-dark.png)
9
+ - A departures board of every running session. When Claude stops to ask you something, the row turns amber and shows the question, so you go straight to the right terminal.
10
+ - What each running session costs so far, how fast it is spending, and how full its context window is.
11
+ - A diff of every file Claude edited or created in a session, taken from the backups Claude Code keeps. Read what changed before you commit it.
12
+ - The full transcript of any session, past or present, and a search across every prompt you have ever sent.
13
+ - Cost per session, project, model, and week, estimated from the tokens in your transcripts, with a breakdown by model and cache use.
14
+ - Per project, the plugins, agents, skills, and MCP servers Claude Code would actually use there, and the exact setting to switch each one on or off.
15
+ - A macOS notification the moment a session needs you, and a reminder if you leave it waiting.
10
16
 
11
- *Every screenshot here is demo data (`CLAUDE_DASH_DEMO=1`) — the projects are invented.*
17
+ **Why you would want it**
18
+
19
+ - You stop checking terminals. The board and the notifications tell you when to look.
20
+ - You stop losing sessions. Every session has a resume button that reopens it in the right folder.
21
+ - You see where the time and the money go, per project.
22
+ - It is read-only. It never writes to Claude's files. Nothing leaves your Mac except one optional call to Anthropic's usage API for your quota meters, using the sign-in Claude Code already has.
23
+ - Nothing to configure. No API keys. Plain Node.js, zero npm dependencies, bound to `127.0.0.1` only.
24
+
25
+ **Who it is for:** anyone who runs Claude Code in more than one terminal.
26
+
27
+ **Requirements:** macOS or Linux (Windows experimental, see below), Node.js ≥ 18, Claude Code.
28
+
29
+ ![Claude Dashboard, dark mode: three live sessions, two of them waiting with the question shown, plus the pinned strip, unpushed work, and today's digest](docs/screenshots/dashboard-dark.png)
30
+
31
+ *Every screenshot here is demo data (`CLAUDE_DASH_DEMO=1`). The projects are invented.*
32
+
33
+ **New in 1.11.0**
34
+
35
+ - Live cost and a `$/h` burn rate on every running session, and a cost breakdown by model, cache use, and subagents in each transcript.
36
+ - A context meter on every running session, amber from 70% full and red from 90%, plus tool-call and failure counts in Mission Control.
37
+ - A Toolset view in each project's slide-over: which plugins, agents, skills, and MCP servers apply there, which settings file decided each one, and the snippet to flip it.
38
+ - Current list prices for every Claude model, your organization's contracted rates when Claude Code's `modelPricing` is set, and an amber `?` on any model the table does not price.
39
+ - The ten most expensive sessions in stats, and your cache hit rate in the weekly report.
40
+ - Cost labels that match your plan: *estimated cost* on API billing, *list-price value* on a subscription.
41
+
42
+ **New in 1.10.0**
43
+
44
+ - The board says what a waiting session needs: the question, the command awaiting permission, or Claude's last line.
45
+ - `Δ n` on every session row, and a changes panel with a diff of every file the session touched.
46
+ - Notes on any session, searchable.
47
+ - A Plugins & MCP inventory: what is installed, enabled, stale, disabled, or needing auth.
48
+ - Agent teams and subagents shown live in Mission Control.
49
+ - An idle reminder when a session has waited on you for an hour.
12
50
 
13
51
  ## Try it in one command
14
52
 
@@ -68,34 +106,37 @@ You should see a dark board listing your projects. If a Claude Code session is r
68
106
  **Using it day to day** — three things cover most of it:
69
107
 
70
108
  - **Glance at the top row.** That's what's running right now. Amber **needs you** means go back to that terminal — Claude asked a question.
71
- - **Click any session title** to read the whole conversation. If it's still running, new messages stream in live.
109
+ - **Click any session title** to read the whole conversation. If it's still running, new messages stream in live. The `≈$ ▾` button in its header shows what it cost, by model.
72
110
  - **Click `open ⬈`** next to an old session to pick it up again — it opens a terminal in the right folder with the conversation restored.
73
111
 
74
112
  If something looks off, the log is at `~/Library/Logs/claude-dashboard.log`. To remove everything, run `./uninstall.sh` from the same folder.
75
113
 
76
114
  ## What you're looking at
77
115
 
78
- - **Departures** — every live `claude` CLI session as a row on the board: start time, project, current task, model, elapsed time, and a split-flap status cell. The cell flips to an amber **needs you** when a session is waiting on your input — readable from across the room. A second line under the title says what it needs: `?` for a question Claude asked (with its options), `🔒` for a tool call awaiting permission (with the command or file), or the last question in Claude's reply, or failing that the first line of its final paragraph.
79
- - **Mission Control** — the `▦` button in the header (or `⌘K` → Mission Control) tiles every live session side by side, each pane following its own transcript. One screen for "what is everything doing right now".
116
+ - **Departures** — every live `claude` CLI session as a row on the board: start time, project, current task, model, cost so far with a `$/h` burn rate, a context-window meter, elapsed time, and a split-flap status cell. The cell flips to an amber **needs you** when a session is waiting on your input — readable from across the room. A second line under the title says what it needs: `?` for a question Claude asked (with its options), `🔒` for a tool call awaiting permission (with the command or file), or the last question in Claude's reply, or failing that the first line of its final paragraph.
117
+ - **Mission Control** — the `▦` button in the header (or `⌘K` → Mission Control) tiles every live session side by side, each pane following its own transcript. Each pane head shows the model, cost and burn rate, the context meter, and how many tool calls the session has made (failures in amber). One screen for "what is everything doing right now".
80
118
  - **Agents** — a session running subagents or an agent team shows them under its Mission Control pane: name, type, model, and a pulsing `●` while an agent has written in the last two minutes, `○` once it has gone quiet (idle teammates stay alive, so quiet is not finished), with the team name when there is one. Finished sessions list them at the top of the transcript.
81
119
  - **Pinned** — `☆ pin` on any transcript sticks that session to a strip under the departures board, so the thing you care about stays in view whether it's running or not.
82
120
  - **Digest** — what happened across every project, grouped by day. Each entry shows the session's recap (Claude's own "away summary" where one exists — click to expand), how many tasks it completed, and an open button. Switch the window between day / 3 days / week; collapses out of the way and remembers your choice.
83
121
  - **Project cards** — sorted by last activity. Branch chip, `●n` uncommitted changes, `↑n` unpushed commits. The sparkline is prompts per day for the last two weeks. Each session row has an `open ⬈` button that resumes the session in your terminal (new window, right directory) or imports it into the Claude desktop app when it's installed — the **Open in** selector in the header names whichever terminal you've configured (auto-detected on first run). The `⧉` button copies the `claude --resume` command instead.
84
- - **Header meters** — your plan and rate-limit tier (Free/Pro/Max/Enterprise/API, detected from Claude Code's local account cache), live session and weekly usage from Anthropic's usage API via your existing Claude Code sign-in, plus extra-usage spend. Set a **weekly budget** in settings and a third meter tracks estimated spend against it, with one alert each at 75 / 90 / 100%. First run may show one macOS Keychain prompt — click Always Allow.
122
+ - **Header meters** — your plan and rate-limit tier (Free/Pro/Max/Enterprise/API, detected from Claude Code's local account cache), live session and weekly usage from Anthropic's usage API via your existing Claude Code sign-in, plus extra-usage spend. Set a **weekly budget** in settings and a third meter tracks estimated cost (list-price value on a subscription) against it, with one alert each at 75 / 90 / 100%. First run may show one macOS Keychain prompt — click Always Allow.
85
123
 
86
- ![Mission Control — every live session tiled side by side, each pane following its own transcript](docs/screenshots/mission-control.png)
124
+ ![Mission Control: every live session tiled side by side, each pane following its own transcript, with the waiting reason at the top of each pane and the agent team under the middle one](docs/screenshots/mission-control.png)
87
125
 
88
- ![Transcript viewer live-following a running session — prompts, replies as markdown, tool calls as one-liners](docs/screenshots/transcript.png)
126
+ ![Transcript viewer live-following a running session: the agent team at the top, then prompts, replies as markdown, and tool calls as one-liners](docs/screenshots/transcript.png)
89
127
 
90
128
  - **Transcripts** — click any session title (cards, digest, search results, project drawer) to read the conversation: your prompts, Claude's replies rendered as markdown, tool calls as compact one-liners, and away-summaries highlighted. Long sessions show the newest ~1200 events. **Running sessions follow live** — a `● live` badge appears, new turns stream in every few seconds, and the view sticks to the bottom unless you've scrolled up to read.
91
129
  - **Search** — the header box searches every prompt you've ever sent plus all session titles (Enter to run, 2+ characters). Narrow with `project:name` or `since:7d` / `since:2026-08-01`. Clicking a prompt result opens the transcript scrolled to the matching turn. If the answer isn't in a prompt, **search inside full transcripts** runs the same query across the conversation bodies — slower, so it's a second click rather than the default.
92
130
  - **Command palette** — `⌘K` from anywhere: fuzzy-jump to any project or session, watch a live session, start a new one, open settings or stats. Arrow keys + Enter.
93
- - **Stats** — click the weekly bar chart in the header: a 26-week activity heatmap, your busiest hours, a weekly-rhythm grid (prompts by day of week and hour), estimated spend per day for the last 90 days, and an all-time per-model token/cost breakdown. The daily spend history is computed from your existing transcripts, so it's full from the first run.
94
- - **Your week with Claude** — a 7-day report (`⌘K`, or the button at the top of stats): sessions, prompts, estimated spend against your budget, busiest day and hour, models used, and how long you typically take to answer a question. Exports as markdown.
131
+ - **Stats** — click the weekly bar chart in the header: a 26-week activity heatmap, your busiest hours, a weekly-rhythm grid (prompts by day of week and hour), estimated cost per day for the last 90 days (list-price value on a subscription), and an all-time per-model token/cost breakdown. The daily cost history is computed from your existing transcripts, so it's full from the first run.
132
+ - **Your week with Claude** — a 7-day report (`⌘K`, or the button at the top of stats): sessions, prompts, estimated cost (list-price value on a subscription) against your budget, busiest day and hour, models used, how long you typically take to answer a question, and your cache hit rate. Exports as markdown.
95
133
  - **Day timeline** — `⌘K` → Day timeline: the last 24 hours as one lane per project, with amber ticks where a session needed you.
96
134
  - **Catch-up** — the `🔔` in the header keeps every notification the server has fired since it started, with an unread count. Useful after a morning away from the desk.
97
135
  - **Export** — any transcript downloads as clean markdown via the `export ⇩` button.
98
136
  - **Changes** — a `Δ n` chip on any session row counts the files Claude edited. Click it (or `changes` in the transcript header) for the list with `+added −removed` per file and a unified diff of what the session changed, from the pre-edit backup Claude Code keeps to the file as it is on disk now. Files with several checkpoints also offer each step (`v1 → v2`). Read-only: nothing here reverts anything.
137
+
138
+ ![The changes panel: three files the session touched, the first expanded to a unified diff from the pre-edit backup to disk](docs/screenshots/changes.png)
139
+
99
140
  - **Notes** — `note` in the transcript header attaches a free-text note to a session (⌘⏎ saves, empty removes). Rows with a note show `≡`, hover to read it, and notes are searchable from the header box. Stored in `config.json` under `sessionNotes`.
100
141
  - **New session** — the `⊕` button on a project card opens a fresh terminal window in that project running `claude`.
101
142
  - **Cost trend** — the small bar chart in the header is estimated cost per week for the last 8 weeks (hover for numbers). Costs include subagent tokens.
@@ -104,13 +145,21 @@ If something looks off, the log is at `~/Library/Logs/claude-dashboard.log`. To
104
145
 
105
146
  ![Stats view: activity heatmap, busiest hours, weekly rhythm, 90-day spend, and per-model cost breakdowns](docs/screenshots/stats.png)
106
147
 
107
- - **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). Read-only; also a quick audit of which projects are missing instructions or memory. Esc closes.
148
+ - **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.
149
+ - **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.
108
150
  - **Plugins & MCP** — `⌘K` → Plugins & MCP (or the button in settings) lists every installed plugin with its marketplace, version, on/off state and a `stale` flag when its marketplace was refreshed after the plugin was last updated, plus every MCP server with its scope, the projects that use it, an `off` chip for servers you have disabled in a project, and an amber `needs auth` when Claude Code says so. Read-only; server secrets never reach the browser.
151
+
152
+ ![Plugins & MCP inventory: plugins with on, off, stale and missing states; MCP servers with scope, projects, disabled flags and needs-auth](docs/screenshots/inventory.png)
153
+
109
154
  - **Unpushed work strip** — an amber band listing every repo with uncommitted changes (`●n`) or unpushed commits (`↑n`), sorted by recent activity. Disappears when everything's clean.
110
155
  - **Dormant** — projects with no activity for 60+ days, tucked away at the bottom.
111
156
  - Worktree sessions (`.claude/worktrees/…`) are folded into their parent project and badged `⎇`.
112
157
  - **Notifications** — the moment any session flips to "waiting for input", you get a macOS notification (with sound) naming the project and what it needs — the question, the command awaiting permission, or Claude's last line. Fires once per wait, never on server restart. Mute a single noisy project from its slide-over (the bell button at the top), or disable everything with `CLAUDE_DASH_NOTIFY=0` in the plist. Notifications arrive via Script Editor/osascript — if you don't see them, allow it under System Settings → Notifications.
113
- - **Cost estimates** — the header shows the estimated list-price value of the last 7 days across all projects; each project card and digest entry shows its share. Computed from token usage in the transcripts at Anthropic list rates (cache reads at 0.1×, cache writes at 1.25×). On a subscription plan these are relative weights, not billed dollars — use them to see where your usage goes. Subagent tokens are included.
158
+ - **Cost estimates** — every session, project, and live row carries a `≈$` figure computed from the token counts in its transcript. On an API-key login it is labelled *estimated cost*; on a subscription it is *list-price value*, a relative weight rather than a bill (hover a cost chip for the wording). Rates are Anthropic list prices bundled with the dashboard (cached 2026-10-06). If your organization deploys Claude Code's `modelPricing` managed setting, the dashboard reads it and reports at your contracted rates; you can also put the same `modelPricing` block in `config.json` (⚙ settings → *copy skeleton*). A model the table doesn't know is priced at the Opus tier and marked with an amber `?` so a stale table is never silent. Subagent tokens are included.
159
+ - **Live cost** — running sessions show their cost so far on the departures board and in Mission Control, with a `$/h` burn rate once the session is five minutes old.
160
+ - **Session cost breakdown** — the `≈$ ▾` button in a transcript's header opens a table by model (input, output, cache read, cache write), the subagent share, and the session's cache hit rate.
161
+ - **Context and tools** — each live session shows how full its context window was on the last turn (`412k / 1M`, amber from 70%, red from 90%), so you can see a compaction or a fresh session coming. Mission Control panes add the session's tool-call count with failures in amber, and the transcript's cost panel shows the same count in its footer. Read from the same incremental transcript scan as everything else; subagents are not included.
162
+ - **Most expensive sessions** — in the stats view: the ten costliest sessions all time, click to open.
114
163
  - **Stuck flag** — a session that's "busy" but has written nothing to its transcript for 10+ minutes gets an amber `quiet Nm` cell; at 20 minutes you get one notification. It's a hint, not a verdict — a session waiting on slow background work can look the same.
115
164
  - **Idle reminder** — a session left on **needs you** for an hour (configurable in settings, `0` to turn off) gets one more notification and a `still waiting` entry in the catch-up bell, and its elapsed cell turns amber with the wait time. Nothing repeats until you answer it.
116
165
 
@@ -136,6 +185,7 @@ The ⚙ gear in the header opens settings — no JSON editing required:
136
185
  - **Rename any project** (writes `names.json`) or **hide it** and its whole subtree (writes `ignore.json`), with an unhide list below
137
186
  - **Claude.ai chats**: pick the `conversations.json` from your claude.ai export to make those chats searchable here
138
187
  - **Updates**: "check for updates" asks GitHub only when you click; when a new release is out, **update now** pulls it in place (git or npm installs) and service installs restart themselves on the new version
188
+ - **Pricing** shows which rate table is active (bundled, `config.json`, or your organization's managed `modelPricing`) and lists any model the table doesn't price; *copy skeleton* puts a `modelPricing` block on the clipboard to paste into `config.json`
139
189
 
140
190
  Everything saves instantly; the underlying files stay hand-editable. Keyboard: `⌘K` for the palette, `/` for search, `Esc` closes anything.
141
191
 
@@ -174,6 +224,21 @@ Edit `ignore.json` — an array of absolute path prefixes. A project is hidden i
174
224
 
175
225
  The server binds to `127.0.0.1` only by default.
176
226
 
227
+ **Your own rates.** To price usage at rates other than Anthropic's list prices, add a `modelPricing` block to `config.json`. It uses the same shape as Claude Code's managed setting: a `multiplier` that scales every cost, and per-model `overrides` in USD per million tokens. ⚙ settings → **Pricing** → *copy skeleton* puts a starting block on your clipboard:
228
+
229
+ ```json
230
+ {
231
+ "modelPricing": {
232
+ "multiplier": 1,
233
+ "overrides": {
234
+ "claude-opus-5-5": { "input": 4, "output": 20, "cacheRead": 0.2, "cacheWrite": 5 }
235
+ }
236
+ }
237
+ }
238
+ ```
239
+
240
+ If your organization deploys `modelPricing` in Claude Code's managed settings, the dashboard reads that instead, and ⚙ settings says which source is active.
241
+
177
242
  ## Windows (experimental — testers wanted)
178
243
 
179
244
  The core is plain cross-platform Node, and Windows support is wired in: paths handle drive letters, notifications use native toasts, and sessions open in Windows Terminal, PowerShell, or cmd (auto-detected). Install from PowerShell in the repo folder:
@@ -188,7 +253,7 @@ That registers a logon Scheduled Task ("ClaudeDashboard") running the server hid
188
253
 
189
254
  ## Access from your phone
190
255
 
191
- The recommended path is [Tailscale](https://tailscale.com): install it on the Mac and your phone, then run `tailscale serve --bg 4517`. That publishes the dashboard over HTTPS inside your private tailnet while the server itself stays loopback-only — nothing is exposed to the internet or your LAN. Alternatively set `CLAUDE_DASH_HOST=0.0.0.0` in the plist to bind to all interfaces, but understand what that means: anyone on the same network can view the dashboard **and use the open/new-session endpoints, which launch terminal commands on this Mac**. Don't do that on a network you don't fully control.
256
+ The recommended path is [Tailscale](https://tailscale.com): install it on the Mac and your phone, then run `tailscale serve --bg 4517`. That publishes the dashboard over HTTPS inside your private tailnet while the server itself stays loopback-only — nothing is exposed to the internet or your LAN. Alternatively set `CLAUDE_DASH_HOST=0.0.0.0` in the plist to bind to all interfaces, but understand what that means: anyone on the same network can view the dashboard **and use the open/new-session endpoints, which launch terminal commands on this Mac**, plus the reveal-file endpoint, which opens Finder on a project's settings files. Don't do that on a network you don't fully control.
192
257
 
193
258
  ## Maintenance
194
259
 
@@ -214,6 +279,11 @@ Every `/api/*` route serves invented data from `lib/demo.js` instead of reading
214
279
  | Activity | `~/.claude/history.jsonl` |
215
280
  | Session todos | `~/.claude/tasks/<sessionId>/` |
216
281
  | Usage meters | Anthropic usage API via Claude Code's Keychain sign-in (fallback: `~/.claude/.statusline-usage-cache`) |
282
+ | Cost, context, tool counts | Token usage, tool calls, and failed tool results in the same transcripts, read incrementally (only bytes added since the last scan) |
283
+ | Pricing overrides | `modelPricing` in Claude Code's managed settings (`/Library/Application Support/ClaudeCode/managed-settings.json` on macOS), then `config.json` |
284
+ | File changes | `~/.claude/file-history/<sessionId>/` (Claude Code's pre-edit backups) |
285
+ | Plugins & MCP | `~/.claude/plugins/installed_plugins.json`, `known_marketplaces.json`, `~/.claude.json`, each project's `.mcp.json` |
286
+ | Toolset | `~/.claude/settings.json`, each project's `.claude/settings.json` and `.claude/settings.local.json`, managed settings, `~/.claude/agents` and `~/.claude/skills`, the project's `.claude/agents` and `.claude/skills`, and each enabled plugin's `agents/` and `skills/` |
217
287
 
218
288
  ## Credits
219
289
 
package/lib/collector.js CHANGED
@@ -7,14 +7,14 @@ const { readQuota } = require('./quota');
7
7
  const { fetchOauthUsage } = require('./usage');
8
8
  const { readRegistry } = require('./registry');
9
9
  const { refreshHistory, activityBuckets, weekHourHeat } = require('./history');
10
- const { scanAllTranscripts, scanFile, groupByProject, sessionTitle, combinedUsage, combinedDays, dailyCostSeries, subagentSummary, dayKey, reasonText } = require('./transcripts');
10
+ const { scanAllTranscripts, scanFile, groupByProject, sessionTitle, combinedUsage, combinedDays, dailyCostSeries, subagentSummary, usageBreakdown, dayKey, reasonText, contextSummary, toolSummary } = require('./transcripts');
11
11
  const { collectGitStatus } = require('./gitstatus');
12
12
  const { friendlyName } = require('./names');
13
13
  const { isIgnored } = require('./ignore');
14
14
  const { worktreeRoot } = require('./paths');
15
15
  const { sendNotification, newlyWaiting, overdueWaits, isProjectMuted, trackWaits } = require('./notify');
16
- const { readConfig } = require('./config');
17
- const { estimateCost, budgetLevel, typicalWait } = require('./pricing');
16
+ const { readConfig, readPricing } = require('./config');
17
+ const { estimateCost, costReport, budgetLevel, typicalWait, burnRate, topByCost } = require('./pricing');
18
18
  const { readPlan } = require('./plan');
19
19
  const { readTeams, agentStatus, resolveTeam } = require('./teams');
20
20
 
@@ -225,6 +225,7 @@ class Collector {
225
225
  assemble() {
226
226
  const registry = readRegistry();
227
227
  const cfg = readConfig(); // read once, not per session
228
+ const pricing = readPricing(); // one table per assemble, threaded into every cost
228
229
  const sessionNames = cfg.sessionNames || {};
229
230
  const sessionNotes = cfg.sessionNotes || {};
230
231
  const liveByProject = new Set(
@@ -259,14 +260,15 @@ class Collector {
259
260
  git,
260
261
  spend7d: sessionMetas.reduce(
261
262
  (sum, m) =>
262
- Date.now() - m.lastActivityAt < SPEND_WINDOW_MS ? sum + estimateCost(combinedUsage(m)) : sum,
263
+ Date.now() - m.lastActivityAt < SPEND_WINDOW_MS ? sum + estimateCost(combinedUsage(m), pricing) : sum,
263
264
  0
264
265
  ),
265
266
  sessions: sessionMetas.slice(0, SESSIONS_PER_PROJECT).map((m) => {
266
267
  const { title, source } = sessionTitle(m, sessionNames);
267
268
  return {
268
269
  sessionId: m.sessionId,
269
- estCost: estimateCost(combinedUsage(m)),
270
+ estCost: estimateCost(combinedUsage(m), pricing),
271
+ assumed: costReport(combinedUsage(m), pricing).assumedModels.length > 0,
270
272
  model: m.model,
271
273
  title,
272
274
  titleSource: source,
@@ -295,10 +297,17 @@ class Collector {
295
297
  const titleBySession = new Map();
296
298
  const modelBySession = new Map();
297
299
  const subsBySession = new Map();
300
+ const usageBySession = new Map();
301
+ const contextBySession = new Map();
302
+ const toolsBySession = new Map();
298
303
  for (const g of this.raw.transcriptGroups.values()) {
299
304
  for (const m of g.sessions) {
300
305
  titleBySession.set(m.sessionId, sessionTitle(m, sessionNames).title);
301
306
  if (m.model) modelBySession.set(m.sessionId, m.model);
307
+ usageBySession.set(m.sessionId, combinedUsage(m));
308
+ contextBySession.set(m.sessionId, contextSummary(m.context));
309
+ const ts = toolSummary(m.tools);
310
+ toolsBySession.set(m.sessionId, ts ? { total: ts.total, errors: ts.errors } : null);
302
311
  const subs = subagentSummary(m);
303
312
  if (subs) subsBySession.set(m.sessionId, subs);
304
313
  }
@@ -338,6 +347,10 @@ class Collector {
338
347
  }
339
348
  }
340
349
  if (quietMin === null) this.stuckNotified.delete(s.sessionId);
350
+ // Rounded before entering state so the fingerprint only moves when a
351
+ // displayed digit would. Null until the transcript has been scanned.
352
+ const report = costReport(usageBySession.get(s.sessionId) || {}, pricing);
353
+ const estCost = report.usd >= 0.005 ? Math.round(report.usd * 100) / 100 : null;
341
354
  return {
342
355
  quietMin,
343
356
  waitingMin: (() => {
@@ -354,6 +367,11 @@ class Collector {
354
367
  isWorktree: worktree !== null,
355
368
  name: s.name,
356
369
  model: modelBySession.get(s.sessionId) || null,
370
+ estCost,
371
+ burnRate: burnRate(estCost, s.startedAt),
372
+ assumed: report.assumedModels.length > 0,
373
+ context: contextBySession.get(s.sessionId) || null,
374
+ tools: toolsBySession.get(s.sessionId) || null,
357
375
  title: titleBySession.get(s.sessionId) || s.name,
358
376
  status: s.status,
359
377
  waitingFor: s.waitingFor,
@@ -402,7 +420,7 @@ class Collector {
402
420
  if (isIgnored(g.path)) continue;
403
421
  for (const m of g.sessions) {
404
422
  const idx = 7 - Math.floor((windowEnd - 1 - m.lastActivityAt) / (7 * DAY));
405
- if (idx >= 0 && idx < 8) weeklyCost[idx] += estimateCost(combinedUsage(m));
423
+ if (idx >= 0 && idx < 8) weeklyCost[idx] += estimateCost(combinedUsage(m), pricing);
406
424
  }
407
425
  }
408
426
  for (let i = 0; i < 8; i++) weeklyCost[i] = Math.round(weeklyCost[i] * 100) / 100;
@@ -438,13 +456,15 @@ class Collector {
438
456
  const g = this.raw.transcriptGroups.get(projectPath.toLowerCase());
439
457
  if (!g) return [];
440
458
  const notes = readConfig().sessionNotes || {};
459
+ const pricing = readPricing();
441
460
  return g.sessions.map((m) => {
442
461
  const { title, source } = this.titleOf(m);
443
462
  return {
444
463
  sessionId: m.sessionId,
445
464
  title,
446
465
  titleSource: source,
447
- estCost: estimateCost(combinedUsage(m)),
466
+ estCost: estimateCost(combinedUsage(m), pricing),
467
+ assumed: costReport(combinedUsage(m), pricing).assumedModels.length > 0,
448
468
  awaySummary: m.awaySummary,
449
469
  note: notes[m.sessionId] || null,
450
470
  changeCount: m.changeCount || 0,
@@ -456,9 +476,21 @@ class Collector {
456
476
  });
457
477
  }
458
478
 
479
+ // Distinct model ids that fell through to the fallback rate, for the
480
+ // settings diagnostics: a non-empty list means the rate table is stale.
481
+ unpricedModels() {
482
+ const pricing = readPricing();
483
+ const out = new Set();
484
+ for (const m of this.raw.metaBySession.values()) {
485
+ for (const id of costReport(combinedUsage(m), pricing).assumedModels) out.add(id);
486
+ }
487
+ return [...out].sort();
488
+ }
489
+
459
490
  // Aggregates for the stats view: daily activity, hour histogram,
460
491
  // per-model usage, totals. Ignored projects excluded throughout.
461
492
  statsSummary() {
493
+ const pricing = readPricing();
462
494
  const DAY = 86400000;
463
495
  const perDay = new Map();
464
496
  const hours = new Array(24).fill(0);
@@ -506,6 +538,9 @@ class Collector {
506
538
  const weekProjects = new Map();
507
539
  const weekModels = new Set();
508
540
  const weekWaits = [0, 0, 0, 0];
541
+ const topCandidates = [];
542
+ let weekCacheRead = 0;
543
+ let weekCacheAll = 0;
509
544
  for (const g of this.raw.transcriptGroups.values()) {
510
545
  if (isIgnored(g.path)) continue;
511
546
  sessionTotal += g.sessions.length;
@@ -528,21 +563,36 @@ class Collector {
528
563
  let mWeekCost = 0;
529
564
  for (const [day, byM] of Object.entries(mDays)) {
530
565
  if (day >= weekDayKey) {
531
- mWeekCost += estimateCost(byM);
566
+ mWeekCost += estimateCost(byM, pricing);
532
567
  for (const model of Object.keys(byM)) weekModels.add(model);
568
+ for (const u of Object.values(byM)) {
569
+ weekCacheRead += u.cacheRead || 0;
570
+ weekCacheAll += (u.input || 0) + (u.cacheRead || 0) + (u.cacheCreation || 0);
571
+ }
533
572
  }
534
573
  }
535
574
  weekCost += mWeekCost;
536
575
  const pn = friendlyName(g.path);
537
576
  weekProjects.set(pn, (weekProjects.get(pn) || 0) + mWeekCost);
538
577
  }
578
+ let mCost = 0;
539
579
  for (const [model, u] of Object.entries(combinedUsage(m))) {
540
580
  const e = byModel[model] || (byModel[model] = { tokens: 0, cost: 0 });
541
- const cost = estimateCost({ [model]: u });
581
+ const cost = estimateCost({ [model]: u }, pricing);
542
582
  e.tokens += (u.input || 0) + (u.output || 0) + (u.cacheRead || 0) + (u.cacheCreation || 0);
543
583
  e.cost += cost;
584
+ mCost += cost;
544
585
  projModels[model] = (projModels[model] || 0) + cost;
545
586
  }
587
+ topCandidates.push({
588
+ sessionId: m.sessionId,
589
+ title: this.titleOf(m).title,
590
+ projectName: friendlyName(g.path),
591
+ projectPath: g.path,
592
+ model: m.model || null,
593
+ cost: Math.round(mCost * 100) / 100,
594
+ lastActivityAt: m.lastActivityAt,
595
+ });
546
596
  }
547
597
  const projTotal = Object.values(projModels).reduce((a, b) => a + b, 0);
548
598
  if (projTotal >= 0.01) {
@@ -555,7 +605,7 @@ class Collector {
555
605
  days,
556
606
  hours,
557
607
  heat: weekHourHeat(allTimestamps),
558
- costDays: dailyCostSeries(sessionDays, 90),
608
+ costDays: dailyCostSeries(sessionDays, 90, Date.now(), pricing),
559
609
  timeline,
560
610
  week: {
561
611
  sessions: weekSessions,
@@ -571,9 +621,11 @@ class Collector {
571
621
  busiestHour: weekHours.some((c) => c) ? weekHours.indexOf(Math.max(...weekHours)) : null,
572
622
  waits: weekWaits,
573
623
  typicalWait: typicalWait(weekWaits),
624
+ cacheHitRatio: weekCacheAll ? Math.round((weekCacheRead / weekCacheAll) * 100) / 100 : null,
574
625
  },
575
626
  byModel,
576
627
  perProject: perProject.slice(0, 15),
628
+ topSessions: topByCost(topCandidates, 10),
577
629
  totals: { sessions: sessionTotal, prompts: promptTotal, cost: Math.round(costTotal * 100) / 100 },
578
630
  };
579
631
  }
@@ -583,7 +635,7 @@ class Collector {
583
635
  for (const g of this.raw.transcriptGroups.values()) {
584
636
  for (const m of g.sessions) {
585
637
  if (m.sessionId === sessionId) {
586
- return { file: m.file, title: this.titleOf(m).title, projectName: friendlyName(g.path), note: (readConfig().sessionNotes || {})[m.sessionId] || null, agents: this.agentsFor(m, this.raw.live.some((s) => s.sessionId === sessionId)) };
638
+ return { file: m.file, title: this.titleOf(m).title, projectName: friendlyName(g.path), note: (readConfig().sessionNotes || {})[m.sessionId] || null, agents: this.agentsFor(m, this.raw.live.some((s) => s.sessionId === sessionId)), usage: usageBreakdown(m, readPricing()) };
587
639
  }
588
640
  }
589
641
  }
package/lib/config.js CHANGED
@@ -4,7 +4,8 @@
4
4
  // pretty-printed so hand-editing stays pleasant.
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { canonicalize } = require('./paths');
7
+ const { canonicalize, managedSettingsPath } = require('./paths');
8
+ const { loadPricing } = require('./pricing');
8
9
 
9
10
  const ROOT = path.join(__dirname, '..');
10
11
 
@@ -62,6 +63,14 @@ function readConfig() {
62
63
  return { ...DEFAULTS, ...readJson(CONFIG_FILE, {}) };
63
64
  }
64
65
 
66
+ // Rate table: Claude Code's managed `modelPricing` first (contracted rates
67
+ // an admin deployed), then the same key in our config.json, then bundled
68
+ // list prices. Both files are small; read fresh every call like readConfig.
69
+ function readPricing() {
70
+ const managed = readJson(managedSettingsPath(), null);
71
+ return loadPricing(managed && managed.modelPricing, readConfig().modelPricing);
72
+ }
73
+
65
74
  function updateConfig(patch) {
66
75
  const next = { ...readConfig() };
67
76
  if (typeof patch.terminal === 'string') next.terminal = patch.terminal;
@@ -239,6 +248,7 @@ function resolvedTerminal() {
239
248
  module.exports = {
240
249
  THEMES,
241
250
  readConfig,
251
+ readPricing,
242
252
  configDir: () => CONFIG_DIR,
243
253
  findOnPath,
244
254
  detectClaudeApp,
package/lib/demo.js CHANGED
@@ -70,6 +70,7 @@ function demoState() {
70
70
  {
71
71
  sessionId: S1, pid: 1111, cwd: '/demo/acme-storefront', projectPath: '/demo/acme-storefront',
72
72
  projectName: 'Acme Storefront', isWorktree: false, model: 'claude-opus-5',
73
+ estCost: 24.6, burnRate: 11, assumed: false, context: { tokens: 412000, window: 1000000, pct: 41 }, tools: { total: 142, errors: 3 },
73
74
  title: 'Wire Stripe checkout into the cart', status: 'waiting',
74
75
  waitingFor: null,
75
76
  waitingReason: { kind: 'question', text: 'Which payment provider should checkout use for the wallet flow?', options: ['Payment Request Button', 'Native per wallet'] },
@@ -80,6 +81,7 @@ function demoState() {
80
81
  {
81
82
  sessionId: S2, pid: 2222, cwd: '/demo/blog-engine', projectPath: '/demo/blog-engine',
82
83
  projectName: 'Blog Engine', isWorktree: false, model: 'claude-fable-5',
84
+ estCost: 61.2, burnRate: 19, assumed: false, context: { tokens: 731000, window: 1000000, pct: 73 }, tools: { total: 388, errors: 0 },
83
85
  title: 'Migrate posts to the new block format', status: 'busy',
84
86
  waitingFor: null, waitingReason: null, startedAt: now - 3 * HOUR - 12 * MIN, statusUpdatedAt: now - MIN, quietMin: null, waitingMin: null,
85
87
  currentTask: { activeForm: 'Converting legacy shortcodes' },
@@ -94,6 +96,7 @@ function demoState() {
94
96
  {
95
97
  sessionId: S4, pid: 4444, cwd: '/demo/data-pipeline', projectPath: '/demo/data-pipeline',
96
98
  projectName: 'Data Pipeline', isWorktree: false, model: 'claude-sonnet-5',
99
+ estCost: 3.4, burnRate: 9, assumed: false, context: { tokens: 186000, window: 1000000, pct: 19 }, tools: { total: 37, errors: 1 },
97
100
  title: 'Backfill the analytics warehouse', status: 'waiting',
98
101
  waitingFor: null,
99
102
  waitingReason: { kind: 'permission', text: 'Bash node scripts/backfill.mjs --apply --from 2026-01-01' },
@@ -215,6 +218,7 @@ function demoStats() {
215
218
  models: ['claude-opus-5', 'claude-fable-5', 'claude-haiku-4-5'],
216
219
  busiestDay: 'Tuesday', busiestHour: 11,
217
220
  waits: [22, 31, 18, 4], typicalWait: 'under 2m',
221
+ cacheHitRatio: 0.91,
218
222
  },
219
223
  byModel: {
220
224
  'claude-opus-5': { tokens: 310e6, cost: 412.5 },
@@ -228,6 +232,11 @@ function demoStats() {
228
232
  { name: 'Claude Dashboard', total: 142.9, models: { 'claude-opus-5': 96.4, 'claude-fable-5': 46.5 } },
229
233
  { name: 'Design System', total: 88.1, models: { 'claude-opus-5': 61.2, 'claude-haiku-4-5': 26.9 } },
230
234
  ],
235
+ topSessions: [
236
+ { sessionId: S2, title: 'Migrate posts to the new block format', projectName: 'Blog Engine', projectPath: '/demo/blog-engine', model: 'claude-fable-5', cost: 61.2, lastActivityAt: now - MIN },
237
+ { sessionId: S1, title: 'Wire Stripe checkout into the cart', projectName: 'Acme Storefront', projectPath: '/demo/acme-storefront', model: 'claude-opus-5', cost: 24.6, lastActivityAt: now - 2 * MIN },
238
+ { sessionId: S3, title: 'Design the onboarding flow', projectName: 'Acme Storefront', projectPath: '/demo/acme-storefront', model: 'claude-opus-5', cost: 18.9, lastActivityAt: now - 3 * HOUR },
239
+ ],
231
240
  totals: { sessions: 212, prompts: 1841, cost: 810.4 },
232
241
  };
233
242
  }
@@ -235,7 +244,7 @@ function demoStats() {
235
244
  // `after` mirrors the live-follow poll: once the viewer has the canned
236
245
  // transcript there is nothing new, so return an empty tail instead of
237
246
  // re-sending every event.
238
- function demoSession(id, after) {
247
+ function demoSessionInner(id, after) {
239
248
  const now = Date.now();
240
249
  if (id === S2) return blogSession(now, after);
241
250
  if (id === S4) return pipelineSession(now, after);
@@ -262,6 +271,24 @@ function demoSession(id, after) {
262
271
  };
263
272
  }
264
273
 
274
+ const DEMO_USAGE = {
275
+ cost: 61.2, tokens: 14_300_000, cacheHitRatio: 0.93,
276
+ byModel: [
277
+ { model: 'claude-fable-5', input: 210_000, output: 480_000, cacheRead: 11_900_000, cacheCreation: 640_000, cost: 55.9 },
278
+ { model: 'claude-haiku-4-5', input: 120_000, output: 90_000, cacheRead: 860_000, cacheCreation: 0, cost: 5.3 },
279
+ ],
280
+ subagents: { count: 4, cost: 5.3, tokens: 1_070_000 },
281
+ assumedModels: [],
282
+ context: { tokens: 731000, window: 1000000, pct: 73 },
283
+ tools: { total: 388, errors: 0, byName: [{ name: 'Edit', count: 141, errors: 0 }, { name: 'Read', count: 122, errors: 0 }, { name: 'Bash', count: 79, errors: 0 }, { name: 'Grep', count: 46, errors: 0 }] },
284
+ };
285
+
286
+ // The usage block rides only on the initial load, like the real endpoint.
287
+ function demoSession(id, after) {
288
+ const t = demoSessionInner(id, after);
289
+ return after ? t : { ...t, usage: DEMO_USAGE };
290
+ }
291
+
265
292
  function blogSession(now, after) {
266
293
  return {
267
294
  sessionId: S2,
@@ -371,5 +398,39 @@ function demoInventory() {
371
398
  };
372
399
  }
373
400
 
374
- module.exports = { demoState, demoStats, demoSession, demoChanges, demoInventory };
401
+ // A believable toolset for the demo project drawer.
402
+ function demoToolset() {
403
+ const local = '.claude/settings.local.json';
404
+ const rec = (label, obj) => ({ label, text: JSON.stringify(obj, null, 2), target: local });
405
+ const mcpRec = (name, on) => ({ label: `${on ? 'disable' : 'enable'} MCP server ${name}`, text: `Run /mcp inside a Claude session in this project and toggle ${name}.`, target: 'a Claude session in this project' });
406
+ return {
407
+ files: { managed: false, local: true, project: true, user: true },
408
+ audit: [
409
+ { level: 'warn', text: 'plugin ado-skills is enabled by user settings but not installed' },
410
+ { level: 'info', text: 'agent reviewer (project) shadows plugin:ecc' },
411
+ ],
412
+ plugins: [
413
+ { name: 'superpowers', kind: 'plugin', enabled: true, source: 'user', pluginId: 'superpowers@claude-plugins-official', decidedBy: 'user', shadowed: [], description: null, installed: true, stale: false, recipe: rec('disable plugin superpowers', { enabledPlugins: { 'superpowers@claude-plugins-official': false } }) },
414
+ { name: 'ecc', kind: 'plugin', enabled: true, source: 'user', pluginId: 'ecc@claude-plugins-official', decidedBy: 'local', shadowed: [], description: null, installed: true, stale: false, recipe: rec('disable plugin ecc', { enabledPlugins: { 'ecc@claude-plugins-official': false } }) },
415
+ { name: 'ado-skills', kind: 'plugin', enabled: true, source: 'user', pluginId: 'ado-skills@bt-ai-tools', decidedBy: 'user', shadowed: [], description: null, installed: false, stale: false, recipe: rec('disable plugin ado-skills', { enabledPlugins: { 'ado-skills@bt-ai-tools': false } }) },
416
+ { name: 'figma', kind: 'plugin', enabled: false, source: 'user', pluginId: 'figma@claude-plugins-official', decidedBy: 'project', shadowed: [], description: null, installed: true, stale: false, recipe: rec('enable plugin figma', { enabledPlugins: { 'figma@claude-plugins-official': true } }) },
417
+ ],
418
+ agents: [
419
+ { name: 'reviewer', kind: 'agent', enabled: true, source: 'project', pluginId: null, decidedBy: 'default', shadowed: ['plugin:ecc'], description: 'Reviews a diff for correctness', recipe: rec('disable agent reviewer', { permissions: { deny: ['Agent(reviewer)'] } }) },
420
+ { name: 'code-explorer', kind: 'agent', enabled: true, source: 'plugin', pluginId: 'ecc@claude-plugins-official', decidedBy: 'default', shadowed: [], description: 'Traces execution paths', recipe: rec('disable agent code-explorer', { permissions: { deny: ['Agent(ecc:code-explorer)'] } }) },
421
+ { name: 'planner', kind: 'agent', enabled: false, source: 'user', pluginId: null, decidedBy: 'local', shadowed: [], description: null, recipe: { label: 'enable agent planner', text: 'Remove the "Agent(planner)" (or "Task(planner)") entry from permissions.deny in .claude/settings.local.json.', target: local } },
422
+ ],
423
+ skills: [
424
+ { name: 'brainstorming', kind: 'skill', enabled: true, mode: null, source: 'plugin', pluginId: 'superpowers@claude-plugins-official', decidedBy: 'plugin', shadowed: [], description: 'Design before code', recipe: rec('disable plugin superpowers', { enabledPlugins: { 'superpowers@claude-plugins-official': false } }) },
425
+ { name: 'deploy', kind: 'skill', enabled: true, mode: 'user-invocable-only', source: 'project', pluginId: null, decidedBy: 'local', shadowed: [], description: 'Ship to staging', recipe: rec('turn off skill deploy', { skillOverrides: { deploy: 'off' } }) },
426
+ { name: 'legacy-notes', kind: 'skill', enabled: false, mode: 'off', source: 'user', pluginId: null, decidedBy: 'user', shadowed: [], description: null, recipe: rec('turn on skill legacy-notes', { skillOverrides: { 'legacy-notes': 'on' } }) },
427
+ ],
428
+ mcp: [
429
+ { name: 'github', kind: 'mcp', enabled: true, source: 'global', pluginId: null, decidedBy: 'default', shadowed: [], description: 'http · https://api.githubcopilot.com/…', needsAuth: false, recipe: mcpRec('github', true) },
430
+ { name: 'postgres', kind: 'mcp', enabled: false, source: 'project', pluginId: null, decidedBy: 'claude.json', shadowed: [], description: 'stdio', needsAuth: false, recipe: mcpRec('postgres', false) },
431
+ ],
432
+ };
433
+ }
434
+
435
+ module.exports = { demoState, demoStats, demoSession, demoChanges, demoInventory, demoToolset };
375
436
 
package/lib/detail.js CHANGED
@@ -5,46 +5,7 @@
5
5
  const fsp = require('fs/promises');
6
6
  const path = require('path');
7
7
  const { CLAUDE_DIR, canonicalize, encodeProjectDir } = require('./paths');
8
-
9
- const MAX_FILE_BYTES = 64 * 1024; // per-file cap for returned content
10
-
11
- async function readCapped(abs) {
12
- try {
13
- const st = await fsp.stat(abs);
14
- if (!st.isFile()) return null;
15
- const fh = await fsp.open(abs, 'r');
16
- try {
17
- const len = Math.min(st.size, MAX_FILE_BYTES);
18
- const buf = Buffer.alloc(len);
19
- await fh.read(buf, 0, len, 0);
20
- let content = buf.toString('utf8');
21
- if (st.size > MAX_FILE_BYTES) content += `\n\n… truncated (${st.size} bytes total)`;
22
- return { content, size: st.size, mtimeMs: st.mtimeMs };
23
- } finally {
24
- await fh.close();
25
- }
26
- } catch {
27
- return null;
28
- }
29
- }
30
-
31
- async function listMd(dir) {
32
- try {
33
- const names = await fsp.readdir(dir);
34
- return names.filter((n) => n.endsWith('.md')).sort();
35
- } catch {
36
- return [];
37
- }
38
- }
39
-
40
- // First `description:` line of YAML frontmatter, if any.
41
- function frontmatterDescription(content) {
42
- if (!content || !content.startsWith('---')) return null;
43
- const end = content.indexOf('\n---', 3);
44
- if (end === -1) return null;
45
- const m = content.slice(0, end).match(/^description:\s*(.+)$/m);
46
- return m ? m[1].trim().replace(/^["']|["']$/g, '').slice(0, 300) : null;
47
- }
8
+ const { readCapped, listMd, frontmatterDescription } = require('./mdfiles');
48
9
 
49
10
  // Skills are directories containing SKILL.md; agents/commands are .md files.
50
11
  async function collectCapabilities(root) {
package/lib/inventory.js CHANGED
@@ -27,6 +27,7 @@ function mergePlugins(installed, enabledMap, marketplaces, now = Date.now()) {
27
27
  rows.push({
28
28
  id, name, marketplace,
29
29
  version: inst ? String(inst.version || '') : null,
30
+ installPath: inst && typeof inst.installPath === 'string' ? inst.installPath : null,
30
31
  installedAt: inst ? ms(inst.installedAt) : null,
31
32
  lastUpdated: updated,
32
33
  enabled: !!(enabledMap && enabledMap[id]),