@pitcher/cli-scripts 3.1.0 → 3.2.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.
- package/README.md +40 -3
- package/dist/index.js +2 -0
- package/dist/src/commands/analytics.d.ts +77 -0
- package/dist/src/commands/analytics.js +309 -1
- package/dist/src/commands/metadataTemplate.js +3 -2
- package/dist/src/commands/roleplay.d.ts +85 -0
- package/dist/src/commands/roleplay.js +449 -0
- package/dist/src/core/dsr-visits.d.ts +50 -0
- package/dist/src/core/dsr-visits.js +191 -0
- package/dist/src/core/pitcherAuth.d.ts +0 -7
- package/dist/src/core/pitcherAuth.js +0 -3
- package/dist/src/core/roleplay-stats.d.ts +79 -0
- package/dist/src/core/roleplay-stats.js +114 -0
- package/dist/src/mcp/hosted.js +1 -1
- package/dist/src/mcp/index.d.ts +1 -1
- package/dist/src/mcp/index.js +1 -1
- package/dist/src/mcp/registry.js +2 -0
- package/dist/src/mcp/renderPolicy.d.ts +1 -1
- package/dist/src/mcp/renderPolicy.js +2 -1
- package/dist/src/mcp/resources/analyticsCatalog.d.ts +1 -1
- package/dist/src/mcp/resources/analyticsCatalog.js +23 -5
- package/dist/src/mcp/server.d.ts +1 -0
- package/dist/src/mcp/server.js +4 -4
- package/dist/src/mcp/tools/analytics.js +88 -3
- package/dist/src/mcp/tools/metadataTemplate.js +3 -2
- package/dist/src/mcp/tools/roleplay.d.ts +2 -0
- package/dist/src/mcp/tools/roleplay.js +106 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ Pitcher's MCP tools come from two places:
|
|
|
44
44
|
|
|
45
45
|
| Server | Tools | Add it with |
|
|
46
46
|
|---|---|---|
|
|
47
|
-
| **Hosted** (OAuth; Claude Code, Claude Desktop, claude.ai, ChatGPT) | everything: content, canvases, DSRs, analytics, L&D, users, … (
|
|
47
|
+
| **Hosted** (OAuth; Claude Code, Claude Desktop, claude.ai, ChatGPT) | everything: content, canvases, DSRs, analytics, L&D, users, … (131 tools, prompts, resources) | `claude mcp add --transport http pitcher https://mcp.pitcher.com/mcp` — Claude Desktop: Settings → Connectors → Add custom connector, same URL |
|
|
48
48
|
| **Local** `p mcp` (stdio) | only `pitcher_dev_sync`, `pitcher_dev_sync_revert`, `pitcher_publish_app` (they zip a folder on YOUR machine) | `claude mcp add pitcher-local -- p mcp` |
|
|
49
49
|
|
|
50
50
|
Claude Code users add both. The local server runs on your `p login` session (no API key, no env block); the domain is fixed when it starts. Signed in to several orgs? Add one entry per org:
|
|
@@ -392,9 +392,46 @@ p analytics viewers --instance-id 01... --canvas-id 01CANVASID... --viewer-type
|
|
|
392
392
|
|
|
393
393
|
# Per-viewer session event timeline for a DSR canvas (member key)
|
|
394
394
|
p analytics sessions --instance-id 01... --canvas-id 01CANVASID... --user rep@example.com --api-key myapikey
|
|
395
|
+
|
|
396
|
+
# Who opened one file, and time per page of it (instance editor or admin)
|
|
397
|
+
p analytics file-users --instance-id 01... --file-id 01FILEID... --start-date 2026-10-01 --end-date 2026-10-31
|
|
398
|
+
p analytics file-pages --instance-id 01... --file-id 01FILEID... --user-emails rep@example.com
|
|
399
|
+
|
|
400
|
+
# Reps' canvas usage; with --canvas-id also seconds per page (section) (instance editor or admin)
|
|
401
|
+
p analytics canvas-users --instance-id 01... --canvas-id 01CANVASID...
|
|
402
|
+
|
|
403
|
+
# Per DSR viewer: each visit and the pages viewed with their time; --format csv for an export (member key)
|
|
404
|
+
p analytics viewer-visits --instance-id 01... --canvas-id 01CANVASID... --limit 25 --format csv
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`file-users`, `file-pages` and `canvas-users` need an instance editor, instance admin or org admin. `canvas-users`
|
|
408
|
+
counts internal users only and presentation time only; it reads `GET /analytics/canvases/users` (PIT-9589).
|
|
409
|
+
`viewer-visits` reads the 10 most recently active viewers by default (`--limit`, max 25; one timeline query each),
|
|
410
|
+
or one with `--user`. `kpi` returns every value and growth figure as a number.
|
|
411
|
+
|
|
412
|
+
MCP: all fourteen verbs are exposed as read-only `pitcher_analytics_*` tools, alongside a `pitcher://analytics/catalog` resource (which verb answers which question — returns, time model, key tier) and four query prompts — `top_content`, `content_engagement_summary`, `who_viewed_canvas`, `adoption_snapshot`.
|
|
413
|
+
|
|
414
|
+
## To run the `roleplay` command:
|
|
415
|
+
|
|
416
|
+
Read AI Roleplay results (PIT-9200; read-only). `kpis`, `sessions` and `rep-summary` read the roleplay analytics
|
|
417
|
+
replica in clickhouse-api and need an **instance or org admin**; dates are optional and there is **no default
|
|
418
|
+
window** (omitted = all time). `team` (assignment completion per rep, as the manager app shows it) gives an admin every
|
|
419
|
+
roleplay assignment on the instance and a coaching manager the assignments they coach. `session` opens one session to
|
|
420
|
+
its rep, a coach of that rep, or an admin, and never returns the transcript or recordings. `scenarios` lists the
|
|
421
|
+
instance's scenarios without prompts. A session with no score is unscored, never 0, and Practice Studio takes never
|
|
422
|
+
appear.
|
|
423
|
+
|
|
424
|
+
```shell
|
|
425
|
+
p roleplay kpis --instance-id 01... --start-date 2026-10-01
|
|
426
|
+
p roleplay sessions --instance-id 01... --rep ann --sort-by score --sort-order desc --limit 50
|
|
427
|
+
p roleplay rep-summary --instance-id 01... --group-by scenario
|
|
428
|
+
p roleplay team --instance-id 01... --status needs_coaching
|
|
429
|
+
p roleplay session --instance-id 01... --entry-id 01ENTRYID...
|
|
430
|
+
p roleplay scenarios --instance-id 01...
|
|
395
431
|
```
|
|
396
432
|
|
|
397
|
-
MCP:
|
|
433
|
+
MCP: `pitcher_roleplay_kpis`, `_sessions`, `_rep_summary`, `_team`, `_session_get`, `_scenarios_list`, all read-only,
|
|
434
|
+
so they are also served to organizations in read-only mode.
|
|
398
435
|
|
|
399
436
|
## To run the `users` command:
|
|
400
437
|
|
|
@@ -502,7 +539,7 @@ p metadata-template scaffold --out ./schema.json
|
|
|
502
539
|
p metadata-template upsert --instance-id 01... --sections-file ./schema.json --api-key myapikey
|
|
503
540
|
```
|
|
504
541
|
|
|
505
|
-
`upsert` merges the incoming sections by section `label` — a section whose label matches an existing one is replaced, other sections are preserved. It
|
|
542
|
+
`upsert` merges the incoming sections by section `label` — a section whose label matches an existing one is **replaced whole** (fields left out of it are removed), other sections are preserved. It cannot remove a whole section (that's a UI operation). `options` for `select`/`multi-select` fields are `{ label, value }` objects (bare strings are rejected). `upsert` requires an instance- or org-admin key; a non-admin member key is rejected (400 → `VALIDATION`).
|
|
506
543
|
|
|
507
544
|
## To run the `dsr` command:
|
|
508
545
|
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { createSharedLinksCommand } from './src/commands/sharedLinks.js';
|
|
|
14
14
|
import { createTagsCommand } from './src/commands/tags.js';
|
|
15
15
|
import { createOrgCommand } from './src/commands/org.js';
|
|
16
16
|
import { createAnalyticsCommand } from './src/commands/analytics.js';
|
|
17
|
+
import { createRoleplayCommand } from './src/commands/roleplay.js';
|
|
17
18
|
import { createAnnouncementsCommand } from './src/commands/announcements.js';
|
|
18
19
|
import { createDsrLandingCommand } from './src/commands/dsrLanding.js';
|
|
19
20
|
import { createEmailTemplateCommand } from './src/commands/emailTemplate.js';
|
|
@@ -61,6 +62,7 @@ program.addCommand(createDsrCommand());
|
|
|
61
62
|
program.addCommand(createLdCommand());
|
|
62
63
|
program.addCommand(createOrgCommand());
|
|
63
64
|
program.addCommand(createAnalyticsCommand());
|
|
65
|
+
program.addCommand(createRoleplayCommand());
|
|
64
66
|
program.addCommand(createMcpCommand());
|
|
65
67
|
program.addCommand(createLoginCommand());
|
|
66
68
|
program.addCommand(createLogoutCommand());
|
|
@@ -9,6 +9,13 @@ export declare function runAnalyticsOrg(opts: CommonOpts, deps?: AnalyticsDeps):
|
|
|
9
9
|
export declare function runAnalyticsInstance(opts: CommonOpts & {
|
|
10
10
|
instanceId?: string;
|
|
11
11
|
}, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
12
|
+
/**
|
|
13
|
+
* KPI values as numbers everywhere (PIT-9589): clickhouse-api sends `growth` as a string
|
|
14
|
+
* (`toFixed()`), and ClickHouse quotes 64-bit integers, so one payload mixes `"88"` and `88`. Only
|
|
15
|
+
* the `data` subtree is walked, and only a string that is a plain decimal literal is converted;
|
|
16
|
+
* dates and labels stay strings. The server contract is left alone (the dashboard reads it).
|
|
17
|
+
*/
|
|
18
|
+
export declare function numifyKpiData(value: unknown): unknown;
|
|
12
19
|
export interface AnalyticsKpiOpts extends CommonOpts {
|
|
13
20
|
instanceId?: string;
|
|
14
21
|
type?: string;
|
|
@@ -176,4 +183,74 @@ export interface AnalyticsSessionsOpts extends CommonOpts {
|
|
|
176
183
|
* window, so dates are sent only when the caller supplies them.
|
|
177
184
|
*/
|
|
178
185
|
export declare function runAnalyticsSessions(opts: AnalyticsSessionsOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
186
|
+
export interface AnalyticsFileUsersOpts extends CommonOpts {
|
|
187
|
+
instanceId?: string;
|
|
188
|
+
fileId?: string;
|
|
189
|
+
startDate?: string;
|
|
190
|
+
endDate?: string;
|
|
191
|
+
pageNumber?: number;
|
|
192
|
+
groupIds?: string;
|
|
193
|
+
userEmails?: string;
|
|
194
|
+
limit?: number;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Who opened ONE file: per user, opens, seconds, days active and the last day, from
|
|
198
|
+
* `GET /analytics/files/users` (editor/admin tier, the Files tab's user modal). Internal users
|
|
199
|
+
* only (the server leaves out external viewers). `pageNumber` (0-based) narrows to one page of the
|
|
200
|
+
* file. Dates default to the last 30 days (the server requires both). Sorted by opens, capped to
|
|
201
|
+
* `--limit`.
|
|
202
|
+
*/
|
|
203
|
+
export declare function runAnalyticsFileUsers(opts: AnalyticsFileUsersOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
204
|
+
export interface AnalyticsFilePagesOpts extends CommonOpts {
|
|
205
|
+
instanceId?: string;
|
|
206
|
+
fileId?: string;
|
|
207
|
+
startDate?: string;
|
|
208
|
+
endDate?: string;
|
|
209
|
+
groupIds?: string;
|
|
210
|
+
userEmails?: string;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Time per page of ONE file, from `GET /analytics/files/pages` (editor/admin tier): internal
|
|
214
|
+
* opens and seconds per page, plus external (DSR) views. Pass one `--user-emails` value for one
|
|
215
|
+
* user's time per page. Dates default to the last 30 days. Every page is returned (no cap).
|
|
216
|
+
*/
|
|
217
|
+
export declare function runAnalyticsFilePages(opts: AnalyticsFilePagesOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
218
|
+
export interface AnalyticsCanvasUsersOpts extends CommonOpts {
|
|
219
|
+
instanceId?: string;
|
|
220
|
+
canvasId?: string;
|
|
221
|
+
startDate?: string;
|
|
222
|
+
endDate?: string;
|
|
223
|
+
groupIds?: string;
|
|
224
|
+
userEmails?: string;
|
|
225
|
+
limit?: number;
|
|
226
|
+
page?: number;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Per-user canvas usage from `GET /analytics/canvases/users` (editor/admin tier, PIT-9589): one row
|
|
230
|
+
* per user and canvas with visits, seconds and the last visit; with `--canvas-id`, each user's
|
|
231
|
+
* seconds per page (section) too. Internal users only, presentation time only. Dates default to
|
|
232
|
+
* the last 30 days.
|
|
233
|
+
*/
|
|
234
|
+
export declare function runAnalyticsCanvasUsers(opts: AnalyticsCanvasUsersOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
235
|
+
export interface AnalyticsViewerVisitsOpts extends CommonOpts {
|
|
236
|
+
instanceId?: string;
|
|
237
|
+
canvasId?: string;
|
|
238
|
+
userEmail?: string;
|
|
239
|
+
viewerType?: string;
|
|
240
|
+
startDate?: string;
|
|
241
|
+
endDate?: string;
|
|
242
|
+
limit?: number;
|
|
243
|
+
format?: string;
|
|
244
|
+
}
|
|
245
|
+
/** Viewers read per call when no single viewer is named; each costs one timeline query. */
|
|
246
|
+
export declare const VIEWER_VISITS_MAX_VIEWERS = 25;
|
|
247
|
+
/**
|
|
248
|
+
* Per viewer of a DSR canvas: each visit, and in it which sections, files and pages were viewed for
|
|
249
|
+
* how long (PIT-9589). One viewer (`--user`, verbatim from `viewers`) or the most recently active
|
|
250
|
+
* viewers (`--limit`, default 10, max 25: one timeline query each). `--format csv` returns the same
|
|
251
|
+
* as CSV text, one row per viewer, visit and item, for export. Reads
|
|
252
|
+
* `/analytics/canvas-details/{viewers,sessions}` (any member, as `viewers`/`sessions`); the server
|
|
253
|
+
* applies its own default window when dates are omitted.
|
|
254
|
+
*/
|
|
255
|
+
export declare function runAnalyticsViewerVisits(opts: AnalyticsViewerVisitsOpts, deps?: AnalyticsDeps): Promise<Envelope<unknown>>;
|
|
179
256
|
export declare function createAnalyticsCommand(): Command;
|
|
@@ -6,6 +6,7 @@ import { emitEnvelope } from '../core/output.js';
|
|
|
6
6
|
import { withAuthFlags } from './withAuth.js';
|
|
7
7
|
import { isKpiType, KPI_TYPES } from '../core/kpiTypes.js';
|
|
8
8
|
import { shapeAnalytics, pageSizeFor, withDefaultWindow, todayIso } from '../core/analytics-shape.js';
|
|
9
|
+
import { summarizeVisits, visitsToCsv } from '../core/dsr-visits.js';
|
|
9
10
|
export async function runAnalyticsOrg(opts, deps = {}) {
|
|
10
11
|
try {
|
|
11
12
|
const client = clientFor(opts, deps);
|
|
@@ -28,6 +29,23 @@ export async function runAnalyticsInstance(opts, deps = {}) {
|
|
|
28
29
|
return fail(toCliError(e));
|
|
29
30
|
}
|
|
30
31
|
}
|
|
32
|
+
const NUMERIC_TEXT = /^-?\d+(\.\d+)?$/;
|
|
33
|
+
/**
|
|
34
|
+
* KPI values as numbers everywhere (PIT-9589): clickhouse-api sends `growth` as a string
|
|
35
|
+
* (`toFixed()`), and ClickHouse quotes 64-bit integers, so one payload mixes `"88"` and `88`. Only
|
|
36
|
+
* the `data` subtree is walked, and only a string that is a plain decimal literal is converted;
|
|
37
|
+
* dates and labels stay strings. The server contract is left alone (the dashboard reads it).
|
|
38
|
+
*/
|
|
39
|
+
export function numifyKpiData(value) {
|
|
40
|
+
if (typeof value === 'string')
|
|
41
|
+
return NUMERIC_TEXT.test(value.trim()) ? Number(value) : value;
|
|
42
|
+
if (Array.isArray(value))
|
|
43
|
+
return value.map(numifyKpiData);
|
|
44
|
+
if (value && typeof value === 'object') {
|
|
45
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, numifyKpiData(v)]));
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
31
49
|
/**
|
|
32
50
|
* KPI metrics from the clickhouse-api analytics service (PIT-7647). Unlike
|
|
33
51
|
* org/instance rollups (Django), this hits `clickhouse.pitcher.com` with the
|
|
@@ -58,7 +76,8 @@ export async function runAnalyticsKpi(opts, deps = {}) {
|
|
|
58
76
|
if (opts.userEmails)
|
|
59
77
|
params.user_emails = opts.userEmails;
|
|
60
78
|
const res = await client.get('/analytics/kpi', { params });
|
|
61
|
-
|
|
79
|
+
const body = res.data;
|
|
80
|
+
return ok(body && typeof body === 'object' && 'data' in body ? { ...body, data: numifyKpiData(body.data) } : body);
|
|
62
81
|
}
|
|
63
82
|
catch (e) {
|
|
64
83
|
return fail(toCliError(e));
|
|
@@ -365,6 +384,241 @@ export async function runAnalyticsSessions(opts, deps = {}) {
|
|
|
365
384
|
return fail(toCliError(e));
|
|
366
385
|
}
|
|
367
386
|
}
|
|
387
|
+
// ── PIT-9589: per-user file and canvas usage, DSR viewer page detail ─────────
|
|
388
|
+
const toNum = (v) => (v == null || v === '' ? 0 : Number(v));
|
|
389
|
+
/**
|
|
390
|
+
* Who opened ONE file: per user, opens, seconds, days active and the last day, from
|
|
391
|
+
* `GET /analytics/files/users` (editor/admin tier, the Files tab's user modal). Internal users
|
|
392
|
+
* only (the server leaves out external viewers). `pageNumber` (0-based) narrows to one page of the
|
|
393
|
+
* file. Dates default to the last 30 days (the server requires both). Sorted by opens, capped to
|
|
394
|
+
* `--limit`.
|
|
395
|
+
*/
|
|
396
|
+
export async function runAnalyticsFileUsers(opts, deps = {}) {
|
|
397
|
+
try {
|
|
398
|
+
if (!opts.instanceId)
|
|
399
|
+
throw createCliError('VALIDATION', '--instance-id is required for analytics file-users');
|
|
400
|
+
if (!opts.fileId)
|
|
401
|
+
throw createCliError('VALIDATION', '--file-id is required for analytics file-users');
|
|
402
|
+
if (opts.pageNumber !== undefined && !(Number.isInteger(opts.pageNumber) && opts.pageNumber >= 0)) {
|
|
403
|
+
throw createCliError('VALIDATION', '--page-number must be a 0-based page index');
|
|
404
|
+
}
|
|
405
|
+
const client = clickhouseClientFor(opts, deps);
|
|
406
|
+
const windowed = withDefaultWindow(opts, todayIso());
|
|
407
|
+
const params = {
|
|
408
|
+
instance_id: opts.instanceId,
|
|
409
|
+
file_id: opts.fileId,
|
|
410
|
+
start_date: windowed.startDate,
|
|
411
|
+
end_date: windowed.endDate,
|
|
412
|
+
};
|
|
413
|
+
if (opts.pageNumber !== undefined)
|
|
414
|
+
params.page_number = opts.pageNumber;
|
|
415
|
+
if (opts.groupIds)
|
|
416
|
+
params.group_ids = opts.groupIds;
|
|
417
|
+
if (opts.userEmails)
|
|
418
|
+
params.user_emails = opts.userEmails;
|
|
419
|
+
const res = await client.get('/analytics/files/users', { params });
|
|
420
|
+
const users = (res.data ?? {}).users ?? [];
|
|
421
|
+
const rows = users
|
|
422
|
+
.map((u) => {
|
|
423
|
+
const days = Array.isArray(u.accountUsage) ? u.accountUsage : [];
|
|
424
|
+
return {
|
|
425
|
+
email: u.who ?? null,
|
|
426
|
+
name: u.userName ?? null,
|
|
427
|
+
role: u.userRole ?? null,
|
|
428
|
+
opens: toNum(u.usageCount),
|
|
429
|
+
seconds: toNum(u.duration),
|
|
430
|
+
days_active: days.length,
|
|
431
|
+
last_opened_on: days[0]?.showDate ?? null,
|
|
432
|
+
};
|
|
433
|
+
})
|
|
434
|
+
.sort((a, b) => b.opens - a.opens || b.seconds - a.seconds);
|
|
435
|
+
return ok(shapeAnalytics(rows, (n) => ({ summary: { file_id: opts.fileId, start_date: windowed.startDate, end_date: windowed.endDate }, rows: n }), { limit: opts.limit }));
|
|
436
|
+
}
|
|
437
|
+
catch (e) {
|
|
438
|
+
return fail(toCliError(e));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Time per page of ONE file, from `GET /analytics/files/pages` (editor/admin tier): internal
|
|
443
|
+
* opens and seconds per page, plus external (DSR) views. Pass one `--user-emails` value for one
|
|
444
|
+
* user's time per page. Dates default to the last 30 days. Every page is returned (no cap).
|
|
445
|
+
*/
|
|
446
|
+
export async function runAnalyticsFilePages(opts, deps = {}) {
|
|
447
|
+
try {
|
|
448
|
+
if (!opts.instanceId)
|
|
449
|
+
throw createCliError('VALIDATION', '--instance-id is required for analytics file-pages');
|
|
450
|
+
if (!opts.fileId)
|
|
451
|
+
throw createCliError('VALIDATION', '--file-id is required for analytics file-pages');
|
|
452
|
+
const client = clickhouseClientFor(opts, deps);
|
|
453
|
+
const windowed = withDefaultWindow(opts, todayIso());
|
|
454
|
+
const params = {
|
|
455
|
+
instance_id: opts.instanceId,
|
|
456
|
+
file_id: opts.fileId,
|
|
457
|
+
start_date: windowed.startDate,
|
|
458
|
+
end_date: windowed.endDate,
|
|
459
|
+
};
|
|
460
|
+
if (opts.groupIds)
|
|
461
|
+
params.group_ids = opts.groupIds;
|
|
462
|
+
if (opts.userEmails)
|
|
463
|
+
params.user_emails = opts.userEmails;
|
|
464
|
+
const res = await client.get('/analytics/files/pages', { params });
|
|
465
|
+
const raw = Array.isArray(res.data) ? res.data : [];
|
|
466
|
+
const pages = raw.map((p) => ({
|
|
467
|
+
page_number: toNum(p.pageNumber) + 1,
|
|
468
|
+
internal_opens: toNum(p.openCount),
|
|
469
|
+
internal_seconds: toNum(p.totalDuration),
|
|
470
|
+
avg_seconds: p.averageDuration == null ? null : Math.round(toNum(p.averageDuration)),
|
|
471
|
+
external_views: toNum(p.viewedCount),
|
|
472
|
+
}));
|
|
473
|
+
return ok({
|
|
474
|
+
file_id: opts.fileId,
|
|
475
|
+
start_date: windowed.startDate,
|
|
476
|
+
end_date: windowed.endDate,
|
|
477
|
+
pages,
|
|
478
|
+
total_count: pages.length,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
catch (e) {
|
|
482
|
+
return fail(toCliError(e));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Per-user canvas usage from `GET /analytics/canvases/users` (editor/admin tier, PIT-9589): one row
|
|
487
|
+
* per user and canvas with visits, seconds and the last visit; with `--canvas-id`, each user's
|
|
488
|
+
* seconds per page (section) too. Internal users only, presentation time only. Dates default to
|
|
489
|
+
* the last 30 days.
|
|
490
|
+
*/
|
|
491
|
+
export async function runAnalyticsCanvasUsers(opts, deps = {}) {
|
|
492
|
+
try {
|
|
493
|
+
if (!opts.instanceId)
|
|
494
|
+
throw createCliError('VALIDATION', '--instance-id is required for analytics canvas-users');
|
|
495
|
+
const client = clickhouseClientFor(opts, deps);
|
|
496
|
+
const windowed = withDefaultWindow(opts, todayIso());
|
|
497
|
+
const page = Number.isInteger(opts.page) && opts.page > 0 ? opts.page : 1;
|
|
498
|
+
const pageSize = pageSizeFor(opts.limit);
|
|
499
|
+
const params = {
|
|
500
|
+
instance_id: opts.instanceId,
|
|
501
|
+
start_date: windowed.startDate,
|
|
502
|
+
end_date: windowed.endDate,
|
|
503
|
+
page,
|
|
504
|
+
page_size: pageSize,
|
|
505
|
+
};
|
|
506
|
+
if (opts.canvasId)
|
|
507
|
+
params.canvas_id = opts.canvasId;
|
|
508
|
+
if (opts.groupIds)
|
|
509
|
+
params.group_ids = opts.groupIds;
|
|
510
|
+
if (opts.userEmails)
|
|
511
|
+
params.user_emails = opts.userEmails;
|
|
512
|
+
const res = await client.get('/analytics/canvases/users', { params });
|
|
513
|
+
const data = (res.data ?? {}).data ?? {};
|
|
514
|
+
const rows = Array.isArray(data.rows) ? data.rows.slice(0, pageSize) : [];
|
|
515
|
+
const total = typeof data.total === 'number' ? data.total : rows.length;
|
|
516
|
+
const hasMore = (page - 1) * pageSize + rows.length < total;
|
|
517
|
+
return ok({
|
|
518
|
+
start_date: windowed.startDate,
|
|
519
|
+
end_date: windowed.endDate,
|
|
520
|
+
...(opts.canvasId ? { canvas_id: opts.canvasId } : {}),
|
|
521
|
+
rows,
|
|
522
|
+
total_count: total,
|
|
523
|
+
returned: rows.length,
|
|
524
|
+
page,
|
|
525
|
+
has_more: hasMore,
|
|
526
|
+
...(hasMore
|
|
527
|
+
? {
|
|
528
|
+
truncated: `Showing ${rows.length} of ${total.toLocaleString('en-US')} (page ${page}) — pass page=${page + 1} or narrow with filters`,
|
|
529
|
+
}
|
|
530
|
+
: {}),
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
catch (e) {
|
|
534
|
+
return fail(toCliError(e));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/** Viewers read per call when no single viewer is named; each costs one timeline query. */
|
|
538
|
+
export const VIEWER_VISITS_MAX_VIEWERS = 25;
|
|
539
|
+
const VIEWER_VISITS_DEFAULT_VIEWERS = 10;
|
|
540
|
+
const VIEWER_VISITS_CONCURRENCY = 3;
|
|
541
|
+
/**
|
|
542
|
+
* Per viewer of a DSR canvas: each visit, and in it which sections, files and pages were viewed for
|
|
543
|
+
* how long (PIT-9589). One viewer (`--user`, verbatim from `viewers`) or the most recently active
|
|
544
|
+
* viewers (`--limit`, default 10, max 25: one timeline query each). `--format csv` returns the same
|
|
545
|
+
* as CSV text, one row per viewer, visit and item, for export. Reads
|
|
546
|
+
* `/analytics/canvas-details/{viewers,sessions}` (any member, as `viewers`/`sessions`); the server
|
|
547
|
+
* applies its own default window when dates are omitted.
|
|
548
|
+
*/
|
|
549
|
+
export async function runAnalyticsViewerVisits(opts, deps = {}) {
|
|
550
|
+
try {
|
|
551
|
+
if (!opts.instanceId)
|
|
552
|
+
throw createCliError('VALIDATION', '--instance-id is required for analytics viewer-visits');
|
|
553
|
+
if (!opts.canvasId)
|
|
554
|
+
throw createCliError('VALIDATION', '--canvas-id is required for analytics viewer-visits');
|
|
555
|
+
const format = (opts.format ?? 'json').toLowerCase();
|
|
556
|
+
if (format !== 'json' && format !== 'csv')
|
|
557
|
+
throw createCliError('VALIDATION', '--format must be json or csv');
|
|
558
|
+
const instanceId = opts.instanceId;
|
|
559
|
+
const canvasId = opts.canvasId;
|
|
560
|
+
const client = clickhouseClientFor(opts, deps);
|
|
561
|
+
const dates = {};
|
|
562
|
+
if (opts.startDate)
|
|
563
|
+
dates.start_date = opts.startDate;
|
|
564
|
+
if (opts.endDate)
|
|
565
|
+
dates.end_date = opts.endDate;
|
|
566
|
+
let viewers;
|
|
567
|
+
let viewersTotal;
|
|
568
|
+
if (opts.userEmail) {
|
|
569
|
+
viewers = [opts.userEmail];
|
|
570
|
+
viewersTotal = 1;
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
const want = Math.min(Math.max(Number.isFinite(opts.limit) ? opts.limit : VIEWER_VISITS_DEFAULT_VIEWERS, 1), VIEWER_VISITS_MAX_VIEWERS);
|
|
574
|
+
const params = {
|
|
575
|
+
instance_id: instanceId,
|
|
576
|
+
canvas_id: canvasId,
|
|
577
|
+
page_size: want,
|
|
578
|
+
sort_by: 'last_active',
|
|
579
|
+
sort_order: 'desc',
|
|
580
|
+
...dates,
|
|
581
|
+
};
|
|
582
|
+
if (opts.viewerType)
|
|
583
|
+
params.viewer_type = opts.viewerType;
|
|
584
|
+
const res = await client.get('/analytics/canvas-details/viewers', { params });
|
|
585
|
+
const list = (res.data ?? {})
|
|
586
|
+
.viewers;
|
|
587
|
+
const items = (list?.items ?? []).slice(0, want);
|
|
588
|
+
viewers = items.map((v) => String(v.normalized_email ?? v.email ?? '')).filter(Boolean);
|
|
589
|
+
viewersTotal = typeof list?.total === 'number' ? list.total : viewers.length;
|
|
590
|
+
}
|
|
591
|
+
const results = new Array(viewers.length);
|
|
592
|
+
let next = 0;
|
|
593
|
+
const worker = async () => {
|
|
594
|
+
while (next < viewers.length) {
|
|
595
|
+
const i = next++;
|
|
596
|
+
const res = await client.get('/analytics/canvas-details/sessions', {
|
|
597
|
+
params: { instance_id: instanceId, canvas_id: canvasId, user_email: viewers[i], ...dates },
|
|
598
|
+
});
|
|
599
|
+
const events = (res.data ?? {}).events ?? [];
|
|
600
|
+
results[i] = { viewer: viewers[i], visits: summarizeVisits(events, canvasId) };
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
await Promise.all(Array.from({ length: Math.min(VIEWER_VISITS_CONCURRENCY, viewers.length) }, worker));
|
|
604
|
+
const truncated = viewersTotal > viewers.length
|
|
605
|
+
? `Covers the ${viewers.length} most recently active of ${viewersTotal.toLocaleString('en-US')} viewers — pass userEmail, or a higher limit (max ${VIEWER_VISITS_MAX_VIEWERS})`
|
|
606
|
+
: undefined;
|
|
607
|
+
const common = {
|
|
608
|
+
canvas_id: canvasId,
|
|
609
|
+
viewers_returned: results.length,
|
|
610
|
+
viewers_total: viewersTotal,
|
|
611
|
+
...(truncated ? { truncated } : {}),
|
|
612
|
+
};
|
|
613
|
+
if (format === 'csv') {
|
|
614
|
+
return ok({ ...common, format: 'csv', filename: `dsr-viewer-visits-${canvasId}.csv`, csv: visitsToCsv(results) });
|
|
615
|
+
}
|
|
616
|
+
return ok({ ...common, viewers: results });
|
|
617
|
+
}
|
|
618
|
+
catch (e) {
|
|
619
|
+
return fail(toCliError(e));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
368
622
|
export function createAnalyticsCommand() {
|
|
369
623
|
const cmd = new Command('analytics').description('read rollup analytics totals (requires an org-admin API key)');
|
|
370
624
|
withAuthFlags(cmd.command('org'))
|
|
@@ -495,5 +749,59 @@ export function createAnalyticsCommand() {
|
|
|
495
749
|
const globals = command.optsWithGlobals();
|
|
496
750
|
emitEnvelope(await runAnalyticsSessions({ ...options, userEmail: options.user, quiet: globals.quiet }), globals);
|
|
497
751
|
});
|
|
752
|
+
withAuthFlags(cmd.command('file-users'))
|
|
753
|
+
.option('--instance-id <id>', 'Instance ID (required)')
|
|
754
|
+
.option('--file-id <id>', 'File ID (required)')
|
|
755
|
+
.option('--start-date <iso>', 'Window start (ISO date; defaults to 30 days ago)')
|
|
756
|
+
.option('--end-date <iso>', 'Window end (ISO date; defaults to today)')
|
|
757
|
+
.option('--page-number <n>', 'Only this page of the file (0-based)', (v) => parseInt(v, 10))
|
|
758
|
+
.option('--group-ids <csv>', 'Comma-separated distribution-group IDs')
|
|
759
|
+
.option('--user-emails <csv>', 'Comma-separated user emails to scope by')
|
|
760
|
+
.option('--limit <n>', 'Max rows to return (default 20, max 100)', (v) => parseInt(v, 10))
|
|
761
|
+
.description('who opened one file: opens, seconds and days active per user (editor/admin key)')
|
|
762
|
+
.action(async (options, command) => {
|
|
763
|
+
const globals = command.optsWithGlobals();
|
|
764
|
+
emitEnvelope(await runAnalyticsFileUsers({ ...options, quiet: globals.quiet }), globals);
|
|
765
|
+
});
|
|
766
|
+
withAuthFlags(cmd.command('file-pages'))
|
|
767
|
+
.option('--instance-id <id>', 'Instance ID (required)')
|
|
768
|
+
.option('--file-id <id>', 'File ID (required)')
|
|
769
|
+
.option('--start-date <iso>', 'Window start (ISO date; defaults to 30 days ago)')
|
|
770
|
+
.option('--end-date <iso>', 'Window end (ISO date; defaults to today)')
|
|
771
|
+
.option('--group-ids <csv>', 'Comma-separated distribution-group IDs')
|
|
772
|
+
.option('--user-emails <csv>', 'Comma-separated user emails to scope by (one email: that user)')
|
|
773
|
+
.description('time per page of one file (editor/admin key)')
|
|
774
|
+
.action(async (options, command) => {
|
|
775
|
+
const globals = command.optsWithGlobals();
|
|
776
|
+
emitEnvelope(await runAnalyticsFilePages({ ...options, quiet: globals.quiet }), globals);
|
|
777
|
+
});
|
|
778
|
+
withAuthFlags(cmd.command('canvas-users'))
|
|
779
|
+
.option('--instance-id <id>', 'Instance ID (required)')
|
|
780
|
+
.option('--canvas-id <id>', "One canvas: adds each user's seconds per page (section)")
|
|
781
|
+
.option('--start-date <iso>', 'Window start (ISO date; defaults to 30 days ago)')
|
|
782
|
+
.option('--end-date <iso>', 'Window end (ISO date; defaults to today)')
|
|
783
|
+
.option('--group-ids <csv>', 'Comma-separated distribution-group IDs')
|
|
784
|
+
.option('--user-emails <csv>', 'Comma-separated user emails to scope by')
|
|
785
|
+
.option('--page <n>', 'Page number', (v) => parseInt(v, 10))
|
|
786
|
+
.option('--limit <n>', 'Rows per page (default 20, max 100)', (v) => parseInt(v, 10))
|
|
787
|
+
.description('per-user canvas usage: visits and seconds per user and canvas (editor/admin key)')
|
|
788
|
+
.action(async (options, command) => {
|
|
789
|
+
const globals = command.optsWithGlobals();
|
|
790
|
+
emitEnvelope(await runAnalyticsCanvasUsers({ ...options, quiet: globals.quiet }), globals);
|
|
791
|
+
});
|
|
792
|
+
withAuthFlags(cmd.command('viewer-visits'))
|
|
793
|
+
.option('--instance-id <id>', 'Instance ID (required)')
|
|
794
|
+
.option('--canvas-id <id>', 'DSR canvas ID (required)')
|
|
795
|
+
.option('--user <email>', 'One viewer, verbatim from analytics viewers (default: the most recent viewers)')
|
|
796
|
+
.option('--viewer-type <type>', "Viewer type when listing: all, internal, external (default 'all')")
|
|
797
|
+
.option('--start-date <iso>', 'Window start (ISO date; server default applies if omitted)')
|
|
798
|
+
.option('--end-date <iso>', 'Window end (ISO date; server default applies if omitted)')
|
|
799
|
+
.option('--limit <n>', 'Viewers to read when no --user (default 10, max 25)', (v) => parseInt(v, 10))
|
|
800
|
+
.option('--format <fmt>', 'json (default) or csv')
|
|
801
|
+
.description('per DSR viewer: each visit and the sections, files and pages viewed with their time')
|
|
802
|
+
.action(async (options, command) => {
|
|
803
|
+
const globals = command.optsWithGlobals();
|
|
804
|
+
emitEnvelope(await runAnalyticsViewerVisits({ ...options, userEmail: options.user, quiet: globals.quiet }), globals);
|
|
805
|
+
});
|
|
498
806
|
return cmd;
|
|
499
807
|
}
|
|
@@ -69,8 +69,9 @@ export async function runMetadataTemplateUpsert(opts, deps = {}) {
|
|
|
69
69
|
const UPSERT_HELP = `
|
|
70
70
|
Upserts the instance's metadata-field schema. POST is an upsert keyed by
|
|
71
71
|
instance that MERGES the incoming sections by section 'label' — a section whose
|
|
72
|
-
label matches an existing one is replaced
|
|
73
|
-
|
|
72
|
+
label matches an existing one is replaced whole (fields left out of it are
|
|
73
|
+
removed), other sections are preserved. It cannot remove a whole section (a
|
|
74
|
+
full-replace / UI operation).
|
|
74
75
|
--instance-id the instance whose schema to upsert (required)
|
|
75
76
|
--sections inline JSON array of sections, OR
|
|
76
77
|
--sections-file path to a JSON file with the sections array (exactly one)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { AxiosInstance } from 'axios';
|
|
3
|
+
import { type CommonOpts } from '../core/http.js';
|
|
4
|
+
import { type Envelope } from '../core/envelope.js';
|
|
5
|
+
/**
|
|
6
|
+
* AI Roleplay reads (PIT-9200, Phase 1: read-only). Two sources:
|
|
7
|
+
* - clickhouse-api `/analytics/roleplay/{kpis,sessions}` (the `roleplay_statements` replica,
|
|
8
|
+
* instance or org admins only): `kpis`, `sessions`, `rep-summary`;
|
|
9
|
+
* - next-core `/core/api/protected/roleplay/coach/*` and appsDB (`cch-*` rows in Postgres):
|
|
10
|
+
* `team`, `session`, `scenarios`.
|
|
11
|
+
* A session with no score is unscored, never 0. Practice Studio takes (`manager-video-practice`)
|
|
12
|
+
* never appear: the coach routes exclude them by default and `team` drops practice assignments.
|
|
13
|
+
*/
|
|
14
|
+
export interface RoleplayDeps {
|
|
15
|
+
client?: AxiosInstance;
|
|
16
|
+
}
|
|
17
|
+
export declare const ROLEPLAY_DIFFICULTIES: readonly ["easy", "medium", "hard"];
|
|
18
|
+
export declare const ROLEPLAY_SESSION_SORT: readonly ["session_at", "rep_name", "score", "duration_ms"];
|
|
19
|
+
export declare const ROLEPLAY_GROUP_BY: readonly ["rep", "scenario"];
|
|
20
|
+
/** rep-summary reads at most this many pages of this size (5000 sessions), newest first. */
|
|
21
|
+
export declare const REP_SUMMARY_PAGE_SIZE = 1000;
|
|
22
|
+
export declare const REP_SUMMARY_MAX_PAGES = 5;
|
|
23
|
+
export declare const REP_SUMMARY_MAX_GROUPS = 50;
|
|
24
|
+
/** next-core's coach/data returns at most this many sessions, newest first, and no total. */
|
|
25
|
+
export declare const COACH_DATA_SESSION_CAP = 1000;
|
|
26
|
+
export declare const TEAM_MAX_ASSIGNMENTS = 50;
|
|
27
|
+
export declare const TEAM_MAX_REPS_PER_ASSIGNMENT = 100;
|
|
28
|
+
export interface RoleplayFilterOpts extends CommonOpts {
|
|
29
|
+
instanceId?: string;
|
|
30
|
+
startDate?: string;
|
|
31
|
+
endDate?: string;
|
|
32
|
+
rep?: string;
|
|
33
|
+
scenario?: string;
|
|
34
|
+
difficulty?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Totals for the window: sessions, scored/unscored, mean score over SCORED sessions only, active reps. */
|
|
37
|
+
export declare function runRoleplayKpis(opts: RoleplayFilterOpts, deps?: RoleplayDeps): Promise<Envelope<unknown>>;
|
|
38
|
+
declare const SESSION_FIELDS: readonly ["entry_id", "session_at", "rep_name", "who", "scenario_name", "persona_name", "difficulty", "score", "evaluated", "rating", "duration_ms"];
|
|
39
|
+
export type RoleplaySessionRow = Record<(typeof SESSION_FIELDS)[number], unknown>;
|
|
40
|
+
/** The row allowlist: nothing else from the replica (provider, recording flags, rubric) leaves. */
|
|
41
|
+
export declare function pickSessionRow(row: Record<string, unknown>): RoleplaySessionRow;
|
|
42
|
+
export interface RoleplaySessionsOpts extends RoleplayFilterOpts {
|
|
43
|
+
sortBy?: string;
|
|
44
|
+
sortOrder?: string;
|
|
45
|
+
limit?: number;
|
|
46
|
+
page?: number;
|
|
47
|
+
}
|
|
48
|
+
export declare function runRoleplaySessions(opts: RoleplaySessionsOpts, deps?: RoleplayDeps): Promise<Envelope<unknown>>;
|
|
49
|
+
export interface RoleplayRepSummaryOpts extends RoleplayFilterOpts {
|
|
50
|
+
groupBy?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Per-rep (keyed on the rep email `who`) or per-scenario (keyed on `scenario_id`) aggregates over
|
|
54
|
+
* the sessions list, read in pages of 1000 (newest first) up to 5000. `complete: false` when the
|
|
55
|
+
* window holds more than that; then the figures cover the newest 5000 only.
|
|
56
|
+
*/
|
|
57
|
+
export declare function runRoleplayRepSummary(opts: RoleplayRepSummaryOpts, deps?: RoleplayDeps): Promise<Envelope<unknown>>;
|
|
58
|
+
export interface RoleplayTeamOpts extends CommonOpts {
|
|
59
|
+
instanceId?: string;
|
|
60
|
+
assignmentId?: string;
|
|
61
|
+
status?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Assignment completion per rep, ported from the manager app: an admin gets every roleplay
|
|
65
|
+
* assignment on the instance (`scope=instance`), anyone else the assignments they coach.
|
|
66
|
+
* Statuses follow `deriveStatus`; raw sessions, evaluations and comments never leave.
|
|
67
|
+
*/
|
|
68
|
+
export declare function runRoleplayTeam(opts: RoleplayTeamOpts, deps?: RoleplayDeps): Promise<Envelope<unknown>>;
|
|
69
|
+
export interface RoleplaySessionGetOpts extends CommonOpts {
|
|
70
|
+
instanceId?: string;
|
|
71
|
+
entryId?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The allowlist. Never the transcript, messages, contact name, graded snapshot, voice session,
|
|
75
|
+
* recording or audio ids, or any video-evaluation field.
|
|
76
|
+
*/
|
|
77
|
+
export declare function pickSessionDetail(id: string, data: Record<string, unknown>): Record<string, unknown>;
|
|
78
|
+
export declare function runRoleplaySessionGet(opts: RoleplaySessionGetOpts, deps?: RoleplayDeps): Promise<Envelope<unknown>>;
|
|
79
|
+
/** The trimmed catalog: never the persona prompt or the evaluation prompt. */
|
|
80
|
+
export declare function pickScenario(s: Record<string, unknown>): Record<string, unknown>;
|
|
81
|
+
export declare function runRoleplayScenariosList(opts: CommonOpts & {
|
|
82
|
+
instanceId?: string;
|
|
83
|
+
}, deps?: RoleplayDeps): Promise<Envelope<unknown>>;
|
|
84
|
+
export declare function createRoleplayCommand(): Command;
|
|
85
|
+
export {};
|