@okostream/mcp 0.12.10 → 0.12.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +128 -37
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -116291,7 +116291,7 @@ function sanitizeScopes(scopes) {
|
|
|
116291
116291
|
}
|
|
116292
116292
|
|
|
116293
116293
|
// build/version.js
|
|
116294
|
-
var OKO_MCP_VERSION = "0.12.
|
|
116294
|
+
var OKO_MCP_VERSION = "0.12.12";
|
|
116295
116295
|
|
|
116296
116296
|
// build/auth.js
|
|
116297
116297
|
var state = null;
|
|
@@ -116416,9 +116416,8 @@ async function exchangeApiKey() {
|
|
|
116416
116416
|
}
|
|
116417
116417
|
throw status === 401 ? new OkoAuthError("invalid_key", "OKO API key is invalid or revoked. Create a fresh key in the OKO MCP install center and replace OKO_API_KEY.") : new OkoAuthError("unavailable", [
|
|
116418
116418
|
`OKO organizer authentication failed with HTTP ${status}.`,
|
|
116419
|
-
serverRefusalNote(errorBody)
|
|
116420
|
-
|
|
116421
|
-
].filter(Boolean).join(" "));
|
|
116419
|
+
serverRefusalNote(errorBody) ?? "Check your connection and try again."
|
|
116420
|
+
].join(" "));
|
|
116422
116421
|
}
|
|
116423
116422
|
const data = await response.json();
|
|
116424
116423
|
if (typeof data.customToken !== "string") {
|
|
@@ -116631,6 +116630,20 @@ var EVENT_ACCESS_FIELD_BY_MODE = {
|
|
|
116631
116630
|
moderate: "chatModerators",
|
|
116632
116631
|
notify: "notificationSenders"
|
|
116633
116632
|
};
|
|
116633
|
+
var EVENT_ACCESS_COMPOSITION_FIELDS = [
|
|
116634
|
+
...Object.values(EVENT_ACCESS_FIELD_BY_MODE),
|
|
116635
|
+
"moderators",
|
|
116636
|
+
"participantUids"
|
|
116637
|
+
];
|
|
116638
|
+
function eventDocumentForOutput(data) {
|
|
116639
|
+
const projected = {};
|
|
116640
|
+
for (const [key, value] of Object.entries(data)) {
|
|
116641
|
+
if (EVENT_ACCESS_COMPOSITION_FIELDS.includes(key))
|
|
116642
|
+
continue;
|
|
116643
|
+
projected[key] = value;
|
|
116644
|
+
}
|
|
116645
|
+
return projected;
|
|
116646
|
+
}
|
|
116634
116647
|
function grantAllowsMode(data, mode) {
|
|
116635
116648
|
const field2 = EVENT_ACCESS_FIELD_BY_MODE[mode];
|
|
116636
116649
|
return asRecord(data.capabilities)[field2] === true;
|
|
@@ -117195,16 +117208,43 @@ async function updateCardImageInTransaction(eventId, cardId, patch) {
|
|
|
117195
117208
|
});
|
|
117196
117209
|
});
|
|
117197
117210
|
}
|
|
117198
|
-
|
|
117211
|
+
function descendPath(root, segments) {
|
|
117212
|
+
let current = root;
|
|
117213
|
+
for (const segment of segments) {
|
|
117214
|
+
if (current === null || typeof current !== "object")
|
|
117215
|
+
return void 0;
|
|
117216
|
+
current = current[segment];
|
|
117217
|
+
}
|
|
117218
|
+
return current;
|
|
117219
|
+
}
|
|
117220
|
+
function valueAfterPatch(fresh, patch, path) {
|
|
117221
|
+
if (Object.prototype.hasOwnProperty.call(patch, path))
|
|
117222
|
+
return patch[path];
|
|
117223
|
+
const segments = path.split(".");
|
|
117224
|
+
for (let cut = segments.length - 1; cut > 0; cut--) {
|
|
117225
|
+
const ancestor = segments.slice(0, cut).join(".");
|
|
117226
|
+
if (!Object.prototype.hasOwnProperty.call(patch, ancestor))
|
|
117227
|
+
continue;
|
|
117228
|
+
return descendPath(patch[ancestor], segments.slice(cut));
|
|
117229
|
+
}
|
|
117230
|
+
return descendPath(fresh, segments);
|
|
117231
|
+
}
|
|
117232
|
+
function scheduleMissesDate(sessionMode, eventDate) {
|
|
117233
|
+
return sessionMode === "schedule" && !eventDate;
|
|
117234
|
+
}
|
|
117235
|
+
async function writeEventPatchWithCards(eventId, basePatch, buildItems, validate = null) {
|
|
117199
117236
|
const eventRef = doc(firestore, "events", eventId);
|
|
117200
117237
|
await runTransaction2(firestore, async (transaction) => {
|
|
117201
117238
|
const snap = await transaction.get(eventRef);
|
|
117202
117239
|
if (!snap.exists())
|
|
117203
117240
|
throw new Error("Event not found.");
|
|
117241
|
+
const fresh = snap.data();
|
|
117204
117242
|
const patch = { ...basePatch };
|
|
117205
117243
|
if (buildItems) {
|
|
117206
|
-
patch["blocks.cards.items"] = buildItems(
|
|
117244
|
+
patch["blocks.cards.items"] = buildItems(fresh);
|
|
117207
117245
|
}
|
|
117246
|
+
if (validate)
|
|
117247
|
+
validate(fresh, patch);
|
|
117208
117248
|
if (Object.keys(patch).length === 0)
|
|
117209
117249
|
return;
|
|
117210
117250
|
transaction.update(eventRef, patch);
|
|
@@ -118425,6 +118465,9 @@ function validateEventSetup(eventId, event) {
|
|
|
118425
118465
|
} else if (eventDateMs < Date.now() - 60 * 60 * 1e3) {
|
|
118426
118466
|
add(warnings, "date_in_past", "meta.eventDate", "Configured webinar date appears to be in the past.");
|
|
118427
118467
|
}
|
|
118468
|
+
if (scheduleMissesDate(event.analyticsSessionMode, meta2.eventDate)) {
|
|
118469
|
+
add(errors, "schedule_missing_date", "analyticsSessionMode", "Session mode is 'schedule' but meta.eventDate is empty, so OKO can never start or end the session automatically and the reminder is never sent. Set eventDate, or switch sessionMode to 'manual' \u2014 oko_update_event accepts both fields in one call.");
|
|
118470
|
+
}
|
|
118428
118471
|
const duplicateFormIds = /* @__PURE__ */ new Set();
|
|
118429
118472
|
const seenFormIds = /* @__PURE__ */ new Set();
|
|
118430
118473
|
forms.forEach((form, index) => {
|
|
@@ -118540,6 +118583,55 @@ function telegramBlocksPatch(args) {
|
|
|
118540
118583
|
"telegram.channelIds": args.telegramChannelIds
|
|
118541
118584
|
};
|
|
118542
118585
|
}
|
|
118586
|
+
function assertScheduleHasDate(fresh, patch) {
|
|
118587
|
+
const missing = scheduleMissesDate(valueAfterPatch(fresh, patch, "analyticsSessionMode"), valueAfterPatch(fresh, patch, "meta.eventDate"));
|
|
118588
|
+
if (!missing)
|
|
118589
|
+
return;
|
|
118590
|
+
throw new Error("sessionMode 'schedule' requires meta.eventDate: without a date OKO can never start the session automatically. Set eventDate in the same call, or switch sessionMode to 'manual' in the same call to clear the date.");
|
|
118591
|
+
}
|
|
118592
|
+
var BLOCK_FORM_BINDING_FIELDS = [
|
|
118593
|
+
"blocks.promoBanner.formId",
|
|
118594
|
+
"blocks.primaryCta.formId"
|
|
118595
|
+
];
|
|
118596
|
+
function formIdSet(formsValue) {
|
|
118597
|
+
return new Set((Array.isArray(formsValue) ? formsValue : []).map((form) => typeof form?.id === "string" ? form.id : null).filter((id) => Boolean(id)));
|
|
118598
|
+
}
|
|
118599
|
+
function formBindingsAfterPatch(fresh, patch) {
|
|
118600
|
+
const bindings = [];
|
|
118601
|
+
for (const field2 of BLOCK_FORM_BINDING_FIELDS) {
|
|
118602
|
+
const value = valueAfterPatch(fresh, patch, field2);
|
|
118603
|
+
if (typeof value === "string" && value)
|
|
118604
|
+
bindings.push({ field: field2, formId: value });
|
|
118605
|
+
}
|
|
118606
|
+
const cards = valueAfterPatch(fresh, patch, "blocks.cards.items");
|
|
118607
|
+
if (Array.isArray(cards)) {
|
|
118608
|
+
cards.forEach((card, index) => {
|
|
118609
|
+
const formId = card && typeof card === "object" && !Array.isArray(card) ? card.formId : null;
|
|
118610
|
+
if (typeof formId === "string" && formId) {
|
|
118611
|
+
bindings.push({ field: `blocks.cards.items[${index}].formId`, formId });
|
|
118612
|
+
}
|
|
118613
|
+
});
|
|
118614
|
+
}
|
|
118615
|
+
return bindings;
|
|
118616
|
+
}
|
|
118617
|
+
function assertFormBindingsExist(fresh, patch) {
|
|
118618
|
+
const formsAfter = formIdSet(valueAfterPatch(fresh, patch, "forms"));
|
|
118619
|
+
for (const field2 of BLOCK_FORM_BINDING_FIELDS) {
|
|
118620
|
+
if (!Object.prototype.hasOwnProperty.call(patch, field2))
|
|
118621
|
+
continue;
|
|
118622
|
+
const value = patch[field2];
|
|
118623
|
+
if (typeof value === "string" && value && !formsAfter.has(value)) {
|
|
118624
|
+
throw new Error(`${field2}: form "${value}" not found in event.forms. Create the form in the same call (or beforehand) \u2014 a button bound to a missing form does nothing for viewers.`);
|
|
118625
|
+
}
|
|
118626
|
+
}
|
|
118627
|
+
const formsBefore = formIdSet(valueAfterPatch(fresh, {}, "forms"));
|
|
118628
|
+
const orphaned = formBindingsAfterPatch(fresh, patch).filter((binding) => !formsAfter.has(binding.formId) && formsBefore.has(binding.formId));
|
|
118629
|
+
if (orphaned.length === 0)
|
|
118630
|
+
return;
|
|
118631
|
+
const listed = orphaned.map((binding) => `${binding.field} -> "${binding.formId}"`).join(", ");
|
|
118632
|
+
const cardsAffected = orphaned.some((binding) => binding.field.startsWith("blocks.cards."));
|
|
118633
|
+
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." : "."));
|
|
118634
|
+
}
|
|
118543
118635
|
function configureBlocksScopes(args) {
|
|
118544
118636
|
return Object.keys(telegramBlocksPatch(args)).length > 0 ? ["setup:write", "telegram:write"] : ["setup:write"];
|
|
118545
118637
|
}
|
|
@@ -118552,7 +118644,7 @@ function registerEventTools(server2) {
|
|
|
118552
118644
|
"limit: maximum events to return, default 30, max 100.",
|
|
118553
118645
|
"cursor: pagination cursor returned as nextCursor by the previous page; reuse it with the same filters.",
|
|
118554
118646
|
"workspaceId: optional workspace filter.",
|
|
118555
|
-
'detail: "compact" (default) returns the light card projection; "full" returns
|
|
118647
|
+
'detail: "compact" (default) returns the light card projection; "full" returns event documents without the access composition, same shape as oko_get_event.',
|
|
118556
118648
|
"from/to: optional ISO timestamps bounding meta.eventDate; sorting switches to eventDate descending and undated events are excluded."
|
|
118557
118649
|
],
|
|
118558
118650
|
returns: "events plus count, limit, hasMore/has_more, nextCursor/next_cursor.",
|
|
@@ -118600,7 +118692,10 @@ function registerEventTools(server2) {
|
|
|
118600
118692
|
let events;
|
|
118601
118693
|
if (args.detail === "full") {
|
|
118602
118694
|
const eventDocuments = await Promise.all(pageItems.map((item) => getDoc(doc(firestore, "events", item.id))));
|
|
118603
|
-
events = eventDocuments.flatMap((item) => item.exists() ? [{
|
|
118695
|
+
events = eventDocuments.flatMap((item) => item.exists() ? [{
|
|
118696
|
+
id: item.id,
|
|
118697
|
+
...serialize(eventDocumentForOutput(item.data()))
|
|
118698
|
+
}] : []);
|
|
118604
118699
|
} else {
|
|
118605
118700
|
events = pageItems.map((item) => {
|
|
118606
118701
|
const data = serialize(item.data());
|
|
@@ -118620,7 +118715,7 @@ function registerEventTools(server2) {
|
|
|
118620
118715
|
description: toolDescription({
|
|
118621
118716
|
summary: "Read one OKO event visible to the organizer.",
|
|
118622
118717
|
args: ["eventId: OKO event document id."],
|
|
118623
|
-
returns: "
|
|
118718
|
+
returns: "event document fields plus id and public event URL. The event's access composition (the historical uid arrays) is never returned; ask oko_list_workspace_access for who has access.",
|
|
118624
118719
|
examples: ["Use after oko_create_webinar to verify setup and return the event URL to the user."],
|
|
118625
118720
|
notes: [
|
|
118626
118721
|
"blocks.chat.mode and blocks.chat.visibility are both legacy: the event page reads neither \u2014 the chat panel exists exactly when blocks.chat.enabled is true, and sending is opened or closed live with oko_set_chat_mode. Ignore both fields."
|
|
@@ -118632,7 +118727,11 @@ function registerEventTools(server2) {
|
|
|
118632
118727
|
}, async ({ eventId }) => {
|
|
118633
118728
|
const { uid } = await authed(["events:read"]);
|
|
118634
118729
|
const data = await readEvent(eventId, uid, "read");
|
|
118635
|
-
return textResult({
|
|
118730
|
+
return textResult({
|
|
118731
|
+
id: eventId,
|
|
118732
|
+
...serialize(eventDocumentForOutput(data)),
|
|
118733
|
+
...eventLinks(eventId)
|
|
118734
|
+
});
|
|
118636
118735
|
});
|
|
118637
118736
|
server2.registerTool("oko_validate_event_setup", {
|
|
118638
118737
|
title: "Validate event setup",
|
|
@@ -118719,7 +118818,8 @@ function registerEventTools(server2) {
|
|
|
118719
118818
|
returns: "event id and the list of Firestore fields updated.",
|
|
118720
118819
|
examples: ["Use for title/date/speaker/video edits; use oko_configure_blocks for blocks and forms."],
|
|
118721
118820
|
notes: [
|
|
118722
|
-
"sessionMode 'schedule' makes OKO start and end the analytics session automatically at eventDate for durationMinutes; 'manual' keeps the Start/End buttons and a Telegram reminder 10 minutes before eventDate. This is still an analytics session, not an event status."
|
|
118821
|
+
"sessionMode 'schedule' makes OKO start and end the analytics session automatically at eventDate for durationMinutes; 'manual' keeps the Start/End buttons and a Telegram reminder 10 minutes before eventDate. This is still an analytics session, not an event status.",
|
|
118822
|
+
"sessionMode 'schedule' and eventDate travel together: setting the mode without a date, or clearing the date while the mode stays, is refused. Change both in one call."
|
|
118723
118823
|
]
|
|
118724
118824
|
}),
|
|
118725
118825
|
inputSchema: {
|
|
@@ -118745,13 +118845,7 @@ function registerEventTools(server2) {
|
|
|
118745
118845
|
if (requiredScopes.size === 0)
|
|
118746
118846
|
throw new Error("Provide at least one field to update.");
|
|
118747
118847
|
const { uid } = await authed([...requiredScopes]);
|
|
118748
|
-
|
|
118749
|
-
if (args.sessionMode === "schedule") {
|
|
118750
|
-
const dateAfterPatch = args.eventDate !== void 0 ? args.eventDate : current.meta?.eventDate ?? null;
|
|
118751
|
-
if (!dateAfterPatch) {
|
|
118752
|
-
throw new Error("sessionMode 'schedule' requires meta.eventDate: without a date OKO can never start the session automatically. Set eventDate in the same call, or keep sessionMode 'manual'.");
|
|
118753
|
-
}
|
|
118754
|
-
}
|
|
118848
|
+
await readEvent(args.eventId, uid, "edit");
|
|
118755
118849
|
const patch = {};
|
|
118756
118850
|
if (args.title !== void 0)
|
|
118757
118851
|
patch.title = args.title.trim();
|
|
@@ -118776,7 +118870,7 @@ function registerEventTools(server2) {
|
|
|
118776
118870
|
patch.analyticsSessionMode = args.sessionMode;
|
|
118777
118871
|
if (args.durationMinutes !== void 0)
|
|
118778
118872
|
patch["meta.durationMinutes"] = args.durationMinutes;
|
|
118779
|
-
await
|
|
118873
|
+
await writeEventPatchWithCards(args.eventId, patch, null, assertScheduleHasDate);
|
|
118780
118874
|
return textResult({ id: args.eventId, updated: Object.keys(patch) });
|
|
118781
118875
|
});
|
|
118782
118876
|
server2.registerTool("oko_configure_blocks", {
|
|
@@ -118787,7 +118881,7 @@ function registerEventTools(server2) {
|
|
|
118787
118881
|
args: [
|
|
118788
118882
|
"eventId: event to edit.",
|
|
118789
118883
|
"chat/promoBanner/primaryCta/videoPlaceholder: optional block patches.",
|
|
118790
|
-
"forms: replaces configured lead forms when provided.",
|
|
118884
|
+
"forms: replaces configured lead forms when provided; removing a form that a button still points at is refused.",
|
|
118791
118885
|
"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."
|
|
118792
118886
|
],
|
|
118793
118887
|
returns: "event id and the list of Firestore fields updated.",
|
|
@@ -118797,7 +118891,8 @@ function registerEventTools(server2) {
|
|
|
118797
118891
|
"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.",
|
|
118798
118892
|
'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.',
|
|
118799
118893
|
"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.",
|
|
118800
|
-
"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."
|
|
118894
|
+
"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.",
|
|
118895
|
+
"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."
|
|
118801
118896
|
]
|
|
118802
118897
|
}),
|
|
118803
118898
|
inputSchema: {
|
|
@@ -118874,17 +118969,7 @@ function registerEventTools(server2) {
|
|
|
118874
118969
|
}))
|
|
118875
118970
|
}));
|
|
118876
118971
|
Object.assign(patch, telegramPatch);
|
|
118877
|
-
|
|
118878
|
-
const knownFormIds = new Set((Array.isArray(formsAfterPatch) ? formsAfterPatch : []).map((form) => typeof form?.id === "string" ? form.id : null).filter((id) => Boolean(id)));
|
|
118879
|
-
for (const [field2, value] of [
|
|
118880
|
-
["blocks.promoBanner.formId", patch["blocks.promoBanner.formId"]],
|
|
118881
|
-
["blocks.primaryCta.formId", patch["blocks.primaryCta.formId"]]
|
|
118882
|
-
]) {
|
|
118883
|
-
if (typeof value === "string" && value && !knownFormIds.has(value)) {
|
|
118884
|
-
throw new Error(`${field2}: form "${value}" not found in event.forms. Create the form in the same call (or beforehand) \u2014 a button bound to a missing form does nothing for viewers.`);
|
|
118885
|
-
}
|
|
118886
|
-
}
|
|
118887
|
-
await updateDoc(doc(firestore, "events", args.eventId), patch);
|
|
118972
|
+
await writeEventPatchWithCards(args.eventId, patch, null, assertFormBindingsExist);
|
|
118888
118973
|
const hints = [];
|
|
118889
118974
|
if (args.chat?.visibility !== void 0) {
|
|
118890
118975
|
hints.push("blocks.chat.visibility was stored but the event page does not read it: the chat panel exists exactly when blocks.chat.enabled is true, and sending is opened or closed live with oko_set_chat_mode. Drop the field.");
|
|
@@ -119759,6 +119844,7 @@ function registerLiveTools(server2) {
|
|
|
119759
119844
|
examples: ["Use visible=false to hide the CTA live; use visible=null to return to setup defaults."],
|
|
119760
119845
|
notes: [
|
|
119761
119846
|
"block=chat with visible=false PAUSES message input: viewers keep seeing the chat history but cannot send. It does not hide the chat panel.",
|
|
119847
|
+
"Chat is the only block whose override the SERVER enforces: it is written as the chat mode and mirrored into the chat security rules, so a paused chat refuses messages from every client. The other four overrides are page presentation only \u2014 they change what viewers see, not what the server accepts.",
|
|
119762
119848
|
"Live overrides sit on top of setup: a block disabled in setup (oko_configure_blocks enabled=false) stays absent whatever the override; visible=null returns an enabled block to its setup default. Use overrides for the live show, setup toggles for permanent changes."
|
|
119763
119849
|
]
|
|
119764
119850
|
}),
|
|
@@ -119813,10 +119899,10 @@ function registerLiveTools(server2) {
|
|
|
119813
119899
|
server2.registerTool("oko_refresh_viewers", {
|
|
119814
119900
|
title: "Refresh viewers",
|
|
119815
119901
|
description: toolDescription({
|
|
119816
|
-
summary: "
|
|
119902
|
+
summary: "Refresh what viewers see: recount the online viewers and make open event pages re-read their config.",
|
|
119817
119903
|
args: ["eventId: event whose viewers should refresh.", "confirm: must be true."],
|
|
119818
|
-
returns: "eventId
|
|
119819
|
-
examples: ["Use after a setup/live change if viewers should reload state immediately."]
|
|
119904
|
+
returns: "eventId, refreshed=true, and online \u2014 the recounted number of viewers present.",
|
|
119905
|
+
examples: ["Use after a setup/live change if viewers should reload state immediately.", "Use when the online counter looks wrong: it is recounted from the actual presence nodes."]
|
|
119820
119906
|
}),
|
|
119821
119907
|
inputSchema: { eventId: eventIdField, confirm: external_exports.boolean().optional().describe("Required true to refresh connected viewers.") },
|
|
119822
119908
|
outputSchema: basicEventOutputSchema,
|
|
@@ -119825,8 +119911,13 @@ function registerLiveTools(server2) {
|
|
|
119825
119911
|
const { uid } = await authed(["live:control"]);
|
|
119826
119912
|
await readEvent(eventId, uid, "live");
|
|
119827
119913
|
requireConfirm(confirm, "oko_refresh_viewers", { eventId });
|
|
119828
|
-
|
|
119829
|
-
|
|
119914
|
+
const resyncPresence = httpsCallable(functions, "resyncPresence");
|
|
119915
|
+
const [resynced] = await Promise.all([
|
|
119916
|
+
resyncPresence({ eventId }),
|
|
119917
|
+
runTransaction(ref(rtdb, `events/${eventId}/ctrl/refreshTick`), (current) => (current ?? 0) + 1)
|
|
119918
|
+
]);
|
|
119919
|
+
const online = typeof resynced.data?.online === "number" ? resynced.data.online : null;
|
|
119920
|
+
return textResult({ eventId, refreshed: true, online });
|
|
119830
119921
|
});
|
|
119831
119922
|
server2.registerTool("oko_set_live_badge", {
|
|
119832
119923
|
title: "Set live badge",
|