memhouse 0.0.1 → 0.3.0

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.
Files changed (96) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +59 -3
  3. package/bin/memhouse.js +487 -0
  4. package/cache.js +850 -0
  5. package/ch-ingest.js +329 -0
  6. package/ch-schema.js +152 -0
  7. package/clickhouse.js +87 -0
  8. package/editors/antigravity.js +1019 -0
  9. package/editors/base.js +321 -0
  10. package/editors/claude.js +386 -0
  11. package/editors/codebuff.js +338 -0
  12. package/editors/codex.js +537 -0
  13. package/editors/commandcode.js +166 -0
  14. package/editors/copilot-jetbrains.js +207 -0
  15. package/editors/copilot.js +260 -0
  16. package/editors/cursor-agent.js +202 -0
  17. package/editors/cursor.js +438 -0
  18. package/editors/gemini.js +231 -0
  19. package/editors/goose.js +358 -0
  20. package/editors/gsd.js +366 -0
  21. package/editors/index.js +179 -0
  22. package/editors/kiro.js +315 -0
  23. package/editors/opencode.js +374 -0
  24. package/editors/vscode.js +413 -0
  25. package/editors/windsurf.js +634 -0
  26. package/editors/zed.js +213 -0
  27. package/index.js +424 -2
  28. package/mcp-server.js +294 -0
  29. package/mem-house/DESIGN.md +119 -0
  30. package/mem-house/delivery/AGENT-INSTALL.md +75 -0
  31. package/mem-house/delivery/PROMPT.md +42 -0
  32. package/mem-house/delivery/install.sh +181 -0
  33. package/mem-house/delivery/kernel-install.md +76 -0
  34. package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
  35. package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
  36. package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
  37. package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
  38. package/mem-house/rls.sql +17 -0
  39. package/mem-house/schema.sql +145 -0
  40. package/mem-house/server/queries.js +878 -0
  41. package/mem-house/server/server.js +228 -0
  42. package/mem-house/shipper/ship.js +402 -0
  43. package/misc/syntax-check.js +38 -0
  44. package/package.json +56 -7
  45. package/pricing.js +88 -0
  46. package/pricing.json +992 -0
  47. package/public/assets/index-BsgjOSTv.js +75 -0
  48. package/public/assets/index-DV6ONi_F.css +2 -0
  49. package/public/index.html +16 -0
  50. package/relay-client.js +306 -0
  51. package/relay-server.js +577 -0
  52. package/server.js +860 -0
  53. package/share-image.js +367 -0
  54. package/ui/eslint.config.js +29 -0
  55. package/ui/index.html +15 -0
  56. package/ui/package-lock.json +4229 -0
  57. package/ui/package.json +38 -0
  58. package/ui/src/App.jsx +364 -0
  59. package/ui/src/components/ActivityHeatmap.jsx +206 -0
  60. package/ui/src/components/AiAuditCard.jsx +254 -0
  61. package/ui/src/components/AnimatedLoader.jsx +14 -0
  62. package/ui/src/components/AnimatedLogo.jsx +96 -0
  63. package/ui/src/components/ChatSidebar.jsx +226 -0
  64. package/ui/src/components/DateRangePicker.jsx +104 -0
  65. package/ui/src/components/EditorDot.jsx +11 -0
  66. package/ui/src/components/EditorIcon.jsx +117 -0
  67. package/ui/src/components/KpiCard.jsx +9 -0
  68. package/ui/src/components/LiveFeed.jsx +128 -0
  69. package/ui/src/components/LoginScreen.jsx +81 -0
  70. package/ui/src/components/MessageRenderer.jsx +148 -0
  71. package/ui/src/components/PageHeader.jsx +11 -0
  72. package/ui/src/components/SectionTitle.jsx +3 -0
  73. package/ui/src/components/ShareModal.jsx +305 -0
  74. package/ui/src/components/TokenTimeline.jsx +258 -0
  75. package/ui/src/hooks/useLive.jsx +15 -0
  76. package/ui/src/index.css +83 -0
  77. package/ui/src/lib/api.js +343 -0
  78. package/ui/src/lib/constants.js +88 -0
  79. package/ui/src/lib/theme.jsx +23 -0
  80. package/ui/src/main.jsx +16 -0
  81. package/ui/src/pages/Artifacts.jsx +575 -0
  82. package/ui/src/pages/Compare.jsx +232 -0
  83. package/ui/src/pages/CostAnalysis.jsx +354 -0
  84. package/ui/src/pages/Dashboard.jsx +529 -0
  85. package/ui/src/pages/DeepAnalysis.jsx +495 -0
  86. package/ui/src/pages/GSD.jsx +726 -0
  87. package/ui/src/pages/MCPs.jsx +744 -0
  88. package/ui/src/pages/ProjectDetail.jsx +325 -0
  89. package/ui/src/pages/Projects.jsx +249 -0
  90. package/ui/src/pages/RelayDashboard.jsx +590 -0
  91. package/ui/src/pages/RelayUserDetail.jsx +466 -0
  92. package/ui/src/pages/Sessions.jsx +535 -0
  93. package/ui/src/pages/Settings.jsx +241 -0
  94. package/ui/src/pages/SqlViewer.jsx +344 -0
  95. package/ui/src/pages/Subscriptions.jsx +411 -0
  96. package/ui/vite.config.js +18 -0
