agentlytics 0.1.11 → 0.1.12

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
@@ -431,14 +431,17 @@ function getCachedOverview(opts = {}) {
431
431
  const hf = hiddenFolderFilter(opts);
432
432
  if (hf.sql) { conditions.push(hf.sql.replace(' AND ', '')); params.push(...hf.params); }
433
433
  if (opts.editor) { conditions.push('source = ?'); params.push(opts.editor); }
434
+ if (opts.folder) { conditions.push('folder = ?'); params.push(opts.folder); }
434
435
  if (opts.dateFrom) { conditions.push('COALESCE(last_updated_at, created_at) >= ?'); params.push(opts.dateFrom); }
435
436
  if (opts.dateTo) { conditions.push('COALESCE(last_updated_at, created_at) <= ?'); params.push(opts.dateTo); }
436
437
  const where = conditions.length > 0 ? ' WHERE ' + conditions.join(' AND ') : '';
437
438
  const whereAnd = conditions.length > 0 ? ' AND ' + conditions.join(' AND ') : '';
438
439
 
439
440
  const totalChats = db.prepare(`SELECT COUNT(*) as cnt FROM chats${where}`).get(...params).cnt;
440
- // Editors list is always unfiltered so the breakdown remains visible
441
- const editors = db.prepare('SELECT source, COUNT(*) as count FROM chats GROUP BY source ORDER BY count DESC').all();
441
+ // When folder-filtered, show only that project's editors; otherwise show all
442
+ const editors = opts.folder
443
+ ? db.prepare(`SELECT source, COUNT(*) as count FROM chats${where} GROUP BY source ORDER BY count DESC`).all(...params)
444
+ : db.prepare('SELECT source, COUNT(*) as count FROM chats GROUP BY source ORDER BY count DESC').all();
442
445
 
443
446
  // By mode
444
447
  const modes = db.prepare(`SELECT mode, COUNT(*) as count FROM chats WHERE mode IS NOT NULL${whereAnd} GROUP BY mode`).all(...params);
@@ -851,6 +854,7 @@ function getCachedDashboardStats(opts = {}) {
851
854
  const hf = hiddenFolderFilter(opts);
852
855
  if (hf.sql) { conditions.push(hf.sql.replace(' AND ', '')); params.push(...hf.params); }
853
856
  if (opts.editor) { conditions.push('source = ?'); params.push(opts.editor); }
857
+ if (opts.folder) { conditions.push('folder = ?'); params.push(opts.folder); }
854
858
  if (opts.dateFrom) { conditions.push('COALESCE(last_updated_at, created_at) >= ?'); params.push(opts.dateFrom); }
855
859
  if (opts.dateTo) { conditions.push('COALESCE(last_updated_at, created_at) <= ?'); params.push(opts.dateTo); }
856
860
  const where = conditions.length > 0 ? ' WHERE ' + conditions.join(' AND ') : '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlytics",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
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/server.js CHANGED
@@ -264,9 +264,11 @@ app.get('/api/schema', (req, res) => {
264
264
 
265
265
  app.get('/api/share-image', (req, res) => {
266
266
  try {
267
- const overview = cache.getCachedOverview();
268
- const stats = cache.getCachedDashboardStats();
269
- const costs = cache.getCostAnalytics({ hiddenFolders: getHiddenFolders() });
267
+ const filterOpts = { hiddenFolders: getHiddenFolders() };
268
+ if (req.query.folder) filterOpts.folder = req.query.folder;
269
+ const overview = cache.getCachedOverview(filterOpts);
270
+ const stats = cache.getCachedDashboardStats(filterOpts);
271
+ const costs = cache.getCostAnalytics(filterOpts);
270
272
  const opts = {};
271
273
  if (req.query.showEditors !== undefined) opts.showEditors = req.query.showEditors !== 'false';
272
274
  if (req.query.showModels !== undefined) opts.showModels = req.query.showModels !== 'false';
@@ -275,6 +277,7 @@ app.get('/api/share-image', (req, res) => {
275
277
  if (req.query.showHours !== undefined) opts.showHours = req.query.showHours !== 'false';
276
278
  if (req.query.username) opts.username = req.query.username;
277
279
  if (req.query.theme) opts.theme = req.query.theme;
280
+ if (req.query.folder) opts.folder = req.query.folder;
278
281
  const svg = generateShareSvg(overview, stats, costs, opts);
279
282
  res.setHeader('Content-Type', 'image/svg+xml');
280
283
  res.send(svg);
package/share-image.js CHANGED
@@ -93,10 +93,12 @@ function generateShareSvg(overview, stats, costs, opts = {}) {
93
93
  };
94
94
  const username = opts.username || '';
95
95
  const t = THEMES[opts.theme] || THEMES.dark;
96
+ const projectFolder = opts.folder || '';
97
+ const projectName = projectFolder ? projectFolder.split(/[/\\]/).pop() : '';
96
98
 
97
99
  const W = 1200;
98
100
  const H_FIXED = 675;
99
- const F = "'Menlo','Monaco','Cascadia Code','Courier New',monospace";
101
+ const F = "\"'Menlo','Monaco','Cascadia Code','Courier New',monospace\"";
100
102
  const editors = overview.editors || [];
101
103
  const tk = stats.tokens || {};
102
104
  const streaks = stats.streaks || {};
@@ -183,9 +185,10 @@ function generateShareSvg(overview, stats, costs, opts = {}) {
183
185
  });
184
186
 
185
187
  // ── Editor bar chart ──
188
+ const colW = (W - pad * 2 - 20) / 2;
189
+ const maxBarW = colW - 120 - 60;
186
190
  const maxEditorCount = Math.max(...editors.map(e => e.count), 1);
187
191
  const editorBarsArr = editors.slice(0, 8).map((e, i) => {
188
- const maxBarW = W / 2 - pad - 140;
189
192
  const barW = Math.max((e.count / maxEditorCount) * maxBarW, 4);
190
193
  const color = EDITOR_COLORS[e.id] || '#6b7280';
191
194
  const label = (EDITOR_LABELS[e.id] || e.id);
@@ -195,7 +198,6 @@ function generateShareSvg(overview, stats, costs, opts = {}) {
195
198
  // ── Cost bar chart ──
196
199
  const maxCostVal = costByEditor.length > 0 ? Math.max(...costByEditor.map(c => c.cost), 0.01) : 1;
197
200
  const costBarsArr = costByEditor.map(c => {
198
- const maxBarW = W / 2 - pad - 140;
199
201
  const barW = Math.max((c.cost / maxCostVal) * maxBarW, 4);
200
202
  const color = EDITOR_COLORS[c.editor] || '#6b7280';
201
203
  const label = EDITOR_LABELS[c.editor] || c.editor;
@@ -209,7 +211,6 @@ function generateShareSvg(overview, stats, costs, opts = {}) {
209
211
  // ── Build sections ──
210
212
  let curY = kpiY + 64 + 18;
211
213
  const sectionSvgs = [];
212
- const colW = (W - pad * 2 - 20) / 2;
213
214
 
214
215
  for (let row = 0; row < rowCount; row++) {
215
216
  const lName = leftSections[row];
@@ -330,7 +331,7 @@ function generateShareSvg(overview, stats, costs, opts = {}) {
330
331
  <path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/>
331
332
  </g>
332
333
  <text x="${pad + 26}" y="68" fill="${t.text}" font-size="16" font-weight="bold" font-family=${F}>Agentlytics</text>
333
- <text x="${pad + 148}" y="68" fill="${t.text4}" font-size="13" font-family=${F}>Your AI coding stats</text>
334
+ <text x="${pad + 148}" y="68" fill="${t.text4}" font-size="13" font-family=${F}>${projectName ? esc(projectName) : 'Your AI coding stats'}</text>
334
335
  <text x="${W - pad}" y="68" fill="${t.text5}" font-size="12" font-family=${F} text-anchor="end">${esc(dateStr)}</text>
335
336
 
336
337
  <!-- Divider -->
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useRef, useCallback } from 'react'
2
- import { X, Download, Share2, BarChart3, DollarSign, Clock, Cpu, Braces, User, Sun, Moon } from 'lucide-react'
3
- import { fetchShareImage } from '../lib/api'
2
+ import { X, Download, Share2, BarChart3, DollarSign, Clock, Cpu, Braces, User, Sun, Moon, FolderOpen } from 'lucide-react'
3
+ import { fetchShareImage, fetchProjects } from '../lib/api'
4
4
 
5
5
  const TOGGLE_ITEMS = [
6
6
  { key: 'showEditors', label: 'Editors', icon: BarChart3 },
@@ -23,6 +23,7 @@ export default function ShareModal({ open, onClose }) {
23
23
  const [svg, setSvg] = useState('')
24
24
  const [loading, setLoading] = useState(false)
25
25
  const [downloading, setDownloading] = useState(false)
26
+ const [projects, setProjects] = useState([])
26
27
  const debounceRef = useRef(null)
27
28
  const backdropRef = useRef(null)
28
29
 
@@ -40,6 +41,7 @@ export default function ShareModal({ open, onClose }) {
40
41
  useEffect(() => {
41
42
  if (!open) return
42
43
  loadPreview(opts)
44
+ fetchProjects().then(p => setProjects(p || [])).catch(() => {})
43
45
  }, [open])
44
46
 
45
47
  const updateOpt = (key, value) => {
@@ -53,25 +55,33 @@ export default function ShareModal({ open, onClose }) {
53
55
  if (!svg) return
54
56
  setDownloading(true)
55
57
  try {
56
- const canvas = document.createElement('canvas')
57
- canvas.width = 1200
58
- canvas.height = 675
59
- const ctx = canvas.getContext('2d')
58
+ const scale = 2
59
+ const svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' })
60
+ const url = URL.createObjectURL(svgBlob)
60
61
  const img = new Image()
61
- const svgB64 = btoa(unescape(encodeURIComponent(svg)))
62
- const dataUrl = `data:image/svg+xml;base64,${svgB64}`
62
+ img.width = 1200 * scale
63
+ img.height = 675 * scale
63
64
  await new Promise((resolve, reject) => {
64
65
  img.onload = resolve
65
66
  img.onerror = reject
66
- img.src = dataUrl
67
+ img.src = url
67
68
  })
68
- ctx.drawImage(img, 0, 0, 1200, 675)
69
- const pngUrl = canvas.toDataURL('image/png')
70
- const a = document.createElement('a')
71
- a.href = pngUrl
72
- a.download = 'agentlytics.png'
73
- a.click()
74
- } catch {
69
+ const canvas = document.createElement('canvas')
70
+ canvas.width = 1200 * scale
71
+ canvas.height = 675 * scale
72
+ const ctx = canvas.getContext('2d')
73
+ ctx.drawImage(img, 0, 0, 1200 * scale, 675 * scale)
74
+ URL.revokeObjectURL(url)
75
+ canvas.toBlob((blob) => {
76
+ if (!blob) return
77
+ const a = document.createElement('a')
78
+ a.href = URL.createObjectURL(blob)
79
+ a.download = 'agentlytics.png'
80
+ a.click()
81
+ setTimeout(() => URL.revokeObjectURL(a.href), 1000)
82
+ }, 'image/png')
83
+ } catch (e) {
84
+ console.error('PNG conversion failed:', e)
75
85
  const blob = new Blob([svg], { type: 'image/svg+xml' })
76
86
  const a = document.createElement('a')
77
87
  a.href = URL.createObjectURL(blob)
@@ -138,6 +148,28 @@ export default function ShareModal({ open, onClose }) {
138
148
  Customize
139
149
  </div>
140
150
 
151
+ <div className="mb-4">
152
+ <div className="text-[11px] font-medium mb-2" style={{ color: 'var(--c-text2)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
153
+ Project
154
+ </div>
155
+ <div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md" style={{ border: '1px solid var(--c-border)', background: 'var(--c-bg)' }}>
156
+ <FolderOpen size={11} style={{ color: 'var(--c-text3)' }} />
157
+ <select
158
+ value={opts.folder || ''}
159
+ onChange={(e) => updateOpt('folder', e.target.value || undefined)}
160
+ className="bg-transparent outline-none text-[12px] w-full cursor-pointer"
161
+ style={{ color: 'var(--c-text)', appearance: 'none' }}
162
+ >
163
+ <option value="">All Projects</option>
164
+ {projects.map(p => (
165
+ <option key={p.folder} value={p.folder}>
166
+ {p.name} ({p.totalSessions})
167
+ </option>
168
+ ))}
169
+ </select>
170
+ </div>
171
+ </div>
172
+
141
173
  <div className="space-y-1.5">
142
174
  {TOGGLE_ITEMS.map(({ key, label, icon: Icon }) => {
143
175
  const active = opts[key]
package/ui/src/lib/api.js CHANGED
@@ -189,6 +189,7 @@ export async function fetchShareImage(opts = {}) {
189
189
  if (opts.showHours !== undefined) q.set('showHours', opts.showHours);
190
190
  if (opts.username) q.set('username', opts.username);
191
191
  if (opts.theme) q.set('theme', opts.theme);
192
+ if (opts.folder) q.set('folder', opts.folder);
192
193
  const qs = q.toString();
193
194
  const res = await fetch(`${BASE}/api/share-image${qs ? '?' + qs : ''}`);
194
195
  return res.text();