agentlytics 0.0.3 → 0.0.5

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.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "index.js",
11
11
  "cache.js",
12
12
  "server.js",
13
+ "share-image.js",
13
14
  "editors/",
14
15
  "ui/src/",
15
16
  "ui/index.html",
package/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const express = require('express');
2
2
  const path = require('path');
3
3
  const cache = require('./cache');
4
+ const { generateShareSvg } = require('./share-image');
4
5
 
5
6
  const app = express();
6
7
  app.use(express.json());
@@ -148,6 +149,19 @@ app.get('/api/schema', (req, res) => {
148
149
  }
149
150
  });
150
151
 
152
+ app.get('/api/share-image', (req, res) => {
153
+ try {
154
+ const overview = cache.getCachedOverview();
155
+ const stats = cache.getCachedDashboardStats();
156
+ const svg = generateShareSvg(overview, stats);
157
+ res.setHeader('Content-Type', 'image/svg+xml');
158
+ res.send(svg);
159
+ } catch (err) {
160
+ console.error('Share image error:', err);
161
+ res.status(500).json({ error: err.message, stack: err.stack });
162
+ }
163
+ });
164
+
151
165
  app.get('/api/refetch', async (req, res) => {
152
166
  res.writeHead(200, {
153
167
  'Content-Type': 'text/event-stream',
package/share-image.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Generates a shareable SVG stats card from cached data.
3
+ */
4
+
5
+ function fmt(n) {
6
+ if (n == null) return '0';
7
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
8
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
9
+ return n.toLocaleString();
10
+ }
11
+
12
+ const EDITOR_COLORS = {
13
+ 'cursor': '#f59e0b',
14
+ 'windsurf': '#06b6d4',
15
+ 'windsurf-next': '#22d3ee',
16
+ 'antigravity': '#a78bfa',
17
+ 'claude-code': '#f97316',
18
+ 'claude': '#f97316',
19
+ 'vscode': '#3b82f6',
20
+ 'vscode-insiders': '#60a5fa',
21
+ 'zed': '#10b981',
22
+ 'opencode': '#ec4899',
23
+ 'gemini-cli': '#4285f4',
24
+ 'copilot-cli': '#8957e5',
25
+ };
26
+
27
+ const EDITOR_LABELS = {
28
+ 'cursor': 'Cursor',
29
+ 'windsurf': 'Windsurf',
30
+ 'windsurf-next': 'WS Next',
31
+ 'antigravity': 'Antigravity',
32
+ 'claude-code': 'Claude Code',
33
+ 'claude': 'Claude Code',
34
+ 'vscode': 'VS Code',
35
+ 'vscode-insiders': 'VS Code Ins.',
36
+ 'zed': 'Zed',
37
+ 'opencode': 'OpenCode',
38
+ 'gemini-cli': 'Gemini CLI',
39
+ 'copilot-cli': 'Copilot CLI',
40
+ };
41
+
42
+ function generateShareSvg(overview, stats) {
43
+ const W = 800, H = 440;
44
+ const F = "Menlo, Monaco, Cascadia Code, Courier New, monospace";
45
+ const editors = overview.editors || [];
46
+ const tk = stats.tokens || {};
47
+ const streaks = stats.streaks || {};
48
+ const topModels = (stats.topModels || []).slice(0, 5);
49
+
50
+ // Editor bar chart
51
+ const maxEditorCount = Math.max(...editors.map(e => e.count), 1);
52
+ const editorBars = editors.slice(0, 8).map((e, i) => {
53
+ const barW = Math.max((e.count / maxEditorCount) * 180, 4);
54
+ const color = EDITOR_COLORS[e.id] || '#6b7280';
55
+ const label = (EDITOR_LABELS[e.id] || e.id).padEnd(12);
56
+ const y = 170 + i * 22;
57
+ return `
58
+ <text x="30" y="${y + 12}" fill="#586e75" font-size="10" font-family="${F}">${esc(label)}</text>
59
+ <rect x="140" y="${y + 1}" width="${barW}" height="14" rx="2" fill="${color}" opacity="0.8"/>
60
+ <text x="${146 + barW}" y="${y + 12}" fill="#839496" font-size="9" font-family="${F}">${e.count}</text>
61
+ `;
62
+ }).join('');
63
+
64
+ // Activity sparkline from hourly data
65
+ const hourly = stats.hourly || new Array(24).fill(0);
66
+ const maxH = Math.max(...hourly, 1);
67
+ const sparkW = 180, sparkH = 40;
68
+ const sparkPoints = hourly.map((v, i) => {
69
+ const x = 590 + (i / 23) * sparkW;
70
+ const y = 180 + sparkH - (v / maxH) * sparkH;
71
+ return `${x},${y}`;
72
+ }).join(' ');
73
+
74
+ // Top models list
75
+ const modelsList = topModels.map((m, i) => {
76
+ const y = 274 + i * 16;
77
+ const name = m.name.length > 24 ? m.name.substring(0, 24) : m.name;
78
+ return `<text x="590" y="${y}" fill="#586e75" font-size="9" font-family="${F}">${esc(name)} <tspan fill="#475569">${m.count}</tspan></text>`;
79
+ }).join('');
80
+
81
+ const dateStr = new Date().toISOString().split('T')[0];
82
+
83
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
84
+ <!-- Background -->
85
+ <rect width="${W}" height="${H}" rx="12" fill="#002b36"/>
86
+ <rect x="0.5" y="0.5" width="${W - 1}" height="${H - 1}" rx="12" fill="none" stroke="#073642" stroke-width="1"/>
87
+
88
+ <!-- Terminal title bar -->
89
+ <rect x="0" y="0" width="${W}" height="32" rx="12" fill="#073642"/>
90
+ <rect x="0" y="16" width="${W}" height="16" fill="#073642"/>
91
+ <circle cx="18" cy="16" r="5" fill="#dc322f" opacity="0.8"/>
92
+ <circle cx="36" cy="16" r="5" fill="#b58900" opacity="0.8"/>
93
+ <circle cx="54" cy="16" r="5" fill="#859900" opacity="0.8"/>
94
+ <text x="${W / 2}" y="20" fill="#586e75" font-size="11" font-family="${F}" text-anchor="middle">agentlytics</text>
95
+
96
+ <!-- Prompt line -->
97
+ <text x="24" y="58" fill="#859900" font-size="12" font-family="${F}">$</text>
98
+ <text x="40" y="58" fill="#93a1a1" font-size="12" font-family="${F}">npx agentlytics</text>
99
+
100
+ <!-- Divider -->
101
+ <line x1="24" y1="68" x2="${W - 24}" y2="68" stroke="#073642" stroke-width="1"/>
102
+
103
+ <!-- KPI row -->
104
+ <rect x="24" y="78" width="175" height="58" rx="6" fill="#073642"/>
105
+ <text x="36" y="96" fill="#586e75" font-size="9" font-family="${F}">sessions</text>
106
+ <text x="36" y="122" fill="#93a1a1" font-size="22" font-weight="bold" font-family="${F}">${fmt(overview.totalChats)}</text>
107
+
108
+ <rect x="210" y="78" width="175" height="58" rx="6" fill="#073642"/>
109
+ <text x="222" y="96" fill="#586e75" font-size="9" font-family="${F}">tokens</text>
110
+ <text x="222" y="122" fill="#93a1a1" font-size="22" font-weight="bold" font-family="${F}">${fmt((tk.input || 0) + (tk.output || 0))}</text>
111
+
112
+ <rect x="396" y="78" width="175" height="58" rx="6" fill="#073642"/>
113
+ <text x="408" y="96" fill="#586e75" font-size="9" font-family="${F}">active_days</text>
114
+ <text x="408" y="122" fill="#93a1a1" font-size="22" font-weight="bold" font-family="${F}">${streaks.totalDays || 0}</text>
115
+
116
+ <rect x="582" y="78" width="194" height="58" rx="6" fill="#073642"/>
117
+ <text x="594" y="96" fill="#586e75" font-size="9" font-family="${F}">streak <tspan fill="#475569">longest:${streaks.longest || 0}</tspan></text>
118
+ <text x="594" y="122" fill="#93a1a1" font-size="22" font-weight="bold" font-family="${F}">${streaks.current || 0} <tspan font-size="11" fill="#586e75">day${(streaks.current || 0) !== 1 ? 's' : ''}</tspan></text>
119
+
120
+ <!-- Editors section -->
121
+ <text x="24" y="160" fill="#859900" font-size="10" font-family="${F}"># editors</text>
122
+ ${editorBars}
123
+
124
+ <!-- Right column: Peak Hours -->
125
+ <text x="590" y="160" fill="#859900" font-size="10" font-family="${F}"># peak_hours</text>
126
+ <polyline points="${sparkPoints}" fill="none" stroke="#268bd2" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>
127
+ <text x="590" y="${180 + sparkH + 14}" fill="#475569" font-size="8" font-family="${F}">00:00</text>
128
+ <text x="${590 + sparkW - 28}" y="${180 + sparkH + 14}" fill="#475569" font-size="8" font-family="${F}">23:00</text>
129
+
130
+ <!-- Top Models -->
131
+ <text x="590" y="258" fill="#859900" font-size="10" font-family="${F}"># models</text>
132
+ ${modelsList}
133
+
134
+ <!-- Token breakdown -->
135
+ <line x1="24" y1="${H - 62}" x2="${W - 24}" y2="${H - 62}" stroke="#073642" stroke-width="1"/>
136
+ <text x="24" y="${H - 44}" fill="#586e75" font-size="9" font-family="${F}">in:${fmt(tk.input)} out:${fmt(tk.output)} cache:${fmt(tk.cacheRead)} tools:${fmt(stats.totalToolCalls || 0)} editors:${editors.length}</text>
137
+
138
+ <!-- Footer -->
139
+ <line x1="24" y1="${H - 28}" x2="${W - 24}" y2="${H - 28}" stroke="#073642" stroke-width="1"/>
140
+ <text x="24" y="${H - 10}" fill="#475569" font-size="9" font-family="${F}">github.com/f/agentlytics</text>
141
+ <text x="${W - 24}" y="${H - 10}" fill="#475569" font-size="9" font-family="${F}" text-anchor="end">${esc(dateStr)}</text>
142
+ </svg>`;
143
+
144
+ return svg;
145
+ }
146
+
147
+ function esc(str) {
148
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
149
+ }
150
+
151
+ module.exports = { generateShareSvg };
package/ui/src/lib/api.js CHANGED
@@ -83,6 +83,11 @@ export async function fetchSchema() {
83
83
  return res.json();
84
84
  }
85
85
 
86
+ export async function fetchShareImage() {
87
+ const res = await fetch(`${BASE}/api/share-image`);
88
+ return res.text();
89
+ }
90
+
86
91
  export async function fetchToolCalls(name, opts = {}) {
87
92
  const q = new URLSearchParams({ name });
88
93
  if (opts.limit) q.set('limit', opts.limit);
@@ -1,11 +1,11 @@
1
1
  import { useState, useEffect } from 'react'
2
2
  import { useNavigate } from 'react-router-dom'
3
- import { ArrowRight, X, Flame, Zap, MessageSquare, Wrench } from 'lucide-react'
3
+ import { ArrowRight, X, Flame, Zap, MessageSquare, Wrench, Share2 } from 'lucide-react'
4
4
  import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, PointElement, LineElement, Filler } from 'chart.js'
5
5
  import { Doughnut, Bar, Line } from 'react-chartjs-2'
6
6
  import KpiCard from '../components/KpiCard'
7
7
  import ActivityHeatmap from '../components/ActivityHeatmap'
8
- import { fetchDailyActivity, fetchOverview as fetchOverviewApi, fetchDashboardStats } from '../lib/api'
8
+ import { fetchDailyActivity, fetchOverview as fetchOverviewApi, fetchDashboardStats, fetchShareImage } from '../lib/api'
9
9
  import { editorColor, editorLabel, formatNumber } from '../lib/constants'
10
10
  import { useTheme } from '../lib/theme'
11
11
 
@@ -30,6 +30,7 @@ export default function Dashboard({ overview }) {
30
30
  const [stats, setStats] = useState(null)
31
31
  const [selectedEditor, setSelectedEditor] = useState(null)
32
32
  const { dark } = useTheme()
33
+ const [sharing, setSharing] = useState(false)
33
34
  const txtColor = dark ? '#888' : '#555'
34
35
  const txtDim = dark ? '#555' : '#999'
35
36
  const gridColor = dark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.06)'
@@ -157,8 +158,70 @@ export default function Dashboard({ overview }) {
157
158
  return s + v * midpoints[i]
158
159
  }, 0) / tk.sessions).toFixed(1) : '—') : '—'
159
160
 
161
+ const handleShare = async () => {
162
+ setSharing(true)
163
+ try {
164
+ const svg = await fetchShareImage()
165
+ if (!svg || svg.startsWith('{')) throw new Error('Failed to fetch image')
166
+
167
+ // Try PNG conversion via canvas, fallback to SVG download
168
+ let downloaded = false
169
+ try {
170
+ const canvas = document.createElement('canvas')
171
+ canvas.width = 1600
172
+ canvas.height = 880
173
+ const ctx = canvas.getContext('2d')
174
+ const img = new Image()
175
+ const svgB64 = btoa(unescape(encodeURIComponent(svg)))
176
+ const dataUrl = `data:image/svg+xml;base64,${svgB64}`
177
+ await new Promise((resolve, reject) => {
178
+ img.onload = resolve
179
+ img.onerror = reject
180
+ img.src = dataUrl
181
+ })
182
+ ctx.drawImage(img, 0, 0, 1600, 880)
183
+ const pngUrl = canvas.toDataURL('image/png')
184
+ const a = document.createElement('a')
185
+ a.href = pngUrl
186
+ a.download = 'agentlytics.png'
187
+ a.click()
188
+ downloaded = true
189
+ } catch {
190
+ // Fallback: download SVG directly
191
+ const blob = new Blob([svg], { type: 'image/svg+xml' })
192
+ const a = document.createElement('a')
193
+ a.href = URL.createObjectURL(blob)
194
+ a.download = 'agentlytics.svg'
195
+ a.click()
196
+ URL.revokeObjectURL(a.href)
197
+ downloaded = true
198
+ }
199
+
200
+ if (downloaded) {
201
+ const text = encodeURIComponent("Here's my agentic coding stats using github.com/f/agentlytics")
202
+ window.open(`https://x.com/intent/post?text=${text}`, '_blank')
203
+ }
204
+ } catch (e) {
205
+ console.error('Share failed:', e)
206
+ }
207
+ setSharing(false)
208
+ }
209
+
160
210
  return (
161
211
  <div className="fade-in space-y-3">
212
+ {/* Share button */}
213
+ <div className="flex justify-end">
214
+ <button
215
+ onClick={handleShare}
216
+ disabled={sharing}
217
+ className="flex items-center gap-1.5 px-3 py-1 text-[11px] rounded-md transition hover:opacity-80"
218
+ style={{ background: '#6366f1', color: '#fff', opacity: sharing ? 0.5 : 1 }}
219
+ >
220
+ <Share2 size={12} />
221
+ {sharing ? 'Generating...' : 'Share Stats'}
222
+ </button>
223
+ </div>
224
+
162
225
  {/* Editor breakdown - top */}
163
226
  <div className="card p-3">
164
227
  <SectionTitle>editors</SectionTitle>