agentlytics 0.1.17 → 0.1.19

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/editors/codex.js CHANGED
@@ -376,9 +376,11 @@ function subtractRawUsage(current, previous) {
376
376
  }
377
377
 
378
378
  function convertToDelta(raw) {
379
+ const cacheRead = Math.min(raw.cached_input_tokens, raw.input_tokens);
380
+ const billableInput = Math.max(raw.input_tokens - cacheRead, 0);
379
381
  return {
380
- inputTokens: raw.input_tokens,
381
- cacheRead: Math.min(raw.cached_input_tokens, raw.input_tokens),
382
+ inputTokens: billableInput,
383
+ cacheRead,
382
384
  outputTokens: raw.output_tokens,
383
385
  totalTokens: raw.total_tokens > 0 ? raw.total_tokens : raw.input_tokens + raw.output_tokens,
384
386
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlytics",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
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/pricing.json CHANGED
@@ -6,7 +6,9 @@
6
6
  "OpenAI": "developers.openai.com/api/docs/pricing",
7
7
  "Google": "ai.google.dev/gemini-api/docs/pricing",
8
8
  "xAI": "docs.x.ai/developers/models",
9
- "DeepSeek": "api-docs.deepseek.com/quick_start/pricing"
9
+ "DeepSeek": "api-docs.deepseek.com/quick_start/pricing",
10
+ "Z.ai": "docs.z.ai/guides/overview/pricing",
11
+ "Zen": "opencode.ai/docs/zen/"
10
12
  },
11
13
  "lastVerified": "2026-03",
12
14
  "notes": {
@@ -818,5 +820,125 @@
818
820
  "output": 25,
819
821
  "cacheRead": 1.25,
820
822
  "cacheWrite": 0
823
+ },
824
+ "glm-5": {
825
+ "input": 1,
826
+ "output": 3.2,
827
+ "cacheRead": 0.2,
828
+ "cacheWrite": 0
829
+ },
830
+ "glm-5-code": {
831
+ "input": 1.2,
832
+ "output": 5,
833
+ "cacheRead": 0.3,
834
+ "cacheWrite": 0
835
+ },
836
+ "glm-4.7": {
837
+ "input": 0.6,
838
+ "output": 2.2,
839
+ "cacheRead": 0.11,
840
+ "cacheWrite": 0
841
+ },
842
+ "glm-4.7-flashx": {
843
+ "input": 0.07,
844
+ "output": 0.4,
845
+ "cacheRead": 0.01,
846
+ "cacheWrite": 0
847
+ },
848
+ "glm-4.6": {
849
+ "input": 0.6,
850
+ "output": 2.2,
851
+ "cacheRead": 0.11,
852
+ "cacheWrite": 0
853
+ },
854
+ "glm-4.5": {
855
+ "input": 0.6,
856
+ "output": 2.2,
857
+ "cacheRead": 0.11,
858
+ "cacheWrite": 0
859
+ },
860
+ "glm-4.5-x": {
861
+ "input": 2.2,
862
+ "output": 8.9,
863
+ "cacheRead": 0.45,
864
+ "cacheWrite": 0
865
+ },
866
+ "glm-4.5-air": {
867
+ "input": 0.2,
868
+ "output": 1.1,
869
+ "cacheRead": 0.03,
870
+ "cacheWrite": 0
871
+ },
872
+ "glm-4.5-airx": {
873
+ "input": 1.1,
874
+ "output": 4.5,
875
+ "cacheRead": 0.22,
876
+ "cacheWrite": 0
877
+ },
878
+ "glm-4-32b-0414-128k": {
879
+ "input": 0.1,
880
+ "output": 0.1,
881
+ "cacheRead": 0,
882
+ "cacheWrite": 0
883
+ },
884
+ "glm-4.7-flash": {
885
+ "input": 0,
886
+ "output": 0,
887
+ "cacheRead": 0,
888
+ "cacheWrite": 0
889
+ },
890
+ "glm-4.5-flash": {
891
+ "input": 0,
892
+ "output": 0,
893
+ "cacheRead": 0,
894
+ "cacheWrite": 0
895
+ },
896
+ "big-pickle": {
897
+ "input": 0,
898
+ "output": 0,
899
+ "cacheRead": 0,
900
+ "cacheWrite": 0
901
+ },
902
+ "minimax-m2.5-free": {
903
+ "input": 0,
904
+ "output": 0,
905
+ "cacheRead": 0,
906
+ "cacheWrite": 0
907
+ },
908
+ "minimax-m2.5": {
909
+ "input": 0.3,
910
+ "output": 1.2,
911
+ "cacheRead": 0.06,
912
+ "cacheWrite": 0.375
913
+ },
914
+ "minimax-m2.1": {
915
+ "input": 0.3,
916
+ "output": 1.2,
917
+ "cacheRead": 0.1,
918
+ "cacheWrite": 0
919
+ },
920
+ "kimi-k2.5": {
921
+ "input": 0.6,
922
+ "output": 3,
923
+ "cacheRead": 0.1,
924
+ "cacheWrite": 0
925
+ },
926
+ "kimi-k2-thinking": {
927
+ "input": 0.4,
928
+ "output": 2.5,
929
+ "cacheRead": 0,
930
+ "cacheWrite": 0
931
+ },
932
+ "kimi-k2": {
933
+ "input": 0.4,
934
+ "output": 2.5,
935
+ "cacheRead": 0,
936
+ "cacheWrite": 0
937
+ },
938
+ "qwen3-coder-480b": {
939
+ "input": 0.45,
940
+ "output": 1.5,
941
+ "cacheRead": 0,
942
+ "cacheWrite": 0
821
943
  }
822
944
  }
