@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One DSR viewer's event timeline (`/analytics/canvas-details/sessions`) → visits, and per visit the
|
|
3
|
+
* sections, files and pages viewed with their time (PIT-9589). The visit rule is the Sharing
|
|
4
|
+
* Analytics app's (`packages/apps/canvas-analytics/src/lib/reconstructSessions.ts`): a visit opens
|
|
5
|
+
* on an `Entered` of the canvas itself and is keyed by its `event_pair_id`; its engaged time is the
|
|
6
|
+
* sum of its focus segments (canvas Entered → Exited). Change them together.
|
|
7
|
+
*
|
|
8
|
+
* Item time: the sum of the item's `Exited` durations (`forhowlong`, seconds) in the visit. An
|
|
9
|
+
* `Entered` with no `Exited` (the unload beacon was dropped, PIT-5833) or an `Exited` reporting 0 is
|
|
10
|
+
* estimated from the gap to the next event of the visit, as the app's timeline does, and flagged
|
|
11
|
+
* `estimated`.
|
|
12
|
+
*/
|
|
13
|
+
const ITEM_TYPES = { Section: 'section', File: 'file', Page: 'page' };
|
|
14
|
+
// The page index is the LAST path segment, `<fileId>/page/<n>` or the older `<fileId>/<n>`, 0-based.
|
|
15
|
+
const PAGE_INDEX_SUFFIX = /\/(?:page\/)?(\d+)$/;
|
|
16
|
+
const ms = (ts) => new Date(ts).getTime();
|
|
17
|
+
const seconds = (from, to) => Math.max(0, Math.round((ms(to) - ms(from)) / 1000));
|
|
18
|
+
function sortPriority(e, anchorId) {
|
|
19
|
+
const isAnchor = e.entity_id === anchorId;
|
|
20
|
+
if (!isAnchor && e.action === 'Exited')
|
|
21
|
+
return 1;
|
|
22
|
+
if (isAnchor && e.action === 'Exited')
|
|
23
|
+
return 2;
|
|
24
|
+
if (isAnchor && e.action === 'Entered')
|
|
25
|
+
return 3;
|
|
26
|
+
if (!isAnchor && e.action === 'Entered')
|
|
27
|
+
return 4;
|
|
28
|
+
return 5;
|
|
29
|
+
}
|
|
30
|
+
/** `reconstructSessions`' grouping: events from an anchor `Entered` until the next one, by pair key. */
|
|
31
|
+
export function groupVisits(events, anchorId) {
|
|
32
|
+
const sorted = [...events].sort((a, b) => a.timestamp.localeCompare(b.timestamp) || sortPriority(a, anchorId) - sortPriority(b, anchorId));
|
|
33
|
+
const groups = new Map();
|
|
34
|
+
let key = null;
|
|
35
|
+
let legacy = 0;
|
|
36
|
+
for (const e of sorted) {
|
|
37
|
+
if (e.entity_id === anchorId && e.action === 'Entered') {
|
|
38
|
+
key = e.event_pair_id ? `pair:${e.event_pair_id}` : `legacy:${++legacy}`;
|
|
39
|
+
}
|
|
40
|
+
if (!key)
|
|
41
|
+
continue;
|
|
42
|
+
const g = groups.get(key);
|
|
43
|
+
if (g)
|
|
44
|
+
g.push(e);
|
|
45
|
+
else
|
|
46
|
+
groups.set(key, [e]);
|
|
47
|
+
}
|
|
48
|
+
return Array.from(groups.values()).filter((g) => g.length > 0);
|
|
49
|
+
}
|
|
50
|
+
/** `computeEngagementDuration`: the wall-clock of each canvas focus segment, summed. */
|
|
51
|
+
export function engagedSeconds(events, anchorId) {
|
|
52
|
+
let total = 0;
|
|
53
|
+
let start = null;
|
|
54
|
+
let last = null;
|
|
55
|
+
const flush = (end) => {
|
|
56
|
+
if (start && end)
|
|
57
|
+
total += seconds(start, end);
|
|
58
|
+
start = null;
|
|
59
|
+
last = null;
|
|
60
|
+
};
|
|
61
|
+
for (const e of events) {
|
|
62
|
+
if (e.entity_id === anchorId) {
|
|
63
|
+
if (e.action === 'Entered') {
|
|
64
|
+
flush(last);
|
|
65
|
+
start = e.timestamp;
|
|
66
|
+
last = e.timestamp;
|
|
67
|
+
}
|
|
68
|
+
else if (e.action === 'Exited') {
|
|
69
|
+
flush(e.timestamp);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (start) {
|
|
73
|
+
last = e.timestamp;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
flush(last);
|
|
77
|
+
return total;
|
|
78
|
+
}
|
|
79
|
+
function itemsOf(events, anchorId) {
|
|
80
|
+
const items = new Map();
|
|
81
|
+
const open = new Map(); // entity → indexes of unmatched Entered
|
|
82
|
+
const item = (e) => {
|
|
83
|
+
const type = ITEM_TYPES[e.original_event_name];
|
|
84
|
+
if (!type || e.entity_id === anchorId)
|
|
85
|
+
return null;
|
|
86
|
+
let it = items.get(e.entity_id);
|
|
87
|
+
if (!it) {
|
|
88
|
+
const pageIdx = type === 'page' ? PAGE_INDEX_SUFFIX.exec(e.entity_id) : null;
|
|
89
|
+
it = {
|
|
90
|
+
type,
|
|
91
|
+
id: e.entity_id,
|
|
92
|
+
name: e.entity_name || e.entity_type || e.entity_id,
|
|
93
|
+
...(pageIdx ? { page_number: Number(pageIdx[1]) + 1 } : {}),
|
|
94
|
+
views: 0,
|
|
95
|
+
seconds: 0,
|
|
96
|
+
estimated: false,
|
|
97
|
+
};
|
|
98
|
+
items.set(e.entity_id, it);
|
|
99
|
+
}
|
|
100
|
+
return it;
|
|
101
|
+
};
|
|
102
|
+
const gapToNext = (i) => (i + 1 < events.length ? seconds(events[i].timestamp, events[i + 1].timestamp) : 0);
|
|
103
|
+
events.forEach((e, i) => {
|
|
104
|
+
const it = item(e);
|
|
105
|
+
if (!it)
|
|
106
|
+
return;
|
|
107
|
+
if (e.action === 'Entered') {
|
|
108
|
+
it.views += 1;
|
|
109
|
+
open.set(e.entity_id, [...(open.get(e.entity_id) ?? []), i]);
|
|
110
|
+
}
|
|
111
|
+
else if (e.action === 'Exited') {
|
|
112
|
+
const pending = open.get(e.entity_id) ?? [];
|
|
113
|
+
const enteredAt = pending.shift();
|
|
114
|
+
open.set(e.entity_id, pending);
|
|
115
|
+
if (enteredAt === undefined)
|
|
116
|
+
it.views = Math.max(it.views, 1); // an Exited-only view (adblock): still a view
|
|
117
|
+
const reported = typeof e.duration === 'number' && e.duration > 0 ? e.duration : 0;
|
|
118
|
+
if (reported > 0) {
|
|
119
|
+
it.seconds += reported;
|
|
120
|
+
}
|
|
121
|
+
else if (enteredAt !== undefined) {
|
|
122
|
+
it.seconds += seconds(events[enteredAt].timestamp, e.timestamp);
|
|
123
|
+
it.estimated = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// Entered with no Exited in the visit: the gap to the next event.
|
|
128
|
+
for (const [id, pending] of open) {
|
|
129
|
+
const it = items.get(id);
|
|
130
|
+
for (const i of pending) {
|
|
131
|
+
it.seconds += gapToNext(i);
|
|
132
|
+
it.estimated = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return Array.from(items.values());
|
|
136
|
+
}
|
|
137
|
+
export function summarizeVisits(events, canvasId) {
|
|
138
|
+
return groupVisits(events, canvasId).map((g, i) => ({
|
|
139
|
+
visit: i + 1,
|
|
140
|
+
started_at: g[0].timestamp,
|
|
141
|
+
ended_at: g[g.length - 1].timestamp,
|
|
142
|
+
engaged_seconds: engagedSeconds(g, canvasId),
|
|
143
|
+
items: itemsOf(g, canvasId),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
/** RFC 4180 cell; a leading = + - @ is prefixed with ' so a spreadsheet never runs it as a formula. */
|
|
147
|
+
export function csvCell(v) {
|
|
148
|
+
let s = v == null ? '' : String(v);
|
|
149
|
+
if (/^[=+\-@\t\r]/.test(s))
|
|
150
|
+
s = `'${s}`;
|
|
151
|
+
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
152
|
+
}
|
|
153
|
+
export const VISITS_CSV_HEADER = [
|
|
154
|
+
'viewer',
|
|
155
|
+
'visit',
|
|
156
|
+
'visit_started_at',
|
|
157
|
+
'visit_engaged_seconds',
|
|
158
|
+
'item_type',
|
|
159
|
+
'item_name',
|
|
160
|
+
'page_number',
|
|
161
|
+
'item_id',
|
|
162
|
+
'views',
|
|
163
|
+
'seconds',
|
|
164
|
+
'estimated',
|
|
165
|
+
];
|
|
166
|
+
export function visitsToCsv(viewers) {
|
|
167
|
+
const lines = [VISITS_CSV_HEADER.join(',')];
|
|
168
|
+
for (const { viewer, visits } of viewers) {
|
|
169
|
+
for (const v of visits) {
|
|
170
|
+
const items = v.items.length ? v.items : [null];
|
|
171
|
+
for (const it of items) {
|
|
172
|
+
lines.push([
|
|
173
|
+
viewer,
|
|
174
|
+
v.visit,
|
|
175
|
+
v.started_at,
|
|
176
|
+
v.engaged_seconds,
|
|
177
|
+
it?.type ?? '',
|
|
178
|
+
it?.name ?? '',
|
|
179
|
+
it?.page_number ?? '',
|
|
180
|
+
it?.id ?? '',
|
|
181
|
+
it?.views ?? '',
|
|
182
|
+
it?.seconds ?? '',
|
|
183
|
+
it ? String(it.estimated) : '',
|
|
184
|
+
]
|
|
185
|
+
.map(csvCell)
|
|
186
|
+
.join(','));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return lines.join('\n') + '\n';
|
|
191
|
+
}
|
|
@@ -28,8 +28,6 @@ export declare function orgSubdomain(domain: string): string;
|
|
|
28
28
|
* in step when a tenant is added.
|
|
29
29
|
*/
|
|
30
30
|
export declare const AUTH0_NATIVE_CLIENT_IDS: Readonly<Record<string, string>>;
|
|
31
|
-
/** The hosted MCP connector's access for an org (`pitcher_organization.settings.mcp.access`, PIT-9768). */
|
|
32
|
-
export type McpAccess = 'off' | 'read_only' | 'full';
|
|
33
31
|
export interface OrgConfig {
|
|
34
32
|
/** The Auth0 CUSTOM domain (`auth0.test-eu.pitcher.com`) — the key into the client-id maps. */
|
|
35
33
|
auth0Domain: string;
|
|
@@ -38,11 +36,6 @@ export interface OrgConfig {
|
|
|
38
36
|
organization: string | null;
|
|
39
37
|
/** `'test' | 'production'`. OPTIONAL upstream: treat unknown as production. */
|
|
40
38
|
environment?: string;
|
|
41
|
-
/**
|
|
42
|
-
* undefined = UNKNOWN: next-core's org lookup failed, or that region's next-core predates the field. Never
|
|
43
|
-
* read it as "off" and never as "on" — the hosted server retries or refuses (`mcp-server` `src/oauth/access.ts`).
|
|
44
|
-
*/
|
|
45
|
-
mcpAccess?: McpAccess;
|
|
46
39
|
}
|
|
47
40
|
export interface Auth0Tokens {
|
|
48
41
|
access_token: string;
|
|
@@ -38,7 +38,6 @@ export const AUTH0_NATIVE_CLIENT_IDS = {
|
|
|
38
38
|
'auth0.test-us.pitcher.com': 'sd6biLE9qGBwUfSzl2uRC1p5JdhKY06N',
|
|
39
39
|
'auth0.us.pitcher.com': 'k6BgMI2Oy26RnyucUsWiL81JOeaufECm',
|
|
40
40
|
};
|
|
41
|
-
const MCP_ACCESS = new Set(['off', 'read_only', 'full']);
|
|
42
41
|
export function createUpstreamError(kind, message) {
|
|
43
42
|
return Object.assign(new Error(message), { isUpstreamError: true, kind });
|
|
44
43
|
}
|
|
@@ -98,8 +97,6 @@ export async function fetchOrgConfig(domain, opts = {}) {
|
|
|
98
97
|
audience: a.audience,
|
|
99
98
|
organization: typeof a.organization === 'string' && a.organization ? a.organization : null,
|
|
100
99
|
environment: typeof json.environment === 'string' ? json.environment : undefined,
|
|
101
|
-
// A value outside the three is a newer next-core than this code: refuse it like "off", not like unknown.
|
|
102
|
-
mcpAccess: typeof json.mcp?.access === 'string' ? (MCP_ACCESS.has(json.mcp.access) ? json.mcp.access : 'off') : undefined,
|
|
103
100
|
};
|
|
104
101
|
}
|
|
105
102
|
async function postJson(url, body, opts) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI Roleplay manager-app's per-rep rules, ported 1:1 so the MCP team tool and the manager
|
|
3
|
+
* app give the same answer on the same data (PIT-9200). Sources, in ng-production-apps
|
|
4
|
+
* `pitcher-coach/manager-app/src/`: `lib/session-score.ts` (reading a score), `lib/stats.ts`
|
|
5
|
+
* (best, mean, trend), `lib/status.ts` (`deriveStatus`) and `hooks/useManagerData.ts`
|
|
6
|
+
* (`getAssignmentSessions`, `getRepStats`). Change them together.
|
|
7
|
+
*
|
|
8
|
+
* The one rule every aggregate here obeys: a session is unscored iff `overallScore` is not a
|
|
9
|
+
* finite number. An unscored session is never read as 0.
|
|
10
|
+
*/
|
|
11
|
+
export interface RoleplaySessionLike {
|
|
12
|
+
userId?: string;
|
|
13
|
+
scenarioId?: string;
|
|
14
|
+
timestamp?: string;
|
|
15
|
+
overallScore?: number | null;
|
|
16
|
+
durationMs?: number | null;
|
|
17
|
+
evaluation?: {
|
|
18
|
+
category_scores?: Array<{
|
|
19
|
+
id?: string;
|
|
20
|
+
skillId?: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
score?: number;
|
|
23
|
+
}>;
|
|
24
|
+
} | null;
|
|
25
|
+
}
|
|
26
|
+
export interface RoleplayAssignmentLike {
|
|
27
|
+
scenarioId: string;
|
|
28
|
+
type: 'mandatory' | 'optional' | string;
|
|
29
|
+
passingScore: number;
|
|
30
|
+
dueDate: string | null;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
resolvedUserIds: string[];
|
|
33
|
+
}
|
|
34
|
+
export type RepStatus = 'passed' | 'in_progress' | 'not_started' | 'overdue' | 'needs_coaching';
|
|
35
|
+
/** The session's score, or null when it has none (`toFiniteOrNull`). */
|
|
36
|
+
export declare function sessionScore(session: RoleplaySessionLike): number | null;
|
|
37
|
+
/** Mean of the finite values, rounded, or null when none are finite (`meanOfFinite`). */
|
|
38
|
+
export declare function meanOfFinite(values: readonly (number | null | undefined)[]): number | null;
|
|
39
|
+
/** Largest finite value, or null (`maxOfFinite`). */
|
|
40
|
+
export declare function maxOfFinite(values: readonly (number | null | undefined)[]): number | null;
|
|
41
|
+
/** Earliest-to-latest window delta over the scored entries only (`scoreTrend`). */
|
|
42
|
+
export declare function scoreTrend(scores: readonly (number | null | undefined)[], windowFor: (scoredCount: number) => number): number | null;
|
|
43
|
+
export declare const avgScore: (sessions: RoleplaySessionLike[]) => number | null;
|
|
44
|
+
export declare const bestScore: (sessions: RoleplaySessionLike[]) => number | null;
|
|
45
|
+
export declare const scoredCount: (sessions: RoleplaySessionLike[]) => number;
|
|
46
|
+
/** Up to 3 sessions per end, capped at half the scored series (`calcTrend`). */
|
|
47
|
+
export declare function calcTrend(sessions: RoleplaySessionLike[]): number | null;
|
|
48
|
+
/**
|
|
49
|
+
* `deriveStatus`: the raw best score is compared with `passingScore` as stored (legacy
|
|
50
|
+
* evaluations may grade on a 5-point scale; the manager app does not normalise either).
|
|
51
|
+
*/
|
|
52
|
+
export declare function deriveStatus(sessions: RoleplaySessionLike[], assignment: RoleplayAssignmentLike, now?: Date): RepStatus;
|
|
53
|
+
/** `getAssignmentSessions`: same scenario, a rep of the assignment, on or after it was created. */
|
|
54
|
+
export declare function assignmentSessions<T extends RoleplaySessionLike>(sessions: T[], assignment: RoleplayAssignmentLike): T[];
|
|
55
|
+
export interface RepStats {
|
|
56
|
+
user_id: string;
|
|
57
|
+
sessions: number;
|
|
58
|
+
scored_sessions: number;
|
|
59
|
+
best_score: number | null;
|
|
60
|
+
avg_score: number | null;
|
|
61
|
+
trend: number | null;
|
|
62
|
+
status: RepStatus;
|
|
63
|
+
last_session_at: string | null;
|
|
64
|
+
total_duration_ms: number;
|
|
65
|
+
}
|
|
66
|
+
/** `getRepStats` for one rep of one assignment, without the raw sessions. */
|
|
67
|
+
export declare function repStats(userId: string, sessions: RoleplaySessionLike[], assignment: RoleplayAssignmentLike, now?: Date): RepStats;
|
|
68
|
+
export interface SkillAverage {
|
|
69
|
+
skill_id: string;
|
|
70
|
+
title: string;
|
|
71
|
+
avg_score: number | null;
|
|
72
|
+
scored_sessions: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Per-skill means over the sessions' `evaluation.category_scores`, keyed by `skillId` (the
|
|
76
|
+
* Skills Registry id). A legacy category without one falls back to its own `id`, which is stable
|
|
77
|
+
* within one scenario; an entry with neither, or with no finite score, is skipped.
|
|
78
|
+
*/
|
|
79
|
+
export declare function skillAverages(sessions: RoleplaySessionLike[]): SkillAverage[];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AI Roleplay manager-app's per-rep rules, ported 1:1 so the MCP team tool and the manager
|
|
3
|
+
* app give the same answer on the same data (PIT-9200). Sources, in ng-production-apps
|
|
4
|
+
* `pitcher-coach/manager-app/src/`: `lib/session-score.ts` (reading a score), `lib/stats.ts`
|
|
5
|
+
* (best, mean, trend), `lib/status.ts` (`deriveStatus`) and `hooks/useManagerData.ts`
|
|
6
|
+
* (`getAssignmentSessions`, `getRepStats`). Change them together.
|
|
7
|
+
*
|
|
8
|
+
* The one rule every aggregate here obeys: a session is unscored iff `overallScore` is not a
|
|
9
|
+
* finite number. An unscored session is never read as 0.
|
|
10
|
+
*/
|
|
11
|
+
/** The session's score, or null when it has none (`toFiniteOrNull`). */
|
|
12
|
+
export function sessionScore(session) {
|
|
13
|
+
const v = session.overallScore;
|
|
14
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
15
|
+
}
|
|
16
|
+
/** Mean of the finite values, rounded, or null when none are finite (`meanOfFinite`). */
|
|
17
|
+
export function meanOfFinite(values) {
|
|
18
|
+
const finite = values.filter((n) => typeof n === 'number' && Number.isFinite(n));
|
|
19
|
+
if (finite.length === 0)
|
|
20
|
+
return null;
|
|
21
|
+
return Math.round(finite.reduce((a, b) => a + b, 0) / finite.length);
|
|
22
|
+
}
|
|
23
|
+
/** Largest finite value, or null (`maxOfFinite`). */
|
|
24
|
+
export function maxOfFinite(values) {
|
|
25
|
+
const finite = values.filter((n) => typeof n === 'number' && Number.isFinite(n));
|
|
26
|
+
return finite.length === 0 ? null : Math.max(...finite);
|
|
27
|
+
}
|
|
28
|
+
/** Earliest-to-latest window delta over the scored entries only (`scoreTrend`). */
|
|
29
|
+
export function scoreTrend(scores, windowFor) {
|
|
30
|
+
const finite = scores.filter((n) => typeof n === 'number' && Number.isFinite(n));
|
|
31
|
+
if (finite.length < 2)
|
|
32
|
+
return null;
|
|
33
|
+
const n = Math.min(windowFor(finite.length), finite.length);
|
|
34
|
+
if (n < 1)
|
|
35
|
+
return null;
|
|
36
|
+
const mean = (xs) => xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
37
|
+
return Math.round(mean(finite.slice(-n)) - mean(finite.slice(0, n)));
|
|
38
|
+
}
|
|
39
|
+
const byTimestamp = (a, b) => new Date(a.timestamp ?? 0).getTime() - new Date(b.timestamp ?? 0).getTime();
|
|
40
|
+
export const avgScore = (sessions) => meanOfFinite(sessions.map(sessionScore));
|
|
41
|
+
export const bestScore = (sessions) => maxOfFinite(sessions.map(sessionScore));
|
|
42
|
+
export const scoredCount = (sessions) => sessions.filter((s) => sessionScore(s) !== null).length;
|
|
43
|
+
/** Up to 3 sessions per end, capped at half the scored series (`calcTrend`). */
|
|
44
|
+
export function calcTrend(sessions) {
|
|
45
|
+
const sorted = [...sessions].sort(byTimestamp);
|
|
46
|
+
return scoreTrend(sorted.map(sessionScore), (count) => Math.min(3, Math.floor(count / 2)));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `deriveStatus`: the raw best score is compared with `passingScore` as stored (legacy
|
|
50
|
+
* evaluations may grade on a 5-point scale; the manager app does not normalise either).
|
|
51
|
+
*/
|
|
52
|
+
export function deriveStatus(sessions, assignment, now = new Date()) {
|
|
53
|
+
const isOverdue = assignment.type === 'mandatory' && !!assignment.dueDate && new Date(assignment.dueDate) < now;
|
|
54
|
+
if (sessions.length === 0)
|
|
55
|
+
return isOverdue ? 'overdue' : 'not_started';
|
|
56
|
+
const best = bestScore(sessions);
|
|
57
|
+
if (best != null && best >= assignment.passingScore)
|
|
58
|
+
return 'passed';
|
|
59
|
+
if (isOverdue)
|
|
60
|
+
return 'overdue';
|
|
61
|
+
const trend = calcTrend(sessions);
|
|
62
|
+
if (scoredCount(sessions) >= 3 && trend != null && trend <= 0)
|
|
63
|
+
return 'needs_coaching';
|
|
64
|
+
return 'in_progress';
|
|
65
|
+
}
|
|
66
|
+
/** `getAssignmentSessions`: same scenario, a rep of the assignment, on or after it was created. */
|
|
67
|
+
export function assignmentSessions(sessions, assignment) {
|
|
68
|
+
const createdAt = new Date(assignment.createdAt).getTime();
|
|
69
|
+
return sessions.filter((s) => s.scenarioId === assignment.scenarioId &&
|
|
70
|
+
typeof s.userId === 'string' &&
|
|
71
|
+
assignment.resolvedUserIds.includes(s.userId) &&
|
|
72
|
+
new Date(s.timestamp ?? 0).getTime() >= createdAt);
|
|
73
|
+
}
|
|
74
|
+
/** `getRepStats` for one rep of one assignment, without the raw sessions. */
|
|
75
|
+
export function repStats(userId, sessions, assignment, now = new Date()) {
|
|
76
|
+
const own = sessions.filter((s) => s.userId === userId).sort(byTimestamp);
|
|
77
|
+
return {
|
|
78
|
+
user_id: userId,
|
|
79
|
+
sessions: own.length,
|
|
80
|
+
scored_sessions: scoredCount(own),
|
|
81
|
+
best_score: bestScore(own),
|
|
82
|
+
avg_score: avgScore(own),
|
|
83
|
+
trend: calcTrend(own),
|
|
84
|
+
status: deriveStatus(own, assignment, now),
|
|
85
|
+
last_session_at: own.length > 0 ? (own[own.length - 1].timestamp ?? null) : null,
|
|
86
|
+
total_duration_ms: own.reduce((sum, s) => sum + (s.durationMs ?? 0), 0),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Per-skill means over the sessions' `evaluation.category_scores`, keyed by `skillId` (the
|
|
91
|
+
* Skills Registry id). A legacy category without one falls back to its own `id`, which is stable
|
|
92
|
+
* within one scenario; an entry with neither, or with no finite score, is skipped.
|
|
93
|
+
*/
|
|
94
|
+
export function skillAverages(sessions) {
|
|
95
|
+
const bySkill = new Map();
|
|
96
|
+
for (const s of sessions) {
|
|
97
|
+
if (sessionScore(s) === null)
|
|
98
|
+
continue;
|
|
99
|
+
for (const c of s.evaluation?.category_scores ?? []) {
|
|
100
|
+
const key = c.skillId || c.id;
|
|
101
|
+
if (!key || typeof c.score !== 'number' || !Number.isFinite(c.score))
|
|
102
|
+
continue;
|
|
103
|
+
const entry = bySkill.get(key) ?? { title: c.title ?? key, scores: [] };
|
|
104
|
+
entry.scores.push(c.score);
|
|
105
|
+
bySkill.set(key, entry);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return Array.from(bySkill.entries()).map(([skill_id, { title, scores }]) => ({
|
|
109
|
+
skill_id,
|
|
110
|
+
title,
|
|
111
|
+
avg_score: meanOfFinite(scores),
|
|
112
|
+
scored_sessions: scores.length,
|
|
113
|
+
}));
|
|
114
|
+
}
|
package/dist/src/mcp/hosted.js
CHANGED
|
@@ -50,7 +50,7 @@ const HOSTED_DESCRIPTIONS = {
|
|
|
50
50
|
pitcher_files_update: 'Update a file by ID (name, folder, access, tags, metadata). sourceUrl replaces/overwrites the file binary; metadata is deep-merged.',
|
|
51
51
|
pitcher_announcements_create: 'Create a wa-announcements appsDB record (title/body, optional media, CTA link, scheduling, group targeting). Requires title and body. Additive.',
|
|
52
52
|
pitcher_dsr_create: 'Create a Digital Sales Room (a canvas share link) and get a clickable link back. Source is a canvas (canvasId) OR files (shareboxFileIds) - exactly one. Optional email/domain gating and a bound landing-page template. Returns the share: id, url (the full link), short_url, and a nested canvas object - there is no full_url field. This endpoint is a GET-OR-CREATE: sharing an already-shared canvas returns the existing link and only updates the fields you pass, so use pitcher_dsr_update to change or remove a value on an existing share. A failed landing bind does not fail the call: it returns the link with a warning - and binding a landing page needs instanceId, without which the bind is skipped with that warning. tags are a ShareBox-only field (they name the canvas the server builds from shareboxFileIds), so non-empty tags together with canvasId are REJECTED rather than silently dropped by the server.',
|
|
53
|
-
pitcher_metadata_template_upsert: "Upsert an instance's metadata-field schema. Merges the incoming sections by section 'label' — a section whose label matches an existing one is
|
|
53
|
+
pitcher_metadata_template_upsert: "Upsert an instance's metadata-field schema. 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. Cannot remove a whole section. To add a field, send the section with all its existing fields plus the new one. Requires an instance- or org-admin role.",
|
|
54
54
|
};
|
|
55
55
|
// Field descriptions that name a stripped key, rewritten for hosted (`tool name → field → text`).
|
|
56
56
|
const HOSTED_FIELD_DESCRIPTIONS = {
|
package/dist/src/mcp/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { buildServer, type McpRunContext, type BuildServerOptions } from './server.js';
|
|
1
|
+
export { buildServer, PITCHER_ICON, type McpRunContext, type BuildServerOptions } from './server.js';
|
|
2
2
|
export { hostedTools, hostedReadOnlyTools, readOnlyPrompts, LOCAL_ONLY, FILE_KEY, ID_KEY, HOSTED_BUILD_OPTIONS, } from './hosted.js';
|
|
3
3
|
export { allTools } from './registry.js';
|
|
4
4
|
export { allPrompts } from './prompts/registry.js';
|
package/dist/src/mcp/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Public entry `@pitcher/cli-scripts/mcp` — what the hosted MCP server (web/packages/mcp-server) consumes.
|
|
2
|
-
export { buildServer } from './server.js';
|
|
2
|
+
export { buildServer, PITCHER_ICON } from './server.js';
|
|
3
3
|
export { hostedTools, hostedReadOnlyTools, readOnlyPrompts, LOCAL_ONLY, FILE_KEY, ID_KEY, HOSTED_BUILD_OPTIONS, } from './hosted.js';
|
|
4
4
|
export { allTools } from './registry.js';
|
|
5
5
|
export { allPrompts } from './prompts/registry.js';
|
package/dist/src/mcp/registry.js
CHANGED
|
@@ -15,6 +15,7 @@ import { metadataTemplateTools } from './tools/metadataTemplate.js';
|
|
|
15
15
|
import { ldTools } from './tools/ld.js';
|
|
16
16
|
import { orgTools } from './tools/org.js';
|
|
17
17
|
import { publishAppTools } from './tools/publishApp.js';
|
|
18
|
+
import { roleplayTools } from './tools/roleplay.js';
|
|
18
19
|
import { rolesTools } from './tools/roles.js';
|
|
19
20
|
import { searchTools } from './tools/search.js';
|
|
20
21
|
import { sharedLinksTools } from './tools/sharedLinks.js';
|
|
@@ -39,6 +40,7 @@ export const allTools = [
|
|
|
39
40
|
...rolesTools,
|
|
40
41
|
...orgTools,
|
|
41
42
|
...analyticsTools,
|
|
43
|
+
...roleplayTools,
|
|
42
44
|
...membershipsTools,
|
|
43
45
|
...groupsTools,
|
|
44
46
|
...metadataTemplateTools,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ANALYTICS_RENDER_POLICY = "When answering Pitcher content-analytics questions (the pitcher_analytics_* tools), return the smallest sufficient representation. Answer in words first; a visual supports the sentence, it never replaces it. Default to prose and escalate ONLY when a trigger below fires.\n\nRendering tiers (evaluate top-down, stop at the first match):\n- A single fact / yes-no / \"who/what/how many is the single X\" (e.g. \"which canvas did I view?\", \"how many viewers?\") -> 1-2 sentences with the number inline. No table, no chart.\n- A ranking request (\"top\", \"most\", \"best\", \"rank\", or \"list the highest/lowest ...\") about content, files, users, or canvases -> a markdown TABLE of the ranked rows (default the top 5-8); columns = only the metric(s) asked for. Treat \"what's my top content?\" as a ranking (a table), NOT a single-item sentence. Lead with a one-line takeaway, then the table.\n- \"over time\" / \"trend\" / \"by week/month\" -> ONE line/area chart artifact (this rule wins over the generic comparison rule below for time series).\n- An explicit visualization request (\"show a chart\", \"chart\", \"visualize\", \"graph\", \"plot\") OR a comparison across a non-time dimension -> ONE focused chart as a single self-contained HTML artifact, only the asked-for series.\n- A broad overview (\"how's everything / my content / adoption doing\", \"give me the picture\", \"dashboard\", QBR) -> a multi-panel dashboard artifact. This is the ceiling \u2014 use it rarely, only for genuinely broad asks.\n\nGuardrails (every tier):\n1. Words first \u2014 even with a chart, lead with a one-sentence answer to the literal question.\n2. Mask names, emails, and user IDs by default (User A/B/...). Reveal an identity only when the analytics tool actually returned it for the authenticated role (the verbs are role-gated); never infer authorization from the user's wording.\n3. If data is empty or trivially small, say so in prose \u2014 do not render an empty chart.\n4. If results are capped/truncated, state the cap (\"top 15 of 118\").\n5. Never show a \"vs prior period\" delta unless the data actually contains prior-period numbers.\n6. Cite the source verb (\"via analytics popularity\").\n\nDo not jump a tier without its trigger. The dashboard is opt-in by intent, never a default.";
|
|
1
|
+
export declare const ANALYTICS_RENDER_POLICY = "When answering Pitcher content-analytics or AI Roleplay questions (the pitcher_analytics_* and pitcher_roleplay_* tools), return the smallest sufficient representation. Answer in words first; a visual supports the sentence, it never replaces it. Default to prose and escalate ONLY when a trigger below fires.\n\nRendering tiers (evaluate top-down, stop at the first match):\n- A single fact / yes-no / \"who/what/how many is the single X\" (e.g. \"which canvas did I view?\", \"how many viewers?\") -> 1-2 sentences with the number inline. No table, no chart.\n- A ranking request (\"top\", \"most\", \"best\", \"rank\", or \"list the highest/lowest ...\") about content, files, users, or canvases -> a markdown TABLE of the ranked rows (default the top 5-8); columns = only the metric(s) asked for. Treat \"what's my top content?\" as a ranking (a table), NOT a single-item sentence. Lead with a one-line takeaway, then the table.\n- \"over time\" / \"trend\" / \"by week/month\" -> ONE line/area chart artifact (this rule wins over the generic comparison rule below for time series).\n- An explicit visualization request (\"show a chart\", \"chart\", \"visualize\", \"graph\", \"plot\") OR a comparison across a non-time dimension -> ONE focused chart as a single self-contained HTML artifact, only the asked-for series.\n- A broad overview (\"how's everything / my content / adoption doing\", \"give me the picture\", \"dashboard\", QBR) -> a multi-panel dashboard artifact. This is the ceiling \u2014 use it rarely, only for genuinely broad asks.\n\nGuardrails (every tier):\n1. Words first \u2014 even with a chart, lead with a one-sentence answer to the literal question.\n2. Mask names, emails, and user IDs by default (User A/B/...). Reveal an identity only when the analytics tool actually returned it for the authenticated role (the verbs are role-gated); never infer authorization from the user's wording.\n3. If data is empty or trivially small, say so in prose \u2014 do not render an empty chart.\n4. If results are capped/truncated, state the cap (\"top 15 of 118\").\n5. Never show a \"vs prior period\" delta unless the data actually contains prior-period numbers.\n6. Cite the source verb (\"via analytics popularity\").\n7. AI Roleplay: a session with no score (null) is unscored, never a 0; never fold it into an average, a pass rate or a ranking.\n\nDo not jump a tier without its trigger. The dashboard is opt-in by intent, never a default.";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// question gets a right-sized reply (a sentence, a table, one chart, or — rarely — a dashboard)
|
|
4
4
|
// instead of a rigid full dashboard on every question. This constant is the single source of truth;
|
|
5
5
|
// the Docs Portal spec (Product/Specs/2026-07-24-analytics-answer-rendering-policy.md) mirrors it.
|
|
6
|
-
export const ANALYTICS_RENDER_POLICY = `When answering Pitcher content-analytics questions (the pitcher_analytics_* tools), return the smallest sufficient representation. Answer in words first; a visual supports the sentence, it never replaces it. Default to prose and escalate ONLY when a trigger below fires.
|
|
6
|
+
export const ANALYTICS_RENDER_POLICY = `When answering Pitcher content-analytics or AI Roleplay questions (the pitcher_analytics_* and pitcher_roleplay_* tools), return the smallest sufficient representation. Answer in words first; a visual supports the sentence, it never replaces it. Default to prose and escalate ONLY when a trigger below fires.
|
|
7
7
|
|
|
8
8
|
Rendering tiers (evaluate top-down, stop at the first match):
|
|
9
9
|
- A single fact / yes-no / "who/what/how many is the single X" (e.g. "which canvas did I view?", "how many viewers?") -> 1-2 sentences with the number inline. No table, no chart.
|
|
@@ -19,5 +19,6 @@ Guardrails (every tier):
|
|
|
19
19
|
4. If results are capped/truncated, state the cap ("top 15 of 118").
|
|
20
20
|
5. Never show a "vs prior period" delta unless the data actually contains prior-period numbers.
|
|
21
21
|
6. Cite the source verb ("via analytics popularity").
|
|
22
|
+
7. AI Roleplay: a session with no score (null) is unscored, never a 0; never fold it into an average, a pass rate or a ranking.
|
|
22
23
|
|
|
23
24
|
Do not jump a tier without its trigger. The dashboard is opt-in by intent, never a default.`;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ResourceDescriptor } from './types.js';
|
|
2
|
-
export declare const ANALYTICS_CATALOG = "# Pitcher analytics catalog\n\
|
|
2
|
+
export declare const ANALYTICS_CATALOG = "# Pitcher analytics catalog\n\nFourteen read-only verbs, each an MCP tool named `pitcher_analytics_<verb>`. Pick the verb that matches the\nquestion, then call its tool. All are read-only; org scope + auth come from the server's API key (never a\ntool input) \u2014 you only pass `instanceId` (and, where noted, `canvasId`/`userEmail`).\n\n## Key tiers\n- **org-admin key required:** org, popularity, users, files, canvases\n- **instance-admin or org-admin:** instance\n- **member key OK (access follows the key user's real role):** kpi, engagement, viewers, sessions, viewer_visits\n- **instance editor, instance admin or org admin:** file_users, file_pages, canvas_users\n\n## Time model\n- **Rollup (no window):** org, instance \u2014 lifetime/current totals.\n- **Date-range, 30-day default:** kpi (activity types need dates), engagement, popularity, `users` with\n `stats: true`, files, canvases \u2014 omit dates to get the last 30 days. (`users` without `stats` is a\n full roster with no window.) Also file_users, file_pages, canvas_users.\n- **Date-range, server default window:** viewers, sessions, viewer_visits \u2014 omit dates and the service picks the window.\n\n## Verbs\n\n| Verb (tool) | Returns | Time | Key | Example question |\n|---|---|---|---|---|\n| `org` | Organization-wide rollup totals (Django) | rollup | org-admin | \"How is the whole org doing overall?\" |\n| `instance` | Single-instance rollup totals (Django) | rollup | instance-admin | \"Totals for this one workspace?\" |\n| `kpi` | KPI metrics for an instance (members, active users, \u2026) | date-range | member | \"How many active users last month?\" |\n| `engagement` | Per-DSR/content engagement rows (capped list + summary) | date-range | member | \"Which content is being engaged with?\" |\n| `popularity` | Most-viewed canvases, ranked (view-analysis) | date-range | org-admin | \"Top 10 most-viewed decks this quarter?\" |\n| `users` | User roster, or active-user stats with `stats: true` | roster: none / stats: date-range | org-admin | \"Who are my active users this month?\" |\n| `files` | File usage analytics (views/opens/engagement per file) | date-range | org-admin | \"Which files get opened the most?\" |\n| `canvases` | Per-section canvas engagement list | date-range | org-admin | \"How are my canvas sections performing?\" |\n| `viewers` | Who viewed a specific DSR canvas (per-viewer engagement) | date-range | member | \"Who viewed this DSR, and how much?\" |\n| `sessions` | One viewer's session event timeline for a DSR canvas (raw) | date-range | member | \"What did this viewer do inside the DSR?\" |\n| `file_users` | Who opened ONE file: opens, seconds, days active per user | date-range | editor | \"Who opened the pricing PDF, and for how long?\" |\n| `file_pages` | Time per page of ONE file (one user with a single `userEmails`) | date-range | editor | \"Which pages of this file do reps spend time on?\" |\n| `canvas_users` | Per user and canvas: visits, seconds; with `canvasId`, seconds per page (section) | date-range | editor | \"Which reps presented this deck, and which slides?\" |\n| `viewer_visits` | Per DSR viewer and visit: sections, files and pages with seconds; `format: csv` to export | date-range | member | \"Export who looked at which pages of this DSR\" |\n\n## Shaping\nRow-heavy verbs (engagement, popularity, users, files, canvases, viewers) return a capped envelope:\n`summary` + `rows` (default 20, max 100 \u2014 raise with `limit`) + `total_count`/`returned`/`has_more`,\nand a `truncated` note when more rows exist. `kpi` and `sessions` return the raw payload (a KPI scalar set\nand an uncapped event timeline, respectively) \u2014 never truncated.\n\n## DSR drill-down\nFor \"who viewed X and what did they do\": call `viewers` (with `canvasId`) to list viewers, then `sessions`\n(with the same `canvasId` + the viewer's `userEmail`) to see that viewer's event timeline.\nA session is one `Entered` of the queried id itself (a canvas or a section), paired by `event_pair_id`; an\n`Entered` with an empty `event_pair_id` counts as one session on its own.\n`session_count` is counted the same way as the overview's `total_sessions`. `canvases` lists sections, and their\nids are valid `canvasId` inputs for `viewers` and `sessions`.\n\n`viewer_visits` does both steps for up to 25 viewers at once and returns per-visit, per-page time; use it for\n\"which pages did each viewer look at\", and `format: \"csv\"` when the user wants a file.\n\n## Per-user usage\n\"Which files did this user open\" is `files` with `userEmails`; \"who opened this file\" is `file_users`; time per page\nof a file is `file_pages`; reps' canvas usage is `canvas_users` (internal users only \u2014 external DSR viewers are\n`viewers` / `viewer_visits`).\n\n## AI Roleplay\nRoleplay results are separate tools (`pitcher_roleplay_*`): kpis, sessions and rep_summary for instance or org admins,\nteam (assignment completion; admins see every assignment, coaching managers their own), session_get and\nscenarios_list. A session with no score is unscored, never 0.\n\n## Notes\n- `popularity` maps to the canvases view-analysis endpoint (there is no separate popularity route).\n- Results can include user identities/emails \u2014 treat as customer data and handle per policy.\n";
|
|
3
3
|
export declare const analyticsCatalogResource: ResourceDescriptor;
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
// A model-facing catalog of the analytics verbs exposed as pitcher_analytics_* tools. It exists so a
|
|
2
2
|
// host can read ONE resource to learn which verb answers which question, the time model each uses, and
|
|
3
|
-
// the key tier each needs — instead of guessing from
|
|
3
|
+
// the key tier each needs — instead of guessing from 14 tool descriptions. Distilled from the analytics
|
|
4
4
|
// runner contracts in src/commands/analytics.ts (the authoritative source); keep in sync on route/gate
|
|
5
5
|
// changes. Definitions mirror the platform analytics vocabulary.
|
|
6
6
|
export const ANALYTICS_CATALOG = `# Pitcher analytics catalog
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Fourteen read-only verbs, each an MCP tool named \`pitcher_analytics_<verb>\`. Pick the verb that matches the
|
|
9
9
|
question, then call its tool. All are read-only; org scope + auth come from the server's API key (never a
|
|
10
10
|
tool input) — you only pass \`instanceId\` (and, where noted, \`canvasId\`/\`userEmail\`).
|
|
11
11
|
|
|
12
12
|
## Key tiers
|
|
13
13
|
- **org-admin key required:** org, popularity, users, files, canvases
|
|
14
14
|
- **instance-admin or org-admin:** instance
|
|
15
|
-
- **member key OK (access follows the key user's real role):** kpi, engagement, viewers, sessions
|
|
15
|
+
- **member key OK (access follows the key user's real role):** kpi, engagement, viewers, sessions, viewer_visits
|
|
16
|
+
- **instance editor, instance admin or org admin:** file_users, file_pages, canvas_users
|
|
16
17
|
|
|
17
18
|
## Time model
|
|
18
19
|
- **Rollup (no window):** org, instance — lifetime/current totals.
|
|
19
20
|
- **Date-range, 30-day default:** kpi (activity types need dates), engagement, popularity, \`users\` with
|
|
20
21
|
\`stats: true\`, files, canvases — omit dates to get the last 30 days. (\`users\` without \`stats\` is a
|
|
21
|
-
full roster with no window.)
|
|
22
|
-
- **Date-range, server default window:** viewers, sessions — omit dates and the service picks the window.
|
|
22
|
+
full roster with no window.) Also file_users, file_pages, canvas_users.
|
|
23
|
+
- **Date-range, server default window:** viewers, sessions, viewer_visits — omit dates and the service picks the window.
|
|
23
24
|
|
|
24
25
|
## Verbs
|
|
25
26
|
|
|
@@ -35,6 +36,10 @@ tool input) — you only pass \`instanceId\` (and, where noted, \`canvasId\`/\`u
|
|
|
35
36
|
| \`canvases\` | Per-section canvas engagement list | date-range | org-admin | "How are my canvas sections performing?" |
|
|
36
37
|
| \`viewers\` | Who viewed a specific DSR canvas (per-viewer engagement) | date-range | member | "Who viewed this DSR, and how much?" |
|
|
37
38
|
| \`sessions\` | One viewer's session event timeline for a DSR canvas (raw) | date-range | member | "What did this viewer do inside the DSR?" |
|
|
39
|
+
| \`file_users\` | Who opened ONE file: opens, seconds, days active per user | date-range | editor | "Who opened the pricing PDF, and for how long?" |
|
|
40
|
+
| \`file_pages\` | Time per page of ONE file (one user with a single \`userEmails\`) | date-range | editor | "Which pages of this file do reps spend time on?" |
|
|
41
|
+
| \`canvas_users\` | Per user and canvas: visits, seconds; with \`canvasId\`, seconds per page (section) | date-range | editor | "Which reps presented this deck, and which slides?" |
|
|
42
|
+
| \`viewer_visits\` | Per DSR viewer and visit: sections, files and pages with seconds; \`format: csv\` to export | date-range | member | "Export who looked at which pages of this DSR" |
|
|
38
43
|
|
|
39
44
|
## Shaping
|
|
40
45
|
Row-heavy verbs (engagement, popularity, users, files, canvases, viewers) return a capped envelope:
|
|
@@ -50,6 +55,19 @@ A session is one \`Entered\` of the queried id itself (a canvas or a section), p
|
|
|
50
55
|
\`session_count\` is counted the same way as the overview's \`total_sessions\`. \`canvases\` lists sections, and their
|
|
51
56
|
ids are valid \`canvasId\` inputs for \`viewers\` and \`sessions\`.
|
|
52
57
|
|
|
58
|
+
\`viewer_visits\` does both steps for up to 25 viewers at once and returns per-visit, per-page time; use it for
|
|
59
|
+
"which pages did each viewer look at", and \`format: "csv"\` when the user wants a file.
|
|
60
|
+
|
|
61
|
+
## Per-user usage
|
|
62
|
+
"Which files did this user open" is \`files\` with \`userEmails\`; "who opened this file" is \`file_users\`; time per page
|
|
63
|
+
of a file is \`file_pages\`; reps' canvas usage is \`canvas_users\` (internal users only — external DSR viewers are
|
|
64
|
+
\`viewers\` / \`viewer_visits\`).
|
|
65
|
+
|
|
66
|
+
## AI Roleplay
|
|
67
|
+
Roleplay results are separate tools (\`pitcher_roleplay_*\`): kpis, sessions and rep_summary for instance or org admins,
|
|
68
|
+
team (assignment completion; admins see every assignment, coaching managers their own), session_get and
|
|
69
|
+
scenarios_list. A session with no score is unscored, never 0.
|
|
70
|
+
|
|
53
71
|
## Notes
|
|
54
72
|
- \`popularity\` maps to the canvases view-analysis endpoint (there is no separate popularity route).
|
|
55
73
|
- Results can include user identities/emails — treat as customer data and handle per policy.
|
package/dist/src/mcp/server.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Credential } from '../core/credential.js';
|
|
|
3
3
|
import type { ToolDescriptor } from './tools/types.js';
|
|
4
4
|
import type { PromptDescriptor } from './prompts/types.js';
|
|
5
5
|
import type { ResourceDescriptor } from './resources/types.js';
|
|
6
|
+
export declare const PITCHER_ICON = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiI+PHJlY3Qgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiIHJ4PSIxMTYiIGZpbGw9IiMyOTMzNUMiLz48Y2lyY2xlIGN4PSIzNzIiIGN5PSIxNTAiIHI9IjMwIiBmaWxsPSIjQ0MwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iTTE1MCAxMTZoOTZhOTIgOTIgMCAwIDEgMCAxODRoLTUwdjk2aC00NnoiLz48cGF0aCBmaWxsPSIjMjkzMzVDIiBkPSJNMTk2IDE2MHY5Nmg1MGE0OCA0OCAwIDAgMCAwLTk2eiIvPjwvc3ZnPg==";
|
|
6
7
|
/**
|
|
7
8
|
* The per-server request context: who every tool call runs as, and against which origin.
|
|
8
9
|
* Captured at build time — local stdio `p mcp` builds one from the `p login` session; the hosted server
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -18,10 +18,10 @@ const version = (() => {
|
|
|
18
18
|
return '0.0.0';
|
|
19
19
|
}
|
|
20
20
|
})();
|
|
21
|
-
// Connector branding shown by MCP hosts (Claude Desktop, Cursor).
|
|
22
|
-
// data URI so it needs no network/asset resolution: a navy (#29335C) tile with a
|
|
21
|
+
// Connector branding shown by MCP hosts (Claude Desktop, Cursor) and on the hosted server's own pages.
|
|
22
|
+
// Self-contained data URI so it needs no network/asset resolution: a navy (#29335C) tile with a
|
|
23
23
|
// white "P" and the Pitcher-red (#CC0000) accent from the wordmark.
|
|
24
|
-
const
|
|
24
|
+
export const PITCHER_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiI+PHJlY3Qgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiIHJ4PSIxMTYiIGZpbGw9IiMyOTMzNUMiLz48Y2lyY2xlIGN4PSIzNzIiIGN5PSIxNTAiIHI9IjMwIiBmaWxsPSIjQ0MwMDAwIi8+PHBhdGggZmlsbD0iI2ZmZmZmZiIgZD0iTTE1MCAxMTZoOTZhOTIgOTIgMCAwIDEgMCAxODRoLTUwdjk2aC00NnoiLz48cGF0aCBmaWxsPSIjMjkzMzVDIiBkPSJNMTk2IDE2MHY5Nmg1MGE0OCA0OCAwIDAgMCAwLTk2eiIvPjwvc3ZnPg==';
|
|
25
25
|
// Compact, sanitized error string (no raw stack) — shared by the prompt/resource callbacks.
|
|
26
26
|
function errorText(e) {
|
|
27
27
|
if (isCliError(e))
|
|
@@ -35,7 +35,7 @@ export function buildServer(ctx, tools = allTools, prompts = allPrompts, resourc
|
|
|
35
35
|
title: 'Pitcher',
|
|
36
36
|
description: 'Search, manage, and share Pitcher content, canvases, files, DSRs, and instances.',
|
|
37
37
|
websiteUrl: 'https://www.pitcher.com',
|
|
38
|
-
icons: [{ src:
|
|
38
|
+
icons: [{ src: PITCHER_ICON, mimeType: 'image/svg+xml', sizes: ['any'] }],
|
|
39
39
|
},
|
|
40
40
|
// Surfaced to the host model via InitializeResult.instructions — the analytics answer-rendering
|
|
41
41
|
// policy so a natural-language question gets a right-sized reply (sentence / table / chart /
|