agentlytics 0.1.15 → 0.1.17

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.
@@ -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
- module.exports = { getAllChats, getMessages, editors, editorLabels, resetCaches };
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 };
@@ -1,6 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
+ const Database = require('better-sqlite3');
4
5
 
5
6
  // OpenCode stores data in different locations depending on the platform
6
7
  // - Windows: %LOCALAPPDATA%\opencode\storage (not Roaming)
@@ -22,6 +23,118 @@ const SESSION_DIR = path.join(STORAGE_DIR, 'session');
22
23
  const MESSAGE_DIR = path.join(STORAGE_DIR, 'message');
23
24
  const PART_DIR = path.join(STORAGE_DIR, 'part');
24
25
 
26
+ // OpenCode also stores data in a SQLite database (older/primary store)
27
+ function getOpenCodeDbPath() {
28
+ const home = os.homedir();
29
+ switch (process.platform) {
30
+ case 'win32':
31
+ return path.join(home, 'AppData', 'Local', 'opencode', 'opencode.db');
32
+ case 'darwin':
33
+ case 'linux':
34
+ default:
35
+ return path.join(home, '.local', 'share', 'opencode', 'opencode.db');
36
+ }
37
+ }
38
+
39
+ const DB_PATH = getOpenCodeDbPath();
40
+
41
+ // ============================================================
42
+ // Query SQLite using better-sqlite3
43
+ // ============================================================
44
+
45
+ function queryDb(sql) {
46
+ if (!fs.existsSync(DB_PATH)) return [];
47
+ try {
48
+ const db = new Database(DB_PATH, { readonly: true });
49
+ const rows = db.prepare(sql).all();
50
+ db.close();
51
+ return rows;
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
56
+
57
+ function getSqliteSessions() {
58
+ return queryDb(
59
+ `SELECT s.id, s.title, s.directory, s.time_created, s.time_updated,
60
+ p.worktree, p.name as project_name,
61
+ (SELECT count(*) FROM message m WHERE m.session_id = s.id) as msg_count
62
+ FROM session s LEFT JOIN project p ON s.project_id = p.id
63
+ ORDER BY s.time_updated DESC`
64
+ );
65
+ }
66
+
67
+ function getSqliteMessages(sessionId) {
68
+ if (!fs.existsSync(DB_PATH)) return [];
69
+ try {
70
+ const db = new Database(DB_PATH, { readonly: true });
71
+ const messages = db.prepare(
72
+ `SELECT m.id as msg_id, m.data as msg_data, m.time_created
73
+ FROM message m WHERE m.session_id = ? ORDER BY m.time_created ASC`
74
+ ).all(sessionId);
75
+
76
+ const result = [];
77
+ for (const msg of messages) {
78
+ let msgData;
79
+ try { msgData = JSON.parse(msg.msg_data); } catch { continue; }
80
+
81
+ const role = msgData.role;
82
+ if (!role) continue;
83
+
84
+ const parts = db.prepare(
85
+ `SELECT data FROM part WHERE message_id = ? ORDER BY time_created ASC`
86
+ ).all(msg.msg_id);
87
+
88
+ const contentParts = [];
89
+ for (const part of parts) {
90
+ let partData;
91
+ try { partData = JSON.parse(part.data); } catch { continue; }
92
+ const type = partData.type;
93
+
94
+ if (type === 'text' && partData.text) {
95
+ contentParts.push(partData.text);
96
+ } else if (type === 'thinking' || type === 'reasoning') {
97
+ if (partData.text) contentParts.push(`[thinking] ${partData.text}`);
98
+ } else if (type === 'tool-call' || type === 'tool_use' || type === 'tool-use' || type === 'tool') {
99
+ const toolName = partData.name || partData.toolName || partData.tool || 'tool';
100
+ let argKeys = '';
101
+ try {
102
+ const input = typeof partData.input === 'string' ? JSON.parse(partData.input) : (partData.input || partData.args || partData.arguments || partData.state?.input || {});
103
+ argKeys = typeof input === 'object' ? Object.keys(input).join(', ') : '';
104
+ } catch {}
105
+ contentParts.push(`[tool-call: ${toolName}(${argKeys})]`);
106
+ } else if (type === 'tool-result' || type === 'tool_result') {
107
+ const preview = (partData.text || partData.output || partData.state?.output || '').substring(0, 500);
108
+ contentParts.push(`[tool-result] ${preview}`);
109
+ }
110
+ }
111
+
112
+ const content = contentParts.join('\n');
113
+ if (!content) continue;
114
+
115
+ let modelValue = null;
116
+ if (typeof msgData.modelID === 'string') {
117
+ modelValue = msgData.modelID;
118
+ } else if (msgData.model && typeof msgData.model === 'object' && msgData.model.modelID) {
119
+ modelValue = msgData.model.modelID;
120
+ } else if (typeof msgData.model === 'string') {
121
+ modelValue = msgData.model;
122
+ }
123
+
124
+ result.push({
125
+ role: role === 'user' ? 'user' : role === 'assistant' ? 'assistant' : role,
126
+ content,
127
+ _model: modelValue,
128
+ });
129
+ }
130
+
131
+ db.close();
132
+ return result;
133
+ } catch {
134
+ return [];
135
+ }
136
+ }
137
+
25
138
  // ============================================================
26
139
  // Scan JSON files from OpenCode storage
27
140
  // ============================================================
@@ -156,27 +269,64 @@ function getMessagesForSession(sessionId) {
156
269
  const name = 'opencode';
157
270
 
158
271
  function getChats() {
159
- const sessions = getAllSessions();
160
-
161
- return sessions.map(s => ({
162
- source: 'opencode',
163
- composerId: s.id,
164
- name: s.title || null,
165
- createdAt: s.time?.created || null,
166
- lastUpdatedAt: s.time?.updated || null,
167
- mode: s.mode || 'opencode',
168
- folder: s.directory || null,
169
- encrypted: false,
170
- bubbleCount: getMessageCount(s.id),
171
- _agent: s.agent,
172
- _model: s.modelID,
173
- _provider: s.providerID,
174
- _sessionData: s,
175
- })).sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0));
272
+ const seen = new Set();
273
+ const chats = [];
274
+
275
+ // 1. JSON file-based sessions (newer storage format)
276
+ const fileSessions = getAllSessions();
277
+ for (const s of fileSessions) {
278
+ seen.add(s.id);
279
+ chats.push({
280
+ source: 'opencode',
281
+ composerId: s.id,
282
+ name: s.title || null,
283
+ createdAt: s.time?.created || null,
284
+ lastUpdatedAt: s.time?.updated || null,
285
+ mode: s.mode || 'opencode',
286
+ folder: s.directory || null,
287
+ encrypted: false,
288
+ bubbleCount: getMessageCount(s.id),
289
+ _agent: s.agent,
290
+ _model: s.modelID,
291
+ _provider: s.providerID,
292
+ _sessionData: s,
293
+ _storageType: 'file',
294
+ });
295
+ }
296
+
297
+ // 2. SQLite sessions (older/primary store) — add any not already found in files
298
+ const dbSessions = getSqliteSessions();
299
+ for (const row of dbSessions) {
300
+ if (seen.has(row.id)) continue;
301
+ seen.add(row.id);
302
+ chats.push({
303
+ source: 'opencode',
304
+ composerId: row.id,
305
+ name: cleanTitle(row.title),
306
+ createdAt: row.time_created || null,
307
+ lastUpdatedAt: row.time_updated || null,
308
+ mode: 'opencode',
309
+ folder: row.worktree || row.directory || null,
310
+ encrypted: false,
311
+ bubbleCount: row.msg_count || 0,
312
+ _storageType: 'sqlite',
313
+ });
314
+ }
315
+
316
+ return chats.sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0));
317
+ }
318
+
319
+ function cleanTitle(title) {
320
+ if (!title) return null;
321
+ if (title.startsWith('New session - ')) return null;
322
+ return title.substring(0, 120) || null;
176
323
  }
177
324
 
178
325
  function getMessages(chat) {
179
- return getMessagesForSession(chat.composerId);
326
+ // Prefer file-based messages; fall back to SQLite
327
+ const fileMessages = getMessagesForSession(chat.composerId);
328
+ if (fileMessages.length > 0) return fileMessages;
329
+ return getSqliteMessages(chat.composerId);
180
330
  }
181
331
 
182
332
  const labels = { 'opencode': 'OpenCode' };
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 };