agentlytics 0.0.11 → 0.1.0

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 CHANGED
@@ -48,6 +48,7 @@ For local development, run `npm run dev` from the repo root. That starts both th
48
48
  - **Deep Analysis** — Tool frequency, model distribution, token breakdown with drill-down
49
49
  - **Compare** — Side-by-side editor comparison with efficiency ratios
50
50
  - **Refetch** — One-click cache rebuild with live progress
51
+ - **Relay** — Multi-user context sharing with MCP server for cross-team AI session querying
51
52
 
52
53
  ## Supported Editors
53
54
 
@@ -72,13 +73,89 @@ For local development, run `npm run dev` from the repo root. That starts both th
72
73
 
73
74
  Codex sessions are read from `${CODEX_HOME:-~/.codex}/sessions/**/*.jsonl`. Reasoning summaries may appear in transcripts when Codex records them in clear text, but encrypted reasoning content is not readable. Codex Desktop and CLI sessions are aggregated into one `codex` editor in analytics.
74
75
 
76
+ ## Relay
77
+
78
+ Relay enables multi-user context sharing across a team. One person starts a relay server, others join and share selected project sessions. An MCP server is exposed so AI clients can query across everyone's coding history.
79
+
80
+ ### Start a relay
81
+
82
+ ```bash
83
+ npx agentlytics --relay
84
+ ```
85
+
86
+ Optionally protect with a password:
87
+
88
+ ```bash
89
+ RELAY_PASSWORD=secret npx agentlytics --relay
90
+ ```
91
+
92
+ This starts a relay server on port `4638` and prints the join command and MCP endpoint:
93
+
94
+ ```
95
+ ⚡ Agentlytics Relay
96
+
97
+ Share this command with your team:
98
+ cd /path/to/project
99
+ npx agentlytics --join 192.168.1.16:4638
100
+
101
+ MCP server endpoint (add to your AI client):
102
+ http://192.168.1.16:4638/mcp
103
+ ```
104
+
105
+ ### Join a relay
106
+
107
+ ```bash
108
+ cd /path/to/your-project
109
+ npx agentlytics --join <host:port>
110
+ ```
111
+
112
+ If the relay is password-protected:
113
+
114
+ ```bash
115
+ RELAY_PASSWORD=secret npx agentlytics --join <host:port>
116
+ ```
117
+
118
+ Username is auto-detected from `git config user.email`. You can override it with `--username <name>`.
119
+
120
+ You'll be prompted to select which projects to share. The client then syncs session data to the relay every 30 seconds.
121
+
122
+ ### MCP Tools
123
+
124
+ Connect your AI client to the relay's MCP endpoint (`http://<host>:4638/mcp`) to access these tools:
125
+
126
+ | Tool | Description |
127
+ |------|-------------|
128
+ | `list_users` | List all connected users and their shared projects |
129
+ | `search_sessions` | Full-text search across all users' chat messages |
130
+ | `get_user_activity` | Get recent sessions for a specific user |
131
+ | `get_session_detail` | Get full conversation messages for a session |
132
+
133
+ Example query to your AI: *"What did alice do in auth.js?"*
134
+
135
+ ### Relay REST API
136
+
137
+ | Endpoint | Description |
138
+ |----------|-------------|
139
+ | `GET /relay/health` | Health check and user count |
140
+ | `GET /relay/users` | List connected users |
141
+ | `GET /relay/search?q=<query>` | Search messages across all users |
142
+ | `GET /relay/activity/:username` | User's recent sessions |
143
+ | `GET /relay/session/:chatId` | Full session detail |
144
+ | `POST /relay/sync` | Receives data from join clients |
145
+
146
+ > Relay is designed for trusted local networks. Set `RELAY_PASSWORD` env on both server and clients to enable password protection.
147
+
75
148
  ## How It Works
76
149
 
