@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 +33 -0
- package/generate-dashboard.mjs +96 -13
- package/package.json +1 -1
- package/src/anonymize.mjs +222 -0
- package/src/cli.mjs +14 -5
- package/src/constants.mjs +4 -1
- package/src/demo.mjs +490 -0
- package/src/html-template.mjs +425 -355
- package/src/mcp.mjs +105 -14
- package/src/render.mjs +9 -0
package/src/mcp.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
+
import { MAX_SESSION_SCAN } from "./constants.mjs";
|
|
3
4
|
|
|
4
5
|
export function parseUserMcpConfig(content) {
|
|
5
6
|
try {
|
|
@@ -46,30 +47,85 @@ export function findPromotionCandidates(servers) {
|
|
|
46
47
|
.sort((a, b) => b.projects.length - a.projects.length || a.name.localeCompare(b.name));
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Scan file-history snapshots for MCP server usage, enriched with project paths
|
|
52
|
+
* and timestamps from session-meta. Returns a map of server name → metadata.
|
|
53
|
+
*
|
|
54
|
+
* Each entry: { name, projects: Set<string>, lastSeen: Date|null }
|
|
55
|
+
*/
|
|
49
56
|
export function scanHistoricalMcpServers(claudeDir) {
|
|
50
|
-
const historical = new Set();
|
|
51
57
|
const fileHistoryDir = join(claudeDir, "file-history");
|
|
52
|
-
if (!existsSync(fileHistoryDir)) return
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
if (!existsSync(fileHistoryDir)) return new Map();
|
|
59
|
+
|
|
60
|
+
// Build session → { projectPath, startTime } lookup from session-meta
|
|
61
|
+
const sessionMeta = new Map();
|
|
62
|
+
const metaDir = join(claudeDir, "usage-data", "session-meta");
|
|
63
|
+
if (existsSync(metaDir)) {
|
|
64
|
+
try {
|
|
65
|
+
for (const file of readdirSync(metaDir)) {
|
|
66
|
+
if (!file.endsWith(".json")) continue;
|
|
67
|
+
try {
|
|
68
|
+
const meta = JSON.parse(readFileSync(join(metaDir, file), "utf8"));
|
|
69
|
+
const sessionId = file.replace(/\.json$/, "");
|
|
70
|
+
sessionMeta.set(sessionId, {
|
|
71
|
+
projectPath: meta.project_path || null,
|
|
72
|
+
startTime: meta.start_time ? new Date(meta.start_time) : null,
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
/* skip malformed meta */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
/* skip unreadable meta dir */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const servers = new Map(); // name → { name, projects: Set, lastSeen: Date|null }
|
|
84
|
+
|
|
56
85
|
try {
|
|
57
|
-
const sessionDirs = readdirSync(fileHistoryDir).sort().slice(-
|
|
86
|
+
const sessionDirs = readdirSync(fileHistoryDir).sort().slice(-MAX_SESSION_SCAN);
|
|
58
87
|
for (const sessionDir of sessionDirs) {
|
|
59
|
-
if (filesRead >= MAX_FILES_TOTAL) break;
|
|
60
88
|
const sessionPath = join(fileHistoryDir, sessionDir);
|
|
61
|
-
if (!statSync(sessionPath).isDirectory()) continue;
|
|
62
89
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
if (!statSync(sessionPath).isDirectory()) continue;
|
|
91
|
+
} catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const meta = sessionMeta.get(sessionDir);
|
|
96
|
+
const projectPath = meta?.projectPath || null;
|
|
97
|
+
const startTime = meta?.startTime || null;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Only read the latest version of each file hash (highest @vN)
|
|
101
|
+
const files = readdirSync(sessionPath);
|
|
102
|
+
const latestByHash = new Map();
|
|
103
|
+
for (const f of files) {
|
|
104
|
+
const atIdx = f.indexOf("@v");
|
|
105
|
+
if (atIdx < 0) continue;
|
|
106
|
+
const hash = f.slice(0, atIdx);
|
|
107
|
+
const ver = parseInt(f.slice(atIdx + 2), 10) || 0;
|
|
108
|
+
const prev = latestByHash.get(hash);
|
|
109
|
+
if (!prev || ver > prev.ver) {
|
|
110
|
+
latestByHash.set(hash, { file: f, ver });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const { file } of latestByHash.values()) {
|
|
115
|
+
const snapPath = join(sessionPath, file);
|
|
67
116
|
try {
|
|
68
117
|
const content = readFileSync(snapPath, "utf8");
|
|
69
118
|
if (!content.includes("mcpServers")) continue;
|
|
70
119
|
const data = JSON.parse(content);
|
|
71
120
|
for (const name of Object.keys(data.mcpServers || {})) {
|
|
72
|
-
|
|
121
|
+
if (!servers.has(name)) {
|
|
122
|
+
servers.set(name, { name, projects: new Set(), lastSeen: null });
|
|
123
|
+
}
|
|
124
|
+
const entry = servers.get(name);
|
|
125
|
+
if (projectPath) entry.projects.add(projectPath);
|
|
126
|
+
if (startTime && (!entry.lastSeen || startTime > entry.lastSeen)) {
|
|
127
|
+
entry.lastSeen = startTime;
|
|
128
|
+
}
|
|
73
129
|
}
|
|
74
130
|
} catch {
|
|
75
131
|
/* skip malformed */
|
|
@@ -82,5 +138,40 @@ export function scanHistoricalMcpServers(claudeDir) {
|
|
|
82
138
|
} catch {
|
|
83
139
|
/* skip unreadable file-history dir */
|
|
84
140
|
}
|
|
85
|
-
return
|
|
141
|
+
return servers;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Classify historical MCP servers as "recent" (seen in last recencyDays) or "former".
|
|
146
|
+
* Recent servers are merged into the current server list if not already present.
|
|
147
|
+
*/
|
|
148
|
+
export function classifyHistoricalServers(
|
|
149
|
+
historicalMap,
|
|
150
|
+
currentNames,
|
|
151
|
+
recencyDays = 30,
|
|
152
|
+
now = null,
|
|
153
|
+
) {
|
|
154
|
+
const cutoff = new Date(now || Date.now());
|
|
155
|
+
cutoff.setDate(cutoff.getDate() - recencyDays);
|
|
156
|
+
|
|
157
|
+
const recent = [];
|
|
158
|
+
const former = [];
|
|
159
|
+
|
|
160
|
+
for (const [name, entry] of historicalMap) {
|
|
161
|
+
if (currentNames.has(name)) continue; // already in current config
|
|
162
|
+
const info = {
|
|
163
|
+
name,
|
|
164
|
+
projects: [...entry.projects].sort(),
|
|
165
|
+
lastSeen: entry.lastSeen,
|
|
166
|
+
};
|
|
167
|
+
if (entry.lastSeen && entry.lastSeen >= cutoff) {
|
|
168
|
+
recent.push(info);
|
|
169
|
+
} else {
|
|
170
|
+
former.push(info);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
recent.sort((a, b) => a.name.localeCompare(b.name));
|
|
175
|
+
former.sort((a, b) => a.name.localeCompare(b.name));
|
|
176
|
+
return { recent, former };
|
|
86
177
|
}
|
package/src/render.mjs
CHANGED
|
@@ -130,6 +130,15 @@ export function renderRepoCard(repo) {
|
|
|
130
130
|
.join("")}</div>`;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
if (repo.similarRepos && repo.similarRepos.length) {
|
|
134
|
+
body += `<div class="label">Similar Configs</div>`;
|
|
135
|
+
body += `<div class="similar-repos">${repo.similarRepos
|
|
136
|
+
.map(
|
|
137
|
+
(r) => `<span class="similar-repo">${esc(r.name)} <small>${r.similarity}%</small></span>`,
|
|
138
|
+
)
|
|
139
|
+
.join("")}</div>`;
|
|
140
|
+
}
|
|
141
|
+
|
|
133
142
|
if (repo.sections.length) {
|
|
134
143
|
body += `<div class="label">Agent Config</div>`;
|
|
135
144
|
body += renderSections(repo.sections);
|