@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.
- package/generate-dashboard.mjs +12 -3
- package/package.json +1 -1
- package/src/assembler.mjs +19 -14
- package/src/cli.mjs +7 -2
- package/src/constants.mjs +39 -1
- package/src/demo.mjs +78 -0
- package/src/helpers.mjs +98 -14
- package/src/mcp.mjs +97 -2
- package/src/pipeline.mjs +134 -1
- package/src/sections.mjs +64 -27
- package/template/dashboard.css +86 -7
- package/template/dashboard.html +8 -6
- package/template/dashboard.js +25 -7
package/generate-dashboard.mjs
CHANGED
|
@@ -43,7 +43,12 @@ import {
|
|
|
43
43
|
computeDashboardDiff,
|
|
44
44
|
} from "./src/analysis.mjs";
|
|
45
45
|
import { getFreshness } from "./src/freshness.mjs";
|
|
46
|
-
import {
|
|
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
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,
|
|
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">↻ 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:
|
|
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
|
-
//
|
|
76
|
-
const
|
|
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
|
|
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("<!-- {{
|
|
147
|
-
html = html.replace("<!-- {{
|
|
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.
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
44
|
-
export function
|
|
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
|
|
122
|
+
const sections = [];
|
|
47
123
|
for (const i of insights) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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 {
|
|
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
|
|
25
|
-
if (!
|
|
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,
|
|
380
|
+
export function renderInsightsCard(insights, prompt) {
|
|
334
381
|
if (!insights || !insights.length) return "";
|
|
335
|
-
const
|
|
336
|
-
return `<div class="card insight-card"${
|
|
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
|
-
${
|
|
386
|
+
${prompt ? `<button class="copy-prompt-btn" title="Copy as a prompt for Claude Code">📋 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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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="
|
|
409
|
-
|
|
410
|
-
|
|
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
|
}
|
package/template/dashboard.css
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
--surface2: #1a1a1a;
|
|
5
5
|
--border: #262626;
|
|
6
6
|
--text: #e5e5e5;
|
|
7
|
-
--text-dim: #
|
|
7
|
+
--text-dim: #999;
|
|
8
8
|
--accent: #c4956a;
|
|
9
|
-
--accent-dim: #
|
|
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: #
|
|
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-
|
|
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-
|
|
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:
|
|
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:
|
|
1257
|
+
color: var(--text-dim);
|
|
1179
1258
|
display: block;
|
|
1180
1259
|
overflow: hidden;
|
|
1181
1260
|
text-overflow: ellipsis;
|
package/template/dashboard.html
CHANGED
|
@@ -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="
|
|
18
|
-
<button class="tab-btn" data-tab="
|
|
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-
|
|
25
|
-
<!-- {{
|
|
25
|
+
<div class="tab-content active" id="tab-home">
|
|
26
|
+
<!-- {{TAB_HOME}} -->
|
|
26
27
|
</div>
|
|
27
28
|
|
|
28
|
-
<div class="tab-content" id="tab-
|
|
29
|
-
<!-- {{
|
|
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>
|
package/template/dashboard.js
CHANGED
|
@@ -133,19 +133,19 @@ function showToast(msg) {
|
|
|
133
133
|
}, 2000);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
// ── Copy
|
|
137
|
-
document.querySelectorAll(".copy-
|
|
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-
|
|
139
|
+
var card = btn.closest("[data-prompt]");
|
|
140
140
|
if (!card) return;
|
|
141
|
-
var
|
|
141
|
+
var prompt = card.dataset.prompt;
|
|
142
142
|
navigator.clipboard
|
|
143
|
-
.writeText(
|
|
143
|
+
.writeText(prompt)
|
|
144
144
|
.then(function () {
|
|
145
|
-
showToast("
|
|
145
|
+
showToast("Prompt copied \u2014 paste into Claude Code");
|
|
146
146
|
})
|
|
147
147
|
.catch(function () {
|
|
148
|
-
showToast("Copy failed \u2014
|
|
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) {
|