package/share-image.js ADDED
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Generates a shareable SVG stats card from cached data.
3
+ * Accepts an `opts` object to toggle sections on/off.
4
+ */
5
+
6
+ function fmt(n) {
7
+ if (n == null) return '0';
8
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
9
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
10
+ return n.toLocaleString();
11
+ }
12
+
13
+ function fmtCost(n) {
14
+ if (n == null || n === 0) return '$0';
15
+ if (n < 0.01) return '<$0.01';
16
+ if (n >= 1000) return '$' + (n / 1000).toFixed(1) + 'K';
17
+ if (n >= 100) return '$' + Math.round(n);
18
+ return '$' + n.toFixed(2);
19
+ }
20
+
21
+ const EDITOR_COLORS = {
22
+ 'cursor': '#f59e0b',
23
+ 'devin': '#2563eb',
24
+ 'devin-next': '#7c3aed',
25
+ 'antigravity': '#a78bfa',
26
+ 'claude-code': '#f97316',
27
+ 'claude': '#f97316',
28
+ 'vscode': '#3b82f6',
29
+ 'vscode-insiders': '#60a5fa',
30
+ 'zed': '#10b981',
31
+ 'opencode': '#656363',
32
+ 'codex': '#3941FF',
33
+ 'gemini-cli': '#4285f4',
34
+ 'copilot-cli': '#8957e5',
35
+ 'copilot-jetbrains': '#8957e5',
36
+ 'cursor-agent': '#f59e0b',
37
+ 'commandcode': '#e11d48',
38
+ 'kiro': '#9046FF',
39
+ };
40
+
41
+ const EDITOR_LABELS = {
42
+ 'cursor': 'Cursor',
43
+ 'devin': 'Devin',
44
+ 'devin-next': 'Devin Next',
45
+ 'antigravity': 'Antigravity',
46
+ 'claude-code': 'Claude Code',
47
+ 'claude': 'Claude Code',
48
+ 'vscode': 'VS Code',
49
+ 'vscode-insiders': 'VS Code Ins.',
50
+ 'zed': 'Zed',
51
+ 'opencode': 'OpenCode',
52
+ 'codex': 'Codex',
53
+ 'gemini-cli': 'Gemini CLI',
54
+ 'copilot-cli': 'GitHub Copilot',
55
+ 'copilot-jetbrains': 'GitHub Copilot - JetBrains',
56
+ 'cursor-agent': 'Cursor Agent',
57
+ 'commandcode': 'Cmd Code',
58
+ 'kiro': 'Kiro',
59
+ };
60
+
61
+ /**
62
+ * @param {object} overview — from getCachedOverview()
63
+ * @param {object} stats — from getCachedDashboardStats()
64
+ * @param {object} costs — from getCostBreakdown()
65
+ * @param {object} [opts] — toggle sections
66
+ * @param {boolean} [opts.showEditors=true]
67
+ * @param {boolean} [opts.showModels=true]
68
+ * @param {boolean} [opts.showCosts=true]
69
+ * @param {boolean} [opts.showTokens=true]
70
+ * @param {boolean} [opts.showHours=true]
71
+ * @param {boolean} [opts.showProjects=true]
72
+ * @param {string} [opts.username]
73
+ * @param {string} [opts.theme='dark'] — 'dark' or 'light'
74
+ */
75
+ const THEMES = {
76
+ dark: {
77
+ bg: '#09090f', bg2: '#111118', card: '#111', border: '#1e1e2a',
78
+ text: '#fff', text2: '#888', text3: '#666', text4: '#555', text5: '#444',
79
+ titleText: '#555',
80
+ hourHigh: '#818cf8', hourMed: '#6366f1', hourLow: '#4f46e5', hourMin: '#1e1b4b',
81
+ },
82
+ light: {
83
+ bg: '#f8f8fa', bg2: '#eeeef2', card: '#e8e8ee', border: '#d0d0d8',
84
+ text: '#111', text2: '#555', text3: '#888', text4: '#999', text5: '#aaa',
85
+ titleText: '#888',
86
+ hourHigh: '#6366f1', hourMed: '#818cf8', hourLow: '#a5b4fc', hourMin: '#e0e7ff',
87
+ },
88
+ };
89
+
90
+ function generateShareSvg(overview, stats, costs, opts = {}) {
91
+ const show = {
92
+ editors: opts.showEditors !== false,
93
+ models: opts.showModels !== false,
94
+ costs: opts.showCosts !== false,
95
+ tokens: opts.showTokens !== false,
96
+ hours: opts.showHours !== false,
97
+ };
98
+ const username = opts.username || '';
99
+ const t = THEMES[opts.theme] || THEMES.dark;
100
+ const projectFolder = opts.folder || '';
101
+ const projectName = projectFolder ? projectFolder.split(/[/\\]/).pop() : '';
102
+
103
+ const W = 1200;
104
+ const H_FIXED = 675;
105
+ const F = "\"'Menlo','Monaco','Cascadia Code','Courier New',monospace\"";
106
+ const editors = overview.editors || [];
107
+ const tk = stats.tokens || {};
108
+ const streaks = stats.streaks || {};
109
+ const topModels = (stats.topModels || []).slice(0, 5);
110
+ const costData = costs || {};
111
+ const totalCost = costData.totalCost || 0;
112
+ const costByEditor = (costData.byEditor || []).slice(0, 6);
113
+ const totalTokens = (tk.input || 0) + (tk.output || 0);
114
+ const now = new Date();
115
+ const dateStr = now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
116
+
117
+ // ── Compute layout ──
118
+ let y = 0;
119
+ const pad = 28;
120
+
121
+ // Title bar
122
+ y += 40;
123
+ // Header area (prompt + branding)
124
+ y += 54;
125
+ // KPI row
126
+ y += 80;
127
+ // Sections
128
+ const midSections = [];
129
+ if (show.editors) midSections.push('editors');
130
+ if (show.costs && totalCost > 0) midSections.push('costs');
131
+ if (show.hours) midSections.push('hours');
132
+ if (show.models && topModels.length > 0) midSections.push('models');
133
+
134
+ // Layout: 2 columns, each section ~row
135
+ const leftSections = [];
136
+ const rightSections = [];
137
+ midSections.forEach((s, i) => {
138
+ if (i % 2 === 0) leftSections.push(s);
139
+ else rightSections.push(s);
140
+ });
141
+
142
+ function sectionHeight(name) {
143
+ if (name === 'editors') return Math.max(editors.slice(0, 8).length * 26 + 34, 70);
144
+ if (name === 'costs') return Math.max(costByEditor.length * 26 + 34, 70);
145
+ if (name === 'hours') return 110;
146
+ if (name === 'models') return topModels.length * 22 + 34;
147
+ return 70;
148
+ }
149
+
150
+ // Mid-section pairs: compute max height of each row
151
+ const rowCount = Math.max(leftSections.length, rightSections.length);
152
+ let midHeight = 0;
153
+ for (let i = 0; i < rowCount; i++) {
154
+ const lh = leftSections[i] ? sectionHeight(leftSections[i]) : 0;
155
+ const rh = rightSections[i] ? sectionHeight(rightSections[i]) : 0;
156
+ midHeight += Math.max(lh, rh) + 8;
157
+ }
158
+
159
+ // Token footer
160
+ const tokenFooterH = show.tokens ? 44 : 0;
161
+ // Footer
162
+ const footerH = 36;
163
+
164
+ const naturalH = y + midHeight + tokenFooterH + footerH + 20;
165
+ // Stretch: if content is shorter than 675, distribute extra space into sections
166
+ const extraSpace = Math.max(0, H_FIXED - naturalH);
167
+ const extraPerRow = rowCount > 0 ? extraSpace / rowCount : 0;
168
+
169
+ // ── KPI cards ──
170
+ const kpiY = 94;
171
+ const kpiCards = [];
172
+ const kpiItems = [
173
+ { label: 'sessions', value: fmt(overview.totalChats) },
174
+ { label: 'tokens', value: fmt(totalTokens) },
175
+ { label: 'active days', value: String(streaks.totalDays || 0) },
176
+ { label: 'streak', value: `${streaks.current || 0}d` },
177
+ ];
178
+ if (show.costs && totalCost > 0) {
179
+ kpiItems.push({ label: 'est. cost', value: fmtCost(totalCost) });
180
+ }
181
+ const kpiW = (W - pad * 2 - (kpiItems.length - 1) * 8) / kpiItems.length;
182
+ kpiItems.forEach((item, i) => {
183
+ const x = pad + i * (kpiW + 8);
184
+ kpiCards.push(`
185
+ <rect x="${x}" y="${kpiY}" width="${kpiW}" height="64" rx="6" fill="${t.card}"/>
186
+ <text x="${x + 14}" y="${kpiY + 22}" fill="${t.text3}" font-size="11" font-family=${F}>${esc(item.label)}</text>
187
+ <text x="${x + 14}" y="${kpiY + 50}" fill="${t.text}" font-size="24" font-weight="bold" font-family=${F}>${esc(item.value)}</text>
188
+ `);
189
+ });
190
+
191
+ // ── Editor bar chart ──
192
+ const colW = (W - pad * 2 - 20) / 2;
193
+ const maxBarW = colW - 120 - 60;
194
+ const maxEditorCount = Math.max(...editors.map(e => e.count), 1);
195
+ const editorBarsArr = editors.slice(0, 8).map((e, i) => {
196
+ const barW = Math.max((e.count / maxEditorCount) * maxBarW, 4);
197
+ const color = EDITOR_COLORS[e.id] || '#6b7280';
198
+ const label = (EDITOR_LABELS[e.id] || e.id);
199
+ return { label, barW, color, count: e.count };
200
+ });
201
+
202
+ // ── Cost bar chart ──
203
+ const maxCostVal = costByEditor.length > 0 ? Math.max(...costByEditor.map(c => c.cost), 0.01) : 1;
204
+ const costBarsArr = costByEditor.map(c => {
205
+ const barW = Math.max((c.cost / maxCostVal) * maxBarW, 4);
206
+ const color = EDITOR_COLORS[c.editor] || '#6b7280';
207
+ const label = EDITOR_LABELS[c.editor] || c.editor;
208
+ return { label, barW, color, value: fmtCost(c.cost) };
209
+ });
210
+
211
+ // ── Hourly sparkline ──
212
+ const hourly = stats.hourly || new Array(24).fill(0);
213
+ const maxHourly = Math.max(...hourly, 1);
214
+
215
+ // ── Build sections ──
216
+ let curY = kpiY + 64 + 18;
217
+ const sectionSvgs = [];
218
+
219
+ for (let row = 0; row < rowCount; row++) {
220
+ const lName = leftSections[row];
221
+ const rName = rightSections[row];
222
+ const lh = lName ? sectionHeight(lName) : 0;
223
+ const rh = rName ? sectionHeight(rName) : 0;
224
+ const rowH = Math.max(lh, rh) + Math.round(extraPerRow);
225
+
226
+ if (lName) sectionSvgs.push(renderSection(lName, pad, curY, colW, rowH));
227
+ if (rName) sectionSvgs.push(renderSection(rName, pad + colW + 20, curY, colW, rowH));
228
+
229
+ curY += rowH + 10;
230
+ }
231
+
232
+ function renderSection(name, sx, sy, sw, sh) {
233
+ let out = '';
234
+ if (name === 'editors') {
235
+ out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># editors</text>`;
236
+ editorBarsArr.forEach((e, i) => {
237
+ const by = sy + 26 + i * 26;
238
+ out += `<text x="${sx}" y="${by + 14}" fill="${t.text2}" font-size="12" font-family=${F}>${esc(e.label)}</text>`;
239
+ out += `<rect x="${sx + 120}" y="${by + 1}" width="${e.barW}" height="16" rx="3" fill="${e.color}" opacity="0.85"/>`;
240
+ out += `<text x="${sx + 128 + e.barW}" y="${by + 14}" fill="${t.text3}" font-size="11" font-family=${F}>${e.count}</text>`;
241
+ });
242
+ } else if (name === 'costs') {
243
+ out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># est. costs by editor</text>`;
244
+ costBarsArr.forEach((c, i) => {
245
+ const by = sy + 26 + i * 26;
246
+ out += `<text x="${sx}" y="${by + 14}" fill="${t.text2}" font-size="12" font-family=${F}>${esc(c.label)}</text>`;
247
+ out += `<rect x="${sx + 120}" y="${by + 1}" width="${c.barW}" height="16" rx="3" fill="${c.color}" opacity="0.85"/>`;
248
+ out += `<text x="${sx + 128 + c.barW}" y="${by + 14}" fill="${t.text3}" font-size="11" font-family=${F}>${c.value}</text>`;
249
+ });
250
+ } else if (name === 'hours') {
251
+ out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># peak hours</text>`;
252
+ const sparkW = sw - 8;
253
+ const sparkH = 56;
254
+ const baseY = sy + 28;
255
+ const barW = sparkW / 24 - 1;
256
+ hourly.forEach((v, i) => {
257
+ const bh = Math.max((v / maxHourly) * sparkH, 1);
258
+ const bx = sx + i * (barW + 1);
259
+ const by = baseY + sparkH - bh;
260
+ const intensity = v / maxHourly;
261
+ const color = intensity > 0.75 ? t.hourHigh : intensity > 0.5 ? t.hourMed : intensity > 0.25 ? t.hourLow : t.hourMin;
262
+ out += `<rect x="${bx}" y="${by}" width="${barW}" height="${bh}" rx="1" fill="${color}" opacity="0.9"/>`;
263
+ });
264
+ out += `<text x="${sx}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>00</text>`;
265
+ out += `<text x="${sx + sparkW / 4}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>06</text>`;
266
+ out += `<text x="${sx + sparkW / 2}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>12</text>`;
267
+ out += `<text x="${sx + sparkW * 3 / 4}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>18</text>`;
268
+ out += `<text x="${sx + sparkW - 8}" y="${baseY + sparkH + 14}" fill="${t.text5}" font-size="9" font-family=${F}>23</text>`;
269
+ } else if (name === 'models') {
270
+ out += `<text x="${sx}" y="${sy + 14}" fill="${t.text4}" font-size="11" font-family=${F}># top models</text>`;
271
+ topModels.forEach((m, i) => {
272
+ const my = sy + 32 + i * 22;
273
+ const mName = m.name.length > 28 ? m.name.substring(0, 28) + '…' : m.name;
274
+ out += `<text x="${sx}" y="${my}" fill="${t.text2}" font-size="11" font-family=${F}>${esc(mName)}</text>`;
275
+ out += `<text x="${sx + sw - 4}" y="${my}" fill="${t.text4}" font-size="11" font-family=${F} text-anchor="end">${m.count}</text>`;
276
+ });
277
+ }
278
+ return out;
279
+ }
280
+
281
+ // ── Token footer line ──
282
+ let tokenLine = '';
283
+ if (show.tokens) {
284
+ const ty = curY + 4;
285
+ const tkPairs = [
286
+ ['input', fmt(tk.input)],
287
+ ['output', fmt(tk.output)],
288
+ ['cache read', fmt(tk.cacheRead)],
289
+ ['cache write', fmt(tk.cacheWrite)],
290
+ ['tools', fmt(stats.totalToolCalls || 0)],
291
+ ['editors', String(editors.length)],
292
+ ];
293
+ let tkX = pad;
294
+ let tkSvg = `<line x1="${pad}" y1="${ty}" x2="${W - pad}" y2="${ty}" stroke="${t.border}" stroke-width="1"/>`;
295
+ tkPairs.forEach(([label, val]) => {
296
+ tkSvg += `<text x="${tkX}" y="${ty + 20}" fill="${t.text5}" font-size="10" font-family=${F}>${label}</text>`;
297
+ tkSvg += `<text x="${tkX}" y="${ty + 34}" fill="${t.text2}" font-size="12" font-weight="600" font-family=${F}>${val}</text>`;
298
+ tkX += (W - pad * 2) / tkPairs.length;
299
+ });
300
+ tokenLine = tkSvg;
301
+ curY = ty + tokenFooterH;
302
+ }
303
+
304
+ // ── Final footer — pin to bottom of canvas ──
305
+ const footerY = H_FIXED - 28;
306
+
307
+ const usernameText = username ? `<text x="${W / 2}" y="${footerY + 14}" fill="${t.text5}" font-size="11" font-family=${F} text-anchor="middle">${esc(username)}</text>` : '';
308
+
309
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="675" viewBox="0 0 ${W} ${H_FIXED}">
310
+ <defs>
311
+ <linearGradient id="accentGrad" x1="0" y1="0" x2="1" y2="0">
312
+ <stop offset="0%" stop-color="#818cf8"/>
313
+ <stop offset="50%" stop-color="#a78bfa"/>
314
+ <stop offset="100%" stop-color="#c084fc"/>
315
+ </linearGradient>
316
+ </defs>
317
+
318
+ <!-- Background -->
319
+ <rect width="${W}" height="${H_FIXED}" rx="12" fill="${t.bg}"/>
320
+ <rect x="0.5" y="0.5" width="${W - 1}" height="${H_FIXED - 1}" rx="12" fill="none" stroke="${t.border}" stroke-width="1"/>
321
+
322
+ <!-- Title bar -->
323
+ <rect x="1" y="1" width="${W - 2}" height="38" rx="12" fill="${t.bg2}"/>
324
+ <rect x="1" y="22" width="${W - 2}" height="17" fill="${t.bg2}"/>
325
+ <circle cx="20" cy="20" r="5" fill="#ef4444" opacity="0.7"/>
326
+ <circle cx="36" cy="20" r="5" fill="#f59e0b" opacity="0.7"/>
327
+ <circle cx="52" cy="20" r="5" fill="#22c55e" opacity="0.7"/>
328
+ <text x="${W / 2}" y="25" fill="${t.text2}" font-size="13" font-weight="600" font-family=${F} text-anchor="middle">agentlytics.io</text>
329
+
330
+ <!-- Accent line -->
331
+ <rect x="${pad}" y="44" width="60" height="2" rx="1" fill="url(#accentGrad)"/>
332
+
333
+ <!-- Branding -->
334
+ <g transform="translate(${pad}, 52) scale(0.8)" stroke="#818cf8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none">
335
+ <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"/>
336
+ </g>
337
+ <text x="${pad + 26}" y="68" fill="${t.text}" font-size="16" font-weight="bold" font-family=${F}>Agentlytics</text>
338
+ <text x="${pad + 148}" y="68" fill="${t.text4}" font-size="13" font-family=${F}>${projectName ? esc(projectName) : 'Your AI coding stats'}</text>
339
+ <text x="${W - pad}" y="68" fill="${t.text5}" font-size="12" font-family=${F} text-anchor="end">${esc(dateStr)}</text>
340
+
341
+ <!-- Divider -->
342
+ <line x1="${pad}" y1="78" x2="${W - pad}" y2="78" stroke="${t.border}" stroke-width="1"/>
343
+
344
+ <!-- KPI cards -->
345
+ ${kpiCards.join('')}
346
+
347
+ <!-- Sections -->
348
+ ${sectionSvgs.join('')}
349
+
350
+ <!-- Token footer -->
351
+ ${tokenLine}
352
+
353
+ <!-- Footer -->
354
+ <line x1="${pad}" y1="${footerY}" x2="${W - pad}" y2="${footerY}" stroke="${t.border}" stroke-width="1"/>
355
+ <text x="${pad}" y="${footerY + 14}" fill="${t.text5}" font-size="11" font-family=${F}>github.com/f/agentlytics</text>
356
+ ${usernameText}
357
+ <text x="${W - pad}" y="${footerY + 14}" fill="${t.text5}" font-size="11" font-family=${F} text-anchor="end">npx agentlytics</text>
358
+ </svg>`;
359
+
360
+ return svg;
361
+ }
362
+
363
+ function esc(str) {
364
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
365
+ }
366
+
367
+ module.exports = { generateShareSvg };
@@ -0,0 +1,29 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
package/ui/index.html ADDED
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Agentlytics — AI Agent Analytics</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ <script type="module" src="/src/main.jsx"></script>
14
+ </body>
15
+ </html>