agentlytics 0.2.6 → 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 +7 -4
- package/editors/antigravity.js +2 -0
- package/editors/base.js +12 -0
- package/editors/claude.js +3 -10
- package/editors/codex.js +2 -0
- package/editors/copilot.js +2 -0
- package/editors/cursor.js +2 -0
- package/editors/vscode.js +2 -0
- package/editors/windsurf.js +3 -0
- package/index.js +35 -24
- package/package.json +1 -1
- package/ui/src/pages/Settings.jsx +101 -1
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
|
|
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++;
|
package/editors/antigravity.js
CHANGED
|
@@ -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
|
@@ -204,18 +204,11 @@ function extractAssistantContent(content) {
|
|
|
204
204
|
// Usage / quota data from Anthropic OAuth API
|
|
205
205
|
// ============================================================
|
|
206
206
|
|
|
207
|
-
function isKeychainAccessAllowed() {
|
|
208
|
-
try {
|
|
209
|
-
const configPath = path.join(os.homedir(), '.agentlytics', 'config.json');
|
|
210
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
211
|
-
return config.allowKeychainAccess === true;
|
|
212
|
-
} catch { return false; }
|
|
213
|
-
}
|
|
214
|
-
|
|
215
207
|
function getClaudeCredentials() {
|
|
216
208
|
// macOS: Keychain; Linux: secret-tool; Windows: not yet supported
|
|
217
|
-
// Requires explicit user permission (
|
|
218
|
-
|
|
209
|
+
// Requires explicit user permission (allowSubscriptionAccess in config)
|
|
210
|
+
const { isSubscriptionAccessAllowed } = require('./base');
|
|
211
|
+
if (!isSubscriptionAccessAllowed()) return null;
|
|
219
212
|
try {
|
|
220
213
|
const { execSync } = require('child_process');
|
|
221
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
|
|
package/editors/copilot.js
CHANGED
|
@@ -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
|
|
package/editors/windsurf.js
CHANGED
|
@@ -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,37 +253,48 @@ const BOT_STYLES = [
|
|
|
253
253
|
];
|
|
254
254
|
|
|
255
255
|
(async () => {
|
|
256
|
-
// ── Ask for
|
|
256
|
+
// ── Ask for subscription access permission (first run only) ──
|
|
257
257
|
const CONFIG_DIR = path.join(os.homedir(), '.agentlytics');
|
|
258
258
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
259
259
|
let agentConfig = {};
|
|
260
260
|
try { agentConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')); } catch {}
|
|
261
261
|
|
|
262
|
-
if (agentConfig.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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());
|
|
276
287
|
});
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
console.log('');
|
|
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)'));
|
|
286
296
|
}
|
|
297
|
+
console.log('');
|
|
287
298
|
}
|
|
288
299
|
|
|
289
300
|
let tick = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlytics",
|
|
3
|
-
"version": "0.2.
|
|
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 – macOS Keychain / Linux secret-tool</li>
|
|
158
|
+
<li>Cursor – local SQLite (state.vscdb)</li>
|
|
159
|
+
<li>Copilot / VS Code – ~/.config/github-copilot/apps.json</li>
|
|
160
|
+
<li>Codex – local auth.json (JWT decode only)</li>
|
|
161
|
+
<li>Windsurf – 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
|
}
|