@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 +1 -1
- package/generate-dashboard.mjs +282 -35
- package/package.json +1 -1
- package/src/anonymize.mjs +6 -2
- package/src/cli.mjs +8 -4
- package/src/constants.mjs +4 -1
- package/src/demo.mjs +76 -2
- package/src/html-template.mjs +565 -363
- package/src/mcp.mjs +105 -14
- package/src/render.mjs +9 -0
- package/src/watch.mjs +2 -2
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
|
-

|
|
37
37
|
|
|
38
38
|
> Screenshots generated with `claude-code-dashboard --demo`
|
|
39
39
|
|
package/generate-dashboard.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 >=
|
|
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
|
-
|
|
276
|
-
if (existsSync(claudeJsonPath)) {
|
|
309
|
+
if (claudeJsonParsed) {
|
|
277
310
|
try {
|
|
278
|
-
const
|
|
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 =
|
|
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
|
-
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
440
|
+
// ccusage not installed or timed out
|
|
368
441
|
}
|
|
442
|
+
}
|
|
369
443
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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
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] =
|
|
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
|
|
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.
|
|
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: [
|
|
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:
|
|
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
|
}
|