77
150
  ```
78
151
  Editor files/APIs → editors/*.js → cache.js (SQLite) → server.js (REST) → React SPA
79
152
  ```
80
153
 
81
- All data is normalized into a local SQLite cache at `~/.agentlytics/cache.db`. The Express server exposes read-only REST endpoints consumed by the React frontend.
154
+ ```
155
+ Relay: join clients → POST /relay/sync → relay.db (SQLite) → MCP server → AI clients
156
+ ```
157
+
158
+ All data is normalized into a local SQLite cache at `~/.agentlytics/cache.db`. The Express server exposes read-only REST endpoints consumed by the React frontend. Relay data is stored separately in `~/.agentlytics/relay.db`.
82
159
 
83
160
  ## API
84
161
 
package/cache.js CHANGED
@@ -2,7 +2,7 @@ const Database = require('better-sqlite3');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const fs = require('fs');
5
- const { getAllChats, getMessages, findChat: findChatRaw } = require('./editors');
5
+ const { getAllChats, getMessages, findChat: findChatRaw, resetCaches } = require('./editors');
6
6
 
7
7
  const CACHE_DIR = path.join(os.homedir(), '.agentlytics');
8
8
  const CACHE_DB = path.join(CACHE_DIR, 'cache.db');
@@ -190,7 +190,9 @@ function analyzeAndStore(chat) {
190
190
  );
191
191
  }
192
192
 
