agentlytics 0.1.15 → 0.1.16
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 +2 -0
- package/editors/antigravity.js +507 -0
- package/editors/claude.js +100 -1
- package/editors/codex.js +61 -0
- package/editors/copilot.js +68 -1
- package/editors/cursor.js +73 -1
- package/editors/index.js +22 -2
- package/editors/vscode.js +70 -1
- package/editors/windsurf.js +154 -59
- package/package.json +1 -1
- package/server.js +10 -0
- package/ui/src/App.jsx +4 -1
- package/ui/src/lib/api.js +5 -0
- package/ui/src/pages/Subscriptions.jsx +413 -0
package/editors/copilot.js
CHANGED
|
@@ -171,6 +171,73 @@ function safeParse(str) {
|
|
|
171
171
|
try { return JSON.parse(str); } catch { return {}; }
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
// ============================================================
|
|
175
|
+
// Usage / quota data from GitHub Copilot internal API
|
|
176
|
+
// ============================================================
|
|
177
|
+
|
|
178
|
+
function getCopilotToken() {
|
|
179
|
+
// GitHub Copilot stores its OAuth token in ~/.config/github-copilot/apps.json
|
|
180
|
+
const appsPath = path.join(os.homedir(), '.config', 'github-copilot', 'apps.json');
|
|
181
|
+
try {
|
|
182
|
+
if (!fs.existsSync(appsPath)) return null;
|
|
183
|
+
const data = JSON.parse(fs.readFileSync(appsPath, 'utf-8'));
|
|
184
|
+
for (const entry of Object.values(data)) {
|
|
185
|
+
if (entry.oauth_token) return { token: entry.oauth_token, user: entry.user || null };
|
|
186
|
+
}
|
|
187
|
+
} catch {}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fetchCopilotStatus(token) {
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
const https = require('https');
|
|
194
|
+
const req = https.get('https://api.github.com/copilot_internal/v2/token', {
|
|
195
|
+
headers: {
|
|
196
|
+
'Authorization': `token ${token}`,
|
|
197
|
+
'Accept': 'application/json',
|
|
198
|
+
'User-Agent': 'agentlytics/1.0',
|
|
199
|
+
},
|
|
200
|
+
timeout: 10000,
|
|
201
|
+
}, (res) => {
|
|
202
|
+
let data = '';
|
|
203
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
204
|
+
res.on('end', () => {
|
|
205
|
+
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
req.on('error', () => resolve(null));
|
|
209
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function getUsage() {
|
|
214
|
+
const creds = getCopilotToken();
|
|
215
|
+
if (!creds) return null;
|
|
216
|
+
|
|
217
|
+
const status = await fetchCopilotStatus(creds.token);
|
|
218
|
+
if (!status || status.message) return null;
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
source: 'copilot-cli',
|
|
222
|
+
plan: {
|
|
223
|
+
name: status.sku || null,
|
|
224
|
+
individual: status.individual || false,
|
|
225
|
+
},
|
|
226
|
+
features: {
|
|
227
|
+
chat: status.chat_enabled || false,
|
|
228
|
+
codeReview: status.code_review_enabled || false,
|
|
229
|
+
agentMode: status.agent_mode_auto_approval || false,
|
|
230
|
+
},
|
|
231
|
+
limits: {
|
|
232
|
+
quotas: status.limited_user_quotas || null,
|
|
233
|
+
resetDate: status.limited_user_reset_date || null,
|
|
234
|
+
},
|
|
235
|
+
user: {
|
|
236
|
+
login: creds.user || null,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
174
241
|
const labels = { 'copilot-cli': 'Copilot CLI' };
|
|
175
242
|
|
|
176
|
-
module.exports = { name, labels, getChats, getMessages };
|
|
243
|
+
module.exports = { name, labels, getChats, getMessages, getUsage };
|
package/editors/cursor.js
CHANGED
|
@@ -341,6 +341,78 @@ function getMessages(chat) {
|
|
|
341
341
|
return msgs;
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
// ============================================================
|
|
345
|
+
// Usage / quota data from Cursor REST API
|
|
346
|
+
// ============================================================
|
|
347
|
+
|
|
348
|
+
function getCursorAccessToken() {
|
|
349
|
+
try {
|
|
350
|
+
const db = new Database(GLOBAL_STORAGE_DB, { readonly: true });
|
|
351
|
+
const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'").get();
|
|
352
|
+
db.close();
|
|
353
|
+
return row ? row.value : null;
|
|
354
|
+
} catch { return null; }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function cursorApiFetch(endpoint, token) {
|
|
358
|
+
return new Promise((resolve) => {
|
|
359
|
+
const https = require('https');
|
|
360
|
+
const url = `https://api2.cursor.sh/auth/${endpoint}`;
|
|
361
|
+
const req = https.get(url, {
|
|
362
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
363
|
+
timeout: 10000,
|
|
364
|
+
}, (res) => {
|
|
365
|
+
let data = '';
|
|
366
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
367
|
+
res.on('end', () => {
|
|
368
|
+
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
req.on('error', () => resolve(null));
|
|
372
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function getUsage() {
|
|
377
|
+
const token = getCursorAccessToken();
|
|
378
|
+
if (!token) return null;
|
|
379
|
+
|
|
380
|
+
const [profile, usage] = await Promise.all([
|
|
381
|
+
cursorApiFetch('full_stripe_profile', token),
|
|
382
|
+
cursorApiFetch('usage', token),
|
|
383
|
+
]);
|
|
384
|
+
|
|
385
|
+
if (!profile && !usage) return null;
|
|
386
|
+
|
|
387
|
+
const result = {
|
|
388
|
+
source: 'cursor',
|
|
389
|
+
plan: {
|
|
390
|
+
name: profile?.individualMembershipType || profile?.membershipType || null,
|
|
391
|
+
status: profile?.subscriptionStatus || null,
|
|
392
|
+
isTeamMember: profile?.isTeamMember || false,
|
|
393
|
+
isYearlyPlan: profile?.isYearlyPlan || false,
|
|
394
|
+
},
|
|
395
|
+
usage: {},
|
|
396
|
+
startOfMonth: usage?.startOfMonth || null,
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Parse per-model usage from the usage endpoint
|
|
400
|
+
if (usage) {
|
|
401
|
+
for (const [model, data] of Object.entries(usage)) {
|
|
402
|
+
if (model === 'startOfMonth') continue;
|
|
403
|
+
result.usage[model] = {
|
|
404
|
+
numRequests: data.numRequests || 0,
|
|
405
|
+
numRequestsTotal: data.numRequestsTotal || 0,
|
|
406
|
+
numTokens: data.numTokens || 0,
|
|
407
|
+
maxRequestUsage: data.maxRequestUsage || null,
|
|
408
|
+
maxTokenUsage: data.maxTokenUsage || null,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
|
|
344
416
|
const labels = { 'cursor': 'Cursor' };
|
|
345
417
|
|
|
346
|
-
module.exports = { name, labels, getChats, getMessages };
|
|
418
|
+
module.exports = { name, labels, getChats, getMessages, getUsage };
|
package/editors/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const cursor = require('./cursor');
|
|
2
2
|
const windsurf = require('./windsurf');
|
|
3
|
+
const antigravity = require('./antigravity');
|
|
3
4
|
const claude = require('./claude');
|
|
4
5
|
const vscode = require('./vscode');
|
|
5
6
|
const zed = require('./zed');
|
|
@@ -12,7 +13,7 @@ const commandcode = require('./commandcode');
|
|
|
12
13
|
const goose = require('./goose');
|
|
13
14
|
const kiro = require('./kiro');
|
|
14
15
|
|
|
15
|
-
const editors = [cursor, windsurf, claude, vscode, zed, opencode, codex, gemini, copilot, cursorAgent, commandcode, goose, kiro];
|
|
16
|
+
const editors = [cursor, windsurf, antigravity, claude, vscode, zed, opencode, codex, gemini, copilot, cursorAgent, commandcode, goose, kiro];
|
|
16
17
|
|
|
17
18
|
// Build a unified source → display-label map from all editor modules
|
|
18
19
|
const editorLabels = {};
|
|
@@ -60,4 +61,23 @@ function resetCaches() {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Get usage / quota data from all editors that support it.
|
|
66
|
+
* Returns an array of usage objects, one per editor/variant.
|
|
67
|
+
*/
|
|
68
|
+
async function getAllUsage() {
|
|
69
|
+
const results = [];
|
|
70
|
+
for (const editor of editors) {
|
|
71
|
+
if (typeof editor.getUsage !== 'function') continue;
|
|
72
|
+
try {
|
|
73
|
+
const usage = await editor.getUsage();
|
|
74
|
+
if (!usage) continue;
|
|
75
|
+
// Windsurf returns an array (one per variant), Cursor returns a single object
|
|
76
|
+
if (Array.isArray(usage)) results.push(...usage);
|
|
77
|
+
else results.push(usage);
|
|
78
|
+
} catch { /* skip broken adapters */ }
|
|
79
|
+
}
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { getAllChats, getMessages, editors, editorLabels, resetCaches, getAllUsage };
|
package/editors/vscode.js
CHANGED
|
@@ -315,6 +315,75 @@ function getMessages(chat) {
|
|
|
315
315
|
return messages;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
// ============================================================
|
|
319
|
+
// Usage / quota data from GitHub Copilot internal API
|
|
320
|
+
// ============================================================
|
|
321
|
+
|
|
322
|
+
function getCopilotToken() {
|
|
323
|
+
const appsPath = path.join(os.homedir(), '.config', 'github-copilot', 'apps.json');
|
|
324
|
+
try {
|
|
325
|
+
if (!fs.existsSync(appsPath)) return null;
|
|
326
|
+
const data = JSON.parse(fs.readFileSync(appsPath, 'utf-8'));
|
|
327
|
+
// Pick the first available oauth_token
|
|
328
|
+
for (const entry of Object.values(data)) {
|
|
329
|
+
if (entry.oauth_token) return { token: entry.oauth_token, user: entry.user || null };
|
|
330
|
+
}
|
|
331
|
+
} catch {}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function fetchCopilotStatus(token) {
|
|
336
|
+
return new Promise((resolve) => {
|
|
337
|
+
const https = require('https');
|
|
338
|
+
const req = https.get('https://api.github.com/copilot_internal/v2/token', {
|
|
339
|
+
headers: {
|
|
340
|
+
'Authorization': `token ${token}`,
|
|
341
|
+
'Accept': 'application/json',
|
|
342
|
+
'User-Agent': 'agentlytics/1.0',
|
|
343
|
+
},
|
|
344
|
+
timeout: 10000,
|
|
345
|
+
}, (res) => {
|
|
346
|
+
let data = '';
|
|
347
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
348
|
+
res.on('end', () => {
|
|
349
|
+
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
req.on('error', () => resolve(null));
|
|
353
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function getUsage() {
|
|
358
|
+
const creds = getCopilotToken();
|
|
359
|
+
if (!creds) return null;
|
|
360
|
+
|
|
361
|
+
const status = await fetchCopilotStatus(creds.token);
|
|
362
|
+
if (!status || status.message) return null;
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
source: 'vscode',
|
|
366
|
+
plan: {
|
|
367
|
+
name: status.sku || null,
|
|
368
|
+
individual: status.individual || false,
|
|
369
|
+
},
|
|
370
|
+
features: {
|
|
371
|
+
chat: status.chat_enabled || false,
|
|
372
|
+
codeReview: status.code_review_enabled || false,
|
|
373
|
+
agentMode: status.agent_mode_auto_approval || false,
|
|
374
|
+
xcode: status.xcode || false,
|
|
375
|
+
mcp: status.mcp || false,
|
|
376
|
+
},
|
|
377
|
+
limits: {
|
|
378
|
+
quotas: status.limited_user_quotas || null,
|
|
379
|
+
resetDate: status.limited_user_reset_date || null,
|
|
380
|
+
},
|
|
381
|
+
user: {
|
|
382
|
+
login: creds.user || null,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
318
387
|
const labels = { 'vscode': 'VS Code', 'vscode-insiders': 'VS Code Insiders' };
|
|
319
388
|
|
|
320
|
-
module.exports = { name, labels, getChats, getMessages };
|
|
389
|
+
module.exports = { name, labels, getChats, getMessages, getUsage };
|
package/editors/windsurf.js
CHANGED
|
@@ -1,32 +1,14 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const fs = require('fs');
|
|
2
5
|
|
|
3
|
-
// Windsurf
|
|
6
|
+
// Windsurf variants: Windsurf, Windsurf Next
|
|
4
7
|
const VARIANTS = [
|
|
5
|
-
{ id: 'windsurf', matchKey: 'ide', matchVal: 'windsurf', https: false },
|
|
6
|
-
{ id: 'windsurf-next', matchKey: 'ide', matchVal: 'windsurf-next', https: false },
|
|
7
|
-
{ id: 'antigravity', matchKey: 'appDataDir', matchVal: 'antigravity', https: true },
|
|
8
|
+
{ id: 'windsurf', matchKey: 'ide', matchVal: 'windsurf', https: false, appName: 'Windsurf', needsMetadata: true },
|
|
9
|
+
{ id: 'windsurf-next', matchKey: 'ide', matchVal: 'windsurf-next', https: false, appName: 'Windsurf - Next', needsMetadata: true },
|
|
8
10
|
];
|
|
9
11
|
|
|
10
|
-
// Antigravity model ID to friendly name mapping
|
|
11
|
-
const ANTIGRAVITY_MODEL_MAP = {
|
|
12
|
-
'MODEL_PLACEHOLDER_M1': 'claude-3-5-sonnet-20241022',
|
|
13
|
-
'MODEL_PLACEHOLDER_M2': 'claude-3-5-sonnet-20241022',
|
|
14
|
-
'MODEL_PLACEHOLDER_M3': 'claude-3-5-sonnet-20241022',
|
|
15
|
-
'MODEL_PLACEHOLDER_M4': 'claude-3-5-haiku-20241022',
|
|
16
|
-
'MODEL_PLACEHOLDER_M5': 'claude-3-5-haiku-20241022',
|
|
17
|
-
'MODEL_PLACEHOLDER_M6': 'claude-3-5-haiku-20241022',
|
|
18
|
-
'MODEL_PLACEHOLDER_M7': 'claude-3-5-sonnet-20241022',
|
|
19
|
-
'MODEL_PLACEHOLDER_M8': 'claude-3.5-sonnet',
|
|
20
|
-
'MODEL_PLACEHOLDER_M9': 'claude-3.5-sonnet',
|
|
21
|
-
'MODEL_PLACEHOLDER_M10': 'claude-3.5-sonnet',
|
|
22
|
-
'MODEL_CLAUDE_4_5_SONNET': 'claude-4.5-sonnet',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function normalizeAntigravityModel(modelId) {
|
|
26
|
-
if (!modelId) return null;
|
|
27
|
-
return ANTIGRAVITY_MODEL_MAP[modelId] || modelId;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
12
|
// ============================================================
|
|
31
13
|
// Cross-platform process utilities
|
|
32
14
|
// ============================================================
|
|
@@ -102,7 +84,7 @@ function getListeningPorts(pid) {
|
|
|
102
84
|
}
|
|
103
85
|
|
|
104
86
|
// ============================================================
|
|
105
|
-
// Find running Windsurf
|
|
87
|
+
// Find running Windsurf language server (port + CSRF token)
|
|
106
88
|
// ============================================================
|
|
107
89
|
|
|
108
90
|
let _lsCache = null;
|
|
@@ -148,7 +130,6 @@ function findLanguageServers() {
|
|
|
148
130
|
const ide = ideMatch ? ideMatch[1] : null;
|
|
149
131
|
const appDataDir = appDirMatch ? appDirMatch[1] : null;
|
|
150
132
|
|
|
151
|
-
// Antigravity has a separate extension server CSRF token
|
|
152
133
|
const extCsrfMatch = commandLine.match(/--extension_server_csrf_token\s+(\S+)/);
|
|
153
134
|
|
|
154
135
|
// Check for explicit server port (Antigravity uses --server_port)
|
|
@@ -169,9 +150,8 @@ function findLanguageServers() {
|
|
|
169
150
|
port = Math.min(...ports);
|
|
170
151
|
}
|
|
171
152
|
|
|
172
|
-
if (ide
|
|
173
|
-
|
|
174
|
-
_lsCache.push({ ide, appDataDir, port, csrf, pid, extCsrf: extCsrfMatch ? extCsrfMatch[1] : null, isHttps });
|
|
153
|
+
if (ide) {
|
|
154
|
+
_lsCache.push({ ide, appDataDir, port, csrf, pid, extCsrf: extCsrfMatch ? extCsrfMatch[1] : null, isHttps: false });
|
|
175
155
|
}
|
|
176
156
|
}
|
|
177
157
|
|
|
@@ -193,18 +173,15 @@ function getLsForVariant(variant) {
|
|
|
193
173
|
// Connect protocol HTTP client for language server RPC
|
|
194
174
|
// ============================================================
|
|
195
175
|
|
|
196
|
-
function callRpc(port, csrf, method, body,
|
|
176
|
+
function callRpc(port, csrf, method, body, extCsrf = null) {
|
|
197
177
|
const data = JSON.stringify(body || {});
|
|
198
|
-
const
|
|
199
|
-
const url = `${scheme}://127.0.0.1:${port}/exa.language_server_pb.LanguageServerService/${method}`;
|
|
200
|
-
const insecure = isHttps ? '-k ' : '';
|
|
178
|
+
const url = `http://127.0.0.1:${port}/exa.language_server_pb.LanguageServerService/${method}`;
|
|
201
179
|
|
|
202
|
-
|
|
203
|
-
const actualCsrf = useMainCsrf ? csrf : (extCsrf || csrf);
|
|
180
|
+
const actualCsrf = extCsrf || csrf;
|
|
204
181
|
|
|
205
182
|
try {
|
|
206
183
|
const result = execSync(
|
|
207
|
-
`curl -s
|
|
184
|
+
`curl -s -X POST ${JSON.stringify(url)} ` +
|
|
208
185
|
`-H "Content-Type: application/json" ` +
|
|
209
186
|
`-H "x-codeium-csrf-token: ${actualCsrf}" ` +
|
|
210
187
|
`-d ${JSON.stringify(data)} ` +
|
|
@@ -220,7 +197,7 @@ function callRpc(port, csrf, method, body, isHttps = false, extCsrf = null, useM
|
|
|
220
197
|
// ============================================================
|
|
221
198
|
|
|
222
199
|
const name = 'windsurf';
|
|
223
|
-
const sources = ['windsurf', 'windsurf-next'
|
|
200
|
+
const sources = ['windsurf', 'windsurf-next'];
|
|
224
201
|
|
|
225
202
|
function getChats() {
|
|
226
203
|
const chats = [];
|
|
@@ -229,17 +206,13 @@ function getChats() {
|
|
|
229
206
|
const ls = getLsForVariant(variant);
|
|
230
207
|
if (!ls) continue;
|
|
231
208
|
|
|
232
|
-
|
|
233
|
-
const useMainCsrf = variant.id === 'antigravity';
|
|
234
|
-
const resp = callRpc(ls.port, ls.csrf, 'GetAllCascadeTrajectories', {}, ls.isHttps, ls.extCsrf, useMainCsrf);
|
|
209
|
+
const resp = callRpc(ls.port, ls.csrf, 'GetAllCascadeTrajectories', {}, ls.extCsrf);
|
|
235
210
|
if (!resp || !resp.trajectorySummaries) continue;
|
|
236
211
|
|
|
237
212
|
for (const [cascadeId, summary] of Object.entries(resp.trajectorySummaries)) {
|
|
238
213
|
const ws = (summary.workspaces || [])[0];
|
|
239
214
|
const folder = ws?.workspaceFolderAbsoluteUri?.replace('file://', '') || null;
|
|
240
215
|
const rawModel = summary.lastGeneratorModelUid;
|
|
241
|
-
// Normalize Antigravity models so they show correctly in dashboard
|
|
242
|
-
const normalizedModel = variant.id === 'antigravity' && rawModel ? normalizeAntigravityModel(rawModel) : rawModel;
|
|
243
216
|
chats.push({
|
|
244
217
|
source: variant.id,
|
|
245
218
|
composerId: cascadeId,
|
|
@@ -253,9 +226,8 @@ function getChats() {
|
|
|
253
226
|
_port: ls.port,
|
|
254
227
|
_csrf: ls.csrf,
|
|
255
228
|
_extCsrf: ls.extCsrf,
|
|
256
|
-
_isHttps: ls.isHttps,
|
|
257
229
|
_stepCount: summary.stepCount,
|
|
258
|
-
_model:
|
|
230
|
+
_model: rawModel,
|
|
259
231
|
_rawModel: rawModel,
|
|
260
232
|
});
|
|
261
233
|
}
|
|
@@ -267,19 +239,16 @@ function getChats() {
|
|
|
267
239
|
function getSteps(chat) {
|
|
268
240
|
if (!chat._port || !chat._csrf) return [];
|
|
269
241
|
|
|
270
|
-
// Determine if this is Antigravity based on source
|
|
271
|
-
const isAntigravity = chat.source === 'antigravity';
|
|
272
|
-
|
|
273
242
|
// Prefer GetCascadeTrajectorySteps (returns more steps than GetCascadeTrajectory)
|
|
274
243
|
const resp = callRpc(chat._port, chat._csrf, 'GetCascadeTrajectorySteps', {
|
|
275
244
|
cascadeId: chat.composerId,
|
|
276
|
-
}, chat.
|
|
245
|
+
}, chat._extCsrf);
|
|
277
246
|
if (resp && resp.steps && resp.steps.length > 0) return resp.steps;
|
|
278
247
|
|
|
279
248
|
// Fallback to old method
|
|
280
249
|
const resp2 = callRpc(chat._port, chat._csrf, 'GetCascadeTrajectory', {
|
|
281
250
|
cascadeId: chat.composerId,
|
|
282
|
-
}, chat.
|
|
251
|
+
}, chat._extCsrf);
|
|
283
252
|
if (resp2 && resp2.trajectory && resp2.trajectory.steps) return resp2.trajectory.steps;
|
|
284
253
|
|
|
285
254
|
return [];
|
|
@@ -291,10 +260,9 @@ function getSteps(chat) {
|
|
|
291
260
|
* We find the overlap with step-based messages by matching the last user message content.
|
|
292
261
|
*/
|
|
293
262
|
function getTailMessages(chat, stepMessages) {
|
|
294
|
-
const isAntigravity = chat.source === 'antigravity';
|
|
295
263
|
const resp = callRpc(chat._port, chat._csrf, 'GetCascadeTrajectory', {
|
|
296
264
|
cascadeId: chat.composerId,
|
|
297
|
-
}, chat.
|
|
265
|
+
}, chat._extCsrf);
|
|
298
266
|
if (!resp || !resp.trajectory) return [];
|
|
299
267
|
|
|
300
268
|
const gm = resp.trajectory.generatorMetadata || [];
|
|
@@ -350,7 +318,7 @@ function getTailMessages(chat, stepMessages) {
|
|
|
350
318
|
return tail;
|
|
351
319
|
}
|
|
352
320
|
|
|
353
|
-
function parseStep(step
|
|
321
|
+
function parseStep(step) {
|
|
354
322
|
const type = step.type || '';
|
|
355
323
|
const meta = step.metadata || {};
|
|
356
324
|
|
|
@@ -386,12 +354,11 @@ function parseStep(step, isAntigravity = false) {
|
|
|
386
354
|
}
|
|
387
355
|
}
|
|
388
356
|
if (parts.length > 0) {
|
|
389
|
-
|
|
390
|
-
const model = meta.generatorModel || meta.generatorModelUid;
|
|
357
|
+
const model = meta.generatorModelUid;
|
|
391
358
|
return {
|
|
392
359
|
role: 'assistant',
|
|
393
360
|
content: parts.join('\n'),
|
|
394
|
-
_model:
|
|
361
|
+
_model: model,
|
|
395
362
|
_toolCalls,
|
|
396
363
|
};
|
|
397
364
|
}
|
|
@@ -462,10 +429,9 @@ function parseStep(step, isAntigravity = false) {
|
|
|
462
429
|
|
|
463
430
|
function getMessages(chat) {
|
|
464
431
|
const steps = getSteps(chat);
|
|
465
|
-
const isAntigravity = chat.source === 'antigravity';
|
|
466
432
|
const messages = [];
|
|
467
433
|
for (const step of steps) {
|
|
468
|
-
const msg = parseStep(step
|
|
434
|
+
const msg = parseStep(step);
|
|
469
435
|
if (msg) messages.push(msg);
|
|
470
436
|
}
|
|
471
437
|
|
|
@@ -478,8 +444,137 @@ function getMessages(chat) {
|
|
|
478
444
|
return messages;
|
|
479
445
|
}
|
|
480
446
|
|
|
447
|
+
// ============================================================
|
|
448
|
+
// Usage / quota data from language server RPC
|
|
449
|
+
// ============================================================
|
|
450
|
+
|
|
451
|
+
function getWindsurfApiKey(appName) {
|
|
452
|
+
if (!appName) return null;
|
|
453
|
+
try {
|
|
454
|
+
const HOME = os.homedir();
|
|
455
|
+
let dbPath;
|
|
456
|
+
switch (process.platform) {
|
|
457
|
+
case 'darwin':
|
|
458
|
+
dbPath = path.join(HOME, 'Library', 'Application Support', appName, 'User', 'globalStorage', 'state.vscdb');
|
|
459
|
+
break;
|
|
460
|
+
case 'win32':
|
|
461
|
+
dbPath = path.join(HOME, 'AppData', 'Roaming', appName, 'User', 'globalStorage', 'state.vscdb');
|
|
462
|
+
break;
|
|
463
|
+
default:
|
|
464
|
+
dbPath = path.join(HOME, '.config', appName, 'User', 'globalStorage', 'state.vscdb');
|
|
465
|
+
}
|
|
466
|
+
if (!fs.existsSync(dbPath)) return null;
|
|
467
|
+
const Database = require('better-sqlite3');
|
|
468
|
+
const db = new Database(dbPath, { readonly: true });
|
|
469
|
+
const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus'").get();
|
|
470
|
+
db.close();
|
|
471
|
+
if (!row) return null;
|
|
472
|
+
const parsed = JSON.parse(row.value);
|
|
473
|
+
return parsed.apiKey || null;
|
|
474
|
+
} catch { return null; }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function getUsage() {
|
|
478
|
+
const results = [];
|
|
479
|
+
|
|
480
|
+
for (const variant of VARIANTS) {
|
|
481
|
+
const ls = getLsForVariant(variant);
|
|
482
|
+
if (!ls) continue;
|
|
483
|
+
|
|
484
|
+
const apiKey = getWindsurfApiKey(variant.appName);
|
|
485
|
+
if (!apiKey) continue;
|
|
486
|
+
const body = {
|
|
487
|
+
metadata: {
|
|
488
|
+
api_key: apiKey,
|
|
489
|
+
ide_name: variant.id,
|
|
490
|
+
ide_version: '1.0.0',
|
|
491
|
+
extension_version: '1.0.0',
|
|
492
|
+
locale: 'en',
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const resp = callRpc(ls.port, ls.csrf, 'GetUserStatus', body, ls.extCsrf);
|
|
497
|
+
if (!resp || !resp.userStatus) continue;
|
|
498
|
+
|
|
499
|
+
const us = resp.userStatus;
|
|
500
|
+
const ps = us.planStatus || {};
|
|
501
|
+
const pi = ps.planInfo || {};
|
|
502
|
+
const modelConfigs = (us.cascadeModelConfigData || {}).clientModelConfigs || [];
|
|
503
|
+
|
|
504
|
+
const models = modelConfigs.map((m) => {
|
|
505
|
+
const qi = m.quotaInfo || {};
|
|
506
|
+
return {
|
|
507
|
+
label: m.label || null,
|
|
508
|
+
model: m.modelOrAlias?.model || null,
|
|
509
|
+
remainingFraction: qi.remainingFraction != null ? qi.remainingFraction : null,
|
|
510
|
+
resetTime: qi.resetTime || null,
|
|
511
|
+
supportsImages: m.supportsImages || false,
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Raw values are in internal units (÷100 for display credits)
|
|
516
|
+
const promptAlloc = (ps.availablePromptCredits || 0) / 100;
|
|
517
|
+
const promptUsed = (ps.usedPromptCredits || 0) / 100;
|
|
518
|
+
const flexAlloc = (ps.availableFlexCredits || 0) / 100;
|
|
519
|
+
const flexUsed = (ps.usedFlexCredits || 0) / 100;
|
|
520
|
+
const flowAlloc = (ps.availableFlowCredits || 0) / 100;
|
|
521
|
+
const monthlyDisplay = (pi.monthlyPromptCredits || 0) / 100;
|
|
522
|
+
|
|
523
|
+
const remainingPrompt = Math.max(0, promptAlloc - promptUsed);
|
|
524
|
+
const remainingFlex = Math.max(0, flexAlloc - flexUsed);
|
|
525
|
+
const totalRemaining = remainingPrompt + remainingFlex;
|
|
526
|
+
|
|
527
|
+
// Credit multipliers per model
|
|
528
|
+
const creditMultipliers = (pi.creditMultiplierOverrides || []).reduce((acc, entry) => {
|
|
529
|
+
const model = entry.modelOrAlias?.model;
|
|
530
|
+
if (model && entry.creditMultiplier != null) acc[model] = entry.creditMultiplier;
|
|
531
|
+
return acc;
|
|
532
|
+
}, {});
|
|
533
|
+
|
|
534
|
+
results.push({
|
|
535
|
+
source: variant.id,
|
|
536
|
+
plan: {
|
|
537
|
+
name: pi.planName || null,
|
|
538
|
+
tier: pi.teamsTier || null,
|
|
539
|
+
monthlyPromptCredits: monthlyDisplay,
|
|
540
|
+
monthlyFlowCredits: (pi.monthlyFlowCredits || 0) / 100,
|
|
541
|
+
canBuyMoreCredits: pi.canBuyMoreCredits || false,
|
|
542
|
+
},
|
|
543
|
+
usage: {
|
|
544
|
+
promptCredits: { allocated: promptAlloc, used: promptUsed, remaining: remainingPrompt },
|
|
545
|
+
flexCredits: { allocated: flexAlloc, used: flexUsed, remaining: remainingFlex },
|
|
546
|
+
flowCredits: { allocated: flowAlloc },
|
|
547
|
+
totalRemainingCredits: totalRemaining,
|
|
548
|
+
},
|
|
549
|
+
billingCycle: {
|
|
550
|
+
start: ps.planStart || null,
|
|
551
|
+
end: ps.planEnd || null,
|
|
552
|
+
},
|
|
553
|
+
topUp: ps.topUpStatus ? {
|
|
554
|
+
monthlyAmount: ps.topUpStatus.monthlyTopUpAmount || null,
|
|
555
|
+
increment: ps.topUpStatus.topUpIncrement || null,
|
|
556
|
+
} : null,
|
|
557
|
+
features: {
|
|
558
|
+
webSearch: pi.cascadeWebSearchEnabled || false,
|
|
559
|
+
browser: pi.browserEnabled || false,
|
|
560
|
+
knowledgeBase: pi.knowledgeBaseEnabled || false,
|
|
561
|
+
autoRunCommands: pi.cascadeCanAutoRunCommands || false,
|
|
562
|
+
commitMessages: pi.canGenerateCommitMessages || false,
|
|
563
|
+
},
|
|
564
|
+
models,
|
|
565
|
+
creditMultipliers,
|
|
566
|
+
user: {
|
|
567
|
+
name: us.name || null,
|
|
568
|
+
email: us.email || null,
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return results.length > 0 ? results : null;
|
|
574
|
+
}
|
|
575
|
+
|
|
481
576
|
function resetCache() { _lsCache = null; }
|
|
482
577
|
|
|
483
|
-
const labels = { 'windsurf': 'Windsurf', 'windsurf-next': 'Windsurf Next'
|
|
578
|
+
const labels = { 'windsurf': 'Windsurf', 'windsurf-next': 'Windsurf Next' };
|
|
484
579
|
|
|
485
|
-
module.exports = { name, sources, labels, getChats, getMessages, resetCache };
|
|
580
|
+
module.exports = { name, sources, labels, getChats, getMessages, resetCache, getUsage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlytics",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
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": {
|
package/server.js
CHANGED
|
@@ -287,6 +287,16 @@ app.get('/api/share-image', (req, res) => {
|
|
|
287
287
|
}
|
|
288
288
|
});
|
|
289
289
|
|
|
290
|
+
app.get('/api/usage', async (req, res) => {
|
|
291
|
+
try {
|
|
292
|
+
const { getAllUsage } = require('./editors');
|
|
293
|
+
const usage = await getAllUsage();
|
|
294
|
+
res.json(usage);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
res.status(500).json({ error: err.message });
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
290
300
|
app.get('/api/refetch', async (req, res) => {
|
|
291
301
|
res.writeHead(200, {
|
|
292
302
|
'Content-Type': 'text/event-stream',
|
package/ui/src/App.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
2
2
|
import { Routes, Route, NavLink } from 'react-router-dom'
|
|
3
|
-
import { Activity, BarChart3, GitCompare, MessageSquare, FolderOpen, DollarSign, Sun, Moon, RefreshCw, AlertTriangle, Github, Terminal, Database, Users, Plug, Copy, Check, Settings as SettingsIcon } from 'lucide-react'
|
|
3
|
+
import { Activity, BarChart3, GitCompare, MessageSquare, FolderOpen, DollarSign, CreditCard, Sun, Moon, RefreshCw, AlertTriangle, Github, Terminal, Database, Users, Plug, Copy, Check, Settings as SettingsIcon } from 'lucide-react'
|
|
4
4
|
import { fetchOverview, refetchAgents, fetchMode, fetchRelayConfig, getAuthToken, setOnAuthFailure } from './lib/api'
|
|
5
5
|
import { useTheme } from './lib/theme'
|
|
6
6
|
import AnimatedLogo from './components/AnimatedLogo'
|
|
@@ -14,6 +14,7 @@ import ProjectDetail from './pages/ProjectDetail'
|
|
|
14
14
|
import CostAnalysis from './pages/CostAnalysis'
|
|
15
15
|
import SqlViewer from './pages/SqlViewer'
|
|
16
16
|
import Settings from './pages/Settings'
|
|
17
|
+
import Subscriptions from './pages/Subscriptions'
|
|
17
18
|
import RelayDashboard from './pages/RelayDashboard'
|
|
18
19
|
import RelayUserDetail from './pages/RelayUserDetail'
|
|
19
20
|
|
|
@@ -90,6 +91,7 @@ export default function App() {
|
|
|
90
91
|
{ to: '/costs', icon: DollarSign, label: 'Costs' },
|
|
91
92
|
{ to: '/analysis', icon: BarChart3, label: 'Analysis' },
|
|
92
93
|
{ to: '/compare', icon: GitCompare, label: 'Compare' },
|
|
94
|
+
{ to: '/subscriptions', icon: CreditCard, label: 'Subscriptions' },
|
|
93
95
|
{ to: '/sql', icon: Database, label: 'SQL' },
|
|
94
96
|
]
|
|
95
97
|
|
|
@@ -213,6 +215,7 @@ export default function App() {
|
|
|
213
215
|
<Route path="/costs" element={<CostAnalysis overview={overview} />} />
|
|
214
216
|
<Route path="/analysis" element={<DeepAnalysis overview={overview} />} />
|
|
215
217
|
<Route path="/compare" element={<Compare overview={overview} />} />
|
|
218
|
+
<Route path="/subscriptions" element={<Subscriptions />} />
|
|
216
219
|
<Route path="/sql" element={<SqlViewer />} />
|
|
217
220
|
<Route path="/settings" element={<Settings />} />
|
|
218
221
|
</Routes>
|
package/ui/src/lib/api.js
CHANGED
|
@@ -203,6 +203,11 @@ export async function fetchToolCalls(name, opts = {}) {
|
|
|
203
203
|
return res.json();
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
export async function fetchUsage() {
|
|
207
|
+
const res = await fetch(`${BASE}/api/usage`);
|
|
208
|
+
return res.json();
|
|
209
|
+
}
|
|
210
|
+
|
|
206
211
|
// ── Relay API ──
|
|
207
212
|
|
|
208
213
|
export async function fetchMode() {
|