@viren/claude-code-dashboard 0.0.7 → 0.0.10

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.
@@ -43,7 +43,12 @@ import {
43
43
  computeDashboardDiff,
44
44
  } from "./src/analysis.mjs";
45
45
  import { getFreshness } from "./src/freshness.mjs";
46
- import { parseUserMcpConfig, parseProjectMcpConfig, scanHistoricalMcpServers } from "./src/mcp.mjs";
46
+ import {
47
+ parseUserMcpConfig,
48
+ parseProjectMcpConfig,
49
+ scanHistoricalMcpServers,
50
+ fetchRegistryServers,
51
+ } from "./src/mcp.mjs";
47
52
  import { handleInit } from "./src/templates.mjs";
48
53
  import { generateCatalogHtml } from "./src/render.mjs";
49
54
  import { generateDashboardHtml } from "./src/assembler.mjs";
@@ -87,7 +92,7 @@ if (cliArgs.demo) {
87
92
 
88
93
  // ── Collect Raw Inputs ────────────────────────────────────────────────────────
89
94
 
90
- function collectRawInputs() {
95
+ async function collectRawInputs() {
91
96
  const scanRoots = getScanRoots();
92
97
  const allRepoPaths = findGitRepos(scanRoots, MAX_DEPTH);
93
98
 
@@ -217,6 +222,9 @@ function collectRawInputs() {
217
222
  entry.projects = new Set([...entry.projects].map((p) => shortPath(p)));
218
223
  }
219
224
 
225
+ // MCP Registry servers
226
+ const registryServers = cliArgs.offline ? [] : await fetchRegistryServers();
227
+
220
228
  // Usage data — session meta files
221
229
  const SESSION_META_LIMIT = 1000;
222
230
  const sessionMetaDir = join(CLAUDE_DIR, "usage-data", "session-meta");
@@ -309,6 +317,7 @@ function collectRawInputs() {
309
317
  projectMcpByRepo,
310
318
  disabledMcpByRepo,
311
319
  historicalMcpMap,
320
+ registryServers,
312
321
  sessionMetaFiles,
313
322
  ccusageData,
314
323
  statsCache,
@@ -321,7 +330,7 @@ function collectRawInputs() {
321
330
 
322
331
  // ── Build Dashboard Data ─────────────────────────────────────────────────────
323
332
 
324
- const rawInputs = collectRawInputs();
333
+ const rawInputs = await collectRawInputs();
325
334
  const data = buildDashboardData(rawInputs);
326
335
 
327
336
  // ── Lint Subcommand ──────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viren/claude-code-dashboard",
3
- "version": "0.0.7",
3
+ "version": "0.0.10",
4
4
  "description": "A visual dashboard for your Claude Code configuration across all repos",
5
5
  "type": "module",
6
6
  "bin": {
package/src/assembler.mjs CHANGED
@@ -2,12 +2,13 @@ import { readFileSync } from "fs";
2
2
  import { fileURLToPath } from "url";
3
3
  import { dirname, join } from "path";
4
4
 
5
- import { esc, insightsToMarkdown } from "./helpers.mjs";
5
+ import { esc, insightsToPrompt } from "./helpers.mjs";
6
6
  import { VERSION, REPO_URL } from "./constants.mjs";
7
7
  import { renderCmd, renderRule, renderRepoCard } from "./render.mjs";
8
8
  import {
9
9
  renderSkillsCard,
10
10
  renderMcpCard,
11
+ renderMcpRecommendedCard,
11
12
  renderToolsCard,
12
13
  renderLangsCard,
13
14
  renderErrorsCard,
@@ -47,6 +48,9 @@ export function generateDashboardHtml(data) {
47
48
  mcpSummary,
48
49
  mcpPromotions,
49
50
  formerMcpServers,
51
+ recommendedMcpServers,
52
+ availableMcpServers,
53
+ registryTotal,
50
54
  consolidationGroups,
51
55
  usageAnalytics,
52
56
  ccusageData,
@@ -68,12 +72,20 @@ export function generateDashboardHtml(data) {
68
72
  <button id="refresh-btn" class="header-btn" title="Copy refresh command to clipboard" aria-label="Copy refresh command">&#8635; refresh</button>
69
73
  <button id="theme-toggle" class="theme-toggle" title="Toggle light/dark mode" aria-label="Toggle theme"><span class="theme-icon"></span></button>
70
74
  </div>
71
- <p class="sub">generated ${timestamp} · <a href="${esc(REPO_URL)}" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none">v${esc(VERSION)}</a></p>`;
75
+ <p class="sub">generated ${timestamp} · <a href="${esc(REPO_URL)}" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:underline;text-underline-offset:2px">v${esc(VERSION)}</a></p>`;
72
76
 
73
77
  const statsBar = renderStatsBar(data);
74
78
 
75
- // Overview tab
76
- const overviewCommands = `<div class="top-grid">
79
+ // Home tab — actionable content first
80
+ const insightsPrompt = insightsToPrompt(insights);
81
+ const insightsHtml = renderInsightsCard(insights, insightsPrompt);
82
+ const mcpRecsHtml = renderMcpRecommendedCard(recommendedMcpServers);
83
+ const chainsHtml = renderChainsCard(chains);
84
+ const consolidationHtml = renderConsolidationCard(consolidationGroups);
85
+ const tabHome = `${insightsHtml}\n ${mcpRecsHtml}\n ${chainsHtml}\n ${consolidationHtml}`;
86
+
87
+ // Config tab — stable reference: commands, rules, skills, MCP servers
88
+ const commandsRulesHtml = `<div class="top-grid">
77
89
  <div class="card" id="section-commands" style="margin-bottom:0">
78
90
  <h2>Global Commands <span class="n">${globalCmds.length}</span></h2>
79
91
  ${globalCmds.map((c) => renderCmd(c)).join("\n ")}
@@ -83,14 +95,7 @@ export function generateDashboardHtml(data) {
83
95
  ${globalRules.map((r) => renderRule(r)).join("\n ")}
84
96
  </div>
85
97
  </div>`;
86
- const insightsMarkdown = insightsToMarkdown(insights);
87
- const insightsHtml = renderInsightsCard(insights, insightsMarkdown);
88
- const chainsHtml = renderChainsCard(chains);
89
- const consolidationHtml = renderConsolidationCard(consolidationGroups);
90
- const tabOverview = `${overviewCommands}\n ${insightsHtml}\n ${chainsHtml}\n ${consolidationHtml}`;
91
-
92
- // Skills & MCP tab
93
- const tabSkillsMcp = `${renderSkillsCard(globalSkills)}\n ${renderMcpCard(mcpSummary, mcpPromotions, formerMcpServers)}`;
98
+ const tabConfig = `${commandsRulesHtml}\n ${renderSkillsCard(globalSkills)}\n ${renderMcpCard(mcpSummary, mcpPromotions, formerMcpServers, availableMcpServers, registryTotal)}`;
94
99
 
95
100
  // Analytics tab
96
101
  const insightsReportHtml = renderInsightsReportCard(insightsReport);
@@ -143,8 +148,8 @@ export function generateDashboardHtml(data) {
143
148
  html = html.replace("/* {{JS}} */", _js);
144
149
  html = html.replace("<!-- {{HEADER}} -->", header);
145
150
  html = html.replace("<!-- {{STATS_BAR}} -->", statsBar);
146
- html = html.replace("<!-- {{TAB_OVERVIEW}} -->", tabOverview);
147
- html = html.replace("<!-- {{TAB_SKILLS_MCP}} -->", tabSkillsMcp);
151
+ html = html.replace("<!-- {{TAB_HOME}} -->", tabHome);
152
+ html = html.replace("<!-- {{TAB_CONFIG}} -->", tabConfig);
148
153
  html = html.replace("<!-- {{TAB_ANALYTICS}} -->", tabAnalytics);
149
154
  html = html.replace("<!-- {{TAB_REPOS}} -->", tabRepos);
150
155
  html = html.replace("<!-- {{TAB_REFERENCE}} -->", tabReference);
package/src/cli.mjs CHANGED
@@ -15,6 +15,7 @@ export function parseArgs(argv) {
15
15
  anonymize: false,
16
16
  demo: false,
17
17
  completions: false,
18
+ offline: false,
18
19
  };
19
20
  let i = 2; // skip node + script
20
21
  if (argv[2] === "init") {
@@ -46,6 +47,7 @@ Options:
46
47
  --watch Regenerate on file changes
47
48
  --diff Show changes since last generation
48
49
  --anonymize Anonymize all data for shareable export
50
+ --offline Skip network fetches (registry, etc.)
49
51
  --demo Generate dashboard with sample data (no scanning)
50
52
  --completions Output shell completion script for bash/zsh
51
53
  --version, -v Show version
@@ -113,6 +115,9 @@ Config file: ~/.claude/dashboard.conf
113
115
  case "--anonymize":
114
116
  args.anonymize = true;
115
117
  break;
118
+ case "--offline":
119
+ args.offline = true;
120
+ break;
116
121
  case "--demo":
117
122
  args.demo = true;
118
123
  break;
@@ -133,11 +138,11 @@ export function generateCompletions() {
133
138
  # eval "$(claude-code-dashboard --completions)"
134
139
  if [ -n "$ZSH_VERSION" ]; then
135
140
  _claude_code_dashboard() {
136
- local -a opts; opts=(init lint --output --open --no-open --json --catalog --quiet --watch --diff --anonymize --demo --completions --help --version)
141
+ local -a opts; opts=(init lint --output --open --no-open --json --catalog --quiet --watch --diff --anonymize --offline --demo --completions --help --version)
137
142
  if (( CURRENT == 2 )); then _describe 'option' opts; fi
138
143
  }; compdef _claude_code_dashboard claude-code-dashboard
139
144
  elif [ -n "$BASH_VERSION" ]; then
140
- _claude_code_dashboard() { COMPREPLY=( $(compgen -W "init lint --output --open --no-open --json --catalog --quiet --watch --diff --anonymize --demo --completions --help --version" -- "\${COMP_WORDS[COMP_CWORD]}") ); }
145
+ _claude_code_dashboard() { COMPREPLY=( $(compgen -W "init lint --output --open --no-open --json --catalog --quiet --watch --diff --anonymize --offline --demo --completions --help --version" -- "\${COMP_WORDS[COMP_CWORD]}") ); }
141
146
  complete -F _claude_code_dashboard claude-code-dashboard
142
147
  fi`);
143
148
  process.exit(0);
package/src/constants.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { join } from "path";
2
2
  import { homedir } from "os";
3
3
 
4
- export const VERSION = "0.0.7";
4
+ export const VERSION = "0.0.10";
5
5
  export const REPO_URL = "https://github.com/VirenMohindra/claude-code-dashboard";
6
6
 
7
7
  export const HOME = homedir();
@@ -98,6 +98,44 @@ export const SKILL_CATEGORIES = {
98
98
  "project-specific": ["storybook", "react-native"],
99
99
  };
100
100
 
101
+ export const MCP_REGISTRY_URL =
102
+ "https://api.anthropic.com/mcp-registry/v0/servers?visibility=commercial&limit=100";
103
+ export const MCP_REGISTRY_TTL_MS = 24 * 60 * 60 * 1000; // 24h
104
+
105
+ /**
106
+ * Maps detected tech stacks and description keywords to relevant MCP server slugs.
107
+ * Used by the pipeline to compute MCP recommendations.
108
+ *
109
+ * Keys: tech stack names (matching STACK_FILES values) or lowercase keywords
110
+ * found in repo descriptions.
111
+ * Values: array of MCP server slugs from the Anthropic registry.
112
+ */
113
+ export const MCP_STACK_HINTS = {
114
+ // Stack-based (keys match STACK_FILES values)
115
+ next: ["vercel", "figma"],
116
+ react: ["figma"],
117
+ python: ["sentry"],
118
+ go: ["sentry"],
119
+ rust: ["sentry"],
120
+ java: ["sentry"],
121
+ expo: ["figma"],
122
+
123
+ // Keyword-based (matched against lowercased repo descriptions)
124
+ supabase: ["supabase"],
125
+ stripe: ["stripe"],
126
+ vercel: ["vercel"],
127
+ sentry: ["sentry"],
128
+ notion: ["notion"],
129
+ linear: ["linear"],
130
+ jira: ["atlassian"],
131
+ confluence: ["atlassian"],
132
+ slack: ["slack"],
133
+ figma: ["figma"],
134
+ github: ["github"],
135
+ huggingface: ["hugging-face"],
136
+ "hugging face": ["hugging-face"],
137
+ };
138
+
101
139
  export const CATEGORY_ORDER = [
102
140
  "workflow",
103
141
  "code-quality",
package/src/demo.mjs CHANGED
@@ -458,6 +458,83 @@ const DEMO_INSIGHTS_HTML = `<!DOCTYPE html>
458
458
  </body>
459
459
  </html>`;
460
460
 
461
+ // ── MCP Registry servers ──────────────────────────────────────────────────────
462
+
463
+ const DEMO_REGISTRY_SERVERS = [
464
+ {
465
+ name: "Vercel",
466
+ slug: "vercel",
467
+ description: "Analyze, debug, and manage projects and deployments",
468
+ url: "https://mcp.vercel.com/",
469
+ installCommand: "claude mcp add --transport http vercel https://mcp.vercel.com",
470
+ worksWith: ["claude", "claude-api", "claude-code"],
471
+ tools: ["list_projects", "get_deployment"],
472
+ },
473
+ {
474
+ name: "Supabase",
475
+ slug: "supabase",
476
+ description: "Manage databases, authentication, and storage",
477
+ url: "https://mcp.supabase.com/mcp",
478
+ installCommand: "claude mcp add --transport http supabase https://mcp.supabase.com/mcp",
479
+ worksWith: ["claude", "claude-api", "claude-code"],
480
+ tools: ["execute_sql", "generate_typescript_types"],
481
+ },
482
+ {
483
+ name: "Stripe",
484
+ slug: "stripe",
485
+ description: "Payment processing and financial infrastructure tools",
486
+ url: "https://mcp.stripe.com",
487
+ installCommand: "claude mcp add --transport http stripe https://mcp.stripe.com",
488
+ worksWith: ["claude", "claude-api", "claude-code"],
489
+ tools: ["create_customer", "create_payment_link"],
490
+ },
491
+ {
492
+ name: "Linear",
493
+ slug: "linear",
494
+ description: "Manage issues, projects & team workflows in Linear",
495
+ url: "https://mcp.linear.app/mcp",
496
+ installCommand: "claude mcp add --transport http linear https://mcp.linear.app/mcp",
497
+ worksWith: ["claude", "claude-api", "claude-code"],
498
+ tools: ["create_issue", "update_issue"],
499
+ },
500
+ {
501
+ name: "Notion",
502
+ slug: "notion",
503
+ description: "Create, edit, search and organize Notion content",
504
+ url: "https://mcp.notion.com/mcp",
505
+ installCommand: "claude mcp add --transport http notion https://mcp.notion.com/mcp",
506
+ worksWith: ["claude", "claude-api", "claude-code"],
507
+ tools: ["search", "create-pages"],
508
+ },
509
+ {
510
+ name: "Atlassian",
511
+ slug: "atlassian",
512
+ description: "Access Jira & Confluence from Claude",
513
+ url: "https://mcp.atlassian.com/v1/mcp",
514
+ installCommand: "claude mcp add --transport http atlassian https://mcp.atlassian.com/v1/mcp",
515
+ worksWith: ["claude", "claude-code"],
516
+ tools: ["getJiraIssue", "createJiraIssue"],
517
+ },
518
+ {
519
+ name: "Slack",
520
+ slug: "slack",
521
+ description: "Send messages, create canvases, and fetch Slack data",
522
+ url: "https://mcp.slack.com/mcp",
523
+ installCommand: "claude mcp add --transport http slack-remote https://mcp.slack.com/mcp",
524
+ worksWith: ["claude", "claude-api", "claude-code"],
525
+ tools: ["slack_send_message", "slack_search_public_and_private"],
526
+ },
527
+ {
528
+ name: "Context7",
529
+ slug: "context7",
530
+ description: "Up-to-date docs for LLMs and AI code editors",
531
+ url: "https://mcp.context7.com/mcp",
532
+ installCommand: "claude mcp add --transport http context7 https://mcp.context7.com/mcp",
533
+ worksWith: ["claude", "claude-api", "claude-code"],
534
+ tools: ["resolve-library-id", "query-docs"],
535
+ },
536
+ ];
537
+
461
538
  // ── Main export ──────────────────────────────────────────────────────────────
462
539
 
463
540
  export function generateDemoRawInputs() {
@@ -473,6 +550,7 @@ export function generateDemoRawInputs() {
473
550
  projectMcpByRepo,
474
551
  disabledMcpByRepo,
475
552
  historicalMcpMap,
553
+ registryServers: DEMO_REGISTRY_SERVERS,
476
554
  sessionMetaFiles: buildDemoSessionMeta(),
477
555
  ccusageData: {
478
556
  totals: { totalCost: 47.82, totalTokens: 28_450_000 },
package/src/helpers.mjs CHANGED
@@ -33,25 +33,109 @@ export function gitCmd(repoDir, ...args) {
33
33
  }
34
34
  }
35
35
 
36
- const INSIGHT_ICONS = {
37
- warning: "\u26A0\uFE0F",
38
- tip: "\u2728",
39
- promote: "\u2B06",
40
- info: "\u2139\uFE0F",
36
+ const PROMPT_GENERATORS = {
37
+ "config-drift"(meta) {
38
+ const list = meta.repos.map((r) => `- ${r.name} (${r.commitsSince} commits since last update)`);
39
+ return [
40
+ "These repos have stale CLAUDE.md files:",
41
+ ...list,
42
+ "",
43
+ "For each repo:",
44
+ "1. Read the current CLAUDE.md",
45
+ "2. Run `git log --oneline` to see what changed since the last config update",
46
+ "3. Check if new patterns, tools, or conventions were introduced that should be documented",
47
+ "4. Propose targeted additions — don't rewrite from scratch, just fill gaps",
48
+ ].join("\n");
49
+ },
50
+ "unconfigured-repos"(meta) {
51
+ if (!meta.repos.length) return "Several repos have no Claude Code configuration.";
52
+ const list = meta.repos.map((r) => `- ${r.name} (${r.techStack.join(", ")})`);
53
+ return [
54
+ "These repos have no CLAUDE.md yet:",
55
+ ...list,
56
+ "",
57
+ "Pick the one you want to configure. For that repo:",
58
+ "1. Read the project structure and config files (package.json, pyproject.toml, etc.)",
59
+ "2. Identify the build system, test framework, and linter",
60
+ "3. Generate a concise CLAUDE.md (50-100 lines) with build/test/lint commands and key conventions",
61
+ ].join("\n");
62
+ },
63
+ "mcp-promotion"(meta) {
64
+ const list = meta.servers.map((s) => `- ${s.name} (used in ${s.projectCount} projects)`);
65
+ return [
66
+ "These MCP servers are installed in multiple projects and should be promoted to global:",
67
+ ...list,
68
+ "",
69
+ "To promote a server, run:",
70
+ " claude mcp add --scope user <server-name> <config>",
71
+ "Then remove the duplicate entries from each project's .mcp.json.",
72
+ ].join("\n");
73
+ },
74
+ "mcp-redundant"(meta) {
75
+ const list = meta.servers.map((s) => `- ${s.name}: remove from ${s.projects.join(", ")}`);
76
+ return [
77
+ "These MCP servers are configured globally AND redundantly in project .mcp.json:",
78
+ ...list,
79
+ "",
80
+ "The global config already covers all projects. Remove the listed project-level entries.",
81
+ ].join("\n");
82
+ },
83
+ "mcp-recommendations"(meta) {
84
+ const list = meta.servers.map(
85
+ (s) =>
86
+ `- ${s.name} — ${s.reasons.join(", ")}` +
87
+ (s.installCommand?.trim() ? `\n ${s.installCommand.trim()}` : ""),
88
+ );
89
+ return [
90
+ "Based on your tech stacks, these MCP servers would be useful:",
91
+ ...list,
92
+ "",
93
+ "Run any install command above to add a server. Which ones interest you?",
94
+ ].join("\n");
95
+ },
96
+ "shared-skills"(meta) {
97
+ const list = meta.skills.map((s) => `- ${s.name} (relevant in ${s.repoCount} repos)`);
98
+ return [
99
+ "These skills are relevant across multiple repos and would benefit from being global:",
100
+ ...list,
101
+ "",
102
+ "To add a skill globally, copy its definition from any project's .claude/commands/ to ~/.claude/commands/.",
103
+ ].join("\n");
104
+ },
105
+ "health-quickwins"(meta) {
106
+ const list = meta.repos.map((r) => `- ${r.name} (${r.healthScore}/100): ${r.topReason}`);
107
+ return [
108
+ "These repos have easy config health improvements:",
109
+ ...list,
110
+ "",
111
+ "Pick a repo and I'll help you make the specific improvement.",
112
+ ].join("\n");
113
+ },
114
+ "insights-report"() {
115
+ return "Run `/insights` in Claude Code to generate a personalized usage report with patterns, friction points, and suggestions.";
116
+ },
41
117
  };
42
118
 
43
- /** Convert an insights array to a markdown string suitable for pasting into Claude Code. */
44
- export function insightsToMarkdown(insights) {
119
+ /** Convert insights to an actionable prompt for pasting into Claude Code. */
120
+ export function insightsToPrompt(insights) {
45
121
  if (!insights || !insights.length) return "";
46
- const lines = ["# Dashboard Insights\n"];
122
+ const sections = [];
47
123
  for (const i of insights) {
48
- const icon = INSIGHT_ICONS[i.type] || INSIGHT_ICONS.info;
49
- lines.push(`## ${icon} ${i.title}`);
50
- if (i.detail) lines.push(i.detail);
51
- if (i.action) lines.push(`**Action:** ${i.action}`);
52
- lines.push("");
124
+ const gen = i.meta?.kind && PROMPT_GENERATORS[i.meta.kind];
125
+ if (gen) {
126
+ sections.push(gen(i.meta));
127
+ } else {
128
+ const lines = [`${i.title}`];
129
+ if (i.detail) lines.push(i.detail);
130
+ if (i.action) lines.push(i.action);
131
+ sections.push(lines.join("\n"));
132
+ }
53
133
  }
54
- return lines.join("\n");
134
+ return [
135
+ "I ran the Claude Code Dashboard and found these items to address:",
136
+ ...sections.map((s, idx) => `${idx + 1}. ${s}`),
137
+ "Which of these would you like to tackle first?",
138
+ ].join("\n\n");
55
139
  }
56
140
 
57
141
  export function anonymizePath(p) {
package/src/mcp.mjs CHANGED
@@ -1,6 +1,11 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
1
+ import { existsSync, readdirSync, readFileSync, writeFileSync, statSync } from "fs";
2
2
  import { join } from "path";
3
- import { MAX_SESSION_SCAN } from "./constants.mjs";
3
+ import {
4
+ MAX_SESSION_SCAN,
5
+ MCP_REGISTRY_URL,
6
+ MCP_REGISTRY_TTL_MS,
7
+ CLAUDE_DIR,
8
+ } from "./constants.mjs";
4
9
 
5
10
  export function parseUserMcpConfig(content) {
6
11
  try {
@@ -175,3 +180,93 @@ export function classifyHistoricalServers(
175
180
  former.sort((a, b) => a.name.localeCompare(b.name));
176
181
  return { recent, former };
177
182
  }
183
+
184
+ /**
185
+ * Pure normalizer: extract claude-code compatible servers from raw registry API response.
186
+ * Returns [] on any malformed input.
187
+ */
188
+ export function normalizeRegistryResponse(raw) {
189
+ try {
190
+ if (!raw || !Array.isArray(raw.servers)) return [];
191
+ return raw.servers
192
+ .map((entry) => {
193
+ // The registry API nests data: entry.server has the MCP spec fields,
194
+ // entry._meta["com.anthropic.api/mcp-registry"] has Anthropic's curated metadata.
195
+ // Also support flat shape (used in tests and demo data).
196
+ const anth = entry?._meta?.["com.anthropic.api/mcp-registry"] || {};
197
+ const srv = entry?.server || entry || {};
198
+ const name = anth.displayName || entry.name || srv.title || "";
199
+ return {
200
+ name,
201
+ slug:
202
+ anth.slug ||
203
+ entry.slug ||
204
+ name
205
+ .toLowerCase()
206
+ .replace(/[^a-z0-9]+/g, "-")
207
+ .replace(/^-|-$/g, ""),
208
+ description: anth.oneLiner || entry.description || srv.description || "",
209
+ url: anth.url || entry.url || "",
210
+ installCommand: anth.claudeCodeCopyText || entry.installCommand || "",
211
+ worksWith: anth.worksWith || entry.worksWith || [],
212
+ tools: anth.toolNames || entry.tools || [],
213
+ };
214
+ })
215
+ .filter((s) => Array.isArray(s.worksWith) && s.worksWith.includes("claude-code"));
216
+ } catch {
217
+ return [];
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Fetch MCP registry servers with 24h file cache.
223
+ * Falls back to stale cache on network failure, returns [] on total failure.
224
+ */
225
+ export async function fetchRegistryServers() {
226
+ const cachePath = join(CLAUDE_DIR, "mcp-registry-cache.json");
227
+
228
+ // Try fresh cache
229
+ try {
230
+ const cached = JSON.parse(readFileSync(cachePath, "utf8"));
231
+ if (cached._ts && Date.now() - cached._ts < MCP_REGISTRY_TTL_MS) {
232
+ return normalizeRegistryResponse(cached.data);
233
+ }
234
+ } catch {
235
+ /* no cache or unreadable */
236
+ }
237
+
238
+ // Fetch from registry
239
+ try {
240
+ const controller = new AbortController();
241
+ const timeout = setTimeout(() => controller.abort(), 8000);
242
+ const res = await fetch(MCP_REGISTRY_URL, { signal: controller.signal });
243
+ clearTimeout(timeout);
244
+
245
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
246
+ const data = await res.json();
247
+ const normalized = normalizeRegistryResponse(data);
248
+
249
+ // Only cache valid responses to avoid 24h blackout on malformed data
250
+ if (Array.isArray(data?.servers) && data.servers.length > 0) {
251
+ try {
252
+ writeFileSync(cachePath, JSON.stringify({ _ts: Date.now(), data }));
253
+ } catch {
254
+ /* non-critical */
255
+ }
256
+ }
257
+
258
+ return normalized;
259
+ } catch {
260
+ /* network failure — try stale cache */
261
+ }
262
+
263
+ // Stale cache fallback (ignore TTL)
264
+ try {
265
+ const cached = JSON.parse(readFileSync(cachePath, "utf8"));
266
+ return normalizeRegistryResponse(cached.data);
267
+ } catch {
268
+ /* total failure */
269
+ }
270
+
271
+ return [];
272
+ }
package/src/pipeline.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  * All data comes in via the `raw` parameter.
7
7
  */
8
8
 
9
- import { SIMILARITY_THRESHOLD } from "./constants.mjs";
9
+ import { SIMILARITY_THRESHOLD, MCP_STACK_HINTS } from "./constants.mjs";
10
10
  import { relativeTime, freshnessClass } from "./freshness.mjs";
11
11
  import {
12
12
  computeHealthScore,
@@ -222,6 +222,86 @@ export function buildDashboardData(raw) {
222
222
  });
223
223
  const mcpCount = mcpSummary.length;
224
224
 
225
+ // ── 3b. MCP Registry — Available & Recommended ────────────────────────
226
+
227
+ const registryServers = raw.registryServers || [];
228
+ const registryTotal = registryServers.length;
229
+
230
+ // Build a set of installed server identifiers (lowercase names)
231
+ // Use mcpSummary which includes user, project, AND recently-active servers
232
+ const installedIds = new Set();
233
+ for (const s of mcpSummary) {
234
+ installedIds.add(s.name.toLowerCase());
235
+ }
236
+
237
+ // Filter out already-installed servers
238
+ const notInstalled = registryServers.filter(
239
+ (s) =>
240
+ !installedIds.has((s.slug || "").toLowerCase()) &&
241
+ !installedIds.has((s.name || "").toLowerCase()),
242
+ );
243
+
244
+ // Collect tech stacks and description text from all repos
245
+ const allRepos = [...configured, ...unconfigured];
246
+ const stackCounts = {}; // key -> count of repos with that stack
247
+ for (const repo of allRepos) {
248
+ for (const stack of repo.techStack || []) {
249
+ const k = stack.toLowerCase();
250
+ stackCounts[k] = (stackCounts[k] || 0) + 1;
251
+ }
252
+ }
253
+
254
+ // Join all descriptions into a single lowercased string for substring matching
255
+ // (supports multi-word keys like "hugging face")
256
+ const allDescText = allRepos
257
+ .flatMap((r) => r.desc || [])
258
+ .join(" ")
259
+ .toLowerCase();
260
+
261
+ // Match hints against stacks and descriptions
262
+ const recommendedSlugs = new Map(); // slug -> { reasons: [], matchCount: 0 }
263
+ for (const [key, slugs] of Object.entries(MCP_STACK_HINTS)) {
264
+ const stackCount = stackCounts[key] || 0;
265
+ const inDesc = allDescText.includes(key);
266
+
267
+ if (stackCount > 0 || inDesc) {
268
+ for (const slug of slugs) {
269
+ if (!recommendedSlugs.has(slug)) {
270
+ recommendedSlugs.set(slug, { reasons: [], matchCount: 0 });
271
+ }
272
+ const entry = recommendedSlugs.get(slug);
273
+ if (stackCount > 0) {
274
+ entry.reasons.push(`${stackCount} ${key} repo${stackCount > 1 ? "s" : ""} detected`);
275
+ entry.matchCount += stackCount;
276
+ }
277
+ if (inDesc) {
278
+ entry.reasons.push("mentioned in repo descriptions");
279
+ entry.matchCount += 1;
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ // Build recommended list from not-installed servers that match hints
286
+ const recommendedMcpServers = [];
287
+ const recommendedSlugSet = new Set();
288
+ for (const server of notInstalled) {
289
+ const slug = (server.slug || "").toLowerCase();
290
+ if (recommendedSlugs.has(slug)) {
291
+ const { reasons, matchCount } = recommendedSlugs.get(slug);
292
+ recommendedMcpServers.push({ ...server, reasons, matchCount });
293
+ recommendedSlugSet.add(slug);
294
+ }
295
+ }
296
+
297
+ // Sort by relevance (more match signals first)
298
+ recommendedMcpServers.sort((a, b) => b.matchCount - a.matchCount || a.name.localeCompare(b.name));
299
+
300
+ // Available = not-installed minus recommended
301
+ const availableMcpServers = notInstalled.filter(
302
+ (s) => !recommendedSlugSet.has((s.slug || "").toLowerCase()),
303
+ );
304
+
225
305
  // ── 4. Usage Analytics ────────────────────────────────────────────────
226
306
 
227
307
  const usageAnalytics = aggregateSessionMeta(raw.sessionMetaFiles || []);
@@ -366,6 +446,10 @@ export function buildDashboardData(raw) {
366
446
  .map((r) => `${r.name} (${r.drift.commitsSince} commits since config update)`)
367
447
  .join(", "),
368
448
  action: "Review and update CLAUDE.md in these repos",
449
+ meta: {
450
+ kind: "config-drift",
451
+ repos: highDriftRepos.map((r) => ({ name: r.name, commitsSince: r.drift.commitsSince })),
452
+ },
369
453
  });
370
454
  }
371
455
 
@@ -381,6 +465,10 @@ export function buildDashboardData(raw) {
381
465
  ? `Top candidates: ${withStack.map((r) => `${r.name} (${r.techStack.join(", ")})`).join(", ")}`
382
466
  : "",
383
467
  action: "Run claude-code-dashboard init --template <stack> in these repos",
468
+ meta: {
469
+ kind: "unconfigured-repos",
470
+ repos: withStack.map((r) => ({ name: r.name, techStack: r.techStack })),
471
+ },
384
472
  });
385
473
  }
386
474
  }
@@ -392,6 +480,10 @@ export function buildDashboardData(raw) {
392
480
  title: `${mcpPromotions.length} MCP server${mcpPromotions.length > 1 ? "s" : ""} could be promoted to global`,
393
481
  detail: mcpPromotions.map((p) => `${p.name} (in ${p.projects.length} projects)`).join(", "),
394
482
  action: "Add to ~/.claude/mcp_config.json for all projects",
483
+ meta: {
484
+ kind: "mcp-promotion",
485
+ servers: mcpPromotions.map((p) => ({ name: p.name, projectCount: p.projects.length })),
486
+ },
395
487
  });
396
488
  }
397
489
 
@@ -403,6 +495,31 @@ export function buildDashboardData(raw) {
403
495
  title: `${redundantMcp.length} MCP server${redundantMcp.length > 1 ? "s are" : " is"} global but also in project .mcp.json`,
404
496
  detail: redundantMcp.map((s) => `${s.name} (${s.projects.join(", ")})`).join("; "),
405
497
  action: "Remove from project .mcp.json — global config already covers all projects",
498
+ meta: {
499
+ kind: "mcp-redundant",
500
+ servers: redundantMcp.map((s) => ({ name: s.name, projects: s.projects })),
501
+ },
502
+ });
503
+ }
504
+
505
+ // MCP recommendations
506
+ if (recommendedMcpServers.length > 0) {
507
+ insights.push({
508
+ type: "tip",
509
+ title: `${recommendedMcpServers.length} MCP server${recommendedMcpServers.length > 1 ? "s" : ""} recommended for your repos`,
510
+ detail: recommendedMcpServers
511
+ .slice(0, 3)
512
+ .map((s) => `${s.name} (${s.reasons.join(", ")})`)
513
+ .join(", "),
514
+ action: "Check the Config tab for install commands",
515
+ meta: {
516
+ kind: "mcp-recommendations",
517
+ servers: recommendedMcpServers.slice(0, 5).map((s) => ({
518
+ name: s.name,
519
+ installCommand: s.installCommand,
520
+ reasons: s.reasons,
521
+ })),
522
+ },
406
523
  });
407
524
  }
408
525
 
@@ -425,6 +542,10 @@ export function buildDashboardData(raw) {
425
542
  title: `${widelyRelevant.length} skill${widelyRelevant.length > 1 ? "s" : ""} relevant across 3+ repos`,
426
543
  detail: top.map(([name, repos]) => `${name} (${repos.length} repos)`).join(", "),
427
544
  action: "Consider adding these skills to your global config",
545
+ meta: {
546
+ kind: "shared-skills",
547
+ skills: top.map(([name, repos]) => ({ name, repoCount: repos.length })),
548
+ },
428
549
  });
429
550
  }
430
551
 
@@ -441,6 +562,14 @@ export function buildDashboardData(raw) {
441
562
  .map((r) => `${r.name} (${r.healthScore}/100): ${r.healthReasons[0]}`)
442
563
  .join("; "),
443
564
  action: "Small changes for measurable improvement",
565
+ meta: {
566
+ kind: "health-quickwins",
567
+ repos: quickWinRepos.map((r) => ({
568
+ name: r.name,
569
+ healthScore: r.healthScore,
570
+ topReason: r.healthReasons[0],
571
+ })),
572
+ },
444
573
  });
445
574
  }
446
575
 
@@ -451,6 +580,7 @@ export function buildDashboardData(raw) {
451
580
  title: "Generate your Claude Code Insights report",
452
581
  detail: "Get personalized usage patterns, friction points, and feature suggestions",
453
582
  action: "Run /insights in Claude Code",
583
+ meta: { kind: "insights-report" },
454
584
  });
455
585
  }
456
586
 
@@ -493,6 +623,9 @@ export function buildDashboardData(raw) {
493
623
  avgHealth,
494
624
  driftCount,
495
625
  mcpCount,
626
+ recommendedMcpServers,
627
+ availableMcpServers,
628
+ registryTotal,
496
629
  scanScope: raw.scanScope,
497
630
  insights,
498
631
  insightsReport,
package/src/sections.mjs CHANGED
@@ -21,8 +21,33 @@ export function renderSkillsCard(globalSkills) {
21
21
  </div>`;
22
22
  }
23
23
 
24
- export function renderMcpCard(mcpSummary, mcpPromotions, formerMcpServers) {
25
- if (!mcpSummary.length) return "";
24
+ export function renderMcpRecommendedCard(recommendedMcpServers) {
25
+ if (!recommendedMcpServers.length) return "";
26
+ return `<div class="card">
27
+ <h2>Recommended MCP Servers <span class="n">${recommendedMcpServers.length}</span></h2>
28
+ ${recommendedMcpServers
29
+ .map(
30
+ (s) =>
31
+ `<div class="mcp-recommended"><span class="mcp-name">${esc(s.name)}</span> <span class="mcp-rec-badge">recommended</span>` +
32
+ (s.description ? `<div class="mcp-desc">${esc(s.description)}</div>` : "") +
33
+ (s.reasons && s.reasons.length
34
+ ? `<div class="mcp-reason">${s.reasons.map((r) => esc(r)).join(", ")}</div>`
35
+ : "") +
36
+ (s.installCommand ? `<code class="mcp-install">${esc(s.installCommand)}</code>` : "") +
37
+ `</div>`,
38
+ )
39
+ .join("\n ")}
40
+ </div>`;
41
+ }
42
+
43
+ export function renderMcpCard(
44
+ mcpSummary,
45
+ mcpPromotions,
46
+ formerMcpServers,
47
+ availableMcpServers,
48
+ registryTotal,
49
+ ) {
50
+ if (!mcpSummary.length && !availableMcpServers.length) return "";
26
51
  const rows = mcpSummary
27
52
  .map((s) => {
28
53
  const disabledClass = s.disabledIn > 0 ? " mcp-disabled" : "";
@@ -62,11 +87,33 @@ export function renderMcpCard(mcpSummary, mcpPromotions, formerMcpServers) {
62
87
  })
63
88
  .join("\n ")}`
64
89
  : "";
90
+ const availableHtml = availableMcpServers.length
91
+ ? `<details class="mcp-section">
92
+ <summary class="label" style="cursor:pointer;margin-top:.75rem">Available <span class="cat-n">${availableMcpServers.length}</span></summary>
93
+ ${availableMcpServers
94
+ .map(
95
+ (s) =>
96
+ `<div class="mcp-available"><span class="mcp-name">${esc(s.name)}</span>` +
97
+ (s.description ? `<div class="mcp-desc">${esc(s.description)}</div>` : "") +
98
+ (s.installCommand ? `<code class="mcp-install">${esc(s.installCommand)}</code>` : "") +
99
+ `</div>`,
100
+ )
101
+ .join("\n ")}
102
+ </details>`
103
+ : "";
104
+
105
+ const registryNote =
106
+ registryTotal > 0
107
+ ? `<div class="mcp-registry-note">${registryTotal} servers in registry</div>`
108
+ : "";
109
+
65
110
  return `<div class="card" id="section-mcp">
66
111
  <h2>MCP Servers <span class="n">${mcpSummary.length}</span></h2>
67
112
  ${rows}
68
113
  ${promoteHtml}
69
114
  ${formerHtml}
115
+ ${availableHtml}
116
+ ${registryNote}
70
117
  </div>`;
71
118
  }
72
119
 
@@ -330,13 +377,13 @@ export function renderReferenceCard() {
330
377
  </div>`;
331
378
  }
332
379
 
333
- export function renderInsightsCard(insights, markdown) {
380
+ export function renderInsightsCard(insights, prompt) {
334
381
  if (!insights || !insights.length) return "";
335
- const mdAttr = markdown ? ` data-markdown="${esc(markdown)}"` : "";
336
- return `<div class="card insight-card"${mdAttr}>
382
+ const promptAttr = prompt ? ` data-prompt="${esc(prompt)}"` : "";
383
+ return `<div class="card insight-card"${promptAttr}>
337
384
  <div class="card-header">
338
385
  <h2>Insights <span class="n">${insights.length}</span></h2>
339
- ${markdown ? `<button class="copy-md-btn" title="Copy as Markdown">&#128203; copy markdown</button>` : ""}
386
+ ${prompt ? `<button class="copy-prompt-btn" title="Copy as a prompt for Claude Code">&#128203; copy as prompt</button>` : ""}
340
387
  </div>
341
388
  ${insights
342
389
  .map(
@@ -389,29 +436,19 @@ export function renderInsightsReportCard(insightsReport) {
389
436
  }
390
437
 
391
438
  export function renderStatsBar(data) {
392
- const {
393
- coveragePct,
394
- configuredCount,
395
- totalRepos,
396
- avgHealth,
397
- globalCmds,
398
- globalSkills,
399
- totalRepoCmds,
400
- mcpCount,
401
- driftCount,
402
- ccusageData,
403
- usageAnalytics,
404
- } = data;
439
+ const { coveragePct, configuredCount, totalRepos, avgHealth, driftCount, ccusageData } = data;
440
+ const driftStat =
441
+ driftCount > 0
442
+ ? `<div class="stat" data-nav="repos" data-section="repo-grid" title="View drifting repos" style="border-color:#f8717133"><b style="color:var(--red)">${driftCount}</b><span>Drifting Repos</span></div>`
443
+ : "";
444
+ const spendStat = ccusageData
445
+ ? `<div class="stat" data-nav="analytics" data-section="section-activity" title="View analytics" style="border-color:#4ade8033"><b style="color:var(--green)">$${Math.round(Number(ccusageData.totals.totalCost) || 0).toLocaleString()}</b><span>Total Spent</span></div>`
446
+ : "";
405
447
  return `<div class="stats">
406
448
  <div class="stat coverage" data-nav="repos" data-section="repo-grid" title="View repos"><b>${coveragePct}%</b><span>Coverage (${configuredCount}/${totalRepos})</span></div>
407
449
  <div class="stat" data-nav="repos" data-section="repo-grid" title="View repos" style="${avgHealth >= 70 ? "border-color:#4ade8033" : avgHealth >= 40 ? "border-color:#fbbf2433" : "border-color:#f8717133"}"><b style="color:${healthScoreColor(avgHealth)}">${avgHealth}</b><span>Avg Health</span></div>
408
- <div class="stat" data-nav="overview" data-section="section-commands" title="View commands"><b>${globalCmds.length}</b><span>Global Commands</span></div>
409
- <div class="stat" data-nav="skills-mcp" data-section="section-skills" title="View skills"><b>${globalSkills.length}</b><span>Skills</span></div>
410
- <div class="stat" data-nav="repos" data-section="repo-grid" title="View repos"><b>${totalRepoCmds}</b><span>Repo Commands</span></div>
411
- ${mcpCount > 0 ? `<div class="stat" data-nav="skills-mcp" data-section="section-mcp" title="View MCP servers"><b>${mcpCount}</b><span>MCP Servers</span></div>` : ""}
412
- ${driftCount > 0 ? `<div class="stat" data-nav="repos" data-section="repo-grid" title="View drifting repos" style="border-color:#f8717133"><b style="color:var(--red)">${driftCount}</b><span>Drifting Repos</span></div>` : ""}
413
- ${ccusageData ? `<div class="stat" data-nav="analytics" data-section="section-activity" title="View analytics" style="border-color:#4ade8033"><b style="color:var(--green)">$${Math.round(Number(ccusageData.totals.totalCost) || 0).toLocaleString()}</b><span>Total Spent</span></div>` : ""}
414
- ${ccusageData ? `<div class="stat" data-nav="analytics" data-section="section-activity" title="View analytics"><b>${formatTokens(ccusageData.totals.totalTokens).replace(" tokens", "")}</b><span>Total Tokens</span></div>` : ""}
415
- ${usageAnalytics.heavySessions > 0 ? `<div class="stat" data-nav="analytics" data-section="section-activity" title="View analytics"><b>${usageAnalytics.heavySessions}</b><span>Heavy Sessions</span></div>` : ""}
450
+ <div class="stat" data-nav="repos" data-section="repo-grid" title="View repos"><b>${totalRepos}</b><span>Repos</span></div>
451
+ ${driftStat}
452
+ ${spendStat}
416
453
  </div>`;
417
454
  }
@@ -4,9 +4,9 @@
4
4
  --surface2: #1a1a1a;
5
5
  --border: #262626;
6
6
  --text: #e5e5e5;
7
- --text-dim: #777;
7
+ --text-dim: #999;
8
8
  --accent: #c4956a;
9
- --accent-dim: #8b6a4a;
9
+ --accent-dim: #b08a60;
10
10
  --green: #4ade80;
11
11
  --blue: #60a5fa;
12
12
  --purple: #a78bfa;
@@ -19,7 +19,7 @@
19
19
  --surface2: #f0f0f0;
20
20
  --border: #e0e0e0;
21
21
  --text: #1a1a1a;
22
- --text-dim: #666;
22
+ --text-dim: #555;
23
23
  --accent: #9b6b47;
24
24
  --accent-dim: #b8956e;
25
25
  --green: #16a34a;
@@ -643,7 +643,7 @@ details.cmd-detail > summary::-webkit-details-marker {
643
643
  .card-header h2 {
644
644
  margin-bottom: 0;
645
645
  }
646
- .copy-md-btn {
646
+ .copy-prompt-btn {
647
647
  background: var(--surface2);
648
648
  border: 1px solid var(--border);
649
649
  border-radius: 5px;
@@ -656,7 +656,7 @@ details.cmd-detail > summary::-webkit-details-marker {
656
656
  color 0.15s;
657
657
  white-space: nowrap;
658
658
  }
659
- .copy-md-btn:hover {
659
+ .copy-prompt-btn:hover {
660
660
  border-color: var(--accent-dim);
661
661
  color: var(--text);
662
662
  }
@@ -797,7 +797,8 @@ details.cmd-detail > summary::-webkit-details-marker {
797
797
  margin-top: 0.5rem;
798
798
  font-size: 0.72rem;
799
799
  color: var(--accent);
800
- text-decoration: none;
800
+ text-decoration: underline;
801
+ text-underline-offset: 2px;
801
802
  }
802
803
  .report-link:hover {
803
804
  text-decoration: underline;
@@ -805,6 +806,84 @@ details.cmd-detail > summary::-webkit-details-marker {
805
806
  .mcp-former {
806
807
  opacity: 0.4;
807
808
  }
809
+
810
+ /* ── MCP Registry Sections ──────────────────────────────── */
811
+ .mcp-section {
812
+ margin-top: 0.75rem;
813
+ }
814
+ .mcp-section > summary {
815
+ list-style: none;
816
+ }
817
+ .mcp-section > summary::-webkit-details-marker {
818
+ display: none;
819
+ }
820
+ .mcp-section > summary:hover {
821
+ color: var(--accent);
822
+ }
823
+ .mcp-recommended {
824
+ border-left: 2px solid var(--accent);
825
+ padding: 0.4rem 0.6rem;
826
+ margin-top: 0.35rem;
827
+ border-radius: 0 4px 4px 0;
828
+ background: var(--surface2);
829
+ }
830
+ .mcp-rec-badge {
831
+ font-size: 0.55rem;
832
+ font-weight: 600;
833
+ text-transform: uppercase;
834
+ letter-spacing: 0.04em;
835
+ padding: 0.1rem 0.35rem;
836
+ border-radius: 3px;
837
+ background: rgba(196, 149, 106, 0.15);
838
+ color: var(--accent);
839
+ border: 1px solid rgba(196, 149, 106, 0.3);
840
+ vertical-align: middle;
841
+ margin-left: 0.3rem;
842
+ }
843
+ .mcp-desc {
844
+ font-size: 0.7rem;
845
+ color: var(--text-dim);
846
+ margin-top: 0.15rem;
847
+ }
848
+ .mcp-reason {
849
+ font-size: 0.65rem;
850
+ color: var(--accent);
851
+ margin-top: 0.1rem;
852
+ font-style: italic;
853
+ }
854
+ .mcp-install {
855
+ display: block;
856
+ font-family: "SF Mono", "Fira Code", "JetBrains Mono", monospace;
857
+ font-size: 0.68rem;
858
+ color: var(--green);
859
+ background: var(--bg);
860
+ border: 1px solid var(--border);
861
+ border-radius: 4px;
862
+ padding: 0.3rem 0.5rem;
863
+ margin-top: 0.25rem;
864
+ cursor: pointer;
865
+ transition:
866
+ background 0.15s,
867
+ border-color 0.15s;
868
+ word-break: break-all;
869
+ }
870
+ .mcp-install:hover {
871
+ background: var(--surface2);
872
+ border-color: var(--accent-dim);
873
+ }
874
+ .mcp-available {
875
+ padding: 0.35rem 0.5rem;
876
+ margin-top: 0.3rem;
877
+ border-radius: 4px;
878
+ background: var(--surface2);
879
+ opacity: 0.85;
880
+ }
881
+ .mcp-registry-note {
882
+ font-size: 0.6rem;
883
+ color: var(--text-dim);
884
+ text-align: right;
885
+ margin-top: 0.75rem;
886
+ }
808
887
  .badge.mcp-former-badge {
809
888
  color: var(--text-dim);
810
889
  border-color: var(--border);
@@ -1175,7 +1254,7 @@ details.cmd-detail > summary::-webkit-details-marker {
1175
1254
  }
1176
1255
  .unconfigured-item .upath {
1177
1256
  font-size: 0.6rem;
1178
- color: #555;
1257
+ color: var(--text-dim);
1179
1258
  display: block;
1180
1259
  overflow: hidden;
1181
1260
  text-overflow: ellipsis;
@@ -13,20 +13,21 @@
13
13
 
14
14
  <!-- {{STATS_BAR}} -->
15
15
 
16
+ <main>
16
17
  <nav class="tab-nav">
17
- <button class="tab-btn active" data-tab="overview">Overview</button>
18
- <button class="tab-btn" data-tab="skills-mcp">Skills & MCP</button>
18
+ <button class="tab-btn active" data-tab="home">Home</button>
19
+ <button class="tab-btn" data-tab="config">Config</button>
19
20
  <button class="tab-btn" data-tab="analytics">Analytics</button>
20
21
  <button class="tab-btn" data-tab="repos">Repos</button>
21
22
  <button class="tab-btn" data-tab="reference">Reference</button>
22
23
  </nav>
23
24
 
24
- <div class="tab-content active" id="tab-overview">
25
- <!-- {{TAB_OVERVIEW}} -->
25
+ <div class="tab-content active" id="tab-home">
26
+ <!-- {{TAB_HOME}} -->
26
27
  </div>
27
28
 
28
- <div class="tab-content" id="tab-skills-mcp">
29
- <!-- {{TAB_SKILLS_MCP}} -->
29
+ <div class="tab-content" id="tab-config">
30
+ <!-- {{TAB_CONFIG}} -->
30
31
  </div>
31
32
 
32
33
  <div class="tab-content" id="tab-analytics">
@@ -42,6 +43,7 @@
42
43
  </div>
43
44
 
44
45
  <!-- {{FOOTER}} -->
46
+ </main>
45
47
 
46
48
  <div class="chart-tooltip" id="chart-tooltip"></div>
47
49
  <script>
@@ -133,19 +133,19 @@ function showToast(msg) {
133
133
  }, 2000);
134
134
  }
135
135
 
136
- // ── Copy Markdown button ────────────────────────────────────
137
- document.querySelectorAll(".copy-md-btn").forEach(function (btn) {
136
+ // ── Copy as Prompt button ────────────────────────────────────
137
+ document.querySelectorAll(".copy-prompt-btn").forEach(function (btn) {
138
138
  btn.addEventListener("click", function () {
139
- var card = btn.closest("[data-markdown]");
139
+ var card = btn.closest("[data-prompt]");
140
140
  if (!card) return;
141
- var md = card.dataset.markdown;
141
+ var prompt = card.dataset.prompt;
142
142
  navigator.clipboard
143
- .writeText(md)
143
+ .writeText(prompt)
144
144
  .then(function () {
145
- showToast("Markdown copied to clipboard");
145
+ showToast("Prompt copied \u2014 paste into Claude Code");
146
146
  })
147
147
  .catch(function () {
148
- showToast("Copy failed \u2014 use browser copy from the insights card");
148
+ showToast("Copy failed \u2014 try selecting the text manually");
149
149
  });
150
150
  });
151
151
  });
@@ -165,6 +165,24 @@ if (refreshBtn) {
165
165
  });
166
166
  }
167
167
 
168
+ // ── Click-to-copy MCP install commands ───────────────────
169
+ document.addEventListener("click", function (e) {
170
+ var install = e.target.closest(".mcp-install");
171
+ if (!install) return;
172
+ navigator.clipboard
173
+ .writeText(install.textContent)
174
+ .then(function () {
175
+ var orig = install.textContent;
176
+ install.textContent = "copied!";
177
+ setTimeout(function () {
178
+ install.textContent = orig;
179
+ }, 1500);
180
+ })
181
+ .catch(function () {
182
+ /* clipboard unavailable (file:// protocol, permissions) */
183
+ });
184
+ });
185
+
168
186
  // Custom tooltip for heatmap cells and peak bars
169
187
  var tip = document.getElementById("chart-tooltip");
170
188
  document.addEventListener("mouseover", function (e) {