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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlytics",
3
- "version": "0.2.7",
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
@@ -4,7 +4,7 @@
4
4
  "version": "0.0.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "vite",
7
+ "dev": "node ../index.js --ui-dev & vite",
8
8
  "build": "vite build",
9
9
  "lint": "eslint .",
10
10
  "preview": "vite preview"
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 [live, setLive] = useState(false)
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
- // Live mode: refetch overview every 60s
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
- refreshOverview()
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, refreshOverview])
126
+ }, [live, rescanAndRefresh])
120
127
 
121
128
  const handleRefetch = async () => {
122
129
  setRefetchState({ scanned: 0, total: 0 })
123
130
  try {
124
- await refetchAgents((p) => setRefetchState({ scanned: p.scanned, total: p.total }))
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={() => setLive(!live)}
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 cacheHitRate = tk && tk.input > 0 ? ((tk.cacheRead / tk.input) * 100).toFixed(1) : 0
168
- const outputInputRatio = tk && tk.input > 0 ? (tk.output / tk.input).toFixed(2) : 0
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 cacheHitRate = data.totalInputTokens > 0 ? ((data.totalCacheRead / data.totalInputTokens) * 100).toFixed(1) : 0
235
- const outputRatio = data.totalInputTokens > 0 ? (data.totalOutputTokens / data.totalInputTokens).toFixed(2) : 0
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 - data.totalCacheRead, color: '#6366f1' },
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 - data.totalCacheRead)}</span>
311
- <span className="flex items-center gap-1"><span className="w-2 h-2 rounded-sm" style={{ background: '#34d399' }} /> cached {formatNumber(data.totalCacheRead)}</span>
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 */}