agentlytics 0.2.7 → 0.2.9
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 +60 -1
- package/cache.js +95 -170
- package/deno.json +9 -0
- package/editors/opencode.js +55 -28
- package/index.js +46 -8
- package/mod.ts +1020 -0
- package/package.json +7 -2
- package/relay-server.js +1 -1
- package/ui/package.json +1 -1
- package/ui/src/App.jsx +13 -8
- package/ui/src/hooks/useLive.jsx +15 -0
- package/ui/src/pages/Dashboard.jsx +3 -2
- package/ui/src/pages/DeepAnalysis.jsx +10 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlytics",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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": {
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"ui/package-lock.json",
|
|
24
24
|
"ui/vite.config.js",
|
|
25
25
|
"ui/eslint.config.js",
|
|
26
|
-
"README.md"
|
|
26
|
+
"README.md",
|
|
27
|
+
"mod.ts",
|
|
28
|
+
"deno.json"
|
|
27
29
|
],
|
|
28
30
|
"scripts": {
|
|
29
31
|
"start": "node index.js",
|
|
@@ -46,6 +48,9 @@
|
|
|
46
48
|
],
|
|
47
49
|
"author": "fkadev",
|
|
48
50
|
"license": "ISC",
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=20.19.0"
|
|
53
|
+
},
|
|
49
54
|
"repository": {
|
|
50
55
|
"type": "git",
|
|
51
56
|
"url": "https://github.com/f/agentlytics"
|
package/relay-server.js
CHANGED
|
@@ -542,7 +542,7 @@ function createRelayApp() {
|
|
|
542
542
|
if (fs.existsSync(index)) {
|
|
543
543
|
res.sendFile(index);
|
|
544
544
|
} else {
|
|
545
|
-
res.status(404).send('UI not built. Run: cd ui && npm install && npm run build');
|
|
545
|
+
res.status(404).send('UI not built. Run: cd ui && npm install && npm run build (or use your preferred package manager)');
|
|
546
546
|
}
|
|
547
547
|
});
|
|
548
548
|
|
package/ui/package.json
CHANGED
package/ui/src/App.jsx
CHANGED
|
@@ -3,6 +3,7 @@ import { Routes, Route, NavLink, useLocation } from 'react-router-dom'
|
|
|
3
3
|
import { Activity, BarChart3, GitCompare, MessageSquare, FolderOpen, DollarSign, CreditCard, Sun, Moon, RefreshCw, AlertTriangle, Github, Terminal, Database, Users, Plug, Copy, Check, Settings as SettingsIcon, Package, ChevronDown } from 'lucide-react'
|
|
4
4
|
import { fetchOverview, refetchAgents, fetchMode, fetchRelayConfig, getAuthToken, setOnAuthFailure } from './lib/api'
|
|
5
5
|
import { useTheme } from './lib/theme'
|
|
6
|
+
import { useLive } from './hooks/useLive'
|
|
6
7
|
import AnimatedLogo from './components/AnimatedLogo'
|
|
7
8
|
import AnimatedLoader from './components/AnimatedLoader'
|
|
8
9
|
import LoginScreen from './components/LoginScreen'
|
|
@@ -70,7 +71,7 @@ function NavDropdown({ icon: Icon, label, items }) {
|
|
|
70
71
|
export default function App() {
|
|
71
72
|
const [overview, setOverview] = useState(null)
|
|
72
73
|
const [refetchState, setRefetchState] = useState(null) // null | { scanned, total }
|
|
73
|
-
const
|
|
74
|
+
const { live, toggle: toggleLive } = useLive()
|
|
74
75
|
const [mode, setMode] = useState(null) // 'local' | 'relay'
|
|
75
76
|
const [needsAuth, setNeedsAuth] = useState(false)
|
|
76
77
|
const [authed, setAuthed] = useState(!!getAuthToken())
|
|
@@ -105,25 +106,29 @@ export default function App() {
|
|
|
105
106
|
if (mode === 'local') refreshOverview()
|
|
106
107
|
}, [mode])
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
const rescanAndRefresh = useCallback(async (onProgress) => {
|
|
110
|
+
await refetchAgents(onProgress)
|
|
111
|
+
const data = await fetchOverview()
|
|
112
|
+
setOverview(data)
|
|
113
|
+
}, [])
|
|
114
|
+
|
|
115
|
+
// Live mode: rescan & refresh every 60s
|
|
109
116
|
useEffect(() => {
|
|
110
117
|
if (live && mode === 'local') {
|
|
111
118
|
liveRef.current = setInterval(() => {
|
|
112
|
-
|
|
119
|
+
rescanAndRefresh().catch(() => {})
|
|
113
120
|
}, 60000)
|
|
114
121
|
} else {
|
|
115
122
|
if (liveRef.current) clearInterval(liveRef.current)
|
|
116
123
|
liveRef.current = null
|
|
117
124
|
}
|
|
118
125
|
return () => { if (liveRef.current) clearInterval(liveRef.current) }
|
|
119
|
-
}, [live,
|
|
126
|
+
}, [live, rescanAndRefresh])
|
|
120
127
|
|
|
121
128
|
const handleRefetch = async () => {
|
|
122
129
|
setRefetchState({ scanned: 0, total: 0 })
|
|
123
130
|
try {
|
|
124
|
-
await
|
|
125
|
-
const data = await fetchOverview()
|
|
126
|
-
setOverview(data)
|
|
131
|
+
await rescanAndRefresh((p) => setRefetchState({ scanned: p.scanned, total: p.total }))
|
|
127
132
|
} catch (e) { console.error(e) }
|
|
128
133
|
setRefetchState(null)
|
|
129
134
|
}
|
|
@@ -187,7 +192,7 @@ export default function App() {
|
|
|
187
192
|
{!isRelay && (
|
|
188
193
|
<>
|
|
189
194
|
<button
|
|
190
|
-
onClick={
|
|
195
|
+
onClick={toggleLive}
|
|
191
196
|
className="flex items-center gap-1.5 px-2 py-0.5 text-[11px] transition"
|
|
192
197
|
style={{
|
|
193
198
|
color: live ? '#22c55e' : 'var(--c-text3)',
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
const KEY = 'agentlytics-live'
|
|
4
|
+
|
|
5
|
+
export function useLive() {
|
|
6
|
+
const [live, setLive] = useState(() => localStorage.getItem(KEY) === 'true')
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
localStorage.setItem(KEY, live)
|
|
10
|
+
}, [live])
|
|
11
|
+
|
|
12
|
+
const toggle = useCallback(() => setLive(l => !l), [])
|
|
13
|
+
|
|
14
|
+
return { live, toggle }
|
|
15
|
+
}
|
|
@@ -164,8 +164,9 @@ export default function Dashboard({ overview }) {
|
|
|
164
164
|
} : null
|
|
165
165
|
|
|
166
166
|
const tk = stats?.tokens
|
|
167
|
-
const
|
|
168
|
-
const
|
|
167
|
+
const totalInputAll = tk ? tk.input + tk.cacheRead + (tk.cacheWrite || 0) : 0
|
|
168
|
+
const cacheHitRate = totalInputAll > 0 ? ((tk.cacheRead / totalInputAll) * 100).toFixed(1) : 0
|
|
169
|
+
const outputInputRatio = totalInputAll > 0 ? (tk.output / totalInputAll).toFixed(3) : 0
|
|
169
170
|
const avgMsgsPerSession = tk && tk.sessions > 0 ? (depthData ? (Object.values(stats.depthBuckets).reduce((s, v, i) => {
|
|
170
171
|
const labels = Object.keys(stats.depthBuckets)
|
|
171
172
|
const midpoints = [1, 3.5, 8, 15.5, 35.5, 75.5, 150]
|
|
@@ -227,12 +227,13 @@ export default function DeepAnalysis({ overview }) {
|
|
|
227
227
|
// Computed insights
|
|
228
228
|
const insights = useMemo(() => {
|
|
229
229
|
if (!data) return null
|
|
230
|
-
const totalTok = data.totalInputTokens + data.totalOutputTokens
|
|
230
|
+
const totalTok = data.totalInputTokens + data.totalOutputTokens + data.totalCacheRead + data.totalCacheWrite
|
|
231
231
|
const msgsPerSession = data.analyzedChats > 0 ? (data.totalMessages / data.analyzedChats).toFixed(1) : 0
|
|
232
232
|
const toolsPerSession = data.analyzedChats > 0 ? (data.totalToolCalls / data.analyzedChats).toFixed(1) : 0
|
|
233
233
|
const tokPerMsg = data.totalMessages > 0 ? Math.round(totalTok / data.totalMessages) : 0
|
|
234
|
-
const
|
|
235
|
-
const
|
|
234
|
+
const totalInputAll = data.totalInputTokens + data.totalCacheRead + data.totalCacheWrite
|
|
235
|
+
const cacheHitRate = totalInputAll > 0 ? ((data.totalCacheRead / totalInputAll) * 100).toFixed(1) : 0
|
|
236
|
+
const outputRatio = totalInputAll > 0 ? (data.totalOutputTokens / totalInputAll).toFixed(3) : 0
|
|
236
237
|
const aiVsHuman = data.totalUserChars > 0 ? (data.totalAssistantChars / data.totalUserChars).toFixed(1) : 0
|
|
237
238
|
return { totalTok, msgsPerSession, toolsPerSession, tokPerMsg, cacheHitRate, outputRatio, aiVsHuman }
|
|
238
239
|
}, [data])
|
|
@@ -300,15 +301,17 @@ export default function DeepAnalysis({ overview }) {
|
|
|
300
301
|
<div>
|
|
301
302
|
<div className="flex items-center justify-between text-[11px] mb-1">
|
|
302
303
|
<span style={{ color: 'var(--c-text2)' }}>input tokens</span>
|
|
303
|
-
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalInputTokens)}</span>
|
|
304
|
+
<span className="font-bold" style={{ color: 'var(--c-white)' }}>{formatNumber(data.totalInputTokens + data.totalCacheRead + data.totalCacheWrite)}</span>
|
|
304
305
|
</div>
|
|
305
306
|
<ProportionBar segments={[
|
|
306
|
-
{ label: 'Fresh input', value: data.totalInputTokens
|
|
307
|
+
{ label: 'Fresh input', value: data.totalInputTokens, color: '#6366f1' },
|
|
308
|
+
{ label: 'Cache write', value: data.totalCacheWrite, color: '#fbbf24' },
|
|
307
309
|
{ label: 'Cache read', value: data.totalCacheRead, color: '#34d399' },
|
|
308
310
|
]} />
|
|
309
311
|
<div className="flex items-center gap-3 mt-1 text-[10px]">
|
|
310
|
-
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> fresh {formatNumber(data.totalInputTokens
|
|
311
|
-
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#
|
|
312
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#6366f1' }} /> fresh {formatNumber(data.totalInputTokens)}</span>
|
|
313
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#fbbf24' }} /> cache write {formatNumber(data.totalCacheWrite)}</span>
|
|
314
|
+
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#34d399' }} /> cache read {formatNumber(data.totalCacheRead)}</span>
|
|
312
315
|
</div>
|
|
313
316
|
</div>
|
|
314
317
|
{/* Output tokens */}
|