@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/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
- const MAX_SESSION_DIRS = 200;
54
- const MAX_FILES_TOTAL = 1000;
55
- let filesRead = 0;
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(-MAX_SESSION_DIRS);
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
- for (const snapFile of readdirSync(sessionPath)) {
64
- if (filesRead >= MAX_FILES_TOTAL) break;
65
- filesRead++;
66
- const snapPath = join(sessionPath, snapFile);
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
- historical.add(name);
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 [...historical];
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);
package/src/watch.mjs CHANGED
@@ -13,8 +13,8 @@ export function startWatch(outputPath, scanRoots, cliArgs) {
13
13
  // and noisy snapshot writes on every file change
14
14
  const forwardedArgs = process.argv
15
15
  .slice(2)
16
- .filter((a) => a !== "--watch" && a !== "--diff")
17
- .concat(["--quiet"]);
16
+ .filter((a) => a !== "--watch" && a !== "--diff" && a !== "--open")
17
+ .concat(["--quiet", "--no-open"]);
18
18
 
19
19
  // Resolve output path to detect and ignore self-writes
20
20
  const resolvedOutput = resolve(outputPath);