@viren/claude-code-dashboard 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,42 @@
1
1
  # claude-code-dashboard
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@viren/claude-code-dashboard)](https://www.npmjs.com/package/@viren/claude-code-dashboard)
4
+ [![CI](https://github.com/VirenMohindra/claude-code-dashboard/actions/workflows/ci.yml/badge.svg)](https://github.com/VirenMohindra/claude-code-dashboard/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
7
+
3
8
  A visual dashboard for your [Claude Code](https://docs.anthropic.com/en/docs/claude-code) configuration across all repos.
4
9
 
5
10
  Scans your home directory for git repos, collects Claude Code configuration (commands, rules, skills, MCP servers, usage data), and generates a self-contained HTML dashboard.
6
11
 
12
+ ## Screenshots
13
+
14
+ ### Dashboard overview — stats, global commands, and rules
15
+
16
+ ![Overview](screenshots/01-overview.png)
17
+
18
+ ### Skills with auto-categorization and MCP server discovery
19
+
20
+ ![Skills and MCP](screenshots/02-skills-mcp.png)
21
+
22
+ ### Usage analytics — tool usage, languages, activity heatmap
23
+
24
+ ![Usage Analytics](screenshots/03-usage-analytics.png)
25
+
26
+ ### Repo cards with search, grouping, and consolidation hints
27
+
28
+ ![Repo Cards](screenshots/04-repo-cards.png)
29
+
30
+ ### Expanded repo — commands, rules, health score, matched skills
31
+
32
+ ![Repo Expanded](screenshots/05-repo-expanded.png)
33
+
34
+ ### Dark mode
35
+
36
+ ![Dark Mode](screenshots/06-dark-mode.png)
37
+
38
+ > Screenshots generated with `claude-code-dashboard --demo`
39
+
7
40
  ## Features
8
41
 
9
42
  ### Core
@@ -20,9 +20,20 @@ import { execFileSync, execFile } from "child_process";
20
20
  import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "fs";
21
21
  import { join, basename, dirname } from "path";
22
22
 
23
- import { VERSION, HOME, CLAUDE_DIR, DEFAULT_OUTPUT, CONF, MAX_DEPTH } from "./src/constants.mjs";
23
+ import {
24
+ VERSION,
25
+ HOME,
26
+ CLAUDE_DIR,
27
+ DEFAULT_OUTPUT,
28
+ CONF,
29
+ MAX_DEPTH,
30
+ REPO_URL,
31
+ SIMILARITY_THRESHOLD,
32
+ } from "./src/constants.mjs";
24
33
  import { parseArgs, generateCompletions } from "./src/cli.mjs";
25
- import { shortPath, anonymizePath } from "./src/helpers.mjs";
34
+ import { shortPath } from "./src/helpers.mjs";
35
+ import { anonymizeAll } from "./src/anonymize.mjs";
36
+ import { generateDemoData } from "./src/demo.mjs";
26
37
  import { findGitRepos, getScanRoots } from "./src/discovery.mjs";
27
38
  import { extractProjectDesc, extractSections, scanMdDir } from "./src/markdown.mjs";
28
39
  import { scanSkillsDir, groupSkillsByCategory } from "./src/skills.mjs";
@@ -44,6 +55,7 @@ import {
44
55
  parseProjectMcpConfig,
45
56
  findPromotionCandidates,
46
57
  scanHistoricalMcpServers,
58
+ classifyHistoricalServers,
47
59
  } from "./src/mcp.mjs";
48
60
  import { aggregateSessionMeta } from "./src/usage.mjs";
49
61
  import { handleInit } from "./src/templates.mjs";
@@ -58,6 +70,33 @@ const cliArgs = parseArgs(process.argv);
58
70
  if (cliArgs.completions) generateCompletions();
59
71
  if (cliArgs.command === "init") handleInit(cliArgs);
60
72
 
73
+ // ── Demo Mode ────────────────────────────────────────────────────────────────
74
+
75
+ if (cliArgs.demo) {
76
+ const demoData = generateDemoData();
77
+ const html = generateDashboardHtml(demoData);
78
+
79
+ const outputPath = cliArgs.output;
80
+ mkdirSync(dirname(outputPath), { recursive: true });
81
+ writeFileSync(outputPath, html);
82
+
83
+ if (!cliArgs.quiet) {
84
+ const sp = shortPath(outputPath);
85
+ console.log(`\n claude-code-dashboard v${VERSION} (demo mode)\n`);
86
+ console.log(` ✓ ${sp}`);
87
+ if (cliArgs.open) console.log(` ✓ opening in browser`);
88
+ console.log(`\n ${REPO_URL}`);
89
+ console.log();
90
+ }
91
+
92
+ if (cliArgs.open) {
93
+ const cmd =
94
+ process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
95
+ execFile(cmd, [outputPath]);
96
+ }
97
+ process.exit(0);
98
+ }
99
+
61
100
  // ── Collect Everything ───────────────────────────────────────────────────────
62
101
 
63
102
  const scanRoots = getScanRoots();
@@ -164,7 +203,7 @@ for (const repo of configured) {
164
203
  const similar = configured
165
204
  .filter((r) => r !== repo)
166
205
  .map((r) => ({ name: r.name, similarity: computeConfigSimilarity(repo, r) }))
167
- .filter((r) => r.similarity >= 40)
206
+ .filter((r) => r.similarity >= SIMILARITY_THRESHOLD)
168
207
  .sort((a, b) => b.similarity - a.similarity)
169
208
  .slice(0, 2);
170
209
  repo.similarRepos = similar;
@@ -296,19 +335,41 @@ for (const s of allMcpServers) {
296
335
  for (const entry of Object.values(mcpByName)) {
297
336
  entry.disabledIn = disabledByServer[entry.name] || 0;
298
337
  }
338
+
339
+ const historicalMcpMap = scanHistoricalMcpServers(CLAUDE_DIR);
340
+ const currentMcpNames = new Set(allMcpServers.map((s) => s.name));
341
+ const { recent: recentMcpServers, former: formerMcpServers } = classifyHistoricalServers(
342
+ historicalMcpMap,
343
+ currentMcpNames,
344
+ );
345
+
346
+ // Normalize all historical project paths
347
+ for (const server of [...recentMcpServers, ...formerMcpServers]) {
348
+ server.projects = server.projects.map((p) => shortPath(p));
349
+ }
350
+
351
+ // Merge recently-seen servers into allMcpServers so they show up as current
352
+ for (const server of recentMcpServers) {
353
+ if (!mcpByName[server.name]) {
354
+ mcpByName[server.name] = {
355
+ name: server.name,
356
+ type: "unknown",
357
+ projects: server.projects,
358
+ userLevel: false,
359
+ disabledIn: disabledByServer[server.name] || 0,
360
+ recentlyActive: true,
361
+ };
362
+ }
363
+ }
299
364
  const mcpSummary = Object.values(mcpByName).sort((a, b) => {
300
365
  if (a.userLevel !== b.userLevel) return a.userLevel ? -1 : 1;
301
366
  return a.name.localeCompare(b.name);
302
367
  });
303
368
  const mcpCount = mcpSummary.length;
304
369
 
305
- const historicalMcpNames = scanHistoricalMcpServers(CLAUDE_DIR);
306
- const currentMcpNames = new Set(allMcpServers.map((s) => s.name));
307
- const formerMcpServers = historicalMcpNames.filter((name) => !currentMcpNames.has(name)).sort();
308
-
309
370
  // ── Usage Analytics ──────────────────────────────────────────────────────────
310
371
 
311
- const SESSION_META_LIMIT = 500;
372
+ const SESSION_META_LIMIT = 1000;
312
373
  const sessionMetaDir = join(CLAUDE_DIR, "usage-data", "session-meta");
313
374
  const sessionMetaFiles = [];
314
375
  if (existsSync(sessionMetaDir)) {
@@ -479,10 +540,18 @@ if (cliArgs.diff) {
479
540
  // ── Anonymize ────────────────────────────────────────────────────────────────
480
541
 
481
542
  if (cliArgs.anonymize) {
482
- for (const repo of [...configured, ...unconfigured]) {
483
- repo.shortPath = anonymizePath(repo.shortPath);
484
- repo.path = anonymizePath(repo.path);
485
- }
543
+ anonymizeAll({
544
+ configured,
545
+ unconfigured,
546
+ globalCmds,
547
+ globalRules,
548
+ globalSkills,
549
+ chains,
550
+ mcpSummary,
551
+ mcpPromotions,
552
+ formerMcpServers,
553
+ consolidationGroups,
554
+ });
486
555
  }
487
556
 
488
557
  // ── JSON Output ──────────────────────────────────────────────────────────────
@@ -622,7 +691,21 @@ const html = generateDashboardHtml({
622
691
  const outputPath = cliArgs.output;
623
692
  mkdirSync(dirname(outputPath), { recursive: true });
624
693
  writeFileSync(outputPath, html);
625
- if (!cliArgs.quiet) console.log(outputPath);
694
+
695
+ if (!cliArgs.quiet) {
696
+ const sp = shortPath(outputPath);
697
+ console.log(`\n claude-code-dashboard v${VERSION}\n`);
698
+ console.log(
699
+ ` ${configuredCount} configured · ${unconfiguredCount} unconfigured · ${totalRepos} repos`,
700
+ );
701
+ console.log(
702
+ ` ${globalCmds.length} global commands · ${globalSkills.length} skills · ${mcpCount} MCP servers`,
703
+ );
704
+ console.log(`\n ✓ ${sp}`);
705
+ if (cliArgs.open) console.log(` ✓ opening in browser`);
706
+ console.log(`\n ${REPO_URL}`);
707
+ console.log();
708
+ }
626
709
 
627
710
  if (cliArgs.open) {
628
711
  const cmd =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viren/claude-code-dashboard",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "A visual dashboard for your Claude Code configuration across all repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Deep anonymization for --anonymize flag.
3
+ *
4
+ * Builds a stable name map (real name -> generic label) and applies it
5
+ * across every data structure that ends up in the HTML output.
6
+ */
7
+
8
+ const REPO_LABELS = [
9
+ "web-app",
10
+ "api-service",
11
+ "mobile-app",
12
+ "data-pipeline",
13
+ "admin-panel",
14
+ "shared-lib",
15
+ "cli-tool",
16
+ "docs-site",
17
+ "auth-service",
18
+ "analytics-engine",
19
+ "worker-queue",
20
+ "config-store",
21
+ "gateway",
22
+ "scheduler",
23
+ "notification-svc",
24
+ "search-index",
25
+ "media-service",
26
+ "payment-gateway",
27
+ "user-service",
28
+ "reporting-tool",
29
+ "internal-dashboard",
30
+ "test-harness",
31
+ "deploy-scripts",
32
+ "design-system",
33
+ "content-api",
34
+ "ml-pipeline",
35
+ "event-bus",
36
+ "cache-layer",
37
+ "log-aggregator",
38
+ "infra-tools",
39
+ ];
40
+
41
+ const PERSON_RE = /\b[A-Z][a-z]+\s[A-Z][a-z]+\b/g;
42
+ const GITHUB_HANDLE_RE = /@[a-zA-Z0-9][-a-zA-Z0-9]{0,38}\b/g;
43
+
44
+ function anonymizePath(p) {
45
+ return p
46
+ .replace(/^\/Users\/[^/]+\//, "~/")
47
+ .replace(/^\/home\/[^/]+\//, "~/")
48
+ .replace(/^C:\\Users\\[^\\]+\\/, "~\\")
49
+ .replace(/^C:\/Users\/[^/]+\//, "~/");
50
+ }
51
+
52
+ function buildNameMap(configured, unconfigured) {
53
+ const map = new Map();
54
+ let idx = 0;
55
+ for (const repo of [...configured, ...unconfigured]) {
56
+ if (!map.has(repo.name)) {
57
+ const label = idx < REPO_LABELS.length ? REPO_LABELS[idx] : `project-${idx + 1}`;
58
+ map.set(repo.name, label);
59
+ idx++;
60
+ }
61
+ }
62
+
63
+ // Extract username from home dir paths to anonymize it too
64
+ for (const repo of [...configured, ...unconfigured]) {
65
+ const m = repo.path.match(/^\/(?:Users|home)\/([^/]+)\//);
66
+ if (m && m[1] && !map.has(m[1])) {
67
+ map.set(m[1], "user");
68
+ break;
69
+ }
70
+ }
71
+
72
+ return map;
73
+ }
74
+
75
+ function mapName(nameMap, name) {
76
+ return nameMap.get(name) || name;
77
+ }
78
+
79
+ /**
80
+ * Replace person names, GitHub handles, and all known repo names in text.
81
+ * Uses case-insensitive replacement to catch "Salsa", "salsa", "SALSA" etc.
82
+ */
83
+ function anonymizeText(text, nameMap) {
84
+ let result = text.replace(PERSON_RE, "[name]").replace(GITHUB_HANDLE_RE, "@[user]");
85
+ if (nameMap) {
86
+ const sorted = [...nameMap.entries()].sort((a, b) => b[0].length - a[0].length);
87
+ for (const [real, anon] of sorted) {
88
+ if (real.length < 3) continue;
89
+ const re = new RegExp(escapeRegex(real), "gi");
90
+ result = result.replace(re, anon);
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+
96
+ function escapeRegex(s) {
97
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
98
+ }
99
+
100
+ function anonymizeMdItems(items) {
101
+ return items.map((item, i) => ({
102
+ ...item,
103
+ name: `item-${String(i + 1).padStart(2, "0")}`,
104
+ desc: "...",
105
+ filepath: "", // prevent render functions from re-reading file content
106
+ }));
107
+ }
108
+
109
+ /**
110
+ * Anonymize all data structures in-place before HTML generation.
111
+ */
112
+ export function anonymizeAll({
113
+ configured,
114
+ unconfigured,
115
+ globalCmds,
116
+ globalRules,
117
+ globalSkills,
118
+ chains,
119
+ mcpSummary,
120
+ mcpPromotions,
121
+ formerMcpServers,
122
+ consolidationGroups,
123
+ }) {
124
+ const nameMap = buildNameMap(configured, unconfigured);
125
+
126
+ // Repos
127
+ for (const repo of [...configured, ...unconfigured]) {
128
+ const anonName = mapName(nameMap, repo.name);
129
+ repo.name = anonName;
130
+ repo.key = anonName;
131
+ repo.path = anonymizePath(repo.path).replace(/[^/]+$/, anonName);
132
+ repo.shortPath = anonymizePath(repo.shortPath).replace(/[^/]+$/, anonName);
133
+
134
+ // Description — redact entirely (contains arbitrary project-specific text)
135
+ repo.desc = repo.desc?.length ? ["[project description redacted]"] : [];
136
+
137
+ // Sections — keep headings (anonymized), redact preview content
138
+ for (const section of repo.sections || []) {
139
+ section.name = anonymizeText(section.name, nameMap);
140
+ section.preview = ["..."];
141
+ }
142
+
143
+ // Commands & rules
144
+ repo.commands = anonymizeMdItems(repo.commands || []);
145
+ repo.rules = anonymizeMdItems(repo.rules || []);
146
+
147
+ // Similar repos
148
+ repo.similarRepos = (repo.similarRepos || []).map((r) => ({
149
+ ...r,
150
+ name: mapName(nameMap, r.name),
151
+ }));
152
+
153
+ // Exemplar name
154
+ if (repo.exemplarName) {
155
+ repo.exemplarName = mapName(nameMap, repo.exemplarName);
156
+ }
157
+
158
+ // Suggestions text — may reference exemplar name
159
+ if (repo.suggestions) {
160
+ repo.suggestions = repo.suggestions.map((s) => anonymizeText(s, nameMap));
161
+ }
162
+
163
+ // MCP servers per repo
164
+ for (const mcp of repo.mcpServers || []) {
165
+ if (mcp.source) {
166
+ const repoName = mcp.source.split("/").pop();
167
+ const anonPath = anonymizePath(mcp.source);
168
+ mcp.source = anonPath.replace(/[^/]+$/, mapName(nameMap, repoName));
169
+ }
170
+ }
171
+ }
172
+
173
+ // Global commands, rules
174
+ globalCmds.splice(0, globalCmds.length, ...anonymizeMdItems(globalCmds));
175
+ globalRules.splice(0, globalRules.length, ...anonymizeMdItems(globalRules));
176
+
177
+ // Global skills — anonymize name, redact description + filepath
178
+ for (let i = 0; i < globalSkills.length; i++) {
179
+ const skill = globalSkills[i];
180
+ skill.name = `skill-${String(i + 1).padStart(2, "0")}`;
181
+ skill.desc = "...";
182
+ skill.filepath = "";
183
+ }
184
+
185
+ // Chains — anonymize node names (may have extra text like "name (backend)")
186
+ for (const chain of chains) {
187
+ chain.nodes = chain.nodes.map((n) => anonymizeText(n.trim(), nameMap));
188
+ }
189
+
190
+ // MCP summary — anonymize project paths
191
+ for (const mcp of mcpSummary) {
192
+ mcp.projects = (mcp.projects || []).map((p) => {
193
+ const anonPath = anonymizePath(p);
194
+ const repoName = p.split("/").pop();
195
+ return anonPath.replace(/[^/]+$/, mapName(nameMap, repoName));
196
+ });
197
+ }
198
+
199
+ // MCP promotions — anonymize project paths
200
+ for (const promo of mcpPromotions) {
201
+ promo.projects = (promo.projects || []).map((p) => {
202
+ const anonPath = anonymizePath(p);
203
+ const repoName = p.split("/").pop();
204
+ return anonPath.replace(/[^/]+$/, mapName(nameMap, repoName));
205
+ });
206
+ }
207
+
208
+ // Former MCP servers — anonymize names and projects
209
+ for (let i = 0; i < formerMcpServers.length; i++) {
210
+ formerMcpServers[i] = {
211
+ name: `former-server-${i + 1}`,
212
+ projects: (formerMcpServers[i].projects || []).map(() => `~/project-${i + 1}`),
213
+ lastSeen: formerMcpServers[i].lastSeen,
214
+ };
215
+ }
216
+
217
+ // Consolidation groups
218
+ for (const group of consolidationGroups) {
219
+ group.repos = (group.repos || []).map((n) => mapName(nameMap, n));
220
+ group.suggestion = `${group.repos.length} ${group.stack} repos with ${group.avgSimilarity}% avg similarity — consider shared global rules`;
221
+ }
222
+ }
package/src/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import { VERSION, DEFAULT_OUTPUT, HOME } from "./constants.mjs";
3
3
  export function parseArgs(argv) {
4
4
  const args = {
5
5
  output: DEFAULT_OUTPUT,
6
- open: false,
6
+ open: process.stdout.isTTY !== false,
7
7
  json: false,
8
8
  catalog: false,
9
9
  command: null,
@@ -13,6 +13,7 @@ export function parseArgs(argv) {
13
13
  watch: false,
14
14
  diff: false,
15
15
  anonymize: false,
16
+ demo: false,
16
17
  completions: false,
17
18
  };
18
19
  let i = 2; // skip node + script
@@ -39,11 +40,13 @@ Options:
39
40
  --output, -o <path> Output path (default: ~/.claude/dashboard.html)
40
41
  --json Output full data model as JSON instead of HTML
41
42
  --catalog Generate a shareable skill catalog HTML page
42
- --open Open the dashboard in your default browser after generating
43
+ --open Open in browser after generating (default: true)
44
+ --no-open Skip opening in browser
43
45
  --quiet Suppress output, just write file
44
46
  --watch Regenerate on file changes
45
47
  --diff Show changes since last generation
46
- --anonymize Anonymize paths for shareable export
48
+ --anonymize Anonymize all data for shareable export
49
+ --demo Generate dashboard with sample data (no scanning)
47
50
  --completions Output shell completion script for bash/zsh
48
51
  --version, -v Show version
49
52
  --help, -h Show this help
@@ -84,6 +87,9 @@ Config file: ~/.claude/dashboard.conf
84
87
  case "--open":
85
88
  args.open = true;
86
89
  break;
90
+ case "--no-open":
91
+ args.open = false;
92
+ break;
87
93
  case "--template":
88
94
  case "-t":
89
95
  args.template = argv[++i];
@@ -107,6 +113,9 @@ Config file: ~/.claude/dashboard.conf
107
113
  case "--anonymize":
108
114
  args.anonymize = true;
109
115
  break;
116
+ case "--demo":
117
+ args.demo = true;
118
+ break;
110
119
  case "--completions":
111
120
  args.completions = true;
112
121
  break;
@@ -124,11 +133,11 @@ export function generateCompletions() {
124
133
  # eval "$(claude-code-dashboard --completions)"
125
134
  if [ -n "$ZSH_VERSION" ]; then
126
135
  _claude_code_dashboard() {
127
- local -a opts; opts=(init lint --output --open --json --catalog --quiet --watch --diff --anonymize --completions --help --version)
136
+ local -a opts; opts=(init lint --output --open --no-open --json --catalog --quiet --watch --diff --anonymize --demo --completions --help --version)
128
137
  if (( CURRENT == 2 )); then _describe 'option' opts; fi
129
138
  }; compdef _claude_code_dashboard claude-code-dashboard
130
139
  elif [ -n "$BASH_VERSION" ]; then
131
- _claude_code_dashboard() { COMPREPLY=( $(compgen -W "init lint --output --open --json --catalog --quiet --watch --diff --anonymize --completions --help --version" -- "\${COMP_WORDS[COMP_CWORD]}") ); }
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]}") ); }
132
141
  complete -F _claude_code_dashboard claude-code-dashboard
133
142
  fi`);
134
143
  process.exit(0);
package/src/constants.mjs CHANGED
@@ -1,13 +1,16 @@
1
1
  import { join } from "path";
2
2
  import { homedir } from "os";
3
3
 
4
- export const VERSION = "0.0.1";
4
+ export const VERSION = "0.0.3";
5
+ export const REPO_URL = "https://github.com/VirenMohindra/claude-code-dashboard";
5
6
 
6
7
  export const HOME = homedir();
7
8
  export const CLAUDE_DIR = join(HOME, ".claude");
8
9
  export const DEFAULT_OUTPUT = join(CLAUDE_DIR, "dashboard.html");
9
10
  export const CONF = join(CLAUDE_DIR, "dashboard.conf");
10
11
  export const MAX_DEPTH = 5;
12
+ export const MAX_SESSION_SCAN = 1000;
13
+ export const SIMILARITY_THRESHOLD = 25;
11
14
 
12
15
  // Freshness thresholds (seconds)
13
16
  export const ONE_DAY = 86_400;