@okostream/mcp 0.18.3 → 0.19.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 +24 -7
- package/dist/server.js +878 -115
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -70598,6 +70598,7 @@ function buildConnectionStatus(status, options2 = {}) {
|
|
|
70598
70598
|
nextSteps: nextSteps(status, options2.minimumVersion, options2.latestVersion),
|
|
70599
70599
|
...options2.minimumVersion ? { minimumVersion: options2.minimumVersion } : {},
|
|
70600
70600
|
...options2.latestVersion ? { latestVersion: options2.latestVersion } : {},
|
|
70601
|
+
...options2.detail && !connected ? { detail: options2.detail } : {},
|
|
70601
70602
|
...options2.uid ? { uid: options2.uid } : {},
|
|
70602
70603
|
...options2.scopes ? { scopes: options2.scopes } : {},
|
|
70603
70604
|
...options2.update ? { update: options2.update } : {}
|
|
@@ -70659,7 +70660,7 @@ var GUIDE_RESOURCES = [
|
|
|
70659
70660
|
"1. Call `oko_whoami` and inspect scopes.",
|
|
70660
70661
|
"2. If the workspace is unclear, call `oko_list_workspaces`.",
|
|
70661
70662
|
"3. Call `oko_create_webinar` with title/date/workspace and video URL when known.",
|
|
70662
|
-
"4. Call `oko_configure_blocks` for video, forms, CTA, banner, and Telegram settings (Telegram channels there need telegram:write plus the notification permission on the event).",
|
|
70663
|
+
"4. Call `oko_configure_blocks` for video, forms, polls, CTA, banner, and Telegram settings (Telegram channels there need telegram:write plus the notification permission on the event).",
|
|
70663
70664
|
"5. Call `oko_configure_cards` with stable card ids before uploading card images.",
|
|
70664
70665
|
"6. Upload images with `oko_upload_card_images_batch` or `oko_upload_image`, then wait for variants.",
|
|
70665
70666
|
"7. Call `oko_validate_event_setup` and fix blocking findings.",
|
|
@@ -70677,7 +70678,7 @@ var GUIDE_RESOURCES = [
|
|
|
70677
70678
|
text: [
|
|
70678
70679
|
"# OKO Platform Invariants",
|
|
70679
70680
|
"",
|
|
70680
|
-
"OKO does not host or encode video. An organizer brings a source such as YouTube or an iframe player. OKO builds the webinar experience around it: an event page, configurable blocks, cards, forms, chat, analytics, and integrations.",
|
|
70681
|
+
"OKO does not host or encode video. An organizer brings a source such as YouTube or an iframe player. OKO builds the webinar experience around it: an event page, configurable blocks, cards, forms, polls, chat, analytics, and integrations.",
|
|
70681
70682
|
"",
|
|
70682
70683
|
"- OKO events have no `draft`, `live`, or `ended` status field.",
|
|
70683
70684
|
"- Public event pages render whenever the event exists and normal access rules allow it.",
|
|
@@ -70710,15 +70711,15 @@ var GUIDE_RESOURCES = [
|
|
|
70710
70711
|
"- `events:read`: list and read events.",
|
|
70711
70712
|
"- `events:write`: create, duplicate, move, and update events.",
|
|
70712
70713
|
"- `events:delete`: trash, restore, and permanently delete already-trashed events.",
|
|
70713
|
-
"- `setup:write`: configure blocks, forms, cards, and images.",
|
|
70714
|
+
"- `setup:write`: configure blocks, forms, polls, cards, and images.",
|
|
70714
70715
|
"- `analytics:read`: read live state, sessions, reports, and chat history.",
|
|
70715
70716
|
"- `analytics:write` and `live:control`: manage analytics sessions.",
|
|
70716
|
-
"- `live:control`: change live visibility and
|
|
70717
|
+
"- `live:control`: change live visibility, refresh viewers and command polls (`oko_control_poll`).",
|
|
70717
70718
|
"- `chat:write` and `chat:moderate`: publish and moderate event chat.",
|
|
70718
70719
|
"- `telegram:write`: link channels and manage Telegram posts.",
|
|
70719
70720
|
"- `workspace:read` and `workspace:write`: workspaces, members, invites, and notifications.",
|
|
70720
70721
|
"- `leads:read`: read form submissions; deletion also requires `setup:write`.",
|
|
70721
|
-
"- `wallet:read`: read the plan, the lumen balance, grants and ledger of the organizer's own wallet (`oko_wallet`); nothing can be bought, changed, granted or spent with it.",
|
|
70722
|
+
"- `wallet:read`: read the plan, the lumen balance, grants and ledger of the organizer's own wallet (`oko_wallet`) and the balance of the wallet that pays for a workspace (`oko_workspace_wallet`); nothing can be bought, changed, granted or spent with it.",
|
|
70722
70723
|
"",
|
|
70723
70724
|
"Payment and terms are web-only: no tool collects a password, card or bank details, or agreement to terms on anyone's behalf. Plans and lumens are bought, and terms accepted, only in the OKO web cabinet."
|
|
70724
70725
|
].join("\n")
|
|
@@ -70735,11 +70736,12 @@ var GUIDE_RESOURCES = [
|
|
|
70735
70736
|
"- `meta.eventDate`: scheduled date.",
|
|
70736
70737
|
"- `video.originalUrl` / `video.embedUrl`: raw and normalized video URLs.",
|
|
70737
70738
|
"- `forms[]`: lead forms with stable ids, fields, consent, submit, and success copy.",
|
|
70739
|
+
"- `polls[]`: polls with stable ids \u2014 a question, 2 to 10 options with their own ids, one answer or several. A separate list beside `forms[]`: writing one never touches the other.",
|
|
70738
70740
|
"- `blocks.primaryCta`, `blocks.promoBanner`, and `blocks.cards`: audience content and live visibility.",
|
|
70739
70741
|
"- `telegram.channelIds`: claimed Telegram channels for notifications.",
|
|
70740
70742
|
"- Access mirrors are server-owned and must not be written directly.",
|
|
70741
70743
|
"",
|
|
70742
|
-
"A `formId` on a CTA, banner, or card opens that event form in a popup. Use `buttonUrl` only for navigation and set exactly one target in new writes.",
|
|
70744
|
+
"A `formId` on a CTA, banner, or card opens that event form in a popup; a `pollId` opens that poll. Use `buttonUrl` only for navigation and set exactly one target in new writes.",
|
|
70743
70745
|
"",
|
|
70744
70746
|
"Do not remove unknown fields or write access mirrors. Use dedicated MCP tools instead of whole-document rewrites."
|
|
70745
70747
|
].join("\n")
|
|
@@ -116320,7 +116322,7 @@ function sanitizeScopes(scopes) {
|
|
|
116320
116322
|
}
|
|
116321
116323
|
|
|
116322
116324
|
// build/version.js
|
|
116323
|
-
var OKO_MCP_VERSION = "0.
|
|
116325
|
+
var OKO_MCP_VERSION = "0.19.0";
|
|
116324
116326
|
|
|
116325
116327
|
// build/auth.js
|
|
116326
116328
|
var state = null;
|
|
@@ -116526,14 +116528,19 @@ async function getConnectionStatus() {
|
|
|
116526
116528
|
}
|
|
116527
116529
|
const detail = error3 instanceof Error ? `${error3.name}: ${error3.message}` : String(error3);
|
|
116528
116530
|
console.error(`[oko-mcp] connection status: unexpected error \u2014 ${detail}`);
|
|
116529
|
-
return buildConnectionStatus("unavailable", {
|
|
116531
|
+
return buildConnectionStatus("unavailable", { detail: `Unexpected client-side error: ${detail}` });
|
|
116530
116532
|
}
|
|
116531
116533
|
}
|
|
116532
116534
|
function authErrorOptions(error3) {
|
|
116533
116535
|
return {
|
|
116534
116536
|
minimumVersion: error3.minimumVersion,
|
|
116535
116537
|
latestVersion: error3.latestVersion,
|
|
116536
|
-
serverMessage: error3.serverMessage
|
|
116538
|
+
serverMessage: error3.serverMessage,
|
|
116539
|
+
// 🔴 An `unavailable` error carries its cause in `message` (the transport failure, the HTTP status with the
|
|
116540
|
+
// server's note, the client-side initialisation error). Until 0.19.0 only the constant sentence of the status
|
|
116541
|
+
// went out, and a failed smoke could not say why (28.09: `oko_whoami` failed after a 200 of the exchange, the
|
|
116542
|
+
// retry passed, and the first cause was lost). The other statuses say all there is in their fixed wording.
|
|
116543
|
+
...error3.status === "unavailable" ? { detail: error3.message } : {}
|
|
116537
116544
|
};
|
|
116538
116545
|
}
|
|
116539
116546
|
function authErrorResult(error3) {
|
|
@@ -116595,7 +116602,7 @@ var canonicalTelegramChannelSchema = telegramChannelSchema.extend({
|
|
|
116595
116602
|
|
|
116596
116603
|
// build/tools/imagePipeline.js
|
|
116597
116604
|
var import_undici = __toESM(require_undici(), 1);
|
|
116598
|
-
import { randomUUID as
|
|
116605
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
116599
116606
|
import { lookup } from "node:dns/promises";
|
|
116600
116607
|
import { stat, readFile, realpath } from "node:fs/promises";
|
|
116601
116608
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -116817,6 +116824,207 @@ function detectAndNormalize(url) {
|
|
|
116817
116824
|
return { provider: "unknown", normalizedUrl: null };
|
|
116818
116825
|
}
|
|
116819
116826
|
|
|
116827
|
+
// build/tools/pollModel.js
|
|
116828
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
116829
|
+
var POLL_ID_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,63}$/;
|
|
116830
|
+
var POLL_OPTION_ID_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,31}$/;
|
|
116831
|
+
var RESERVED_IDS = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
116832
|
+
function isSafeId(id, pattern) {
|
|
116833
|
+
return typeof id === "string" && pattern.test(id) && !RESERVED_IDS.has(id);
|
|
116834
|
+
}
|
|
116835
|
+
var MAX_POLLS = 10;
|
|
116836
|
+
var MIN_POLL_OPTIONS = 2;
|
|
116837
|
+
var MAX_POLL_OPTIONS = 10;
|
|
116838
|
+
var POLL_QUESTION_MAX = 300;
|
|
116839
|
+
var POLL_OPTION_TEXT_MAX = 200;
|
|
116840
|
+
var POLL_TITLE_MAX = 200;
|
|
116841
|
+
var POLL_LABEL_MAX = 60;
|
|
116842
|
+
var POLL_THANKS_MAX = 500;
|
|
116843
|
+
var DEFAULT_POLL_CTRL = {
|
|
116844
|
+
closed: false,
|
|
116845
|
+
live: false,
|
|
116846
|
+
liveAt: null,
|
|
116847
|
+
resultsShown: false,
|
|
116848
|
+
round: 1
|
|
116849
|
+
};
|
|
116850
|
+
var POLL_ACTIONS = ["close", "open", "show_results", "hide_results", "launch", "stop", "reset"];
|
|
116851
|
+
var CONTROL_ROOM_POLL_ACTIONS = ["launch", "reset"];
|
|
116852
|
+
function isRecord(value) {
|
|
116853
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
116854
|
+
}
|
|
116855
|
+
function boundedText(value, max) {
|
|
116856
|
+
if (typeof value !== "string")
|
|
116857
|
+
return null;
|
|
116858
|
+
const trimmed = value.trim();
|
|
116859
|
+
return trimmed && trimmed.length <= max ? trimmed : null;
|
|
116860
|
+
}
|
|
116861
|
+
function parsePoll(raw) {
|
|
116862
|
+
if (!isRecord(raw))
|
|
116863
|
+
return null;
|
|
116864
|
+
const id = typeof raw.id === "string" ? raw.id : "";
|
|
116865
|
+
const question = boundedText(raw.question, POLL_QUESTION_MAX);
|
|
116866
|
+
if (!isSafeId(id, POLL_ID_PATTERN) || !question || !Array.isArray(raw.options))
|
|
116867
|
+
return null;
|
|
116868
|
+
const options2 = [];
|
|
116869
|
+
const seen = /* @__PURE__ */ new Set();
|
|
116870
|
+
for (const item of raw.options) {
|
|
116871
|
+
if (!isRecord(item))
|
|
116872
|
+
return null;
|
|
116873
|
+
const optionId = typeof item.id === "string" ? item.id : "";
|
|
116874
|
+
const text = boundedText(item.text, POLL_OPTION_TEXT_MAX);
|
|
116875
|
+
if (!isSafeId(optionId, POLL_OPTION_ID_PATTERN) || !text || seen.has(optionId))
|
|
116876
|
+
return null;
|
|
116877
|
+
seen.add(optionId);
|
|
116878
|
+
options2.push({ id: optionId, text });
|
|
116879
|
+
}
|
|
116880
|
+
if (options2.length < MIN_POLL_OPTIONS || options2.length > MAX_POLL_OPTIONS)
|
|
116881
|
+
return null;
|
|
116882
|
+
return {
|
|
116883
|
+
id,
|
|
116884
|
+
question,
|
|
116885
|
+
options: options2,
|
|
116886
|
+
multiple: raw.multiple === true,
|
|
116887
|
+
title: boundedText(raw.title, POLL_TITLE_MAX),
|
|
116888
|
+
submitLabel: boundedText(raw.submitLabel, POLL_LABEL_MAX),
|
|
116889
|
+
thanksText: boundedText(raw.thanksText, POLL_THANKS_MAX)
|
|
116890
|
+
};
|
|
116891
|
+
}
|
|
116892
|
+
function parseEventPolls(raw) {
|
|
116893
|
+
if (!Array.isArray(raw))
|
|
116894
|
+
return [];
|
|
116895
|
+
const polls = [];
|
|
116896
|
+
const ids = /* @__PURE__ */ new Set();
|
|
116897
|
+
for (const item of raw.slice(0, MAX_POLLS)) {
|
|
116898
|
+
const poll = parsePoll(item);
|
|
116899
|
+
if (poll && !ids.has(poll.id)) {
|
|
116900
|
+
ids.add(poll.id);
|
|
116901
|
+
polls.push(poll);
|
|
116902
|
+
}
|
|
116903
|
+
}
|
|
116904
|
+
return polls;
|
|
116905
|
+
}
|
|
116906
|
+
function pollIdSet(polls) {
|
|
116907
|
+
return new Set((Array.isArray(polls) ? polls : []).map((poll) => isRecord(poll) && typeof poll.id === "string" ? poll.id.trim() : "").filter(Boolean));
|
|
116908
|
+
}
|
|
116909
|
+
function readPollState(node) {
|
|
116910
|
+
const raw = isRecord(node) ? node : {};
|
|
116911
|
+
const round = typeof raw.round === "number" && Number.isInteger(raw.round) && raw.round >= 1 ? raw.round : 1;
|
|
116912
|
+
return {
|
|
116913
|
+
closed: raw.closed === true,
|
|
116914
|
+
live: raw.live === true,
|
|
116915
|
+
liveAt: typeof raw.liveAt === "number" && Number.isFinite(raw.liveAt) ? raw.liveAt : null,
|
|
116916
|
+
resultsShown: raw.resultsShown === true,
|
|
116917
|
+
round
|
|
116918
|
+
};
|
|
116919
|
+
}
|
|
116920
|
+
function readPollTally(node) {
|
|
116921
|
+
const raw = isRecord(node) ? node : {};
|
|
116922
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
116923
|
+
const options2 = {};
|
|
116924
|
+
if (isRecord(raw.options)) {
|
|
116925
|
+
for (const [optionId, value] of Object.entries(raw.options)) {
|
|
116926
|
+
if (isSafeId(optionId, POLL_OPTION_ID_PATTERN))
|
|
116927
|
+
options2[optionId] = count(value);
|
|
116928
|
+
}
|
|
116929
|
+
}
|
|
116930
|
+
return { total: count(raw.total), options: options2 };
|
|
116931
|
+
}
|
|
116932
|
+
function readSessionPolls(raw) {
|
|
116933
|
+
if (!Array.isArray(raw))
|
|
116934
|
+
return [];
|
|
116935
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
116936
|
+
const rounds = [];
|
|
116937
|
+
for (const item of raw) {
|
|
116938
|
+
if (!isRecord(item))
|
|
116939
|
+
continue;
|
|
116940
|
+
if (typeof item.pollId !== "string" || !item.pollId)
|
|
116941
|
+
continue;
|
|
116942
|
+
if (typeof item.round !== "number" || !Number.isInteger(item.round) || item.round < 1)
|
|
116943
|
+
continue;
|
|
116944
|
+
const options2 = Array.isArray(item.options) ? item.options.flatMap((option) => isRecord(option) && typeof option.id === "string" ? [{ id: option.id, text: typeof option.text === "string" ? option.text : option.id, count: count(option.count) }] : []) : [];
|
|
116945
|
+
rounds.push({
|
|
116946
|
+
pollId: item.pollId,
|
|
116947
|
+
question: typeof item.question === "string" ? item.question : "",
|
|
116948
|
+
round: item.round,
|
|
116949
|
+
multiple: item.multiple === true,
|
|
116950
|
+
options: options2,
|
|
116951
|
+
total: count(item.total)
|
|
116952
|
+
});
|
|
116953
|
+
}
|
|
116954
|
+
return rounds;
|
|
116955
|
+
}
|
|
116956
|
+
var OPTION_ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
116957
|
+
function newPollId() {
|
|
116958
|
+
return `p-${randomUUID2()}`;
|
|
116959
|
+
}
|
|
116960
|
+
function newOptionId(taken = /* @__PURE__ */ new Set()) {
|
|
116961
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
116962
|
+
const chars = Array.from(randomUUID2().replace(/-/g, "").slice(0, 8), (char) => OPTION_ID_ALPHABET[parseInt(char, 16) % OPTION_ID_ALPHABET.length]);
|
|
116963
|
+
const id = `o${chars.join("")}`;
|
|
116964
|
+
if (!taken.has(id))
|
|
116965
|
+
return id;
|
|
116966
|
+
}
|
|
116967
|
+
return `o${randomUUID2().replace(/-/g, "").slice(0, 16)}`;
|
|
116968
|
+
}
|
|
116969
|
+
var orNull = (value) => value?.trim() || null;
|
|
116970
|
+
function buildPollsForWrite(inputs, ids = { poll: newPollId, option: newOptionId }) {
|
|
116971
|
+
if (inputs.length > MAX_POLLS)
|
|
116972
|
+
throw new Error(`At most ${MAX_POLLS} polls fit on a page.`);
|
|
116973
|
+
const seenPolls = /* @__PURE__ */ new Set();
|
|
116974
|
+
return inputs.map((input, index) => {
|
|
116975
|
+
const where2 = `polls[${index}]`;
|
|
116976
|
+
const id = input.id ?? ids.poll();
|
|
116977
|
+
if (!isSafeId(id, POLL_ID_PATTERN)) {
|
|
116978
|
+
throw new Error(`${where2}.id "${id}" is not a usable poll id: a letter first, then letters, digits or hyphens, up to 64 characters.`);
|
|
116979
|
+
}
|
|
116980
|
+
if (seenPolls.has(id))
|
|
116981
|
+
throw new Error(`${where2}.id "${id}" is used by another poll in this call: every poll needs its own id.`);
|
|
116982
|
+
seenPolls.add(id);
|
|
116983
|
+
const question = input.question.trim();
|
|
116984
|
+
if (!question)
|
|
116985
|
+
throw new Error(`${where2}.question is empty.`);
|
|
116986
|
+
if (question.length > POLL_QUESTION_MAX)
|
|
116987
|
+
throw new Error(`${where2}.question is longer than ${POLL_QUESTION_MAX} characters.`);
|
|
116988
|
+
const taken = new Set(input.options.flatMap((option) => option.id ? [option.id] : []));
|
|
116989
|
+
const seenOptions = /* @__PURE__ */ new Set();
|
|
116990
|
+
const options2 = input.options.map((option, optionIndex) => {
|
|
116991
|
+
const optionWhere = `${where2}.options[${optionIndex}]`;
|
|
116992
|
+
const optionId = option.id ?? ids.option(taken);
|
|
116993
|
+
taken.add(optionId);
|
|
116994
|
+
if (!isSafeId(optionId, POLL_OPTION_ID_PATTERN)) {
|
|
116995
|
+
throw new Error(`${optionWhere}.id "${optionId}" is not a usable option id: a letter first, then letters, digits or hyphens, up to 32 characters.`);
|
|
116996
|
+
}
|
|
116997
|
+
if (seenOptions.has(optionId))
|
|
116998
|
+
throw new Error(`${optionWhere}.id "${optionId}" is used by another option of the same poll.`);
|
|
116999
|
+
seenOptions.add(optionId);
|
|
117000
|
+
const text = option.text.trim();
|
|
117001
|
+
if (!text)
|
|
117002
|
+
throw new Error(`${optionWhere}.text is empty.`);
|
|
117003
|
+
if (text.length > POLL_OPTION_TEXT_MAX)
|
|
117004
|
+
throw new Error(`${optionWhere}.text is longer than ${POLL_OPTION_TEXT_MAX} characters.`);
|
|
117005
|
+
return { id: optionId, text };
|
|
117006
|
+
});
|
|
117007
|
+
if (options2.length < MIN_POLL_OPTIONS || options2.length > MAX_POLL_OPTIONS) {
|
|
117008
|
+
throw new Error(`${where2} needs ${MIN_POLL_OPTIONS} to ${MAX_POLL_OPTIONS} options, got ${options2.length}.`);
|
|
117009
|
+
}
|
|
117010
|
+
const capped = (name7, max) => {
|
|
117011
|
+
const value = orNull(input[name7]);
|
|
117012
|
+
if (value !== null && value.length > max)
|
|
117013
|
+
throw new Error(`${where2}.${name7} is longer than ${max} characters.`);
|
|
117014
|
+
return value;
|
|
117015
|
+
};
|
|
117016
|
+
return {
|
|
117017
|
+
id,
|
|
117018
|
+
question,
|
|
117019
|
+
options: options2,
|
|
117020
|
+
multiple: input.multiple === true,
|
|
117021
|
+
title: capped("title", POLL_TITLE_MAX),
|
|
117022
|
+
submitLabel: capped("submitLabel", POLL_LABEL_MAX),
|
|
117023
|
+
thanksText: capped("thanksText", POLL_THANKS_MAX)
|
|
117024
|
+
};
|
|
117025
|
+
});
|
|
117026
|
+
}
|
|
117027
|
+
|
|
116820
117028
|
// build/tools/formatting.js
|
|
116821
117029
|
var RETIRED_SESSION_FIELDS = ["nmoEvaluation"];
|
|
116822
117030
|
function cursorMeta(count, max, hasMore, nextCursor) {
|
|
@@ -116987,17 +117195,31 @@ function reportQualityCaveats(dataQuality) {
|
|
|
116987
117195
|
caveats.push("watchTimeUnmeasured");
|
|
116988
117196
|
if (marks.watchTime === "unknown_duration")
|
|
116989
117197
|
caveats.push("watchTimeUncapped");
|
|
117198
|
+
if (marks.polls === "unmeasured")
|
|
117199
|
+
caveats.push("pollsUnmeasured");
|
|
116990
117200
|
return caveats;
|
|
116991
117201
|
}
|
|
116992
117202
|
var REPORT_QUALITY_CAVEAT_TEXT = {
|
|
116993
117203
|
baselineMissing: "The session baseline was not saved: every figure below is counted from zero and includes everything the event accumulated before this broadcast.",
|
|
116994
117204
|
startTimeUnknown: "The broadcast start time was not saved: the report is stamped with the moment it ended and its duration reads zero.",
|
|
116995
117205
|
watchTimeUnmeasured: "Watch time was not measured: the engagement snapshot could not be read.",
|
|
116996
|
-
watchTimeUncapped: "Watch time is not capped by the broadcast length \u2014 it is unknown, so nothing vouches for the total, average and median times."
|
|
117206
|
+
watchTimeUncapped: "Watch time is not capped by the broadcast length \u2014 it is unknown, so nothing vouches for the total, average and median times.",
|
|
117207
|
+
pollsUnmeasured: "The votes of this session could not be counted: an empty polls section means \u201Cnot measured\u201D, not \u201Cnobody voted\u201D."
|
|
116997
117208
|
};
|
|
116998
117209
|
function sessionQualityWarnings(dataQuality) {
|
|
116999
117210
|
return reportQualityCaveats(dataQuality).map((caveat) => REPORT_QUALITY_CAVEAT_TEXT[caveat]);
|
|
117000
117211
|
}
|
|
117212
|
+
function pollRoundMarkdown(round) {
|
|
117213
|
+
const name7 = round.question || "(a poll no longer on the page)";
|
|
117214
|
+
const several = round.multiple ? ", several answers allowed" : "";
|
|
117215
|
+
const voters = `${round.total} ${round.total === 1 ? "voter" : "voters"}`;
|
|
117216
|
+
const lines = [`- ${name7} \u2014 round ${round.round}${several}: ${voters}`];
|
|
117217
|
+
for (const option of round.options) {
|
|
117218
|
+
const share = round.total > 0 ? ` (${Math.round(option.count / round.total * 1e3) / 10}%)` : "";
|
|
117219
|
+
lines.push(` - ${option.text}: ${option.count}${share}`);
|
|
117220
|
+
}
|
|
117221
|
+
return lines;
|
|
117222
|
+
}
|
|
117001
117223
|
function sessionReportMarkdown({ eventId, event, sessionId, session }) {
|
|
117002
117224
|
const lines = [
|
|
117003
117225
|
`# OKO session report`,
|
|
@@ -117027,6 +117249,12 @@ function sessionReportMarkdown({ eventId, event, sessionId, session }) {
|
|
|
117027
117249
|
markdownMetric("CTA clicks", session.ctaClicks),
|
|
117028
117250
|
markdownMetric("Cards clicks", session.cardsClicks)
|
|
117029
117251
|
];
|
|
117252
|
+
const pollRounds = readSessionPolls(session.polls);
|
|
117253
|
+
if (pollRounds.length > 0) {
|
|
117254
|
+
lines.push("", "## Polls", "Votes cast during this session. A round that ran through several sessions is split between their reports.");
|
|
117255
|
+
for (const round of pollRounds)
|
|
117256
|
+
lines.push(...pollRoundMarkdown(round));
|
|
117257
|
+
}
|
|
117030
117258
|
const warnings = sessionQualityWarnings(session.dataQuality);
|
|
117031
117259
|
if (warnings.length > 0) {
|
|
117032
117260
|
lines.push("", "## What this report did not measure");
|
|
@@ -117423,10 +117651,10 @@ async function uploadImageFromSource(eventId, kind, cardId, source) {
|
|
|
117423
117651
|
const uploaded2 = await uploadMasterImage(eventId, kind, cardId, { sourceUrl: source.url });
|
|
117424
117652
|
return { uploaded: uploaded2, sourceBytes: uploaded2.sourceBytes ?? 0, sourceVia: "server", notes: [SERVER_FETCH_NOTE] };
|
|
117425
117653
|
} catch (error3) {
|
|
117426
|
-
const
|
|
117427
|
-
if (!
|
|
117654
|
+
const refusal7 = serverFetchRefusal(error3);
|
|
117655
|
+
if (!refusal7)
|
|
117428
117656
|
throw error3;
|
|
117429
|
-
serverFailure =
|
|
117657
|
+
serverFailure = refusal7;
|
|
117430
117658
|
}
|
|
117431
117659
|
let buffer;
|
|
117432
117660
|
const fetchNotes = [];
|
|
@@ -117520,7 +117748,7 @@ function readCardsConfig(eventData) {
|
|
|
117520
117748
|
// 🔴 The UUID is generated only when it is needed: previously the
|
|
117521
117749
|
// crypto RNG was invoked for every card on every read, and the value
|
|
117522
117750
|
// was discarded (review 02.09, R3c-14)
|
|
117523
|
-
id: hasUsableId ? item.id :
|
|
117751
|
+
id: hasUsableId ? item.id : randomUUID3(),
|
|
117524
117752
|
title: safeString(item.title).trim() || null,
|
|
117525
117753
|
text: safeString(item.text).trim() || null,
|
|
117526
117754
|
buttonLabel: safeString(item.buttonLabel).trim() || null,
|
|
@@ -117530,7 +117758,8 @@ function readCardsConfig(eventData) {
|
|
|
117530
117758
|
imageVariants: readImageVariants(item.imageVariants),
|
|
117531
117759
|
imageWidth: typeof item.imageWidth === "number" ? item.imageWidth : null,
|
|
117532
117760
|
imageHeight: typeof item.imageHeight === "number" ? item.imageHeight : null,
|
|
117533
|
-
formId: safeString(item.formId).trim() || null
|
|
117761
|
+
formId: safeString(item.formId).trim() || null,
|
|
117762
|
+
pollId: safeString(item.pollId).trim() || null
|
|
117534
117763
|
};
|
|
117535
117764
|
});
|
|
117536
117765
|
const ratio = safeString(raw.cardImageRatio);
|
|
@@ -117569,6 +117798,9 @@ function eventFormIds(eventData) {
|
|
|
117569
117798
|
const rawForms = Array.isArray(eventData.forms) ? eventData.forms : [];
|
|
117570
117799
|
return new Set(rawForms.map((form) => form && typeof form === "object" && !Array.isArray(form) ? safeString(form.id).trim() : "").filter(Boolean));
|
|
117571
117800
|
}
|
|
117801
|
+
function eventPollIds(eventData) {
|
|
117802
|
+
return pollIdSet(eventData.polls);
|
|
117803
|
+
}
|
|
117572
117804
|
function cardHasImage(item) {
|
|
117573
117805
|
return Boolean(item.imageMasterUrl || item.imageUrl || item.imageVariants);
|
|
117574
117806
|
}
|
|
@@ -117579,7 +117811,8 @@ var cardItemInputSchema = external_exports.object({
|
|
|
117579
117811
|
text: external_exports.string().max(MAX_CARD_TEXT_LENGTH).nullable().optional(),
|
|
117580
117812
|
buttonLabel: external_exports.string().max(MAX_CARD_BUTTON_LABEL_LENGTH).nullable().optional(),
|
|
117581
117813
|
buttonUrl: external_exports.string().max(MAX_CARD_BUTTON_URL_LENGTH).nullable().optional(),
|
|
117582
|
-
formId: cardIdSchema.nullable().optional()
|
|
117814
|
+
formId: cardIdSchema.nullable().optional(),
|
|
117815
|
+
pollId: cardIdSchema.nullable().optional().describe("Opens this event poll (an id of oko_configure_blocks polls) instead of a link or a form. A button has one action: set at most one of buttonUrl, formId, pollId.")
|
|
117583
117816
|
}).strict();
|
|
117584
117817
|
var imageSourceSchema = external_exports.object({
|
|
117585
117818
|
path: external_exports.string().min(1).optional().describe("Local image path on the MCP client machine (inside OKO_IMAGE_ROOTS)."),
|
|
@@ -117587,13 +117820,14 @@ var imageSourceSchema = external_exports.object({
|
|
|
117587
117820
|
}).strict().refine((source) => Boolean(source.path) !== Boolean(source.url), {
|
|
117588
117821
|
message: "Provide exactly one image source: path or url."
|
|
117589
117822
|
});
|
|
117590
|
-
function buildCardsItems({ inputs, existing, formIds }) {
|
|
117823
|
+
function buildCardsItems({ inputs, existing, formIds, pollIds }) {
|
|
117591
117824
|
const existingById = new Map(existing.map((item) => [item.id, item]));
|
|
117592
117825
|
const seen = /* @__PURE__ */ new Set();
|
|
117593
117826
|
const generatedCardIds = [];
|
|
117594
117827
|
const preservedImageCardIds = [];
|
|
117828
|
+
const droppedBindings = [];
|
|
117595
117829
|
const items = inputs.map((input) => {
|
|
117596
|
-
const id = input.id ??
|
|
117830
|
+
const id = input.id ?? randomUUID3();
|
|
117597
117831
|
if (seen.has(id))
|
|
117598
117832
|
throw new Error(`Duplicate card id "${id}". Each card id must be unique.`);
|
|
117599
117833
|
seen.add(id);
|
|
@@ -117605,9 +117839,20 @@ function buildCardsItems({ inputs, existing, formIds }) {
|
|
|
117605
117839
|
if (formId && !formIds.has(formId)) {
|
|
117606
117840
|
throw new Error(`Form ${formId} not found in event.forms. Create it with oko_configure_blocks forms first, or set formId:null.`);
|
|
117607
117841
|
}
|
|
117842
|
+
const pollId = normalizeNullableText(input.pollId);
|
|
117843
|
+
if (pollId && !pollIds.has(pollId)) {
|
|
117844
|
+
throw new Error(`Poll ${pollId} not found in event.polls. Create it with oko_configure_blocks polls first, or set pollId:null.`);
|
|
117845
|
+
}
|
|
117846
|
+
if (formId && pollId) {
|
|
117847
|
+
throw new Error(`Card ${id} cannot open a form and a poll at once: a button has one action. Keep formId or pollId and set the other to null.`);
|
|
117848
|
+
}
|
|
117608
117849
|
const previous = existingById.get(id);
|
|
117609
117850
|
if (previous && cardHasImage(previous))
|
|
117610
117851
|
preservedImageCardIds.push(id);
|
|
117852
|
+
if (previous?.formId && !formId)
|
|
117853
|
+
droppedBindings.push({ cardId: id, kind: "form", targetId: previous.formId });
|
|
117854
|
+
if (previous?.pollId && !pollId)
|
|
117855
|
+
droppedBindings.push({ cardId: id, kind: "poll", targetId: previous.pollId });
|
|
117611
117856
|
return {
|
|
117612
117857
|
id,
|
|
117613
117858
|
title: normalizeNullableText(input.title),
|
|
@@ -117619,10 +117864,11 @@ function buildCardsItems({ inputs, existing, formIds }) {
|
|
|
117619
117864
|
imageVariants: previous?.imageVariants ?? null,
|
|
117620
117865
|
imageWidth: previous?.imageWidth ?? null,
|
|
117621
117866
|
imageHeight: previous?.imageHeight ?? null,
|
|
117622
|
-
formId
|
|
117867
|
+
formId,
|
|
117868
|
+
pollId
|
|
117623
117869
|
};
|
|
117624
117870
|
});
|
|
117625
|
-
return { items, generatedCardIds, preservedImageCardIds };
|
|
117871
|
+
return { items, generatedCardIds, preservedImageCardIds, droppedBindings };
|
|
117626
117872
|
}
|
|
117627
117873
|
async function deleteStoragePaths(eventId, paths) {
|
|
117628
117874
|
const uniquePaths = [...new Set(paths.filter(Boolean))];
|
|
@@ -117893,6 +118139,8 @@ var connectionStatusOutputSchema = external_exports.object({
|
|
|
117893
118139
|
status: external_exports.enum(OKO_CONNECTION_STATUSES),
|
|
117894
118140
|
errorCode: external_exports.string().nullable(),
|
|
117895
118141
|
message: external_exports.string(),
|
|
118142
|
+
/** Why the connection could not be verified (`unavailable` only): the cause, next to the stable message. */
|
|
118143
|
+
detail: external_exports.string().optional(),
|
|
117896
118144
|
product: external_exports.object({
|
|
117897
118145
|
name: external_exports.string(),
|
|
117898
118146
|
summary: external_exports.string(),
|
|
@@ -118070,6 +118318,15 @@ var walletOutputSchema = external_exports.object({
|
|
|
118070
118318
|
hints: external_exports.array(external_exports.string()),
|
|
118071
118319
|
...cursorMetaSchema
|
|
118072
118320
|
}).passthrough();
|
|
118321
|
+
var workspaceWalletOutputSchema = external_exports.object({
|
|
118322
|
+
workspaceId: external_exports.string(),
|
|
118323
|
+
hasWallet: external_exports.boolean(),
|
|
118324
|
+
available: external_exports.number().int().nullable(),
|
|
118325
|
+
unit: external_exports.literal("lumens"),
|
|
118326
|
+
payerIsCaller: external_exports.boolean(),
|
|
118327
|
+
billingUrl: external_exports.string().url(),
|
|
118328
|
+
hints: external_exports.array(external_exports.string())
|
|
118329
|
+
}).passthrough();
|
|
118073
118330
|
var FORM_FIELD_TYPES = ["text", "tel", "email", "textarea", "select", "checkbox"];
|
|
118074
118331
|
var formFieldSchema = external_exports.object({
|
|
118075
118332
|
id: external_exports.string().min(1).optional(),
|
|
@@ -118098,6 +118355,19 @@ var formSchema = external_exports.object({
|
|
|
118098
118355
|
consentRequired: external_exports.boolean().default(true),
|
|
118099
118356
|
fields: external_exports.array(formFieldSchema).max(30)
|
|
118100
118357
|
}).strict();
|
|
118358
|
+
var pollOptionSchema = external_exports.object({
|
|
118359
|
+
id: external_exports.string().regex(POLL_OPTION_ID_PATTERN, "A letter first, then letters, digits or hyphens, up to 32 characters.").optional().describe("Stable option id. Keep it when editing a poll: the votes of an option are tied to its id, so a renamed option keeps its count. Omit for a NEW option."),
|
|
118360
|
+
text: external_exports.string().min(1).max(POLL_OPTION_TEXT_MAX).describe("The option as viewers read it.")
|
|
118361
|
+
}).strict();
|
|
118362
|
+
var pollSchema = external_exports.object({
|
|
118363
|
+
id: external_exports.string().regex(POLL_ID_PATTERN, "A letter first, then letters, digits or hyphens, up to 64 characters.").optional().describe("Stable poll id; buttons bind to it (pollId). Keep it when editing. Omit for a NEW poll."),
|
|
118364
|
+
question: external_exports.string().min(1).max(POLL_QUESTION_MAX).describe("The question viewers answer."),
|
|
118365
|
+
options: external_exports.array(pollOptionSchema).min(MIN_POLL_OPTIONS).max(MAX_POLL_OPTIONS),
|
|
118366
|
+
multiple: external_exports.boolean().optional().describe("true: viewers may pick several options (checkboxes); false or omitted: exactly one."),
|
|
118367
|
+
title: external_exports.string().max(POLL_TITLE_MAX).nullable().optional().describe("Heading of the popup; null or omitted: the question is the heading."),
|
|
118368
|
+
submitLabel: external_exports.string().max(POLL_LABEL_MAX).nullable().optional().describe("Label of the vote button."),
|
|
118369
|
+
thanksText: external_exports.string().max(POLL_THANKS_MAX).nullable().optional().describe("Shown after the vote while the results are hidden.")
|
|
118370
|
+
}).strict();
|
|
118101
118371
|
var profileDisplayNameSchema = external_exports.string().max(40).nullable();
|
|
118102
118372
|
var timezoneSchema = external_exports.string().min(1).max(100).regex(/^[A-Za-z0-9_+./-]+$/, "Use an IANA timezone such as Europe/Moscow or UTC.").refine((value) => {
|
|
118103
118373
|
try {
|
|
@@ -118171,14 +118441,14 @@ var SCOPE_UNLOCKS = {
|
|
|
118171
118441
|
"setup:write": "image uploads and setup-only changes",
|
|
118172
118442
|
"analytics:read": "analytics, sessions, and reports",
|
|
118173
118443
|
"analytics:write": "starting, ending, and repairing analytics sessions",
|
|
118174
|
-
"live:control": "live block visibility, live badge, viewer refresh",
|
|
118444
|
+
"live:control": "live block visibility, live badge, viewer refresh, poll commands",
|
|
118175
118445
|
"chat:write": "sending organizer chat messages",
|
|
118176
118446
|
"chat:moderate": "chat mode, moderation, bans and mutes",
|
|
118177
118447
|
"telegram:write": "claiming channels and publishing Telegram posts",
|
|
118178
118448
|
"workspace:read": "listing workspaces, members, invites, notifications",
|
|
118179
118449
|
"workspace:write": "workspace management, invites, profile edits",
|
|
118180
118450
|
"leads:read": "reading and exporting leads",
|
|
118181
|
-
"wallet:read": "reading the plan, the lumen balance, grants and ledger of the organizer's own wallet"
|
|
118451
|
+
"wallet:read": "reading the plan, the lumen balance, grants and ledger of the organizer's own wallet, and the balance of the wallet that pays for a workspace"
|
|
118182
118452
|
};
|
|
118183
118453
|
function presetsWithScope(scope) {
|
|
118184
118454
|
return Object.entries(MCP_PRESETS).filter(([, scopes]) => scopes.includes(scope)).map(([name7]) => name7);
|
|
@@ -118422,6 +118692,7 @@ var SCOPE_WORKAROUNDS = {
|
|
|
118422
118692
|
"oko_trash_event|events:delete": { text: "This key cannot delete events. Tell the organizer which event to remove; they can do it in the OKO cabinet, or create a key with the full preset." },
|
|
118423
118693
|
"oko_delete_event_permanently|events:delete": { text: "This key cannot delete events. Tell the organizer which event to remove; they can do it in the OKO cabinet, or create a key with the full preset." },
|
|
118424
118694
|
"oko_wallet|wallet:read": { text: "A key created before wallet:read existed does not carry it, and the scopes of a key cannot be changed: the organizer creates a new key in the OKO cabinet, puts it into OKO_API_KEY of the MCP client and revokes the old one. Until then the balance and the ledger are visible in the cabinet under Billing." },
|
|
118695
|
+
"oko_workspace_wallet|wallet:read": { text: "A key created before wallet:read existed does not carry it, and the scopes of a key cannot be changed: the organizer creates a new key in the OKO cabinet, puts it into OKO_API_KEY of the MCP client and revokes the old one. Until then the balance is visible in the cabinet under Billing." },
|
|
118425
118696
|
"oko_publish_telegram_post|telegram:write": { text: "This key cannot publish to Telegram. The organizer can publish the announcement from the OKO cabinet, or create a key with the full preset." },
|
|
118426
118697
|
// 🔴 The most frequent refusal of this whole release (`ADV-A6-19`): an
|
|
118427
118698
|
// `operator` key calling oko_configure_blocks with channel ids. The advice
|
|
@@ -119239,6 +119510,8 @@ function paidBlocksTurnedOn(patch) {
|
|
|
119239
119510
|
blocks.push("cards");
|
|
119240
119511
|
if (Array.isArray(patch.forms) && patch.forms.length > 0)
|
|
119241
119512
|
blocks.push("forms");
|
|
119513
|
+
if (Array.isArray(patch.polls) && patch.polls.length > 0)
|
|
119514
|
+
blocks.push("polls");
|
|
119242
119515
|
return blocks;
|
|
119243
119516
|
}
|
|
119244
119517
|
function pageSoldItsShow(event) {
|
|
@@ -119403,6 +119676,59 @@ function controlRoomPreview(event) {
|
|
|
119403
119676
|
};
|
|
119404
119677
|
}
|
|
119405
119678
|
|
|
119679
|
+
// build/tools/pollBindings.js
|
|
119680
|
+
var BLOCK_POLL_BINDING_FIELDS = [
|
|
119681
|
+
"blocks.promoBanner.pollId",
|
|
119682
|
+
"blocks.primaryCta.pollId"
|
|
119683
|
+
];
|
|
119684
|
+
var FORM_FIELD_OF = {
|
|
119685
|
+
"blocks.promoBanner.pollId": "blocks.promoBanner.formId",
|
|
119686
|
+
"blocks.primaryCta.pollId": "blocks.primaryCta.formId"
|
|
119687
|
+
};
|
|
119688
|
+
function pollBindingsAfterPatch(fresh, patch) {
|
|
119689
|
+
const bindings = [];
|
|
119690
|
+
for (const field2 of BLOCK_POLL_BINDING_FIELDS) {
|
|
119691
|
+
const value = valueAfterPatch(fresh, patch, field2);
|
|
119692
|
+
if (typeof value === "string" && value)
|
|
119693
|
+
bindings.push({ field: field2, pollId: value });
|
|
119694
|
+
}
|
|
119695
|
+
const cards = valueAfterPatch(fresh, patch, "blocks.cards.items");
|
|
119696
|
+
if (Array.isArray(cards)) {
|
|
119697
|
+
cards.forEach((card, index) => {
|
|
119698
|
+
const pollId = card && typeof card === "object" && !Array.isArray(card) ? card.pollId : null;
|
|
119699
|
+
if (typeof pollId === "string" && pollId) {
|
|
119700
|
+
bindings.push({ field: `blocks.cards.items[${index}].pollId`, pollId });
|
|
119701
|
+
}
|
|
119702
|
+
});
|
|
119703
|
+
}
|
|
119704
|
+
return bindings;
|
|
119705
|
+
}
|
|
119706
|
+
function assertPollBindingsExist(fresh, patch) {
|
|
119707
|
+
const pollsAfter = pollIdSet(valueAfterPatch(fresh, patch, "polls"));
|
|
119708
|
+
for (const field2 of BLOCK_POLL_BINDING_FIELDS) {
|
|
119709
|
+
if (!Object.prototype.hasOwnProperty.call(patch, field2))
|
|
119710
|
+
continue;
|
|
119711
|
+
const value = patch[field2];
|
|
119712
|
+
if (typeof value !== "string" || !value)
|
|
119713
|
+
continue;
|
|
119714
|
+
if (!pollsAfter.has(value)) {
|
|
119715
|
+
throw new Error(`${field2}: poll "${value}" not found in event.polls. Create the poll in the same call (or beforehand) \u2014 a button bound to a missing poll does nothing for viewers.`);
|
|
119716
|
+
}
|
|
119717
|
+
const formField = FORM_FIELD_OF[field2];
|
|
119718
|
+
const form = valueAfterPatch(fresh, patch, formField);
|
|
119719
|
+
if (typeof form === "string" && form) {
|
|
119720
|
+
throw new Error(`${field2}: this button already opens the form "${form}", and a button has one action. Pass ${field2.split(".")[1]}: {formId: null} in the same call to switch it to the poll.`);
|
|
119721
|
+
}
|
|
119722
|
+
}
|
|
119723
|
+
const pollsBefore = pollIdSet(valueAfterPatch(fresh, {}, "polls"));
|
|
119724
|
+
const orphaned = pollBindingsAfterPatch(fresh, patch).filter((binding) => !pollsAfter.has(binding.pollId) && pollsBefore.has(binding.pollId));
|
|
119725
|
+
if (orphaned.length === 0)
|
|
119726
|
+
return;
|
|
119727
|
+
const listed = orphaned.map((binding) => `${binding.field} -> "${binding.pollId}"`).join(", ");
|
|
119728
|
+
const cardsAffected = orphaned.some((binding) => binding.field.startsWith("blocks.cards."));
|
|
119729
|
+
throw new Error(`This call removes a poll that buttons still point at: ${listed}. A button bound to a missing poll does nothing for viewers, so the removal is refused while something still points at it. Unbind them first: pass promoBanner: {pollId: null} and/or primaryCta: {pollId: null} in this same call` + (cardsAffected ? ", and clear the card bindings with oko_configure_cards (pollId: null on those cards) before removing the poll." : "."));
|
|
119730
|
+
}
|
|
119731
|
+
|
|
119406
119732
|
// build/tools/eventCreate.js
|
|
119407
119733
|
async function assertCanCreateInWorkspace(uid, workspaceId) {
|
|
119408
119734
|
if (workspaceId === uid)
|
|
@@ -119480,6 +119806,11 @@ function buildDuplicateEventDocument(input) {
|
|
|
119480
119806
|
meta: withoutSessionPlan(source.meta) ?? { eventDate: null, speakerName: null, description: null },
|
|
119481
119807
|
blocks: source.blocks ?? {},
|
|
119482
119808
|
...Array.isArray(source.forms) ? { forms: source.forms } : {},
|
|
119809
|
+
// Polls travel with the copy like forms do: a button of `blocks` can point at a pollId, and a copy without
|
|
119810
|
+
// `polls` would leave that button bound to a poll the new document does not have. Only the CONFIGURATION is
|
|
119811
|
+
// copied — the votes, the rounds and the state of the show stay with the source, so the copy starts at round 1
|
|
119812
|
+
// with no votes (the web's duplicate does the same, `EventsPageClient.tsx`).
|
|
119813
|
+
...Array.isArray(source.polls) ? { polls: source.polls } : {},
|
|
119483
119814
|
// 🔴 The copy does NOT inherit server-owned Telegram markers.
|
|
119484
119815
|
// `reminderSent` is set by CF17 after sending the reminder, and the same
|
|
119485
119816
|
// function skips events with this flag forever: a copy of an event whose
|
|
@@ -119538,7 +119869,7 @@ function scheduleWindowPassed(sessionMode, eventDateMs, rawDurationMinutes, nowM
|
|
|
119538
119869
|
}
|
|
119539
119870
|
|
|
119540
119871
|
// build/tools/sessionPlan.js
|
|
119541
|
-
function
|
|
119872
|
+
function isRecord2(value) {
|
|
119542
119873
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
119543
119874
|
}
|
|
119544
119875
|
function timestampMillis(value) {
|
|
@@ -119550,7 +119881,7 @@ function timestampMillis(value) {
|
|
|
119550
119881
|
return null;
|
|
119551
119882
|
}
|
|
119552
119883
|
function readSessionPlan(event, durationMsOf) {
|
|
119553
|
-
const meta2 =
|
|
119884
|
+
const meta2 = isRecord2(event.meta) ? event.meta : {};
|
|
119554
119885
|
const startMs = timestampMillis(meta2.sessionStartAt);
|
|
119555
119886
|
const endMs = timestampMillis(meta2.sessionEndAt);
|
|
119556
119887
|
if (startMs !== null && endMs !== null && endMs > startMs)
|
|
@@ -119568,7 +119899,7 @@ function sessionPlanWarnings(event) {
|
|
|
119568
119899
|
const plan = sessionPlanOf(event);
|
|
119569
119900
|
if (!plan || plan.source !== "meta")
|
|
119570
119901
|
return [];
|
|
119571
|
-
const meta2 =
|
|
119902
|
+
const meta2 = isRecord2(event.meta) ? event.meta : {};
|
|
119572
119903
|
const eventDateMs = timestampMillis(meta2.eventDate);
|
|
119573
119904
|
if (eventDateMs === null)
|
|
119574
119905
|
return [];
|
|
@@ -119772,11 +120103,40 @@ function validateEventSetup(eventId, event, nowMs = Date.now()) {
|
|
|
119772
120103
|
if (id && !formIds.has(id))
|
|
119773
120104
|
add(errors, "form_binding_missing", path, `Referenced formId "${id}" does not exist in event.forms.`);
|
|
119774
120105
|
};
|
|
120106
|
+
const rawPolls = Array.isArray(event.polls) ? event.polls : [];
|
|
120107
|
+
const servedPollIds = new Set(parseEventPolls(event.polls).map((poll) => poll.id));
|
|
120108
|
+
const seenPollIds = /* @__PURE__ */ new Set();
|
|
120109
|
+
rawPolls.slice(0, MAX_POLLS).forEach((raw, index) => {
|
|
120110
|
+
const id = safeString(asRecord(raw).id).trim();
|
|
120111
|
+
if (id && seenPollIds.has(id)) {
|
|
120112
|
+
add(errors, "poll_id_duplicate", `polls[${index}].id`, `Duplicate poll id: ${id}. OKO serves the first poll with an id and drops the rest.`);
|
|
120113
|
+
} else if (parsePoll(raw) === null) {
|
|
120114
|
+
add(errors, "poll_invalid", `polls[${index}]`, "This poll is not complete, so viewers cannot open it: a poll needs its own id, a question and 2 to 10 options, each with an id and a text. Complete it or remove it with oko_configure_blocks polls.");
|
|
120115
|
+
}
|
|
120116
|
+
if (id)
|
|
120117
|
+
seenPollIds.add(id);
|
|
120118
|
+
});
|
|
120119
|
+
if (rawPolls.length > MAX_POLLS) {
|
|
120120
|
+
add(warnings, "polls_too_many", "polls", `The page lists ${rawPolls.length} polls; OKO serves the first ${MAX_POLLS} and ignores the rest.`);
|
|
120121
|
+
}
|
|
120122
|
+
const checkPollBinding = (path, pollId) => {
|
|
120123
|
+
const id = safeString(pollId).trim();
|
|
120124
|
+
if (id && !servedPollIds.has(id)) {
|
|
120125
|
+
add(errors, "poll_binding_missing", path, `Referenced pollId "${id}" is not a poll OKO serves: it does not exist in event.polls, or it is not complete.`);
|
|
120126
|
+
}
|
|
120127
|
+
};
|
|
120128
|
+
const checkTargetsConflict = (path, formId, pollId) => {
|
|
120129
|
+
if (safeString(formId).trim() && safeString(pollId).trim()) {
|
|
120130
|
+
add(warnings, "poll_binding_shadowed", path, "The button is bound to a form AND a poll; the form wins, so the poll never opens. Keep one: set the other to null.");
|
|
120131
|
+
}
|
|
120132
|
+
};
|
|
119775
120133
|
const promo = asRecord(blocks.promoBanner);
|
|
119776
120134
|
if (promo.enabled === true) {
|
|
119777
120135
|
checkFormBinding("blocks.promoBanner.formId", promo.formId);
|
|
119778
|
-
|
|
119779
|
-
|
|
120136
|
+
checkPollBinding("blocks.promoBanner.pollId", promo.pollId);
|
|
120137
|
+
checkTargetsConflict("blocks.promoBanner", promo.formId, promo.pollId);
|
|
120138
|
+
if (!safeString(promo.link).trim() && !safeString(promo.formId).trim() && !safeString(promo.pollId).trim()) {
|
|
120139
|
+
add(suggestions, "promo_target_missing", "blocks.promoBanner", "Enabled promo banner has no link, form or poll binding.");
|
|
119780
120140
|
}
|
|
119781
120141
|
if (!safeString(promo.imageUrl).trim()) {
|
|
119782
120142
|
add(warnings, "promo_image_missing", "blocks.promoBanner.imageUrl", "Enabled promo banner has no image.");
|
|
@@ -119789,14 +120149,16 @@ function validateEventSetup(eventId, event, nowMs = Date.now()) {
|
|
|
119789
120149
|
const cta = asRecord(blocks.primaryCta);
|
|
119790
120150
|
if (cta.enabled === true) {
|
|
119791
120151
|
checkFormBinding("blocks.primaryCta.formId", cta.formId);
|
|
120152
|
+
checkPollBinding("blocks.primaryCta.pollId", cta.pollId);
|
|
120153
|
+
checkTargetsConflict("blocks.primaryCta", cta.formId, cta.pollId);
|
|
119792
120154
|
if (!safeString(cta.title).trim() && !safeString(cta.text).trim()) {
|
|
119793
120155
|
add(suggestions, "cta_copy_missing", "blocks.primaryCta", "Enabled CTA has no title or text.");
|
|
119794
120156
|
}
|
|
119795
120157
|
if (!safeString(cta.buttonLabel).trim()) {
|
|
119796
120158
|
add(warnings, "cta_button_missing", "blocks.primaryCta.buttonLabel", "Enabled CTA has no button label.");
|
|
119797
120159
|
}
|
|
119798
|
-
if (safeString(cta.buttonLabel).trim() && !safeString(cta.buttonUrl).trim() && !safeString(cta.formId).trim()) {
|
|
119799
|
-
add(warnings, "cta_target_missing", "blocks.primaryCta", "CTA button has no URL or
|
|
120160
|
+
if (safeString(cta.buttonLabel).trim() && !safeString(cta.buttonUrl).trim() && !safeString(cta.formId).trim() && !safeString(cta.pollId).trim()) {
|
|
120161
|
+
add(warnings, "cta_target_missing", "blocks.primaryCta", "CTA button has no URL, form or poll binding.");
|
|
119800
120162
|
}
|
|
119801
120163
|
}
|
|
119802
120164
|
const videoPlaceholder = asRecord(asRecord(blocks.video).placeholder);
|
|
@@ -119825,10 +120187,13 @@ function validateEventSetup(eventId, event, nowMs = Date.now()) {
|
|
|
119825
120187
|
cardIds.add(card.id);
|
|
119826
120188
|
if (!card.title && !card.text)
|
|
119827
120189
|
add(suggestions, "card_copy_missing", path, "Card has no title or text.");
|
|
119828
|
-
if (card.buttonLabel && !card.buttonUrl && !card.formId)
|
|
119829
|
-
add(warnings, "card_target_missing", path, "Card button has no URL or
|
|
120190
|
+
if (card.buttonLabel && !card.buttonUrl && !card.formId && !card.pollId)
|
|
120191
|
+
add(warnings, "card_target_missing", path, "Card button has no URL, form or poll binding.");
|
|
119830
120192
|
if (card.formId && !formIds.has(card.formId))
|
|
119831
120193
|
add(errors, "card_form_missing", `${path}.formId`, `Referenced formId "${card.formId}" does not exist.`);
|
|
120194
|
+
if (card.pollId && !servedPollIds.has(card.pollId))
|
|
120195
|
+
add(errors, "card_poll_missing", `${path}.pollId`, `Referenced pollId "${card.pollId}" is not a poll OKO serves: it does not exist in event.polls, or it is not complete.`);
|
|
120196
|
+
checkTargetsConflict(path, card.formId, card.pollId);
|
|
119832
120197
|
if (card.imageMasterUrl && !card.imageVariants)
|
|
119833
120198
|
add(warnings, "card_variants_pending", `${path}.imageVariants`, "Card image variants are not ready yet.");
|
|
119834
120199
|
});
|
|
@@ -119878,20 +120243,20 @@ function stateWithin(now, window2) {
|
|
|
119878
120243
|
return "open";
|
|
119879
120244
|
return "after";
|
|
119880
120245
|
}
|
|
119881
|
-
function
|
|
120246
|
+
function isRecord3(value) {
|
|
119882
120247
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
119883
120248
|
}
|
|
119884
120249
|
function toMillis2(value) {
|
|
119885
120250
|
if (typeof value === "number" && Number.isFinite(value))
|
|
119886
120251
|
return value;
|
|
119887
|
-
if (
|
|
120252
|
+
if (isRecord3(value) && typeof value.toMillis === "function") {
|
|
119888
120253
|
const ms = value.toMillis();
|
|
119889
120254
|
return typeof ms === "number" && Number.isFinite(ms) ? ms : null;
|
|
119890
120255
|
}
|
|
119891
120256
|
return null;
|
|
119892
120257
|
}
|
|
119893
120258
|
function storedWindow(event) {
|
|
119894
|
-
const billing =
|
|
120259
|
+
const billing = isRecord3(event.billing) ? event.billing : {};
|
|
119895
120260
|
const start = toMillis2(billing.windowStart);
|
|
119896
120261
|
const end = toMillis2(billing.windowEnd);
|
|
119897
120262
|
return start !== null && end !== null && end > start ? { windowStart: new Date(start), windowEnd: new Date(end) } : null;
|
|
@@ -119900,12 +120265,12 @@ function instantMs(value) {
|
|
|
119900
120265
|
return toMillis2(value);
|
|
119901
120266
|
}
|
|
119902
120267
|
function windowFromEvent(event, nowMs) {
|
|
119903
|
-
const billing =
|
|
120268
|
+
const billing = isRecord3(event.billing) ? event.billing : {};
|
|
119904
120269
|
const storedStart = toMillis2(billing.windowStart);
|
|
119905
120270
|
const storedEnd = toMillis2(billing.windowEnd);
|
|
119906
120271
|
let window2 = storedStart !== null && storedEnd !== null ? { windowStart: new Date(storedStart), windowEnd: new Date(storedEnd) } : null;
|
|
119907
120272
|
if (!window2) {
|
|
119908
|
-
const meta2 =
|
|
120273
|
+
const meta2 = isRecord3(event.meta) ? event.meta : {};
|
|
119909
120274
|
const eventDateMs = toMillis2(meta2.eventDate);
|
|
119910
120275
|
if (eventDateMs !== null)
|
|
119911
120276
|
window2 = showWindow(new Date(eventDateMs));
|
|
@@ -119914,18 +120279,18 @@ function windowFromEvent(event, nowMs) {
|
|
|
119914
120279
|
}
|
|
119915
120280
|
|
|
119916
120281
|
// build/tools/eventDate.js
|
|
119917
|
-
function
|
|
120282
|
+
function isRecord4(value) {
|
|
119918
120283
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
119919
120284
|
}
|
|
119920
120285
|
function readMeta(event) {
|
|
119921
|
-
return
|
|
120286
|
+
return isRecord4(event.meta) ? event.meta : {};
|
|
119922
120287
|
}
|
|
119923
120288
|
function judgeEventDateChange(fresh, newValue, nowMs) {
|
|
119924
120289
|
const newMs = newValue === null ? null : instantMs(newValue);
|
|
119925
120290
|
const currentMs = instantMs(readMeta(fresh).eventDate);
|
|
119926
120291
|
if (newMs !== null && currentMs !== null && newMs === currentMs)
|
|
119927
120292
|
return { allowed: true };
|
|
119928
|
-
const billing =
|
|
120293
|
+
const billing = isRecord4(fresh.billing) ? fresh.billing : null;
|
|
119929
120294
|
if (!billing || billing.state !== "published" && billing.state !== "held")
|
|
119930
120295
|
return { allowed: true };
|
|
119931
120296
|
const window2 = storedWindow(fresh);
|
|
@@ -120103,6 +120468,10 @@ function assertFormBindingsExist(fresh, patch) {
|
|
|
120103
120468
|
const cardsAffected = orphaned.some((binding) => binding.field.startsWith("blocks.cards."));
|
|
120104
120469
|
throw new Error(`This call removes a form that buttons still point at: ${listed}. A button bound to a missing form does nothing for viewers, so the removal is refused while something still points at it. Unbind them first: pass promoBanner: {formId: null} and/or primaryCta: {formId: null} in this same call` + (cardsAffected ? ", and clear the card bindings with oko_configure_cards before removing the form." : "."));
|
|
120105
120470
|
}
|
|
120471
|
+
function assertBindingsExist(fresh, patch) {
|
|
120472
|
+
assertFormBindingsExist(fresh, patch);
|
|
120473
|
+
assertPollBindingsExist(fresh, patch);
|
|
120474
|
+
}
|
|
120106
120475
|
function configureBlocksScopes(args) {
|
|
120107
120476
|
return Object.keys(telegramBlocksPatch(args)).length > 0 ? ["setup:write", "telegram:write"] : ["setup:write"];
|
|
120108
120477
|
}
|
|
@@ -120404,26 +120773,30 @@ function registerEventTools(server2) {
|
|
|
120404
120773
|
server2.registerTool("oko_configure_blocks", {
|
|
120405
120774
|
title: "Configure event blocks",
|
|
120406
120775
|
description: toolDescription({
|
|
120407
|
-
summary: "Configure supported event setup blocks, forms, and Telegram channel ids.",
|
|
120776
|
+
summary: "Configure supported event setup blocks, forms, polls, and Telegram channel ids.",
|
|
120408
120777
|
exampleCall: { "eventId": "Kx9pQ2mR7tL4vB8nW3cY", "chat": { "enabled": true }, "promoBanner": { "enabled": false }, "primaryCta": { "enabled": true, "title": "Get the slides", "buttonLabel": "Download", "buttonUrl": "https://example.com/slides" }, "videoPlaceholder": { "enabled": true, "mode": "glass", "showTimer": true, "title": "The webinar starts at 14:00", "subtitle": "Moscow time" } },
|
|
120409
120778
|
args: [
|
|
120410
120779
|
"eventId: event to edit.",
|
|
120411
120780
|
"chat/promoBanner/primaryCta/videoPlaceholder: optional block patches.",
|
|
120412
|
-
"forms: replaces configured lead forms when provided; removing a form that a button still points at is refused.",
|
|
120781
|
+
"forms: replaces configured lead forms when provided; removing a form that a button still points at is refused. Omit to leave the forms (and the polls) untouched.",
|
|
120782
|
+
"polls: replaces configured polls when provided (max 10, each with a question and 2-10 options); omit to leave the polls (and the forms) untouched. Pass every existing poll and option back with its id (oko_get_event shows them) \u2014 an option sent without an id is a NEW option, and the votes of an option are tied to its id. Removing a poll that a button still points at is refused.",
|
|
120783
|
+
"promoBanner.pollId / primaryCta.pollId: the button opens this poll in a popup (a card's pollId is set in oko_configure_cards). A button has one action: link, form or poll. null removes the binding.",
|
|
120413
120784
|
"telegramChannelIds: replaces configured Telegram channels when provided; needs the telegram:write scope AND the notification permission on the event, and is refused before anything is written when either is missing.",
|
|
120414
|
-
"confirm: only for a call that turns on a paid block (promoBanner, primaryCta, forms) on a PUBLISHED page \u2014 see the note on paid blocks."
|
|
120785
|
+
"confirm: only for a call that turns on a paid block (promoBanner, primaryCta, forms, polls) on a PUBLISHED page \u2014 see the note on paid blocks."
|
|
120415
120786
|
],
|
|
120416
|
-
returns: "event id and the list of Firestore fields updated; after a paid block, also payment (charged, balanceAfter) and a hint.",
|
|
120787
|
+
returns: "event id and the list of Firestore fields updated; after polls, the polls as stored (their ids, and the ids of their options); after a paid block, also payment (charged, balanceAfter) and a hint.",
|
|
120417
120788
|
examples: ["Use after oko_create_webinar to enable chat, bind a CTA/form, and configure other audience blocks."],
|
|
120418
120789
|
notes: [
|
|
120419
|
-
"\u{1F534} Paid blocks after publication: the promo banner, the CTA and the
|
|
120790
|
+
"\u{1F534} Paid blocks after publication: the promo banner, the CTA, the forms and the polls cost 10 lumens each (the cards block is paid in oko_configure_cards; forms are ONE block however many there are, and so are polls \u2014 the polls are their own block, never part of the forms) when they are turned on AFTER the page is published. On a draft they are free \u2014 they enter the bill at publication. On a published page, a call that turns one on without confirm writes NOTHING and answers with the quote: the blocks, the lumens, the wallet that pays and who that is. Show the organizer, and repeat the call with confirm: true after they agree; the wallet is charged once for the whole set and only then is the patch written. Turning a block off and on again in the same show costs nothing more, and a block already paid for is not charged again. Refusals carry errorCode and whatToDo: INSUFFICIENT_TOKENS (the wallet that pays \u2014 the workspace owner's \u2014 is short), ENTITLEMENT_REQUIRED (that owner has no live subscription), PAYER_UNRESOLVED, EDIT_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH; nothing is written or charged then. The key needs the events:write scope besides setup:write for this. Unpublishing the page later does NOT return these top-up charges.",
|
|
120420
120791
|
'\u{1F534} "Turn off the chat" is ambiguous, and the two meanings are different actions. chat.enabled=false HIDES the chat panel from the page for everyone \u2014 the history is not deleted (messages are kept for 90 days) and comes back when the block is enabled again \u2014 but during or after a webinar that is almost never what is asked for. Stopping new messages is a different tool: oko_set_chat_mode with mode="off" leaves the panel and the history on the page and only closes the input. When the organizer says "turn off the chat", "close the chat", "stop the chat", "mute the chat", "disable chat", assume they mean CLOSING INTAKE (oko_set_chat_mode off), and ask before hiding the panel. The same trap in reverse: "turn the chat back on" after a pause means mode="public", not this field.',
|
|
120421
120792
|
"A block toggle and its image are independent: enabling a block here works with an image already uploaded via oko_upload_image, and uploading an image never enables the block by itself.",
|
|
120422
120793
|
"promoBanner has no text field by design: the banner is image+link. blocks.promoBanner.text seen in event documents is legacy and is never rendered on the event page.",
|
|
120423
120794
|
'videoPlaceholder.enabled only allows the placeholder; viewers see it after oko_set_block_visibility with block "placeholder" and visible true (a live override, stored outside the event document). Chat, CTA, banner and cards appear as soon as they are enabled here.',
|
|
120424
120795
|
"Setup toggle vs live override: enabled=false removes a block from the page for everyone and persists (its uploaded image stays); oko_set_block_visibility hides or shows an enabled block temporarily and is the tool for the live show. Uploading an image never enables a block.",
|
|
120425
120796
|
"telegramChannelIds is the only field here that is not a page setting: it decides which channel carries this event's messages, and the automatic reminder goes out under the event owner's name. Omit the field to configure blocks with setup:write alone; pass it only with a key that holds telegram:write and for an organizer who may send notifications for this event.",
|
|
120426
|
-
"forms and the buttons bound to them travel together in both directions: binding a button to a form that does not exist is refused, and so is replacing forms in a way that removes a form the banner, the CTA or a card button still points at. Unbind in the same call with promoBanner/primaryCta formId:null; card bindings are cleared with oko_configure_cards first. A binding that was already broken before the call is not touched and does not block it."
|
|
120797
|
+
"forms and the buttons bound to them travel together in both directions: binding a button to a form that does not exist is refused, and so is replacing forms in a way that removes a form the banner, the CTA or a card button still points at. Unbind in the same call with promoBanner/primaryCta formId:null; card bindings are cleared with oko_configure_cards first. A binding that was already broken before the call is not touched and does not block it. Polls follow the same rule with pollId.",
|
|
120798
|
+
"forms and polls are separate lists: a call with forms never touches polls, and a call with polls never touches forms (leave either argument out to keep that list as it is).",
|
|
120799
|
+
"Polls: a poll opens from a button (promoBanner.pollId, primaryCta.pollId, a card's pollId) and, with the control room, can be launched for everyone from oko_control_poll. Viewers see the results only after the organizer shows them (oko_control_poll show_results). Removing a poll takes it off the page; the votes already cast stay in the data and in the session reports. The vote counts are in oko_get_live_state (live) and oko_get_session_report (per session)."
|
|
120427
120800
|
]
|
|
120428
120801
|
}),
|
|
120429
120802
|
inputSchema: {
|
|
@@ -120435,12 +120808,13 @@ function registerEventTools(server2) {
|
|
|
120435
120808
|
closeOnSessionEnd: external_exports.boolean().optional().describe("Session trigger: close message intake when the analytics session ends (default false)."),
|
|
120436
120809
|
visibility: external_exports.enum(["always", "hidden"]).optional()
|
|
120437
120810
|
}).optional(),
|
|
120438
|
-
promoBanner: external_exports.object({ enabled: external_exports.boolean().optional(), link: external_exports.string().max(1e3).optional(), visibility: external_exports.enum(["always", "hidden"]).optional(), formId: external_exports.string().nullable().optional() }).optional(),
|
|
120439
|
-
primaryCta: external_exports.object({ enabled: external_exports.boolean().optional(), visibility: external_exports.enum(["always", "hidden"]).optional(), title: external_exports.string().nullable().optional(), text: external_exports.string().nullable().optional(), buttonLabel: external_exports.string().nullable().optional(), buttonUrl: external_exports.string().nullable().optional(), formId: external_exports.string().nullable().optional() }).optional(),
|
|
120811
|
+
promoBanner: external_exports.object({ enabled: external_exports.boolean().optional(), link: external_exports.string().max(1e3).optional(), visibility: external_exports.enum(["always", "hidden"]).optional(), formId: external_exports.string().nullable().optional(), pollId: external_exports.string().regex(POLL_ID_PATTERN).nullable().optional().describe("Opens this poll (an id from polls) when the banner is clicked; null removes the binding.") }).optional(),
|
|
120812
|
+
primaryCta: external_exports.object({ enabled: external_exports.boolean().optional(), visibility: external_exports.enum(["always", "hidden"]).optional(), title: external_exports.string().nullable().optional(), text: external_exports.string().nullable().optional(), buttonLabel: external_exports.string().nullable().optional(), buttonUrl: external_exports.string().nullable().optional(), formId: external_exports.string().nullable().optional(), pollId: external_exports.string().regex(POLL_ID_PATTERN).nullable().optional().describe("Opens this poll (an id from polls) when the CTA button is clicked; null removes the binding.") }).optional(),
|
|
120440
120813
|
videoPlaceholder: external_exports.object({ enabled: external_exports.boolean().optional(), mode: external_exports.enum(["glass", "image"]).optional(), title: external_exports.string().nullable().optional(), subtitle: external_exports.string().nullable().optional(), showTimer: external_exports.boolean().optional() }).optional(),
|
|
120441
120814
|
forms: external_exports.array(formSchema).max(20).optional(),
|
|
120815
|
+
polls: external_exports.array(pollSchema).max(MAX_POLLS).optional().describe("Replaces the page's polls. Omit to leave them as they are; [] removes them all."),
|
|
120442
120816
|
telegramChannelIds: external_exports.array(external_exports.number().int()).max(20).optional(),
|
|
120443
|
-
confirm: external_exports.boolean().optional().describe("Needed only when the call turns on a paid block (banner, CTA, forms) on an already PUBLISHED page: without it nothing is written and the answer is the quote (lumens, who pays); true after the organizer confirmed the charge.")
|
|
120817
|
+
confirm: external_exports.boolean().optional().describe("Needed only when the call turns on a paid block (banner, CTA, forms, polls) on an already PUBLISHED page: without it nothing is written and the answer is the quote (lumens, who pays); true after the organizer confirmed the charge.")
|
|
120444
120818
|
},
|
|
120445
120819
|
outputSchema: updateOutputSchema,
|
|
120446
120820
|
annotations: writeTool
|
|
@@ -120470,6 +120844,8 @@ function registerEventTools(server2) {
|
|
|
120470
120844
|
patch["blocks.promoBanner.visibility"] = args.promoBanner.visibility;
|
|
120471
120845
|
if (args.promoBanner?.formId !== void 0)
|
|
120472
120846
|
patch["blocks.promoBanner.formId"] = args.promoBanner.formId;
|
|
120847
|
+
if (args.promoBanner?.pollId !== void 0)
|
|
120848
|
+
patch["blocks.promoBanner.pollId"] = args.promoBanner.pollId;
|
|
120473
120849
|
if (args.primaryCta?.enabled !== void 0)
|
|
120474
120850
|
patch["blocks.primaryCta.enabled"] = args.primaryCta.enabled;
|
|
120475
120851
|
if (args.primaryCta?.visibility !== void 0)
|
|
@@ -120484,6 +120860,8 @@ function registerEventTools(server2) {
|
|
|
120484
120860
|
patch["blocks.primaryCta.buttonUrl"] = normalizeCardButtonUrl(args.primaryCta.buttonUrl);
|
|
120485
120861
|
if (args.primaryCta?.formId !== void 0)
|
|
120486
120862
|
patch["blocks.primaryCta.formId"] = args.primaryCta.formId;
|
|
120863
|
+
if (args.primaryCta?.pollId !== void 0)
|
|
120864
|
+
patch["blocks.primaryCta.pollId"] = args.primaryCta.pollId;
|
|
120487
120865
|
if (args.videoPlaceholder?.enabled !== void 0)
|
|
120488
120866
|
patch["blocks.video.placeholder.enabled"] = args.videoPlaceholder.enabled;
|
|
120489
120867
|
if (args.videoPlaceholder?.mode !== void 0)
|
|
@@ -120516,6 +120894,8 @@ function registerEventTools(server2) {
|
|
|
120516
120894
|
...field2.type === "select" || field2.type === "checkbox" ? { options: [...new Set((field2.options ?? []).map((option) => option.trim()).filter(Boolean))] } : {}
|
|
120517
120895
|
}))
|
|
120518
120896
|
}));
|
|
120897
|
+
if (args.polls !== void 0)
|
|
120898
|
+
patch.polls = buildPollsForWrite(args.polls);
|
|
120519
120899
|
Object.assign(patch, telegramPatch);
|
|
120520
120900
|
const payment = await settleBlockExtras({
|
|
120521
120901
|
tool: "oko_configure_blocks",
|
|
@@ -120524,11 +120904,11 @@ function registerEventTools(server2) {
|
|
|
120524
120904
|
event: current,
|
|
120525
120905
|
patch,
|
|
120526
120906
|
confirm: args.confirm,
|
|
120527
|
-
precheck: () =>
|
|
120907
|
+
precheck: () => assertBindingsExist(current, patch)
|
|
120528
120908
|
});
|
|
120529
120909
|
if (payment.kind === "quote")
|
|
120530
120910
|
requireConfirm(args.confirm, "oko_configure_blocks", payment.preview);
|
|
120531
|
-
await writeEventPatchWithCards(args.eventId, patch, null,
|
|
120911
|
+
await writeEventPatchWithCards(args.eventId, patch, null, assertBindingsExist);
|
|
120532
120912
|
const paid = paymentFields(payment);
|
|
120533
120913
|
const hints = [...paid.hints ?? []];
|
|
120534
120914
|
if (args.chat?.visibility !== void 0) {
|
|
@@ -120557,7 +120937,20 @@ function registerEventTools(server2) {
|
|
|
120557
120937
|
if (args.videoPlaceholder?.enabled === false) {
|
|
120558
120938
|
hints.push('If a live override made the placeholder visible earlier, reset it with oko_set_block_visibility block "placeholder" visible null.');
|
|
120559
120939
|
}
|
|
120560
|
-
|
|
120940
|
+
const writtenPolls = Array.isArray(patch.polls) ? patch.polls : null;
|
|
120941
|
+
if (writtenPolls && writtenPolls.length > 0) {
|
|
120942
|
+
hints.push("A poll reaches viewers through a button that opens it: bind one with promoBanner.pollId, primaryCta.pollId or a card's pollId (oko_configure_cards). Viewers vote in a popup and see the results only after the organizer shows them (oko_control_poll show_results). Launching a poll for everyone and resetting it need the control room.");
|
|
120943
|
+
}
|
|
120944
|
+
if (writtenPolls && writtenPolls.length === 0) {
|
|
120945
|
+
hints.push("The polls were taken off the page. Votes already cast stay in the data and in the session reports.");
|
|
120946
|
+
}
|
|
120947
|
+
return textResult({
|
|
120948
|
+
id: args.eventId,
|
|
120949
|
+
updated: Object.keys(patch),
|
|
120950
|
+
...writtenPolls ? { polls: writtenPolls } : {},
|
|
120951
|
+
...paid.payment ? { payment: paid.payment } : {},
|
|
120952
|
+
...hints.length ? { hints } : {}
|
|
120953
|
+
});
|
|
120561
120954
|
});
|
|
120562
120955
|
server2.registerTool("oko_configure_cards", {
|
|
120563
120956
|
title: "Configure event cards",
|
|
@@ -120569,18 +120962,19 @@ function registerEventTools(server2) {
|
|
|
120569
120962
|
"enabled: optional Cards block enabled flag; turning it on for a PUBLISHED page is a paid top-up \u2014 see the note on paid blocks.",
|
|
120570
120963
|
"visibility: optional default visibility, always or hidden.",
|
|
120571
120964
|
"cardImageRatio: optional image ratio, one of auto, 1:1, 4:3, 3:2, 16:9, 2:3, 3:4.",
|
|
120572
|
-
"items: optional full replacement array, max 6 cards. Each item may include id, title, text, buttonLabel, buttonUrl, formId.",
|
|
120965
|
+
"items: optional full replacement array, max 6 cards. Each item may include id, title, text, buttonLabel, buttonUrl, formId, pollId.",
|
|
120573
120966
|
"confirm: only for enabled: true on a published page \u2014 see the note on paid blocks."
|
|
120574
120967
|
],
|
|
120575
120968
|
returns: "event id, updated Firestore fields, normalized cards config, generated card ids, preserved image ids, removed card ids, and warnings; after a paid top-up, also payment (charged, balanceAfter) and a hint.",
|
|
120576
120969
|
examples: [
|
|
120577
120970
|
"Use after oko_create_webinar to create cards before uploading per-card images.",
|
|
120578
120971
|
"Call oko_get_event first and pass the full desired items array when editing existing cards.",
|
|
120579
|
-
"Use formId instead of buttonUrl when a card button should open a lead form."
|
|
120972
|
+
"Use formId instead of buttonUrl when a card button should open a lead form, and pollId when it should open a poll (an id from oko_configure_blocks polls)."
|
|
120580
120973
|
],
|
|
120581
120974
|
notes: [
|
|
120582
120975
|
"\u{1F534} Paid block after publication: the Cards block costs 10 lumens (one block however many cards) when it is turned on AFTER the page is published; on a draft it is free \u2014 it enters the bill at publication. On a published page, enabled: true without confirm writes NOTHING (not the card list either) and answers with the quote: the lumens and the wallet that pays. Show the organizer, and repeat the call with confirm: true after they agree; the wallet is charged once and only then is the patch written. A block already paid for, or turned off and on again in the same show, is not charged twice. Refusals carry errorCode and whatToDo: INSUFFICIENT_TOKENS, ENTITLEMENT_REQUIRED, PAYER_UNRESOLVED, EDIT_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH; nothing is written or charged then. The key needs the events:write scope besides setup:write for this. Unpublishing the page later does NOT return top-up charges.",
|
|
120583
120976
|
"This tool replaces blocks.cards.items when items is provided; omitted text fields become null.",
|
|
120977
|
+
"A card button has ONE action: buttonUrl, formId or pollId. The bindings travel with the cards you send: pass formId and pollId back as oko_get_event shows them, or the card loses its binding. A binding the replacement drops is named in warnings (the write still goes through); a pollId that is not in the page's polls is refused.",
|
|
120584
120978
|
"Existing card images are preserved only when the same card id remains in the replacement list.",
|
|
120585
120979
|
"buttonUrl allows http(s), mailto, or tel URLs; javascript/data URLs are rejected.",
|
|
120586
120980
|
"If a removed card had an image, Storage cleanup is not automatic; this matches the web Setup behavior."
|
|
@@ -120612,6 +121006,7 @@ function registerEventTools(server2) {
|
|
|
120612
121006
|
const preservedImageCardIds = [];
|
|
120613
121007
|
let removedCardIds = [];
|
|
120614
121008
|
let removedCardsWithImages = [];
|
|
121009
|
+
let droppedBindings = [];
|
|
120615
121010
|
if (args.enabled !== void 0)
|
|
120616
121011
|
patch["blocks.cards.enabled"] = args.enabled;
|
|
120617
121012
|
if (args.visibility !== void 0)
|
|
@@ -120623,13 +121018,15 @@ function registerEventTools(server2) {
|
|
|
120623
121018
|
const built = buildCardsItems({
|
|
120624
121019
|
inputs: args.items,
|
|
120625
121020
|
existing: freshCards.items,
|
|
120626
|
-
formIds: eventFormIds(fresh)
|
|
121021
|
+
formIds: eventFormIds(fresh),
|
|
121022
|
+
pollIds: eventPollIds(fresh)
|
|
120627
121023
|
});
|
|
120628
121024
|
finalCards.items = built.items;
|
|
120629
121025
|
generatedCardIds.length = 0;
|
|
120630
121026
|
preservedImageCardIds.length = 0;
|
|
120631
121027
|
generatedCardIds.push(...built.generatedCardIds);
|
|
120632
121028
|
preservedImageCardIds.push(...built.preservedImageCardIds);
|
|
121029
|
+
droppedBindings = built.droppedBindings;
|
|
120633
121030
|
const nextIds = new Set(built.items.map((item) => item.id));
|
|
120634
121031
|
const removed = freshCards.items.filter((item) => !nextIds.has(item.id));
|
|
120635
121032
|
removedCardIds = removed.map((item) => item.id);
|
|
@@ -120654,6 +121051,9 @@ function registerEventTools(server2) {
|
|
|
120654
121051
|
}
|
|
120655
121052
|
const paid = paymentFields(payment);
|
|
120656
121053
|
const warnings = removedCardsWithImages.length > 0 ? [`Removed cards had images and their Storage files were not deleted: ${removedCardsWithImages.join(", ")}.`] : [];
|
|
121054
|
+
for (const dropped of droppedBindings) {
|
|
121055
|
+
warnings.push(`Card ${dropped.cardId} no longer opens the ${dropped.kind} "${dropped.targetId}": the replacement item did not carry ${dropped.kind === "poll" ? "pollId" : "formId"}. If that was not intended, repeat the call with ${dropped.kind === "poll" ? "pollId" : "formId"}: "${dropped.targetId}" on that card.`);
|
|
121056
|
+
}
|
|
120657
121057
|
if (finalCards.repairedIdPositions.length > 0) {
|
|
120658
121058
|
warnings.push(`Cards at position(s) ${finalCards.repairedIdPositions.join(", ")} had no usable id in Firestore; a temporary one was generated, so their previous images could not be matched and were dropped. Re-upload images for these cards.`);
|
|
120659
121059
|
if (args.items === void 0) {
|
|
@@ -120969,22 +121369,12 @@ function registerLeadTools(server2) {
|
|
|
120969
121369
|
// build/tools/unpublication.js
|
|
120970
121370
|
var TOOL2 = "oko_unpublish_event";
|
|
120971
121371
|
var NOTHING_WILL_CHANGE = "The page and its lumens stay exactly as they are now.";
|
|
120972
|
-
function
|
|
121372
|
+
function isRecord5(value) {
|
|
120973
121373
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
120974
121374
|
}
|
|
120975
121375
|
function refusal4(eventId, body) {
|
|
120976
121376
|
return { ...textResult({ tool: TOOL2, eventId, ...body }), isError: true };
|
|
120977
121377
|
}
|
|
120978
|
-
function notPublishedRefusal(eventId) {
|
|
120979
|
-
return refusal4(eventId, {
|
|
120980
|
-
errorCode: "NOT_PUBLISHED",
|
|
120981
|
-
message: `This page has no publication to withdraw. ${NOTHING_WILL_CHANGE}`,
|
|
120982
|
-
whatToDo: [
|
|
120983
|
-
"Explain that only a published page can be unpublished; a draft needs no withdrawal.",
|
|
120984
|
-
"Do not call with confirm:true: the answer will not change."
|
|
120985
|
-
]
|
|
120986
|
-
});
|
|
120987
|
-
}
|
|
120988
121378
|
function dateRequiredRefusal(eventId) {
|
|
120989
121379
|
return refusal4(eventId, {
|
|
120990
121380
|
errorCode: "DATE_REQUIRED",
|
|
@@ -121014,7 +121404,7 @@ function topUpsPaid(billing) {
|
|
|
121014
121404
|
if (!Array.isArray(additions))
|
|
121015
121405
|
return 0;
|
|
121016
121406
|
return additions.reduce((sum, item) => {
|
|
121017
|
-
const amount =
|
|
121407
|
+
const amount = isRecord5(item) ? item[ADDITION_AMOUNT_FIELD] : null;
|
|
121018
121408
|
return sum + (typeof amount === "number" && Number.isFinite(amount) && amount > 0 ? amount : 0);
|
|
121019
121409
|
}, 0);
|
|
121020
121410
|
}
|
|
@@ -121052,12 +121442,10 @@ function unpublishAnswerHints(outcome, topUps) {
|
|
|
121052
121442
|
return [forfeitedNote(forfeited)].filter(Boolean);
|
|
121053
121443
|
}
|
|
121054
121444
|
function previewUnpublish(eventId, event, nowMs) {
|
|
121055
|
-
const billing =
|
|
121445
|
+
const billing = isRecord5(event.billing) ? event.billing : {};
|
|
121056
121446
|
const state2 = billing.state === "published" || billing.state === "held" ? billing.state : "unpublished";
|
|
121057
121447
|
if (state2 === "unpublished")
|
|
121058
121448
|
return { kind: "already" };
|
|
121059
|
-
if (state2 !== "published" && state2 !== "held")
|
|
121060
|
-
return { kind: "refuse", result: notPublishedRefusal(eventId) };
|
|
121061
121449
|
const window2 = windowFromEvent(event, nowMs);
|
|
121062
121450
|
if (state2 === "held") {
|
|
121063
121451
|
return { kind: "refuse", result: windowOpenRefusal(eventId, "after", window2?.window.windowStart ?? null) };
|
|
@@ -121124,7 +121512,7 @@ function registerUnpublicationTools(server2) {
|
|
|
121124
121512
|
"Publishing again afterwards (oko_publish_event) is a new show and a new charge, with a fresh show window from the new date.",
|
|
121125
121513
|
"A date INSIDE the page's show window moves with oko_update_event directly, with no refund and no new charge \u2014 prefer that when the charge may be partly forfeited. A date outside the window: unpublish (refund, up to the charge), change the date with oko_update_event, publish again (new charge); oko_update_event refuses the move otherwise (EVENT_DATE_LOCKED).",
|
|
121126
121514
|
"One rule for every page: an old page that paid nothing is unpublished the same way before its window opens (the refund is 0 lumens) and refused the same way once it has taken place (WINDOW_OPEN, windowState after).",
|
|
121127
|
-
"Refusals carry errorCode and whatToDo:
|
|
121515
|
+
"Refusals carry errorCode and whatToDo: DATE_REQUIRED, WINDOW_OPEN, EDIT_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH. A page that is already a draft is not a refusal: the call answers alreadyUnpublished."
|
|
121128
121516
|
]
|
|
121129
121517
|
}),
|
|
121130
121518
|
inputSchema: {
|
|
@@ -121179,7 +121567,7 @@ function registerUnpublicationTools(server2) {
|
|
|
121179
121567
|
throw new ToolRefusal(named);
|
|
121180
121568
|
throw error3;
|
|
121181
121569
|
}
|
|
121182
|
-
const hints = unpublishAnswerHints(outcome, topUpsPaid(
|
|
121570
|
+
const hints = unpublishAnswerHints(outcome, topUpsPaid(isRecord5(event.billing) ? event.billing : {}));
|
|
121183
121571
|
return textResult({
|
|
121184
121572
|
...outcome,
|
|
121185
121573
|
title,
|
|
@@ -121320,7 +121708,7 @@ function registerLifecycleTools(server2) {
|
|
|
121320
121708
|
returns: "new event id, public/setup/analytics URLs, source event id, and workspaceId.",
|
|
121321
121709
|
examples: ["Use to create a reusable copy of a configured webinar before editing the new copy."],
|
|
121322
121710
|
notes: [
|
|
121323
|
-
"Copies title, video, meta, blocks, forms, and Telegram config; does not copy analytics sessions, leads, or the publication \u2014 the copy is a new page, born a draft.",
|
|
121711
|
+
"Copies title, video, meta, blocks, forms, polls, and Telegram config; does not copy analytics sessions, leads, poll votes (the copy starts round 1 with no votes), or the publication \u2014 the copy is a new page, born a draft.",
|
|
121324
121712
|
"Live overrides are NOT copied: the copy starts with chat mode public and the placeholder hidden, whatever the source had. Set them again with oko_set_chat_mode and oko_set_block_visibility on the new event.",
|
|
121325
121713
|
"Requires only events:write plus createEvents in the target workspace. To move (not copy) an event, use oko_move_event with the same scope.",
|
|
121326
121714
|
"Telegram config is copied only when the key holds telegram:write AND the organizer may send notifications for the source event; otherwise the copy starts with Telegram off and a hint says so. Deciding which channel carries an event is a send permission, not a setup one."
|
|
@@ -121448,6 +121836,275 @@ function killSwitchHint(state2) {
|
|
|
121448
121836
|
return null;
|
|
121449
121837
|
}
|
|
121450
121838
|
|
|
121839
|
+
// build/tools/polls.js
|
|
121840
|
+
var TOOL3 = "oko_control_poll";
|
|
121841
|
+
function refusal5(eventId, pollId, body) {
|
|
121842
|
+
return { ...textResult({ tool: TOOL3, eventId, ...pollId ? { pollId } : {}, ...body }), isError: true };
|
|
121843
|
+
}
|
|
121844
|
+
var POLL_ACTION_EFFECT = {
|
|
121845
|
+
close: "Voting closes: viewers who open the poll are told it is closed, and a popup that was launched for everyone is put out.",
|
|
121846
|
+
open: "Voting opens again: viewers can vote in the current round.",
|
|
121847
|
+
show_results: "Viewers see the results of the current round (the bars and the shares, refreshed every couple of seconds). Without this only the organizer sees the count.",
|
|
121848
|
+
hide_results: "Viewers no longer see the results; only the organizer sees the count.",
|
|
121849
|
+
launch: "The poll pops up for every viewer who has not voted in this round (and for those who join while it is launched); a viewer who closes it does not see it again until the next launch. Voting is opened if it was closed.",
|
|
121850
|
+
stop: "The popup is put out for everyone. Viewers can still open the poll from its button.",
|
|
121851
|
+
reset: "A new round starts: the count is zero, every viewer may vote again, and the popup and the results are put out. The votes of the earlier rounds stay in the data and in the session reports."
|
|
121852
|
+
};
|
|
121853
|
+
function needsControlRoom(action) {
|
|
121854
|
+
return CONTROL_ROOM_POLL_ACTIONS.includes(action);
|
|
121855
|
+
}
|
|
121856
|
+
var CONTROL_ROOM_NOTE = `launch and reset use the live control room of this show (${CONTROL_ROOM_LUMENS} lumens, Pro plan and above), which the organizer buys with the "Enable control room" button in the event's analytics \u2014 no MCP tool buys it. When the control room is not open for the show, OKO refuses with CONTROL_ROOM_REQUIRED and changes nothing. close, open, show_results, hide_results and stop need no control room on any plan.`;
|
|
121857
|
+
function planPollCommand(args) {
|
|
121858
|
+
const { eventId, event, pollId, action, state: state2 } = args;
|
|
121859
|
+
const poll = parseEventPolls(event.polls).find((item) => item.id === pollId);
|
|
121860
|
+
if (!poll) {
|
|
121861
|
+
const incomplete = pollIdSet(event.polls).has(pollId);
|
|
121862
|
+
throw new ToolRefusal(refusal5(eventId, pollId, incomplete ? {
|
|
121863
|
+
errorCode: "POLL_NOT_READY",
|
|
121864
|
+
message: "This poll is on the page but is not complete (a question and 2 to 10 options are needed), so OKO does not serve it and cannot command it. Nothing was changed.",
|
|
121865
|
+
whatToDo: [
|
|
121866
|
+
"Complete the poll with oko_configure_blocks polls (pass the poll with its id, a question and at least two options), then repeat.",
|
|
121867
|
+
"Do not retry as is: the answer will not change until the poll is complete."
|
|
121868
|
+
]
|
|
121869
|
+
} : {
|
|
121870
|
+
errorCode: "POLL_NOT_FOUND",
|
|
121871
|
+
message: "The page has no poll with this id. Nothing was changed.",
|
|
121872
|
+
whatToDo: [
|
|
121873
|
+
"Read the ids with oko_get_event (polls[].id) or create the poll with oko_configure_blocks polls, then repeat.",
|
|
121874
|
+
"Do not retry as is: the answer will not change until the poll exists."
|
|
121875
|
+
]
|
|
121876
|
+
}));
|
|
121877
|
+
}
|
|
121878
|
+
return {
|
|
121879
|
+
poll,
|
|
121880
|
+
preview: {
|
|
121881
|
+
eventId,
|
|
121882
|
+
pollId,
|
|
121883
|
+
question: poll.question,
|
|
121884
|
+
action,
|
|
121885
|
+
effect: POLL_ACTION_EFFECT[action],
|
|
121886
|
+
needsControlRoom: needsControlRoom(action),
|
|
121887
|
+
// A state that could not be read is `null`, never the defaults: «round 1, open» would be a guess.
|
|
121888
|
+
currentState: state2,
|
|
121889
|
+
...state2 === null ? { currentStateNote: `The state of the poll could not be read just now${args.stateUnreadable ? ` (${args.stateUnreadable})` : ""}; the command is judged by the server all the same.` } : {},
|
|
121890
|
+
note: needsControlRoom(action) ? `Nothing has been sent yet. ${CONTROL_ROOM_NOTE}` : "Nothing has been sent yet."
|
|
121891
|
+
}
|
|
121892
|
+
};
|
|
121893
|
+
}
|
|
121894
|
+
function controlPollRefusalResult(eventId, pollId, error3) {
|
|
121895
|
+
if (!error3 || typeof error3 !== "object")
|
|
121896
|
+
return null;
|
|
121897
|
+
const { code, message, details } = error3;
|
|
121898
|
+
if (typeof code !== "string" || !code.startsWith("functions/"))
|
|
121899
|
+
return null;
|
|
121900
|
+
const info = asRecord(details);
|
|
121901
|
+
const text = safeString(message);
|
|
121902
|
+
if (info.reason === "control_room_required") {
|
|
121903
|
+
return refusal5(eventId, pollId, {
|
|
121904
|
+
errorCode: "CONTROL_ROOM_REQUIRED",
|
|
121905
|
+
message: "OKO refused this command: launching a poll for everyone and resetting its count use the live control room, and this show has not bought it. Nothing was changed.",
|
|
121906
|
+
whatToDo: [
|
|
121907
|
+
`Tell the organizer: the control room of a show costs ${CONTROL_ROOM_LUMENS} lumens and needs the Pro plan or higher; they buy it with the "Enable control room" button in the event's analytics. No MCP tool buys it.`,
|
|
121908
|
+
"Offer what needs no control room: show_results (viewers see the bars), close, open, hide_results, stop \u2014 and the poll stays available from its button for everyone.",
|
|
121909
|
+
"Do not retry as is: the answer will not change until the control room is bought."
|
|
121910
|
+
]
|
|
121911
|
+
});
|
|
121912
|
+
}
|
|
121913
|
+
if (info.reason === "not_published") {
|
|
121914
|
+
return refusal5(eventId, pollId, {
|
|
121915
|
+
errorCode: "PAGE_NOT_PUBLISHED",
|
|
121916
|
+
message: "OKO refused this command: launching and resetting need a published page, because a draft has no viewers. Nothing was changed.",
|
|
121917
|
+
whatToDo: [
|
|
121918
|
+
"Publish the page first: oko_publish_event without confirm shows the bill, then again with confirm:true.",
|
|
121919
|
+
"Do not retry as is: the answer will not change until the page is published."
|
|
121920
|
+
]
|
|
121921
|
+
});
|
|
121922
|
+
}
|
|
121923
|
+
if (code === "functions/permission-denied") {
|
|
121924
|
+
if (/live permission/i.test(text))
|
|
121925
|
+
return livePermissionRejectionResult(TOOL3, { eventId });
|
|
121926
|
+
if (/scope|no longer grants/i.test(text))
|
|
121927
|
+
return scopeRejectionResult(TOOL3, "live:control");
|
|
121928
|
+
return null;
|
|
121929
|
+
}
|
|
121930
|
+
if (code === "functions/not-found") {
|
|
121931
|
+
return /poll/i.test(text) ? refusal5(eventId, pollId, {
|
|
121932
|
+
errorCode: "POLL_NOT_FOUND",
|
|
121933
|
+
message: "The page has no poll with this id (or the poll is not complete, so OKO does not serve it). Nothing was changed.",
|
|
121934
|
+
whatToDo: ["Read the polls with oko_get_event (polls[]) and repeat with a complete poll's id."]
|
|
121935
|
+
}) : refusal5(eventId, null, {
|
|
121936
|
+
errorCode: "EVENT_NOT_FOUND",
|
|
121937
|
+
message: "Event not found.",
|
|
121938
|
+
whatToDo: ["Check the id: oko_list_my_events lists the events this organizer can see."]
|
|
121939
|
+
});
|
|
121940
|
+
}
|
|
121941
|
+
if (code === "functions/failed-precondition" && /trash/i.test(text))
|
|
121942
|
+
return eventInTrashRefusal(eventId, TOOL3);
|
|
121943
|
+
if (code === "functions/aborted") {
|
|
121944
|
+
return refusal5(eventId, pollId, {
|
|
121945
|
+
errorCode: "POLL_CHANGED_AT_THE_SAME_MOMENT",
|
|
121946
|
+
message: "Another command reached the same poll at the same moment, so this one was not applied. Nothing was lost.",
|
|
121947
|
+
whatToDo: [
|
|
121948
|
+
"Read the poll's state with oko_get_live_state (polls[]) and repeat the command only if it is still needed."
|
|
121949
|
+
]
|
|
121950
|
+
});
|
|
121951
|
+
}
|
|
121952
|
+
return null;
|
|
121953
|
+
}
|
|
121954
|
+
function pollsLiveFrom(args) {
|
|
121955
|
+
const { event, states, tallies } = args;
|
|
121956
|
+
const raw = Array.isArray(event.polls) ? event.polls : [];
|
|
121957
|
+
const served = parseEventPolls(event.polls);
|
|
121958
|
+
const hints = [];
|
|
121959
|
+
if (served.length < Math.min(raw.length, 10)) {
|
|
121960
|
+
hints.push(`${Math.min(raw.length, 10) - served.length} poll(s) on the page are not served (incomplete, or a repeated id): they are not listed here. oko_validate_event_setup names them.`);
|
|
121961
|
+
}
|
|
121962
|
+
if (served.length === 0)
|
|
121963
|
+
return { polls: [], read: "none", hints };
|
|
121964
|
+
const nodes = states.kind === "read" && states.node && typeof states.node === "object" && !Array.isArray(states.node) ? states.node : {};
|
|
121965
|
+
let unreadable = 0;
|
|
121966
|
+
const polls = served.map((poll) => {
|
|
121967
|
+
const state2 = states.kind === "read" ? Object.hasOwn(nodes, poll.id) ? readPollState(nodes[poll.id]) : { ...DEFAULT_POLL_CTRL } : null;
|
|
121968
|
+
const tallyRead = tallies.get(poll.id);
|
|
121969
|
+
if (state2 === null || !tallyRead || tallyRead.kind === "unreadable") {
|
|
121970
|
+
unreadable += 1;
|
|
121971
|
+
return { pollId: poll.id, question: poll.question, multiple: poll.multiple, state: state2, tally: null };
|
|
121972
|
+
}
|
|
121973
|
+
const tally = readPollTally(tallyRead.node);
|
|
121974
|
+
const known = new Set(poll.options.map((option) => option.id));
|
|
121975
|
+
const removed = Object.entries(tally.options).filter(([id]) => !known.has(id)).map(([id, count]) => ({ id, count }));
|
|
121976
|
+
return {
|
|
121977
|
+
pollId: poll.id,
|
|
121978
|
+
question: poll.question,
|
|
121979
|
+
multiple: poll.multiple,
|
|
121980
|
+
state: state2,
|
|
121981
|
+
tally: {
|
|
121982
|
+
round: state2.round,
|
|
121983
|
+
total: tally.total,
|
|
121984
|
+
options: poll.options.map((option) => ({ id: option.id, text: option.text, count: Object.hasOwn(tally.options, option.id) ? tally.options[option.id] ?? 0 : 0 })),
|
|
121985
|
+
...removed.length > 0 ? { removedOptions: removed } : {}
|
|
121986
|
+
}
|
|
121987
|
+
};
|
|
121988
|
+
});
|
|
121989
|
+
const read = unreadable === 0 ? "ok" : unreadable === polls.length ? "unreadable" : "partial";
|
|
121990
|
+
if (read !== "ok") {
|
|
121991
|
+
const reasons = [
|
|
121992
|
+
...states.kind === "unreadable" ? [states.reason] : [],
|
|
121993
|
+
...[...tallies.values()].flatMap((item) => item.kind === "unreadable" ? [item.reason] : [])
|
|
121994
|
+
];
|
|
121995
|
+
hints.push(`${unreadable} of ${polls.length} poll(s) could not be read from the show (${[...new Set(reasons)].join("; ") || "no answer"}): their state or count is null, which is NOT zero. Repeat the call; do not report a count from a null.`);
|
|
121996
|
+
}
|
|
121997
|
+
hints.push("polls[].tally is the live count of the CURRENT round of each poll, shared by the whole page across sessions. The server raises it at every vote and recounts it from the votes at the end of a session, so it can be a vote short in between; the figures of one session are in oko_get_session_report.");
|
|
121998
|
+
return { polls, read, hints };
|
|
121999
|
+
}
|
|
122000
|
+
async function readPollsLive(eventId, event) {
|
|
122001
|
+
const served = parseEventPolls(event.polls);
|
|
122002
|
+
if (served.length === 0)
|
|
122003
|
+
return pollsLiveFrom({ event, states: { kind: "read", node: null }, tallies: /* @__PURE__ */ new Map() });
|
|
122004
|
+
let states;
|
|
122005
|
+
try {
|
|
122006
|
+
states = { kind: "read", node: (await get(ref(rtdb, `events/${eventId}/pollState`))).val() };
|
|
122007
|
+
} catch (error3) {
|
|
122008
|
+
states = { kind: "unreadable", reason: error3 instanceof Error ? error3.message : String(error3) };
|
|
122009
|
+
}
|
|
122010
|
+
const tallies = /* @__PURE__ */ new Map();
|
|
122011
|
+
if (states.kind === "read") {
|
|
122012
|
+
const nodes = states.node && typeof states.node === "object" && !Array.isArray(states.node) ? states.node : {};
|
|
122013
|
+
await Promise.all(served.map(async (poll) => {
|
|
122014
|
+
const round = Object.hasOwn(nodes, poll.id) ? readPollState(nodes[poll.id]).round : DEFAULT_POLL_CTRL.round;
|
|
122015
|
+
try {
|
|
122016
|
+
tallies.set(poll.id, { kind: "read", node: (await get(ref(rtdb, `events/${eventId}/pollTally/${poll.id}/${round}`))).val() });
|
|
122017
|
+
} catch (error3) {
|
|
122018
|
+
tallies.set(poll.id, { kind: "unreadable", reason: error3 instanceof Error ? error3.message : String(error3) });
|
|
122019
|
+
}
|
|
122020
|
+
}));
|
|
122021
|
+
}
|
|
122022
|
+
return pollsLiveFrom({ event, states, tallies });
|
|
122023
|
+
}
|
|
122024
|
+
async function readPollStateForPreview(eventId, pollId) {
|
|
122025
|
+
try {
|
|
122026
|
+
const node = (await get(ref(rtdb, `events/${eventId}/pollState/${pollId}`))).val();
|
|
122027
|
+
return { state: readPollState(node) };
|
|
122028
|
+
} catch (error3) {
|
|
122029
|
+
return { state: null, stateUnreadable: error3 instanceof Error ? error3.message : String(error3) };
|
|
122030
|
+
}
|
|
122031
|
+
}
|
|
122032
|
+
function pollCommandHints(action, state2) {
|
|
122033
|
+
const hints = [POLL_ACTION_EFFECT[action]];
|
|
122034
|
+
if (action === "reset")
|
|
122035
|
+
hints.push(`The poll is now in round ${state2.round}.`);
|
|
122036
|
+
if (action === "launch" || action === "open")
|
|
122037
|
+
hints.push("Voting is open.");
|
|
122038
|
+
return hints;
|
|
122039
|
+
}
|
|
122040
|
+
var callControlPoll = async (request) => {
|
|
122041
|
+
const call2 = httpsCallable(functions, "controlPoll");
|
|
122042
|
+
return (await call2(request)).data;
|
|
122043
|
+
};
|
|
122044
|
+
async function sendPollCommand(request, call2 = callControlPoll) {
|
|
122045
|
+
let answer;
|
|
122046
|
+
try {
|
|
122047
|
+
answer = await call2(request);
|
|
122048
|
+
} catch (error3) {
|
|
122049
|
+
const named = controlPollRefusalResult(request.eventId, request.pollId, error3);
|
|
122050
|
+
if (named)
|
|
122051
|
+
throw new ToolRefusal(named);
|
|
122052
|
+
throw error3;
|
|
122053
|
+
}
|
|
122054
|
+
return readPollState(answer.state);
|
|
122055
|
+
}
|
|
122056
|
+
function registerPollTools(server2) {
|
|
122057
|
+
server2.registerTool("oko_control_poll", {
|
|
122058
|
+
title: "Control a poll",
|
|
122059
|
+
description: toolDescription({
|
|
122060
|
+
summary: "Command one poll of an event during the show: close or reopen voting, show or hide the results to viewers, launch it as a popup for everyone, stop the popup, or reset the count (a new round). Without confirm nothing is sent: the call asks for confirmation and its preview says what viewers will get.",
|
|
122061
|
+
exampleCall: { eventId: "Kx9pQ2mR7tL4vB8nW3cY", pollId: "p-1b2c3d4e", action: "show_results", confirm: true },
|
|
122062
|
+
args: [
|
|
122063
|
+
"eventId: event whose poll to command.",
|
|
122064
|
+
"pollId: the poll's id (oko_get_event \u2192 polls[].id).",
|
|
122065
|
+
`action: ${POLL_ACTIONS.join(" | ")}.`,
|
|
122066
|
+
"confirm: must be true \u2014 every command changes what viewers see or can do right now."
|
|
122067
|
+
],
|
|
122068
|
+
returns: "eventId, pollId, action and the poll's state after the command (closed, live, liveAt, resultsShown, round), plus hints.",
|
|
122069
|
+
examples: [
|
|
122070
|
+
"Use show_results when the organizer says to show the results; hide_results to take them down.",
|
|
122071
|
+
"Use launch to pop the poll up for everyone (control room); reset to start a new round with the count at zero (control room).",
|
|
122072
|
+
"Read the current count with oko_get_live_state (polls[])."
|
|
122073
|
+
],
|
|
122074
|
+
notes: [
|
|
122075
|
+
`Needs the live:control scope and the right to control the show (control live) on the event. ${CONTROL_ROOM_NOTE}`,
|
|
122076
|
+
"close puts out a launched popup and stops votes (open lets them in again); show_results and hide_results decide whether VIEWERS see the bars \u2014 the organizer always sees the count; stop only puts the popup out.",
|
|
122077
|
+
"reset starts a new round: viewers may vote again and the count restarts at zero. Nothing is deleted \u2014 the votes of the earlier rounds stay in the data and in the session reports.",
|
|
122078
|
+
"Refusals carry errorCode and whatToDo: CONTROL_ROOM_REQUIRED, PAGE_NOT_PUBLISHED (launch and reset on a draft), POLL_NOT_FOUND, POLL_NOT_READY, POLL_CHANGED_AT_THE_SAME_MOMENT, LIVE_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH. Nothing was changed after any of them; relay them and do not retry as is."
|
|
122079
|
+
]
|
|
122080
|
+
}),
|
|
122081
|
+
inputSchema: {
|
|
122082
|
+
eventId: eventIdField,
|
|
122083
|
+
pollId: external_exports.string().regex(POLL_ID_PATTERN).describe("Id of a poll of the event (oko_get_event \u2192 polls[].id)."),
|
|
122084
|
+
action: external_exports.enum(POLL_ACTIONS).describe("The command."),
|
|
122085
|
+
confirm: external_exports.boolean().optional().describe("Required true to send the command.")
|
|
122086
|
+
},
|
|
122087
|
+
outputSchema: basicEventOutputSchema,
|
|
122088
|
+
// Not idempotent: a second `reset` is another round and a second `launch` is another popup (`liveAt` moves).
|
|
122089
|
+
annotations: writeTool
|
|
122090
|
+
}, async ({ eventId, pollId, action, confirm }) => {
|
|
122091
|
+
const { uid } = await authed(["live:control"]);
|
|
122092
|
+
const event = await readEvent(eventId, uid, "live");
|
|
122093
|
+
const plan = planPollCommand({ eventId, event, pollId, action, ...await readPollStateForPreview(eventId, pollId) });
|
|
122094
|
+
requireConfirm(confirm, "oko_control_poll", plan.preview);
|
|
122095
|
+
const state2 = await sendPollCommand({ eventId, pollId, action });
|
|
122096
|
+
return textResult({
|
|
122097
|
+
eventId,
|
|
122098
|
+
pollId,
|
|
122099
|
+
action,
|
|
122100
|
+
question: plan.poll.question,
|
|
122101
|
+
state: state2,
|
|
122102
|
+
hints: pollCommandHints(action, state2),
|
|
122103
|
+
...eventLinks(eventId)
|
|
122104
|
+
});
|
|
122105
|
+
});
|
|
122106
|
+
}
|
|
122107
|
+
|
|
121451
122108
|
// build/tools/live.js
|
|
121452
122109
|
var CHAT_LIVE_SETTINGS = ["mode", "slowModeMs", "automodEnabled", "allowViewerLinks"];
|
|
121453
122110
|
async function writeChatControl(eventId, mode) {
|
|
@@ -121459,7 +122116,7 @@ function registerLiveTools(server2) {
|
|
|
121459
122116
|
description: toolDescription({
|
|
121460
122117
|
summary: "Read main RTDB live controls, aggregate analytics, and chat settings for an event.",
|
|
121461
122118
|
args: ["eventId: event to inspect."],
|
|
121462
|
-
returns: "eventId, agg metrics, ctrl live overrides, chat settings (mode, slowModeMs, automodEnabled, allowViewerLinks, plus accepting \u2014 whether message intake is open right now, as the server enforces it), and analyticsPaused \u2014 true/false, or null when the platform's kill switch could not be read. ctrl.chatMode is the live control for message intake: public, off, or absent (then the event's starting state blocks.chat.intake applies and accepting shows the result). ctrl.<block>Visible is an override; when it is absent the block follows its setup default \u2014 oko_get_event \u2192 live gives the resolved visibleNow per block.",
|
|
122119
|
+
returns: "eventId, agg metrics, ctrl live overrides, chat settings (mode, slowModeMs, automodEnabled, allowViewerLinks, plus accepting \u2014 whether message intake is open right now, as the server enforces it), polls (one entry per poll the page serves: its state \u2014 closed, live, liveAt, resultsShown, round \u2014 and the live count of the current round per option; a figure that could not be read is null, never zero) with pollsRead ok|partial|unreadable|none, and analyticsPaused \u2014 true/false, or null when the platform's kill switch could not be read. ctrl.chatMode is the live control for message intake: public, off, or absent (then the event's starting state blocks.chat.intake applies and accepting shows the result). ctrl.<block>Visible is an override; when it is absent the block follows its setup default \u2014 oko_get_event \u2192 live gives the resolved visibleNow per block.",
|
|
121463
122120
|
examples: ["Use during a webinar before changing live block visibility or starting/ending sessions."],
|
|
121464
122121
|
notes: [
|
|
121465
122122
|
"Live state does not carry the event team: to see who has access to an event and with which permissions, use oko_list_workspace_access on the event's workspace."
|
|
@@ -121470,11 +122127,12 @@ function registerLiveTools(server2) {
|
|
|
121470
122127
|
annotations: readOnlyTool
|
|
121471
122128
|
}, async ({ eventId }) => {
|
|
121472
122129
|
const { uid } = await authed(["analytics:read"]);
|
|
121473
|
-
await readEvent(eventId, uid, "analytics");
|
|
121474
|
-
const [aggSnap, ctrlSnap, killSwitch, ...chatSnaps] = await Promise.all([
|
|
122130
|
+
const event = await readEvent(eventId, uid, "analytics");
|
|
122131
|
+
const [aggSnap, ctrlSnap, killSwitch, pollsLive, ...chatSnaps] = await Promise.all([
|
|
121475
122132
|
get(ref(rtdb, `events/${eventId}/agg`)),
|
|
121476
122133
|
get(ref(rtdb, `events/${eventId}/ctrl`)),
|
|
121477
122134
|
readKillSwitch(eventId),
|
|
122135
|
+
readPollsLive(eventId, event),
|
|
121478
122136
|
...CHAT_LIVE_SETTINGS.map((field2) => get(ref(chatRtdb, `settings/${eventId}/${field2}`)))
|
|
121479
122137
|
]);
|
|
121480
122138
|
const chat = {};
|
|
@@ -121490,12 +122148,15 @@ function registerLiveTools(server2) {
|
|
|
121490
122148
|
const killSwitchNote = killSwitchHint(killSwitch);
|
|
121491
122149
|
if (killSwitchNote)
|
|
121492
122150
|
liveStateHints.push(killSwitchNote);
|
|
122151
|
+
liveStateHints.push(...pollsLive.hints);
|
|
121493
122152
|
return textResult({
|
|
121494
122153
|
eventId,
|
|
121495
122154
|
agg: aggSnap.val() ?? {},
|
|
121496
122155
|
analyticsPaused: killSwitchFlag(killSwitch),
|
|
121497
122156
|
ctrl: ctrlSnap.val() ?? {},
|
|
121498
122157
|
chat,
|
|
122158
|
+
polls: pollsLive.polls,
|
|
122159
|
+
pollsRead: pollsLive.read,
|
|
121499
122160
|
hints: liveStateHints
|
|
121500
122161
|
});
|
|
121501
122162
|
});
|
|
@@ -121834,9 +122495,9 @@ async function assertSessionHasNothingToEnd(eventId, sessionId) {
|
|
|
121834
122495
|
read = { ok: false };
|
|
121835
122496
|
readFailure = error3 instanceof Error ? error3.message : String(error3);
|
|
121836
122497
|
}
|
|
121837
|
-
const
|
|
121838
|
-
if (
|
|
121839
|
-
throw new Error(readFailure ? `${
|
|
122498
|
+
const refusal7 = sessionRepairRefusal(sessionRepairAction({ liveSessionBaseline: sessionBaselineFromRead(read) }));
|
|
122499
|
+
if (refusal7)
|
|
122500
|
+
throw new Error(readFailure ? `${refusal7} (read failed: ${readFailure})` : refusal7);
|
|
121840
122501
|
}
|
|
121841
122502
|
async function attemptCleanup(path, run) {
|
|
121842
122503
|
try {
|
|
@@ -121974,9 +122635,9 @@ function extendSessionRefusalResult(eventId, error3) {
|
|
|
121974
122635
|
return null;
|
|
121975
122636
|
const info = details && typeof details === "object" ? details : {};
|
|
121976
122637
|
const text = safeString(message);
|
|
121977
|
-
const
|
|
122638
|
+
const refusal7 = (body) => ({ ...textResult({ tool: "oko_extend_session", eventId, ...body }), isError: true });
|
|
121978
122639
|
if (info.reason === "session_not_running") {
|
|
121979
|
-
return
|
|
122640
|
+
return refusal7({
|
|
121980
122641
|
errorCode: "SESSION_NOT_RUNNING",
|
|
121981
122642
|
message: text || "No analytics session is running on this page, so there is nothing to extend.",
|
|
121982
122643
|
whatToDo: [
|
|
@@ -121985,14 +122646,14 @@ function extendSessionRefusalResult(eventId, error3) {
|
|
|
121985
122646
|
});
|
|
121986
122647
|
}
|
|
121987
122648
|
if (info.reason === "planned_end_invalid") {
|
|
121988
|
-
return
|
|
122649
|
+
return refusal7({
|
|
121989
122650
|
errorCode: "PLANNED_END_INVALID",
|
|
121990
122651
|
message: text || "The new end must be a moment in the future.",
|
|
121991
122652
|
whatToDo: ["Pass endsAt later than now, or addMinutes to move the current end forward."]
|
|
121992
122653
|
});
|
|
121993
122654
|
}
|
|
121994
122655
|
if (info.reason === "outside_show_window") {
|
|
121995
|
-
return
|
|
122656
|
+
return refusal7({
|
|
121996
122657
|
errorCode: "OUTSIDE_SHOW_WINDOW",
|
|
121997
122658
|
message: text || "The show window of this page is over: a session can no longer be extended.",
|
|
121998
122659
|
whatToDo: ["End the session with oko_end_session; to run the show on another date, duplicate the page with oko_duplicate_event."]
|
|
@@ -122268,7 +122929,7 @@ function registerSessionTools(server2) {
|
|
|
122268
122929
|
"timelineLimit: max timeline buckets per session when includeTimeline=true.",
|
|
122269
122930
|
"includeSnapshots: include baseline/final snapshots; default false."
|
|
122270
122931
|
],
|
|
122271
|
-
returns: "sessions plus count, limit, hasMore/has_more, nextCursor/next_cursor, and analyticsPaused \u2014 true/false, or null when the platform's kill switch could not be read. Heavy timeline/snapshot fields are omitted unless requested.",
|
|
122932
|
+
returns: "sessions plus count, limit, hasMore/has_more, nextCursor/next_cursor, and analyticsPaused \u2014 true/false, or null when the platform's kill switch could not be read. Heavy timeline/snapshot fields are omitted unless requested. A session carries polls (the poll rounds voted in it) when viewers voted.",
|
|
122272
122933
|
examples: ["Use default options for dashboard-like summaries; request includeTimeline only for chart/debug tasks."]
|
|
122273
122934
|
}),
|
|
122274
122935
|
inputSchema: {
|
|
@@ -122331,7 +122992,10 @@ function registerSessionTools(server2) {
|
|
|
122331
122992
|
"includeSnapshots: include baseline/final snapshots; default false."
|
|
122332
122993
|
],
|
|
122333
122994
|
returns: "event/session ids, report URL, markdown summary, event summary, and compact session object. For a session that has not ended yet, also analyticsPaused \u2014 true/false, or null when the platform's kill switch could not be read.",
|
|
122334
|
-
examples: ["Use after a webinar when the user asks for the finished session report without manually opening the report page."]
|
|
122995
|
+
examples: ["Use after a webinar when the user asks for the finished session report without manually opening the report page."],
|
|
122996
|
+
notes: [
|
|
122997
|
+
"Polls: when viewers voted during the session, the markdown has a Polls section and session.polls lists each poll round with its options and counts (votes cast IN THIS SESSION; a round that ran through several sessions is split between their reports; shares are of the voters, so with several answers allowed they can pass 100%). dataQualityWarnings says when the vote count of a session failed (pollsUnmeasured): then an empty polls section means not measured, not nobody voted."
|
|
122998
|
+
]
|
|
122335
122999
|
}),
|
|
122336
123000
|
inputSchema: {
|
|
122337
123001
|
eventId: eventIdField,
|
|
@@ -122384,7 +123048,7 @@ function registerSessionTools(server2) {
|
|
|
122384
123048
|
|
|
122385
123049
|
// build/tools/telegramBilling.js
|
|
122386
123050
|
var TELEGRAM_POST_PRICE_LUMENS = 5;
|
|
122387
|
-
var
|
|
123051
|
+
var TOOL4 = "oko_publish_telegram_post";
|
|
122388
123052
|
function telegramPostCost(scheduled) {
|
|
122389
123053
|
return {
|
|
122390
123054
|
lumens: TELEGRAM_POST_PRICE_LUMENS,
|
|
@@ -122394,26 +123058,26 @@ function telegramPostCost(scheduled) {
|
|
|
122394
123058
|
when: scheduled ? "Scheduling is free. At the scheduled time the platform checks that the payer's plan includes Telegram posts and the wallet holds enough lumens; otherwise the post is refused then (deliveryError plan_required or no_lumens) and nothing is charged." : "The plan and the balance are checked before the send. A refused post is not sent and nothing is charged."
|
|
122395
123059
|
};
|
|
122396
123060
|
}
|
|
122397
|
-
function
|
|
122398
|
-
return { ...textResult({ tool:
|
|
123061
|
+
function refusal6(body) {
|
|
123062
|
+
return { ...textResult({ tool: TOOL4, ...body }), isError: true };
|
|
122399
123063
|
}
|
|
122400
|
-
function
|
|
123064
|
+
function isRecord6(value) {
|
|
122401
123065
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
122402
123066
|
}
|
|
122403
123067
|
function telegramPostRefusalResult(eventId, postId, error3, callerUid) {
|
|
122404
|
-
if (!
|
|
123068
|
+
if (!isRecord6(error3))
|
|
122405
123069
|
return null;
|
|
122406
123070
|
const { code, message, details } = error3;
|
|
122407
123071
|
if (typeof code !== "string" || !code.startsWith("functions/"))
|
|
122408
123072
|
return null;
|
|
122409
|
-
const info =
|
|
123073
|
+
const info = isRecord6(details) ? details : {};
|
|
122410
123074
|
const text = safeString(message);
|
|
122411
123075
|
const afterwards = "After the cause is fixed, create a new post with oko_publish_telegram_post and delete this failed one with oko_cancel_telegram_post (confirm:true) so the history shows one post.";
|
|
122412
123076
|
if (info.errorCode === "ENTITLEMENT_REQUIRED") {
|
|
122413
123077
|
const reason = typeof info.reason === "string" ? info.reason : null;
|
|
122414
123078
|
const inactive = reason === "expired" || reason === "grace_over" || reason === "period_ended";
|
|
122415
123079
|
const callerIsPayer = typeof info.payerUid === "string" && info.payerUid === callerUid;
|
|
122416
|
-
return
|
|
123080
|
+
return refusal6({
|
|
122417
123081
|
errorCode: "ENTITLEMENT_REQUIRED",
|
|
122418
123082
|
eventId,
|
|
122419
123083
|
postId,
|
|
@@ -122432,7 +123096,7 @@ function telegramPostRefusalResult(eventId, postId, error3, callerUid) {
|
|
|
122432
123096
|
if (info.errorCode === "INSUFFICIENT_TOKENS") {
|
|
122433
123097
|
const callerIsPayer = info.actorIsPayer === true || info.callerIsPayer === true;
|
|
122434
123098
|
const needed = typeof info.needed === "number" ? info.needed : TELEGRAM_POST_PRICE_LUMENS;
|
|
122435
|
-
return
|
|
123099
|
+
return refusal6({
|
|
122436
123100
|
errorCode: "INSUFFICIENT_TOKENS",
|
|
122437
123101
|
eventId,
|
|
122438
123102
|
postId,
|
|
@@ -122450,7 +123114,7 @@ function telegramPostRefusalResult(eventId, postId, error3, callerUid) {
|
|
|
122450
123114
|
});
|
|
122451
123115
|
}
|
|
122452
123116
|
if (typeof info.errorCode !== "string" && info.reason === "payer_unresolved") {
|
|
122453
|
-
return
|
|
123117
|
+
return refusal6({
|
|
122454
123118
|
errorCode: "PAYER_UNRESOLVED",
|
|
122455
123119
|
eventId,
|
|
122456
123120
|
postId,
|
|
@@ -122753,13 +123417,13 @@ function registerTelegramTools(server2) {
|
|
|
122753
123417
|
}
|
|
122754
123418
|
|
|
122755
123419
|
// build/tools/wallet.js
|
|
122756
|
-
var
|
|
123420
|
+
var TOOL5 = "oko_wallet";
|
|
122757
123421
|
var GRANTS_LIMIT = 50;
|
|
122758
123422
|
var LEDGER_MAX = 50;
|
|
122759
123423
|
var LEDGER_DEFAULT = 20;
|
|
122760
123424
|
var GRANT_BUCKETS = ["plan", "rollover", "topup"];
|
|
122761
123425
|
var LEDGER_KINDS = ["grant", "charge", "refund", "expire", "settlement"];
|
|
122762
|
-
function
|
|
123426
|
+
function isRecord7(value) {
|
|
122763
123427
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
122764
123428
|
}
|
|
122765
123429
|
function isSafeInt(value) {
|
|
@@ -122774,10 +123438,10 @@ function isoOrNull(value) {
|
|
|
122774
123438
|
return ms === null ? null : new Date(ms).toISOString();
|
|
122775
123439
|
}
|
|
122776
123440
|
function readWalletBalance(raw) {
|
|
122777
|
-
return
|
|
123441
|
+
return isRecord7(raw) && isSafeInt(raw.balance) ? raw.balance : null;
|
|
122778
123442
|
}
|
|
122779
123443
|
function grantForOutput(id, raw) {
|
|
122780
|
-
if (!
|
|
123444
|
+
if (!isRecord7(raw))
|
|
122781
123445
|
return null;
|
|
122782
123446
|
if (!GRANT_BUCKETS.includes(raw.bucket))
|
|
122783
123447
|
return null;
|
|
@@ -122796,13 +123460,13 @@ function grantForOutput(id, raw) {
|
|
|
122796
123460
|
};
|
|
122797
123461
|
}
|
|
122798
123462
|
function ledgerRowForOutput(id, raw) {
|
|
122799
|
-
if (!
|
|
123463
|
+
if (!isRecord7(raw))
|
|
122800
123464
|
return null;
|
|
122801
123465
|
if (!LEDGER_KINDS.includes(raw.kind))
|
|
122802
123466
|
return null;
|
|
122803
123467
|
if (!isSafeInt(raw.amount) || !isSafeInt(raw.delta) || !isSafeInt(raw.balanceAfter))
|
|
122804
123468
|
return null;
|
|
122805
|
-
const ref2 =
|
|
123469
|
+
const ref2 = isRecord7(raw.ref) ? raw.ref : {};
|
|
122806
123470
|
const text = (value) => typeof value === "string" && value ? value : null;
|
|
122807
123471
|
const sessionId = text(ref2.sessionId) ?? text(raw.sessionId);
|
|
122808
123472
|
const isSessionRow = raw.kind === "charge" && sessionId !== null;
|
|
@@ -122829,19 +123493,19 @@ var PLAN_SOURCES = ["lava", "operator", "legacy-exit", "transition-b", "smoke"];
|
|
|
122829
123493
|
function momentMs(value) {
|
|
122830
123494
|
if (value instanceof Date)
|
|
122831
123495
|
return Number.isNaN(value.getTime()) ? null : value.getTime();
|
|
122832
|
-
if (
|
|
123496
|
+
if (isRecord7(value) && typeof value.toMillis === "function") {
|
|
122833
123497
|
const ms = value.toMillis();
|
|
122834
123498
|
return typeof ms === "number" && Number.isFinite(ms) ? ms : null;
|
|
122835
123499
|
}
|
|
122836
123500
|
return null;
|
|
122837
123501
|
}
|
|
122838
123502
|
function readOrganizerPlan(accessData) {
|
|
122839
|
-
if (!
|
|
123503
|
+
if (!isRecord7(accessData))
|
|
122840
123504
|
return { state: "unreadable" };
|
|
122841
123505
|
const raw = accessData.plan;
|
|
122842
123506
|
if (raw === void 0 || raw === null)
|
|
122843
123507
|
return { state: "none" };
|
|
122844
|
-
if (!
|
|
123508
|
+
if (!isRecord7(raw))
|
|
122845
123509
|
return { state: "unreadable" };
|
|
122846
123510
|
const tier = PLAN_TIERS.find((candidate) => candidate === raw.tier);
|
|
122847
123511
|
const status = PLAN_STATUSES.find((candidate) => candidate === raw.status);
|
|
@@ -122909,7 +123573,7 @@ function walletReadRefusal() {
|
|
|
122909
123573
|
return {
|
|
122910
123574
|
...textResult({
|
|
122911
123575
|
errorCode: "PLATFORM_REJECTED_READ",
|
|
122912
|
-
tool:
|
|
123576
|
+
tool: TOOL5,
|
|
122913
123577
|
message: "OKO's security rules refused to read this organizer's wallet although the key carries wallet:read. The organizer behind the key may be blocked or inactive, or the platform has not opened wallets to MCP keys yet.",
|
|
122914
123578
|
whatToDo: [
|
|
122915
123579
|
"Do not retry: rules do not change between attempts.",
|
|
@@ -122949,7 +123613,7 @@ function walletAnswer(input) {
|
|
|
122949
123613
|
unreadableLedgerRows += 1;
|
|
122950
123614
|
}
|
|
122951
123615
|
const last = page.at(-1);
|
|
122952
|
-
const lastAt =
|
|
123616
|
+
const lastAt = isRecord7(last?.data) ? last.data.at : void 0;
|
|
122953
123617
|
const nextCursor = hasMore ? timestampCursorFrom(lastAt, last?.id) : null;
|
|
122954
123618
|
const planBlock = planRead ? planAnswer(planRead) : null;
|
|
122955
123619
|
const hints = [];
|
|
@@ -122986,6 +123650,67 @@ async function readPlanDocument(uid) {
|
|
|
122986
123650
|
throw error3;
|
|
122987
123651
|
}
|
|
122988
123652
|
}
|
|
123653
|
+
var WORKSPACE_TOOL = "oko_workspace_wallet";
|
|
123654
|
+
function workspaceWalletRefusal(workspaceId, body) {
|
|
123655
|
+
return { ...textResult({ tool: WORKSPACE_TOOL, workspaceId, ...body }), isError: true };
|
|
123656
|
+
}
|
|
123657
|
+
function workspaceWalletRefusalResult(workspaceId, error3) {
|
|
123658
|
+
if (!error3 || typeof error3 !== "object")
|
|
123659
|
+
return null;
|
|
123660
|
+
const { code, details } = error3;
|
|
123661
|
+
if (typeof code !== "string" || !code.startsWith("functions/"))
|
|
123662
|
+
return null;
|
|
123663
|
+
const reason = isRecord7(details) ? details.reason : null;
|
|
123664
|
+
if (reason === "wallet_view_forbidden") {
|
|
123665
|
+
return workspaceWalletRefusal(workspaceId, {
|
|
123666
|
+
errorCode: "WALLET_VIEW_FORBIDDEN",
|
|
123667
|
+
message: "OKO does not show this wallet to this organizer. The balance of the wallet that pays for a workspace is told only to the payer and to the members who may spend from it (create events, edit setup, control the live show, start sessions, send notifications).",
|
|
123668
|
+
whatToDo: [
|
|
123669
|
+
"Check the workspace id with oko_list_workspaces: it must be a workspace this organizer belongs to.",
|
|
123670
|
+
"If it is, ask the workspace owner for the right to spend from the workspace, or ask them for the balance. Do not guess a figure.",
|
|
123671
|
+
"Do not retry as is: rights do not change between attempts."
|
|
123672
|
+
]
|
|
123673
|
+
});
|
|
123674
|
+
}
|
|
123675
|
+
if (reason === "wallet_unreadable") {
|
|
123676
|
+
return workspaceWalletRefusal(workspaceId, {
|
|
123677
|
+
errorCode: "WALLET_UNREADABLE",
|
|
123678
|
+
message: "The balance could not be read just now. It is not zero: it is unknown.",
|
|
123679
|
+
whatToDo: [
|
|
123680
|
+
"Repeat the call in a moment. If it keeps failing, tell the organizer the balance is visible in the OKO cabinet under Billing.",
|
|
123681
|
+
"Do not report a balance and do not say the wallet is empty."
|
|
123682
|
+
]
|
|
123683
|
+
});
|
|
123684
|
+
}
|
|
123685
|
+
return null;
|
|
123686
|
+
}
|
|
123687
|
+
function workspaceWalletAnswer(workspaceId, answer) {
|
|
123688
|
+
const data = isRecord7(answer) ? answer : {};
|
|
123689
|
+
const hasWallet = data.hasWallet;
|
|
123690
|
+
const available = data.available;
|
|
123691
|
+
if (typeof hasWallet !== "boolean") {
|
|
123692
|
+
throw new Error("The server's answer about the workspace wallet is malformed (hasWallet is not a yes/no). This is a platform data fault: report it, do not present a balance.");
|
|
123693
|
+
}
|
|
123694
|
+
if (hasWallet && !isSafeInt(available)) {
|
|
123695
|
+
throw new Error("The workspace wallet exists but its balance is not a whole number. This is a platform data fault: report it, do not present the balance as zero.");
|
|
123696
|
+
}
|
|
123697
|
+
const payerIsCaller = data.payerIsCaller === true;
|
|
123698
|
+
const hints = [];
|
|
123699
|
+
if (!hasWallet)
|
|
123700
|
+
hints.push("The payer of this workspace has no wallet yet (nothing was ever granted): available is null, which is not zero. Plans and lumens are bought only in the OKO web cabinet.");
|
|
123701
|
+
if (!payerIsCaller)
|
|
123702
|
+
hints.push("This is the wallet of the workspace OWNER, who pays for the pages of this workspace. Only the number is told; the owner's plan, grants and ledger are theirs. Only the owner tops it up, in the OKO web cabinet.");
|
|
123703
|
+
hints.push(`This tool only reads. Plans and lumens are bought, changed and cancelled only in the OKO web cabinet (${billingUrl()}): send the organizer there, and never ask for card details or a password.`);
|
|
123704
|
+
return {
|
|
123705
|
+
workspaceId: safeString(data.workspaceId, workspaceId),
|
|
123706
|
+
hasWallet,
|
|
123707
|
+
available: hasWallet ? available : null,
|
|
123708
|
+
unit: "lumens",
|
|
123709
|
+
payerIsCaller,
|
|
123710
|
+
billingUrl: billingUrl(),
|
|
123711
|
+
hints
|
|
123712
|
+
};
|
|
123713
|
+
}
|
|
122989
123714
|
function registerWalletTools(server2) {
|
|
122990
123715
|
server2.registerTool("oko_wallet", {
|
|
122991
123716
|
title: "Read lumen wallet",
|
|
@@ -123002,7 +123727,7 @@ function registerWalletTools(server2) {
|
|
|
123002
123727
|
],
|
|
123003
123728
|
notes: [
|
|
123004
123729
|
"Needs the wallet:read scope. A key created before that scope existed gets SCOPE_REQUIRED and its scopes cannot be changed: the organizer creates a new key in the OKO cabinet.",
|
|
123005
|
-
"Shows only the wallet of the organizer behind the key. A page in a shared workspace is paid from the WORKSPACE OWNER's wallet: the publish, session and Telegram previews and refusals name that payer and tell its balance to whoever may spend from it (publish, start a session, send posts); the owner's grants and ledger are theirs to read, not this key's.",
|
|
123730
|
+
"Shows only the wallet of the organizer behind the key. A page in a shared workspace is paid from the WORKSPACE OWNER's wallet: the publish, session and Telegram previews and refusals name that payer and tell its balance to whoever may spend from it (publish, start a session, send posts); the owner's grants and ledger are theirs to read, not this key's. The balance of the wallet that pays for a workspace is one call away: oko_workspace_wallet.",
|
|
123006
123731
|
"This tool cannot buy, change, cancel, grant, top up or spend anything. A plan and lumens are bought, changed and cancelled in the OKO web cabinet only (billingUrl): send the organizer there, and never ask for card data or a password.",
|
|
123007
123732
|
"The plan shown is the organizer's own, the one behind this key. A paid action on a page of a shared workspace is judged by the PAYER's plan (the workspace owner's), which this key cannot read: the refusal of the paid tool names what is missing.",
|
|
123008
123733
|
"Refusals of the paid tools use the same codes as the web: ENTITLEMENT_REQUIRED (the plan does not include the feature) and INSUFFICIENT_TOKENS (not enough lumens); PLATFORM_REJECTED_READ here means the platform refused the read."
|
|
@@ -123042,6 +123767,43 @@ function registerWalletTools(server2) {
|
|
|
123042
123767
|
throw error3;
|
|
123043
123768
|
}
|
|
123044
123769
|
});
|
|
123770
|
+
server2.registerTool("oko_workspace_wallet", {
|
|
123771
|
+
title: "Read a workspace's lumen balance",
|
|
123772
|
+
description: toolDescription({
|
|
123773
|
+
summary: "Read how many lumens the wallet that pays for a workspace holds \u2014 one number. For a shared workspace that is the WORKSPACE OWNER's wallet; for the organizer's personal workspace (its id is the organizer's own uid) it is their own. Read-only: it cannot buy anything.",
|
|
123774
|
+
exampleCall: { workspaceId: "yH4dN8sQ2wE6rT1uI9oP" },
|
|
123775
|
+
args: ["workspaceId: the workspace whose payer's balance to read (oko_list_workspaces)."],
|
|
123776
|
+
returns: `workspaceId, hasWallet, available (whole lumens; null when the payer has no wallet yet \u2014 not zero), unit "lumens", payerIsCaller (true: it is the organizer's own wallet), billingUrl and hints.`,
|
|
123777
|
+
examples: [
|
|
123778
|
+
"Use before publishing a page or starting a session in a shared workspace, to tell whether the wallet that pays can cover it.",
|
|
123779
|
+
"oko_wallet shows the organizer's OWN plan, grants and ledger; this tool shows the balance that pays for a workspace."
|
|
123780
|
+
],
|
|
123781
|
+
notes: [
|
|
123782
|
+
"Needs the wallet:read scope (presets operator and full). Allowed for the payer and for members who may spend from the payer's wallet (create events, edit setup, control the live show, start sessions, send notifications); anyone else, and a workspace that does not exist, get the same refusal WALLET_VIEW_FORBIDDEN.",
|
|
123783
|
+
"Only the number is told to a member. The owner's plan, grants and ledger stay the owner's: use oko_wallet for your own.",
|
|
123784
|
+
"An unreadable balance is the refusal WALLET_UNREADABLE, never zero.",
|
|
123785
|
+
"This tool cannot buy, change, cancel, grant, top up or spend anything. Plans and lumens are bought only in the OKO web cabinet (billingUrl): send the organizer there, and never ask for card data or a password."
|
|
123786
|
+
]
|
|
123787
|
+
}),
|
|
123788
|
+
inputSchema: {
|
|
123789
|
+
workspaceId: external_exports.string().min(1).max(128).regex(/^[^/]+$/, "A workspace id has no slash.").describe("Workspace id, as oko_list_workspaces lists it (a personal workspace's id is the organizer's uid).")
|
|
123790
|
+
},
|
|
123791
|
+
outputSchema: workspaceWalletOutputSchema,
|
|
123792
|
+
annotations: readOnlyTool
|
|
123793
|
+
}, async ({ workspaceId }) => {
|
|
123794
|
+
await authed(["wallet:read"]);
|
|
123795
|
+
const read = httpsCallable(functions, "readWorkspaceWallet");
|
|
123796
|
+
let answer;
|
|
123797
|
+
try {
|
|
123798
|
+
answer = (await read({ workspaceId })).data;
|
|
123799
|
+
} catch (error3) {
|
|
123800
|
+
const named = workspaceWalletRefusalResult(workspaceId, error3);
|
|
123801
|
+
if (named)
|
|
123802
|
+
throw new ToolRefusal(named);
|
|
123803
|
+
throw error3;
|
|
123804
|
+
}
|
|
123805
|
+
return textResult(workspaceWalletAnswer(workspaceId, answer));
|
|
123806
|
+
});
|
|
123045
123807
|
}
|
|
123046
123808
|
|
|
123047
123809
|
// build/tools/workspaceRefusal.js
|
|
@@ -123148,9 +123910,9 @@ function registerWorkspaceTools(server2) {
|
|
|
123148
123910
|
const created = await createWorkspace({ name: name7.trim() });
|
|
123149
123911
|
return textResult({ workspaceId: created.data.workspaceId, name: created.data.name ?? name7.trim(), created: true });
|
|
123150
123912
|
} catch (error3) {
|
|
123151
|
-
const
|
|
123152
|
-
if (
|
|
123153
|
-
throw new ToolRefusal(
|
|
123913
|
+
const refusal7 = workspaceRefusalResult("oko_create_workspace", error3);
|
|
123914
|
+
if (refusal7)
|
|
123915
|
+
throw new ToolRefusal(refusal7);
|
|
123154
123916
|
throw error3;
|
|
123155
123917
|
}
|
|
123156
123918
|
});
|
|
@@ -123285,12 +124047,12 @@ function registerWorkspaceTools(server2) {
|
|
|
123285
124047
|
...args.comment !== void 0 ? { comment: args.comment.trim() } : {}
|
|
123286
124048
|
});
|
|
123287
124049
|
} catch (error3) {
|
|
123288
|
-
const
|
|
124050
|
+
const refusal7 = workspaceRefusalResult("oko_update_workspace_member", error3, {
|
|
123289
124051
|
workspaceId: args.workspaceId,
|
|
123290
124052
|
uid: args.uid
|
|
123291
124053
|
});
|
|
123292
|
-
if (
|
|
123293
|
-
throw new ToolRefusal(
|
|
124054
|
+
if (refusal7)
|
|
124055
|
+
throw new ToolRefusal(refusal7);
|
|
123294
124056
|
throw error3;
|
|
123295
124057
|
}
|
|
123296
124058
|
const createdFields = ["permissions"];
|
|
@@ -123414,9 +124176,9 @@ function registerWorkspaceTools(server2) {
|
|
|
123414
124176
|
const result = await claimInvite({ code: code.trim().toUpperCase() });
|
|
123415
124177
|
return textResult({ ...serialize(result.data) });
|
|
123416
124178
|
} catch (error3) {
|
|
123417
|
-
const
|
|
123418
|
-
if (
|
|
123419
|
-
throw new ToolRefusal(
|
|
124179
|
+
const refusal7 = workspaceRefusalResult("oko_claim_workspace_invite", error3);
|
|
124180
|
+
if (refusal7)
|
|
124181
|
+
throw new ToolRefusal(refusal7);
|
|
123420
124182
|
throw error3;
|
|
123421
124183
|
}
|
|
123422
124184
|
});
|
|
@@ -123498,6 +124260,7 @@ function registerTools(server2) {
|
|
|
123498
124260
|
registerPublicationTools(server2);
|
|
123499
124261
|
registerUnpublicationTools(server2);
|
|
123500
124262
|
registerLiveTools(server2);
|
|
124263
|
+
registerPollTools(server2);
|
|
123501
124264
|
registerSessionTools(server2);
|
|
123502
124265
|
registerChatTools(server2);
|
|
123503
124266
|
registerLeadTools(server2);
|