@viren/claude-code-dashboard 0.0.2 → 0.0.4

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
@@ -33,7 +33,7 @@ Scans your home directory for git repos, collects Claude Code configuration (com
33
33
 
34
34
  ### Dark mode
35
35
 
36
- ![Dark Mode](screenshots/06-light-mode.png)
36
+ ![Dark Mode](screenshots/06-dark-mode.png)
37
37
 
38
38
  > Screenshots generated with `claude-code-dashboard --demo`
39
39
 
@@ -20,7 +20,16 @@ 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
34
  import { shortPath } from "./src/helpers.mjs";
26
35
  import { anonymizeAll } from "./src/anonymize.mjs";
@@ -46,6 +55,7 @@ import {
46
55
  parseProjectMcpConfig,
47
56
  findPromotionCandidates,
48
57
  scanHistoricalMcpServers,
58
+ classifyHistoricalServers,
49
59
  } from "./src/mcp.mjs";
50
60
  import { aggregateSessionMeta } from "./src/usage.mjs";
51
61
  import { handleInit } from "./src/templates.mjs";
@@ -69,7 +79,15 @@ if (cliArgs.demo) {
69
79
  const outputPath = cliArgs.output;
70
80
  mkdirSync(dirname(outputPath), { recursive: true });
71
81
  writeFileSync(outputPath, html);
72
- if (!cliArgs.quiet) console.log(outputPath);
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
+ }
73
91
 
74
92
  if (cliArgs.open) {
75
93
  const cmd =
@@ -185,7 +203,7 @@ for (const repo of configured) {
185
203
  const similar = configured
186
204
  .filter((r) => r !== repo)
187
205
  .map((r) => ({ name: r.name, similarity: computeConfigSimilarity(repo, r) }))
188
- .filter((r) => r.similarity >= 40)
206
+ .filter((r) => r.similarity >= SIMILARITY_THRESHOLD)
189
207
  .sort((a, b) => b.similarity - a.similarity)
190
208
  .slice(0, 2);
191
209
  repo.similarRepos = similar;
@@ -242,6 +260,7 @@ function parseChains() {
242
260
  const chains = parseChains();
243
261
 
244
262
  // MCP Server Discovery
263
+ const claudeJsonPath = join(HOME, ".claude.json");
245
264
  const allMcpServers = [];
246
265
 
247
266
  const userMcpPath = join(CLAUDE_DIR, "mcp_config.json");
@@ -254,6 +273,21 @@ if (existsSync(userMcpPath)) {
254
273
  }
255
274
  }
256
275
 
276
+ // ~/.claude.json is the primary location where `claude mcp add` writes
277
+ let claudeJsonParsed = null;
278
+ if (existsSync(claudeJsonPath)) {
279
+ try {
280
+ const content = readFileSync(claudeJsonPath, "utf8");
281
+ claudeJsonParsed = JSON.parse(content);
282
+ const existing = new Set(allMcpServers.filter((s) => s.scope === "user").map((s) => s.name));
283
+ for (const s of parseUserMcpConfig(content)) {
284
+ if (!existing.has(s.name)) allMcpServers.push(s);
285
+ }
286
+ } catch {
287
+ // skip if unreadable
288
+ }
289
+ }
290
+
257
291
  for (const repoDir of allRepoPaths) {
258
292
  const mcpPath = join(repoDir, ".mcp.json");
259
293
  if (existsSync(mcpPath)) {
@@ -272,11 +306,9 @@ for (const repoDir of allRepoPaths) {
272
306
 
273
307
  // Disabled MCP servers
274
308
  const disabledMcpByRepo = {};
275
- const claudeJsonPath = join(HOME, ".claude.json");
276
- if (existsSync(claudeJsonPath)) {
309
+ if (claudeJsonParsed) {
277
310
  try {
278
- const claudeJsonContent = readFileSync(claudeJsonPath, "utf8");
279
- const claudeJson = JSON.parse(claudeJsonContent);
311
+ const claudeJson = claudeJsonParsed;
280
312
  for (const [path, entry] of Object.entries(claudeJson)) {
281
313
  if (
282
314
  typeof entry === "object" &&
@@ -317,19 +349,41 @@ for (const s of allMcpServers) {
317
349
  for (const entry of Object.values(mcpByName)) {
318
350
  entry.disabledIn = disabledByServer[entry.name] || 0;
319
351
  }
352
+
353
+ const historicalMcpMap = scanHistoricalMcpServers(CLAUDE_DIR);
354
+ const currentMcpNames = new Set(allMcpServers.map((s) => s.name));
355
+ const { recent: recentMcpServers, former: formerMcpServers } = classifyHistoricalServers(
356
+ historicalMcpMap,
357
+ currentMcpNames,
358
+ );
359
+
360
+ // Normalize all historical project paths
361
+ for (const server of [...recentMcpServers, ...formerMcpServers]) {
362
+ server.projects = server.projects.map((p) => shortPath(p));
363
+ }
364
+
365
+ // Merge recently-seen servers into allMcpServers so they show up as current
366
+ for (const server of recentMcpServers) {
367
+ if (!mcpByName[server.name]) {
368
+ mcpByName[server.name] = {
369
+ name: server.name,
370
+ type: "unknown",
371
+ projects: server.projects,
372
+ userLevel: false,
373
+ disabledIn: disabledByServer[server.name] || 0,
374
+ recentlyActive: true,
375
+ };
376
+ }
377
+ }
320
378
  const mcpSummary = Object.values(mcpByName).sort((a, b) => {
321
379
  if (a.userLevel !== b.userLevel) return a.userLevel ? -1 : 1;
322
380
  return a.name.localeCompare(b.name);
323
381
  });
324
382
  const mcpCount = mcpSummary.length;
325
383
 
326
- const historicalMcpNames = scanHistoricalMcpServers(CLAUDE_DIR);
327
- const currentMcpNames = new Set(allMcpServers.map((s) => s.name));
328
- const formerMcpServers = historicalMcpNames.filter((name) => !currentMcpNames.has(name)).sort();
329
-
330
384
  // ── Usage Analytics ──────────────────────────────────────────────────────────
331
385
 
332
- const SESSION_META_LIMIT = 500;
386
+ const SESSION_META_LIMIT = 1000;
333
387
  const sessionMetaDir = join(CLAUDE_DIR, "usage-data", "session-meta");
334
388
  const sessionMetaFiles = [];
335
389
  if (existsSync(sessionMetaDir)) {
@@ -357,35 +411,98 @@ let ccusageData = null;
357
411
  const ccusageCachePath = join(CLAUDE_DIR, "ccusage-cache.json");
358
412
  const CCUSAGE_TTL_MS = 60 * 60 * 1000;
359
413
 
360
- if (!cliArgs.quiet) {
414
+ try {
415
+ const cached = JSON.parse(readFileSync(ccusageCachePath, "utf8"));
416
+ if (cached._ts && Date.now() - cached._ts < CCUSAGE_TTL_MS && cached.totals && cached.daily) {
417
+ ccusageData = cached;
418
+ }
419
+ } catch {
420
+ /* no cache or stale */
421
+ }
422
+
423
+ if (!ccusageData) {
361
424
  try {
362
- const cached = JSON.parse(readFileSync(ccusageCachePath, "utf8"));
363
- if (cached._ts && Date.now() - cached._ts < CCUSAGE_TTL_MS && cached.totals && cached.daily) {
364
- ccusageData = cached;
425
+ const raw = execFileSync("npx", ["ccusage", "--json"], {
426
+ encoding: "utf8",
427
+ timeout: 30_000,
428
+ stdio: ["pipe", "pipe", "pipe"],
429
+ });
430
+ const parsed = JSON.parse(raw);
431
+ if (parsed.totals && parsed.daily) {
432
+ ccusageData = parsed;
433
+ try {
434
+ writeFileSync(ccusageCachePath, JSON.stringify({ ...parsed, _ts: Date.now() }));
435
+ } catch {
436
+ /* non-critical */
437
+ }
365
438
  }
366
439
  } catch {
367
- /* no cache or stale */
440
+ // ccusage not installed or timed out
368
441
  }
442
+ }
369
443
 
370
- if (!ccusageData) {
371
- try {
372
- const raw = execFileSync("npx", ["ccusage", "--json"], {
373
- encoding: "utf8",
374
- timeout: 30_000,
375
- stdio: ["pipe", "pipe", "pipe"],
444
+ // Claude Code Insights report (generated by /insights)
445
+ let insightsReport = null;
446
+ const reportPath = join(CLAUDE_DIR, "usage-data", "report.html");
447
+ if (existsSync(reportPath)) {
448
+ try {
449
+ const reportHtml = readFileSync(reportPath, "utf8");
450
+
451
+ // Extract subtitle — reformat ISO dates to readable format
452
+ const subtitleMatch = reportHtml.match(/<p class="subtitle">([^<]+)<\/p>/);
453
+ let subtitle = subtitleMatch ? subtitleMatch[1] : null;
454
+ if (subtitle) {
455
+ subtitle = subtitle.replace(/(\d{4})-(\d{2})-(\d{2})/g, (_, y, m2, d) => {
456
+ const dt = new Date(`${y}-${m2}-${d}T00:00:00Z`);
457
+ return dt.toLocaleDateString("en-US", {
458
+ month: "short",
459
+ day: "numeric",
460
+ year: "numeric",
461
+ timeZone: "UTC",
462
+ });
376
463
  });
377
- const parsed = JSON.parse(raw);
378
- if (parsed.totals && parsed.daily) {
379
- ccusageData = parsed;
380
- try {
381
- writeFileSync(ccusageCachePath, JSON.stringify({ ...parsed, _ts: Date.now() }));
382
- } catch {
383
- /* non-critical */
384
- }
385
- }
386
- } catch {
387
- // ccusage not installed or timed out
388
464
  }
465
+
466
+ // Extract glance sections (content may contain <strong> tags)
467
+ const glanceSections = [];
468
+ const glanceRe =
469
+ /<div class="glance-section"><strong>([^<]+)<\/strong>\s*([\s\S]*?)<a[^>]*class="see-more"/g;
470
+ let m;
471
+ while ((m = glanceRe.exec(reportHtml)) !== null) {
472
+ const text = m[2].replace(/<[^>]+>/g, "").trim();
473
+ glanceSections.push({ label: m[1].replace(/:$/, ""), text });
474
+ }
475
+
476
+ // Extract stats
477
+ const statsRe = /<div class="stat-value">([^<]+)<\/div><div class="stat-label">([^<]+)<\/div>/g;
478
+ const reportStats = [];
479
+ while ((m = statsRe.exec(reportHtml)) !== null) {
480
+ const value = m[1];
481
+ const label = m[2];
482
+ // Mark lines stat for diff-style rendering
483
+ const isDiff = /^[+-]/.test(value) && value.includes("/");
484
+ reportStats.push({ value, label, isDiff });
485
+ }
486
+
487
+ // Extract friction categories
488
+ const frictionRe =
489
+ /<div class="friction-title">([^<]+)<\/div>\s*<div class="friction-desc">([^<]+)<\/div>/g;
490
+ const frictionPoints = [];
491
+ while ((m = frictionRe.exec(reportHtml)) !== null) {
492
+ frictionPoints.push({ title: m[1], desc: m[2] });
493
+ }
494
+
495
+ if (glanceSections.length > 0 || reportStats.length > 0) {
496
+ insightsReport = {
497
+ subtitle,
498
+ glance: glanceSections,
499
+ stats: reportStats,
500
+ friction: frictionPoints.slice(0, 3),
501
+ filePath: reportPath,
502
+ };
503
+ }
504
+ } catch {
505
+ // skip if unreadable
389
506
  }
390
507
  }
391
508
 
@@ -423,6 +540,19 @@ if (sessionMetaFiles.length > 0) {
423
540
  }
424
541
  }
425
542
 
543
+ // Supplement dailyActivity with ccusage data (fills gaps like Feb 17-22)
544
+ if (ccusageData && ccusageData.daily) {
545
+ const existingDates = new Set((statsCache.dailyActivity || []).map((d) => d.date));
546
+ const ccusageSupplemental = ccusageData.daily
547
+ .filter((d) => d.date && !existingDates.has(d.date) && d.totalTokens > 0)
548
+ .map((d) => ({ date: d.date, messageCount: Math.max(1, Math.round(d.totalTokens / 10000)) }));
549
+ if (ccusageSupplemental.length > 0) {
550
+ statsCache.dailyActivity = [...(statsCache.dailyActivity || []), ...ccusageSupplemental].sort(
551
+ (a, b) => a.date.localeCompare(b.date),
552
+ );
553
+ }
554
+ }
555
+
426
556
  // ── Computed Stats ───────────────────────────────────────────────────────────
427
557
 
428
558
  const totalRepos = allRepoPaths.length;
@@ -438,6 +568,107 @@ const driftCount = configured.filter(
438
568
  (r) => r.drift && (r.drift.level === "medium" || r.drift.level === "high"),
439
569
  ).length;
440
570
 
571
+ // ── Insights ──────────────────────────────────────────────────────────────────
572
+ const insights = [];
573
+
574
+ // Drift alerts
575
+ const highDriftRepos = configured.filter((r) => r.drift?.level === "high");
576
+ if (highDriftRepos.length > 0) {
577
+ insights.push({
578
+ type: "warning",
579
+ title: `${highDriftRepos.length} repo${highDriftRepos.length > 1 ? "s have" : " has"} high config drift`,
580
+ detail: highDriftRepos
581
+ .map((r) => `${r.name} (${r.drift.commitsSince} commits since config update)`)
582
+ .join(", "),
583
+ action: "Review and update CLAUDE.md in these repos",
584
+ });
585
+ }
586
+
587
+ // Coverage
588
+ if (unconfigured.length > 0 && totalRepos > 0) {
589
+ const pct = Math.round((unconfigured.length / totalRepos) * 100);
590
+ if (pct >= 40) {
591
+ const withStack = unconfigured.filter((r) => r.techStack?.length > 0).slice(0, 3);
592
+ insights.push({
593
+ type: "info",
594
+ title: `${unconfigured.length} repos unconfigured (${pct}%)`,
595
+ detail: withStack.length
596
+ ? `Top candidates: ${withStack.map((r) => `${r.name} (${r.techStack.join(", ")})`).join(", ")}`
597
+ : "",
598
+ action: "Run claude-code-dashboard init --template <stack> in these repos",
599
+ });
600
+ }
601
+ }
602
+
603
+ // MCP promotions
604
+ if (mcpPromotions.length > 0) {
605
+ insights.push({
606
+ type: "promote",
607
+ title: `${mcpPromotions.length} MCP server${mcpPromotions.length > 1 ? "s" : ""} could be promoted to global`,
608
+ detail: mcpPromotions.map((p) => `${p.name} (in ${p.projects.length} projects)`).join(", "),
609
+ action: "Add to ~/.claude/mcp_config.json for all projects",
610
+ });
611
+ }
612
+
613
+ // Redundant project-scope MCP configs (global server also in project .mcp.json)
614
+ const redundantMcp = Object.values(mcpByName).filter((s) => s.userLevel && s.projects.length > 0);
615
+ if (redundantMcp.length > 0) {
616
+ insights.push({
617
+ type: "tip",
618
+ title: `${redundantMcp.length} MCP server${redundantMcp.length > 1 ? "s are" : " is"} global but also in project .mcp.json`,
619
+ detail: redundantMcp.map((s) => `${s.name} (${s.projects.join(", ")})`).join("; "),
620
+ action: "Remove from project .mcp.json — global config already covers all projects",
621
+ });
622
+ }
623
+
624
+ // Skill sharing opportunities
625
+ const skillMatchCounts = {};
626
+ for (const r of configured) {
627
+ for (const sk of r.matchedSkills || []) {
628
+ const skName = typeof sk === "string" ? sk : sk.name;
629
+ if (!skillMatchCounts[skName]) skillMatchCounts[skName] = [];
630
+ skillMatchCounts[skName].push(r.name);
631
+ }
632
+ }
633
+ const widelyRelevant = Object.entries(skillMatchCounts)
634
+ .filter(([, repos]) => repos.length >= 3)
635
+ .sort((a, b) => b[1].length - a[1].length);
636
+ if (widelyRelevant.length > 0) {
637
+ const top = widelyRelevant.slice(0, 3);
638
+ insights.push({
639
+ type: "info",
640
+ title: `${widelyRelevant.length} skill${widelyRelevant.length > 1 ? "s" : ""} relevant across 3+ repos`,
641
+ detail: top.map(([name, repos]) => `${name} (${repos.length} repos)`).join(", "),
642
+ action: "Consider adding these skills to your global config",
643
+ });
644
+ }
645
+
646
+ // Health quick wins — repos closest to next tier
647
+ const quickWinRepos = configured
648
+ .filter((r) => r.healthScore > 0 && r.healthScore < 80 && r.healthReasons?.length > 0)
649
+ .sort((a, b) => b.healthScore - a.healthScore)
650
+ .slice(0, 3);
651
+ if (quickWinRepos.length > 0) {
652
+ insights.push({
653
+ type: "tip",
654
+ title: "Quick wins to improve config health",
655
+ detail: quickWinRepos
656
+ .map((r) => `${r.name} (${r.healthScore}/100): ${r.healthReasons[0]}`)
657
+ .join("; "),
658
+ action: "Small changes for measurable improvement",
659
+ });
660
+ }
661
+
662
+ // Insights report nudge
663
+ if (!insightsReport) {
664
+ insights.push({
665
+ type: "info",
666
+ title: "Generate your Claude Code Insights report",
667
+ detail: "Get personalized usage patterns, friction points, and feature suggestions",
668
+ action: "Run /insights in Claude Code",
669
+ });
670
+ }
671
+
441
672
  const now = new Date();
442
673
  const timestamp =
443
674
  now
@@ -644,6 +875,8 @@ const html = generateDashboardHtml({
644
875
  driftCount,
645
876
  mcpCount,
646
877
  scanScope,
878
+ insights,
879
+ insightsReport,
647
880
  });
648
881
 
649
882
  // ── Write HTML Output ────────────────────────────────────────────────────────
@@ -651,7 +884,21 @@ const html = generateDashboardHtml({
651
884
  const outputPath = cliArgs.output;
652
885
  mkdirSync(dirname(outputPath), { recursive: true });
653
886
  writeFileSync(outputPath, html);
654
- if (!cliArgs.quiet) console.log(outputPath);
887
+
888
+ if (!cliArgs.quiet) {
889
+ const sp = shortPath(outputPath);
890
+ console.log(`\n claude-code-dashboard v${VERSION}\n`);
891
+ console.log(
892
+ ` ${configuredCount} configured · ${unconfiguredCount} unconfigured · ${totalRepos} repos`,
893
+ );
894
+ console.log(
895
+ ` ${globalCmds.length} global commands · ${globalSkills.length} skills · ${mcpCount} MCP servers`,
896
+ );
897
+ console.log(`\n ✓ ${sp}`);
898
+ if (cliArgs.open) console.log(` ✓ opening in browser`);
899
+ console.log(`\n ${REPO_URL}`);
900
+ console.log();
901
+ }
655
902
 
656
903
  if (cliArgs.open) {
657
904
  const cmd =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viren/claude-code-dashboard",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "A visual dashboard for your Claude Code configuration across all repos",
5
5
  "type": "module",
6
6
  "bin": {
package/src/anonymize.mjs CHANGED
@@ -205,9 +205,13 @@ export function anonymizeAll({
205
205
  });
206
206
  }
207
207
 
208
- // Former MCP servers — anonymize names
208
+ // Former MCP servers — anonymize names and projects
209
209
  for (let i = 0; i < formerMcpServers.length; i++) {
210
- formerMcpServers[i] = `former-server-${i + 1}`;
210
+ formerMcpServers[i] = {
211
+ name: `former-server-${i + 1}`,
212
+ projects: (formerMcpServers[i].projects || []).map(() => `~/project-${i + 1}`),
213
+ lastSeen: formerMcpServers[i].lastSeen,
214
+ };
211
215
  }
212
216
 
213
217
  // Consolidation groups
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,
@@ -40,7 +40,8 @@ Options:
40
40
  --output, -o <path> Output path (default: ~/.claude/dashboard.html)
41
41
  --json Output full data model as JSON instead of HTML
42
42
  --catalog Generate a shareable skill catalog HTML page
43
- --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
44
45
  --quiet Suppress output, just write file
45
46
  --watch Regenerate on file changes
46
47
  --diff Show changes since last generation
@@ -86,6 +87,9 @@ Config file: ~/.claude/dashboard.conf
86
87
  case "--open":
87
88
  args.open = true;
88
89
  break;
90
+ case "--no-open":
91
+ args.open = false;
92
+ break;
89
93
  case "--template":
90
94
  case "-t":
91
95
  args.template = argv[++i];
@@ -129,11 +133,11 @@ export function generateCompletions() {
129
133
  # eval "$(claude-code-dashboard --completions)"
130
134
  if [ -n "$ZSH_VERSION" ]; then
131
135
  _claude_code_dashboard() {
132
- local -a opts; opts=(init lint --output --open --json --catalog --quiet --watch --diff --anonymize --demo --completions --help --version)
136
+ local -a opts; opts=(init lint --output --open --no-open --json --catalog --quiet --watch --diff --anonymize --demo --completions --help --version)
133
137
  if (( CURRENT == 2 )); then _describe 'option' opts; fi
134
138
  }; compdef _claude_code_dashboard claude-code-dashboard
135
139
  elif [ -n "$BASH_VERSION" ]; then
136
- _claude_code_dashboard() { COMPREPLY=( $(compgen -W "init lint --output --open --json --catalog --quiet --watch --diff --anonymize --demo --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]}") ); }
137
141
  complete -F _claude_code_dashboard claude-code-dashboard
138
142
  fi`);
139
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.2";
4
+ export const VERSION = "0.0.4";
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;
package/src/demo.mjs CHANGED
@@ -407,9 +407,20 @@ export function generateDemoData() {
407
407
  disabledIn: 0,
408
408
  },
409
409
  { name: "sentry", type: "http", projects: [], userLevel: true, disabledIn: 0 },
410
+ {
411
+ name: "figma",
412
+ type: "stdio",
413
+ projects: ["~/work/acme-web"],
414
+ userLevel: false,
415
+ disabledIn: 0,
416
+ recentlyActive: true,
417
+ },
410
418
  ],
411
419
  mcpPromotions: [{ name: "github", projects: ["~/work/acme-web", "~/work/payments-api"] }],
412
- formerMcpServers: ["redis", "datadog"],
420
+ formerMcpServers: [
421
+ { name: "redis", projects: ["~/work/cache-service"], lastSeen: null },
422
+ { name: "datadog", projects: [], lastSeen: null },
423
+ ],
413
424
  consolidationGroups: [
414
425
  {
415
426
  stack: "next",
@@ -473,7 +484,70 @@ export function generateDemoData() {
473
484
  ),
474
485
  driftCount: configured.filter((r) => r.drift.level === "medium" || r.drift.level === "high")
475
486
  .length,
476
- mcpCount: 4,
487
+ mcpCount: 5,
477
488
  scanScope: "~/work (depth 5)",
489
+ insights: [
490
+ {
491
+ type: "warning",
492
+ title: "2 repos have high config drift",
493
+ detail:
494
+ "payments-api (23 commits since config update), acme-web (18 commits since config update)",
495
+ action: "Review and update CLAUDE.md in these repos",
496
+ },
497
+ {
498
+ type: "promote",
499
+ title: "1 MCP server could be promoted to global",
500
+ detail: "github (in 2 projects)",
501
+ action: "Add to ~/.claude/mcp_config.json for all projects",
502
+ },
503
+ {
504
+ type: "info",
505
+ title: "12 repos unconfigured (52%)",
506
+ detail: "Top candidates: mobile-app (expo), admin-portal (next), data-pipeline (python)",
507
+ action: "Run claude-code-dashboard init --template <stack> in these repos",
508
+ },
509
+ {
510
+ type: "tip",
511
+ title: "Quick wins to improve config health",
512
+ detail:
513
+ "design-system (65/100): add commands; shared-utils (60/100): add CLAUDE.md description",
514
+ action: "Small changes for measurable improvement",
515
+ },
516
+ ],
517
+ insightsReport: {
518
+ subtitle: "1,386 messages across 117 sessions (365 total) | 2026-02-23 to 2026-03-10",
519
+ stats: [
520
+ { value: "1,386", label: "Messages" },
521
+ { value: "+33,424/-2,563", label: "Lines" },
522
+ { value: "632", label: "Files" },
523
+ { value: "14", label: "Days" },
524
+ { value: "99", label: "Msgs/Day" },
525
+ ],
526
+ glance: [
527
+ {
528
+ label: "What's working",
529
+ text: "Full end-to-end shipping workflow — implementation through PR creation to production deployment in single sessions.",
530
+ },
531
+ {
532
+ label: "What's hindering you",
533
+ text: "Claude frequently jumps into fixes without checking actual state first, costing correction cycles.",
534
+ },
535
+ {
536
+ label: "Quick wins to try",
537
+ text: "Create custom slash commands for repeated workflows like PR reviews and Slack message drafting.",
538
+ },
539
+ ],
540
+ friction: [
541
+ {
542
+ title: "Wrong Target / Misidentification",
543
+ desc: "Claude acts on the wrong file or setting before you catch the mistake.",
544
+ },
545
+ {
546
+ title: "Premature Solutions",
547
+ desc: "Jumps into fixes without first checking actual state of the codebase.",
548
+ },
549
+ ],
550
+ filePath: "~/.claude/usage-data/report.html",
551
+ },
478
552
  };
479
553
  }