package/server.js CHANGED
@@ -333,6 +333,27 @@ app.put('/api/config', (req, res) => {
333
333
  }
334
334
  });
335
335
 
336
+ app.get('/api/check-ai', async (req, res) => {
337
+ const folder = req.query.folder;
338
+ if (!folder) return res.status(400).json({ error: 'folder query param required' });
339
+ try {
340
+ const { execFile } = require('child_process');
341
+ const result = await new Promise((resolve, reject) => {
342
+ execFile('npx', ['-y', 'check-ai', '--json', folder], { timeout: 60000, maxBuffer: 1024 * 1024 }, (err, stdout) => {
343
+ try {
344
+ const json = JSON.parse(stdout);
345
+ resolve(json);
346
+ } catch (e) {
347
+ reject(new Error(err ? err.message : 'Failed to parse check-ai output'));
348
+ }
349
+ });
350
+ });
351
+ res.json(result);
352
+ } catch (err) {
353
+ res.status(500).json({ error: err.message });
354
+ }
355
+ });
356
+
336
357
  app.get('/api/all-projects', (req, res) => {
337
358
  try {
338
359
  res.json(cache.getCachedProjects({ ...parseDateOpts(req.query), includeHidden: true }));
@@ -0,0 +1,255 @@
1
+ import { useState, useEffect, useRef } from 'react'
2
+ import { ShieldCheck, Loader2, CheckCircle2, AlertTriangle, RefreshCw, Info, ChevronRight, ChevronDown, Brush, FileText, FlaskConical, Bot, Lock, Puzzle, Plug, Package } from 'lucide-react'
3
+ import { fetchCheckAi } from '../lib/api'
4
+ import SectionTitle from './SectionTitle'
5
+
6
+ const GRADE_COLORS = {
7
+ 'A+': '#22c55e', A: '#22c55e',
8
+ 'B+': '#4ade80', B: '#4ade80',
9
+ 'C+': '#facc15', C: '#facc15',
10
+ 'D+': '#f97316', D: '#f97316',
11
+ F: '#ef4444',
12
+ }
13
+
14
+ const SECTION_ICONS = {
15
+ 'Repo Hygiene': Brush,
16
+ 'Grounding Docs': FileText,
17
+ 'Testing': FlaskConical,
18
+ 'Agent Configs': Bot,
19
+ 'AI Context': Lock,
20
+ 'Prompts & Skills': Puzzle,
21
+ 'MCP': Plug,
22
+ 'AI Dependencies': Package,
23
+ 'AI Deps': Package,
24
+ }
25
+
26
+ function Tip({ missing }) {
27
+ const [open, setOpen] = useState(false)
28
+ const ref = useRef(null)
29
+
30
+ useEffect(() => {
31
+ if (!open) return
32
+ const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }
33
+ document.addEventListener('mousedown', handler)
34
+ return () => document.removeEventListener('mousedown', handler)
35
+ }, [open])
36
+
37
+ const weighted = missing.filter(f => f.weight > 0)
38
+ const items = weighted.length > 0 ? weighted : missing.slice(0, 5)
39
+ if (items.length === 0) return null
40
+
41
+ return (
42
+ <span ref={ref} className="relative flex-shrink-0">
43
+ <button
44
+ onClick={(e) => { e.stopPropagation(); setOpen(!open) }}
45
+ className="flex items-center justify-center w-4 h-4 rounded-full transition hover:opacity-80"
46
+ style={{ color: 'var(--c-text3)', background: 'var(--c-bg3)' }}
47
+ >
48
+ <Info size={10} />
49
+ </button>
50
+ {open && (
51
+ <div
52
+ className="absolute right-0 top-6 z-50 w-64 p-2.5 rounded shadow-lg"
53
+ style={{ background: 'var(--c-bg)', border: '1px solid var(--c-border)', boxShadow: '0 4px 24px rgba(0,0,0,0.3)' }}
54
+ >
55
+ <div className="text-[11px] font-medium mb-1.5" style={{ color: 'var(--c-white)' }}>How to improve</div>
56
+ <div className="space-y-1">
57
+ {items.map(f => (
58
+ <div key={f.id} className="text-[11px]" style={{ color: 'var(--c-text2)' }}>
59
+ <span style={{ color: 'var(--c-text)' }}>+ {f.label}</span>
60
+ {f.weight > 0 && <span style={{ color: 'var(--c-text3)' }}> ({f.weight}pt)</span>}
61
+ </div>
62
+ ))}
63
+ </div>
64
+ </div>
65
+ )}
66
+ </span>
67
+ )
68
+ }
69
+
70
+ export default function AiAuditCard({ folder }) {
71
+ const [audit, setAudit] = useState(null)
72
+ const [loading, setLoading] = useState(false)
73
+ const [error, setError] = useState(null)
74
+ const [expanded, setExpanded] = useState(new Set())
75
+ const ran = useRef(false)
76
+
77
+ const runAudit = async () => {
78
+ setLoading(true)
79
+ setError(null)
80
+ try {
81
+ const result = await fetchCheckAi(folder)
82
+ if (result.error) throw new Error(result.error)
83
+ setAudit(result)
84
+ } catch (e) {
85
+ setError(e.message)
86
+ }
87
+ setLoading(false)
88
+ }
89
+
90
+ useEffect(() => {
91
+ if (folder && !ran.current) {
92
+ ran.current = true
93
+ runAudit()
94
+ }
95
+ }, [folder])
96
+
97
+ if (loading) {
98
+ return (
99
+ <div className="card p-4">
100
+ <div className="flex items-center gap-2">
101
+ <Loader2 size={14} className="animate-spin" style={{ color: 'var(--c-accent)' }} />
102
+ <span className="text-[12px]" style={{ color: 'var(--c-text2)' }}>Running AI readiness audit...</span>
103
+ </div>
104
+ </div>
105
+ )
106
+ }
107
+
108
+ if (error) {
109
+ return (
110
+ <div className="card p-4">
111
+ <div className="flex items-center justify-between">
112
+ <div className="flex items-center gap-2">
113
+ <AlertTriangle size={14} style={{ color: '#ef4444' }} />
114
+ <span className="text-[12px]" style={{ color: 'var(--c-text2)' }}>Audit failed: {error}</span>
115
+ </div>
116
+ <button
117
+ onClick={runAudit}
118
+ className="flex items-center gap-1.5 px-2.5 py-1 text-[11px] rounded transition hover:opacity-80"
119
+ style={{ color: 'var(--c-text2)', border: '1px solid var(--c-border)' }}
120
+ >
121
+ <RefreshCw size={10} />
122
+ Retry
123
+ </button>
124
+ </div>
125
+ </div>
126
+ )
127
+ }
128
+
129
+ if (!audit) return null
130
+
131
+ const gradeColor = GRADE_COLORS[audit.grade] || 'var(--c-text2)'
132
+ const sections = audit.sections || {}
133
+ const findings = audit.findings || []
134
+
135
+ const findingsBySection = {}
136
+ for (const f of findings) {
137
+ const sec = f.section || 'Other'
138
+ if (!findingsBySection[sec]) findingsBySection[sec] = []
139
+ findingsBySection[sec].push(f)
140
+ }
141
+
142
+ return (
143
+ <div className="card p-4 space-y-3">
144
+ {/* Header row */}
145
+ <div className="flex items-center gap-3">
146
+ <ShieldCheck size={16} style={{ color: gradeColor }} />
147
+ <div>
148
+ <SectionTitle>ai readiness audit</SectionTitle>
149
+ <div className="text-[10px] -mt-1.5" style={{ color: 'var(--c-text3)' }}>powered by <code style={{ fontFamily: 'JetBrains Mono, monospace' }}>npx check-ai</code></div>
150
+ </div>
151
+ <div className="flex items-center gap-2 ml-auto">
152
+ <span className="text-[11px]" style={{ color: 'var(--c-text3)' }}>
153
+ {audit.checks?.passed || 0}/{audit.checks?.total || 0} checks
154
+ &middot; {audit.points?.earned || 0}/{audit.points?.max || 0} pts
155
+ </span>
156
+ <button
157
+ onClick={runAudit}
158
+ className="flex items-center gap-1 px-2 py-1 text-[11px] rounded transition hover:opacity-80"
159
+ style={{ color: 'var(--c-text2)', border: '1px solid var(--c-border)' }}
160
+ >
161
+ <RefreshCw size={10} />
162
+ </button>
163
+ </div>
164
+ </div>
165
+
166
+ {/* Score row */}
167
+ <div className="flex items-center gap-3">
168
+ <div
169
+ className="w-11 h-11 rounded-lg flex items-center justify-center text-base font-black flex-shrink-0"
170
+ style={{ background: `${gradeColor}15`, color: gradeColor, border: `1.5px solid ${gradeColor}30` }}
171
+ >
172
+ {audit.grade}
173
+ </div>
174
+ <div className="flex-1 min-w-0">
175
+ <div className="flex items-baseline gap-1.5">
176
+ <span className="text-base font-bold" style={{ color: 'var(--c-white)' }}>{audit.score}</span>
177
+ <span className="text-[11px]" style={{ color: 'var(--c-text3)' }}>/10</span>
178
+ <span className="text-[11px] ml-1" style={{ color: 'var(--c-text2)' }}>{audit.label}</span>
179
+ </div>
180
+ <div className="w-full h-2 rounded-full overflow-hidden mt-1" style={{ background: 'var(--c-code-bg)' }}>
181
+ <div className="h-full rounded-full" style={{ width: `${((audit.score || 0) / 10 * 100).toFixed(1)}%`, background: gradeColor }} />
182
+ </div>
183
+ </div>
184
+ </div>
185
+
186
+ {/* Section rows */}
187
+ <div>
188
+ {Object.entries(sections).map(([name, sec]) => {
189
+ const Icon = SECTION_ICONS[name] || FileText
190
+ const sectionFindings = findingsBySection[name] || []
191
+ const passed = sectionFindings.filter(f => f.found)
192
+ const missing = sectionFindings.filter(f => !f.found)
193
+ const pct = sec.pct || 0
194
+ const barColor = pct >= 70 ? '#22c55e' : pct >= 40 ? '#facc15' : pct > 0 ? '#f97316' : 'var(--c-text3)'
195
+
196
+ // Build description from found findings
197
+ const details = []
198
+ for (const f of passed) {
199
+ if (f.detail) details.push(f.detail)
200
+ else if (f.matchedPath) details.push(f.matchedPath)
201
+ else if (f.matches && f.matches.length > 0) details.push(f.matches.join(', '))
202
+ else details.push(f.label)
203
+ }
204
+ const desc = details.length > 0
205
+ ? details.slice(0, 3).join(' · ') + (details.length > 3 ? ' …' : '')
206
+ : 'none detected'
207
+
208
+ const isOpen = expanded.has(name)
209
+ const toggle = () => setExpanded(prev => {
210
+ const next = new Set(prev)
211
+ if (next.has(name)) next.delete(name)
212
+ else next.add(name)
213
+ return next
214
+ })
215
+
216
+ return (
217
+ <div key={name} style={{ borderBottom: '1px solid var(--c-border)' }}>
218
+ {/* Main row — clickable */}
219
+ <div className="flex items-center gap-2.5 py-2 cursor-pointer transition hover:bg-[var(--c-bg3)]" onClick={toggle}>
220
+ {isOpen
221
+ ? <ChevronDown size={12} style={{ color: 'var(--c-text3)', flexShrink: 0 }} />
222
+ : <ChevronRight size={12} style={{ color: 'var(--c-text3)', flexShrink: 0 }} />
223
+ }
224
+ <Icon size={14} style={{ color: 'var(--c-text3)', flexShrink: 0 }} />
225
+ <span className="text-[12px] font-medium w-32 flex-shrink-0" style={{ color: 'var(--c-white)' }}>{name}</span>
226
+ <span className="text-[11px] flex-1 truncate" style={{ color: passed.length > 0 ? 'var(--c-text2)' : 'var(--c-text3)' }}>
227
+ {desc}
228
+ </span>
229
+ <span className="text-[11px] flex-shrink-0" style={{ color: 'var(--c-text3)' }}>{passed.length}/{sectionFindings.length}</span>
230
+ <div className="w-20 h-2 rounded-full overflow-hidden flex-shrink-0" style={{ background: 'var(--c-code-bg)' }}>
231
+ <div className="h-full rounded-full" style={{ width: `${pct}%`, background: barColor }} />
232
+ </div>
233
+ <span className="text-[11px] w-9 text-right font-bold flex-shrink-0" style={{ color: barColor }}>{pct}%</span>
234
+ {missing.length > 0 && <Tip missing={missing} />}
235
+ </div>
236
+ {/* Expanded: found findings */}
237
+ {isOpen && passed.length > 0 && (
238
+ <div className="flex flex-wrap gap-x-4 gap-y-0.5 pb-2 pl-12">
239
+ {passed.map(f => (
240
+ <span key={f.id} className="inline-flex items-center gap-1.5 text-[11px] py-0.5">
241
+ <CheckCircle2 size={10} style={{ color: '#22c55e', flexShrink: 0 }} />
242
+ <span style={{ color: 'var(--c-text)' }}>{f.label}</span>
243
+ {f.matchedPath && <span style={{ color: 'var(--c-text3)' }}>{f.matchedPath}</span>}
244
+ {f.detail && <span style={{ color: 'var(--c-text2)' }}>— {f.detail}</span>}
245
+ </span>
246
+ ))}
247
+ </div>
248
+ )}
249
+ </div>
250
+ )
251
+ })}
252
+ </div>
253
+ </div>
254
+ )
255
+ }
package/ui/src/lib/api.js CHANGED
@@ -203,6 +203,12 @@ export async function fetchToolCalls(name, opts = {}) {
203
203
  return res.json();
204
204
  }
205
205
 
206
+ export async function fetchCheckAi(folder) {
207
+ const q = new URLSearchParams({ folder });
208
+ const res = await fetch(`${BASE}/api/check-ai?${q}`);
209
+ return res.json();
210
+ }
211
+
206
212
  export async function fetchUsage() {
207
213
  const res = await fetch(`${BASE}/api/usage`);
208
214
  return res.json();
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useMemo } from 'react'
2
2
  import { useSearchParams, useNavigate } from 'react-router-dom'
3
- import { ArrowLeft, Search, FolderOpen, Calendar, MessageSquare, Wrench, Cpu, Zap, AlertTriangle } from 'lucide-react'
3
+ import { ArrowLeft, Search, FolderOpen, Calendar, MessageSquare, Wrench, Cpu, Zap, AlertTriangle, ShieldCheck } from 'lucide-react'
4
4
  import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'
5
5
  import { Doughnut, Bar } from 'react-chartjs-2'
6
6
  import { fetchProjects, fetchChats, fetchCosts } from '../lib/api'
@@ -10,6 +10,7 @@ import KpiCard from '../components/KpiCard'
10
10
  import EditorIcon from '../components/EditorIcon'
11
11
  import SectionTitle from '../components/SectionTitle'
12
12
  import ChatSidebar from '../components/ChatSidebar'
13
+ import AiAuditCard from '../components/AiAuditCard'
13
14
 
14
15
  ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement)
15
16
 
@@ -233,6 +234,9 @@ export default function ProjectDetail() {
233
234
  </div>
234
235
  </div>
235
236
 
237
+ {/* AI Readiness Audit */}
238
+ <AiAuditCard folder={folder} />
239
+
236
240
  {/* Sessions */}
237
241
  <div>
238
242
  <div className="flex items-center gap-3 mb-2">