agentlytics 0.2.4 → 0.2.6
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/editors/antigravity.js +10 -1
- package/editors/base.js +185 -0
- package/editors/claude.js +21 -1
- package/editors/codex.js +6 -0
- package/editors/commandcode.js +6 -1
- package/editors/copilot.js +6 -1
- package/editors/cursor-agent.js +6 -1
- package/editors/cursor.js +9 -1
- package/editors/gemini.js +10 -1
- package/editors/goose.js +50 -1
- package/editors/index.js +52 -1
- package/editors/kiro.js +10 -1
- package/editors/opencode.js +32 -1
- package/editors/vscode.js +13 -1
- package/editors/windsurf.js +14 -1
- package/editors/zed.js +31 -1
- package/index.js +38 -0
- package/package.json +1 -1
- package/server.js +288 -0
- package/ui/src/App.jsx +3 -0
- package/ui/src/components/PageHeader.jsx +11 -0
- package/ui/src/lib/api.js +7 -0
- package/ui/src/pages/Artifacts.jsx +16 -29
- package/ui/src/pages/Compare.jsx +4 -3
- package/ui/src/pages/CostAnalysis.jsx +6 -9
- package/ui/src/pages/Dashboard.jsx +15 -12
- package/ui/src/pages/DeepAnalysis.jsx +6 -5
- package/ui/src/pages/MCPs.jsx +744 -0
- package/ui/src/pages/ProjectDetail.jsx +1 -1
- package/ui/src/pages/Projects.jsx +9 -6
- package/ui/src/pages/RelayDashboard.jsx +4 -1
- package/ui/src/pages/RelayUserDetail.jsx +1 -1
- package/ui/src/pages/Sessions.jsx +19 -19
- package/ui/src/pages/Settings.jsx +3 -5
- package/ui/src/pages/SqlViewer.jsx +15 -16
- package/ui/src/pages/Subscriptions.jsx +4 -7
package/editors/antigravity.js
CHANGED
|
@@ -1005,4 +1005,13 @@ function getArtifacts(folder) {
|
|
|
1005
1005
|
return artifacts;
|
|
1006
1006
|
}
|
|
1007
1007
|
|
|
1008
|
-
|
|
1008
|
+
function getMCPServers() {
|
|
1009
|
+
const { parseMcpConfigFile } = require('./base');
|
|
1010
|
+
// Antigravity uses similar config to Windsurf: ~/.codeium/antigravity/mcp_config.json
|
|
1011
|
+
const globalConfig = path.join(HOME, '.codeium', 'antigravity', 'mcp_config.json');
|
|
1012
|
+
return [
|
|
1013
|
+
...parseMcpConfigFile(globalConfig, { editor: 'antigravity', label: 'Antigravity', scope: 'global' }),
|
|
1014
|
+
];
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
module.exports = { name, labels, getChats, getMessages, resetCache, getUsage, getArtifacts, getMCPServers };
|
package/editors/base.js
CHANGED
|
@@ -118,7 +118,192 @@ function scanArtifacts(folder, { editor, label, files = [], dirs = [] }) {
|
|
|
118
118
|
return artifacts;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Parse a standard MCP config JSON file (mcpServers format).
|
|
123
|
+
* Returns array of { name, command, args, env, envKeys, url, transport, disabled }.
|
|
124
|
+
*/
|
|
125
|
+
function parseMcpConfigFile(filePath, { editor, label, scope }) {
|
|
126
|
+
const fs = require('fs');
|
|
127
|
+
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
128
|
+
try {
|
|
129
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
130
|
+
const data = JSON.parse(raw);
|
|
131
|
+
const servers = data.mcpServers || data.mcp_servers || data.servers || {};
|
|
132
|
+
return Object.entries(servers).map(([name, cfg]) => ({
|
|
133
|
+
name,
|
|
134
|
+
editor,
|
|
135
|
+
editorLabel: label,
|
|
136
|
+
scope,
|
|
137
|
+
configPath: filePath,
|
|
138
|
+
command: cfg.command || null,
|
|
139
|
+
args: cfg.args || [],
|
|
140
|
+
_env: cfg.env || {},
|
|
141
|
+
env: cfg.env ? Object.keys(cfg.env) : [],
|
|
142
|
+
url: cfg.url || null,
|
|
143
|
+
transport: cfg.url ? (cfg.transport || 'http') : (cfg.transport || 'stdio'),
|
|
144
|
+
disabled: cfg.disabled || false,
|
|
145
|
+
disabledTools: cfg.disabledTools || [],
|
|
146
|
+
}));
|
|
147
|
+
} catch { return []; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Query an MCP server for its tools list via JSON-RPC 2.0.
|
|
152
|
+
* For stdio servers: spawns the command and communicates via stdin/stdout.
|
|
153
|
+
* For HTTP servers: sends POST to the URL.
|
|
154
|
+
* Returns a Promise<string[]> of tool names, or [] on failure.
|
|
155
|
+
* Timeout: 10s per server.
|
|
156
|
+
*/
|
|
157
|
+
function queryMcpServerTools(server) {
|
|
158
|
+
const TIMEOUT = 10000;
|
|
159
|
+
|
|
160
|
+
if (server.url && !server.command) {
|
|
161
|
+
// HTTP/SSE transport — send JSON-RPC via POST
|
|
162
|
+
return queryMcpServerToolsHttp(server.url, TIMEOUT);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!server.command) return Promise.resolve([]);
|
|
166
|
+
|
|
167
|
+
// stdio transport — spawn the process
|
|
168
|
+
return queryMcpServerToolsStdio(server, TIMEOUT);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function queryMcpServerToolsHttp(url, timeout) {
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
const controller = new AbortController();
|
|
174
|
+
const timer = setTimeout(() => { controller.abort(); resolve([]); }, timeout);
|
|
175
|
+
const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' };
|
|
176
|
+
|
|
177
|
+
const parseToolsFromResponse = async (r) => {
|
|
178
|
+
const ct = r.headers.get('content-type') || '';
|
|
179
|
+
const text = await r.text();
|
|
180
|
+
if (ct.includes('text/event-stream')) {
|
|
181
|
+
// Parse SSE: lines starting with "data: "
|
|
182
|
+
for (const line of text.split('\n')) {
|
|
183
|
+
if (!line.startsWith('data: ')) continue;
|
|
184
|
+
try {
|
|
185
|
+
const msg = JSON.parse(line.slice(6));
|
|
186
|
+
if (msg.result && msg.result.tools) return msg.result.tools.map(t => t.name);
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const msg = JSON.parse(text);
|
|
193
|
+
return (msg.result && msg.result.tools) ? msg.result.tools.map(t => t.name) : [];
|
|
194
|
+
} catch { return []; }
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// 1. Initialize
|
|
198
|
+
fetch(url, {
|
|
199
|
+
method: 'POST', headers,
|
|
200
|
+
body: JSON.stringify({
|
|
201
|
+
jsonrpc: '2.0', id: 1, method: 'initialize',
|
|
202
|
+
params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'agentlytics', version: '1.0.0' } },
|
|
203
|
+
}),
|
|
204
|
+
signal: controller.signal,
|
|
205
|
+
})
|
|
206
|
+
.then(async (r) => {
|
|
207
|
+
const sessionId = r.headers.get('mcp-session-id');
|
|
208
|
+
const h = { ...headers };
|
|
209
|
+
if (sessionId) h['mcp-session-id'] = sessionId;
|
|
210
|
+
await r.text(); // consume body
|
|
211
|
+
|
|
212
|
+
// 2. Send initialized notification
|
|
213
|
+
await fetch(url, {
|
|
214
|
+
method: 'POST', headers: h,
|
|
215
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
|
|
216
|
+
signal: controller.signal,
|
|
217
|
+
}).then(r2 => r2.text());
|
|
218
|
+
|
|
219
|
+
// 3. Request tools/list
|
|
220
|
+
const r3 = await fetch(url, {
|
|
221
|
+
method: 'POST', headers: h,
|
|
222
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }),
|
|
223
|
+
signal: controller.signal,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
resolve(await parseToolsFromResponse(r3));
|
|
228
|
+
})
|
|
229
|
+
.catch(() => { clearTimeout(timer); resolve([]); });
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function queryMcpServerToolsStdio(server, timeout) {
|
|
234
|
+
const { spawn } = require('child_process');
|
|
235
|
+
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
const env = { ...process.env, ...(server._env || {}) };
|
|
238
|
+
let child;
|
|
239
|
+
try {
|
|
240
|
+
child = spawn(server.command, server.args || [], {
|
|
241
|
+
env, stdio: ['pipe', 'pipe', 'pipe'],
|
|
242
|
+
});
|
|
243
|
+
} catch { return resolve([]); }
|
|
244
|
+
|
|
245
|
+
let stdout = '';
|
|
246
|
+
let done = false;
|
|
247
|
+
let initReceived = false;
|
|
248
|
+
|
|
249
|
+
const finish = (tools) => {
|
|
250
|
+
if (done) return;
|
|
251
|
+
done = true;
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
try { child.kill(); } catch {}
|
|
254
|
+
resolve(tools);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const timer = setTimeout(() => finish([]), timeout);
|
|
258
|
+
|
|
259
|
+
child.stdout.on('data', (chunk) => {
|
|
260
|
+
stdout += chunk.toString();
|
|
261
|
+
// Parse newline-delimited JSON-RPC responses
|
|
262
|
+
const lines = stdout.split('\n');
|
|
263
|
+
// Keep the last (possibly incomplete) line in the buffer
|
|
264
|
+
stdout = lines.pop() || '';
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
const trimmed = line.trim();
|
|
267
|
+
if (!trimmed) continue;
|
|
268
|
+
try {
|
|
269
|
+
const msg = JSON.parse(trimmed);
|
|
270
|
+
// Got initialize response — now send initialized + tools/list
|
|
271
|
+
if (msg.id === 1 && msg.result && !initReceived) {
|
|
272
|
+
initReceived = true;
|
|
273
|
+
try {
|
|
274
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
|
|
275
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }) + '\n');
|
|
276
|
+
} catch { finish([]); }
|
|
277
|
+
}
|
|
278
|
+
// Got tools/list response
|
|
279
|
+
if (msg.id === 2 && msg.result && msg.result.tools) {
|
|
280
|
+
finish(msg.result.tools.map(t => t.name));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
} catch { /* incomplete JSON, skip */ }
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
child.on('error', () => finish([]));
|
|
288
|
+
child.on('exit', () => finish([]));
|
|
289
|
+
|
|
290
|
+
// Send initialize
|
|
291
|
+
const initMsg = JSON.stringify({
|
|
292
|
+
jsonrpc: '2.0', id: 1, method: 'initialize',
|
|
293
|
+
params: {
|
|
294
|
+
protocolVersion: '2024-11-05',
|
|
295
|
+
capabilities: {},
|
|
296
|
+
clientInfo: { name: 'agentlytics', version: '1.0.0' },
|
|
297
|
+
},
|
|
298
|
+
}) + '\n';
|
|
299
|
+
|
|
300
|
+
try { child.stdin.write(initMsg); } catch { finish([]); }
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
121
304
|
module.exports = {
|
|
122
305
|
getAppDataPath,
|
|
123
306
|
scanArtifacts,
|
|
307
|
+
parseMcpConfigFile,
|
|
308
|
+
queryMcpServerTools,
|
|
124
309
|
};
|
package/editors/claude.js
CHANGED
|
@@ -204,8 +204,18 @@ function extractAssistantContent(content) {
|
|
|
204
204
|
// Usage / quota data from Anthropic OAuth API
|
|
205
205
|
// ============================================================
|
|
206
206
|
|
|
207
|
+
function isKeychainAccessAllowed() {
|
|
208
|
+
try {
|
|
209
|
+
const configPath = path.join(os.homedir(), '.agentlytics', 'config.json');
|
|
210
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
211
|
+
return config.allowKeychainAccess === true;
|
|
212
|
+
} catch { return false; }
|
|
213
|
+
}
|
|
214
|
+
|
|
207
215
|
function getClaudeCredentials() {
|
|
208
216
|
// macOS: Keychain; Linux: secret-tool; Windows: not yet supported
|
|
217
|
+
// Requires explicit user permission (allowKeychainAccess in config)
|
|
218
|
+
if (!isKeychainAccessAllowed()) return null;
|
|
209
219
|
try {
|
|
210
220
|
const { execSync } = require('child_process');
|
|
211
221
|
let raw;
|
|
@@ -311,4 +321,14 @@ function getArtifacts(folder) {
|
|
|
311
321
|
});
|
|
312
322
|
}
|
|
313
323
|
|
|
314
|
-
|
|
324
|
+
function getMCPServers() {
|
|
325
|
+
const { parseMcpConfigFile } = require('./base');
|
|
326
|
+
const results = [];
|
|
327
|
+
// Global: ~/.claude.json (has mcpServers key)
|
|
328
|
+
const globalFile = path.join(os.homedir(), '.claude.json');
|
|
329
|
+
results.push(...parseMcpConfigFile(globalFile, { editor: 'claude-code', label: 'Claude Code', scope: 'global' }));
|
|
330
|
+
// Project-level: .mcp.json (scanned per-project later via getAllMCPServers)
|
|
331
|
+
return results;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts, getMCPServers };
|
package/editors/codex.js
CHANGED
|
@@ -519,6 +519,11 @@ function getArtifacts(folder) {
|
|
|
519
519
|
});
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
+
function getMCPServers() {
|
|
523
|
+
// Codex doesn't have native MCP server configuration
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
|
|
522
527
|
module.exports = {
|
|
523
528
|
name,
|
|
524
529
|
labels,
|
|
@@ -526,4 +531,5 @@ module.exports = {
|
|
|
526
531
|
getChats,
|
|
527
532
|
getMessages,
|
|
528
533
|
getUsage,
|
|
534
|
+
getMCPServers,
|
|
529
535
|
};
|
package/editors/commandcode.js
CHANGED
|
@@ -158,4 +158,9 @@ function extractAssistantContent(content) {
|
|
|
158
158
|
|
|
159
159
|
const labels = { 'commandcode': 'Command Code' };
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
function getMCPServers() {
|
|
162
|
+
// CommandCode uses Claude's .mcp.json format (project-level, handled by claude.js)
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { name, labels, getChats, getMessages, getMCPServers };
|
package/editors/copilot.js
CHANGED
|
@@ -250,4 +250,9 @@ function getArtifacts(folder) {
|
|
|
250
250
|
});
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
|
|
253
|
+
function getMCPServers() {
|
|
254
|
+
// Copilot CLI shares MCP config with VS Code (handled by vscode.js)
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts, getMCPServers };
|
package/editors/cursor-agent.js
CHANGED
|
@@ -194,4 +194,9 @@ function getMessages(chat) {
|
|
|
194
194
|
|
|
195
195
|
const labels = { 'cursor-agent': 'Cursor Agent' };
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
function getMCPServers() {
|
|
198
|
+
// Cursor Agent shares MCP config with Cursor (handled by cursor.js)
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = { name, labels, getChats, getMessages, getMCPServers };
|
package/editors/cursor.js
CHANGED
|
@@ -425,4 +425,12 @@ function getArtifacts(folder) {
|
|
|
425
425
|
});
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
-
|
|
428
|
+
function getMCPServers() {
|
|
429
|
+
const { parseMcpConfigFile } = require('./base');
|
|
430
|
+
const globalConfig = path.join(HOME, '.cursor', 'mcp.json');
|
|
431
|
+
return [
|
|
432
|
+
...parseMcpConfigFile(globalConfig, { editor: 'cursor', label: 'Cursor', scope: 'global' }),
|
|
433
|
+
];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts, getMCPServers };
|
package/editors/gemini.js
CHANGED
|
@@ -183,4 +183,13 @@ function getArtifacts(folder) {
|
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
|
|
186
|
+
function getMCPServers() {
|
|
187
|
+
const { parseMcpConfigFile } = require('./base');
|
|
188
|
+
// Global: ~/.gemini/settings.json (mcpServers key)
|
|
189
|
+
const globalSettings = path.join(os.homedir(), '.gemini', 'settings.json');
|
|
190
|
+
return [
|
|
191
|
+
...parseMcpConfigFile(globalSettings, { editor: 'gemini-cli', label: 'Gemini CLI', scope: 'global' }),
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { name, labels, getChats, getMessages, getArtifacts, getMCPServers };
|
package/editors/goose.js
CHANGED
|
@@ -306,4 +306,53 @@ function getArtifacts(folder) {
|
|
|
306
306
|
});
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
|
|
309
|
+
function getMCPServers() {
|
|
310
|
+
const results = [];
|
|
311
|
+
// Goose stores MCP config in ~/.config/goose/config.yaml under extensions
|
|
312
|
+
// Also check profiles.yaml for MCP server entries
|
|
313
|
+
const configFiles = [CONFIG_PATH, path.join(os.homedir(), '.config', 'goose', 'profiles.yaml')];
|
|
314
|
+
for (const cfgPath of configFiles) {
|
|
315
|
+
if (!fs.existsSync(cfgPath)) continue;
|
|
316
|
+
try {
|
|
317
|
+
const raw = fs.readFileSync(cfgPath, 'utf-8');
|
|
318
|
+
// Simple YAML parsing for mcpServers / extensions blocks
|
|
319
|
+
let currentServer = null;
|
|
320
|
+
let inMcp = false;
|
|
321
|
+
for (const line of raw.split('\n')) {
|
|
322
|
+
if (line.match(/^\s*(mcpServers|extensions):/)) { inMcp = true; continue; }
|
|
323
|
+
if (inMcp && line.match(/^\s{2}\w/) && !line.match(/^\s{4}/)) {
|
|
324
|
+
// New top-level key under mcpServers
|
|
325
|
+
if (currentServer) results.push(currentServer);
|
|
326
|
+
const nameMatch = line.match(/^\s{2}(\S+):/);
|
|
327
|
+
if (nameMatch) {
|
|
328
|
+
currentServer = {
|
|
329
|
+
name: nameMatch[1],
|
|
330
|
+
editor: 'goose',
|
|
331
|
+
editorLabel: 'Goose',
|
|
332
|
+
scope: 'global',
|
|
333
|
+
configPath: cfgPath,
|
|
334
|
+
command: null, args: [], env: [], url: null,
|
|
335
|
+
transport: 'stdio', disabled: false, disabledTools: [],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (inMcp && currentServer) {
|
|
341
|
+
const cmdMatch = line.match(/^\s+command:\s*(.+)/);
|
|
342
|
+
if (cmdMatch) currentServer.command = cmdMatch[1].trim();
|
|
343
|
+
const urlMatch = line.match(/^\s+url:\s*(.+)/);
|
|
344
|
+
if (urlMatch) { currentServer.url = urlMatch[1].trim(); currentServer.transport = 'http'; }
|
|
345
|
+
}
|
|
346
|
+
if (line.match(/^\S/) && !line.match(/^\s/) && inMcp) {
|
|
347
|
+
if (currentServer) results.push(currentServer);
|
|
348
|
+
currentServer = null;
|
|
349
|
+
inMcp = false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (currentServer) results.push(currentServer);
|
|
353
|
+
} catch {}
|
|
354
|
+
}
|
|
355
|
+
return results;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = { name, labels, getChats, getMessages, resetCache, getArtifacts, getMCPServers };
|
package/editors/index.js
CHANGED
|
@@ -119,4 +119,55 @@ function getAllArtifacts(folder) {
|
|
|
119
119
|
return Array.from(seen.values());
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Get all MCP servers from all editors.
|
|
124
|
+
* Also scans project folders for project-level MCP configs (.mcp.json, .cursor/mcp.json, etc.)
|
|
125
|
+
*/
|
|
126
|
+
function getAllMCPServers(projectFolders = []) {
|
|
127
|
+
const { parseMcpConfigFile } = require('./base');
|
|
128
|
+
const path = require('path');
|
|
129
|
+
const fs = require('fs');
|
|
130
|
+
const servers = [];
|
|
131
|
+
|
|
132
|
+
// 1. Collect global MCP servers from each editor
|
|
133
|
+
for (const editor of editors) {
|
|
134
|
+
if (typeof editor.getMCPServers !== 'function') continue;
|
|
135
|
+
try {
|
|
136
|
+
servers.push(...editor.getMCPServers());
|
|
137
|
+
} catch { /* skip broken adapters */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 2. Scan project folders for project-level MCP configs
|
|
141
|
+
const projectConfigs = [
|
|
142
|
+
{ file: '.mcp.json', editor: 'claude-code', label: 'Claude Code' },
|
|
143
|
+
{ file: '.cursor/mcp.json', editor: 'cursor', label: 'Cursor' },
|
|
144
|
+
{ file: '.vscode/mcp.json', editor: 'vscode', label: 'VS Code' },
|
|
145
|
+
{ file: '.gemini/settings.json', editor: 'gemini-cli', label: 'Gemini CLI' },
|
|
146
|
+
{ file: '.kiro/settings/mcp.json', editor: 'kiro', label: 'Kiro' },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const seenProjects = new Set();
|
|
150
|
+
for (const folder of projectFolders) {
|
|
151
|
+
if (!folder || seenProjects.has(folder)) continue;
|
|
152
|
+
seenProjects.add(folder);
|
|
153
|
+
for (const pc of projectConfigs) {
|
|
154
|
+
const configPath = path.join(folder, pc.file);
|
|
155
|
+
if (!fs.existsSync(configPath)) continue;
|
|
156
|
+
const found = parseMcpConfigFile(configPath, { editor: pc.editor, label: pc.label, scope: 'project' });
|
|
157
|
+
for (const s of found) {
|
|
158
|
+
s.projectFolder = folder;
|
|
159
|
+
}
|
|
160
|
+
servers.push(...found);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 3. Deduplicate by name+editor (keep first occurrence, prefer global over project)
|
|
165
|
+
const seen = new Map();
|
|
166
|
+
for (const s of servers) {
|
|
167
|
+
const key = `${s.name}::${s.editor}::${s.scope}`;
|
|
168
|
+
if (!seen.has(key)) seen.set(key, s);
|
|
169
|
+
}
|
|
170
|
+
return Array.from(seen.values());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { getAllChats, getMessages, editors, editorLabels, resetCaches, getAllUsage, getAllArtifacts, getAllMCPServers };
|
package/editors/kiro.js
CHANGED
|
@@ -303,4 +303,13 @@ function getArtifacts(folder) {
|
|
|
303
303
|
});
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
|
|
306
|
+
function getMCPServers() {
|
|
307
|
+
const { parseMcpConfigFile } = require('./base');
|
|
308
|
+
// Global: ~/.kiro/settings/mcp.json
|
|
309
|
+
const globalConfig = path.join(os.homedir(), '.kiro', 'settings', 'mcp.json');
|
|
310
|
+
return [
|
|
311
|
+
...parseMcpConfigFile(globalConfig, { editor: 'kiro', label: 'Kiro', scope: 'global' }),
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = { name, labels, getChats, getMessages, getArtifacts, getMCPServers };
|
package/editors/opencode.js
CHANGED
|
@@ -313,4 +313,35 @@ function getMessages(chat) {
|
|
|
313
313
|
|
|
314
314
|
const labels = { 'opencode': 'OpenCode' };
|
|
315
315
|
|
|
316
|
-
|
|
316
|
+
function getMCPServers() {
|
|
317
|
+
const { parseMcpConfigFile } = require('./base');
|
|
318
|
+
// OpenCode: ~/.config/opencode/opencode.json (mcp key maps to mcpServers format)
|
|
319
|
+
const globalConfig = path.join(os.homedir(), '.config', 'opencode', 'opencode.json');
|
|
320
|
+
const results = [];
|
|
321
|
+
if (fs.existsSync(globalConfig)) {
|
|
322
|
+
try {
|
|
323
|
+
const data = JSON.parse(fs.readFileSync(globalConfig, 'utf-8'));
|
|
324
|
+
const servers = data.mcp || {};
|
|
325
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
326
|
+
if (typeof cfg !== 'object') continue;
|
|
327
|
+
results.push({
|
|
328
|
+
name,
|
|
329
|
+
editor: 'opencode',
|
|
330
|
+
editorLabel: 'OpenCode',
|
|
331
|
+
scope: 'global',
|
|
332
|
+
configPath: globalConfig,
|
|
333
|
+
command: cfg.command || null,
|
|
334
|
+
args: cfg.args || [],
|
|
335
|
+
env: cfg.env ? Object.keys(cfg.env) : [],
|
|
336
|
+
url: cfg.url || null,
|
|
337
|
+
transport: cfg.type || (cfg.url ? 'http' : 'stdio'),
|
|
338
|
+
disabled: false,
|
|
339
|
+
disabledTools: [],
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
return results;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = { name, labels, getChats, getMessages, getMCPServers };
|
package/editors/vscode.js
CHANGED
|
@@ -396,4 +396,16 @@ function getArtifacts(folder) {
|
|
|
396
396
|
});
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
-
|
|
399
|
+
function getMCPServers() {
|
|
400
|
+
const { parseMcpConfigFile } = require('./base');
|
|
401
|
+
const results = [];
|
|
402
|
+
for (const variant of VARIANTS) {
|
|
403
|
+
if (!fs.existsSync(variant.appSupport)) continue;
|
|
404
|
+
// User-level: <appSupport>/User/mcp.json
|
|
405
|
+
const userMcp = path.join(variant.appSupport, 'User', 'mcp.json');
|
|
406
|
+
results.push(...parseMcpConfigFile(userMcp, { editor: variant.id, label: variant.id === 'vscode' ? 'VS Code' : 'VS Code Insiders', scope: 'global' }));
|
|
407
|
+
}
|
|
408
|
+
return results;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts, getMCPServers };
|
package/editors/windsurf.js
CHANGED
|
@@ -588,4 +588,17 @@ function getArtifacts(folder) {
|
|
|
588
588
|
});
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
-
|
|
591
|
+
function getMCPServers() {
|
|
592
|
+
const { parseMcpConfigFile } = require('./base');
|
|
593
|
+
const results = [];
|
|
594
|
+
const configs = [
|
|
595
|
+
{ file: path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'), editor: 'windsurf', label: 'Windsurf' },
|
|
596
|
+
{ file: path.join(os.homedir(), '.codeium', 'windsurf-next', 'mcp_config.json'), editor: 'windsurf-next', label: 'Windsurf Next' },
|
|
597
|
+
];
|
|
598
|
+
for (const c of configs) {
|
|
599
|
+
results.push(...parseMcpConfigFile(c.file, { editor: c.editor, label: c.label, scope: 'global' }));
|
|
600
|
+
}
|
|
601
|
+
return results;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
module.exports = { name, sources, labels, getChats, getMessages, resetCache, getUsage, getArtifacts, getMCPServers };
|
package/editors/zed.js
CHANGED
|
@@ -180,4 +180,34 @@ function extractContent(content) {
|
|
|
180
180
|
|
|
181
181
|
const labels = { 'zed': 'Zed' };
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
function getMCPServers() {
|
|
184
|
+
const results = [];
|
|
185
|
+
// Zed stores MCP servers in ~/.config/zed/settings.json under context_servers key
|
|
186
|
+
const settingsPath = path.join(getZedDataPath(), 'settings.json');
|
|
187
|
+
if (!fs.existsSync(settingsPath)) return results;
|
|
188
|
+
try {
|
|
189
|
+
const data = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
190
|
+
const servers = data.context_servers || {};
|
|
191
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
192
|
+
if (typeof cfg !== 'object') continue;
|
|
193
|
+
const settings = cfg.settings || {};
|
|
194
|
+
results.push({
|
|
195
|
+
name,
|
|
196
|
+
editor: 'zed',
|
|
197
|
+
editorLabel: 'Zed',
|
|
198
|
+
scope: 'global',
|
|
199
|
+
configPath: settingsPath,
|
|
200
|
+
command: settings.command || cfg.command || null,
|
|
201
|
+
args: settings.args || cfg.args || [],
|
|
202
|
+
env: settings.env ? Object.keys(settings.env) : [],
|
|
203
|
+
url: settings.url || cfg.url || null,
|
|
204
|
+
transport: (settings.url || cfg.url) ? 'http' : 'stdio',
|
|
205
|
+
disabled: false,
|
|
206
|
+
disabledTools: [],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
return results;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = { name, labels, getChats, getMessages, getMCPServers };
|
package/index.js
CHANGED
|
@@ -253,6 +253,39 @@ const BOT_STYLES = [
|
|
|
253
253
|
];
|
|
254
254
|
|
|
255
255
|
(async () => {
|
|
256
|
+
// ── Ask for keychain access permission (first run only) ──
|
|
257
|
+
const CONFIG_DIR = path.join(os.homedir(), '.agentlytics');
|
|
258
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
259
|
+
let agentConfig = {};
|
|
260
|
+
try { agentConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')); } catch {}
|
|
261
|
+
|
|
262
|
+
if (agentConfig.allowKeychainAccess === undefined) {
|
|
263
|
+
// Only relevant on macOS / Linux where keychain/secret-tool is used
|
|
264
|
+
if (process.platform === 'darwin' || process.platform === 'linux') {
|
|
265
|
+
const storeName = process.platform === 'darwin' ? 'Keychain' : 'secret store';
|
|
266
|
+
console.log(chalk.yellow(` ⚠ Some subscription details (e.g. Claude Code) require ${storeName} access.`));
|
|
267
|
+
console.log(chalk.dim(` This reads stored credentials to show plan/usage info.`));
|
|
268
|
+
console.log('');
|
|
269
|
+
const readline = require('readline');
|
|
270
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
271
|
+
const answer = await new Promise(r => {
|
|
272
|
+
rl.question(chalk.bold(` Allow ${storeName} access for subscription details? (y/N) `), (a) => {
|
|
273
|
+
rl.close();
|
|
274
|
+
r(a.trim().toLowerCase());
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
agentConfig.allowKeychainAccess = answer === 'y' || answer === 'yes';
|
|
278
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
279
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(agentConfig, null, 2));
|
|
280
|
+
if (agentConfig.allowKeychainAccess) {
|
|
281
|
+
console.log(chalk.green(` ✓ ${storeName} access enabled`));
|
|
282
|
+
} else {
|
|
283
|
+
console.log(chalk.dim(` – ${storeName} access skipped (subscription details won't be collected)`));
|
|
284
|
+
}
|
|
285
|
+
console.log('');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
256
289
|
let tick = 0;
|
|
257
290
|
const startTime = Date.now();
|
|
258
291
|
const result = await cache.scanAllAsync((p) => {
|
|
@@ -282,6 +315,11 @@ const BOT_STYLES = [
|
|
|
282
315
|
const http = require('http');
|
|
283
316
|
const net = require('net');
|
|
284
317
|
|
|
318
|
+
// Pre-cache MCP server tool lists (runs in background, non-blocking)
|
|
319
|
+
app.initMcpToolsCache().then(() => {
|
|
320
|
+
console.log(chalk.green(' ✓ MCP tools cached'));
|
|
321
|
+
}).catch(() => {});
|
|
322
|
+
|
|
285
323
|
function isPortFree(port) {
|
|
286
324
|
return new Promise((resolve) => {
|
|
287
325
|
const tester = net.createServer()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlytics",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode, Command Code",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|