@okostream/mcp 0.20.0 → 0.22.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 +7 -1
- package/dist/server.js +341 -172
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -141,7 +141,9 @@ Refusals are explicit and never require guessing:
|
|
|
141
141
|
window it bought (the result carries `windowStart`, `windowEnd` and the next
|
|
142
142
|
steps); a date outside the window, a cleared date, or any date on a page whose
|
|
143
143
|
show is over is refused. `oko_update_event` may also return `warnings` (advice,
|
|
144
|
-
never a refusal).
|
|
144
|
+
never a refusal). It also sets the page's look: `pageTheme` (`auto`, `light`, `dark`) and `pageAccent`,
|
|
145
|
+
the color of buttons and links — nine palette colors are free on every plan; a custom `#rrggbb` is a Pro
|
|
146
|
+
and Max right (on other plans it is stored and the page shows the default color until the plan allows it).
|
|
145
147
|
- `WORKSPACE_LIMIT`, `SEATS_FULL`, `ENTITLEMENT_REQUIRED` — the plan of the
|
|
146
148
|
workspace owner does not hold another shared workspace, another member, or
|
|
147
149
|
the capability at all. A refused invitation stays valid. Plans are bought only
|
|
@@ -184,6 +186,10 @@ always works. The result's `sourceVia` says who fetched the image.
|
|
|
184
186
|
costs 10 lumens each for that show: the tool answers with the quote first and
|
|
185
187
|
writes only after `confirm=true`; unpublishing later does not return these
|
|
186
188
|
top-ups.
|
|
189
|
+
A block exists on the page only while it is enabled; `visibility: "hidden"` is
|
|
190
|
+
the default an enabled block starts in, and the live switch shows or hides an
|
|
191
|
+
enabled block during the show. To prepare a banner and reveal it later, enable
|
|
192
|
+
it hidden (`{"promoBanner": {"enabled": true, "visibility": "hidden"}}`).
|
|
187
193
|
The live control room is a paid option of a published page (50 lumens, Pro
|
|
188
194
|
and above) bought in the analytics screen, not through MCP.
|
|
189
195
|
- Operate live audience controls, chat, moderation, and analytics sessions.
|
package/dist/server.js
CHANGED
|
@@ -119137,7 +119137,7 @@ function sanitizeScopes(scopes) {
|
|
|
119137
119137
|
}
|
|
119138
119138
|
|
|
119139
119139
|
// build/version.js
|
|
119140
|
-
var OKO_MCP_VERSION = "0.
|
|
119140
|
+
var OKO_MCP_VERSION = "0.22.0";
|
|
119141
119141
|
|
|
119142
119142
|
// build/auth.js
|
|
119143
119143
|
var state = null;
|
|
@@ -119484,175 +119484,245 @@ function pinnedLookup(address, family) {
|
|
|
119484
119484
|
// build/videoUrl.js
|
|
119485
119485
|
var TWITCH_PARENT = "okostream.app";
|
|
119486
119486
|
function extractIframeSrc(input) {
|
|
119487
|
-
const
|
|
119488
|
-
if (!
|
|
119487
|
+
const tag = /<iframe\b[^>]*>/i.exec(input);
|
|
119488
|
+
if (!tag)
|
|
119489
119489
|
return null;
|
|
119490
|
-
const match =
|
|
119491
|
-
|
|
119492
|
-
|
|
119493
|
-
var YT_ID_RE = /^[\w-]{11}$/;
|
|
119494
|
-
function extractYouTubeId(url) {
|
|
119495
|
-
let parsed;
|
|
119496
|
-
try {
|
|
119497
|
-
parsed = new URL(url);
|
|
119498
|
-
} catch {
|
|
119499
|
-
return null;
|
|
119500
|
-
}
|
|
119501
|
-
const host = parsed.hostname.replace(/^www\./, "");
|
|
119502
|
-
if (host === "youtu.be") {
|
|
119503
|
-
const id = parsed.pathname.slice(1).split("/")[0] ?? "";
|
|
119504
|
-
return YT_ID_RE.test(id) ? id : null;
|
|
119505
|
-
}
|
|
119506
|
-
if (host !== "youtube.com" && host !== "m.youtube.com")
|
|
119490
|
+
const match = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/i.exec(tag[0]);
|
|
119491
|
+
const src = (match?.[1] ?? match?.[2] ?? match?.[3] ?? "").trim().replace(/&/gi, "&");
|
|
119492
|
+
if (!src)
|
|
119507
119493
|
return null;
|
|
119508
|
-
|
|
119509
|
-
if (path.startsWith("/embed/")) {
|
|
119510
|
-
const id = path.slice(7).split("/")[0] ?? "";
|
|
119511
|
-
return YT_ID_RE.test(id) ? id : null;
|
|
119512
|
-
}
|
|
119513
|
-
if (path.startsWith("/shorts/")) {
|
|
119514
|
-
const id = path.slice(8).split("/")[0] ?? "";
|
|
119515
|
-
return YT_ID_RE.test(id) ? id : null;
|
|
119516
|
-
}
|
|
119517
|
-
if (path.startsWith("/live/")) {
|
|
119518
|
-
const id = path.slice(6).split("/")[0] ?? "";
|
|
119519
|
-
return YT_ID_RE.test(id) ? id : null;
|
|
119520
|
-
}
|
|
119521
|
-
if (path === "/watch") {
|
|
119522
|
-
const id = parsed.searchParams.get("v") ?? "";
|
|
119523
|
-
return YT_ID_RE.test(id) ? id : null;
|
|
119524
|
-
}
|
|
119525
|
-
return null;
|
|
119494
|
+
return src.startsWith("//") ? `https:${src}` : src;
|
|
119526
119495
|
}
|
|
119527
|
-
function
|
|
119496
|
+
function parseUrl(url) {
|
|
119528
119497
|
try {
|
|
119529
|
-
return new URL(url)
|
|
119498
|
+
return new URL(url);
|
|
119530
119499
|
} catch {
|
|
119531
119500
|
return null;
|
|
119532
119501
|
}
|
|
119533
119502
|
}
|
|
119534
|
-
|
|
119535
|
-
|
|
119536
|
-
|
|
119537
|
-
|
|
119538
|
-
|
|
119503
|
+
var WRAP_START_RE = /^[<(\[{«„“"'`]+/;
|
|
119504
|
+
var WRAP_END_RE = /[>)\]}»“”"'`.,;:!?]+$/;
|
|
119505
|
+
var BARE_HOST_RE = /^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(?::\d+)?(?:[/?#]|$)/i;
|
|
119506
|
+
function wordAsAddress(word, bare) {
|
|
119507
|
+
let link = word.replace(WRAP_START_RE, "").replace(WRAP_END_RE, "").replace(/&/gi, "&");
|
|
119508
|
+
if (/^https?:\/\//i.test(link))
|
|
119509
|
+
link = link.replace(/^http:/i, "https:");
|
|
119510
|
+
else if (link.startsWith("//"))
|
|
119511
|
+
link = `https:${link}`;
|
|
119512
|
+
else if (bare && BARE_HOST_RE.test(link))
|
|
119513
|
+
link = `https://${link}`;
|
|
119514
|
+
else
|
|
119539
119515
|
return null;
|
|
119540
|
-
|
|
119541
|
-
|
|
119542
|
-
const oid = parsed.searchParams.get("oid");
|
|
119543
|
-
const id = parsed.searchParams.get("id");
|
|
119544
|
-
return oid && id ? { oid, id } : null;
|
|
119545
|
-
}
|
|
119546
|
-
const match = parsed.pathname.match(/\/video(-?\d+)_(\d+)/);
|
|
119547
|
-
return match ? { oid: match[1] ?? "", id: match[2] ?? "" } : null;
|
|
119516
|
+
const parsed = parseUrl(link);
|
|
119517
|
+
return parsed && parsed.hostname.includes(".") ? link : null;
|
|
119548
119518
|
}
|
|
119549
|
-
function
|
|
119550
|
-
|
|
119551
|
-
|
|
119552
|
-
|
|
119553
|
-
|
|
119554
|
-
|
|
119555
|
-
|
|
119556
|
-
|
|
119557
|
-
|
|
119558
|
-
for (const pattern of patterns) {
|
|
119559
|
-
const match = parsed.pathname.match(pattern);
|
|
119560
|
-
if (match?.[1])
|
|
119561
|
-
return match[1];
|
|
119519
|
+
function readVideoAddress(input) {
|
|
119520
|
+
const fromCode = extractIframeSrc(input);
|
|
119521
|
+
const words = fromCode !== null ? [fromCode] : input.split(/\s+/).filter(Boolean);
|
|
119522
|
+
for (const bare of [false, true]) {
|
|
119523
|
+
for (const word of words) {
|
|
119524
|
+
const address = wordAsAddress(word, bare);
|
|
119525
|
+
if (address !== null)
|
|
119526
|
+
return address;
|
|
119527
|
+
}
|
|
119562
119528
|
}
|
|
119563
119529
|
return null;
|
|
119564
119530
|
}
|
|
119565
|
-
|
|
119566
|
-
|
|
119567
|
-
|
|
119568
|
-
|
|
119569
|
-
|
|
119570
|
-
} catch {
|
|
119571
|
-
return null;
|
|
119572
|
-
}
|
|
119573
|
-
if (parsed.hostname.replace(/^www\./, "") !== "vimeo.com")
|
|
119574
|
-
return null;
|
|
119575
|
-
const event = parsed.pathname.match(VIMEO_EVENT_PATH_RE);
|
|
119576
|
-
return event ? `https://vimeo.com/event/${event[1]}/embed${event[2] ? `/${event[2]}` : ""}` : null;
|
|
119531
|
+
function withTwitchParent(parsed) {
|
|
119532
|
+
const params = new URLSearchParams(parsed.search);
|
|
119533
|
+
params.delete("parent");
|
|
119534
|
+
params.append("parent", TWITCH_PARENT);
|
|
119535
|
+
return `${parsed.origin}${parsed.pathname}?${params.toString()}`;
|
|
119577
119536
|
}
|
|
119578
|
-
|
|
119579
|
-
|
|
119580
|
-
|
|
119581
|
-
|
|
119582
|
-
|
|
119583
|
-
|
|
119584
|
-
|
|
119585
|
-
|
|
119586
|
-
|
|
119587
|
-
|
|
119588
|
-
|
|
119589
|
-
|
|
119590
|
-
|
|
119591
|
-
|
|
119592
|
-
|
|
119593
|
-
|
|
119594
|
-
|
|
119595
|
-
|
|
119596
|
-
|
|
119597
|
-
|
|
119598
|
-
|
|
119599
|
-
|
|
119600
|
-
|
|
119537
|
+
var YT_ID_RE = /^[\w-]{11}$/;
|
|
119538
|
+
var VK_HOSTS = /* @__PURE__ */ new Set(["vk.com", "vk.ru", "vkvideo.ru"]);
|
|
119539
|
+
var VK_VIDEO_RE = /(?:video|clip)(-?\d+)_(\d+)/;
|
|
119540
|
+
var VIMEO_EVENT_PATH_RE = /^\/event\/(\d+)(?:\/embed(?:\/([A-Za-z0-9]+))?)?\/?$/;
|
|
119541
|
+
var VIMEO_VIDEO_PATH_RE = /^\/(?:(?:channels\/[^/]+|groups\/[^/]+\/videos|showcase\/[^/]+\/video|album\/[^/]+\/video)\/)?(\d+)(?:\/([0-9a-z]+))?\/?$/i;
|
|
119542
|
+
var KINESCOPE_NOT_A_VIDEO = /* @__PURE__ */ new Set(["embed", "pl", "playlist", "chat"]);
|
|
119543
|
+
var bareHost = (host) => host.replace(/^(?:www|m)\./, "");
|
|
119544
|
+
var SERVICES = [
|
|
119545
|
+
{
|
|
119546
|
+
provider: "youtube",
|
|
119547
|
+
owns: (host) => ["youtube.com", "music.youtube.com", "youtu.be", "youtube-nocookie.com"].includes(bareHost(host)),
|
|
119548
|
+
player: (parsed, address, host) => {
|
|
119549
|
+
const site = bareHost(host);
|
|
119550
|
+
if (site !== "youtube.com" && site !== "youtube-nocookie.com")
|
|
119551
|
+
return null;
|
|
119552
|
+
const match = /^\/embed(?:\/([^/]*))?\/?$/.exec(parsed.pathname);
|
|
119553
|
+
if (!match)
|
|
119554
|
+
return null;
|
|
119555
|
+
const part = match[1] ?? "";
|
|
119556
|
+
if (part === "" || part === "videoseries")
|
|
119557
|
+
return parsed.searchParams.get("list") ? address : null;
|
|
119558
|
+
if (part === "live_stream")
|
|
119559
|
+
return parsed.searchParams.get("channel") ? address : null;
|
|
119560
|
+
return YT_ID_RE.test(part) ? address : null;
|
|
119561
|
+
},
|
|
119562
|
+
page: (parsed, host) => {
|
|
119563
|
+
const site = bareHost(host);
|
|
119564
|
+
const path = parsed.pathname;
|
|
119565
|
+
let id = null;
|
|
119566
|
+
if (site === "youtu.be")
|
|
119567
|
+
id = path.slice(1).split("/")[0] ?? null;
|
|
119568
|
+
else if (site === "youtube.com" || site === "music.youtube.com") {
|
|
119569
|
+
if (path === "/watch")
|
|
119570
|
+
id = parsed.searchParams.get("v");
|
|
119571
|
+
else if (path === "/playlist") {
|
|
119572
|
+
const list = parsed.searchParams.get("list");
|
|
119573
|
+
return list ? `https://www.youtube.com/embed?listType=playlist&list=${encodeURIComponent(list)}` : null;
|
|
119574
|
+
} else {
|
|
119575
|
+
const match = /^\/(?:shorts|live|v)\/([^/]+)/.exec(path);
|
|
119576
|
+
id = match?.[1] ?? null;
|
|
119577
|
+
}
|
|
119578
|
+
}
|
|
119579
|
+
return id && YT_ID_RE.test(id) ? `https://www.youtube.com/embed/${id}` : null;
|
|
119580
|
+
}
|
|
119581
|
+
},
|
|
119582
|
+
{
|
|
119583
|
+
provider: "vk",
|
|
119584
|
+
owns: (host) => VK_HOSTS.has(bareHost(host)),
|
|
119585
|
+
// The player's own address (`video_ext.php?oid=…&id=…`, the `src` of VK's embed code) — its host, its `hash` and the rest: `hash` is
|
|
119586
|
+
// the access key of a video shared by link (met live 2026-11-11: only the pasted embed code played, because nothing rewrote it).
|
|
119587
|
+
player: (parsed, address) => parsed.pathname.endsWith("/video_ext.php") && parsed.searchParams.get("oid") && parsed.searchParams.get("id") ? address : null,
|
|
119588
|
+
// A page names the video in its path (`/video-OWNER_ID`, `/clip-OWNER_ID`, also behind `/playlist/…/` or `/@club/…`) or in the `z`
|
|
119589
|
+
// parameter VK adds when a video is opened over a wall, a community or a catalogue; `z` wins — it is the video the person looks at.
|
|
119590
|
+
page: (parsed) => {
|
|
119591
|
+
if (parsed.pathname.endsWith("/video_ext.php"))
|
|
119592
|
+
return null;
|
|
119593
|
+
const match = VK_VIDEO_RE.exec(parsed.searchParams.get("z") ?? "") ?? VK_VIDEO_RE.exec(parsed.pathname);
|
|
119594
|
+
return match ? `https://vk.com/video_ext.php?oid=${match[1]}&id=${match[2]}&hd=2` : null;
|
|
119595
|
+
}
|
|
119596
|
+
},
|
|
119597
|
+
{
|
|
119598
|
+
provider: "rutube",
|
|
119599
|
+
owns: (host) => bareHost(host) === "rutube.ru",
|
|
119600
|
+
player: (parsed, address) => /^\/play\/embed\/[a-f0-9]+\/?$/i.test(parsed.pathname) ? address : null,
|
|
119601
|
+
// `/video/<id>/`, a private video (`/video/private/<id>/?p=<key>` — the key rides along: without it the player shows a stub),
|
|
119602
|
+
// `/shorts/<id>/` and `/live/video/<id>/` (Rutube's own FAQ: a short is embedded as a video).
|
|
119603
|
+
page: (parsed) => {
|
|
119604
|
+
const match = /^\/(?:video(?:\/private)?|shorts|live\/video)\/([a-f0-9]+)(?:\/|$)/i.exec(parsed.pathname);
|
|
119605
|
+
if (!match)
|
|
119606
|
+
return null;
|
|
119607
|
+
const key = parsed.searchParams.get("p");
|
|
119608
|
+
return `https://rutube.ru/play/embed/${match[1]}${key ? `/?p=${encodeURIComponent(key)}` : ""}`;
|
|
119609
|
+
}
|
|
119610
|
+
},
|
|
119611
|
+
{
|
|
119612
|
+
provider: "vimeo",
|
|
119613
|
+
owns: (host) => ["vimeo.com", "player.vimeo.com"].includes(bareHost(host)),
|
|
119614
|
+
player: (parsed, address, host) => bareHost(host) === "player.vimeo.com" && /^\/video\/\d+\/?$/.test(parsed.pathname) ? address : null,
|
|
119615
|
+
page: (parsed, host) => {
|
|
119616
|
+
if (bareHost(host) !== "vimeo.com")
|
|
119617
|
+
return null;
|
|
119618
|
+
const event = VIMEO_EVENT_PATH_RE.exec(parsed.pathname);
|
|
119619
|
+
if (event)
|
|
119620
|
+
return `https://vimeo.com/event/${event[1]}/embed${event[2] ? `/${event[2]}` : ""}`;
|
|
119621
|
+
const video = VIMEO_VIDEO_PATH_RE.exec(parsed.pathname);
|
|
119622
|
+
if (!video)
|
|
119623
|
+
return null;
|
|
119624
|
+
const key = video[2] ?? parsed.searchParams.get("h");
|
|
119625
|
+
return `https://player.vimeo.com/video/${video[1]}${key ? `?h=${encodeURIComponent(key)}` : ""}`;
|
|
119626
|
+
}
|
|
119627
|
+
},
|
|
119628
|
+
{
|
|
119629
|
+
provider: "kinescope",
|
|
119630
|
+
owns: (host) => bareHost(host) === "kinescope.io",
|
|
119631
|
+
player: (parsed, address) => /^\/embed\/[A-Za-z0-9-]+\/?$/.test(parsed.pathname) ? address : null,
|
|
119632
|
+
// `/<id>`: digits in Kinescope's own examples, also letters and digits or a dashed id.
|
|
119633
|
+
page: (parsed) => {
|
|
119634
|
+
const id = /^\/([A-Za-z0-9-]+)\/?$/.exec(parsed.pathname)?.[1];
|
|
119635
|
+
return id && !KINESCOPE_NOT_A_VIDEO.has(id.toLowerCase()) ? `https://kinescope.io/embed/${id}` : null;
|
|
119636
|
+
}
|
|
119637
|
+
},
|
|
119638
|
+
{
|
|
119639
|
+
provider: "dzen",
|
|
119640
|
+
owns: (host) => bareHost(host) === "dzen.ru",
|
|
119641
|
+
player: (parsed, address) => /^\/embed\/[A-Za-z0-9_-]+\/?$/.test(parsed.pathname) ? address : null,
|
|
119642
|
+
page: (parsed) => {
|
|
119643
|
+
const match = /^\/video\/watch\/([A-Za-z0-9_-]+)/.exec(parsed.pathname);
|
|
119644
|
+
return match ? `https://dzen.ru/embed/${match[1]}` : null;
|
|
119645
|
+
}
|
|
119646
|
+
},
|
|
119647
|
+
{
|
|
119648
|
+
provider: "twitch",
|
|
119649
|
+
owns: (host) => ["twitch.tv", "player.twitch.tv", "clips.twitch.tv"].includes(bareHost(host)),
|
|
119650
|
+
player: (parsed, _address, host) => {
|
|
119651
|
+
const site = bareHost(host);
|
|
119652
|
+
const has = (name7) => Boolean(parsed.searchParams.get(name7));
|
|
119653
|
+
if (site === "player.twitch.tv")
|
|
119654
|
+
return has("channel") || has("video") || has("collection") ? withTwitchParent(parsed) : null;
|
|
119655
|
+
if (site === "clips.twitch.tv" && /^\/embed\/?$/.test(parsed.pathname))
|
|
119656
|
+
return has("clip") ? withTwitchParent(parsed) : null;
|
|
119657
|
+
return null;
|
|
119658
|
+
},
|
|
119659
|
+
page: (parsed, host) => {
|
|
119660
|
+
const site = bareHost(host);
|
|
119661
|
+
const path = parsed.pathname;
|
|
119662
|
+
const clip = site === "clips.twitch.tv" ? /^\/([A-Za-z0-9_-]+)\/?$/.exec(path) : /^\/[A-Za-z0-9_]+\/clip\/([A-Za-z0-9_-]+)/.exec(path);
|
|
119663
|
+
if (clip && clip[1] !== "embed")
|
|
119664
|
+
return `https://clips.twitch.tv/embed?clip=${clip[1]}&parent=${TWITCH_PARENT}`;
|
|
119665
|
+
if (site !== "twitch.tv")
|
|
119666
|
+
return null;
|
|
119667
|
+
const recording = /^\/videos\/(\d+)/.exec(path);
|
|
119668
|
+
if (recording)
|
|
119669
|
+
return `https://player.twitch.tv/?video=v${recording[1]}&parent=${TWITCH_PARENT}`;
|
|
119670
|
+
const channel = /^\/([A-Za-z0-9_]+)\/?$/.exec(path);
|
|
119671
|
+
return channel ? `https://player.twitch.tv/?channel=${channel[1]}&parent=${TWITCH_PARENT}` : null;
|
|
119672
|
+
}
|
|
119673
|
+
},
|
|
119674
|
+
{
|
|
119675
|
+
provider: "dailymotion",
|
|
119676
|
+
owns: (host) => ["dailymotion.com", "geo.dailymotion.com", "dai.ly"].includes(bareHost(host)),
|
|
119677
|
+
// Today's player (`geo.dailymotion.com/player/<player>.html?video=<id>`, and `…/player.html?video=<id>` from its oEmbed) and the
|
|
119678
|
+
// older one (`www.dailymotion.com/embed/video/<id>`).
|
|
119679
|
+
player: (parsed, address, host) => {
|
|
119680
|
+
const site = bareHost(host);
|
|
119681
|
+
if (site === "geo.dailymotion.com" && /^\/player(?:\/[A-Za-z0-9]+)?\.html$/.test(parsed.pathname)) {
|
|
119682
|
+
return parsed.searchParams.get("video") || parsed.searchParams.get("playlist") ? address : null;
|
|
119683
|
+
}
|
|
119684
|
+
return site === "dailymotion.com" && /^\/embed\/video\/[A-Za-z0-9]+\/?$/.test(parsed.pathname) ? address : null;
|
|
119685
|
+
},
|
|
119686
|
+
page: (parsed, host) => {
|
|
119687
|
+
const site = bareHost(host);
|
|
119688
|
+
const match = site === "dai.ly" ? /^\/([A-Za-z0-9]+)\/?$/.exec(parsed.pathname) : site === "dailymotion.com" ? /^\/video\/([A-Za-z0-9]+)/.exec(parsed.pathname) : null;
|
|
119689
|
+
return match ? `https://www.dailymotion.com/embed/video/${match[1]}` : null;
|
|
119690
|
+
}
|
|
119691
|
+
},
|
|
119692
|
+
{
|
|
119693
|
+
provider: "wistia",
|
|
119694
|
+
owns: (host) => host === "wistia.com" || host.endsWith(".wistia.com") || host === "fast.wistia.net",
|
|
119695
|
+
player: (parsed, address, host) => (host === "fast.wistia.net" || host === "fast.wistia.com") && /^\/embed\/iframe\/[A-Za-z0-9]+\/?$/.test(parsed.pathname) ? address : null,
|
|
119696
|
+
page: (parsed, host) => {
|
|
119697
|
+
if (host === "fast.wistia.net")
|
|
119698
|
+
return null;
|
|
119699
|
+
const match = /\/medias\/([A-Za-z0-9]+)/.exec(parsed.pathname);
|
|
119700
|
+
return match ? `https://fast.wistia.net/embed/iframe/${match[1]}` : null;
|
|
119701
|
+
}
|
|
119601
119702
|
}
|
|
119602
|
-
|
|
119603
|
-
}
|
|
119703
|
+
];
|
|
119604
119704
|
function detectAndNormalize(url) {
|
|
119605
|
-
const
|
|
119606
|
-
|
|
119705
|
+
const address = url.trim();
|
|
119706
|
+
const parsed = address ? parseUrl(address) : null;
|
|
119707
|
+
if (!parsed)
|
|
119708
|
+
return { provider: "unknown", normalizedUrl: null };
|
|
119709
|
+
const host = parsed.hostname.toLowerCase();
|
|
119710
|
+
const service = SERVICES.find((candidate) => candidate.owns(host));
|
|
119711
|
+
if (!service)
|
|
119607
119712
|
return { provider: "unknown", normalizedUrl: null };
|
|
119608
|
-
|
|
119609
|
-
|
|
119610
|
-
|
|
119611
|
-
|
|
119612
|
-
|
|
119613
|
-
|
|
119614
|
-
|
|
119615
|
-
|
|
119616
|
-
|
|
119617
|
-
|
|
119618
|
-
|
|
119619
|
-
|
|
119620
|
-
|
|
119621
|
-
return { provider: "rutube", normalizedUrl: null };
|
|
119622
|
-
const vimeoEvent = extractVimeoEventEmbed(trimmed);
|
|
119623
|
-
if (vimeoEvent)
|
|
119624
|
-
return { provider: "vimeo", normalizedUrl: vimeoEvent };
|
|
119625
|
-
const vimeoId = extractPathId(trimmed, "vimeo.com", [/\/video\/(\d+)/, /^\/(\d+)/]);
|
|
119626
|
-
if (vimeoId)
|
|
119627
|
-
return { provider: "vimeo", normalizedUrl: `https://player.vimeo.com/video/${vimeoId}` };
|
|
119628
|
-
if (trimmed.includes("vimeo.com"))
|
|
119629
|
-
return { provider: "vimeo", normalizedUrl: null };
|
|
119630
|
-
const kinescopeId = extractPathId(trimmed, "kinescope.io", [/\/embed\/([a-zA-Z0-9]+)/, /^\/([a-zA-Z0-9]+)$/]);
|
|
119631
|
-
if (kinescopeId)
|
|
119632
|
-
return { provider: "kinescope", normalizedUrl: `https://kinescope.io/embed/${kinescopeId}` };
|
|
119633
|
-
if (trimmed.includes("kinescope.io"))
|
|
119634
|
-
return { provider: "kinescope", normalizedUrl: null };
|
|
119635
|
-
const dzenId = extractPathId(trimmed, "dzen.ru", [/\/embed\/([a-zA-Z0-9_-]+)/, /\/video\/watch\/([a-zA-Z0-9_-]+)/]);
|
|
119636
|
-
if (dzenId)
|
|
119637
|
-
return { provider: "dzen", normalizedUrl: `https://dzen.ru/embed/${dzenId}` };
|
|
119638
|
-
if (trimmed.includes("dzen.ru"))
|
|
119639
|
-
return { provider: "dzen", normalizedUrl: null };
|
|
119640
|
-
const twitchEmbed = extractTwitchEmbed(trimmed);
|
|
119641
|
-
if (twitchEmbed)
|
|
119642
|
-
return { provider: "twitch", normalizedUrl: twitchEmbed };
|
|
119643
|
-
if (trimmed.includes("twitch.tv"))
|
|
119644
|
-
return { provider: "twitch", normalizedUrl: null };
|
|
119645
|
-
const dailymotionId = extractPathId(trimmed, "dailymotion.com", [/\/embed\/video\/([a-zA-Z0-9]+)/, /\/video\/([a-zA-Z0-9]+)/]) ?? extractPathId(trimmed, "dai.ly", [/^\/([a-zA-Z0-9]+)/]);
|
|
119646
|
-
if (dailymotionId)
|
|
119647
|
-
return { provider: "dailymotion", normalizedUrl: `https://www.dailymotion.com/embed/video/${dailymotionId}` };
|
|
119648
|
-
if (trimmed.includes("dailymotion.com") || trimmed.includes("dai.ly"))
|
|
119649
|
-
return { provider: "dailymotion", normalizedUrl: null };
|
|
119650
|
-
const wistiaId = extractPathId(trimmed, "wistia.com", [/\/medias\/([a-zA-Z0-9]+)/]) ?? extractPathId(trimmed, "fast.wistia.net", [/\/embed\/iframe\/([a-zA-Z0-9]+)/]);
|
|
119651
|
-
if (wistiaId)
|
|
119652
|
-
return { provider: "wistia", normalizedUrl: `https://fast.wistia.net/embed/iframe/${wistiaId}` };
|
|
119653
|
-
if (trimmed.includes("wistia.com") || trimmed.includes("wistia.net"))
|
|
119654
|
-
return { provider: "wistia", normalizedUrl: null };
|
|
119655
|
-
return { provider: "unknown", normalizedUrl: null };
|
|
119713
|
+
return { provider: service.provider, normalizedUrl: service.player(parsed, address, host) ?? service.page(parsed, host) };
|
|
119714
|
+
}
|
|
119715
|
+
function readVideoInput(input) {
|
|
119716
|
+
const address = readVideoAddress(input);
|
|
119717
|
+
if (address === null)
|
|
119718
|
+
return { address: null, provider: "unknown", embedUrl: null, how: "none" };
|
|
119719
|
+
const detected = detectAndNormalize(address);
|
|
119720
|
+
if (detected.provider === "unknown")
|
|
119721
|
+
return { address, provider: "unknown", embedUrl: address, how: "unknown" };
|
|
119722
|
+
if (detected.normalizedUrl === null) {
|
|
119723
|
+
return { address, provider: detected.provider, embedUrl: address, how: extractIframeSrc(input) !== null ? "player" : "unread" };
|
|
119724
|
+
}
|
|
119725
|
+
return { address, provider: detected.provider, embedUrl: detected.normalizedUrl, how: detected.normalizedUrl === address ? "player" : "page" };
|
|
119656
119726
|
}
|
|
119657
119727
|
|
|
119658
119728
|
// build/tools/pollModel.js
|
|
@@ -119895,12 +119965,20 @@ function normalizeVideo(videoUrl) {
|
|
|
119895
119965
|
const raw = videoUrl?.trim();
|
|
119896
119966
|
if (!raw)
|
|
119897
119967
|
return { originalUrl: null, embedUrl: null };
|
|
119898
|
-
const
|
|
119899
|
-
|
|
119900
|
-
|
|
119901
|
-
|
|
119902
|
-
|
|
119903
|
-
|
|
119968
|
+
const reading = readVideoInput(raw);
|
|
119969
|
+
return { originalUrl: raw, embedUrl: assertEmbeddableUrl(reading.embedUrl ?? raw) };
|
|
119970
|
+
}
|
|
119971
|
+
function videoLinkWarning(videoUrl) {
|
|
119972
|
+
const raw = videoUrl?.trim();
|
|
119973
|
+
if (!raw)
|
|
119974
|
+
return null;
|
|
119975
|
+
const reading = readVideoInput(raw);
|
|
119976
|
+
if (reading.how !== "unread")
|
|
119977
|
+
return null;
|
|
119978
|
+
return {
|
|
119979
|
+
code: "video_link_unread",
|
|
119980
|
+
message: `The link was recognized as ${reading.provider}, but no video was found in it. It is stored as it stands and the player may stay empty: open the page to check, or pass the embed code (<iframe \u2026>) from the service's "Share \u2192 Embed".`
|
|
119981
|
+
};
|
|
119904
119982
|
}
|
|
119905
119983
|
function csvRows(headers, rows) {
|
|
119906
119984
|
return [headers, ...rows].map((row) => row.map(csvCell).join(",")).join("\n");
|
|
@@ -122308,6 +122386,19 @@ function registerChatTools(server2) {
|
|
|
122308
122386
|
});
|
|
122309
122387
|
}
|
|
122310
122388
|
|
|
122389
|
+
// build/pageAccent.js
|
|
122390
|
+
var PAGE_THEMES = ["auto", "light", "dark"];
|
|
122391
|
+
var PAGE_ACCENT_KEYS = ["sky", "blue", "violet", "pink", "orange", "amber", "green", "teal", "graphite"];
|
|
122392
|
+
var HEX6 = /^#[0-9a-fA-F]{6}$/;
|
|
122393
|
+
function parsePageAccent(value) {
|
|
122394
|
+
const text = value.trim();
|
|
122395
|
+
if (PAGE_ACCENT_KEYS.includes(text))
|
|
122396
|
+
return text;
|
|
122397
|
+
if (HEX6.test(text))
|
|
122398
|
+
return text.toLowerCase();
|
|
122399
|
+
throw new Error(`pageAccent must be one of ${PAGE_ACCENT_KEYS.join(", ")} or a color like "#0a7fa3" (six hex digits); got ${JSON.stringify(value)}.`);
|
|
122400
|
+
}
|
|
122401
|
+
|
|
122311
122402
|
// build/tools/publication.js
|
|
122312
122403
|
var SERVER_REFUSAL_CODES = /* @__PURE__ */ new Set(["ENTITLEMENT_REQUIRED", "INSUFFICIENT_TOKENS", "WORKSPACE_INACTIVE"]);
|
|
122313
122404
|
function refusal2(eventId, body, tool = "oko_publish_event") {
|
|
@@ -123030,6 +123121,71 @@ async function readLiveLayer(eventId, data, nowMs) {
|
|
|
123030
123121
|
return { live, hints };
|
|
123031
123122
|
}
|
|
123032
123123
|
|
|
123124
|
+
// build/tools/blockOffAdvice.js
|
|
123125
|
+
var BLOCK_LEVELS_SENTENCE = "Three levels: `enabled` decides whether the block exists on the page at all; `visibility` (always | hidden) is the default it starts in; the live override (oko_set_block_visibility, the analytics buttons) shows or hides an ENABLED block during the show \u2014 on a disabled block it does nothing.";
|
|
123126
|
+
var PREPARING_FIELDS = {
|
|
123127
|
+
promoBanner: ["visibility", "link", "formId", "pollId"],
|
|
123128
|
+
primaryCta: ["visibility", "title", "text", "buttonLabel", "buttonUrl", "formId", "pollId"]
|
|
123129
|
+
};
|
|
123130
|
+
function touches(patch, fields) {
|
|
123131
|
+
return Boolean(patch) && fields.some((field2) => patch[field2] !== void 0);
|
|
123132
|
+
}
|
|
123133
|
+
function disabledBlockHints(args, storedBlocks) {
|
|
123134
|
+
const hints = [];
|
|
123135
|
+
const names = { promoBanner: "promo banner", primaryCta: "CTA" };
|
|
123136
|
+
for (const key of ["promoBanner", "primaryCta"]) {
|
|
123137
|
+
const patch = args[key];
|
|
123138
|
+
if (!touches(patch, PREPARING_FIELDS[key]))
|
|
123139
|
+
continue;
|
|
123140
|
+
const enabledAfter = patch?.enabled !== void 0 ? patch.enabled === true : asRecord(storedBlocks[key]).enabled === true;
|
|
123141
|
+
if (enabledAfter)
|
|
123142
|
+
continue;
|
|
123143
|
+
hints.push(`The ${names[key]} is still OFF in setup (enabled is not true), so viewers will not see it at all and a live show from analytics cannot reveal it \u2014 its visibility is only the default of an enabled block. To keep it on the page but hidden until the moment, set enabled together with visibility: oko_configure_blocks {"${key}": {"enabled": true, "visibility": "hidden"}}, then show it live with oko_set_block_visibility. On a published page turning a paid block on is charged first and needs confirm \u2014 quote it to the organizer.`);
|
|
123144
|
+
}
|
|
123145
|
+
return hints;
|
|
123146
|
+
}
|
|
123147
|
+
function disabledCardsHint(args, storedEnabled) {
|
|
123148
|
+
const preparing = args.visibility !== void 0 || Array.isArray(args.items) && args.items.length > 0;
|
|
123149
|
+
const enabledAfter = args.enabled !== void 0 ? args.enabled === true : storedEnabled;
|
|
123150
|
+
if (!preparing || enabledAfter)
|
|
123151
|
+
return [];
|
|
123152
|
+
return [
|
|
123153
|
+
'The cards block is still OFF in setup (enabled is not true), so viewers will not see the cards at all and a live show from analytics cannot reveal them \u2014 visibility is only the default of an enabled block. To keep them on the page but hidden until the moment, call oko_configure_cards {"enabled": true, "visibility": "hidden"}, then show them live with oko_set_block_visibility block "cards". On a published page turning the block on is a paid top-up and needs confirm \u2014 quote it to the organizer.'
|
|
123154
|
+
];
|
|
123155
|
+
}
|
|
123156
|
+
function disabledOverrideHint(block, visible) {
|
|
123157
|
+
const enable = block === "cards" ? 'oko_configure_cards {"enabled": true, "visibility": "hidden"}' : block === "banner" ? 'oko_configure_blocks {"promoBanner": {"enabled": true, "visibility": "hidden"}}' : block === "cta" ? 'oko_configure_blocks {"primaryCta": {"enabled": true, "visibility": "hidden"}}' : block === "placeholder" ? 'oko_configure_blocks {"videoPlaceholder": {"enabled": true}}' : 'oko_configure_blocks {"chat": {"enabled": true}}';
|
|
123158
|
+
return `The ${block} block is OFF in setup: it does not exist on the page, so ${visible === null ? "there is nothing for this override to reset" : "this live override was stored but viewers see nothing"}. Enable it first (${enable} \u2014 hidden by default if it should appear later), then use this tool at the moment of the show.`;
|
|
123159
|
+
}
|
|
123160
|
+
function preparedButOffFindings(blocks, cards) {
|
|
123161
|
+
const out = [];
|
|
123162
|
+
const tail = " Viewers will not see it, and it cannot be shown live; enable it (hidden by default if it should appear later): ";
|
|
123163
|
+
const promo = asRecord(blocks.promoBanner);
|
|
123164
|
+
if (promo.enabled !== true && (safeString(promo.imageUrl).trim() || safeString(promo.imageMasterUrl).trim() || safeString(promo.link).trim())) {
|
|
123165
|
+
out.push({
|
|
123166
|
+
code: "promo_prepared_but_off",
|
|
123167
|
+
path: "blocks.promoBanner.enabled",
|
|
123168
|
+
message: "The promo banner has an image or link prepared but is OFF in setup." + tail + 'oko_configure_blocks {"promoBanner": {"enabled": true, "visibility": "hidden"}}.'
|
|
123169
|
+
});
|
|
123170
|
+
}
|
|
123171
|
+
const cta = asRecord(blocks.primaryCta);
|
|
123172
|
+
if (cta.enabled !== true && (safeString(cta.title).trim() || safeString(cta.text).trim() || safeString(cta.buttonUrl).trim())) {
|
|
123173
|
+
out.push({
|
|
123174
|
+
code: "cta_prepared_but_off",
|
|
123175
|
+
path: "blocks.primaryCta.enabled",
|
|
123176
|
+
message: "The CTA has a title, text or button URL prepared but is OFF in setup." + tail + 'oko_configure_blocks {"primaryCta": {"enabled": true, "visibility": "hidden"}}.'
|
|
123177
|
+
});
|
|
123178
|
+
}
|
|
123179
|
+
if (!cards.enabled && cards.items.length > 0) {
|
|
123180
|
+
out.push({
|
|
123181
|
+
code: "cards_prepared_but_off",
|
|
123182
|
+
path: "blocks.cards.enabled",
|
|
123183
|
+
message: "The cards block has cards prepared but is OFF in setup." + tail + 'oko_configure_cards {"enabled": true, "visibility": "hidden"}.'
|
|
123184
|
+
});
|
|
123185
|
+
}
|
|
123186
|
+
return out;
|
|
123187
|
+
}
|
|
123188
|
+
|
|
123033
123189
|
// build/tools/setupValidation.js
|
|
123034
123190
|
function scheduleWindowPassedByPlan(event, eventDateMs, rawDurationMinutes, nowMs) {
|
|
123035
123191
|
if (event.analyticsSessionMode !== "schedule")
|
|
@@ -123142,9 +123298,10 @@ function validateEventSetup(eventId, event, nowMs = Date.now()) {
|
|
|
123142
123298
|
} else if (safeString(promo.imageMasterUrl).trim() && !readImageVariants(promo.imageVariants)) {
|
|
123143
123299
|
add(warnings, "promo_variants_pending", "blocks.promoBanner.imageVariants", "Promo banner image variants are not ready yet.");
|
|
123144
123300
|
}
|
|
123145
|
-
} else if (safeString(promo.imageUrl).trim() || safeString(promo.imageMasterUrl).trim()) {
|
|
123146
|
-
add(suggestions, "promo_disabled_with_image", "blocks.promoBanner.enabled", 'Promo banner has an uploaded image but is disabled in setup, so viewers never see it. Enable it with oko_configure_blocks promoBanner.enabled=true; to hide it only during the live show, keep it enabled and use oko_set_block_visibility block "banner" visible=false.');
|
|
123147
123301
|
}
|
|
123302
|
+
const cardsForLint = readCardsConfig(event);
|
|
123303
|
+
for (const finding of preparedButOffFindings(blocks, cardsForLint))
|
|
123304
|
+
add(warnings, finding.code, finding.path, finding.message);
|
|
123148
123305
|
const cta = asRecord(blocks.primaryCta);
|
|
123149
123306
|
if (cta.enabled === true) {
|
|
123150
123307
|
checkFormBinding("blocks.primaryCta.formId", cta.formId);
|
|
@@ -123562,6 +123719,7 @@ function registerEventTools(server2) {
|
|
|
123562
123719
|
notes: [
|
|
123563
123720
|
"billing (server-owned) is the publication record. Blocks and the control room paid AFTER publication are listed in billing.additions (one element per block or per control room, with the source payEventExtras or reconcile) and billing.controlRoom: true once the control room is paid; the show's paid set is billing.invoice plus billing.additions. Never write it.",
|
|
123564
123721
|
"blocks.promoBanner.visibility, blocks.primaryCta.visibility, blocks.cards.visibility and blocks.video.placeholder.enabled are SETUP values \u2014 the default a block starts with. A live override (oko_set_block_visibility, or the organizer's switch in analytics) replaces it without touching setup, so these fields do not say what viewers see now: `live.<block>.visibleNow` does, with `decidedBy` naming the layer that decided. oko_get_live_state shows the raw overrides (ctrl.bannerVisible and so on).",
|
|
123722
|
+
"meta.pageTheme and meta.pageAccent are the page's look AS STORED (absent = auto and sky). A custom hex in meta.pageAccent is painted only while the plan that pays for the page holds the right (Pro and Max); otherwise the page shows sky \u2014 this tool does not say which, so for a custom color on an unknown plan read oko_wallet.",
|
|
123565
123723
|
"blocks.chat: the panel exists exactly when blocks.chat.enabled is true (false HIDES the panel; messages are kept for 90 days and come back when it is enabled again). Message intake is ONE live state \u2014 oko_set_chat_mode reads and writes it; blocks.chat.intake is only the STARTING state an untouched event has (closed by default, or open), and blocks.chat.openOnSessionStart / closeOnSessionEnd are two triggers that flip that same state when an analytics session starts or ends. blocks.chat.mode and blocks.chat.visibility are legacy and read by nobody \u2014 ignore those two."
|
|
123566
123724
|
]
|
|
123567
123725
|
}),
|
|
@@ -123619,7 +123777,7 @@ function registerEventTools(server2) {
|
|
|
123619
123777
|
args: [
|
|
123620
123778
|
"title: webinar title.",
|
|
123621
123779
|
"workspaceId: optional workspace id; omitted means personal workspace.",
|
|
123622
|
-
"videoUrl: optional iframe
|
|
123780
|
+
"videoUrl: optional link to the video or the service's embed code (<iframe \u2026>); OKO reads the player address out of it \u2014 an embed code or a player address is stored as it stands, a link it cannot read is stored too and named in warnings (video_link_unread).",
|
|
123623
123781
|
"eventDate: optional ISO datetime.",
|
|
123624
123782
|
"chatEnabled: whether the chat block is on the page at creation (default true \u2014 the chat exists practically always). It does NOT open message sending: a new event starts closed, so the chat is visible and silent until the organizer opens it (oko_set_chat_mode, or the analytics switch), sets blocks.chat.intake to open, or enables the session triggers in oko_configure_blocks."
|
|
123625
123783
|
],
|
|
@@ -123666,7 +123824,7 @@ function registerEventTools(server2) {
|
|
|
123666
123824
|
...eventLinks(docRef.id),
|
|
123667
123825
|
workspaceId,
|
|
123668
123826
|
runtime: created.runtime,
|
|
123669
|
-
warnings: sessionWarnings,
|
|
123827
|
+
warnings: [...videoLinkWarning(args.videoUrl) ? [videoLinkWarning(args.videoUrl)] : [], ...sessionWarnings],
|
|
123670
123828
|
hints: [
|
|
123671
123829
|
"Analytics sessions: the page starts and ends its session by itself at the event time (sessionMode schedule; a session costs 1 lumen per minute, taken when it ends). Change the time of the session or turn the auto start off with oko_update_event (sessionMode, sessionStartAt, sessionEndAt).",
|
|
123672
123830
|
...eventRuntimeHint(created.runtime),
|
|
@@ -123681,15 +123839,16 @@ function registerEventTools(server2) {
|
|
|
123681
123839
|
description: toolDescription({
|
|
123682
123840
|
summary: "Update safe top-level event/setup metadata fields.",
|
|
123683
123841
|
exampleCall: { "eventId": "Kx9pQ2mR7tL4vB8nW3cY", "title": "Autumn product webinar (rescheduled)", "eventDate": "2026-09-12T07:00:00.000Z", "speakerName": "Jane Doe", "description": "", "durationMinutes": 60 },
|
|
123684
|
-
args: ["eventId plus any subset of title, videoUrl, eventDate, speakerName, description, pinned, sessionMode, durationMinutes, sessionStartAt, sessionEndAt."],
|
|
123842
|
+
args: ["eventId plus any subset of title, videoUrl, eventDate, speakerName, description, pinned, sessionMode, durationMinutes, sessionStartAt, sessionEndAt, pageTheme, pageAccent."],
|
|
123685
123843
|
returns: "event id, the list of Firestore fields updated, and session warnings (plan versus event time, lumens versus plan, a plan with no sessions).",
|
|
123686
|
-
examples: ["Use for title/date/speaker/video edits; use oko_configure_blocks for blocks and forms."],
|
|
123844
|
+
examples: ["Use for title/date/speaker/video edits; use oko_configure_blocks for blocks and forms.", "videoUrl takes a link to the video or the service's embed code (<iframe \u2026>); a link OKO cannot read is stored as it stands and named in warnings (video_link_unread)."],
|
|
123687
123845
|
notes: [
|
|
123688
123846
|
"An ANALYTICS SESSION measures the show and costs 1 lumen per minute, taken when it ends (no debt; it stops by itself when the lumens run out). It always has a start and an end: the page's session plan \u2014 sessionStartAt and sessionEndAt, both or neither \u2014 and without them the plan is the event time (eventDate to eventDate + durationMinutes).",
|
|
123689
123847
|
"sessionMode now means ONLY the automatic start: 'schedule' (the default of every new page) starts the session by itself at the plan's start; 'manual' turns that off \u2014 the organizer starts it with oko_start_session and a Telegram reminder comes 10 minutes before eventDate. Either way the session ends at the end of its plan, and oko_extend_session moves that end while it runs. This is still an analytics session, not an event status.",
|
|
123690
123848
|
"sessionStartAt/sessionEndAt: pass BOTH ISO datetimes to set a session time different from the show's, or pass null for both to follow the event time again. A session plan that differs from the event time is reported as a warning (moving eventDate does not move an explicit plan).",
|
|
123691
123849
|
"Setting sessionMode 'schedule' on a page with no date, or clearing the date of a page in 'schedule', in one call is refused (nothing could start from it); a page born with the auto start on and no date yet is normal and can be edited freely.",
|
|
123692
123850
|
'On a published page eventDate moves only INSIDE the show window bought at publication (from a day before the event date to two days after it): a date inside is written; a date outside, a cleared date, or any date on a page that shows "held"/\u041F\u0440\u043E\u0432\u0435\u0434\u0435\u043D\u043E is refused with errorCode EVENT_DATE_LOCKED, windowStart/windowEnd and whatToDo \u2014 every other field in the same call still goes through if eventDate is dropped from it. For a date outside the window: oko_unpublish_event (refunds, only before the window opens and only for a page that was charged), change the date, oko_publish_event (new charge); or duplicate the page with oko_duplicate_event for the new date. A draft or an unpublished page moves freely.',
|
|
123851
|
+
`pageTheme (auto | light | dark) is how the viewer page is painted: auto follows each viewer's system, light and dark look the same for everyone. pageAccent is the color of the page's buttons and links: one of the nine palette colors (sky, blue, violet, pink, orange, amber, green, teal, graphite) or a custom "#rrggbb". The nine palette colors are free on every plan. A custom hex is a right of the Pro and Max plans (of the plan that pays for the page): on a plan without it the value is still stored, but the page shows the default color (sky) until the plan allows it \u2014 the choice is not lost. The color applies to buttons and links in both the light and the dark theme and is adjusted for readability (a custom color is lightened or darkened as needed, keeping its hue). Both are read back as stored in oko_get_event, meta.pageTheme and meta.pageAccent; "sky" is the default.`,
|
|
123693
123852
|
"The result may carry warnings (never a refusal), each { code, message }: about the date \u2014 the show would start inside the window but not fit into it, or the date is already behind us while sessionMode is 'schedule' (the scheduler then starts the analytics session right away) \u2014 and about the session (see above)."
|
|
123694
123853
|
]
|
|
123695
123854
|
}),
|
|
@@ -123704,7 +123863,9 @@ function registerEventTools(server2) {
|
|
|
123704
123863
|
sessionMode: external_exports.enum(["manual", "schedule"]).optional().describe("Automatic start of the analytics session: 'schedule' starts it by itself at the start of the session plan (the default of a new page), 'manual' leaves the start to oko_start_session. The session always ends at the end of its plan."),
|
|
123705
123864
|
durationMinutes: external_exports.number().int().min(15).max(1440).optional().describe("Event duration in minutes (15-1440); with eventDate it is the default session plan."),
|
|
123706
123865
|
sessionStartAt: external_exports.string().datetime().nullable().optional().describe("Start of the session plan (ISO datetime), or null to follow the event time. Pass together with sessionEndAt."),
|
|
123707
|
-
sessionEndAt: external_exports.string().datetime().nullable().optional().describe("End of the session plan (ISO datetime), or null to follow the event time. Pass together with sessionStartAt.")
|
|
123866
|
+
sessionEndAt: external_exports.string().datetime().nullable().optional().describe("End of the session plan (ISO datetime), or null to follow the event time. Pass together with sessionStartAt."),
|
|
123867
|
+
pageTheme: external_exports.enum(PAGE_THEMES).optional().describe("How the viewer page is painted: auto (follows each viewer's system, the default), light or dark."),
|
|
123868
|
+
pageAccent: external_exports.string().max(16).optional().describe(`Color of the page's buttons and links: sky (the default), blue, violet, pink, orange, amber, green, teal, graphite \u2014 free on every plan \u2014 or a custom "#rrggbb" (Pro and Max; on other plans stored, page shows sky).`)
|
|
123708
123869
|
},
|
|
123709
123870
|
outputSchema: updateOutputSchema,
|
|
123710
123871
|
annotations: writeTool
|
|
@@ -123712,7 +123873,7 @@ function registerEventTools(server2) {
|
|
|
123712
123873
|
const requiredScopes = /* @__PURE__ */ new Set();
|
|
123713
123874
|
if (args.title !== void 0 || args.pinned !== void 0)
|
|
123714
123875
|
requiredScopes.add("events:write");
|
|
123715
|
-
if (args.videoUrl !== void 0 || args.eventDate !== void 0 || args.speakerName !== void 0 || args.description !== void 0 || args.sessionMode !== void 0 || args.durationMinutes !== void 0 || args.sessionStartAt !== void 0 || args.sessionEndAt !== void 0) {
|
|
123876
|
+
if (args.videoUrl !== void 0 || args.eventDate !== void 0 || args.speakerName !== void 0 || args.description !== void 0 || args.sessionMode !== void 0 || args.durationMinutes !== void 0 || args.sessionStartAt !== void 0 || args.sessionEndAt !== void 0 || args.pageTheme !== void 0 || args.pageAccent !== void 0) {
|
|
123716
123877
|
requiredScopes.add("setup:write");
|
|
123717
123878
|
}
|
|
123718
123879
|
if (requiredScopes.size === 0)
|
|
@@ -123739,6 +123900,10 @@ function registerEventTools(server2) {
|
|
|
123739
123900
|
}
|
|
123740
123901
|
if (args.pinned !== void 0)
|
|
123741
123902
|
patch.pinned = args.pinned;
|
|
123903
|
+
if (args.pageTheme !== void 0)
|
|
123904
|
+
patch["meta.pageTheme"] = args.pageTheme;
|
|
123905
|
+
if (args.pageAccent !== void 0)
|
|
123906
|
+
patch["meta.pageAccent"] = parsePageAccent(args.pageAccent);
|
|
123742
123907
|
if (args.sessionMode !== void 0)
|
|
123743
123908
|
patch.analyticsSessionMode = args.sessionMode;
|
|
123744
123909
|
if (args.durationMinutes !== void 0)
|
|
@@ -123759,7 +123924,9 @@ function registerEventTools(server2) {
|
|
|
123759
123924
|
await writeEventPatchWithCards(args.eventId, patch, null, validate);
|
|
123760
123925
|
const touchesSession = ["meta.eventDate", "meta.durationMinutes", "meta.sessionStartAt", "meta.sessionEndAt", "analyticsSessionMode"].some((key) => key in patch);
|
|
123761
123926
|
const sessionWarnings = touchesSession ? await collectSessionWarnings(args.eventId, await readEvent(args.eventId, uid, "read")) : [];
|
|
123927
|
+
const videoWarning = videoLinkWarning(args.videoUrl);
|
|
123762
123928
|
const warnings = [
|
|
123929
|
+
...videoWarning ? [videoWarning] : [],
|
|
123763
123930
|
...dateAdvice.map((message) => ({ code: "event_date_advice", message })),
|
|
123764
123931
|
...sessionWarnings
|
|
123765
123932
|
];
|
|
@@ -123772,7 +123939,7 @@ function registerEventTools(server2) {
|
|
|
123772
123939
|
server2.registerTool("oko_configure_blocks", {
|
|
123773
123940
|
title: "Configure event blocks",
|
|
123774
123941
|
description: toolDescription({
|
|
123775
|
-
summary: "Configure supported event setup blocks, forms, polls, and Telegram channel ids.",
|
|
123942
|
+
summary: "Configure supported event setup blocks, forms, polls, and Telegram channel ids. " + BLOCK_LEVELS_SENTENCE + ' To upload now and show later, set {"promoBanner": {"enabled": true, "visibility": "hidden"}} (visibility alone leaves a disabled block off the page), then call oko_set_block_visibility at the moment.',
|
|
123776
123943
|
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" } },
|
|
123777
123944
|
args: [
|
|
123778
123945
|
"eventId: event to edit.",
|
|
@@ -123927,6 +124094,7 @@ function registerEventTools(server2) {
|
|
|
123927
124094
|
if (args.promoBanner?.enabled === true && !bannerHasImage) {
|
|
123928
124095
|
hints.push(`The promo banner is enabled but has no image yet: upload one with oko_upload_image kind "promoBanner". ${IMAGE_ADVICE.promoBanner}`);
|
|
123929
124096
|
}
|
|
124097
|
+
hints.push(...disabledBlockHints(args, currentBlocks));
|
|
123930
124098
|
if (args.chat?.enabled !== void 0 || args.promoBanner?.enabled !== void 0 || args.primaryCta?.enabled !== void 0 || args.videoPlaceholder?.enabled !== void 0) {
|
|
123931
124099
|
hints.push("Setup toggles (enabled) decide whether a block exists on the page and persist; live overrides (oko_set_block_visibility) show or hide an enabled block temporarily during the show. Uploading an image never enables a block.");
|
|
123932
124100
|
}
|
|
@@ -124059,6 +124227,7 @@ function registerEventTools(server2) {
|
|
|
124059
124227
|
warnings.push("The ids shown for those positions are provisional (generated on read) and were NOT stored: oko_upload_image and oko_delete_image will not accept them. Save the cards first by passing items to oko_configure_cards \u2014 the saved ids are the ones to use.");
|
|
124060
124228
|
}
|
|
124061
124229
|
}
|
|
124230
|
+
const hints = [...paid.hints ?? [], ...disabledCardsHint(args, readCardsConfig(event).enabled)];
|
|
124062
124231
|
return textResult({
|
|
124063
124232
|
eventId: args.eventId,
|
|
124064
124233
|
updated: Object.keys(patch),
|
|
@@ -124069,7 +124238,7 @@ function registerEventTools(server2) {
|
|
|
124069
124238
|
preservedImageCardIds,
|
|
124070
124239
|
warnings,
|
|
124071
124240
|
...paid.payment ? { payment: paid.payment } : {},
|
|
124072
|
-
...
|
|
124241
|
+
...hints.length ? { hints } : {}
|
|
124073
124242
|
});
|
|
124074
124243
|
});
|
|
124075
124244
|
}
|
|
@@ -125165,7 +125334,7 @@ function registerLiveTools(server2) {
|
|
|
125165
125334
|
server2.registerTool("oko_set_block_visibility", {
|
|
125166
125335
|
title: "Set live block visibility",
|
|
125167
125336
|
description: toolDescription({
|
|
125168
|
-
summary: "Set or reset a live RTDB visibility override for a public event block.",
|
|
125337
|
+
summary: "Set or reset a live RTDB visibility override for a public event block. " + BLOCK_LEVELS_SENTENCE + ' For "upload now, show later": oko_configure_blocks {"promoBanner": {"enabled": true, "visibility": "hidden"}} first, then this tool with block "banner", visible true at the moment.',
|
|
125169
125338
|
exampleCall: { "eventId": "Kx9pQ2mR7tL4vB8nW3cY", "block": "placeholder", "visible": true, "confirm": true },
|
|
125170
125339
|
args: ["eventId: event to control.", "block: chat, banner, cta, cards, or placeholder.", "visible: true/false, or null to reset override.", "confirm: must be true."],
|
|
125171
125340
|
returns: "eventId, block, and effective override value.",
|
|
@@ -125199,7 +125368,7 @@ function registerLiveTools(server2) {
|
|
|
125199
125368
|
placeholder: setupBlocks.video?.placeholder?.enabled === true
|
|
125200
125369
|
};
|
|
125201
125370
|
const chatHint = visible === null ? "Live control cleared: message intake follows the starting state (blocks.chat.intake)." : visible ? "Message sending is open (chat mode public); the panel was not touched." : "Message sending is paused (chat mode off); history stays visible, the panel was not touched.";
|
|
125202
|
-
const hints = !setupEnabled[block] ? [
|
|
125371
|
+
const hints = !setupEnabled[block] ? [disabledOverrideHint(block, visible)] : block === "chat" ? [chatHint] : [visible === null ? "Override removed: the block follows its setup visibility again." : `Live override applied on top of setup; visible=null returns the ${block} block to its setup default.`];
|
|
125203
125372
|
if (controlRoom)
|
|
125204
125373
|
hints.push(String(controlRoom.note));
|
|
125205
125374
|
const paths = {
|
|
@@ -126833,7 +127002,7 @@ function registerWalletTools(server2) {
|
|
|
126833
127002
|
"limit: ledger rows per page, default 20, max 50, newest first.",
|
|
126834
127003
|
"cursor: nextCursor returned by the previous page. The plan and the grants come with the first page only; the balance with every page."
|
|
126835
127004
|
],
|
|
126836
|
-
returns: `billingUrl (the cabinet's Billing page), plan on the first page (tier core|pro|max|legacy, status active|past_due|canceled_at_period_end|expired, source lava|operator|legacy-exit|transition-b|smoke or null, periodEnd and graceUntil as ISO times or null; plan is null when the organizer has no plan) with planRead ok|unreadable (unreadable: the plan could not be read, so null means unknown, not no plan), hasWallet, balance (whole lumens, null when the organizer has no wallet yet), unit "lumens", grants on the first page (id, bucket plan|rollover|topup, amount, remaining, expiresAt, createdAt), ledger rows (id, kind grant|charge|refund|expire|settlement, amount, delta, balanceAfter, by, at, eventId, showId, workspaceId, postId, sessionId, minutes, forgiven), unreadableGrants/unreadableLedgerRows (rows that did not read: say so, never hide them) and cursor pagination metadata.`,
|
|
127005
|
+
returns: `billingUrl (the cabinet's Billing page), plan on the first page (tier core|pro|max|legacy, status active|past_due|canceled_at_period_end|expired, source lava|tribute|operator|legacy-exit|transition-b|smoke or null, periodEnd and graceUntil as ISO times or null; plan is null when the organizer has no plan) with planRead ok|unreadable (unreadable: the plan could not be read, so null means unknown, not no plan), hasWallet, balance (whole lumens, null when the organizer has no wallet yet), unit "lumens", grants on the first page (id, bucket plan|rollover|topup, amount, remaining, expiresAt, createdAt), ledger rows (id, kind grant|charge|refund|expire|settlement, amount, delta, balanceAfter, by, at, eventId, showId, workspaceId, postId, sessionId, minutes, forgiven), unreadableGrants/unreadableLedgerRows (rows that did not read: say so, never hide them) and cursor pagination metadata.`,
|
|
126837
127006
|
examples: [
|
|
126838
127007
|
"Use before publishing a page or starting a session when the organizer asks how many lumens are left or what was charged.",
|
|
126839
127008
|
"Use to explain a charge or a refund: every ledger row names what paid (eventId, sessionId, postId) and the balance after it."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okostream/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Control OKO Stream webinars from Claude, Codex, and other MCP clients.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": {
|
|
@@ -55,8 +55,13 @@
|
|
|
55
55
|
"firebase": "^12.16.0",
|
|
56
56
|
"ipaddr.js": "2.4.0",
|
|
57
57
|
"openai": "^6.46.0",
|
|
58
|
-
"typescript": "
|
|
58
|
+
"typescript": "~6.0.3",
|
|
59
59
|
"undici": "7.30.1",
|
|
60
60
|
"zod": "^3.25.76"
|
|
61
|
+
},
|
|
62
|
+
"allowScripts": {
|
|
63
|
+
"@firebase/util@1.15.3": true,
|
|
64
|
+
"esbuild@0.24.2": true,
|
|
65
|
+
"protobufjs@7.6.6": true
|
|
61
66
|
}
|
|
62
67
|
}
|