193
- function scanAll(onProgress) {
193
+ function scanAll(onProgress, opts = {}) {
194
+ const force = opts.force || false;
195
+ if (force || opts.resetCaches) resetCaches();
194
196
  const chats = getAllChats();
195
197
  const total = chats.length;
196
198
  let scanned = 0;
@@ -199,8 +201,8 @@ function scanAll(onProgress) {
199
201
 
200
202
  // Check which chats need updating
201
203
  const existing = {};
202
- for (const row of db.prepare('SELECT id, last_updated_at FROM chats').all()) {
203
- existing[row.id] = row.last_updated_at;
204
+ for (const row of db.prepare('SELECT id, last_updated_at, bubble_count FROM chats').all()) {
205
+ existing[row.id] = { ts: row.last_updated_at, bc: row.bubble_count };
204
206
  }
205
207
 
206
208
  const ins = insertChat();
@@ -224,11 +226,14 @@ function scanAll(onProgress) {
224
226
  // Analyze messages for chats that are new or updated
225
227
  for (const chat of chats) {
226
228
  scanned++;
227
- const cachedTs = existing[chat.composerId];
228
229
  const chatTs = chat.lastUpdatedAt || chat.createdAt || 0;
229
230
 
230
- // Skip if already cached and not updated
231
- if (cachedTs && cachedTs >= chatTs) {
231
+ // Skip if already cached and not updated (unless force rescan)
232
+ const cached = existing[chat.composerId];
233
+ const cachedTs = cached ? cached.ts : null;
234
+ const cachedBc = cached ? cached.bc : null;
235
+ const chatBc = chat.bubbleCount || 0;
236
+ if (!force && cachedTs && cachedTs >= chatTs && cachedBc >= chatBc) {
232
237
  // Check if stats exist
233
238
  const hasStat = db.prepare('SELECT 1 FROM chat_stats WHERE chat_id = ?').get(chat.composerId);
234
239
  if (hasStat) {
package/editors/index.js CHANGED
@@ -54,4 +54,10 @@ function findChat(idPrefix) {
54
54
  return chats.find((c) => c.composerId.startsWith(idPrefix));
55
55
  }
56
56
 
57
- module.exports = { getAllChats, getMessages, findChat, editors };
57
+ function resetCaches() {
58
+ for (const editor of editors) {
59
+ if (typeof editor.resetCache === 'function') editor.resetCache();
60
+ }
61
+ }
62
+
63
+ module.exports = { getAllChats, getMessages, findChat, editors, resetCaches };
@@ -114,6 +114,7 @@ function getChats() {
114
114
  mode: 'cascade',
115
115
  folder,
116
116
  encrypted: false,
117
+ bubbleCount: summary.stepCount || 0,
117
118
  _port: ls.port,
118
119
  _csrf: ls.csrf,
119
120
  _https: variant.https,
@@ -126,62 +127,213 @@ function getChats() {
126
127
  return chats;
127
128
  }
128
129
 
129
- function getMessages(chat) {
130
+ function getSteps(chat) {
130
131
  if (!chat._port || !chat._csrf) return [];
131
132
 
133
+ // Prefer GetCascadeTrajectorySteps (returns more steps than GetCascadeTrajectory)
134
+ const resp = callRpc(chat._port, chat._csrf, 'GetCascadeTrajectorySteps', {
135
+ cascadeId: chat.composerId,
136
+ }, chat._https);
137
+ if (resp && resp.steps && resp.steps.length > 0) return resp.steps;
138
+
139
+ // Fallback to old method
140
+ const resp2 = callRpc(chat._port, chat._csrf, 'GetCascadeTrajectory', {
141
+ cascadeId: chat.composerId,
142
+ }, chat._https);
143
+ if (resp2 && resp2.trajectory && resp2.trajectory.steps) return resp2.trajectory.steps;
144
+
145
+ return [];
146
+ }
147
+
148
+ /**
149
+ * Get the tail messages beyond the step limit using generatorMetadata.
150
+ * The last generatorMetadata entry with messagePrompts has the conversation context.
151
+ * We find the overlap with step-based messages by matching the last user message content.
152
+ */
153
+ function getTailMessages(chat, stepMessages) {
132
154
  const resp = callRpc(chat._port, chat._csrf, 'GetCascadeTrajectory', {
133
155
  cascadeId: chat.composerId,
134
156
  }, chat._https);
135
- if (!resp || !resp.trajectory || !resp.trajectory.steps) return [];
157
+ if (!resp || !resp.trajectory) return [];
136
158
 
137
- const messages = [];
138
- for (const step of resp.trajectory.steps) {
139
- const type = step.type || '';
140
- const meta = step.metadata || {};
141
-
142
- if (type === 'CORTEX_STEP_TYPE_USER_INPUT' && step.userInput) {
143
- messages.push({
144
- role: 'user',
145
- content: step.userInput.userResponse || step.userInput.items?.map(i => i.text).join('') || '',
146
- });
147
- } else if (type === 'CORTEX_STEP_TYPE_PLANNER_RESPONSE' && step.plannerResponse) {
148
- const pr = step.plannerResponse;
149
- const parts = [];
150
- if (pr.thinking) parts.push(`[thinking] ${pr.thinking}`);
151
- // Text content: prefer modifiedResponse > response > textContent
152
- const text = pr.modifiedResponse || pr.response || pr.textContent || '';
153
- if (text.trim()) parts.push(text.trim());
154
- // Tool calls
155
- const _toolCalls = [];
156
- if (pr.toolCalls && pr.toolCalls.length > 0) {
157
- for (const tc of pr.toolCalls) {
158
- let args = {};
159
- try { args = tc.argumentsJson ? JSON.parse(tc.argumentsJson) : {}; } catch { args = {}; }
160
- const argKeys = typeof args === 'object' ? Object.keys(args).join(', ') : '';
161
- parts.push(`[tool-call: ${tc.name}(${argKeys})]`);
162
- _toolCalls.push({ name: tc.name, args });
163
- }
164
- }
165
- if (parts.length > 0) {
166
- messages.push({
167
- role: 'assistant',
168
- content: parts.join('\n'),
169
- _model: meta.generatorModelUid,
170
- _toolCalls,
171
- });
159
+ const gm = resp.trajectory.generatorMetadata || [];
160
+ // Find the last entry that has messagePrompts
161
+ let lastWithMsgs = null;
162
+ for (let i = gm.length - 1; i >= 0; i--) {
163
+ if (gm[i].chatModel && gm[i].chatModel.messagePrompts && gm[i].chatModel.messagePrompts.length > 0) {
164
+ lastWithMsgs = gm[i];
165
+ break;
166
+ }
167
+ }
168
+ if (!lastWithMsgs) return [];
169
+
170
+ const mp = lastWithMsgs.chatModel.messagePrompts;
171
+
172
+ // Find the last user message from step-based parsing
173
+ let lastUserContent = '';
174
+ for (let i = stepMessages.length - 1; i >= 0; i--) {
175
+ if (stepMessages[i].role === 'user' && stepMessages[i].content.length > 20) {
176
+ lastUserContent = stepMessages[i].content;
177
+ break;
178
+ }
179
+ }
180
+ if (!lastUserContent) return [];
181
+
182
+ // Find this message in the messagePrompts (search from end for efficiency)
183
+ const needle = lastUserContent.substring(0, 50);
184
+ let matchIdx = -1;
185
+ for (let i = mp.length - 1; i >= 0; i--) {
186
+ if (mp[i].source === 'CHAT_MESSAGE_SOURCE_USER' && mp[i].prompt && mp[i].prompt.includes(needle)) {
187
+ matchIdx = i;
188
+ break;
189
+ }
190
+ }
191
+ if (matchIdx < 0 || matchIdx >= mp.length - 1) return [];
192
+
193
+ // Convert everything after the match point to messages
194
+ const tail = [];
195
+ for (let i = matchIdx + 1; i < mp.length; i++) {
196
+ const m = mp[i];
197
+ const src = m.source || '';
198
+ const prompt = m.prompt || '';
199
+ if (!prompt || !prompt.trim()) continue;
200
+
201
+ let role;
202
+ if (src === 'CHAT_MESSAGE_SOURCE_USER') role = 'user';
203
+ else if (src === 'CHAT_MESSAGE_SOURCE_SYSTEM') role = 'assistant';
204
+ else if (src === 'CHAT_MESSAGE_SOURCE_TOOL') role = 'tool';
205
+ else continue;
206
+
207
+ tail.push({ role, content: prompt });
208
+ }
209
+ return tail;
210
+ }
211
+
212
+ function parseStep(step) {
213
+ const type = step.type || '';
214
+ const meta = step.metadata || {};
215
+
216
+ if (type === 'CORTEX_STEP_TYPE_USER_INPUT' && step.userInput) {
217
+ return {
218
+ role: 'user',
219
+ content: step.userInput.userResponse || step.userInput.items?.map(i => i.text).join('') || '',
220
+ };
221
+ }
222
+
223
+ if (type === 'CORTEX_STEP_TYPE_ASK_USER_QUESTION' && step.askUserQuestion) {
224
+ const q = step.askUserQuestion;
225
+ return {
226
+ role: 'user',
227
+ content: q.userResponse || q.question || '',
228
+ };
229
+ }
230
+
231
+ if (type === 'CORTEX_STEP_TYPE_PLANNER_RESPONSE' && step.plannerResponse) {
232
+ const pr = step.plannerResponse;
233
+ const parts = [];
234
+ if (pr.thinking) parts.push(`[thinking] ${pr.thinking}`);
235
+ const text = pr.modifiedResponse || pr.response || pr.textContent || '';
236
+ if (text.trim()) parts.push(text.trim());
237
+ const _toolCalls = [];
238
+ if (pr.toolCalls && pr.toolCalls.length > 0) {
239
+ for (const tc of pr.toolCalls) {
240
+ let args = {};
241
+ try { args = tc.argumentsJson ? JSON.parse(tc.argumentsJson) : {}; } catch { args = {}; }
242
+ const argKeys = typeof args === 'object' ? Object.keys(args).join(', ') : '';
243
+ parts.push(`[tool-call: ${tc.name}(${argKeys})]`);
244
+ _toolCalls.push({ name: tc.name, args });
172
245
  }
173
- } else if (type === 'CORTEX_STEP_TYPE_TOOL_EXECUTION' && step.toolExecution) {
174
- const te = step.toolExecution;
175
- const toolName = te.toolName || te.name || 'tool';
176
- const result = te.output || te.result || '';
177
- const preview = typeof result === 'string' ? result.substring(0, 500) : JSON.stringify(result).substring(0, 500);
178
- messages.push({
179
- role: 'tool',
180
- content: `[${toolName}] ${preview}`,
181
- });
182
246
  }
247
+ if (parts.length > 0) {
248
+ return {
249
+ role: 'assistant',
250
+ content: parts.join('\n'),
251
+ _model: meta.generatorModelUid,
252
+ _toolCalls,
253
+ };
254
+ }
255
+ return null;
256
+ }
257
+
258
+ // Tool-like step types
259
+ if (type === 'CORTEX_STEP_TYPE_TOOL_EXECUTION' && step.toolExecution) {
260
+ const te = step.toolExecution;
261
+ const toolName = te.toolName || te.name || 'tool';
262
+ const result = te.output || te.result || '';
263
+ const preview = typeof result === 'string' ? result.substring(0, 500) : JSON.stringify(result).substring(0, 500);
264
+ return { role: 'tool', content: `[${toolName}] ${preview}` };
265
+ }
266
+
267
+ if (type === 'CORTEX_STEP_TYPE_RUN_COMMAND' && step.runCommand) {
268
+ const rc = step.runCommand;
269
+ const cmd = rc.command || rc.commandLine || '';
270
+ const out = (rc.output || rc.stdout || '').substring(0, 500);
271
+ return { role: 'tool', content: `[run_command] ${cmd}${out ? '\n' + out : ''}` };
272
+ }
273
+
274
+ if (type === 'CORTEX_STEP_TYPE_COMMAND_STATUS' && step.commandStatus) {
275
+ const cs = step.commandStatus;
276
+ const out = (cs.output || cs.stdout || '').substring(0, 500);
277
+ return out ? { role: 'tool', content: `[command_status] ${out}` } : null;
278
+ }
279
+
280
+ if (type === 'CORTEX_STEP_TYPE_VIEW_FILE' && step.viewFile) {
281
+ const vf = step.viewFile;
282
+ const filePath = vf.filePath || vf.path || '';
283
+ return { role: 'tool', content: `[view_file] ${filePath}` };
284
+ }
285
+
286
+ if (type === 'CORTEX_STEP_TYPE_CODE_ACTION' && step.codeAction) {
287
+ const ca = step.codeAction;
288
+ const filePath = ca.filePath || ca.path || '';
289
+ return { role: 'tool', content: `[code_action] ${filePath}` };
290
+ }
291
+
292
+ if (type === 'CORTEX_STEP_TYPE_GREP_SEARCH' && step.grepSearch) {
293
+ const gs = step.grepSearch;
294
+ const query = gs.query || gs.pattern || '';
295
+ return { role: 'tool', content: `[grep_search] ${query}` };
183
296
  }
297
+
298
+ if (type === 'CORTEX_STEP_TYPE_LIST_DIRECTORY' && step.listDirectory) {
299
+ const ld = step.listDirectory;
300
+ const dir = ld.directoryPath || ld.path || '';
301
+ return { role: 'tool', content: `[list_directory] ${dir}` };
302
+ }
303
+
304
+ if (type === 'CORTEX_STEP_TYPE_MCP_TOOL' && step.mcpTool) {
305
+ const mt = step.mcpTool;
306
+ const name = mt.toolName || mt.name || 'mcp_tool';
307
+ return { role: 'tool', content: `[${name}]` };
308
+ }
309
+
310
+ // Skip non-content steps
311
+ if (type === 'CORTEX_STEP_TYPE_CHECKPOINT' || type === 'CORTEX_STEP_TYPE_RETRIEVE_MEMORY' ||
312
+ type === 'CORTEX_STEP_TYPE_MEMORY' || type === 'CORTEX_STEP_TYPE_TODO_LIST' ||
313
+ type === 'CORTEX_STEP_TYPE_EXIT_PLAN_MODE' || type === 'CORTEX_STEP_TYPE_PROXY_WEB_SERVER') {
314
+ return null;
315
+ }
316
+
317
+ return null;
318
+ }
319
+
320
+ function getMessages(chat) {
321
+ const steps = getSteps(chat);
322
+ const messages = [];
323
+ for (const step of steps) {
324
+ const msg = parseStep(step);
325
+ if (msg) messages.push(msg);
326
+ }
327
+
328
+ // If steps are truncated, fill in the tail from generatorMetadata
329
+ const tail = getTailMessages(chat, messages);
330
+ if (tail.length > 0) {
331
+ messages.push(...tail);
332
+ }
333
+
184
334
  return messages;
185
335
  }
186
336
 
187
- module.exports = { name, sources, getChats, getMessages };
337
+ function resetCache() { _lsCache = null; }
338
+
339
+ module.exports = { name, sources, getChats, getMessages, resetCache };
package/index.js CHANGED
@@ -8,8 +8,120 @@ const { execSync } = require('child_process');
8
8
 
9
9
  const HOME = os.homedir();
10
10
  const PORT = process.env.PORT || 4637;
11
+ const RELAY_PORT = process.env.RELAY_PORT || 4638;
11
12
  const noCache = process.argv.includes('--no-cache');
12
13
  const collectOnly = process.argv.includes('--collect');
14
+ const isRelay = process.argv.includes('--relay');
15
+ const joinIndex = process.argv.indexOf('--join');
16
+ const isJoin = joinIndex !== -1;
17
+
18
+ // ── Relay mode ───────────────────────────────────────────────
19
+ if (isRelay) {
20
+ const { initRelayDb, getRelayDb, createRelayApp } = require('./relay-server');
21
+ const { wireMcpToExpress } = require('./mcp-server');
22
+
23
+ console.log('');
24
+ console.log(chalk.bold(' ⚡ Agentlytics Relay'));
25
+ console.log(chalk.dim(' Multi-user context sharing server'));
26
+ console.log('');
27
+
28
+ initRelayDb();
29
+ console.log(chalk.green(' ✓ Relay database initialized'));
30
+
31
+ const app = createRelayApp();
32
+ wireMcpToExpress(app, getRelayDb);
33
+ console.log(chalk.green(' ✓ MCP server registered'));
34
+
35
+ if (process.env.RELAY_PASSWORD) {
36
+ console.log(chalk.green(' ✓ Password protection enabled'));
37
+ } else {
38
+ console.log(chalk.yellow(' ⚠ No password set (set RELAY_PASSWORD env to protect)'));
39
+ }
40
+
41
+ app.listen(RELAY_PORT, () => {
42
+ const localIp = getLocalIp();
43
+ const relayUrl = `http://${localIp}:${RELAY_PORT}`;
44
+
45
+ console.log('');
46
+ console.log(chalk.green(` ✓ Relay server running on port ${RELAY_PORT}`));
47
+ console.log('');
48
+ console.log(chalk.bold(' Share this command with your team:'));
49
+ console.log('');
50
+ console.log(chalk.cyan(` npx agentlytics --join ${localIp}:${RELAY_PORT} --username <name>`));
51
+ console.log('');
52
+ console.log(chalk.bold(' MCP server endpoint (add to your AI client):'));
53
+ console.log('');
54
+ console.log(chalk.cyan(` ${relayUrl}/mcp`));
55
+ console.log('');
56
+ console.log(chalk.dim(' REST endpoints:'));
57
+ console.log(chalk.dim(` GET ${relayUrl}/relay/health`));
58
+ console.log(chalk.dim(` GET ${relayUrl}/relay/users`));
59
+ console.log(chalk.dim(` GET ${relayUrl}/relay/search?q=<query>`));
60
+ console.log(chalk.dim(` GET ${relayUrl}/relay/activity/<username>`));
61
+ console.log(chalk.dim(` GET ${relayUrl}/relay/session/<chatId>`));
62
+ console.log('');
63
+ console.log(chalk.dim(' Press Ctrl+C to stop'));
64
+ console.log('');
65
+ });
66
+
67
+ // Skip the rest of the normal flow
68
+ return;
69
+ }
70
+
71
+ // ── Join mode ────────────────────────────────────────────────
72
+ if (isJoin) {
73
+ (async () => {
74
+ const relayAddress = process.argv[joinIndex + 1];
75
+ const usernameIndex = process.argv.indexOf('--username');
76
+ let username = usernameIndex !== -1 ? process.argv[usernameIndex + 1] : null;
77
+
78
+ if (!relayAddress) {
79
+ console.error(chalk.red('\n ✗ Missing relay address. Usage: npx agentlytics --join <host:port> --username <name>\n'));
80
+ process.exit(1);
81
+ }
82
+
83
+ // Auto-detect username from git config if not provided
84
+ if (!username) {
85
+ try {
86
+ const gitEmail = execSync('git config user.email', { encoding: 'utf-8' }).trim();
87
+ if (gitEmail) username = gitEmail;
88
+ } catch {}
89
+ }
90
+
91
+ // If still no username, ask interactively
92
+ if (!username) {
93
+ const readline = require('readline');
94
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
95
+ username = await new Promise(r => {
96
+ rl.question(chalk.bold('\n Enter your username: '), (answer) => {
97
+ rl.close();
98
+ r(answer.trim());
99
+ });
100
+ });
101
+ if (!username) {
102
+ console.error(chalk.red('\n ✗ Username is required.\n'));
103
+ process.exit(1);
104
+ }
105
+ }
106
+
107
+ const { startJoinClient } = require('./relay-client');
108
+ startJoinClient(relayAddress, username);
109
+ })();
110
+
111
+ // Skip the rest of the normal flow
112
+ return;
113
+ }
114
+
115
+ // ── Helper: get local IP for relay ───────────────────────────
116
+ function getLocalIp() {
117
+ const interfaces = os.networkInterfaces();
118
+ for (const name of Object.keys(interfaces)) {
119
+ for (const iface of interfaces[name]) {
120
+ if (iface.family === 'IPv4' && !iface.internal) return iface.address;
121
+ }
122
+ }
123
+ return 'localhost';
124
+ }
13
125
 
14
126
  console.log('');
15
127
  console.log(chalk.bold(' ⚡ Agentlytics'));
@@ -96,7 +208,7 @@ console.log(chalk.dim(' Initializing cache database...'));
96
208
  cache.initDb();
97
209
 
98
210
  // Scan all editors and populate cache
99
- console.log(chalk.dim(' Scanning editors: Cursor, Windsurf, Claude Code, VS Code, Zed, Antigravity, OpenCode, Codex, Gemini CLI, Copilot CLI, Cursor Agent'));
211
+ console.log(chalk.dim(' Scanning editors: Cursor, Windsurf, Claude Code, VS Code, Zed, Antigravity, OpenCode, Codex, Gemini CLI, Copilot CLI, Cursor Agent, Command Code'));
100
212
  const startTime = Date.now();
101
213
  const result = cache.scanAll((progress) => {
102
214
  process.stdout.write(chalk.dim(`\r Scanning: ${progress.scanned}/${progress.total} chats (${progress.analyzed} analyzed, ${progress.skipped} cached)`));