@viren/claude-code-dashboard 0.0.1 → 0.0.2
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 +33 -0
- package/generate-dashboard.mjs +34 -5
- package/package.json +1 -1
- package/src/anonymize.mjs +218 -0
- package/src/cli.mjs +8 -3
- package/src/constants.mjs +1 -1
- package/src/demo.mjs +479 -0
package/README.md
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
# claude-code-dashboard
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@viren/claude-code-dashboard)
|
|
4
|
+
[](https://github.com/VirenMohindra/claude-code-dashboard/actions/workflows/ci.yml)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](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
|
+

|
|
17
|
+
|
|
18
|
+
### Skills with auto-categorization and MCP server discovery
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
### Usage analytics — tool usage, languages, activity heatmap
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
### Repo cards with search, grouping, and consolidation hints
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
### Expanded repo — commands, rules, health score, matched skills
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
### Dark mode
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
> Screenshots generated with `claude-code-dashboard --demo`
|
|
39
|
+
|
|
7
40
|
## Features
|
|
8
41
|
|
|
9
42
|
### Core
|
package/generate-dashboard.mjs
CHANGED
|
@@ -22,7 +22,9 @@ import { join, basename, dirname } from "path";
|
|
|
22
22
|
|
|
23
23
|
import { VERSION, HOME, CLAUDE_DIR, DEFAULT_OUTPUT, CONF, MAX_DEPTH } from "./src/constants.mjs";
|
|
24
24
|
import { parseArgs, generateCompletions } from "./src/cli.mjs";
|
|
25
|
-
import { shortPath
|
|
25
|
+
import { shortPath } from "./src/helpers.mjs";
|
|
26
|
+
import { anonymizeAll } from "./src/anonymize.mjs";
|
|
27
|
+
import { generateDemoData } from "./src/demo.mjs";
|
|
26
28
|
import { findGitRepos, getScanRoots } from "./src/discovery.mjs";
|
|
27
29
|
import { extractProjectDesc, extractSections, scanMdDir } from "./src/markdown.mjs";
|
|
28
30
|
import { scanSkillsDir, groupSkillsByCategory } from "./src/skills.mjs";
|
|
@@ -58,6 +60,25 @@ const cliArgs = parseArgs(process.argv);
|
|
|
58
60
|
if (cliArgs.completions) generateCompletions();
|
|
59
61
|
if (cliArgs.command === "init") handleInit(cliArgs);
|
|
60
62
|
|
|
63
|
+
// ── Demo Mode ────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
if (cliArgs.demo) {
|
|
66
|
+
const demoData = generateDemoData();
|
|
67
|
+
const html = generateDashboardHtml(demoData);
|
|
68
|
+
|
|
69
|
+
const outputPath = cliArgs.output;
|
|
70
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
71
|
+
writeFileSync(outputPath, html);
|
|
72
|
+
if (!cliArgs.quiet) console.log(outputPath);
|
|
73
|
+
|
|
74
|
+
if (cliArgs.open) {
|
|
75
|
+
const cmd =
|
|
76
|
+
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
77
|
+
execFile(cmd, [outputPath]);
|
|
78
|
+
}
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
// ── Collect Everything ───────────────────────────────────────────────────────
|
|
62
83
|
|
|
63
84
|
const scanRoots = getScanRoots();
|
|
@@ -479,10 +500,18 @@ if (cliArgs.diff) {
|
|
|
479
500
|
// ── Anonymize ────────────────────────────────────────────────────────────────
|
|
480
501
|
|
|
481
502
|
if (cliArgs.anonymize) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
503
|
+
anonymizeAll({
|
|
504
|
+
configured,
|
|
505
|
+
unconfigured,
|
|
506
|
+
globalCmds,
|
|
507
|
+
globalRules,
|
|
508
|
+
globalSkills,
|
|
509
|
+
chains,
|
|
510
|
+
mcpSummary,
|
|
511
|
+
mcpPromotions,
|
|
512
|
+
formerMcpServers,
|
|
513
|
+
consolidationGroups,
|
|
514
|
+
});
|
|
486
515
|
}
|
|
487
516
|
|
|
488
517
|
// ── JSON Output ──────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -0,0 +1,218 @@
|
|
|
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
|
|
209
|
+
for (let i = 0; i < formerMcpServers.length; i++) {
|
|
210
|
+
formerMcpServers[i] = `former-server-${i + 1}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Consolidation groups
|
|
214
|
+
for (const group of consolidationGroups) {
|
|
215
|
+
group.repos = (group.repos || []).map((n) => mapName(nameMap, n));
|
|
216
|
+
group.suggestion = `${group.repos.length} ${group.stack} repos with ${group.avgSimilarity}% avg similarity — consider shared global rules`;
|
|
217
|
+
}
|
|
218
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -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
|
|
@@ -43,7 +44,8 @@ Options:
|
|
|
43
44
|
--quiet Suppress output, just write file
|
|
44
45
|
--watch Regenerate on file changes
|
|
45
46
|
--diff Show changes since last generation
|
|
46
|
-
--anonymize Anonymize
|
|
47
|
+
--anonymize Anonymize all data for shareable export
|
|
48
|
+
--demo Generate dashboard with sample data (no scanning)
|
|
47
49
|
--completions Output shell completion script for bash/zsh
|
|
48
50
|
--version, -v Show version
|
|
49
51
|
--help, -h Show this help
|
|
@@ -107,6 +109,9 @@ Config file: ~/.claude/dashboard.conf
|
|
|
107
109
|
case "--anonymize":
|
|
108
110
|
args.anonymize = true;
|
|
109
111
|
break;
|
|
112
|
+
case "--demo":
|
|
113
|
+
args.demo = true;
|
|
114
|
+
break;
|
|
110
115
|
case "--completions":
|
|
111
116
|
args.completions = true;
|
|
112
117
|
break;
|
|
@@ -124,11 +129,11 @@ export function generateCompletions() {
|
|
|
124
129
|
# eval "$(claude-code-dashboard --completions)"
|
|
125
130
|
if [ -n "$ZSH_VERSION" ]; then
|
|
126
131
|
_claude_code_dashboard() {
|
|
127
|
-
local -a opts; opts=(init lint --output --open --json --catalog --quiet --watch --diff --anonymize --completions --help --version)
|
|
132
|
+
local -a opts; opts=(init lint --output --open --json --catalog --quiet --watch --diff --anonymize --demo --completions --help --version)
|
|
128
133
|
if (( CURRENT == 2 )); then _describe 'option' opts; fi
|
|
129
134
|
}; compdef _claude_code_dashboard claude-code-dashboard
|
|
130
135
|
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]}") ); }
|
|
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]}") ); }
|
|
132
137
|
complete -F _claude_code_dashboard claude-code-dashboard
|
|
133
138
|
fi`);
|
|
134
139
|
process.exit(0);
|
package/src/constants.mjs
CHANGED
package/src/demo.mjs
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate realistic fake data for --demo flag.
|
|
3
|
+
* Produces a complete data object ready for generateDashboardHtml().
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function daysAgo(n) {
|
|
7
|
+
return Math.floor(Date.now() / 1000) - n * 86400;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DEMO_CONFIGURED = [
|
|
11
|
+
{
|
|
12
|
+
key: "acme-web",
|
|
13
|
+
name: "acme-web",
|
|
14
|
+
path: "~/work/acme-web",
|
|
15
|
+
shortPath: "~/work/acme-web",
|
|
16
|
+
commands: [
|
|
17
|
+
{ name: "dev", desc: "Start dev server with hot reload", filepath: "" },
|
|
18
|
+
{ name: "test", desc: "Run vitest suite with coverage", filepath: "" },
|
|
19
|
+
{ name: "deploy", desc: "Deploy to staging via Vercel", filepath: "" },
|
|
20
|
+
{ name: "lint", desc: "Run ESLint + Prettier check", filepath: "" },
|
|
21
|
+
],
|
|
22
|
+
rules: [
|
|
23
|
+
{ name: "architecture", desc: "App router conventions and component patterns", filepath: "" },
|
|
24
|
+
{
|
|
25
|
+
name: "testing",
|
|
26
|
+
desc: "Test behavior not implementation, use MSW for mocks",
|
|
27
|
+
filepath: "",
|
|
28
|
+
},
|
|
29
|
+
{ name: "styling", desc: "Tailwind utility-first, no inline styles", filepath: "" },
|
|
30
|
+
],
|
|
31
|
+
desc: [
|
|
32
|
+
"Customer-facing web application built with Next.js 15 and React Server Components.",
|
|
33
|
+
"Uses Supabase for auth and database, deployed on Vercel.",
|
|
34
|
+
],
|
|
35
|
+
sections: [
|
|
36
|
+
{
|
|
37
|
+
name: "Architecture",
|
|
38
|
+
preview: [
|
|
39
|
+
"App router with RSC",
|
|
40
|
+
"Shared components in /ui",
|
|
41
|
+
"Feature modules in /features",
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "Testing",
|
|
46
|
+
preview: ["Vitest for unit tests", "Playwright for E2E", "MSW for API mocking"],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "Deployment",
|
|
50
|
+
preview: ["Preview deploys on PRs", "Staging auto-deploy from main"],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
freshness: daysAgo(3),
|
|
54
|
+
freshnessText: "3 days ago",
|
|
55
|
+
freshnessClass: "fresh",
|
|
56
|
+
techStack: ["next", "react"],
|
|
57
|
+
healthScore: 95,
|
|
58
|
+
healthReasons: ["Has CLAUDE.md", "Has commands", "Has rules", "Recently updated"],
|
|
59
|
+
hasAgentsFile: true,
|
|
60
|
+
configPattern: "modular",
|
|
61
|
+
drift: { level: "synced", commitsSince: 0 },
|
|
62
|
+
similarRepos: [{ name: "marketing-site", similarity: 72 }],
|
|
63
|
+
matchedSkills: [{ name: "e2e-test" }, { name: "react-doctor" }],
|
|
64
|
+
mcpServers: [
|
|
65
|
+
{ name: "playwright", type: "stdio", scope: "project", source: "~/work/acme-web" },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: "payments-api",
|
|
70
|
+
name: "payments-api",
|
|
71
|
+
path: "~/work/payments-api",
|
|
72
|
+
shortPath: "~/work/payments-api",
|
|
73
|
+
commands: [
|
|
74
|
+
{ name: "test", desc: "Run pytest with coverage", filepath: "" },
|
|
75
|
+
{ name: "migrate", desc: "Run alembic migrations", filepath: "" },
|
|
76
|
+
{ name: "serve", desc: "Start FastAPI dev server", filepath: "" },
|
|
77
|
+
],
|
|
78
|
+
rules: [
|
|
79
|
+
{ name: "security", desc: "Input validation on all endpoints, no raw SQL", filepath: "" },
|
|
80
|
+
{
|
|
81
|
+
name: "error-handling",
|
|
82
|
+
desc: "Structured errors with codes, never expose internals",
|
|
83
|
+
filepath: "",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
desc: ["Payment processing API handling Stripe integration and subscription management."],
|
|
87
|
+
sections: [
|
|
88
|
+
{
|
|
89
|
+
name: "Security",
|
|
90
|
+
preview: ["Validate all inputs with Pydantic", "Rate limiting on sensitive endpoints"],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "Database",
|
|
94
|
+
preview: ["PostgreSQL via SQLAlchemy", "Alembic for migrations"],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
freshness: daysAgo(12),
|
|
98
|
+
freshnessText: "12 days ago",
|
|
99
|
+
freshnessClass: "fresh",
|
|
100
|
+
techStack: ["python"],
|
|
101
|
+
healthScore: 80,
|
|
102
|
+
healthReasons: ["Has CLAUDE.md", "Has commands", "Has rules"],
|
|
103
|
+
hasAgentsFile: true,
|
|
104
|
+
configPattern: "modular",
|
|
105
|
+
drift: { level: "low", commitsSince: 4 },
|
|
106
|
+
similarRepos: [],
|
|
107
|
+
matchedSkills: [{ name: "systematic-debugging" }],
|
|
108
|
+
mcpServers: [],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
key: "mobile-app",
|
|
112
|
+
name: "mobile-app",
|
|
113
|
+
path: "~/work/mobile-app",
|
|
114
|
+
shortPath: "~/work/mobile-app",
|
|
115
|
+
commands: [
|
|
116
|
+
{ name: "ios", desc: "Run on iOS simulator", filepath: "" },
|
|
117
|
+
{ name: "android", desc: "Run on Android emulator", filepath: "" },
|
|
118
|
+
{ name: "test", desc: "Run Jest test suite", filepath: "" },
|
|
119
|
+
],
|
|
120
|
+
rules: [
|
|
121
|
+
{ name: "navigation", desc: "React Navigation v7 patterns and deep linking", filepath: "" },
|
|
122
|
+
],
|
|
123
|
+
desc: ["Cross-platform mobile app built with Expo and React Native."],
|
|
124
|
+
sections: [
|
|
125
|
+
{
|
|
126
|
+
name: "Navigation",
|
|
127
|
+
preview: ["File-based routing via expo-router", "Deep link config in app.json"],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
freshness: daysAgo(45),
|
|
131
|
+
freshnessText: "1 month ago",
|
|
132
|
+
freshnessClass: "aging",
|
|
133
|
+
techStack: ["expo", "react"],
|
|
134
|
+
healthScore: 60,
|
|
135
|
+
healthReasons: ["Has CLAUDE.md", "Has commands"],
|
|
136
|
+
hasAgentsFile: true,
|
|
137
|
+
configPattern: "monolithic",
|
|
138
|
+
drift: { level: "medium", commitsSince: 18 },
|
|
139
|
+
similarRepos: [{ name: "acme-web", similarity: 45 }],
|
|
140
|
+
matchedSkills: [{ name: "react-doctor" }],
|
|
141
|
+
mcpServers: [],
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
key: "infra-tools",
|
|
145
|
+
name: "infra-tools",
|
|
146
|
+
path: "~/work/infra-tools",
|
|
147
|
+
shortPath: "~/work/infra-tools",
|
|
148
|
+
commands: [
|
|
149
|
+
{ name: "build", desc: "Build all Go binaries", filepath: "" },
|
|
150
|
+
{ name: "test", desc: "Run go test ./...", filepath: "" },
|
|
151
|
+
],
|
|
152
|
+
rules: [],
|
|
153
|
+
desc: ["Internal CLI tools for infrastructure automation."],
|
|
154
|
+
sections: [{ name: "Build", preview: ["Go 1.22", "Multi-binary workspace layout"] }],
|
|
155
|
+
freshness: daysAgo(90),
|
|
156
|
+
freshnessText: "3 months ago",
|
|
157
|
+
freshnessClass: "stale",
|
|
158
|
+
techStack: ["go"],
|
|
159
|
+
healthScore: 40,
|
|
160
|
+
healthReasons: ["Has CLAUDE.md", "Has commands"],
|
|
161
|
+
hasAgentsFile: true,
|
|
162
|
+
configPattern: "minimal",
|
|
163
|
+
drift: { level: "high", commitsSince: 34 },
|
|
164
|
+
similarRepos: [],
|
|
165
|
+
matchedSkills: [],
|
|
166
|
+
mcpServers: [],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: "marketing-site",
|
|
170
|
+
name: "marketing-site",
|
|
171
|
+
path: "~/work/marketing-site",
|
|
172
|
+
shortPath: "~/work/marketing-site",
|
|
173
|
+
commands: [
|
|
174
|
+
{ name: "dev", desc: "Start Next.js dev server", filepath: "" },
|
|
175
|
+
{ name: "build", desc: "Build static export", filepath: "" },
|
|
176
|
+
],
|
|
177
|
+
rules: [
|
|
178
|
+
{ name: "content", desc: "All copy comes from CMS, never hardcode text", filepath: "" },
|
|
179
|
+
],
|
|
180
|
+
desc: ["Public marketing website with blog and documentation."],
|
|
181
|
+
sections: [
|
|
182
|
+
{ name: "Content", preview: ["MDX for blog posts", "Contentlayer for type-safe content"] },
|
|
183
|
+
],
|
|
184
|
+
freshness: daysAgo(7),
|
|
185
|
+
freshnessText: "1 week ago",
|
|
186
|
+
freshnessClass: "fresh",
|
|
187
|
+
techStack: ["next"],
|
|
188
|
+
healthScore: 70,
|
|
189
|
+
healthReasons: ["Has CLAUDE.md", "Has commands", "Has rules"],
|
|
190
|
+
hasAgentsFile: true,
|
|
191
|
+
configPattern: "modular",
|
|
192
|
+
drift: { level: "low", commitsSince: 2 },
|
|
193
|
+
similarRepos: [{ name: "acme-web", similarity: 68 }],
|
|
194
|
+
matchedSkills: [{ name: "e2e-test" }],
|
|
195
|
+
mcpServers: [],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
key: "shared-ui",
|
|
199
|
+
name: "shared-ui",
|
|
200
|
+
path: "~/work/shared-ui",
|
|
201
|
+
shortPath: "~/work/shared-ui",
|
|
202
|
+
commands: [
|
|
203
|
+
{ name: "storybook", desc: "Start Storybook dev server", filepath: "" },
|
|
204
|
+
{ name: "build", desc: "Build and publish to internal registry", filepath: "" },
|
|
205
|
+
],
|
|
206
|
+
rules: [
|
|
207
|
+
{ name: "components", desc: "All components must have stories and a11y tests", filepath: "" },
|
|
208
|
+
],
|
|
209
|
+
desc: ["Shared component library used across web projects."],
|
|
210
|
+
sections: [
|
|
211
|
+
{
|
|
212
|
+
name: "Components",
|
|
213
|
+
preview: ["Radix primitives", "Tailwind variants", "Storybook for documentation"],
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
freshness: daysAgo(14),
|
|
217
|
+
freshnessText: "2 weeks ago",
|
|
218
|
+
freshnessClass: "fresh",
|
|
219
|
+
techStack: ["react"],
|
|
220
|
+
healthScore: 75,
|
|
221
|
+
healthReasons: ["Has CLAUDE.md", "Has commands", "Has rules"],
|
|
222
|
+
hasAgentsFile: true,
|
|
223
|
+
configPattern: "modular",
|
|
224
|
+
drift: { level: "synced", commitsSince: 0 },
|
|
225
|
+
similarRepos: [{ name: "acme-web", similarity: 55 }],
|
|
226
|
+
matchedSkills: [],
|
|
227
|
+
mcpServers: [],
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const DEMO_UNCONFIGURED = [
|
|
232
|
+
{
|
|
233
|
+
key: "data-scripts",
|
|
234
|
+
name: "data-scripts",
|
|
235
|
+
path: "~/work/data-scripts",
|
|
236
|
+
shortPath: "~/work/data-scripts",
|
|
237
|
+
techStack: ["python"],
|
|
238
|
+
suggestions: ["Add CLAUDE.md with project overview", "Add commands for common tasks"],
|
|
239
|
+
exemplarName: "payments-api",
|
|
240
|
+
mcpServers: [],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
key: "legacy-admin",
|
|
244
|
+
name: "legacy-admin",
|
|
245
|
+
path: "~/work/legacy-admin",
|
|
246
|
+
shortPath: "~/work/legacy-admin",
|
|
247
|
+
techStack: ["react"],
|
|
248
|
+
suggestions: ["Add CLAUDE.md", "Add architecture rules"],
|
|
249
|
+
exemplarName: "acme-web",
|
|
250
|
+
mcpServers: [],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
key: "ops-runbooks",
|
|
254
|
+
name: "ops-runbooks",
|
|
255
|
+
path: "~/work/ops-runbooks",
|
|
256
|
+
shortPath: "~/work/ops-runbooks",
|
|
257
|
+
techStack: [],
|
|
258
|
+
suggestions: [],
|
|
259
|
+
exemplarName: "",
|
|
260
|
+
mcpServers: [],
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
const DEMO_GLOBAL_CMDS = [
|
|
265
|
+
{ name: "commit", desc: "Stage changes and create a conventional commit", filepath: "" },
|
|
266
|
+
{ name: "review-pr", desc: "Fetch PR diff and perform code review", filepath: "" },
|
|
267
|
+
{ name: "dashboard", desc: "Generate and open the config dashboard", filepath: "" },
|
|
268
|
+
{ name: "test-all", desc: "Run full test suite with coverage report", filepath: "" },
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const DEMO_GLOBAL_RULES = [
|
|
272
|
+
{ name: "git-workflow", desc: "Branch naming, commit conventions, PR process", filepath: "" },
|
|
273
|
+
{
|
|
274
|
+
name: "code-standards",
|
|
275
|
+
desc: "Architecture principles, error handling, quality gates",
|
|
276
|
+
filepath: "",
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "communication",
|
|
280
|
+
desc: "Slack message formatting, draft-before-send policy",
|
|
281
|
+
filepath: "",
|
|
282
|
+
},
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
const DEMO_GLOBAL_SKILLS = [
|
|
286
|
+
{
|
|
287
|
+
name: "e2e-test",
|
|
288
|
+
desc: "Run Playwright E2E tests against a branch",
|
|
289
|
+
filepath: "",
|
|
290
|
+
source: { type: "custom" },
|
|
291
|
+
category: "code-quality",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "react-doctor",
|
|
295
|
+
desc: "Catch common React bugs and performance issues",
|
|
296
|
+
filepath: "",
|
|
297
|
+
source: { type: "skills.sh", repo: "community/react-tools" },
|
|
298
|
+
category: "debugging",
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "systematic-debugging",
|
|
302
|
+
desc: "Structured root cause analysis with diagnostic plans",
|
|
303
|
+
filepath: "",
|
|
304
|
+
source: { type: "superpowers", repo: "obra/superpowers-skills" },
|
|
305
|
+
category: "debugging",
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "writing-plans",
|
|
309
|
+
desc: "Create implementation plans from specs before coding",
|
|
310
|
+
filepath: "",
|
|
311
|
+
source: { type: "superpowers", repo: "obra/superpowers-skills" },
|
|
312
|
+
category: "workflow",
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: "code-review",
|
|
316
|
+
desc: "Review PR diffs for correctness, style, and security",
|
|
317
|
+
filepath: "",
|
|
318
|
+
source: { type: "superpowers", repo: "obra/superpowers-skills" },
|
|
319
|
+
category: "code-quality",
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "find-session",
|
|
323
|
+
desc: "Search past Claude Code sessions by keyword or date",
|
|
324
|
+
filepath: "",
|
|
325
|
+
source: { type: "custom" },
|
|
326
|
+
category: "research",
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "slack-digest",
|
|
330
|
+
desc: "Extract action items and decisions from Slack threads",
|
|
331
|
+
filepath: "",
|
|
332
|
+
source: { type: "custom" },
|
|
333
|
+
category: "integrations",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "deploy-staging",
|
|
337
|
+
desc: "Deploy current branch to staging environment",
|
|
338
|
+
filepath: "",
|
|
339
|
+
source: { type: "custom" },
|
|
340
|
+
category: "workflow",
|
|
341
|
+
},
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
function generateDemoHeatmap() {
|
|
345
|
+
const days = [];
|
|
346
|
+
const now = new Date();
|
|
347
|
+
for (let i = 364; i >= 0; i--) {
|
|
348
|
+
const d = new Date(now);
|
|
349
|
+
d.setDate(d.getDate() - i);
|
|
350
|
+
const date = d.toISOString().slice(0, 10);
|
|
351
|
+
// Weighted random: more activity on weekdays
|
|
352
|
+
const isWeekday = d.getDay() > 0 && d.getDay() < 6;
|
|
353
|
+
const base = isWeekday ? 8 : 2;
|
|
354
|
+
const messageCount = Math.floor(Math.random() * base * 3);
|
|
355
|
+
if (messageCount > 0) days.push({ date, messageCount });
|
|
356
|
+
}
|
|
357
|
+
return days;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function generateDemoHourCounts() {
|
|
361
|
+
const counts = {};
|
|
362
|
+
// Peak at 10am and 2pm
|
|
363
|
+
for (let h = 0; h < 24; h++) {
|
|
364
|
+
const peak = Math.exp(-((h - 10) ** 2) / 18) + Math.exp(-((h - 14) ** 2) / 12);
|
|
365
|
+
counts[String(h)] = Math.round(peak * 120 + Math.random() * 20);
|
|
366
|
+
}
|
|
367
|
+
return counts;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function generateDemoData() {
|
|
371
|
+
const now = new Date();
|
|
372
|
+
const timestamp =
|
|
373
|
+
now
|
|
374
|
+
.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
|
|
375
|
+
.toLowerCase() +
|
|
376
|
+
" at " +
|
|
377
|
+
now.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).toLowerCase();
|
|
378
|
+
|
|
379
|
+
const configured = DEMO_CONFIGURED;
|
|
380
|
+
const unconfigured = DEMO_UNCONFIGURED;
|
|
381
|
+
const totalRepos = configured.length + unconfigured.length;
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
configured,
|
|
385
|
+
unconfigured,
|
|
386
|
+
globalCmds: DEMO_GLOBAL_CMDS,
|
|
387
|
+
globalRules: DEMO_GLOBAL_RULES,
|
|
388
|
+
globalSkills: DEMO_GLOBAL_SKILLS,
|
|
389
|
+
chains: [
|
|
390
|
+
{ nodes: ["shared-ui", "acme-web", "marketing-site"], arrow: "→" },
|
|
391
|
+
{ nodes: ["payments-api", "acme-web"], arrow: "→" },
|
|
392
|
+
],
|
|
393
|
+
mcpSummary: [
|
|
394
|
+
{ name: "playwright", type: "stdio", projects: [], userLevel: true, disabledIn: 0 },
|
|
395
|
+
{
|
|
396
|
+
name: "github",
|
|
397
|
+
type: "stdio",
|
|
398
|
+
projects: ["~/work/acme-web", "~/work/payments-api"],
|
|
399
|
+
userLevel: false,
|
|
400
|
+
disabledIn: 0,
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: "postgres",
|
|
404
|
+
type: "stdio",
|
|
405
|
+
projects: ["~/work/payments-api"],
|
|
406
|
+
userLevel: false,
|
|
407
|
+
disabledIn: 0,
|
|
408
|
+
},
|
|
409
|
+
{ name: "sentry", type: "http", projects: [], userLevel: true, disabledIn: 0 },
|
|
410
|
+
],
|
|
411
|
+
mcpPromotions: [{ name: "github", projects: ["~/work/acme-web", "~/work/payments-api"] }],
|
|
412
|
+
formerMcpServers: ["redis", "datadog"],
|
|
413
|
+
consolidationGroups: [
|
|
414
|
+
{
|
|
415
|
+
stack: "next",
|
|
416
|
+
repos: ["acme-web", "marketing-site"],
|
|
417
|
+
avgSimilarity: 68,
|
|
418
|
+
suggestion: "2 next repos with 68% avg similarity — consider shared global rules",
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
stack: "react",
|
|
422
|
+
repos: ["acme-web", "shared-ui", "mobile-app"],
|
|
423
|
+
avgSimilarity: 45,
|
|
424
|
+
suggestion: "3 react repos with 45% avg similarity — consider shared global rules",
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
usageAnalytics: {
|
|
428
|
+
totalSessions: 247,
|
|
429
|
+
topTools: [
|
|
430
|
+
{ name: "Read", count: 1842 },
|
|
431
|
+
{ name: "Edit", count: 1356 },
|
|
432
|
+
{ name: "Bash", count: 987 },
|
|
433
|
+
{ name: "Grep", count: 654 },
|
|
434
|
+
{ name: "Write", count: 432 },
|
|
435
|
+
{ name: "Glob", count: 321 },
|
|
436
|
+
{ name: "Agent", count: 198 },
|
|
437
|
+
{ name: "WebSearch", count: 87 },
|
|
438
|
+
],
|
|
439
|
+
topLanguages: [
|
|
440
|
+
{ name: "TypeScript", count: 4521 },
|
|
441
|
+
{ name: "Python", count: 2103 },
|
|
442
|
+
{ name: "JavaScript", count: 1456 },
|
|
443
|
+
{ name: "Go", count: 432 },
|
|
444
|
+
{ name: "Markdown", count: 321 },
|
|
445
|
+
],
|
|
446
|
+
errorCategories: [
|
|
447
|
+
{ name: "lint_error", count: 45 },
|
|
448
|
+
{ name: "type_error", count: 32 },
|
|
449
|
+
{ name: "test_failure", count: 28 },
|
|
450
|
+
],
|
|
451
|
+
heavySessions: 12,
|
|
452
|
+
},
|
|
453
|
+
ccusageData: {
|
|
454
|
+
totals: { totalCost: 47.82, totalTokens: 28_450_000 },
|
|
455
|
+
daily: [],
|
|
456
|
+
},
|
|
457
|
+
statsCache: {
|
|
458
|
+
dailyActivity: generateDemoHeatmap(),
|
|
459
|
+
hourCounts: generateDemoHourCounts(),
|
|
460
|
+
modelUsage: {
|
|
461
|
+
"claude-sonnet-4-6": { inputTokens: 18_200_000, outputTokens: 6_800_000 },
|
|
462
|
+
"claude-haiku-4-5": { inputTokens: 2_400_000, outputTokens: 1_050_000 },
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
timestamp,
|
|
466
|
+
coveragePct: Math.round((configured.length / totalRepos) * 100),
|
|
467
|
+
totalRepos,
|
|
468
|
+
configuredCount: configured.length,
|
|
469
|
+
unconfiguredCount: unconfigured.length,
|
|
470
|
+
totalRepoCmds: configured.reduce((sum, r) => sum + r.commands.length, 0),
|
|
471
|
+
avgHealth: Math.round(
|
|
472
|
+
configured.reduce((sum, r) => sum + r.healthScore, 0) / configured.length,
|
|
473
|
+
),
|
|
474
|
+
driftCount: configured.filter((r) => r.drift.level === "medium" || r.drift.level === "high")
|
|
475
|
+
.length,
|
|
476
|
+
mcpCount: 4,
|
|
477
|
+
scanScope: "~/work (depth 5)",
|
|
478
|
+
};
|
|
479
|
+
}
|