@agent-analytics/cli 0.1.9 → 0.1.11

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 (3) hide show
  1. package/bin/cli.mjs +343 -286
  2. package/lib/api.mjs +37 -25
  3. package/package.json +1 -1
package/bin/cli.mjs CHANGED
@@ -18,6 +18,13 @@
18
18
  * npx @agent-analytics/cli init <name> — Alias for create
19
19
  * npx @agent-analytics/cli delete <id> — Delete a project
20
20
  * npx @agent-analytics/cli revoke-key — Revoke and regenerate API key
21
+ * npx @agent-analytics/cli experiments list <project> — List experiments
22
+ * npx @agent-analytics/cli experiments create <p> ... — Create experiment
23
+ * npx @agent-analytics/cli experiments get <id> — Get experiment with results
24
+ * npx @agent-analytics/cli experiments pause <id> — Pause experiment
25
+ * npx @agent-analytics/cli experiments resume <id> — Resume experiment
26
+ * npx @agent-analytics/cli experiments complete <id> — Complete experiment
27
+ * npx @agent-analytics/cli experiments delete <id> — Delete experiment
21
28
  * npx @agent-analytics/cli delete-account — Delete your account (opens dashboard)
22
29
  * npx @agent-analytics/cli whoami — Show current account
23
30
  */
@@ -47,6 +54,22 @@ function requireKey() {
47
54
  return new AgentAnalyticsAPI(key, getBaseUrl());
48
55
  }
49
56
 
57
+ function withApi(fn) {
58
+ return async (...args) => {
59
+ const api = requireKey();
60
+ try {
61
+ return await fn(api, ...args);
62
+ } catch (err) {
63
+ error(err.message);
64
+ }
65
+ };
66
+ }
67
+
68
+ function ifEmpty(arr, label) {
69
+ if (!arr || arr.length === 0) { log(` No ${label} found.`); return true; }
70
+ return false;
71
+ }
72
+
50
73
  // ==================== COMMANDS ====================
51
74
 
52
75
  async function cmdLogin(token) {
@@ -82,343 +105,257 @@ async function cmdLogin(token) {
82
105
  }
83
106
  }
84
107
 
85
- async function cmdCreate(name, domain) {
108
+ const cmdCreate = withApi(async (api, name, domain) => {
86
109
  if (!name) error('Usage: npx @agent-analytics/cli create <project-name> --domain https://mysite.com');
87
110
  if (!domain) error('Usage: npx @agent-analytics/cli create <project-name> --domain https://mysite.com\n\nThe domain is required so we can restrict tracking to your site.');
88
111
 
89
- const api = requireKey();
90
-
91
112
  heading(`Creating project: ${name}`);
92
113
 
93
- try {
94
- const data = await api.createProject(name, domain);
95
-
96
- success(data.existing
97
- ? `Found existing project for ${BOLD}${domain}${RESET}!\n`
98
- : `Project created for ${BOLD}${domain}${RESET}!\n`);
114
+ const data = await api.createProject(name, domain);
99
115
 
100
- heading('1. Add this snippet to your site:');
101
- log(`${CYAN}${data.snippet}${RESET}\n`);
116
+ success(data.existing
117
+ ? `Found existing project for ${BOLD}${domain}${RESET}!\n`
118
+ : `Project created for ${BOLD}${domain}${RESET}!\n`);
102
119
 
103
- heading('2. Your agent queries stats with:');
104
- log(`${CYAN}${data.api_example}${RESET}\n`);
120
+ heading('1. Add this snippet to your site:');
121
+ log(`${CYAN}${data.snippet}${RESET}\n`);
105
122
 
106
- heading('Project token (for the snippet):');
107
- log(`${YELLOW}${data.project_token}${RESET}\n`);
123
+ heading('2. Your agent queries stats with:');
124
+ log(`${CYAN}${data.api_example}${RESET}\n`);
108
125
 
109
- } catch (err) {
110
- error(`Failed to create project: ${err.message}`);
111
- }
112
- }
126
+ heading('Project token (for the snippet):');
127
+ log(`${YELLOW}${data.project_token}${RESET}\n`);
128
+ });
113
129
 
