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.
@@ -1005,4 +1005,13 @@ function getArtifacts(folder) {
1005
1005
  return artifacts;
1006
1006
  }
1007
1007
 
1008
- module.exports = { name, labels, getChats, getMessages, resetCache, getUsage, getArtifacts };
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
- module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts };
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
  };
@@ -158,4 +158,9 @@ function extractAssistantContent(content) {
158
158
 
159
159
  const labels = { 'commandcode': 'Command Code' };
160
160
 
161
- module.exports = { name, labels, getChats, getMessages };
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 };
@@ -250,4 +250,9 @@ function getArtifacts(folder) {
250
250
  });
251
251
  }
252
252
 
253
- module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts };
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 };
@@ -194,4 +194,9 @@ function getMessages(chat) {
194
194
 
195
195
  const labels = { 'cursor-agent': 'Cursor Agent' };
196
196
 
197
- module.exports = { name, labels, getChats, getMessages };
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
- module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts };
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
- module.exports = { name, labels, getChats, getMessages, getArtifacts };
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
- module.exports = { name, labels, getChats, getMessages, resetCache, getArtifacts };
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
- module.exports = { getAllChats, getMessages, editors, editorLabels, resetCaches, getAllUsage, getAllArtifacts };
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
- module.exports = { name, labels, getChats, getMessages, getArtifacts };
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 };
@@ -313,4 +313,35 @@ function getMessages(chat) {
313
313
 
314
314
  const labels = { 'opencode': 'OpenCode' };
315
315
 
316
- module.exports = { name, labels, getChats, getMessages };
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
- module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts };
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 };
@@ -588,4 +588,17 @@ function getArtifacts(folder) {
588
588
  });
589
589
  }
590
590
 
591
- module.exports = { name, sources, labels, getChats, getMessages, resetCache, getUsage, getArtifacts };
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
- module.exports = { name, labels, getChats, getMessages };
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.4",
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": {