agentlytics 0.2.5 → 0.2.7

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/cache.js CHANGED
@@ -778,9 +778,10 @@ async function scanAllAsync(onProgress, opts = {}) {
778
778
  let analyzed = 0;
779
779
  let skipped = 0;
780
780
 
781
+ // Build existing cache map with both timestamp and bubble count
781
782
  const existing = {};
782
- for (const row of db.prepare('SELECT id, last_updated_at FROM chats').all()) {
783
- existing[row.id] = row.last_updated_at;
783
+ for (const row of db.prepare('SELECT id, last_updated_at, bubble_count FROM chats').all()) {
784
+ existing[row.id] = { ts: row.last_updated_at, bc: row.bubble_count };
784
785
  }
785
786
 
786
787
  // Normalize folder paths
@@ -803,10 +804,12 @@ async function scanAllAsync(onProgress, opts = {}) {
803
804
 
804
805
  for (const chat of chats) {
805
806
  scanned++;
806
- const cachedTs = existing[chat.composerId];
807
807
  const chatTs = chat.lastUpdatedAt || chat.createdAt || 0;
808
+ const chatBc = chat.bubbleCount || 0;
808
809
 
809
- if (cachedTs && cachedTs >= chatTs) {
810
+ // Skip if already cached, not updated, and bubble count hasn't grown
811
+ const cached = existing[chat.composerId];
812
+ if (cached && cached.ts && cached.ts >= chatTs && cached.bc >= chatBc) {
810
813
  const hasStat = db.prepare('SELECT 1 FROM chat_stats WHERE chat_id = ?').get(chat.composerId);
811
814
  if (hasStat) {
812
815
  skipped++;
@@ -855,6 +855,8 @@ function getMessages(chat) {
855
855
  // ============================================================
856
856
 
857
857
  function getUsage() {
858
+ const { isSubscriptionAccessAllowed } = require('./base');
859
+ if (!isSubscriptionAccessAllowed()) return null;
858
860
  const resp = callRpc('GetUserStatus', {});
859
861
  if (!resp || !resp.userStatus) return null;
860
862
 
package/editors/base.js CHANGED
@@ -1,8 +1,19 @@
1
1
  const path = require('path');
2
2
  const os = require('os');
3
+ const fs = require('fs');
3
4
 
4
5
  const HOME = os.homedir();
5
6
 
7
+ // --- Permission check ---
8
+
9
+ function isSubscriptionAccessAllowed() {
10
+ try {
11
+ const configPath = path.join(HOME, '.agentlytics', 'config.json');
12
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
13
+ return config.allowSubscriptionAccess === true;
14
+ } catch { return false; }
15
+ }
16
+
6
17
  // --- Platform utilities ---
7
18
 
8
19
  /**
@@ -303,6 +314,7 @@ function queryMcpServerToolsStdio(server, timeout) {
303
314
 
304
315
  module.exports = {
305
316
  getAppDataPath,
317
+ isSubscriptionAccessAllowed,
306
318
  scanArtifacts,
307
319
  parseMcpConfigFile,
308
320
  queryMcpServerTools,
package/editors/claude.js CHANGED
@@ -206,6 +206,9 @@ function extractAssistantContent(content) {
206
206
 
207
207
  function getClaudeCredentials() {
208
208
  // macOS: Keychain; Linux: secret-tool; Windows: not yet supported
209
+ // Requires explicit user permission (allowSubscriptionAccess in config)
210
+ const { isSubscriptionAccessAllowed } = require('./base');
211
+ if (!isSubscriptionAccessAllowed()) return null;
209
212
  try {
210
213
  const { execSync } = require('child_process');
211
214
  let raw;
package/editors/codex.js CHANGED
@@ -477,6 +477,8 @@ function decodeJwtPayload(token) {
477
477
  }
478
478
 
479
479
  async function getUsage() {
480
+ const { isSubscriptionAccessAllowed } = require('./base');
481
+ if (!isSubscriptionAccessAllowed()) return null;
480
482
  const auth = getCodexAuth();
481
483
  if (!auth || !auth.tokens) return null;
482
484
 
@@ -211,6 +211,8 @@ function fetchCopilotStatus(token) {
211
211
  }
212
212
 
213
213
  async function getUsage() {
214
+ const { isSubscriptionAccessAllowed } = require('./base');
215
+ if (!isSubscriptionAccessAllowed()) return null;
214
216
  const creds = getCopilotToken();
215
217
  if (!creds) return null;
216
218
 
package/editors/cursor.js CHANGED
@@ -374,6 +374,8 @@ function cursorApiFetch(endpoint, token) {
374
374
  }
375
375
 
376
376
  async function getUsage() {
377
+ const { isSubscriptionAccessAllowed } = require('./base');
378
+ if (!isSubscriptionAccessAllowed()) return null;
377
379
  const token = getCursorAccessToken();
378
380
  if (!token) return null;
379
381
 
package/editors/vscode.js CHANGED
@@ -355,6 +355,8 @@ function fetchCopilotStatus(token) {
355
355
  }
356
356
 
357
357
  async function getUsage() {
358
+ const { isSubscriptionAccessAllowed } = require('./base');
359
+ if (!isSubscriptionAccessAllowed()) return null;
358
360
  const creds = getCopilotToken();
359
361
  if (!creds) return null;
360
362
 
@@ -476,6 +476,9 @@ function getWindsurfApiKey(appName) {
476
476
  }
477
477
 
478
478
  function getUsage() {
479
+ const { isSubscriptionAccessAllowed } = require('./base');
480
+ if (!isSubscriptionAccessAllowed()) return [];
481
+
479
482
  const results = [];
480
483
 
481
484
  for (const variant of VARIANTS) {
package/index.js CHANGED
@@ -253,6 +253,50 @@ const BOT_STYLES = [
253
253
  ];
254
254
 
255
255
  (async () => {
256
+ // ── Ask for subscription access permission (first run only) ──
257
+ const CONFIG_DIR = path.join(os.homedir(), '.agentlytics');
258
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
259
+ let agentConfig = {};
260
+ try { agentConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')); } catch {}
261
+
262
+ if (agentConfig.allowSubscriptionAccess === undefined) {
263
+ console.log(chalk.yellow(' ⚠ Subscription & usage details require access to local auth tokens.'));
264
+ console.log('');
265
+ console.log(chalk.dim(' To show your plan and usage info, Agentlytics needs to read'));
266
+ console.log(chalk.dim(' locally stored tokens from the following sources:'));
267
+ console.log('');
268
+ console.log(chalk.dim(' • Claude Code – macOS Keychain / Linux secret-tool'));
269
+ console.log(chalk.dim(' • Cursor – local SQLite (state.vscdb)'));
270
+ console.log(chalk.dim(' • Copilot – ~/.config/github-copilot/apps.json'));
271
+ console.log(chalk.dim(' • VS Code – ~/.config/github-copilot/apps.json'));
272
+ console.log(chalk.dim(' • Codex – local auth.json (JWT decode only)'));
273
+ console.log(chalk.dim(' • Windsurf – local SQLite (state.vscdb)'));
274
+ console.log('');
275
+ console.log(chalk.dim(' These tokens are used to query each editor\'s own API for'));
276
+ console.log(chalk.dim(' your plan name and usage limits.'));
277
+ console.log('');
278
+ console.log(chalk.bold.white(' → Tokens are kept in-memory only and never sent to any'));
279
+ console.log(chalk.bold.white(' third-party service. They are discarded after the request.'));
280
+ console.log('');
281
+ const readline = require('readline');
282
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
283
+ const answer = await new Promise(r => {
284
+ rl.question(chalk.bold(' Allow local token inspection for subscription details? (y/N) '), (a) => {
285
+ rl.close();
286
+ r(a.trim().toLowerCase());
287
+ });
288
+ });
289
+ agentConfig.allowSubscriptionAccess = answer === 'y' || answer === 'yes';
290
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
291
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(agentConfig, null, 2));
292
+ if (agentConfig.allowSubscriptionAccess) {
293
+ console.log(chalk.green(' ✓ Subscription access enabled'));
294
+ } else {
295
+ console.log(chalk.dim(' – Subscription access skipped (plan/usage details won\'t be collected)'));
296
+ }
297
+ console.log('');
298
+ }
299
+
256
300
  let tick = 0;
257
301
  const startTime = Date.now();
258
302
  const result = await cache.scanAllAsync((p) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlytics",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
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": {
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect } from 'react'
2
- import { Settings as SettingsIcon, EyeOff, Eye, FolderOpen, Search } from 'lucide-react'
2
+ import { Settings as SettingsIcon, EyeOff, Eye, FolderOpen, Search, ShieldCheck, ShieldOff, AlertTriangle, X } from 'lucide-react'
3
3
  import { fetchConfig, updateConfig, fetchAllProjects } from '../lib/api'
4
4
  import { editorLabel, formatNumber, formatDate } from '../lib/constants'
5
5
  import EditorIcon from '../components/EditorIcon'
@@ -13,6 +13,7 @@ export default function Settings() {
13
13
  const [loading, setLoading] = useState(true)
14
14
  const [saving, setSaving] = useState(false)
15
15
  const [search, setSearch] = useState('')
16
+ const [showConfirm, setShowConfirm] = useState(false)
16
17
 
17
18
  useEffect(() => {
18
19
  Promise.all([fetchConfig(), fetchAllProjects()]).then(([cfg, projs]) => {
@@ -47,10 +48,45 @@ export default function Settings() {
47
48
 
48
49
  const sorted = [...filtered].sort((a, b) => a.name.localeCompare(b.name))
49
50
 
51
+ const subscriptionAccess = !!config.allowSubscriptionAccess
52
+
53
+ const toggleSubscriptionAccess = async () => {
54
+ setSaving(true)
55
+ const newConfig = await updateConfig({ allowSubscriptionAccess: !subscriptionAccess })
56
+ setConfig(newConfig)
57
+ setSaving(false)
58
+ setShowConfirm(false)
59
+ }
60
+
50
61
  return (
51
62
  <div className="fade-in space-y-3">
52
63
  <PageHeader icon={SettingsIcon} title="Settings" />
53
64
 
65
+ <div className="card overflow-hidden">
66
+ <div className="px-3 py-2 flex items-center justify-between" style={{ borderBottom: '1px solid var(--c-border)' }}>
67
+ <SectionTitle>
68
+ {subscriptionAccess ? <ShieldCheck size={11} className="inline mr-1" /> : <ShieldOff size={11} className="inline mr-1" />}
69
+ subscription access
70
+ </SectionTitle>
71
+ <button
72
+ onClick={() => setShowConfirm(true)}
73
+ disabled={saving}
74
+ className="text-[11px] px-2 py-0.5 rounded transition"
75
+ style={{
76
+ background: subscriptionAccess ? 'rgba(34,197,94,0.08)' : 'rgba(239,68,68,0.08)',
77
+ color: subscriptionAccess ? '#22c55e' : '#ef4444',
78
+ border: `1px solid ${subscriptionAccess ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)'}`,
79
+ }}
80
+ >
81
+ {subscriptionAccess ? 'Enabled' : 'Disabled'}
82
+ </button>
83
+ </div>
84
+ <div className="text-[11px] px-3 py-2" style={{ color: 'var(--c-text3)' }}>
85
+ When enabled, Agentlytics reads locally stored auth tokens (Keychain, SQLite, config files) to show your plan and usage info for each editor.
86
+ Tokens are kept in-memory only and <span style={{ color: 'var(--c-text2)' }}>never sent to any third-party service</span>.
87
+ </div>
88
+ </div>
89
+
54
90
  <div className="card overflow-hidden">
55
91
  <div className="px-3 py-2 flex items-center justify-between" style={{ borderBottom: '1px solid var(--c-border)' }}>
56
92
  <SectionTitle>
@@ -88,6 +124,70 @@ export default function Settings() {
88
124
  <div className="text-center py-6 text-[12px]" style={{ color: 'var(--c-text3)' }}>no projects match filter</div>
89
125
  )}
90
126
  </div>
127
+ {showConfirm && (
128
+ <ConfirmModal
129
+ enabling={!subscriptionAccess}
130
+ saving={saving}
131
+ onConfirm={toggleSubscriptionAccess}
132
+ onCancel={() => setShowConfirm(false)}
133
+ />
134
+ )}
135
+ </div>
136
+ )
137
+ }
138
+
139
+ function ConfirmModal({ enabling, saving, onConfirm, onCancel }) {
140
+ return (
141
+ <div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.6)' }} onClick={onCancel}>
142
+ <div className="card w-[420px] mx-4" onClick={e => e.stopPropagation()} style={{ background: 'var(--c-bg2)', border: '1px solid var(--c-border)' }}>
143
+ <div className="flex items-center justify-between px-4 py-3" style={{ borderBottom: '1px solid var(--c-border)' }}>
144
+ <div className="flex items-center gap-2 text-[13px] font-medium" style={{ color: 'var(--c-white)' }}>
145
+ <AlertTriangle size={14} style={{ color: enabling ? '#fbbf24' : '#ef4444' }} />
146
+ {enabling ? 'Enable' : 'Disable'} Subscription Access
147
+ </div>
148
+ <button onClick={onCancel} className="p-1 rounded hover:bg-[var(--c-bg3)]" style={{ color: 'var(--c-text3)' }}>
149
+ <X size={14} />
150
+ </button>
151
+ </div>
152
+ <div className="px-4 py-3 text-[11px] space-y-2" style={{ color: 'var(--c-text2)' }}>
153
+ {enabling ? (
154
+ <>
155
+ <p>This will allow Agentlytics to read locally stored auth tokens from:</p>
156
+ <ul className="space-y-1 pl-3" style={{ color: 'var(--c-text3)' }}>
157
+ <li>Claude Code &ndash; macOS Keychain / Linux secret-tool</li>
158
+ <li>Cursor &ndash; local SQLite (state.vscdb)</li>
159
+ <li>Copilot / VS Code &ndash; ~/.config/github-copilot/apps.json</li>
160
+ <li>Codex &ndash; local auth.json (JWT decode only)</li>
161
+ <li>Windsurf &ndash; local SQLite (state.vscdb)</li>
162
+ </ul>
163
+ <p style={{ color: 'var(--c-text2)' }}>Tokens are kept <strong>in-memory only</strong> and never sent to any third-party service.</p>
164
+ </>
165
+ ) : (
166
+ <p>This will stop Agentlytics from reading any local auth tokens. Subscription and plan details will no longer be collected.</p>
167
+ )}
168
+ </div>
169
+ <div className="flex justify-end gap-2 px-4 py-3" style={{ borderTop: '1px solid var(--c-border)' }}>
170
+ <button
171
+ onClick={onCancel}
172
+ className="text-[11px] px-3 py-1 rounded transition"
173
+ style={{ color: 'var(--c-text3)', border: '1px solid var(--c-border)' }}
174
+ >
175
+ Cancel
176
+ </button>
177
+ <button
178
+ onClick={onConfirm}
179
+ disabled={saving}
180
+ className="text-[11px] px-3 py-1 rounded transition font-medium"
181
+ style={{
182
+ background: enabling ? 'rgba(34,197,94,0.12)' : 'rgba(239,68,68,0.12)',
183
+ color: enabling ? '#22c55e' : '#ef4444',
184
+ border: `1px solid ${enabling ? 'rgba(34,197,94,0.2)' : 'rgba(239,68,68,0.2)'}`,
185
+ }}
186
+ >
187
+ {saving ? 'Saving...' : enabling ? 'Yes, enable' : 'Yes, disable'}
188
+ </button>
189
+ </div>
190
+ </div>
91
191
  </div>
92
192
  )
93
193
  }