114
- async function cmdProjects() {
115
- const api = requireKey();
130
+ const cmdProjects = withApi(async (api) => {
131
+ const data = await api.listProjects();
132
+ const projects = data.projects;
116
133
 
117
- try {
118
- const data = await api.listProjects();
119
- const projects = data.projects;
134
+ if (!projects || projects.length === 0) {
135
+ log('No projects yet. Create one:');
136
+ log(` ${CYAN}npx @agent-analytics/cli create my-site${RESET}`);
137
+ return;
138
+ }
120
139
 
121
- if (!projects || projects.length === 0) {
122
- log('No projects yet. Create one:');
123
- log(` ${CYAN}npx @agent-analytics/cli create my-site${RESET}`);
124
- return;
125
- }
140
+ heading(`Your Projects (${projects.length})`);
141
+ log('');
126
142
 
127
- heading(`Your Projects (${projects.length})`);
143
+ for (const p of projects) {
144
+ const created = new Date(p.created_at).toLocaleDateString();
145
+ log(` ${BOLD}${p.name}${RESET} ${DIM}created ${created}${RESET}`);
146
+ log(` ${DIM}token:${RESET} ${p.project_token}`);
147
+ log(` ${DIM}origins:${RESET} ${p.allowed_origins || '*'}`);
128
148
  log('');
129
-
130
- for (const p of projects) {
131
- const created = new Date(p.created_at).toLocaleDateString();
132
- log(` ${BOLD}${p.name}${RESET} ${DIM}created ${created}${RESET}`);
133
- log(` ${DIM}token:${RESET} ${p.project_token}`);
134
- log(` ${DIM}origins:${RESET} ${p.allowed_origins || '*'}`);
135
- log('');
136
- }
137
- } catch (err) {
138
- error(`Failed to list projects: ${err.message}`);
139
149
  }
140
- }
150
+ });
141
151
 
142
- async function cmdStats(project, days = 7) {
152
+ const cmdStats = withApi(async (api, project, days = 7) => {
143
153
  if (!project) error('Usage: npx @agent-analytics/cli stats <project-name> [--days N]');
144
154
 
145
- const api = requireKey();
155
+ const result = await api.getStats(project, days, { returnHeaders: true });
156
+ const data = result.data;
157
+ const headers = result.headers;
146
158
 
147
- try {
148
- const result = await api.getStats(project, days, { returnHeaders: true });
149
- const data = result.data;
150
- const headers = result.headers;
151
-
152
- heading(`Stats: ${project} (last ${days} days)`);
153
- log('');
154
-
155
- if (data.totals) {
156
- log(` ${BOLD}Total events:${RESET} ${data.totals.total_events || 0}`);
157
- log(` ${BOLD}Unique users:${RESET} ${data.totals.unique_users || 0}`);
158
- }
159
+ heading(`Stats: ${project} (last ${days} days)`);
160
+ log('');
159
161
 
160
- if (data.events && data.events.length > 0) {
161
- log('');
162
- heading('Events:');
163
- for (const e of data.events) {
164
- log(` ${e.event} ${DIM}→${RESET} ${BOLD}${e.count}${RESET} ${DIM}(${e.unique_users} users)${RESET}`);
165
- }
166
- }
162
+ if (data.totals) {
163
+ log(` ${BOLD}Total events:${RESET} ${data.totals.total_events || 0}`);
164
+ log(` ${BOLD}Unique users:${RESET} ${data.totals.unique_users || 0}`);
165
+ }
167
166
 
168
- if (data.daily && data.daily.length > 0) {
169
- log('');
170
- heading('Daily:');
171
- for (const d of data.daily) {
172
- const bar = '█'.repeat(Math.min(Math.ceil(d.total_events / 5), 40));
173
- log(` ${d.date} ${GREEN}${bar}${RESET} ${d.total_events} events`);
174
- }
167
+ if (data.events && data.events.length > 0) {
168
+ log('');
169
+ heading('Events:');
170
+ for (const e of data.events) {
171
+ log(` ${e.event} ${DIM}→${RESET} ${BOLD}${e.count}${RESET} ${DIM}(${e.unique_users} users)${RESET}`);
175
172
  }
173
+ }
176
174
 
177
- // Monthly usage summary from response headers
178
- const monthlyUsage = headers['x-monthly-usage'];
179
- if (monthlyUsage) {
180
- const events = parseInt(monthlyUsage, 10);
181
- const bill = (events / 1000) * 2;
182
- const monthlyLimit = headers['x-monthly-limit'];
183
- const pct = headers['x-monthly-usage-percent'];
184
- log('');
185
- if (monthlyLimit && pct) {
186
- const capDollars = (parseInt(monthlyLimit, 10) / 1000) * 2;
187
- log(` ${DIM}Monthly usage:${RESET} ${events.toLocaleString()} events ($${bill.toFixed(2)}) — ${pct}% of $${capDollars.toFixed(2)} cap`);
188
- } else {
189
- log(` ${DIM}Monthly usage:${RESET} ${events.toLocaleString()} events ($${bill.toFixed(2)})`);
190
- }
175
+ if (data.daily && data.daily.length > 0) {
176
+ log('');
177
+ heading('Daily:');
178
+ for (const d of data.daily) {
179
+ const bar = '█'.repeat(Math.min(Math.ceil(d.total_events / 5), 40));
180
+ log(` ${d.date} ${GREEN}${bar}${RESET} ${d.total_events} events`);
191
181
  }
182
+ }
192
183
 
184
+ // Monthly usage summary from response headers
185
+ const monthlyUsage = headers['x-monthly-usage'];
186
+ if (monthlyUsage) {
187
+ const events = parseInt(monthlyUsage, 10);
188
+ const bill = (events / 1000) * 2;
189
+ const monthlyLimit = headers['x-monthly-limit'];
190
+ const pct = headers['x-monthly-usage-percent'];
193
191
  log('');
194
- } catch (err) {
195
- error(`Failed to get stats: ${err.message}`);
192
+ if (monthlyLimit && pct) {
193
+ const capDollars = (parseInt(monthlyLimit, 10) / 1000) * 2;
194
+ log(` ${DIM}Monthly usage:${RESET} ${events.toLocaleString()} events ($${bill.toFixed(2)}) — ${pct}% of $${capDollars.toFixed(2)} cap`);
195
+ } else {
196
+ log(` ${DIM}Monthly usage:${RESET} ${events.toLocaleString()} events ($${bill.toFixed(2)})`);
197
+ }
196
198
  }
197
- }
198
199
 
199
- async function cmdEvents(project, opts = {}) {
200
- if (!project) error('Usage: npx @agent-analytics/cli events <project-name> [--days N] [--limit N]');
200
+ log('');
201
+ });
201
202
 
202
- const api = requireKey();
203
+ const cmdEvents = withApi(async (api, project, opts = {}) => {
204
+ if (!project) error('Usage: npx @agent-analytics/cli events <project-name> [--days N] [--limit N]');
203
205
 
204
- try {
205
- const data = await api.getEvents(project, opts);
206
+ const data = await api.getEvents(project, opts);
206
207
 
207
- heading(`Events: ${project}`);
208
- log('');
208
+ heading(`Events: ${project}`);
209
+ log('');
209
210
 
210
- if (!data.events || data.events.length === 0) {
211
- log(' No events yet.');
212
- return;
213
- }
211
+ if (ifEmpty(data.events, 'events')) return;
214
212
 
215
- for (const e of data.events) {
216
- const time = new Date(e.timestamp).toLocaleString();
217
- log(` ${DIM}${time}${RESET} ${BOLD}${e.event}${RESET} ${DIM}${e.user_id || ''}${RESET}`);
218
- if (e.properties) {
219
- log(` ${DIM}${JSON.stringify(e.properties)}${RESET}`);
220
- }
213
+ for (const e of data.events) {
214
+ const time = new Date(e.timestamp).toLocaleString();
215
+ log(` ${DIM}${time}${RESET} ${BOLD}${e.event}${RESET} ${DIM}${e.user_id || ''}${RESET}`);
216
+ if (e.properties) {
217
+ log(` ${DIM}${JSON.stringify(e.properties)}${RESET}`);
221
218
  }
222
- log('');
223
- } catch (err) {
224
- error(`Failed to get events: ${err.message}`);
225
219
  }
226
- }
220
+ log('');
221
+ });
227
222
 
228
- async function cmdPropertiesReceived(project, opts = {}) {
223
+ const cmdPropertiesReceived = withApi(async (api, project, opts = {}) => {
229
224
  if (!project) error('Usage: npx @agent-analytics/cli properties-received <project-name> [--since DATE] [--sample N]');
230
225
 
231
- const api = requireKey();
226
+ const data = await api.getPropertiesReceived(project, opts);
232
227
 
233
- try {
234
- const data = await api.getPropertiesReceived(project, opts);
235
-
236
- heading(`Received Properties: ${project}`);
237
- log('');
228
+ heading(`Received Properties: ${project}`);
229
+ log('');
238
230
 
239
- if (!data.properties || data.properties.length === 0) {
240
- log(' No properties found.');
241
- return;
242
- }
231
+ if (ifEmpty(data.properties, 'properties')) return;
243
232
 
244
- // Group by event for display
245
- const byEvent = {};
246
- for (const p of data.properties) {
247
- if (!byEvent[p.event]) byEvent[p.event] = [];
248
- byEvent[p.event].push(p.key);
249
- }
233
+ // Group by event for display
234
+ const byEvent = {};
235
+ for (const p of data.properties) {
236
+ if (!byEvent[p.event]) byEvent[p.event] = [];
237
+ byEvent[p.event].push(p.key);
238
+ }
250
239
 
251
- for (const [event, keys] of Object.entries(byEvent)) {
252
- log(` ${BOLD}${event}${RESET}`);
253
- for (const key of keys) {
254
- log(` ${CYAN}${key}${RESET}`);
255
- }
240
+ for (const [event, keys] of Object.entries(byEvent)) {
241
+ log(` ${BOLD}${event}${RESET}`);
242
+ for (const key of keys) {
243
+ log(` ${CYAN}${key}${RESET}`);
256
244
  }
257
-
258
- log(`\n${DIM}Sampled from last ${data.sample_size} events${RESET}`);
259
- log('');
260
- } catch (err) {
261
- error(`Failed to get properties: ${err.message}`);
262
245
  }
263
- }
264
-
265
- async function cmdInsights(project, period = '7d') {
266
- if (!project) error('Usage: npx @agent-analytics/cli insights <project-name> [--period 7d]');
267
246
 
268
- const api = requireKey();
247
+ log(`\n${DIM}Sampled from last ${data.sample_size} events${RESET}`);
248
+ log('');
249
+ });
269
250
 
270
- try {
271
- const data = await api.getInsights(project, { period });
251
+ const cmdInsights = withApi(async (api, project, period = '7d') => {
252
+ if (!project) error('Usage: npx @agent-analytics/cli insights <project-name> [--period 7d]');
272
253
 
273
- heading(`Insights: ${project} (${period} vs previous)`);
274
- log('');
254
+ const data = await api.getInsights(project, { period });
275
255
 
276
- const m = data.metrics;
277
- for (const [key, metric] of Object.entries(m)) {
278
- const label = key.replace(/_/g, ' ');
279
- const arrow = metric.change > 0 ? `${GREEN}↑` : metric.change < 0 ? `${RED}↓` : `${DIM}—`;
280
- const pct = metric.change_pct !== null ? ` (${metric.change_pct > 0 ? '+' : ''}${metric.change_pct}%)` : '';
281
- log(` ${BOLD}${label}:${RESET} ${metric.current} ${arrow}${pct}${RESET} ${DIM}was ${metric.previous}${RESET}`);
282
- }
256
+ heading(`Insights: ${project} (${period} vs previous)`);
257
+ log('');
283
258
 
284
- log('');
285
- log(` ${BOLD}Trend:${RESET} ${data.trend}`);
286
- log('');
287
- } catch (err) {
288
- error(`Failed to get insights: ${err.message}`);
259
+ const m = data.metrics;
260
+ for (const [key, metric] of Object.entries(m)) {
261
+ const label = key.replace(/_/g, ' ');
262
+ const arrow = metric.change > 0 ? `${GREEN}↑` : metric.change < 0 ? `${RED}↓` : `${DIM}—`;
263
+ const pct = metric.change_pct !== null ? ` (${metric.change_pct > 0 ? '+' : ''}${metric.change_pct}%)` : '';
264
+ log(` ${BOLD}${label}:${RESET} ${metric.current} ${arrow}${pct}${RESET} ${DIM}was ${metric.previous}${RESET}`);
289
265
  }
290
- }
291
266
 
292
- async function cmdBreakdown(project, property, opts = {}) {
293
- if (!project || !property) error('Usage: npx @agent-analytics/cli breakdown <project-name> --property <key> [--event page_view] [--limit 20]');
267
+ log('');
268
+ log(` ${BOLD}Trend:${RESET} ${data.trend}`);
269
+ log('');
270
+ });
294
271
 
295
- const api = requireKey();
272
+ const cmdBreakdown = withApi(async (api, project, property, opts = {}) => {
273
+ if (!project || !property) error('Usage: npx @agent-analytics/cli breakdown <project-name> --property <key> [--event page_view] [--limit 20]');
296
274
 
297
- try {
298
- const data = await api.getBreakdown(project, { property, ...opts });
275
+ const data = await api.getBreakdown(project, { property, ...opts });
299
276
 
300
- heading(`Breakdown: ${project} — ${property}${data.event ? ` (${data.event})` : ''}`);
301
- log('');
277
+ heading(`Breakdown: ${project} — ${property}${data.event ? ` (${data.event})` : ''}`);
278
+ log('');
302
279
 
303
- if (!data.values || data.values.length === 0) {
304
- log(' No data found.');
305
- return;
306
- }
280
+ if (ifEmpty(data.values, 'data')) return;
307
281
 
308
- for (const v of data.values) {
309
- log(` ${BOLD}${v.value}${RESET} ${v.count} events ${DIM}(${v.unique_users} users)${RESET}`);
310
- }
311
- log(`\n${DIM}${data.total_with_property} of ${data.total_events} events have this property${RESET}`);
312
- log('');
313
- } catch (err) {
314
- error(`Failed to get breakdown: ${err.message}`);
282
+ for (const v of data.values) {
283
+ log(` ${BOLD}${v.value}${RESET} ${v.count} events ${DIM}(${v.unique_users} users)${RESET}`);
315
284
  }
316
- }
285
+ log(`\n${DIM}${data.total_with_property} of ${data.total_events} events have this property${RESET}`);
286
+ log('');
287
+ });
317
288
 
318
- async function cmdPages(project, type = 'entry', opts = {}) {
289
+ const cmdPages = withApi(async (api, project, type = 'entry', opts = {}) => {
319
290
  if (!project) error('Usage: npx @agent-analytics/cli pages <project-name> [--type entry|exit|both] [--limit 20]');
320
291
 
321
- const api = requireKey();
292
+ const data = await api.getPages(project, { type, ...opts });
322
293
 
323
- try {
324
- const data = await api.getPages(project, { type, ...opts });
325
-
326
- heading(`Pages: ${project} (${type})`);
327
- log('');
294
+ heading(`Pages: ${project} (${type})`);
295
+ log('');
328
296
 
329
- const pages = data.entry_pages || data.exit_pages || [];
330
- if (pages.length === 0) {
331
- log(' No page data found.');
332
- return;
333
- }
297
+ const pages = data.entry_pages || data.exit_pages || [];
298
+ if (ifEmpty(pages, 'page data')) return;
334
299
 
335
- for (const p of pages) {
336
- const bounceStr = `${Math.round(p.bounce_rate * 100)}% bounce`;
337
- const durStr = `${Math.round(p.avg_duration / 1000)}s avg`;
338
- log(` ${BOLD}${p.page}${RESET} ${p.sessions} sessions ${DIM}${bounceStr} ${durStr} ${p.avg_events} events/session${RESET}`);
339
- }
300
+ for (const p of pages) {
301
+ const bounceStr = `${Math.round(p.bounce_rate * 100)}% bounce`;
302
+ const durStr = `${Math.round(p.avg_duration / 1000)}s avg`;
303
+ log(` ${BOLD}${p.page}${RESET} ${p.sessions} sessions ${DIM}${bounceStr} ${durStr} ${p.avg_events} events/session${RESET}`);
304
+ }
340
305
 
341
- if (data.exit_pages && data.entry_pages) {
342
- log('');
343
- heading('Exit pages:');
344
- for (const p of data.exit_pages) {
345
- log(` ${BOLD}${p.page}${RESET} ${p.sessions} sessions`);
346
- }
347
- }
306
+ if (data.exit_pages && data.entry_pages) {
348
307
  log('');
349
- } catch (err) {
350
- error(`Failed to get pages: ${err.message}`);
308
+ heading('Exit pages:');
309
+ for (const p of data.exit_pages) {
310
+ log(` ${BOLD}${p.page}${RESET} ${p.sessions} sessions`);
311
+ }
351
312
  }
352
- }
313
+ log('');
314
+ });
353
315
 
354
- async function cmdSessionsDist(project) {
316
+ const cmdSessionsDist = withApi(async (api, project) => {
355
317
  if (!project) error('Usage: npx @agent-analytics/cli sessions-dist <project-name>');
356
318
 
357
- const api = requireKey();
319
+ const data = await api.getSessionDistribution(project);
358
320
 
359
- try {
360
- const data = await api.getSessionDistribution(project);
361
-
362
- heading(`Session Distribution: ${project}`);
363
- log('');
321
+ heading(`Session Distribution: ${project}`);
322
+ log('');
364
323
 
365
- if (!data.distribution || data.distribution.length === 0) {
366
- log(' No session data found.');
367
- return;
368
- }
324
+ if (ifEmpty(data.distribution, 'session data')) return;
369
325
 
370
- for (const b of data.distribution) {
371
- const bar = '█'.repeat(Math.min(Math.ceil(b.pct / 2), 40));
372
- log(` ${b.bucket.padEnd(7)} ${GREEN}${bar}${RESET} ${b.sessions} (${b.pct}%)`);
373
- }
374
-
375
- log('');
376
- log(` ${BOLD}Median:${RESET} ${data.median_bucket} ${BOLD}Engaged:${RESET} ${data.engaged_pct}% (sessions ≥30s)`);
377
- log('');
378
- } catch (err) {
379
- error(`Failed to get session distribution: ${err.message}`);
326
+ for (const b of data.distribution) {
327
+ const bar = '█'.repeat(Math.min(Math.ceil(b.pct / 2), 40));
328
+ log(` ${b.bucket.padEnd(7)} ${GREEN}${bar}${RESET} ${b.sessions} (${b.pct}%)`);
380
329
  }
381
- }
382
330
 
383
- async function cmdHeatmap(project) {
384
- if (!project) error('Usage: npx @agent-analytics/cli heatmap <project-name>');
331
+ log('');
332
+ log(` ${BOLD}Median:${RESET} ${data.median_bucket} ${BOLD}Engaged:${RESET} ${data.engaged_pct}% (sessions ≥30s)`);
333
+ log('');
334
+ });
385
335
 
386
- const api = requireKey();
336
+ const cmdHeatmap = withApi(async (api, project) => {
337
+ if (!project) error('Usage: npx @agent-analytics/cli heatmap <project-name>');
387
338
 
388
- try {
389
- const data = await api.getHeatmap(project);
339
+ const data = await api.getHeatmap(project);
390
340
 
391
- heading(`Heatmap: ${project}`);
392
- log('');
341
+ heading(`Heatmap: ${project}`);
342
+ log('');
393
343
 
394
- if (!data.heatmap || data.heatmap.length === 0) {
395
- log(' No heatmap data found.');
396
- return;
397
- }
344
+ if (ifEmpty(data.heatmap, 'heatmap data')) return;
398
345
 
399
- if (data.peak) {
400
- log(` ${BOLD}Peak:${RESET} ${data.peak.day_name} at ${data.peak.hour}:00 (${data.peak.events} events, ${data.peak.users} users)`);
401
- }
402
- log(` ${BOLD}Busiest day:${RESET} ${data.busiest_day}`);
403
- log(` ${BOLD}Busiest hour:${RESET} ${data.busiest_hour}:00`);
404
- log('');
405
- } catch (err) {
406
- error(`Failed to get heatmap: ${err.message}`);
346
+ if (data.peak) {
347
+ log(` ${BOLD}Peak:${RESET} ${data.peak.day_name} at ${data.peak.hour}:00 (${data.peak.events} events, ${data.peak.users} users)`);
407
348
  }
408
- }
349
+ log(` ${BOLD}Busiest day:${RESET} ${data.busiest_day}`);
350
+ log(` ${BOLD}Busiest hour:${RESET} ${data.busiest_hour}:00`);
351
+ log('');
352
+ });
409
353
 
410
- async function cmdDelete(id) {
354
+ const cmdDelete = withApi(async (api, id) => {
411
355
  if (!id) error('Usage: npx @agent-analytics/cli delete <project-id>');
412
-
413
- const api = requireKey();
414
-
415
- try {
416
- await api.deleteProject(id);
417
- success(`Project ${id} deleted`);
418
- } catch (err) {
419
- error(`Failed to delete project: ${err.message}`);
420
- }
421
- }
356
+ await api.deleteProject(id);
357
+ success(`Project ${id} deleted`);
358
+ });
422
359
 
423
360
  function cmdDeleteAccount() {
424
361
  heading('Delete Account');
@@ -428,42 +365,146 @@ function cmdDeleteAccount() {
428
365
  log('');
429
366
  }
430
367
 
431
- async function cmdRevokeKey() {
432
- const api = requireKey();
433
-
434
- try {
435
- const data = await api.revokeKey();
436
- setApiKey(data.api_key);
437
-
438
- warn('Old API key revoked');
439
- success('New API key generated and saved\n');
440
- heading('New API key:');
441
- log(`${YELLOW}${data.api_key}${RESET}`);
442
- log(`${DIM}Saved to ~/.config/agent-analytics/config.json${RESET}\n`);
443
- warn('Update your agent with this new key!');
444
- } catch (err) {
445
- error(`Failed to revoke key: ${err.message}`);
368
+ const cmdRevokeKey = withApi(async (api) => {
369
+ const data = await api.revokeKey();
370
+ setApiKey(data.api_key);
371
+
372
+ warn('Old API key revoked');
373
+ success('New API key generated and saved\n');
374
+ heading('New API key:');
375
+ log(`${YELLOW}${data.api_key}${RESET}`);
376
+ log(`${DIM}Saved to ~/.config/agent-analytics/config.json${RESET}\n`);
377
+ warn('Update your agent with this new key!');
378
+ });
379
+
380
+ const cmdWhoami = withApi(async (api) => {
381
+ const data = await api.getAccount();
382
+ heading('Account');
383
+ log(` ${BOLD}Email:${RESET} ${data.email}`);
384
+ log(` ${BOLD}GitHub:${RESET} ${data.github_login || 'N/A'}`);
385
+ log(` ${BOLD}Tier:${RESET} ${data.tier}`);
386
+ log(` ${BOLD}Projects:${RESET} ${data.projects_count}/${data.projects_limit}`);
387
+ if (data.tier === 'pro' && data.monthly_spend_cap_dollars != null) {
388
+ log(` ${BOLD}Spend cap:${RESET} $${data.monthly_spend_cap_dollars.toFixed(2)}/month`);
446
389
  }
447
- }
390
+ log('');
391
+ });
448
392
 
449
- async function cmdWhoami() {
450
- const api = requireKey();
393
+ // ==================== EXPERIMENTS ====================
451
394
 
452
- try {
453
- const data = await api.getAccount();
454
- heading('Account');
455
- log(` ${BOLD}Email:${RESET} ${data.email}`);
456
- log(` ${BOLD}GitHub:${RESET} ${data.github_login || 'N/A'}`);
457
- log(` ${BOLD}Tier:${RESET} ${data.tier}`);
458
- log(` ${BOLD}Projects:${RESET} ${data.projects_count}/${data.projects_limit}`);
459
- if (data.tier === 'pro' && data.monthly_spend_cap_dollars != null) {
460
- log(` ${BOLD}Spend cap:${RESET} $${data.monthly_spend_cap_dollars.toFixed(2)}/month`);
395
+ const cmdExperiments = withApi(async (api, sub, ...rest) => {
396
+ if (!sub) error('Usage: npx @agent-analytics/cli experiments <list|create|get|pause|resume|complete|delete> ...');
397
+
398
+ switch (sub) {
399
+ case 'list': {
400
+ const project = rest[0];
401
+ if (!project) error('Usage: npx @agent-analytics/cli experiments list <project>');
402
+ const data = await api.listExperiments(project);
403
+ heading(`Experiments: ${project}`);
404
+ log('');
405
+ if (ifEmpty(data.experiments, 'experiments')) return;
406
+ for (const e of data.experiments) {
407
+ const status = e.status === 'active' ? `${GREEN}active${RESET}` : e.status === 'paused' ? `${YELLOW}paused${RESET}` : `${DIM}completed${RESET}`;
408
+ log(` ${BOLD}${e.name}${RESET} ${DIM}${e.id}${RESET} ${status} ${DIM}goal: ${e.goal_event}${RESET}`);
409
+ const variants = e.variants.map(v => `${v.key}(${v.weight}%)`).join(', ');
410
+ log(` variants: ${variants}`);
411
+ if (e.winner) log(` ${GREEN}winner: ${e.winner}${RESET}`);
412
+ }
413
+ log('');
414
+ break;
461
415
  }
462
- log('');
463
- } catch (err) {
464
- error(`Failed to get account: ${err.message}`);
416
+ case 'create': {
417
+ const project = rest[0];
418
+ if (!project) error('Usage: npx @agent-analytics/cli experiments create <project> --name <name> --variants control,new_cta --goal <event> [--weights 60,40]');
419
+ const name = getArg('--name');
420
+ const variantsStr = getArg('--variants');
421
+ const goal = getArg('--goal');
422
+ if (!name || !variantsStr || !goal) error('Required: --name, --variants, --goal');
423
+ const variants = variantsStr.split(',').map(v => v.trim());
424
+ const weightsStr = getArg('--weights');
425
+ const weights = weightsStr ? weightsStr.split(',').map(w => parseInt(w.trim(), 10)) : undefined;
426
+ const data = await api.createExperiment(project, { name, variants, goal_event: goal, weights });
427
+ success(`Experiment created: ${BOLD}${data.name}${RESET} (${data.id})`);
428
+ log(` ${DIM}variants:${RESET} ${data.variants.map(v => `${v.key}(${v.weight}%)`).join(', ')}`);
429
+ log(` ${DIM}goal:${RESET} ${data.goal_event}`);
430
+ log('');
431
+ break;
432
+ }
433
+ case 'get': {
434
+ const id = rest[0];
435
+ if (!id) error('Usage: npx @agent-analytics/cli experiments get <id>');
436
+ const data = await api.getExperiment(id);
437
+ const status = data.status === 'active' ? `${GREEN}active${RESET}` : data.status === 'paused' ? `${YELLOW}paused${RESET}` : `${DIM}completed${RESET}`;
438
+ heading(`Experiment: ${data.name}`);
439
+ log(` ${DIM}id:${RESET} ${data.id} status: ${status} ${DIM}goal: ${data.goal_event}${RESET}`);
440
+ if (data.winner) log(` ${GREEN}winner: ${data.winner}${RESET}`);
441
+ log('');
442
+ if (data.results) {
443
+ heading('Results:');
444
+ for (const v of data.results.variants) {
445
+ const rate = (v.conversion_rate * 100).toFixed(1);
446
+ log(` ${BOLD}${v.key}${RESET} ${v.unique_users} users ${v.conversions} conversions ${CYAN}${rate}%${RESET}`);
447
+ }
448
+ log('');
449
+ if (data.results.probability_best) {
450
+ heading('Probability best:');
451
+ for (const [k, v] of Object.entries(data.results.probability_best)) {
452
+ const pct = (v * 100).toFixed(1);
453
+ log(` ${BOLD}${k}:${RESET} ${pct}%`);
454
+ }
455
+ }
456
+ if (data.results.lift) {
457
+ heading('Lift:');
458
+ for (const [k, v] of Object.entries(data.results.lift)) {
459
+ const pct = (v * 100).toFixed(1);
460
+ const arrow = v > 0 ? `${GREEN}+${pct}%` : v < 0 ? `${RED}${pct}%` : `${DIM}0%`;
461
+ log(` ${BOLD}${k}:${RESET} ${arrow}${RESET}`);
462
+ }
463
+ }
464
+ log('');
465
+ log(` ${BOLD}Sufficient data:${RESET} ${data.results.sufficient_data ? `${GREEN}yes` : `${YELLOW}no`}${RESET}`);
466
+ if (data.results.recommendation) {
467
+ log(` ${BOLD}Recommendation:${RESET} ${data.results.recommendation}`);
468
+ }
469
+ } else {
470
+ log(` ${DIM}No results available yet (need exposure + conversion events)${RESET}`);
471
+ }
472
+ log('');
473
+ break;
474
+ }
475
+ case 'pause': {
476
+ const id = rest[0];
477
+ if (!id) error('Usage: npx @agent-analytics/cli experiments pause <id>');
478
+ await api.updateExperiment(id, { status: 'paused' });
479
+ success(`Experiment ${id} paused`);
480
+ break;
481
+ }
482
+ case 'resume': {
483
+ const id = rest[0];
484
+ if (!id) error('Usage: npx @agent-analytics/cli experiments resume <id>');
485
+ await api.updateExperiment(id, { status: 'active' });
486
+ success(`Experiment ${id} resumed`);
487
+ break;
488
+ }
489
+ case 'complete': {
490
+ const id = rest[0];
491
+ if (!id) error('Usage: npx @agent-analytics/cli experiments complete <id> [--winner <variant>]');
492
+ const winner = getArg('--winner');
493
+ await api.updateExperiment(id, { status: 'completed', winner });
494
+ success(`Experiment ${id} completed${winner ? ` — winner: ${winner}` : ''}`);
495
+ break;
496
+ }
497
+ case 'delete': {
498
+ const id = rest[0];
499
+ if (!id) error('Usage: npx @agent-analytics/cli experiments delete <id>');
500
+ await api.deleteExperiment(id);
501
+ success(`Experiment ${id} deleted`);
502
+ break;
503
+ }
504
+ default:
505
+ error(`Unknown experiments subcommand: ${sub}. Use: list, create, get, pause, resume, complete, delete`);
465
506
  }
466
- }
507
+ });
467
508
 
468
509
  function showHelp() {
469
510
  log(`
@@ -486,6 +527,14 @@ ${BOLD}COMMANDS${RESET}
486
527
  ${CYAN}pages${RESET} <name> Entry/exit page performance
487
528
  ${CYAN}sessions-dist${RESET} <name> Session duration distribution
488
529
  ${CYAN}heatmap${RESET} <name> Peak hours & busiest days
530
+ ${CYAN}experiments${RESET} <sub> A/B testing (pro only)
531
+ ${DIM}list <project>${RESET} List experiments
532
+ ${DIM}create <project>${RESET} Create experiment
533
+ ${DIM}get <id>${RESET} Get experiment with results
534
+ ${DIM}pause <id>${RESET} Pause experiment
535
+ ${DIM}resume <id>${RESET} Resume experiment
536
+ ${DIM}complete <id>${RESET} Complete experiment
537
+ ${DIM}delete <id>${RESET} Delete experiment
489
538
  ${CYAN}whoami${RESET} Show current account
490
539
  ${CYAN}revoke-key${RESET} Revoke and regenerate API key
491
540
  ${CYAN}delete-account${RESET} Delete your account (opens dashboard)
@@ -500,6 +549,11 @@ ${BOLD}OPTIONS${RESET}
500
549
  --property <key> Property key for breakdown (required)
501
550
  --event <name> Filter by event name (breakdown only)
502
551
  --type <T> Page type: entry, exit, both (default: entry)
552
+ --name <name> Experiment name (experiments create)
553
+ --variants <a,b> Comma-separated variant keys (experiments create)
554
+ --goal <event> Goal event name (experiments create)
555
+ --weights <50,50> Comma-separated weights (experiments create, optional)
556
+ --winner <variant> Winning variant (experiments complete, optional)
503
557
 
504
558
  ${BOLD}ENVIRONMENT${RESET}
505
559
  AGENT_ANALYTICS_API_KEY API key (overrides config file)
@@ -581,6 +635,9 @@ try {
581
635
  case 'heatmap':
582
636
  await cmdHeatmap(args[1]);
583
637
  break;
638
+ case 'experiments':
639
+ await cmdExperiments(args[1], args[2]);
640
+ break;
584
641
  case 'delete':
585
642
  await cmdDelete(args[1]);
586
643
  break;
package/lib/api.mjs CHANGED
@@ -11,6 +11,13 @@ export class AgentAnalyticsAPI {
11
11
  this.baseUrl = baseUrl;
12
12
  }
13
13
 
14
+ _qs(params) {
15
+ return Object.entries(params)
16
+ .filter(([, v]) => v != null)
17
+ .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
18
+ .join('&');
19
+ }
20
+
14
21
  async request(method, path, body, { returnHeaders = false } = {}) {
15
22
  const opts = {
16
23
  method,
@@ -68,55 +75,60 @@ export class AgentAnalyticsAPI {
68
75
 
69
76
  // Stats
70
77
  async getStats(project, days = 7, { returnHeaders = false } = {}) {
71
- return this.request('GET', `/stats?project=${encodeURIComponent(project)}&days=${days}`, undefined, { returnHeaders });
78
+ return this.request('GET', `/stats?${this._qs({ project, days })}`, undefined, { returnHeaders });
72
79
  }
73
80
 
74
81
  async getEvents(project, { event, days = 7, limit = 100 } = {}) {
75
- let qs = `project=${encodeURIComponent(project)}&days=${days}&limit=${limit}`;
76
- if (event) qs += `&event=${encodeURIComponent(event)}`;
77
- return this.request('GET', `/events?${qs}`);
82
+ return this.request('GET', `/events?${this._qs({ project, days, limit, event })}`);
78
83
  }
79
84
 
80
85
  async getProperties(project, days = 30) {
81
- return this.request('GET', `/properties?project=${encodeURIComponent(project)}&days=${days}`);
86
+ return this.request('GET', `/properties?${this._qs({ project, days })}`);
82
87
  }
83
88
 
84
89
  async getPropertiesReceived(project, { since, sample } = {}) {
85
- let qs = `project=${encodeURIComponent(project)}`;
86
- if (since) qs += `&since=${encodeURIComponent(since)}`;
87
- if (sample) qs += `&sample=${sample}`;
88
- return this.request('GET', `/properties/received?${qs}`);
90
+ return this.request('GET', `/properties/received?${this._qs({ project, since, sample })}`);
89
91
  }
90
92
 
91
93
  // Analytics
92
94
  async getBreakdown(project, { property, event, since, limit = 20 } = {}) {
93
- let qs = `project=${encodeURIComponent(project)}&property=${encodeURIComponent(property)}`;
94
- if (event) qs += `&event=${encodeURIComponent(event)}`;
95
- if (since) qs += `&since=${encodeURIComponent(since)}`;
96
- if (limit) qs += `&limit=${limit}`;
97
- return this.request('GET', `/breakdown?${qs}`);
95
+ return this.request('GET', `/breakdown?${this._qs({ project, property, event, since, limit })}`);
98
96
  }
99
97
 
100
98
  async getInsights(project, { period = '7d' } = {}) {
101
- return this.request('GET', `/insights?project=${encodeURIComponent(project)}&period=${period}`);
99
+ return this.request('GET', `/insights?${this._qs({ project, period })}`);
102
100
  }
103
101
 
104
102
  async getPages(project, { type = 'entry', since, limit = 20 } = {}) {
105
- let qs = `project=${encodeURIComponent(project)}&type=${type}`;
106
- if (since) qs += `&since=${encodeURIComponent(since)}`;
107
- if (limit) qs += `&limit=${limit}`;
108
- return this.request('GET', `/pages?${qs}`);
103
+ return this.request('GET', `/pages?${this._qs({ project, type, since, limit })}`);
109
104
  }
110
105
 
111
106
  async getSessionDistribution(project, { since } = {}) {
112
- let qs = `project=${encodeURIComponent(project)}`;
113
- if (since) qs += `&since=${encodeURIComponent(since)}`;
114
- return this.request('GET', `/sessions/distribution?${qs}`);
107
+ return this.request('GET', `/sessions/distribution?${this._qs({ project, since })}`);
115
108
  }
116
109
 
117
110
  async getHeatmap(project, { since } = {}) {
118
- let qs = `project=${encodeURIComponent(project)}`;
119
- if (since) qs += `&since=${encodeURIComponent(since)}`;
120
- return this.request('GET', `/heatmap?${qs}`);
111
+ return this.request('GET', `/heatmap?${this._qs({ project, since })}`);
112
+ }
113
+
114
+ // Experiments
115
+ async createExperiment(project, { name, variants, goal_event, weights }) {
116
+ return this.request('POST', '/experiments', { project, name, variants, goal_event, weights });
117
+ }
118
+
119
+ async listExperiments(project) {
120
+ return this.request('GET', `/experiments?${this._qs({ project })}`);
121
+ }
122
+
123
+ async getExperiment(id) {
124
+ return this.request('GET', `/experiments/${id}`);
125
+ }
126
+
127
+ async updateExperiment(id, { status, winner }) {
128
+ return this.request('PATCH', `/experiments/${id}`, { status, winner });
129
+ }
130
+
131
+ async deleteExperiment(id) {
132
+ return this.request('DELETE', `/experiments/${id}`);
121
133
  }
122
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-analytics/cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Web analytics your AI agent can read. CLI for managing projects and querying stats.",
5
5
  "bin": {
6
6
  "agent-analytics": "./bin/cli.mjs"