agentlytics 0.0.10 → 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 +84 -1
- package/cache.js +12 -7
- package/editors/index.js +7 -1
- package/editors/windsurf.js +199 -47
- package/index.js +125 -3
- package/mcp-server.js +279 -0
- package/package.json +6 -1
- package/relay-client.js +307 -0
- package/relay-server.js +552 -0
- package/server.js +4 -0
- package/ui/src/App.jsx +154 -45
- package/ui/src/components/ChatSidebar.jsx +27 -155
- package/ui/src/components/EditorBreakdown.jsx +22 -0
- package/ui/src/components/LiveFeed.jsx +138 -0
- package/ui/src/components/LoginScreen.jsx +79 -0
- package/ui/src/components/MessageRenderer.jsx +167 -0
- package/ui/src/components/ModelBreakdown.jsx +23 -0
- package/ui/src/components/SectionTitle.jsx +3 -0
- package/ui/src/lib/api.js +115 -0
- package/ui/src/pages/ChatDetail.jsx +5 -164
- package/ui/src/pages/Dashboard.jsx +1 -4
- package/ui/src/pages/RelayDashboard.jsx +380 -0
- package/ui/src/pages/RelaySessionDetail.jsx +32 -0
- package/ui/src/pages/RelayUserDetail.jsx +204 -0
- package/ui/src/pages/Sessions.jsx +14 -1
- package/ui/vite.config.js +2 -1
package/README.md
CHANGED
|
@@ -32,6 +32,12 @@ npx agentlytics
|
|
|
32
32
|
|
|
33
33
|
Opens at **http://localhost:4637**. Requires Node.js ≥ 18, macOS.
|
|
34
34
|
|
|
35
|
+
To only build the cache database without starting the server:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx agentlytics --collect
|
|
39
|
+
```
|
|
40
|
+
|
|
35
41
|
For local development, run `npm run dev` from the repo root. That starts both the backend on `http://localhost:4637` and the Vite frontend on `http://localhost:5173`.
|
|
36
42
|
|
|
37
43
|
## Features
|
|
@@ -42,6 +48,7 @@ For local development, run `npm run dev` from the repo root. That starts both th
|
|
|
42
48
|
- **Deep Analysis** — Tool frequency, model distribution, token breakdown with drill-down
|
|
43
49
|
- **Compare** — Side-by-side editor comparison with efficiency ratios
|
|
44
50
|
- **Refetch** — One-click cache rebuild with live progress
|
|
51
|
+
- **Relay** — Multi-user context sharing with MCP server for cross-team AI session querying
|
|
45
52
|
|
|
46
53
|
## Supported Editors
|
|
47
54
|
|
|
@@ -66,13 +73,89 @@ For local development, run `npm run dev` from the repo root. That starts both th
|
|
|
66
73
|
|
|
67
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.
|
|
68
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
|
+
|
|
69
148
|
## How It Works
|
|
70
149
|
|
|
71
150
|
```
|
|
72
151
|
Editor files/APIs → editors/*.js → cache.js (SQLite) → server.js (REST) → React SPA
|
|
73
152
|
```
|
|
74
153
|
|
|
75
|
-
|
|
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`.
|
|
76
159
|
|
|
77
160
|
## API
|
|
78
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
|
-
|
|
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
|
-
|
|
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 };
|
package/editors/windsurf.js
CHANGED
|
@@ -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
|
|
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
|
|
157
|
+
if (!resp || !resp.trajectory) return [];
|
|
136
158
|
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
337
|
+
function resetCache() { _lsCache = null; }
|
|
338
|
+
|
|
339
|
+
module.exports = { name, sources, getChats, getMessages, resetCache };
|
package/index.js
CHANGED
|
@@ -8,18 +8,132 @@ 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');
|
|
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
|
+
}
|
|
12
125
|
|
|
13
126
|
console.log('');
|
|
14
127
|
console.log(chalk.bold(' ⚡ Agentlytics'));
|
|
15
128
|
console.log(chalk.dim(' Comprehensive analytics for your AI coding agents'));
|
|
129
|
+
if (collectOnly) console.log(chalk.cyan(' ⟳ Collect-only mode (no server)'));
|
|
16
130
|
console.log('');
|
|
17
131
|
|
|
18
132
|
// ── Build UI if not already built ──────────────────────────
|
|
19
133
|
const publicIndex = path.join(__dirname, 'public', 'index.html');
|
|
20
134
|
const uiDir = path.join(__dirname, 'ui');
|
|
21
135
|
|
|
22
|
-
if (!fs.existsSync(publicIndex) && fs.existsSync(uiDir)) {
|
|
136
|
+
if (!collectOnly && !fs.existsSync(publicIndex) && fs.existsSync(uiDir)) {
|
|
23
137
|
console.log(chalk.cyan(' ⟳ Building dashboard UI (first run)...'));
|
|
24
138
|
try {
|
|
25
139
|
const uiModules = path.join(uiDir, 'node_modules');
|
|
@@ -37,7 +151,7 @@ if (!fs.existsSync(publicIndex) && fs.existsSync(uiDir)) {
|
|
|
37
151
|
console.log('');
|
|
38
152
|
}
|
|
39
153
|
|
|
40
|
-
if (!fs.existsSync(publicIndex)) {
|
|
154
|
+
if (!collectOnly && !fs.existsSync(publicIndex)) {
|
|
41
155
|
console.error(chalk.red(' ✗ No built UI found at public/index.html'));
|
|
42
156
|
console.error(chalk.dim(' Run: cd ui && npm install && npm run build'));
|
|
43
157
|
process.exit(1);
|
|
@@ -94,7 +208,7 @@ console.log(chalk.dim(' Initializing cache database...'));
|
|
|
94
208
|
cache.initDb();
|
|
95
209
|
|
|
96
210
|
// Scan all editors and populate cache
|
|
97
|
-
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'));
|
|
98
212
|
const startTime = Date.now();
|
|
99
213
|
const result = cache.scanAll((progress) => {
|
|
100
214
|
process.stdout.write(chalk.dim(`\r Scanning: ${progress.scanned}/${progress.total} chats (${progress.analyzed} analyzed, ${progress.skipped} cached)`));
|
|
@@ -104,6 +218,14 @@ console.log('');
|
|
|
104
218
|
console.log(chalk.green(` ✓ Cache ready: ${result.total} chats, ${result.analyzed} analyzed, ${result.skipped} cached (${elapsed}s)`));
|
|
105
219
|
console.log('');
|
|
106
220
|
|
|
221
|
+
// In collect-only mode, exit after cache is built
|
|
222
|
+
if (collectOnly) {
|
|
223
|
+
const cacheDbPath = path.join(os.homedir(), '.agentlytics', 'cache.db');
|
|
224
|
+
console.log(chalk.dim(` Cache file: ${cacheDbPath}`));
|
|
225
|
+
console.log('');
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
107
229
|
// Start server
|
|
108
230
|
const app = require('./server');
|
|
109
231
|
app.listen(PORT, () => {
|