agentlytics 0.1.14 → 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.
@@ -1,18 +1,88 @@
1
+ const { execSync } = require('child_process');
1
2
  const path = require('path');
2
- const fs = require('fs');
3
3
  const os = require('os');
4
- const { execSync } = require('child_process');
5
- const http = require('http');
6
-
7
- const HOME = os.homedir();
4
+ const fs = require('fs');
8
5
 
9
- // Windsurf-family variants: Windsurf, Windsurf Next, Antigravity
6
+ // Windsurf variants: Windsurf, Windsurf Next
10
7
  const VARIANTS = [
11
- { id: 'windsurf', matchKey: 'ide', matchVal: 'windsurf', https: false },
12
- { id: 'windsurf-next', matchKey: 'ide', matchVal: 'windsurf-next', https: false },
13
- { 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 },
14
10
  ];
15
11
 
12
+ // ============================================================
13
+ // Cross-platform process utilities
14
+ // ============================================================
15
+
16
+ const IS_WINDOWS = process.platform === 'win32';
17
+
18
+ function getProcessList() {
19
+ try {
20
+ if (IS_WINDOWS) {
21
+ // wmic provides CSV-formatted process data
22
+ const output = execSync('wmic process get CommandLine,ProcessId /format:csv', {
23
+ encoding: 'utf-8',
24
+ maxBuffer: 10 * 1024 * 1024,
25
+ });
26
+ // Parse CSV: skip header, split by comma
27
+ const lines = output.split('\n').slice(1);
28
+ return lines.map(line => {
29
+ const parts = line.split(',');
30
+ if (parts.length < 2) return null;
31
+ const commandLine = parts.slice(0, -1).join(',').trim().replace(/^"|"$/g, '');
32
+ const pid = parts[parts.length - 1].trim();
33
+ return { commandLine, pid };
34
+ }).filter(Boolean);
35
+ } else {
36
+ // ps aux on Unix-like systems
37
+ const output = execSync('ps aux', { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
38
+ return output.split('\n').slice(1).map(line => {
39
+ const parts = line.trim().split(/\s+/);
40
+ if (parts.length < 11) return null;
41
+ const pid = parts[1];
42
+ const commandLine = parts.slice(10).join(' ');
43
+ return { commandLine, pid };
44
+ }).filter(Boolean);
45
+ }
46
+ } catch { return []; }
47
+ }
48
+
49
+ function getListeningPorts(pid) {
50
+ try {
51
+ if (IS_WINDOWS) {
52
+ // netstat -ano shows PID in the last column
53
+ const output = execSync(`netstat -ano | findstr ${pid}`, {
54
+ encoding: 'utf-8',
55
+ maxBuffer: 10 * 1024 * 1024,
56
+ });
57
+ const ports = [];
58
+ for (const line of output.split('\n')) {
59
+ // Match: 127.0.0.1:PORT ... LISTENING PID
60
+ // Check if line ends with the PID we're looking for
61
+ if (!line.trim().endsWith(pid)) continue;
62
+ const match = line.match(/127\.0\.0\.1:(\d+).*LISTENING/);
63
+ if (match) {
64
+ ports.push(parseInt(match[1]));
65
+ }
66
+ }
67
+ return ports;
68
+ } else {
69
+ // lsof on Unix-like systems
70
+ const output = execSync(`lsof -i TCP -P -n -a -p ${pid} 2>/dev/null`, {
71
+ encoding: 'utf-8',
72
+ maxBuffer: 10 * 1024 * 1024,
73
+ });
74
+ const ports = [];
75
+ for (const line of output.split('\n')) {
76
+ const match = line.match(/TCP\s+127\.0\.0\.1:(\d+)\s+\(LISTEN\)/);
77
+ if (match) {
78
+ ports.push(parseInt(match[1]));
79
+ }
80
+ }
81
+ return ports;
82
+ }
83
+ } catch { return []; }
84
+ }
85
+
16
86
  // ============================================================
17
87
  // Find running Windsurf language server (port + CSRF token)
18
88
  // ============================================================
@@ -22,43 +92,69 @@ let _lsCache = null;
22
92
  function findLanguageServers() {
23
93
  if (_lsCache) return _lsCache;
24
94
  _lsCache = [];
25
- try {
26
- const ps = execSync('ps aux', { encoding: 'utf-8', maxBuffer: 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
27
- // Also grab env vars for processes that use WINDSURF_CSRF_TOKEN instead of --csrf_token
28
- const psEnv = execSync('ps eww -A', { encoding: 'utf-8', maxBuffer: 2 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }).split('\n');
29
- const envCsrfByPid = {};
30
- for (const envLine of psEnv) {
31
- const envCsrf = envLine.match(/WINDSURF_CSRF_TOKEN=(\S+)/);
32
- if (envCsrf) {
33
- const envPid = envLine.match(/^\s*(\d+)/);
34
- if (envPid) envCsrfByPid[envPid[1]] = envCsrf[1];
95
+
96
+ // Language server executable name varies by platform
97
+ const serverProcessName = IS_WINDOWS
98
+ ? 'language_server_windows'
99
+ : process.platform === 'darwin'
100
+ ? 'language_server_macos'
101
+ : 'language_server_linux';
102
+
103
+ // On macOS/Linux, also check env vars for WINDSURF_CSRF_TOKEN (newer Windsurf Next passes CSRF via env, not CLI arg)
104
+ const envCsrfByPid = {};
105
+ if (!IS_WINDOWS) {
106
+ try {
107
+ const psEnv = execSync('ps eww -A', { encoding: 'utf-8', maxBuffer: 2 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
108
+ for (const envLine of psEnv.split('\n')) {
109
+ const envCsrf = envLine.match(/WINDSURF_CSRF_TOKEN=(\S+)/);
110
+ if (envCsrf) {
111
+ const envPid = envLine.match(/^\s*(\d+)/);
112
+ if (envPid) envCsrfByPid[envPid[1]] = envCsrf[1];
113
+ }
35
114
  }
115
+ } catch {}
116
+ }
117
+
118
+ for (const proc of getProcessList()) {
119
+ const { commandLine, pid } = proc;
120
+ if (!commandLine.includes(serverProcessName)) continue;
121
+
122
+ const csrfMatch = commandLine.match(/--csrf_token\s+(\S+)/);
123
+ const ideMatch = commandLine.match(/--ide_name\s+(\S+)/);
124
+ const appDirMatch = commandLine.match(/--app_data_dir\s+(\S+)/);
125
+
126
+ // Try CLI arg first, then env var fallback
127
+ const csrf = csrfMatch ? csrfMatch[1] : envCsrfByPid[pid] || null;
128
+ if (!csrf) continue;
129
+
130
+ const ide = ideMatch ? ideMatch[1] : null;
131
+ const appDataDir = appDirMatch ? appDirMatch[1] : null;
132
+
133
+ const extCsrfMatch = commandLine.match(/--extension_server_csrf_token\s+(\S+)/);
134
+
135
+ // Check for explicit server port (Antigravity uses --server_port)
136
+ const serverPortMatch = commandLine.match(/--server_port\s+(\d+)/);
137
+
138
+ // Find actual listening ports for this process
139
+ const ports = getListeningPorts(pid);
140
+ if (ports.length === 0) continue;
141
+
142
+ // Use explicit server_port if available, otherwise use lowest port
143
+ let port;
144
+ if (serverPortMatch) {
145
+ port = parseInt(serverPortMatch[1], 10);
146
+ if (!ports.includes(port)) {
147
+ port = Math.min(...ports);
148
+ }
149
+ } else {
150
+ port = Math.min(...ports);
36
151
  }
37
152
 
38
- for (const line of ps.split('\n')) {
39
- if (!line.includes('language_server_macos')) continue;
40
- const csrfMatch = line.match(/--csrf_token\s+(\S+)/);
41
- const ideMatch = line.match(/--ide_name\s+(\S+)/);
42
- const appDirMatch = line.match(/--app_data_dir\s+(\S+)/);
43
- const pidMatch = line.match(/^\S+\s+(\d+)/);
44
- if (!pidMatch) continue;
45
- const pid = pidMatch[1];
46
- const csrf = csrfMatch ? csrfMatch[1] : envCsrfByPid[pid] || null;
47
- if (!csrf) continue;
48
- const ide = ideMatch ? ideMatch[1] : 'windsurf';
49
- const appDataDir = appDirMatch ? appDirMatch[1] : null;
50
- // Find port by checking listening sockets for this process
51
- try {
52
- const lsof = execSync(`lsof -i TCP -P -n -a -p ${pid} 2>/dev/null`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
53
- for (const l of lsof.split('\n')) {
54
- const portMatch = l.match(/TCP\s+127\.0\.0\.1:(\d+)\s+\(LISTEN\)/);
55
- if (portMatch) {
56
- _lsCache.push({ ide, appDataDir, port: parseInt(portMatch[1]), csrf, pid });
57
- }
58
- }
59
- } catch { /* skip */ }
153
+ if (ide) {
154
+ _lsCache.push({ ide, appDataDir, port, csrf, pid, extCsrf: extCsrfMatch ? extCsrfMatch[1] : null, isHttps: false });
60
155
  }
61
- } catch { /* ps failed */ }
156
+ }
157
+
62
158
  return _lsCache;
63
159
  }
64
160
 
@@ -66,10 +162,9 @@ function getLsForVariant(variant) {
66
162
  const servers = findLanguageServers();
67
163
  let matches;
68
164
  if (variant.matchKey === 'appDataDir') {
69
- matches = servers.filter(s => s.appDataDir === variant.matchVal);
165
+ matches = servers.filter(s => s.appDataDir?.includes(variant.matchVal));
70
166
  } else {
71
- // Exclude servers that have appDataDir set (they belong to a different variant)
72
- matches = servers.filter(s => s.ide === variant.matchVal && !s.appDataDir);
167
+ matches = servers.filter(s => s.ide === variant.matchVal);
73
168
  }
74
169
  return matches.length > 0 ? matches[0] : null;
75
170
  }
@@ -78,16 +173,17 @@ function getLsForVariant(variant) {
78
173
  // Connect protocol HTTP client for language server RPC
79
174
  // ============================================================
80
175
 
81
- function callRpc(port, csrf, method, body, useHttps) {
176
+ function callRpc(port, csrf, method, body, extCsrf = null) {
82
177
  const data = JSON.stringify(body || {});
83
- const scheme = useHttps ? 'https' : 'http';
84
- const url = `${scheme}://127.0.0.1:${port}/exa.language_server_pb.LanguageServerService/${method}`;
85
- const insecure = useHttps ? '-k ' : '';
178
+ const url = `http://127.0.0.1:${port}/exa.language_server_pb.LanguageServerService/${method}`;
179
+
180
+ const actualCsrf = extCsrf || csrf;
181
+
86
182
  try {
87
183
  const result = execSync(
88
- `curl -s ${insecure}-X POST ${JSON.stringify(url)} ` +
184
+ `curl -s -X POST ${JSON.stringify(url)} ` +
89
185
  `-H "Content-Type: application/json" ` +
90
- `-H "x-codeium-csrf-token: ${csrf}" ` +
186
+ `-H "x-codeium-csrf-token: ${actualCsrf}" ` +
91
187
  `-d ${JSON.stringify(data)} ` +
92
188
  `--max-time 10`,
93
189
  { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }
@@ -101,7 +197,7 @@ function callRpc(port, csrf, method, body, useHttps) {
101
197
  // ============================================================
102
198
 
103
199
  const name = 'windsurf';
104
- const sources = ['windsurf', 'windsurf-next', 'antigravity'];
200
+ const sources = ['windsurf', 'windsurf-next'];
105
201
 
106
202
  function getChats() {
107
203
  const chats = [];
@@ -110,12 +206,13 @@ function getChats() {
110
206
  const ls = getLsForVariant(variant);
111
207
  if (!ls) continue;
112
208
 
113
- const resp = callRpc(ls.port, ls.csrf, 'GetAllCascadeTrajectories', {}, variant.https);
209
+ const resp = callRpc(ls.port, ls.csrf, 'GetAllCascadeTrajectories', {}, ls.extCsrf);
114
210
  if (!resp || !resp.trajectorySummaries) continue;
115
211
 
116
212
  for (const [cascadeId, summary] of Object.entries(resp.trajectorySummaries)) {
117
213
  const ws = (summary.workspaces || [])[0];
118
214
  const folder = ws?.workspaceFolderAbsoluteUri?.replace('file://', '') || null;
215
+ const rawModel = summary.lastGeneratorModelUid;
119
216
  chats.push({
120
217
  source: variant.id,
121
218
  composerId: cascadeId,
@@ -128,9 +225,10 @@ function getChats() {
128
225
  bubbleCount: summary.stepCount || 0,
129
226
  _port: ls.port,
130
227
  _csrf: ls.csrf,
131
- _https: variant.https,
228
+ _extCsrf: ls.extCsrf,
132
229
  _stepCount: summary.stepCount,
133
- _model: summary.lastGeneratorModelUid,
230
+ _model: rawModel,
231
+ _rawModel: rawModel,
134
232
  });
135
233
  }
136
234
  }
@@ -144,13 +242,13 @@ function getSteps(chat) {
144
242
  // Prefer GetCascadeTrajectorySteps (returns more steps than GetCascadeTrajectory)
145
243
  const resp = callRpc(chat._port, chat._csrf, 'GetCascadeTrajectorySteps', {
146
244
  cascadeId: chat.composerId,
147
- }, chat._https);
245
+ }, chat._extCsrf);
148
246
  if (resp && resp.steps && resp.steps.length > 0) return resp.steps;
149
247
 
150
248
  // Fallback to old method
151
249
  const resp2 = callRpc(chat._port, chat._csrf, 'GetCascadeTrajectory', {
152
250
  cascadeId: chat.composerId,
153
- }, chat._https);
251
+ }, chat._extCsrf);
154
252
  if (resp2 && resp2.trajectory && resp2.trajectory.steps) return resp2.trajectory.steps;
155
253
 
156
254
  return [];
@@ -164,7 +262,7 @@ function getSteps(chat) {
164
262
  function getTailMessages(chat, stepMessages) {
165
263
  const resp = callRpc(chat._port, chat._csrf, 'GetCascadeTrajectory', {
166
264
  cascadeId: chat.composerId,
167
- }, chat._https);
265
+ }, chat._extCsrf);
168
266
  if (!resp || !resp.trajectory) return [];
169
267
 
170
268
  const gm = resp.trajectory.generatorMetadata || [];
@@ -256,10 +354,11 @@ function parseStep(step) {
256
354
  }
257
355
  }
258
356
  if (parts.length > 0) {
357
+ const model = meta.generatorModelUid;
259
358
  return {
260
359
  role: 'assistant',
261
360
  content: parts.join('\n'),
262
- _model: meta.generatorModelUid,
361
+ _model: model,
263
362
  _toolCalls,
264
363
  };
265
364
  }
@@ -345,8 +444,137 @@ function getMessages(chat) {
345
444
  return messages;
346
445
  }
347
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
+
348
576
  function resetCache() { _lsCache = null; }
349
577
 
350
- const labels = { 'windsurf': 'Windsurf', 'windsurf-next': 'Windsurf Next', 'antigravity': 'Antigravity' };
578
+ const labels = { 'windsurf': 'Windsurf', 'windsurf-next': 'Windsurf Next' };
351
579
 
352
- module.exports = { name, sources, labels, getChats, getMessages, resetCache };
580
+ module.exports = { name, sources, labels, getChats, getMessages, resetCache, getUsage };
package/editors/zed.js CHANGED
@@ -2,9 +2,26 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
4
  const { execSync } = require('child_process');
5
- const { getAppDataPath } = require('./base');
6
5
 
7
- const THREADS_DB = path.join(getAppDataPath('Zed'), 'threads', 'threads.db');
6
+ const Database = require('better-sqlite3');
7
+
8
+ // Zed stores data in different locations depending on the platform
9
+ // - Windows: %LOCALAPPDATA%\Zed (not Roaming)
10
+ // - macOS: ~/Library/Application Support/Zed
11
+ // - Linux: ~/.config/Zed
12
+ function getZedDataPath() {
13
+ const home = os.homedir();
14
+ switch (process.platform) {
15
+ case 'win32':
16
+ return path.join(home, 'AppData', 'Local', 'Zed');
17
+ case 'darwin':
18
+ return path.join(home, 'Library', 'Application Support', 'Zed');
19
+ default: // linux, etc.
20
+ return path.join(home, '.config', 'Zed');
21
+ }
22
+ }
23
+
24
+ const THREADS_DB = path.join(getZedDataPath(), 'threads', 'threads.db');
8
25
 
9
26
  // ============================================================
10
27
  // Decompress zstd blob via CLI
@@ -25,30 +42,32 @@ function decompressZstd(buf) {
25
42
  }
26
43
 
27
44
  // ============================================================
28
- // Query SQLite via CLI (avoids native module dependency)
45
+ // Query SQLite using better-sqlite3 (cross-platform)
29
46
  // ============================================================
30
47
 
31
48
  function queryDb(sql) {
32
49
  if (!fs.existsSync(THREADS_DB)) return [];
33
50
  try {
34
- const raw = execSync(
35
- `sqlite3 -json ${JSON.stringify(THREADS_DB)} ${JSON.stringify(sql)}`,
36
- { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }
37
- );
38
- return JSON.parse(raw);
39
- } catch { return []; }
51
+ const db = new Database(THREADS_DB, { readonly: true });
52
+ const rows = db.prepare(sql).all();
53
+ db.close();
54
+ return rows;
55
+ } catch (e) {
56
+ // Silently fail if database is locked or inaccessible
57
+ return [];
58
+ }
40
59
  }
41
60
 
42
- function queryBlobHex(id) {
61
+ function queryBlob(id) {
43
62
  if (!fs.existsSync(THREADS_DB)) return null;
44
63
  try {
45
- const hex = execSync(
46
- `sqlite3 ${JSON.stringify(THREADS_DB)} "SELECT hex(data) FROM threads WHERE id = '${id}'"`,
47
- { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }
48
- ).trim();
49
- if (!hex) return null;
50
- return Buffer.from(hex, 'hex');
51
- } catch { return null; }
64
+ const db = new Database(THREADS_DB, { readonly: true });
65
+ const row = db.prepare('SELECT data FROM threads WHERE id = ?').get(id);
66
+ db.close();
67
+ return row ? row.data : null;
68
+ } catch {
69
+ return null;
70
+ }
52
71
  }
53
72
 
54
73
  // ============================================================
@@ -71,13 +90,14 @@ function getChats() {
71
90
  mode: 'thread',
72
91
  folder: null,
73
92
  encrypted: false,
93
+ bubbleCount: 0,
74
94
  _dataType: row.data_type,
75
95
  _gitBranch: row.worktree_branch,
76
96
  }));
77
97
  }
78
98
 
79
99
  function getMessages(chat) {
80
- const blob = queryBlobHex(chat.composerId);
100
+ const blob = queryBlob(chat.composerId);
81
101
  if (!blob) return [];
82
102
 
83
103
  let json;
@@ -88,7 +108,10 @@ function getMessages(chat) {
88
108
  } else {
89
109
  json = blob.toString('utf-8');
90
110
  }
91
- } catch { return []; }
111
+ } catch (e) {
112
+ // Decompression failed - zstd CLI not available
113
+ return [];
114
+ }
92
115
 
93
116
  let data;
94
117
  try { data = JSON.parse(json); } catch { return []; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlytics",
3
- "version": "0.1.14",
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>
@@ -12,6 +12,7 @@ const PATHS = {
12
12
  antigravity: 'M21.751 22.607c1.34 1.005 3.35.335 1.508-1.508C17.73 15.74 18.904 1 12.037 1 5.17 1 6.342 15.74.815 21.1c-2.01 2.009.167 2.511 1.507 1.506 5.192-3.517 4.857-9.714 9.715-9.714 4.857 0 4.522 6.197 9.714 9.715z',
13
13
  command: 'M6,2A4,4 0 0,1 10,6V8H14V6A4,4 0 0,1 18,2A4,4 0 0,1 22,6A4,4 0 0,1 18,10H16V14H18A4,4 0 0,1 22,18A4,4 0 0,1 18,22A4,4 0 0,1 14,18V16H10V18A4,4 0 0,1 6,22A4,4 0 0,1 2,18A4,4 0 0,1 6,14H8V10H6A4,4 0 0,1 2,6A4,4 0 0,1 6,2M16,18A2,2 0 0,0 18,20A2,2 0 0,0 20,18A2,2 0 0,0 18,16H16V18M14,10H10V14H14V10M6,16A2,2 0 0,0 4,18A2,2 0 0,0 6,20A2,2 0 0,0 8,18V16H6M8,6A2,2 0 0,0 6,4A2,2 0 0,0 4,6A2,2 0 0,0 6,8H8V6M18,8A2,2 0 0,0 20,6A2,2 0 0,0 18,4A2,2 0 0,0 16,6V8H18Z',
14
14
  goose: 'M22.112 23.596C23.018 23.399 23.979 22.864 23.979 22.864L22.297 21.479C21.466 20.795 20.759 19.973 20.206 19.05C19.441 17.774 18.385 16.697 17.125 15.908L16.509 15.55C16.298 15.403 16.151 15.175 16.13 14.917C16.117 14.751 16.157 14.602 16.25 14.471C16.57 14.019 18.227 12.053 18.531 11.802C18.922 11.479 19.357 11.21 19.762 10.902L19.934 10.77C19.936 10.768 19.938 10.767 19.94 10.765C20.07 10.663 20.192 10.554 20.29 10.425C20.641 10.018 20.726 9.658 20.747 9.499C20.7 9.346 20.558 9.003 20.163 8.608C20.411 8.623 20.71 8.819 20.982 9.05C21.165 8.758 21.356 8.45 21.547 8.141C21.674 7.934 21.485 7.78 21.48 7.775L21.479 7.775C21.479 7.775 21.479 7.774 21.479 7.774C21.474 7.769 21.319 7.579 21.114 7.707C20.673 7.979 20.234 8.252 19.842 8.5C19.842 8.5 19.379 8.49 18.829 8.964C18.7 9.062 18.591 9.184 18.489 9.314L18.484 9.32C18.439 9.377 18.396 9.434 18.352 9.492C18.044 9.897 17.775 10.332 17.452 10.723C17.201 11.027 15.235 12.684 14.783 13.004C14.652 13.097 14.504 13.137 14.337 13.124C14.08 13.103 13.851 12.956 13.704 12.745L13.346 12.129C12.557 10.868 11.48 9.813 10.204 9.048C9.281 8.495 8.459 7.787 7.775 6.957L6.39 5.275C6.39 5.275 5.854 6.236 5.658 7.141C5.931 7.474 6.644 8.298 7.473 8.928C6.581 8.509 5.922 8.184 5.402 7.913C5.322 8.506 5.353 9.403 5.436 10.098C5.999 10.344 6.957 10.724 7.933 10.926C7.152 11.108 6.296 11.141 5.635 11.128C5.751 11.558 5.914 11.997 6.132 12.438C6.224 12.642 6.326 12.842 6.435 13.037C6.785 13.133 8.159 13.333 8.89 13.169C8.163 13.428 6.942 13.865 6.942 13.865C7.88 15.031 8.916 15.98 8.916 15.98C10.492 15.133 10.852 15.017 12.034 14.244C10.119 15.802 9.622 16.438 9.085 17.091L8.71 17.617C8.516 17.89 8.347 18.18 8.206 18.484C7.734 19.5 7.065 21.666 7.065 21.666C6.946 22.043 7.222 22.32 7.589 22.189C7.589 22.189 9.754 21.521 10.77 21.048C11.074 20.907 11.364 20.738 11.637 20.544L12.163 20.169C12.339 20.024 12.514 19.882 12.707 19.714C12.707 19.714 14.03 21.282 15.39 22.313C15.39 22.313 15.826 21.092 16.086 20.364C15.921 21.096 16.121 22.469 16.218 22.819C16.412 22.929 16.612 23.03 16.816 23.123C17.258 23.341 17.696 23.503 18.126 23.619C18.113 22.958 18.146 22.102 18.329 21.321C18.531 22.297 18.91 23.256 19.157 23.819C19.851 23.902 20.748 23.933 21.341 23.853C21.07 23.333 20.746 22.673 20.326 21.781C20.956 22.611 21.781 23.324 22.113 23.597L22.112 23.596Z',
15
+ kiro: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m-1.5 14.5v-9l7 4.5-7 4.5',
15
16
  // Terminal icon for generic editors
16
17
  terminal: 'M4 17.27V19h16v-1.73ZM4 5v1.73l7.07 4.55L4 15.82v1.73l10-6.46Z',
17
18
  }
@@ -34,6 +35,7 @@ const EDITOR_ICONS = {
34
35
  'codex': 'terminal',
35
36
  'commandcode': 'command',
36
37
  'goose': 'goose',
38
+ 'kiro': 'kiro',
37
39
  }
38
40
 
39
41
  export default function EditorIcon({ source, size = 16, className = '' }) {
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() {
@@ -15,6 +15,7 @@ export const EDITOR_COLORS = {
15
15
  'cursor-agent': '#f59e0b',
16
16
  'commandcode': '#e11d48',
17
17
  'goose': '#333333',
18
+ 'kiro': '#ff9900',
18
19
  };
19
20
 
20
21
  export const EDITOR_LABELS = {
@@ -34,6 +35,7 @@ export const EDITOR_LABELS = {
34
35
  'cursor-agent': 'Cursor Agent',
35
36
  'commandcode': 'Command Code',
36
37
  'goose': 'Goose',
38
+ 'kiro': 'Kiro',
37
39
  };
38
40
 
39
41
  export function editorColor(src) {