primitive-admin 1.0.37 → 1.0.39

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.
@@ -1,49 +1,220 @@
1
1
  import { ApiClient } from "../lib/api-client.js";
2
2
  import { resolveAppId } from "../lib/config.js";
3
- import { error, info, keyValue, formatTable, formatId, formatDate, formatNumber, formatPercent, formatDuration, json, divider, heading, } from "../lib/output.js";
3
+ import { error, info, formatTable, formatId, formatDate, formatNumber, formatPercent, formatDuration, json, heading, keyValue, dim, } from "../lib/output.js";
4
+ /** Format a value that is already a percentage (e.g. 33.3 → "33.3%") */
5
+ function formatPctRaw(value) {
6
+ if (value === undefined || value === null)
7
+ return "-";
8
+ return `${value.toFixed(1)}%`;
9
+ }
4
10
  export function registerAnalyticsCommands(program) {
5
11
  const analytics = program
6
12
  .command("analytics")
7
13
  .description("View usage analytics, user activity, and integration metrics")
8
14
  .addHelpText("after", `
15
+ Commands:
16
+ overview DAU / WAU / MAU and growth accounting
17
+ daily-active Daily active users time series
18
+ rolling-active Rolling active users (28 points)
19
+ cohort-retention Weekly cohort retention matrix
20
+ top-users Most active users
21
+ user-search Search users by email or ULID
22
+ user-detail Detailed activity for a single user
23
+ user-snapshot Latest context snapshot for a user
24
+ events Paginated event feed
25
+ events-grouped Events grouped by a dimension
26
+ integrations Integration usage metrics
27
+ workflows Top workflows by runs
28
+ prompts Top prompts by executions
29
+
9
30
  Examples:
10
31
  $ primitive analytics overview
11
- $ primitive analytics top-users --window-days 7 --limit 20
12
- $ primitive analytics integrations
32
+ $ primitive analytics daily-active --window-days 14
33
+ $ primitive analytics top-users --limit 20
34
+ $ primitive analytics user-search --query user@example.com
35
+ $ primitive analytics user-detail <user-ulid>
36
+ $ primitive analytics events --window-days 7 --page 0
37
+ $ primitive analytics events-grouped --group-by feature
38
+ $ primitive analytics cohort-retention
39
+ $ primitive analytics workflows --limit 5
40
+ $ primitive analytics prompts --window-days 14
13
41
  `);
14
- // Overview
42
+ // ── Overview (DAU / WAU / MAU / Growth) ──────────────────────────
15
43
  analytics
16
44
  .command("overview")
17
- .description("Get analytics overview for an app")
45
+ .description("Show DAU, WAU, MAU, and growth accounting")
18
46
  .argument("[app-id]", "App ID (uses current app if not specified)")
19
47
  .option("--app <app-id>", "App ID")
20
- .option("--window-days <n>", "Time window in days", "30")
48
+ .option("--window-days <n>", "Growth window in days", "28")
21
49
  .option("--json", "Output as JSON")
22
50
  .action(async (appId, options) => {
23
51
  const resolvedAppId = resolveAppId(appId, options);
24
52
  const client = new ApiClient();
25
53
  try {
26
- const result = await client.getAnalyticsOverview(resolvedAppId, {
54
+ const [dau, wau, mau, growth] = await Promise.all([
55
+ client.getAnalyticsOverviewDau(resolvedAppId),
56
+ client.getAnalyticsOverviewWau(resolvedAppId),
57
+ client.getAnalyticsOverviewMau(resolvedAppId),
58
+ client.getAnalyticsOverviewGrowth(resolvedAppId, {
59
+ windowDays: parseInt(options.windowDays),
60
+ }),
61
+ ]);
62
+ if (options.json) {
63
+ json({ dau, wau, mau, growth });
64
+ return;
65
+ }
66
+ heading("Active Users");
67
+ console.log();
68
+ const formatDelta = (d) => d > 0 ? `+${(d * 100).toFixed(1)}%` : d === 0 ? "0%" : `${(d * 100).toFixed(1)}%`;
69
+ console.log(formatTable([
70
+ { metric: "DAU", value: dau.value, previous: dau.previous, delta: formatDelta(dau.deltaPct) },
71
+ { metric: "WAU", value: wau.value, previous: wau.previous, delta: formatDelta(wau.deltaPct) },
72
+ { metric: "MAU", value: mau.value, previous: mau.previous, delta: formatDelta(mau.deltaPct) },
73
+ ], [
74
+ { header: "METRIC", key: "metric" },
75
+ { header: "CURRENT", key: "value", align: "right", format: formatNumber },
76
+ { header: "PREVIOUS", key: "previous", align: "right", format: formatNumber },
77
+ { header: "CHANGE", key: "delta", align: "right" },
78
+ ]));
79
+ console.log();
80
+ heading(`Growth Accounting (${growth.window_days} days)`);
81
+ console.log();
82
+ keyValue("Current active", String(growth.current_active));
83
+ keyValue("Previous active", String(growth.previous_active));
84
+ keyValue("New users", String(growth.new_users));
85
+ keyValue("Retained", String(growth.retained_users));
86
+ keyValue("Reactivated", String(growth.reactivated_users));
87
+ keyValue("Churned", String(growth.churned_users));
88
+ keyValue("Delta", formatDelta(growth.deltaPct));
89
+ }
90
+ catch (err) {
91
+ error(err.message);
92
+ process.exit(1);
93
+ }
94
+ });
95
+ // ── Daily Active ─────────────────────────────────────────────────
96
+ analytics
97
+ .command("daily-active")
98
+ .description("Daily active users time series")
99
+ .argument("[app-id]", "App ID (uses current app if not specified)")
100
+ .option("--app <app-id>", "App ID")
101
+ .option("--window-days <n>", "Time window in days (7-90)", "28")
102
+ .option("--json", "Output as JSON")
103
+ .action(async (appId, options) => {
104
+ const resolvedAppId = resolveAppId(appId, options);
105
+ const client = new ApiClient();
106
+ try {
107
+ const result = await client.getAnalyticsDailyActive(resolvedAppId, {
27
108
  windowDays: parseInt(options.windowDays),
28
109
  });
29
110
  if (options.json) {
30
111
  json(result);
31
112
  return;
32
113
  }
33
- heading(`Analytics Overview (${result.windowDays} days)`);
114
+ heading(`Daily Active Users (${result.window_days} days)`);
34
115
  console.log();
35
- keyValue("Daily Active Users (DAU)", formatNumber(result.totals.dau));
36
- keyValue("Weekly Active Users (WAU)", formatNumber(result.totals.wau));
37
- keyValue("Monthly Active Users (MAU)", formatNumber(result.totals.mau));
38
- keyValue("Total Events", formatNumber(result.totals.totalEvents));
39
- if (result.series && result.series.length > 0) {
40
- divider();
41
- info("Activity Series:");
42
- console.log(formatTable(result.series.slice(-10), [
43
- { header: "DATE", key: "bucketStart", format: (v) => v.slice(0, 10) },
44
- { header: "USERS", key: "activeUsers", align: "right" },
45
- { header: "EVENTS", key: "totalEvents", align: "right" },
46
- ]));
116
+ if (!result.rows || result.rows.length === 0) {
117
+ info("No data found.");
118
+ return;
119
+ }
120
+ console.log(formatTable(result.rows, [
121
+ { header: "DATE", key: "day_label" },
122
+ { header: "ACTIVE USERS", key: "active_users", align: "right", format: formatNumber },
123
+ ]));
124
+ }
125
+ catch (err) {
126
+ error(err.message);
127
+ process.exit(1);
128
+ }
129
+ });
130
+ // ── Rolling Active ───────────────────────────────────────────────
131
+ analytics
132
+ .command("rolling-active")
133
+ .description("Rolling active users (28 data points)")
134
+ .argument("[app-id]", "App ID (uses current app if not specified)")
135
+ .option("--app <app-id>", "App ID")
136
+ .option("--window-days <n>", "Rolling window size in days (1-28)", "7")
137
+ .option("--json", "Output as JSON")
138
+ .action(async (appId, options) => {
139
+ const resolvedAppId = resolveAppId(appId, options);
140
+ const client = new ApiClient();
141
+ try {
142
+ const result = await client.getAnalyticsRollingActive(resolvedAppId, {
143
+ windowDays: parseInt(options.windowDays),
144
+ });
145
+ if (options.json) {
146
+ json(result);
147
+ return;
148
+ }
149
+ heading(`Rolling Active Users (${result.window_days}-day window)`);
150
+ console.log();
151
+ if (!result.rows || result.rows.length === 0) {
152
+ info("No data found.");
153
+ return;
154
+ }
155
+ console.log(formatTable(result.rows, [
156
+ { header: "DATE", key: "day_label" },
157
+ { header: "ACTIVE USERS", key: "active_users", align: "right", format: formatNumber },
158
+ ]));
159
+ }
160
+ catch (err) {
161
+ error(err.message);
162
+ process.exit(1);
163
+ }
164
+ });
165
+ // ── Cohort Retention ─────────────────────────────────────────────
166
+ analytics
167
+ .command("cohort-retention")
168
+ .description("Weekly cohort retention matrix")
169
+ .argument("[app-id]", "App ID (uses current app if not specified)")
170
+ .option("--app <app-id>", "App ID")
171
+ .option("--json", "Output as JSON")
172
+ .action(async (appId, options) => {
173
+ const resolvedAppId = resolveAppId(appId, options);
174
+ const client = new ApiClient();
175
+ try {
176
+ const result = await client.getAnalyticsCohortRetention(resolvedAppId);
177
+ if (options.json) {
178
+ json(result);
179
+ return;
180
+ }
181
+ heading("Cohort Retention");
182
+ console.log();
183
+ if (!result.rows || result.rows.length === 0) {
184
+ info("No cohort data found.");
185
+ return;
186
+ }
187
+ // Build week headers
188
+ const weekHeaders = result.weeks.map((w) => `W${w}`);
189
+ // Build table rows with retention percentages
190
+ const tableRows = result.rows.map((row) => {
191
+ const entry = {
192
+ cohort: row.signup_week_label,
193
+ size: row.cohort_size,
194
+ };
195
+ row.retention.forEach((r, i) => {
196
+ entry[`w${result.weeks[i]}`] =
197
+ r !== null ? `${r.toFixed(0)}%` : "-";
198
+ });
199
+ return entry;
200
+ });
201
+ const columns = [
202
+ { header: "COHORT", key: "cohort" },
203
+ { header: "SIZE", key: "size", align: "right", format: formatNumber },
204
+ ...weekHeaders.map((h, i) => ({
205
+ header: h,
206
+ key: `w${result.weeks[i]}`,
207
+ align: "right",
208
+ })),
209
+ ];
210
+ console.log(formatTable(tableRows, columns));
211
+ // Print averages row
212
+ if (result.averages) {
213
+ const avgStr = result.averages
214
+ .map((a) => a !== null ? `${a.toFixed(0)}%` : "-")
215
+ .join(" ");
216
+ console.log();
217
+ dim(`Averages: ${avgStr}`);
47
218
  }
48
219
  }
49
220
  catch (err) {
@@ -51,7 +222,7 @@ Examples:
51
222
  process.exit(1);
52
223
  }
53
224
  });
54
- // Top users
225
+ // ── Top Users ────────────────────────────────────────────────────
55
226
  analytics
56
227
  .command("top-users")
57
228
  .description("Get top users by activity")
@@ -80,6 +251,7 @@ Examples:
80
251
  }
81
252
  console.log(formatTable(result.results, [
82
253
  { header: "USER", key: "userUlid", format: formatId },
254
+ { header: "EMAIL", key: "email" },
83
255
  { header: "EVENTS", key: "eventCount", align: "right" },
84
256
  { header: "FIRST SEEN", key: "firstSeen", format: formatDate },
85
257
  { header: "LAST SEEN", key: "lastSeen", format: formatDate },
@@ -90,53 +262,93 @@ Examples:
90
262
  process.exit(1);
91
263
  }
92
264
  });
93
- // User details
265
+ // ── User Search ──────────────────────────────────────────────────
94
266
  analytics
95
- .command("user")
96
- .description("Get analytics for a specific user")
267
+ .command("user-search")
268
+ .description("Search users by email or ULID")
97
269
  .argument("[app-id]", "App ID (uses current app if not specified)")
98
- .argument("<user-ulid>", "User ULID")
99
270
  .option("--app <app-id>", "App ID")
100
- .option("--window-days <n>", "Time window in days", "30")
271
+ .requiredOption("--query <q>", "Email or ULID to search for")
272
+ .option("--limit <n>", "Max results", "25")
101
273
  .option("--json", "Output as JSON")
102
- .action(async (appIdOrUserUlid, userUlidOrUndefined, options) => {
103
- let resolvedAppId;
104
- let userUlid;
105
- if (userUlidOrUndefined) {
106
- resolvedAppId = resolveAppId(appIdOrUserUlid, options);
107
- userUlid = userUlidOrUndefined;
274
+ .action(async (appId, options) => {
275
+ const resolvedAppId = resolveAppId(appId, options);
276
+ const client = new ApiClient();
277
+ try {
278
+ const result = await client.getAnalyticsUserSearch(resolvedAppId, {
279
+ q: options.query,
280
+ limit: parseInt(options.limit),
281
+ });
282
+ if (options.json) {
283
+ json(result);
284
+ return;
285
+ }
286
+ heading(`User Search: "${result.query}"`);
287
+ console.log();
288
+ if (!result.results || result.results.length === 0) {
289
+ info("No users found.");
290
+ return;
291
+ }
292
+ console.log(formatTable(result.results, [
293
+ { header: "USER", key: "userUlid", format: formatId },
294
+ { header: "EMAIL", key: "email" },
295
+ { header: "NAME", key: "name" },
296
+ { header: "EVENTS", key: "eventCount", align: "right" },
297
+ { header: "LAST SEEN", key: "lastSeen", format: formatDate },
298
+ ]));
108
299
  }
109
- else {
110
- resolvedAppId = resolveAppId(undefined, options);
111
- userUlid = appIdOrUserUlid;
300
+ catch (err) {
301
+ error(err.message);
302
+ process.exit(1);
112
303
  }
304
+ });
305
+ // ── User Detail ──────────────────────────────────────────────────
306
+ analytics
307
+ .command("user-detail")
308
+ .description("Detailed activity for a single user")
309
+ .argument("<user-ulid>", "User ULID")
310
+ .argument("[app-id]", "App ID (uses current app if not specified)")
311
+ .option("--app <app-id>", "App ID")
312
+ .option("--json", "Output as JSON")
313
+ .action(async (userUlid, appId, options) => {
314
+ const resolvedAppId = resolveAppId(appId, options);
113
315
  const client = new ApiClient();
114
316
  try {
115
- const result = await client.getAnalyticsUserTimeline(resolvedAppId, userUlid, {
116
- windowDays: parseInt(options.windowDays),
117
- });
317
+ const result = await client.getAnalyticsUserDetail(resolvedAppId, userUlid);
118
318
  if (options.json) {
119
319
  json(result);
120
320
  return;
121
321
  }
122
- heading(`User Activity (${result.windowDays} days)`);
123
- keyValue("User", userUlid);
322
+ heading("User Detail");
124
323
  console.log();
125
- if (result.breakdown && result.breakdown.length > 0) {
126
- info("Activity Breakdown:");
127
- console.log(formatTable(result.breakdown.slice(0, 15), [
324
+ keyValue("ULID", result.user.user_ulid);
325
+ if (result.user.email)
326
+ keyValue("Email", result.user.email);
327
+ if (result.user.name)
328
+ keyValue("Name", result.user.name);
329
+ console.log();
330
+ keyValue("First seen", result.stats.first_seen ? formatDate(result.stats.first_seen) : "-");
331
+ keyValue("Last active", result.stats.last_active ? formatDate(result.stats.last_active) : "-");
332
+ keyValue("Total events", String(result.stats.total_events));
333
+ keyValue("Days active", String(result.stats.days_active));
334
+ if (result.events_by_action?.length > 0) {
335
+ console.log();
336
+ heading("Events by Action");
337
+ console.log();
338
+ console.log(formatTable(result.events_by_action, [
128
339
  { header: "ACTION", key: "action" },
129
- { header: "FEATURE", key: "feature" },
130
- { header: "ROUTE", key: "route" },
131
- { header: "EVENTS", key: "events", align: "right" },
340
+ { header: "COUNT", key: "event_count", align: "right", format: formatNumber },
341
+ { header: "LAST OCCURRED", key: "last_occurred", format: formatDate },
132
342
  ]));
133
343
  }
134
- if (result.timeline && result.timeline.length > 0) {
135
- divider();
136
- info("Recent Timeline:");
137
- console.log(formatTable(result.timeline.slice(-10), [
138
- { header: "TIME", key: "bucketStart", format: formatDate },
139
- { header: "EVENTS", key: "events", align: "right" },
344
+ if (result.events_by_feature?.length > 0) {
345
+ console.log();
346
+ heading("Events by Feature");
347
+ console.log();
348
+ console.log(formatTable(result.events_by_feature, [
349
+ { header: "FEATURE", key: "feature" },
350
+ { header: "COUNT", key: "event_count", align: "right", format: formatNumber },
351
+ { header: "PCT", key: "pct", align: "right", format: formatPctRaw },
140
352
  ]));
141
353
  }
142
354
  }
@@ -145,7 +357,123 @@ Examples:
145
357
  process.exit(1);
146
358
  }
147
359
  });
148
- // Integration metrics
360
+ // ── User Snapshot ────────────────────────────────────────────────
361
+ analytics
362
+ .command("user-snapshot")
363
+ .description("Latest context snapshot for a user")
364
+ .argument("<user-ulid>", "User ULID")
365
+ .argument("[app-id]", "App ID (uses current app if not specified)")
366
+ .option("--app <app-id>", "App ID")
367
+ .option("--json", "Output as JSON")
368
+ .action(async (userUlid, appId, options) => {
369
+ const resolvedAppId = resolveAppId(appId, options);
370
+ const client = new ApiClient();
371
+ try {
372
+ const result = await client.getAnalyticsUserSnapshot(resolvedAppId, userUlid);
373
+ if (options.json) {
374
+ json(result);
375
+ return;
376
+ }
377
+ heading("User Snapshot");
378
+ console.log();
379
+ if (!result.snapshot) {
380
+ info("No snapshot found for this user.");
381
+ return;
382
+ }
383
+ keyValue("Timestamp", formatDate(result.snapshot.timestamp));
384
+ console.log();
385
+ console.log(JSON.stringify(result.snapshot.values, null, 2));
386
+ }
387
+ catch (err) {
388
+ error(err.message);
389
+ process.exit(1);
390
+ }
391
+ });
392
+ // ── Events Feed ──────────────────────────────────────────────────
393
+ analytics
394
+ .command("events")
395
+ .description("Paginated event feed")
396
+ .argument("[app-id]", "App ID (uses current app if not specified)")
397
+ .option("--app <app-id>", "App ID")
398
+ .option("--window-days <n>", "Time window in days", "7")
399
+ .option("--page <n>", "Page number (0-based)", "0")
400
+ .option("--json", "Output as JSON")
401
+ .action(async (appId, options) => {
402
+ const resolvedAppId = resolveAppId(appId, options);
403
+ const client = new ApiClient();
404
+ try {
405
+ const result = await client.getAnalyticsEvents(resolvedAppId, {
406
+ windowDays: parseInt(options.windowDays),
407
+ page: parseInt(options.page),
408
+ });
409
+ if (options.json) {
410
+ json(result);
411
+ return;
412
+ }
413
+ heading(`Events (page ${result.page}, ${result.total_rows} total)`);
414
+ console.log();
415
+ if (!result.rows || result.rows.length === 0) {
416
+ info("No events found.");
417
+ return;
418
+ }
419
+ console.log(formatTable(result.rows, [
420
+ { header: "TIME", key: "timestamp", format: formatDate },
421
+ { header: "USER", key: "user_ulid", format: formatId },
422
+ { header: "ACTION", key: "action" },
423
+ { header: "FEATURE", key: "feature" },
424
+ { header: "ROUTE", key: "route" },
425
+ { header: "COUNTRY", key: "country" },
426
+ ]));
427
+ const totalPages = Math.ceil(result.total_rows / result.page_size);
428
+ if (result.page + 1 < totalPages) {
429
+ console.log();
430
+ dim(`Page ${result.page + 1} of ${totalPages}. Use --page ${result.page + 1} for the next page.`);
431
+ }
432
+ }
433
+ catch (err) {
434
+ error(err.message);
435
+ process.exit(1);
436
+ }
437
+ });
438
+ // ── Events Grouped ───────────────────────────────────────────────
439
+ analytics
440
+ .command("events-grouped")
441
+ .description("Events grouped by a dimension")
442
+ .argument("[app-id]", "App ID (uses current app if not specified)")
443
+ .option("--app <app-id>", "App ID")
444
+ .option("--group-by <dim>", "Dimension: action, feature, route, country, deviceType, plan, day", "action")
445
+ .option("--window-days <n>", "Time window in days", "7")
446
+ .option("--json", "Output as JSON")
447
+ .action(async (appId, options) => {
448
+ const resolvedAppId = resolveAppId(appId, options);
449
+ const client = new ApiClient();
450
+ try {
451
+ const result = await client.getAnalyticsEventsGrouped(resolvedAppId, {
452
+ groupBy: options.groupBy,
453
+ windowDays: parseInt(options.windowDays),
454
+ });
455
+ if (options.json) {
456
+ json(result);
457
+ return;
458
+ }
459
+ heading(`Events by ${result.group_by}`);
460
+ console.log();
461
+ if (!result.rows || result.rows.length === 0) {
462
+ info("No events found.");
463
+ return;
464
+ }
465
+ console.log(formatTable(result.rows, [
466
+ { header: result.group_by.toUpperCase(), key: "group_value" },
467
+ { header: "EVENTS", key: "events", align: "right", format: formatNumber },
468
+ { header: "UNIQUE USERS", key: "unique_users", align: "right", format: formatNumber },
469
+ ]));
470
+ }
471
+ catch (err) {
472
+ error(err.message);
473
+ process.exit(1);
474
+ }
475
+ });
476
+ // ── Integration Metrics ──────────────────────────────────────────
149
477
  analytics
150
478
  .command("integrations")
151
479
  .description("Get integration usage metrics")
@@ -174,8 +502,89 @@ Examples:
174
502
  { header: "INTEGRATION", key: "integrationKey" },
175
503
  { header: "CALLS", key: "invocations", align: "right", format: formatNumber },
176
504
  { header: "ERROR RATE", key: "errorRate", align: "right", format: formatPercent },
177
- { header: "AVG LATENCY", key: "avgDurationMs", align: "right", format: formatDuration },
178
- { header: "P95 LATENCY", key: "p95DurationMs", align: "right", format: formatDuration },
505
+ { header: "MEDIAN", key: "medianDurationMs", align: "right", format: formatDuration },
506
+ { header: "P95", key: "p95DurationMs", align: "right", format: formatDuration },
507
+ ]));
508
+ }
509
+ catch (err) {
510
+ error(err.message);
511
+ process.exit(1);
512
+ }
513
+ });
514
+ // ── Top Workflows ────────────────────────────────────────────────
515
+ analytics
516
+ .command("workflows")
517
+ .description("Top workflows by runs")
518
+ .argument("[app-id]", "App ID (uses current app if not specified)")
519
+ .option("--app <app-id>", "App ID")
520
+ .option("--window-days <n>", "Time window in days", "30")
521
+ .option("--limit <n>", "Number of workflows to show", "10")
522
+ .option("--json", "Output as JSON")
523
+ .action(async (appId, options) => {
524
+ const resolvedAppId = resolveAppId(appId, options);
525
+ const client = new ApiClient();
526
+ try {
527
+ const result = await client.getAnalyticsTopWorkflows(resolvedAppId, {
528
+ windowDays: parseInt(options.windowDays),
529
+ limit: parseInt(options.limit),
530
+ });
531
+ if (options.json) {
532
+ json(result);
533
+ return;
534
+ }
535
+ heading(`Top Workflows (${result.windowDays} days)`);
536
+ console.log();
537
+ if (!result.workflows || result.workflows.length === 0) {
538
+ info("No workflow data found.");
539
+ return;
540
+ }
541
+ console.log(formatTable(result.workflows, [
542
+ { header: "WORKFLOW", key: "workflowKey" },
543
+ { header: "RUNS", key: "runs", align: "right", format: formatNumber },
544
+ { header: "SUCCESS", key: "successRate", align: "right", format: formatPercent },
545
+ { header: "MEDIAN", key: "medianDurationMs", align: "right", format: formatDuration },
546
+ { header: "P95", key: "p95", align: "right", format: formatDuration },
547
+ { header: "TOKENS", key: "totalTokens", align: "right", format: formatNumber },
548
+ ]));
549
+ }
550
+ catch (err) {
551
+ error(err.message);
552
+ process.exit(1);
553
+ }
554
+ });
555
+ // ── Top Prompts ──────────────────────────────────────────────────
556
+ analytics
557
+ .command("prompts")
558
+ .description("Top prompts by executions")
559
+ .argument("[app-id]", "App ID (uses current app if not specified)")
560
+ .option("--app <app-id>", "App ID")
561
+ .option("--window-days <n>", "Time window in days", "30")
562
+ .option("--limit <n>", "Number of prompts to show", "10")
563
+ .option("--json", "Output as JSON")
564
+ .action(async (appId, options) => {
565
+ const resolvedAppId = resolveAppId(appId, options);
566
+ const client = new ApiClient();
567
+ try {
568
+ const result = await client.getAnalyticsTopPrompts(resolvedAppId, {
569
+ windowDays: parseInt(options.windowDays),
570
+ limit: parseInt(options.limit),
571
+ });
572
+ if (options.json) {
573
+ json(result);
574
+ return;
575
+ }
576
+ heading(`Top Prompts (${result.windowDays} days)`);
577
+ console.log();
578
+ if (!result.prompts || result.prompts.length === 0) {
579
+ info("No prompt data found.");
580
+ return;
581
+ }
582
+ console.log(formatTable(result.prompts, [
583
+ { header: "PROMPT", key: "promptKey" },
584
+ { header: "RUNS", key: "executions", align: "right", format: formatNumber },
585
+ { header: "MEDIAN", key: "medianDurationMs", align: "right", format: formatDuration },
586
+ { header: "P95", key: "p95DurationMs", align: "right", format: formatDuration },
587
+ { header: "TOKENS", key: "totalTokens", align: "right", format: formatNumber },
179
588
  ]));
180
589
  }
181
590
  catch (err) {