@okostream/mcp 0.12.1 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,190 +0,0 @@
1
- // Управление тем, что зритель видит прямо сейчас.
2
- //
3
- // Это не управление видео: поток идёт из плеера организатора. Отсюда
4
- // меняются видимость блоков, режим чата, плашка эфира и обновление страниц.
5
- // У события нет статуса — только живое состояние в RTDB.
6
- import { z } from "zod";
7
- import { requireConfirm, textResult } from "../utils.js";
8
- import { authed } from "./context.js";
9
- import { basicEventOutputSchema, boolOrNull, eventIdField, idempotentWriteTool, liveStateOutputSchema, readOnlyTool, toolDescription, writeTool } from "./schemas.js";
10
- import { chatRtdb, functions, rtdb } from "../firebase.js";
11
- import { get, ref, remove, set } from "firebase/database";
12
- import { httpsCallable } from "firebase/functions";
13
- import { readEvent } from "./eventAccess.js";
14
- export function registerLiveTools(server) {
15
- server.registerTool("oko_get_live_state", {
16
- title: "Get live state",
17
- description: toolDescription({
18
- summary: "Read main RTDB live controls, aggregate analytics, and chat settings for an event.",
19
- args: ["eventId: event to inspect."],
20
- returns: "eventId, agg metrics, ctrl live overrides, and chat settings.",
21
- examples: ["Use during a webinar before changing live block visibility or starting/ending sessions."],
22
- }),
23
- inputSchema: { eventId: eventIdField },
24
- outputSchema: liveStateOutputSchema,
25
- annotations: readOnlyTool,
26
- }, async ({ eventId }) => {
27
- const { uid } = await authed(["analytics:read"]);
28
- await readEvent(eventId, uid, "analytics");
29
- const [aggSnap, ctrlSnap, chatSettingsSnap] = await Promise.all([
30
- get(ref(rtdb, `events/${eventId}/agg`)),
31
- get(ref(rtdb, `events/${eventId}/ctrl`)),
32
- get(ref(chatRtdb, `settings/${eventId}`)),
33
- ]);
34
- return textResult({ eventId, agg: aggSnap.val() ?? {}, ctrl: ctrlSnap.val() ?? {}, chat: chatSettingsSnap.val() ?? {} });
35
- });
36
- server.registerTool("oko_set_block_visibility", {
37
- title: "Set live block visibility",
38
- description: toolDescription({
39
- summary: "Set or reset a live RTDB visibility override for a public event block.",
40
- args: ["eventId: event to control.", "block: chat, banner, cta, cards, or placeholder.", "visible: true/false, or null to reset override.", "confirm: must be true."],
41
- returns: "eventId, block, and effective override value.",
42
- examples: ["Use visible=false to hide the CTA live; use visible=null to return to setup defaults."],
43
- notes: [
44
- "block=chat with visible=false PAUSES message input: viewers keep seeing the chat history but cannot send. It does not hide the chat panel.",
45
- ],
46
- }),
47
- inputSchema: {
48
- eventId: eventIdField,
49
- block: z.enum(["chat", "banner", "cta", "cards", "placeholder"]).describe("Public block whose live override should change."),
50
- visible: boolOrNull.describe("true/false to force visibility, null to reset override."),
51
- confirm: z.boolean().optional().describe("Required true for live visibility change."),
52
- },
53
- outputSchema: basicEventOutputSchema,
54
- annotations: idempotentWriteTool,
55
- }, async ({ eventId, block, visible, confirm }) => {
56
- const { uid } = await authed(["live:control"]);
57
- await readEvent(eventId, uid, "live");
58
- requireConfirm(confirm, "oko_set_block_visibility", { eventId, block, visible });
59
- const paths = {
60
- chat: "chatVisible",
61
- banner: "bannerVisible",
62
- cta: "ctaVisible",
63
- cards: "cardsVisible",
64
- placeholder: "placeholderVisible",
65
- };
66
- const target = ref(rtdb, `events/${eventId}/ctrl/${paths[block]}`);
67
- if (visible === null)
68
- await remove(target);
69
- else
70
- await set(target, visible);
71
- return textResult({ eventId, block, visible });
72
- });
73
- server.registerTool("oko_refresh_viewers", {
74
- title: "Refresh viewers",
75
- description: toolDescription({
76
- summary: "Trigger a live viewer refresh tick for an event.",
77
- args: ["eventId: event whose viewers should refresh.", "confirm: must be true."],
78
- returns: "eventId and refreshed=true.",
79
- examples: ["Use after a setup/live change if viewers should reload state immediately."],
80
- }),
81
- inputSchema: { eventId: eventIdField, confirm: z.boolean().optional().describe("Required true to refresh connected viewers.") },
82
- outputSchema: basicEventOutputSchema,
83
- annotations: writeTool,
84
- }, async ({ eventId, confirm }) => {
85
- const { uid } = await authed(["live:control"]);
86
- await readEvent(eventId, uid, "live");
87
- requireConfirm(confirm, "oko_refresh_viewers", { eventId });
88
- await set(ref(rtdb, `events/${eventId}/ctrl/refreshTick`), Date.now());
89
- return textResult({ eventId, refreshed: true });
90
- });
91
- server.registerTool("oko_set_live_badge", {
92
- title: "Set live badge",
93
- description: toolDescription({
94
- summary: "Control the public 'Live' badge on the event page.",
95
- args: [
96
- "eventId: event to control.",
97
- "mode: session (follows the analytics session, product default), manual (badge shown now for durationMinutes), schedule (shown around meta.eventDate), hidden (never shown).",
98
- "durationMinutes: badge window for manual/schedule, 15-1440 minutes, default 120.",
99
- "confirm: must be true.",
100
- ],
101
- returns: "eventId, mode, durationMinutes.",
102
- examples: ['Use mode="manual" during an unscheduled stream; return to mode="session" afterwards.'],
103
- notes: ["The badge is a page indicator only; it never starts or stops video or analytics."],
104
- }),
105
- inputSchema: {
106
- eventId: eventIdField,
107
- mode: z.enum(["session", "manual", "schedule", "hidden"]).describe("Live badge mode."),
108
- durationMinutes: z.number().int().min(15).max(1440).optional().describe("Badge window in minutes for manual/schedule modes; default 120."),
109
- confirm: z.boolean().optional().describe("Required true to change the public live badge."),
110
- },
111
- outputSchema: basicEventOutputSchema,
112
- annotations: idempotentWriteTool,
113
- }, async ({ eventId, mode, durationMinutes, confirm }) => {
114
- const { uid } = await authed(["live:control"]);
115
- await readEvent(eventId, uid, "live");
116
- requireConfirm(confirm, "oko_set_live_badge", { eventId, mode, durationMinutes: durationMinutes ?? null });
117
- const value = { mode, durationMinutes: durationMinutes ?? 120 };
118
- await set(ref(rtdb, `events/${eventId}/ctrl/liveBadge`), value);
119
- return textResult({ eventId, ...value });
120
- });
121
- server.registerTool("oko_set_chat_mode", {
122
- title: "Set chat mode",
123
- description: toolDescription({
124
- summary: "Set who can send chat messages on the public event page.",
125
- args: [
126
- "eventId: event to control.",
127
- "mode: public (anyone), auth (signed-in viewers only), owner (organizers only), off (input closed, history stays visible), or null to reset to the product default (public).",
128
- "confirm: must be true.",
129
- ],
130
- returns: "eventId and the effective chat mode.",
131
- examples: ['Use mode="off" to pause a chat storm; use mode=null or "public" to reopen.'],
132
- notes: [
133
- 'This is the full replacement for the legacy pause via oko_set_block_visibility(block="chat"): prefer oko_set_chat_mode.',
134
- "History always stays visible to viewers; mode only gates sending.",
135
- ],
136
- }),
137
- inputSchema: {
138
- eventId: eventIdField,
139
- mode: z.enum(["public", "auth", "owner", "off"]).nullable().describe("Chat mode; null resets the override to the default."),
140
- confirm: z.boolean().optional().describe("Required true to change the live chat mode."),
141
- },
142
- outputSchema: basicEventOutputSchema,
143
- annotations: idempotentWriteTool,
144
- }, async ({ eventId, mode, confirm }) => {
145
- const { uid } = await authed(["live:control"]);
146
- await readEvent(eventId, uid, "live");
147
- requireConfirm(confirm, "oko_set_chat_mode", { eventId, mode });
148
- const target = ref(rtdb, `events/${eventId}/ctrl/chatMode`);
149
- if (mode === null)
150
- await remove(target);
151
- else
152
- await set(target, mode);
153
- return textResult({ eventId, mode });
154
- });
155
- server.registerTool("oko_force_viewer_reload", {
156
- title: "Force viewer reload",
157
- description: toolDescription({
158
- summary: "Force a hard page reload for every connected viewer (emergency recovery).",
159
- args: ["eventId: event whose viewer pages must reload.", "confirm: must be true."],
160
- returns: "eventId and reloaded=true.",
161
- examples: ["Use only when viewer pages are stuck (broken bundle or state) and oko_refresh_viewers did not help."],
162
- notes: [
163
- "Server enforces a 120-second cooldown per event; a repeated call returns the seconds left.",
164
- "Viewers reload with up to 4 seconds of jitter. Prefer oko_refresh_viewers for ordinary content updates.",
165
- ],
166
- }),
167
- inputSchema: {
168
- eventId: eventIdField,
169
- confirm: z.boolean().optional().describe("Required true to hard-reload all viewer pages."),
170
- },
171
- outputSchema: basicEventOutputSchema,
172
- annotations: writeTool,
173
- }, async ({ eventId, confirm }) => {
174
- const { uid } = await authed(["live:control"]);
175
- await readEvent(eventId, uid, "live");
176
- requireConfirm(confirm, "oko_force_viewer_reload", { eventId });
177
- const forceReload = httpsCallable(functions, "forceViewerReload");
178
- try {
179
- await forceReload({ eventId });
180
- }
181
- catch (error) {
182
- const details = error.details;
183
- if (typeof details?.retryInSec === "number") {
184
- throw new Error(`Force reload is cooling down: retry in ${details.retryInSec} seconds.`);
185
- }
186
- throw error;
187
- }
188
- return textResult({ eventId, reloaded: true });
189
- });
190
- }
@@ -1,105 +0,0 @@
1
- // Внутренние уведомления организатора: чтение ленты и отметка прочитанным.
2
- //
3
- // Документы создают только Cloud Functions; отсюда доступны чтение, отметка
4
- // и удаление своих. Текст уведомления не хранится — клиент собирает его из
5
- // типа и meta, поэтому наружу уходит структура, а не готовая строка.
6
- import { collection, doc, getDocs, limit, orderBy, query, startAfter, updateDoc } from "firebase/firestore";
7
- import { z } from "zod";
8
- import { firestore } from "../firebase.js";
9
- import { serialize, textResult } from "../utils.js";
10
- import { authed } from "./context.js";
11
- import { cursorMeta, parseTimestampCursor, pickPage, timestampCursorFrom } from "./formatting.js";
12
- import { cursorField, idempotentWriteTool, limitField, notificationsMarkOutputSchema, notificationsOutputSchema, readOnlyTool, toolDescription } from "./schemas.js";
13
- export function registerNotificationTools(server) {
14
- server.registerTool("oko_list_notifications", {
15
- title: "List notifications",
16
- description: toolDescription({
17
- summary: "List in-app notifications for the authenticated organizer.",
18
- args: [
19
- "unreadOnly: only unread notifications, default false.",
20
- "limit: max notifications, default 20, max 100.",
21
- "cursor: createdAt cursor returned as nextCursor by the previous page.",
22
- ],
23
- returns: "notifications plus unreadCountOnPage and cursor pagination metadata.",
24
- examples: ["Use when the organizer asks what changed in OKO or what notifications are waiting."],
25
- }),
26
- inputSchema: {
27
- unreadOnly: z.boolean().default(false).describe("Return only unread notifications."),
28
- limit: limitField(100, 20),
29
- cursor: cursorField,
30
- },
31
- outputSchema: notificationsOutputSchema,
32
- annotations: readOnlyTool,
33
- }, async ({ unreadOnly, limit: max, cursor }) => {
34
- const { uid } = await authed(["workspace:read"]);
35
- const fetchLimit = unreadOnly ? Math.min(max * 5 + 1, 501) : max + 1;
36
- const constraints = [
37
- orderBy("createdAt", "desc"),
38
- limit(fetchLimit),
39
- ];
40
- if (cursor)
41
- constraints.splice(1, 0, startAfter(parseTimestampCursor(cursor)));
42
- const snap = await getDocs(query(collection(firestore, "userNotifications", uid, "items"), ...constraints));
43
- const matchingDocs = unreadOnly ? snap.docs.filter((item) => item.data().read !== true) : snap.docs;
44
- const { page, hasMore: hasMoreInMatches } = pickPage(matchingDocs, max);
45
- const hasMore = hasMoreInMatches || snap.docs.length >= fetchLimit;
46
- const cursorSource = hasMoreInMatches ? page.at(-1) : snap.docs.at(-1);
47
- const notifications = page.map((item) => ({ id: item.id, ...serialize(item.data()) }));
48
- const nextCursor = hasMore ? timestampCursorFrom(cursorSource?.data().createdAt) : null;
49
- return textResult({
50
- notifications,
51
- unreadCountOnPage: notifications.filter((item) => item.read !== true).length,
52
- ...cursorMeta(notifications.length, max, hasMore, nextCursor),
53
- });
54
- });
55
- server.registerTool("oko_mark_notifications_read", {
56
- title: "Mark notifications read",
57
- description: toolDescription({
58
- summary: "Mark own OKO in-app notifications as read.",
59
- args: [
60
- "notificationIds: optional explicit notification ids, max 100.",
61
- "allUnread: true to mark unread notifications on the first page, max controlled by limit.",
62
- "limit: when allUnread=true, max unread notifications to mark, default 100, max 200.",
63
- ],
64
- returns: "markedRead count and notification ids updated.",
65
- examples: ["Use after oko_list_notifications when the user asks to clear the bell/unread state."],
66
- notes: ["Provide either notificationIds or allUnread=true. This can update only the current organizer's own notifications."],
67
- }),
68
- inputSchema: {
69
- notificationIds: z.array(z.string().min(1).max(160)).max(100).optional().describe("Specific notification document ids."),
70
- allUnread: z.boolean().default(false).describe("Mark unread notifications as read."),
71
- limit: limitField(200, 100),
72
- },
73
- outputSchema: notificationsMarkOutputSchema,
74
- annotations: idempotentWriteTool,
75
- }, async ({ notificationIds, allUnread, limit: max }) => {
76
- // 🔴 Гейт намеренно на запись, хотя инструмент читается как «пометить
77
- // прочитанным». Он делает `updateDoc` и сам помечен `idempotentWriteTool`;
78
- // пока гейтом стоял `workspace:read`, ключ пресета `readOnly`
79
- // (`events:read · analytics:read · workspace:read · leads:read`) мог
80
- // погасить до 200 уведомлений организатора. Единственный смысл пресета
81
- // «только чтение» — что он не пишет. Ущерб здесь не в чужих данных
82
- // (пишем только в свои), а в нарушенном обещании: организатор не увидит
83
- // уведомлений о выданных правах и созданных событиях.
84
- const { uid } = await authed(["workspace:write"]);
85
- if (Boolean(notificationIds?.length) === allUnread) {
86
- throw new Error("Provide either notificationIds or allUnread=true.");
87
- }
88
- let ids = [...new Set(notificationIds ?? [])];
89
- if (allUnread) {
90
- // 🔴 Отбор обязан совпадать с тем, что список ПОКАЗЫВАЕТ как
91
- // непрочитанное, иначе получается неустранимая петля: список считает
92
- // непрочитанным всё, где `read !== true` (в том числе документ без
93
- // поля), а `where("read", "==", false)` документ без поля не находит —
94
- // и не находит его же `where("read", "!=", true)`, потому что запросы
95
- // с неравенством в Firestore пропускают документы без поля вовсе.
96
- // Такое уведомление висело бы вечно, а модель не могла бы его погасить.
97
- // Поэтому фильтруем клиентом тем же предикатом, что и список, набирая
98
- // с запасом — приём уже применён в `oko_list_notifications` выше.
99
- const snap = await getDocs(query(collection(firestore, "userNotifications", uid, "items"), limit(Math.min(max * 5 + 1, 501))));
100
- ids = snap.docs.filter((item) => item.data().read !== true).slice(0, max).map((item) => item.id);
101
- }
102
- await Promise.all(ids.map((id) => updateDoc(doc(firestore, "userNotifications", uid, "items", id), { read: true })));
103
- return textResult({ markedRead: ids.length, notificationIds: ids });
104
- });
105
- }
@@ -1,296 +0,0 @@
1
- // Схемы ввода и вывода инструментов MCP плюс их аннотации.
2
- //
3
- // Схема вывода — часть публичного контракта: клиент проверяет по ней
4
- // structuredContent, и запись, прошедшая в базу при непрошедшей схеме, даёт
5
- // худший из исходов — побочный эффект с последующим отказом. Поэтому схемы
6
- // пропускают дополнительные поля (`passthrough`), а не ломаются на них.
7
- //
8
- // Аннотации (readOnly / write / idempotent / destructive) читает клиент,
9
- // решая, спрашивать ли подтверждение. Инвентарь и области доступа —
10
- // в `public-contract.json`, он же эталон для protocol-теста.
11
- import { z } from "zod";
12
- import { OKO_CONNECTION_STATUSES } from "../onboarding.js";
13
- import { canonicalTelegramChannelSchema } from "../telegramChannelSchema.js";
14
- import { CARD_IMAGE_RATIOS, IMAGE_KINDS } from "./imagePipeline.js";
15
- export const boolOrNull = z.boolean().nullable();
16
- export const DEFAULT_LIST_LIMIT = 30;
17
- export const MAX_LIST_LIMIT = 100;
18
- export const MAX_EXPORT_LIMIT = 200;
19
- export const ANALYTICS_DEFAULT_LIMIT = 10;
20
- export const ANALYTICS_TIMELINE_DEFAULT_LIMIT = 120;
21
- export const MAX_WORKSPACE_NAME_LENGTH = 120;
22
- export const MAX_WORKSPACE_INVITE_CODE_LENGTH = 64;
23
- export const TELEGRAM_POST_STATUSES = ["draft", "scheduled", "sent", "failed", "delivery_unknown"];
24
- export const readOnlyTool = {
25
- readOnlyHint: true,
26
- destructiveHint: false,
27
- idempotentHint: true,
28
- openWorldHint: true,
29
- };
30
- export const writeTool = {
31
- readOnlyHint: false,
32
- destructiveHint: false,
33
- idempotentHint: false,
34
- openWorldHint: true,
35
- };
36
- export const idempotentWriteTool = {
37
- readOnlyHint: false,
38
- destructiveHint: false,
39
- idempotentHint: true,
40
- openWorldHint: true,
41
- };
42
- export const destructiveTool = {
43
- readOnlyHint: false,
44
- destructiveHint: true,
45
- idempotentHint: false,
46
- openWorldHint: true,
47
- };
48
- export const unknownRecordSchema = z.record(z.unknown());
49
- export const basicEventOutputSchema = z.object({
50
- eventId: z.string(),
51
- }).passthrough();
52
- export const updateOutputSchema = z.object({
53
- id: z.string().optional(),
54
- eventId: z.string().optional(),
55
- updated: z.array(z.string()).optional(),
56
- }).passthrough();
57
- export const eventLinksOutputShape = {
58
- url: z.string().url(),
59
- setupUrl: z.string().url(),
60
- analyticsUrl: z.string().url(),
61
- };
62
- export const createWebinarOutputSchema = z.object({
63
- id: z.string(),
64
- workspaceId: z.string(),
65
- sourceEventId: z.string().optional(),
66
- ...eventLinksOutputShape,
67
- }).passthrough();
68
- export const connectionStatusOutputSchema = z.object({
69
- connected: z.boolean(),
70
- status: z.enum(OKO_CONNECTION_STATUSES),
71
- errorCode: z.string().nullable(),
72
- message: z.string(),
73
- product: z.object({
74
- name: z.string(),
75
- summary: z.string(),
76
- videoBoundary: z.string(),
77
- accountRequirement: z.string(),
78
- accessModel: z.string(),
79
- }),
80
- organizerAccountRequired: z.boolean(),
81
- credentialConfigured: z.boolean(),
82
- links: z.object({
83
- website: z.string().url(),
84
- signIn: z.string().url(),
85
- installCenter: z.string().url(),
86
- }),
87
- nextSteps: z.array(z.object({
88
- id: z.string(),
89
- label: z.string(),
90
- url: z.string().url().optional(),
91
- command: z.string().optional(),
92
- })),
93
- minimumVersion: z.string().optional(),
94
- uid: z.string().optional(),
95
- scopes: z.array(z.string()).optional(),
96
- });
97
- export const cursorMetaSchema = {
98
- count: z.number().int().nonnegative(),
99
- limit: z.number().int().positive(),
100
- hasMore: z.boolean(),
101
- nextCursor: z.string().nullable(),
102
- has_more: z.boolean(),
103
- next_cursor: z.string().nullable(),
104
- };
105
- export const workspacesOutputSchema = z.object({
106
- workspaces: z.array(unknownRecordSchema),
107
- count: z.number().int().nonnegative(),
108
- }).passthrough();
109
- export const eventsOutputSchema = z.object({
110
- events: z.array(unknownRecordSchema),
111
- ...cursorMetaSchema,
112
- }).passthrough();
113
- export const eventOutputSchema = z.object({
114
- id: z.string(),
115
- ...eventLinksOutputShape,
116
- }).passthrough();
117
- export const liveStateOutputSchema = z.object({
118
- eventId: z.string(),
119
- agg: unknownRecordSchema,
120
- ctrl: unknownRecordSchema,
121
- chat: unknownRecordSchema,
122
- }).passthrough();
123
- export const analyticsOutputSchema = z.object({
124
- sessions: z.array(unknownRecordSchema),
125
- ...cursorMetaSchema,
126
- }).passthrough();
127
- export const chatOutputSchema = z.object({
128
- messages: z.array(unknownRecordSchema),
129
- ...cursorMetaSchema,
130
- }).passthrough();
131
- export const leadsOutputSchema = z.object({
132
- leads: z.array(unknownRecordSchema).optional(),
133
- csv: z.string().optional(),
134
- ...cursorMetaSchema,
135
- }).passthrough();
136
- export const imageOutputSchema = z.object({
137
- eventId: z.string(),
138
- kind: z.enum(IMAGE_KINDS),
139
- imageId: z.string(),
140
- storagePath: z.string(),
141
- masterUrl: z.string().url(),
142
- variantsPending: z.boolean(),
143
- sourceBytes: z.number().int().nonnegative(),
144
- masterBytes: z.number().int().nonnegative(),
145
- imageWidth: z.number().int().positive(),
146
- imageHeight: z.number().int().positive(),
147
- note: z.string(),
148
- }).passthrough();
149
- export const deleteImageOutputSchema = z.object({
150
- eventId: z.string(),
151
- kind: z.enum(IMAGE_KINDS),
152
- imageId: z.string().nullable(),
153
- deleted: z.boolean(),
154
- storageDeletesAttempted: z.number().int().nonnegative(),
155
- storageDeleteWarnings: z.array(z.string()),
156
- }).passthrough();
157
- export const imageVariantsOutputSchema = z.object({
158
- eventId: z.string(),
159
- kind: z.enum(IMAGE_KINDS),
160
- cardId: z.string().nullable(),
161
- imageId: z.string().nullable(),
162
- imageUrl: z.string().url().nullable(),
163
- imageMasterUrl: z.string().url().nullable(),
164
- imageVariants: unknownRecordSchema.nullable(),
165
- ready: z.boolean(),
166
- reason: z.string().nullable(),
167
- elapsedMs: z.number().int().nonnegative(),
168
- }).passthrough();
169
- export const cardImagesBatchOutputSchema = z.object({
170
- eventId: z.string(),
171
- uploads: z.array(unknownRecordSchema),
172
- variants: z.array(unknownRecordSchema),
173
- ready: z.boolean(),
174
- warnings: z.array(z.string()),
175
- }).passthrough();
176
- export const setupValidationOutputSchema = z.object({
177
- eventId: z.string(),
178
- ok: z.boolean(),
179
- score: z.number().int().min(0).max(100),
180
- errors: z.array(unknownRecordSchema),
181
- warnings: z.array(unknownRecordSchema),
182
- suggestions: z.array(unknownRecordSchema),
183
- nextActions: z.array(z.string()),
184
- links: unknownRecordSchema,
185
- }).passthrough();
186
- export const workspaceAccessOutputSchema = z.object({
187
- workspaceId: z.string(),
188
- workspace: unknownRecordSchema,
189
- members: z.array(unknownRecordSchema),
190
- invites: z.array(unknownRecordSchema),
191
- }).passthrough();
192
- export const telegramChannelsOutputSchema = z.object({
193
- channels: z.array(canonicalTelegramChannelSchema),
194
- count: z.number().int().nonnegative(),
195
- }).passthrough();
196
- export const telegramPostsOutputSchema = z.object({
197
- posts: z.array(unknownRecordSchema),
198
- ...cursorMetaSchema,
199
- }).passthrough();
200
- export const sessionReportOutputSchema = z.object({
201
- eventId: z.string(),
202
- sessionId: z.string(),
203
- reportUrl: z.string().url(),
204
- markdown: z.string(),
205
- session: unknownRecordSchema,
206
- }).passthrough();
207
- export const profileOutputSchema = z.object({
208
- uid: z.string(),
209
- profile: unknownRecordSchema,
210
- updated: z.array(z.string()),
211
- }).passthrough();
212
- export const deleteLeadOutputSchema = z.object({
213
- eventId: z.string(),
214
- leadId: z.string(),
215
- deleted: z.boolean(),
216
- }).passthrough();
217
- export const notificationsOutputSchema = z.object({
218
- notifications: z.array(unknownRecordSchema),
219
- unreadCountOnPage: z.number().int().nonnegative(),
220
- ...cursorMetaSchema,
221
- }).passthrough();
222
- export const notificationsMarkOutputSchema = z.object({
223
- markedRead: z.number().int().nonnegative(),
224
- notificationIds: z.array(z.string()),
225
- }).passthrough();
226
- export const formFieldSchema = z.object({
227
- id: z.string().min(1).optional(),
228
- label: z.string().max(120),
229
- type: z.enum(["text", "tel", "email", "textarea"]).default("text"),
230
- required: z.boolean().default(false),
231
- placeholder: z.string().max(160).nullable().optional(),
232
- }).strict();
233
- export const formSchema = z.object({
234
- id: z.string().min(1).optional(),
235
- title: z.string().max(160).nullable().optional(),
236
- description: z.string().max(1000).nullable().optional(),
237
- submitLabel: z.string().max(80).nullable().optional(),
238
- successText: z.string().max(500).nullable().optional(),
239
- consentText: z.string().max(500).nullable().optional(),
240
- consentRequired: z.boolean().default(true),
241
- fields: z.array(formFieldSchema).max(30),
242
- }).strict();
243
- export const profileDisplayNameSchema = z.string().max(40).nullable();
244
- export const timezoneSchema = z.string()
245
- .min(1)
246
- .max(100)
247
- .regex(/^[A-Za-z0-9_+./-]+$/, "Use an IANA timezone such as Europe/Moscow or UTC.");
248
- export const cardsConfigOutputSchema = z.object({
249
- eventId: z.string(),
250
- updated: z.array(z.string()),
251
- cards: z.object({
252
- enabled: z.boolean(),
253
- visibility: z.enum(["always", "hidden"]),
254
- cardImageRatio: z.enum(CARD_IMAGE_RATIOS),
255
- items: z.array(unknownRecordSchema),
256
- }).passthrough(),
257
- generatedCardIds: z.array(z.string()),
258
- removedCardIds: z.array(z.string()),
259
- removedCardsWithImages: z.array(z.string()),
260
- preservedImageCardIds: z.array(z.string()),
261
- warnings: z.array(z.string()),
262
- }).passthrough();
263
- export const workspacePermissionsSchema = z.object({
264
- createEvents: z.boolean().optional(),
265
- editSetup: z.boolean().optional(),
266
- deleteEvents: z.boolean().optional(),
267
- controlLive: z.boolean().optional(),
268
- moderateChat: z.boolean().optional(),
269
- startSession: z.boolean().optional(),
270
- viewAnalytics: z.boolean().optional(),
271
- downloadReports: z.boolean().optional(),
272
- sendNotifications: z.boolean().optional(),
273
- }).strict().default({});
274
- export const WORKSPACE_PERMISSION_KEYS = [
275
- "createEvents",
276
- "editSetup",
277
- "deleteEvents",
278
- "controlLive",
279
- "moderateChat",
280
- "startSession",
281
- "viewAnalytics",
282
- "downloadReports",
283
- "sendNotifications",
284
- ];
285
- export const eventIdField = z.string().min(1).describe("OKO event document id, for example KJsAku48821hB4PXTW0S.");
286
- export const limitField = (max = MAX_LIST_LIMIT, defaultValue = DEFAULT_LIST_LIMIT) => z.number().int().min(1).max(max).default(defaultValue).describe(`Maximum items to return, 1-${max}.`);
287
- export const cursorField = z.string().min(1).optional().describe("Cursor returned as nextCursor by the previous page.");
288
- export function toolDescription({ summary, args = [], returns, examples = [], notes = [], }) {
289
- return [
290
- summary,
291
- args.length ? `Args:\n${args.map((item) => `- ${item}`).join("\n")}` : "",
292
- `Returns:\n- ${returns}`,
293
- examples.length ? `Examples:\n${examples.map((item) => `- ${item}`).join("\n")}` : "",
294
- notes.length ? `Notes:\n${notes.map((item) => `- ${item}`).join("\n")}` : "",
295
- ].filter(Boolean).join("\n\n");
296
- }