@pitcher/cli-scripts 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/dsrLanding.js +1 -1
- package/dist/src/commands/roleplay.d.ts +154 -3
- package/dist/src/commands/roleplay.js +276 -23
- package/dist/src/core/dsr-engagement.d.ts +152 -0
- package/dist/src/core/dsr-engagement.js +301 -0
- package/dist/src/core/envelope.d.ts +1 -0
- package/dist/src/core/envelope.js +6 -1
- package/dist/src/core/errors.d.ts +3 -1
- package/dist/src/core/errors.js +2 -2
- package/dist/src/core/http.js +7 -5
- package/dist/src/core/ld.js +2 -1
- package/dist/src/core/me.d.ts +3 -0
- package/dist/src/core/me.js +10 -0
- package/dist/src/core/roleplay-stats.d.ts +48 -7
- package/dist/src/core/roleplay-stats.js +68 -11
- package/dist/src/mcp/apps/confirm/define.d.ts +66 -0
- package/dist/src/mcp/apps/confirm/define.js +208 -0
- package/dist/src/mcp/apps/confirm/hash.d.ts +4 -0
- package/dist/src/mcp/apps/confirm/hash.js +23 -0
- package/dist/src/mcp/apps/confirm/types.d.ts +96 -0
- package/dist/src/mcp/apps/confirm/types.js +15 -0
- package/dist/src/mcp/apps/contentPicker.d.ts +104 -0
- package/dist/src/mcp/apps/contentPicker.js +268 -0
- package/dist/src/mcp/apps/dsrEngagement.d.ts +194 -0
- package/dist/src/mcp/apps/dsrEngagement.js +737 -0
- package/dist/src/mcp/apps/registry.d.ts +14 -0
- package/dist/src/mcp/apps/registry.js +25 -0
- package/dist/src/mcp/apps/roleplayManager.d.ts +228 -0
- package/dist/src/mcp/apps/roleplayManager.js +606 -0
- package/dist/src/mcp/apps/roleplayReps.d.ts +79 -0
- package/dist/src/mcp/apps/roleplayReps.js +328 -0
- package/dist/src/mcp/apps/roleplayWrites.d.ts +6 -0
- package/dist/src/mcp/apps/roleplayWrites.js +509 -0
- package/dist/src/mcp/apps/text.d.ts +41 -0
- package/dist/src/mcp/apps/text.js +58 -0
- package/dist/src/mcp/apps/types.d.ts +23 -0
- package/dist/src/mcp/apps/types.js +12 -0
- package/dist/src/mcp/apps/writeConfirm.d.ts +3 -0
- package/dist/src/mcp/apps/writeConfirm.js +8 -0
- package/dist/src/mcp/groups.d.ts +42 -0
- package/dist/src/mcp/groups.js +89 -0
- package/dist/src/mcp/hosted.d.ts +36 -0
- package/dist/src/mcp/hosted.js +47 -1
- package/dist/src/mcp/index.d.ts +9 -3
- package/dist/src/mcp/index.js +9 -2
- package/dist/src/mcp/prompts/registry.js +8 -7
- package/dist/src/mcp/prompts/types.d.ts +3 -0
- package/dist/src/mcp/registry.d.ts +4 -0
- package/dist/src/mcp/registry.js +29 -24
- package/dist/src/mcp/renderPolicy.d.ts +1 -0
- package/dist/src/mcp/renderPolicy.js +8 -2
- package/dist/src/mcp/resources/registry.js +2 -2
- package/dist/src/mcp/resources/types.d.ts +3 -0
- package/dist/src/mcp/result.d.ts +2 -1
- package/dist/src/mcp/result.js +8 -6
- package/dist/src/mcp/server.d.ts +62 -0
- package/dist/src/mcp/server.js +161 -10
- package/dist/src/mcp/tools/roleplay.js +34 -5
- package/dist/src/mcp/tools/types.d.ts +35 -0
- package/package.json +2 -1
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { runFilesGet } from '../../commands/files.js';
|
|
4
|
+
import { ok, fail } from '../../core/envelope.js';
|
|
5
|
+
import { createCliError } from '../../core/errors.js';
|
|
6
|
+
import { clientFor, toCliError } from '../../core/http.js';
|
|
7
|
+
import { orgOrigin } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* MCP Apps content picker (PIT-10066): a files-only gallery with multi-select that creates a DSR
|
|
10
|
+
* through the ShareBox path of the existing pitcher_dsr_create tool. Owner rulings P1 + P5: files only.
|
|
11
|
+
*/
|
|
12
|
+
export const contentPickerView = {
|
|
13
|
+
name: 'content-picker',
|
|
14
|
+
title: 'Pitcher content picker',
|
|
15
|
+
description: 'Thumbnail gallery of files with multi-select and DSR (ShareBox) creation.',
|
|
16
|
+
csp: (ctx) => ({ resourceDomains: orgOrigin(ctx) }),
|
|
17
|
+
permissions: { clipboardWrite: {} },
|
|
18
|
+
// rollout: the shared enable_mcp_apps flag (owner ruling P7), no per-view flag
|
|
19
|
+
};
|
|
20
|
+
export const PICKER_PAGE_SIZE = 24;
|
|
21
|
+
export const PICKER_MAX_BYTES = 120000;
|
|
22
|
+
export const ITEM_MAX_BYTES = 60000;
|
|
23
|
+
export const TEMPLATES_MAX_BYTES = 30000;
|
|
24
|
+
const jsonBytes = (v) => Buffer.byteLength(JSON.stringify(v), 'utf8');
|
|
25
|
+
/**
|
|
26
|
+
* Drop items from the end until the serialized payload fits; report how many were dropped.
|
|
27
|
+
* Trade-off: the next page starts after the FULL server page, so a dropped item is never reachable by
|
|
28
|
+
* paging (only by a narrower search). Accepted: 24 items rarely come near 120 KB (thumbnail URLs are ~1 KB).
|
|
29
|
+
*/
|
|
30
|
+
export function fitToBudget(data, max = PICKER_MAX_BYTES) {
|
|
31
|
+
let items = data.items;
|
|
32
|
+
const size = (xs) => jsonBytes({ ...data, items: xs });
|
|
33
|
+
while (items.length && size(items) > max)
|
|
34
|
+
items = items.slice(0, -1);
|
|
35
|
+
const dropped = data.items.length - items.length;
|
|
36
|
+
return dropped ? { ...data, items, truncated: dropped } : { ...data, items };
|
|
37
|
+
}
|
|
38
|
+
/** Model-facing text: one short line per file, no URLs. structuredContent carries the thumbnails. */
|
|
39
|
+
export function pickerText(data) {
|
|
40
|
+
const d = data;
|
|
41
|
+
const lines = d.items.map((i) => `${i.id} ${i.fileType ?? ''} ${i.name}${i.shareable ? '' : ' (cannot be shared)'}`.replace(/\s+/g, ' ').trim());
|
|
42
|
+
const tail = d.hasMore || d.truncated ? ' More available.' : '';
|
|
43
|
+
return `${d.items.length} files.${tail} If your host shows the content picker view, the user sees these items there; otherwise list them for the user.\n${lines.join('\n')}`;
|
|
44
|
+
}
|
|
45
|
+
const str = (v) => (typeof v === 'string' && v !== '' ? v : null);
|
|
46
|
+
function fileTypeOf(row) {
|
|
47
|
+
const ext = str(row.content_extension);
|
|
48
|
+
if (ext)
|
|
49
|
+
return ext.replace(/^\./, '').toUpperCase();
|
|
50
|
+
return str(row.content_type);
|
|
51
|
+
}
|
|
52
|
+
/** A files-list (or retrieve) row -> PickerItem. `shareable` is false only for an explicit can_share=false. */
|
|
53
|
+
export function toPickerItem(row) {
|
|
54
|
+
const folder = row.folder;
|
|
55
|
+
const permissions = row.permissions;
|
|
56
|
+
return {
|
|
57
|
+
id: String(row.id),
|
|
58
|
+
name: typeof row.name === 'string' ? row.name : String(row.id),
|
|
59
|
+
subtitle: str(folder?.name),
|
|
60
|
+
fileType: fileTypeOf(row),
|
|
61
|
+
shareable: permissions?.can_share !== false,
|
|
62
|
+
thumbnailUrl: str(row.thumbnail_url),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Every File.ContentType a user would filter by (Django filemodels.py File.ContentType). */
|
|
66
|
+
export const CONTENT_FILTERS = ['pdf', 'video', 'image', 'web', 'audio', 'attch'];
|
|
67
|
+
const isNotFound = (e) => toCliError(e).code === 'NOT_FOUND';
|
|
68
|
+
/**
|
|
69
|
+
* next-core's account rule (shared-links/route.ts): lodash `merge({}, defaults, org.settings, instance.settings)`
|
|
70
|
+
* then a truthiness test. merge copies an explicit instance value, null included, over the org value; only
|
|
71
|
+
* `undefined` falls through, so this is NOT `??`. Either read failing = null (unknown; the 400 path covers it).
|
|
72
|
+
*/
|
|
73
|
+
async function readRequiresAccount(client, instanceId) {
|
|
74
|
+
const [orgRes, instRes] = await Promise.allSettled([
|
|
75
|
+
client.get('/api/v1/organization/'),
|
|
76
|
+
client.get(`/api/v1/instances/${encodeURIComponent(instanceId)}`),
|
|
77
|
+
]);
|
|
78
|
+
if (orgRes.status !== 'fulfilled' || instRes.status !== 'fulfilled')
|
|
79
|
+
return null;
|
|
80
|
+
const org = orgRes.value.data?.settings?.canvas?.require_account_on_canvas_creation;
|
|
81
|
+
const inst = instRes.value.data?.settings?.canvas?.require_account_on_canvas_creation;
|
|
82
|
+
return Boolean(inst !== undefined ? inst : org);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* requiresAccount memo (C3): a search keystroke re-runs page 1, which would re-read two settings endpoints
|
|
86
|
+
* every time. Keyed by sha256(credential) + instanceId (never the raw token), 60 s TTL, at most 1000 entries
|
|
87
|
+
* with oldest-first eviction. A null (read failed) is never stored, so the next call retries.
|
|
88
|
+
*/
|
|
89
|
+
export const REQUIRES_ACCOUNT_TTL_MS = 60000;
|
|
90
|
+
export const REQUIRES_ACCOUNT_CACHE_MAX = 1000;
|
|
91
|
+
const requiresAccountCache = new Map();
|
|
92
|
+
/** Test hook: empty the requiresAccount memo. */
|
|
93
|
+
export function clearRequiresAccountCache() {
|
|
94
|
+
requiresAccountCache.clear();
|
|
95
|
+
}
|
|
96
|
+
function requiresAccountKey(opts, instanceId) {
|
|
97
|
+
const c = opts.credential;
|
|
98
|
+
if (!c)
|
|
99
|
+
return null; // no explicit credential (local CLI session): nothing stable to key on, no memo
|
|
100
|
+
const secret = c.kind === 'bearer' ? `bearer:${c.token}` : `apiKey:${c.key}`;
|
|
101
|
+
return `${createHash('sha256').update(secret).digest('hex')}:${instanceId}`;
|
|
102
|
+
}
|
|
103
|
+
async function cachedRequiresAccount(client, opts, instanceId) {
|
|
104
|
+
const key = requiresAccountKey(opts, instanceId);
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
if (key) {
|
|
107
|
+
const hit = requiresAccountCache.get(key);
|
|
108
|
+
if (hit && hit.expires > now)
|
|
109
|
+
return hit.value;
|
|
110
|
+
if (hit)
|
|
111
|
+
requiresAccountCache.delete(key);
|
|
112
|
+
}
|
|
113
|
+
const value = await readRequiresAccount(client, instanceId);
|
|
114
|
+
if (key && value !== null) {
|
|
115
|
+
// Map keeps insertion order, so the first key is the oldest entry.
|
|
116
|
+
while (requiresAccountCache.size >= REQUIRES_ACCOUNT_CACHE_MAX) {
|
|
117
|
+
const oldest = requiresAccountCache.keys().next().value;
|
|
118
|
+
requiresAccountCache.delete(oldest);
|
|
119
|
+
}
|
|
120
|
+
requiresAccountCache.set(key, { value, expires: Date.now() + REQUIRES_ACCOUNT_TTL_MS });
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
export async function runContentPicker(opts, deps = {}) {
|
|
125
|
+
try {
|
|
126
|
+
if (!opts.instanceId)
|
|
127
|
+
throw createCliError('VALIDATION', 'instanceId is required');
|
|
128
|
+
const client = clientFor(opts, deps);
|
|
129
|
+
const page = opts.page ?? 1;
|
|
130
|
+
const createDsr = opts.availableTools?.has('pitcher_dsr_create') === true;
|
|
131
|
+
const contentTypes = opts.contentTypes ?? [];
|
|
132
|
+
const params = {
|
|
133
|
+
instance_id: opts.instanceId,
|
|
134
|
+
type__in: 'content,asset',
|
|
135
|
+
page,
|
|
136
|
+
page_size: PICKER_PAGE_SIZE,
|
|
137
|
+
};
|
|
138
|
+
if (opts.query)
|
|
139
|
+
params.search = opts.query;
|
|
140
|
+
if (contentTypes.length)
|
|
141
|
+
params.content_type__in = contentTypes.join(',');
|
|
142
|
+
// Settings are read only where they matter: page 1 (the view keeps the first answer) of a server that can create.
|
|
143
|
+
const settings = createDsr && page === 1 ? cachedRequiresAccount(client, opts, opts.instanceId) : Promise.resolve(null);
|
|
144
|
+
const files = client.get('/api/v1/files/', { params }).then((res) => ({ rows: (res.data?.results ?? []), hasMore: Boolean(res.data?.next) }), (e) => {
|
|
145
|
+
// DRF answers 404 "Invalid page." past the last page (a file deleted between two requests): exhausted, not an error.
|
|
146
|
+
if (page > 1 && isNotFound(e))
|
|
147
|
+
return { rows: [], hasMore: false };
|
|
148
|
+
throw e;
|
|
149
|
+
});
|
|
150
|
+
const [list, requiresAccount] = await Promise.all([files, settings]);
|
|
151
|
+
return ok(fitToBudget({
|
|
152
|
+
instanceId: opts.instanceId,
|
|
153
|
+
query: opts.query ?? null,
|
|
154
|
+
contentTypes,
|
|
155
|
+
page,
|
|
156
|
+
items: list.rows.map(toPickerItem),
|
|
157
|
+
hasMore: list.hasMore,
|
|
158
|
+
actions: { createDsr },
|
|
159
|
+
requiresAccount,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
return fail(toCliError(e));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export async function runContentItem(opts, deps = {}) {
|
|
167
|
+
const env = await runFilesGet(opts, deps);
|
|
168
|
+
if (!env.ok)
|
|
169
|
+
return env;
|
|
170
|
+
const row = (env.data ?? {});
|
|
171
|
+
const item = toPickerItem(row);
|
|
172
|
+
const thumbs = Array.isArray(row.content_thumbnails) ? row.content_thumbnails : [];
|
|
173
|
+
let pages = thumbs.slice(0, 4);
|
|
174
|
+
const data = () => ({
|
|
175
|
+
id: item.id,
|
|
176
|
+
name: item.name,
|
|
177
|
+
thumbnailUrl: item.thumbnailUrl,
|
|
178
|
+
fileType: item.fileType,
|
|
179
|
+
size: typeof row.size === 'number' ? row.size : null,
|
|
180
|
+
shareable: item.shareable,
|
|
181
|
+
pages,
|
|
182
|
+
});
|
|
183
|
+
while (pages.length && jsonBytes(data()) > ITEM_MAX_BYTES)
|
|
184
|
+
pages = pages.slice(0, -1);
|
|
185
|
+
return ok(data());
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Share-tagged canvas templates, newest first (the rule next-core's fetchShareboxTemplate and the rep UI use).
|
|
189
|
+
* The access-scoped Django list may not start with next-core's own default, so the view's default option sends no templateId.
|
|
190
|
+
*/
|
|
191
|
+
export async function runShareboxTemplates(opts, deps = {}) {
|
|
192
|
+
try {
|
|
193
|
+
if (!opts.instanceId)
|
|
194
|
+
throw createCliError('VALIDATION', 'instanceId is required');
|
|
195
|
+
const client = clientFor(opts, deps);
|
|
196
|
+
const res = await client.get('/api/v1/canvases/', {
|
|
197
|
+
params: {
|
|
198
|
+
instance_id: opts.instanceId,
|
|
199
|
+
is_template: true,
|
|
200
|
+
is_section: false,
|
|
201
|
+
tags__in: 'share',
|
|
202
|
+
ordering: '-created_at',
|
|
203
|
+
fields: 'id,name,thumbnail_url',
|
|
204
|
+
page_size: 50,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
const rows = (res.data?.results ?? []);
|
|
208
|
+
const all = rows.map((r) => ({
|
|
209
|
+
id: String(r.id),
|
|
210
|
+
name: typeof r.name === 'string' ? r.name : String(r.id),
|
|
211
|
+
thumbnailUrl: str(r.thumbnail_url),
|
|
212
|
+
}));
|
|
213
|
+
let templates = all;
|
|
214
|
+
while (templates.length && jsonBytes({ templates }) > TEMPLATES_MAX_BYTES)
|
|
215
|
+
templates = templates.slice(0, -1);
|
|
216
|
+
const dropped = all.length - templates.length;
|
|
217
|
+
return ok(dropped ? { templates, truncated: dropped } : { templates });
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
return fail(toCliError(e));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
export const contentPickerTools = [
|
|
224
|
+
{
|
|
225
|
+
name: 'pitcher_content_picker',
|
|
226
|
+
title: 'Pick files (visual gallery)',
|
|
227
|
+
description: 'Interactive picker; in hosts without UI it returns a text list (id, type, name). Shows the user a gallery of files ' +
|
|
228
|
+
'in an instance to browse, filter, select and share as a DSR. For an id lookup during a task use pitcher_search instead.',
|
|
229
|
+
inputShape: {
|
|
230
|
+
instanceId: z.string().describe('Instance ID (required)'),
|
|
231
|
+
query: z.string().optional().describe('Search words (file name or tag). Omit to browse the newest files'),
|
|
232
|
+
contentTypes: z.array(z.enum(CONTENT_FILTERS)).optional().describe('Filter by content type'),
|
|
233
|
+
page: z.number().int().min(1).optional().describe('1-based'),
|
|
234
|
+
},
|
|
235
|
+
run: runContentPicker,
|
|
236
|
+
toText: pickerText,
|
|
237
|
+
readOnly: true,
|
|
238
|
+
access: 'read', // PIT-10114: explicit for every app tool
|
|
239
|
+
ui: { view: 'content-picker' },
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: 'pitcher_content_item',
|
|
243
|
+
title: 'File detail (picker UI helper)',
|
|
244
|
+
description: 'UI helper for the Pitcher content picker view: returns one file summary for its detail panel. ' +
|
|
245
|
+
'Not for general use: prefer pitcher_files_get.',
|
|
246
|
+
inputShape: { id: z.string().describe('File ID') },
|
|
247
|
+
run: runContentItem,
|
|
248
|
+
toText: (d) => {
|
|
249
|
+
const x = d;
|
|
250
|
+
return `file ${x.id} ${x.name}`;
|
|
251
|
+
},
|
|
252
|
+
readOnly: true,
|
|
253
|
+
access: 'read', // PIT-10114: explicit for every app tool
|
|
254
|
+
ui: { view: 'content-picker', visibility: ['app'] },
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: 'pitcher_sharebox_templates',
|
|
258
|
+
title: 'ShareBox templates (picker UI helper)',
|
|
259
|
+
description: "UI helper for the Pitcher content picker view: lists the instance's ShareBox templates for the DSR confirm step.",
|
|
260
|
+
inputShape: { instanceId: z.string().describe('Instance ID') },
|
|
261
|
+
run: runShareboxTemplates,
|
|
262
|
+
toText: (d) => d.templates.map((t) => `${t.id} ${t.name}`).join('\n') ||
|
|
263
|
+
'No ShareBox templates',
|
|
264
|
+
readOnly: true,
|
|
265
|
+
access: 'read', // PIT-10114: explicit for every app tool
|
|
266
|
+
ui: { view: 'content-picker', visibility: ['app'] },
|
|
267
|
+
},
|
|
268
|
+
];
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { AxiosInstance } from 'axios';
|
|
2
|
+
import { type DsrHeader, type DsrPageStat, type DsrUnavailable, type DsrViewerRow } from '../../core/dsr-engagement.js';
|
|
3
|
+
import { type Envelope } from '../../core/envelope.js';
|
|
4
|
+
import { type CommonOpts } from '../../core/http.js';
|
|
5
|
+
import type { ToolDescriptor } from '../tools/types.js';
|
|
6
|
+
import { type AppViewDescriptor } from './types.js';
|
|
7
|
+
export type { DsrHeader, DsrPageStat, DsrUnavailable, DsrViewerRow } from '../../core/dsr-engagement.js';
|
|
8
|
+
/**
|
|
9
|
+
* MCP Apps DSR engagement view (PIT-10067): who opened a DSR canvas, one viewer's visit timeline, and time per page
|
|
10
|
+
* of each file, plus a Content tab over a stated sample of the most recently active viewers. Read-only. Every read
|
|
11
|
+
* runs with the caller's own credential through clickhouse-api, which re-checks canvas access per call (the canvas
|
|
12
|
+
* fetch is the gate), so a user sees exactly the viewers Sharing Analytics shows them. The runners live here, not
|
|
13
|
+
* in commands/analytics.ts: registry.test.ts pins one `run*` per CLI tool, and app tools are not CLI tools.
|
|
14
|
+
*/
|
|
15
|
+
export declare const dsrEngagementView: AppViewDescriptor;
|
|
16
|
+
/** structuredContent budget per result (the picker's, PIT-10066 D14). */
|
|
17
|
+
export declare const DSR_VIEW_MAX_BYTES = 120000;
|
|
18
|
+
/** Model-facing text budget per result, in UTF-8 bytes. */
|
|
19
|
+
export declare const DSR_TEXT_MAX_BYTES = 4096;
|
|
20
|
+
export declare const VIEWERS_PAGE_SIZE = 25;
|
|
21
|
+
export declare const VIEWER_VISITS_MAX = 50;
|
|
22
|
+
export declare const VISIT_ITEMS_MAX = 60;
|
|
23
|
+
export declare const FILE_META_MAX = 200;
|
|
24
|
+
export declare const CONTENT_MAX_VIEWERS = 15;
|
|
25
|
+
export declare const CONTENT_CONCURRENCY = 3;
|
|
26
|
+
export declare const CONTENT_DEADLINE_MS = 20000;
|
|
27
|
+
export declare const VIEWER_DEADLINE_MS = 25000;
|
|
28
|
+
export type DsrViewerType = 'external' | 'internal' | 'all';
|
|
29
|
+
export type DsrWindow = '30d' | '90d' | '365d';
|
|
30
|
+
export type DsrSortBy = 'last_active' | 'total_engagement_time' | 'session_count';
|
|
31
|
+
export interface DsrScope {
|
|
32
|
+
instanceId: string;
|
|
33
|
+
canvasId: string;
|
|
34
|
+
viewerType: DsrViewerType;
|
|
35
|
+
window: DsrWindow;
|
|
36
|
+
/** The ISO date `window` mapped to on the server's clock; null for 365d (the route's own default). */
|
|
37
|
+
startDate: string | null;
|
|
38
|
+
}
|
|
39
|
+
export interface DsrViewersPage {
|
|
40
|
+
rows: DsrViewerRow[];
|
|
41
|
+
total: number;
|
|
42
|
+
page: number;
|
|
43
|
+
hasMore: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface DsrFileStat {
|
|
46
|
+
fileId: string;
|
|
47
|
+
name: string;
|
|
48
|
+
seconds: number;
|
|
49
|
+
views: number;
|
|
50
|
+
viewers?: number;
|
|
51
|
+
estimated: boolean;
|
|
52
|
+
pageCount: number;
|
|
53
|
+
dense: boolean;
|
|
54
|
+
pages: DsrPageStat[];
|
|
55
|
+
pagesTruncated?: number;
|
|
56
|
+
thumbnailUrl: string | null;
|
|
57
|
+
contentType: string | null;
|
|
58
|
+
}
|
|
59
|
+
export interface DsrVisitItemView {
|
|
60
|
+
type: 'section' | 'file' | 'page';
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
fileId?: string;
|
|
64
|
+
page?: number;
|
|
65
|
+
views: number;
|
|
66
|
+
seconds: number;
|
|
67
|
+
estimated: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface DsrVisitView {
|
|
70
|
+
visit: number;
|
|
71
|
+
startedAt: string;
|
|
72
|
+
endedAt: string;
|
|
73
|
+
engagedSeconds: number;
|
|
74
|
+
items: DsrVisitItemView[];
|
|
75
|
+
itemsTruncated?: number;
|
|
76
|
+
}
|
|
77
|
+
export interface DsrSectionStat {
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
seconds: number;
|
|
81
|
+
views: number;
|
|
82
|
+
}
|
|
83
|
+
export interface DsrViewerData {
|
|
84
|
+
kind: 'viewer';
|
|
85
|
+
scope: DsrScope;
|
|
86
|
+
identity: string;
|
|
87
|
+
/** From the overview row when dsr_view has it, else null (the view keeps the clicked row's flag). */
|
|
88
|
+
isExternal: boolean | null;
|
|
89
|
+
totals: {
|
|
90
|
+
visits: number;
|
|
91
|
+
engagedSeconds: number;
|
|
92
|
+
firstVisit: string | null;
|
|
93
|
+
lastVisit: string | null;
|
|
94
|
+
};
|
|
95
|
+
files: DsrFileStat[];
|
|
96
|
+
sections: DsrSectionStat[];
|
|
97
|
+
visits: DsrVisitView[];
|
|
98
|
+
visitsTotal: number;
|
|
99
|
+
metaTruncated?: number;
|
|
100
|
+
metaSkipped?: boolean;
|
|
101
|
+
truncated?: string;
|
|
102
|
+
}
|
|
103
|
+
export interface DsrOverviewData {
|
|
104
|
+
kind: 'overview';
|
|
105
|
+
scope: DsrScope;
|
|
106
|
+
header: DsrHeader;
|
|
107
|
+
viewers: DsrViewersPage;
|
|
108
|
+
unavailable: DsrUnavailable[];
|
|
109
|
+
viewer?: DsrViewerData;
|
|
110
|
+
fetchedAt: string;
|
|
111
|
+
}
|
|
112
|
+
export interface DsrViewersPageData extends DsrViewersPage {
|
|
113
|
+
kind: 'viewers-page';
|
|
114
|
+
scope: DsrScope;
|
|
115
|
+
sortBy: DsrSortBy;
|
|
116
|
+
search: string | null;
|
|
117
|
+
unavailable: DsrUnavailable[];
|
|
118
|
+
}
|
|
119
|
+
export interface DsrContentData {
|
|
120
|
+
kind: 'content';
|
|
121
|
+
scope: DsrScope;
|
|
122
|
+
canvasName: string | null;
|
|
123
|
+
files: DsrFileStat[];
|
|
124
|
+
sections: Array<DsrSectionStat & {
|
|
125
|
+
viewers: number;
|
|
126
|
+
}>;
|
|
127
|
+
viewersCovered: number;
|
|
128
|
+
viewersSampled: number;
|
|
129
|
+
viewersFailed: number;
|
|
130
|
+
viewersTotal: number | null;
|
|
131
|
+
sampleSource: 'recent' | 'listed';
|
|
132
|
+
deadlineHit: boolean;
|
|
133
|
+
/** The viewers read's `_meta.unavailable` ([] when identities were passed in). */
|
|
134
|
+
unavailable: DsrUnavailable[];
|
|
135
|
+
metaSkipped?: boolean;
|
|
136
|
+
metaTruncated?: number;
|
|
137
|
+
truncated?: string;
|
|
138
|
+
}
|
|
139
|
+
export type DsrResult = DsrOverviewData | DsrViewersPageData | DsrViewerData | DsrContentData;
|
|
140
|
+
interface DsrOpts extends CommonOpts {
|
|
141
|
+
instanceId?: string;
|
|
142
|
+
canvasId?: string;
|
|
143
|
+
viewerType?: string;
|
|
144
|
+
window?: string;
|
|
145
|
+
viewerIdentity?: string;
|
|
146
|
+
viewerIdentities?: string[];
|
|
147
|
+
page?: number;
|
|
148
|
+
sortBy?: string;
|
|
149
|
+
search?: string;
|
|
150
|
+
}
|
|
151
|
+
export interface DsrDeps {
|
|
152
|
+
client?: AxiosInstance;
|
|
153
|
+
/** Clock in ms (tests drive the deadlines). Default Date.now. */
|
|
154
|
+
now?: () => number;
|
|
155
|
+
/** Abort signal firing after `ms`. Default AbortSignal.timeout. */
|
|
156
|
+
abortAfter?: (ms: number) => AbortSignal;
|
|
157
|
+
}
|
|
158
|
+
/** `today (UTC) - N days` as YYYY-MM-DD; null for 365d, which sends no dates (the route's default). */
|
|
159
|
+
export declare function windowStart(w: DsrWindow, nowMs: number): string | null;
|
|
160
|
+
/**
|
|
161
|
+
* Viewer (7.4 item 2): items per visit are already capped at 60 and the newest 50 visits kept; then, while over
|
|
162
|
+
* `budget`, drop the oldest visit, then files from the end. File totals were computed over every visit before any
|
|
163
|
+
* cut. Measured with `truncated` included.
|
|
164
|
+
*/
|
|
165
|
+
export declare function fitViewer(data: DsrViewerData, budget?: number): DsrViewerData;
|
|
166
|
+
/** Content (7.4 item 3): pages are capped at 300 per file already; drop files from the end. */
|
|
167
|
+
export declare function fitContent(data: DsrContentData, budget?: number): DsrContentData;
|
|
168
|
+
/**
|
|
169
|
+
* The drilled identity's flag. The External and Internal filters decide it. Under `all` an address can be listed
|
|
170
|
+
* twice (signed in, and through the link with the marker) and the drill covers both, so only a page that holds
|
|
171
|
+
* every row can tell: one agreeing flag there, else null (PR 6010 review).
|
|
172
|
+
*/
|
|
173
|
+
export declare function flagOf(page: DsrViewersPage, identity: string, viewerType: DsrViewerType): boolean | null;
|
|
174
|
+
export declare function runDsrView(opts: DsrOpts, deps?: DsrDeps): Promise<Envelope<DsrOverviewData>>;
|
|
175
|
+
export declare function runDsrViewersPage(opts: DsrOpts, deps?: DsrDeps): Promise<Envelope<DsrViewersPageData>>;
|
|
176
|
+
export declare function runDsrViewer(opts: DsrOpts, deps?: DsrDeps): Promise<Envelope<DsrViewerData>>;
|
|
177
|
+
/**
|
|
178
|
+
* Content (6.4, D21): up to 15 deduped identities (passed by the view, else page 1 of the viewers by last active),
|
|
179
|
+
* their timelines three at a time under ONE 20 s deadline whose signal reaches every request; a failed or late
|
|
180
|
+
* timeline is "not covered", never a tool failure, unless every timeline failed.
|
|
181
|
+
*/
|
|
182
|
+
export declare function runDsrContent(opts: DsrOpts, deps?: DsrDeps): Promise<Envelope<DsrContentData>>;
|
|
183
|
+
/** 7.3 item 5: the full scope of the list, so the model's next page is the same list. */
|
|
184
|
+
export declare function continuationArgs(scope: DsrScope, sortBy: DsrSortBy, search: string | null, page: number, total: number): string;
|
|
185
|
+
/**
|
|
186
|
+
* 8.2 row 6: the list is lost and the viewers exist, or the counts that could say "none" are lost with it (then 0 is
|
|
187
|
+
* missing data, never "nobody opened", PR 6010 review).
|
|
188
|
+
*/
|
|
189
|
+
export declare const listFailed: (unavailable: DsrUnavailable[], viewers: number) => boolean;
|
|
190
|
+
export declare function dsrViewText(raw: unknown): string;
|
|
191
|
+
export declare function dsrViewersPageText(raw: unknown): string;
|
|
192
|
+
export declare function dsrViewerText(raw: unknown): string;
|
|
193
|
+
export declare function dsrContentText(raw: unknown): string;
|
|
194
|
+
export declare const dsrEngagementTools: ToolDescriptor[];
|