@okostream/mcp 0.12.14 → 0.12.16

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.
Files changed (2) hide show
  1. package/dist/server.js +76 -54
  2. 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.14";
116294
+ var OKO_MCP_VERSION = "0.12.16";
116295
116295
 
116296
116296
  // build/auth.js
116297
116297
  var state = null;
@@ -117212,15 +117212,39 @@ async function uploadMasterImage(eventId, kind, cardId, source) {
117212
117212
  });
117213
117213
  return result.data;
117214
117214
  }
117215
- async function updateCardImageInTransaction(eventId, cardId, patch) {
117215
+ function imageReplacedSinceRead(fresh, expected) {
117216
+ return fresh.imageMasterUrl !== expected.imageMasterUrl || fresh.imageUrl !== expected.imageUrl;
117217
+ }
117218
+ async function clearEventImageInTransaction(eventId, kind, cardId, expected) {
117216
117219
  const eventRef = doc(firestore, "events", eventId);
117217
117220
  await runTransaction2(firestore, async (transaction) => {
117218
117221
  const snap = await transaction.get(eventRef);
117219
117222
  if (!snap.exists())
117220
117223
  throw new Error("Event not found.");
117221
117224
  const data = snap.data();
117225
+ const fresh = readImageRef(data, kind, cardId);
117226
+ if (imageReplacedSinceRead(fresh, expected)) {
117227
+ throw new Error("The image changed between reading it and deleting it \u2014 someone replaced it in the meantime. Nothing was deleted and no file was removed. Read the event again and repeat the call if the new image should go too.");
117228
+ }
117229
+ if (kind === "card") {
117230
+ transaction.update(eventRef, {
117231
+ "blocks.cards.items": cardItemsWithImage(data, cardId, {
117232
+ imageUrl: null,
117233
+ imageMasterUrl: null,
117234
+ imageVariants: null,
117235
+ imageWidth: null,
117236
+ imageHeight: null
117237
+ })
117238
+ });
117239
+ return;
117240
+ }
117241
+ const prefix = kind === "promoBanner" ? "blocks.promoBanner" : "blocks.video.placeholder";
117222
117242
  transaction.update(eventRef, {
117223
- "blocks.cards.items": cardItemsWithImage(data, cardId, patch)
117243
+ [`${prefix}.imageMasterUrl`]: deleteField(),
117244
+ [`${prefix}.imageUrl`]: deleteField(),
117245
+ [`${prefix}.imageVariants`]: deleteField(),
117246
+ [`${prefix}.imageWidth`]: deleteField(),
117247
+ [`${prefix}.imageHeight`]: deleteField()
117224
117248
  });
117225
117249
  });
117226
117250
  }
@@ -117972,17 +117996,22 @@ function toMillis(value) {
117972
117996
  return null;
117973
117997
  }
117974
117998
  function parseTimestampCursor(cursor) {
117975
- const millis = Number(cursor);
117976
- if (Number.isFinite(millis) && millis > 0)
117977
- return Timestamp.fromMillis(millis);
117978
- const parsed = Date.parse(cursor);
117979
- if (Number.isFinite(parsed))
117980
- return Timestamp.fromMillis(parsed);
117981
- throw new Error("Invalid cursor. Use the nextCursor value returned by the previous page.");
117982
- }
117983
- function timestampCursorFrom(value) {
117999
+ const separator = cursor.indexOf("|");
118000
+ const head = separator === -1 ? cursor : cursor.slice(0, separator);
118001
+ const docId = separator === -1 ? "" : cursor.slice(separator + 1);
118002
+ const millis = Number(head);
118003
+ const at = Number.isFinite(millis) && millis > 0 ? millis : Date.parse(head);
118004
+ if (!Number.isFinite(at)) {
118005
+ throw new Error("Invalid cursor. Use the nextCursor value returned by the previous page.");
118006
+ }
118007
+ const boundary = Timestamp.fromMillis(at);
118008
+ return docId ? [boundary, docId] : [boundary];
118009
+ }
118010
+ function timestampCursorFrom(value, docId) {
117984
118011
  const millis = toMillis(value);
117985
- return millis == null ? null : String(millis);
118012
+ if (millis == null)
118013
+ return null;
118014
+ return docId ? `${millis}|${docId}` : String(millis);
117986
118015
  }
117987
118016
  var ISO_WITH_OFFSET = /(?:Z|[+-]\d{2}:?\d{2})$/;
117988
118017
  function parseIsoBound(value, label) {
@@ -118744,7 +118773,7 @@ function registerEventTools(server2) {
118744
118773
  limit(args.limit + 1)
118745
118774
  ];
118746
118775
  if (args.cursor)
118747
- constraints.splice(constraints.length - 1, 0, startAfter(parseTimestampCursor(args.cursor)));
118776
+ constraints.splice(constraints.length - 1, 0, startAfter(...parseTimestampCursor(args.cursor)));
118748
118777
  const snap = await getDocs(query2(collection(firestore, "eventAccess", uid, "grants"), ...constraints)).catch((error3) => {
118749
118778
  if (!isFirestorePermissionDenied(error3))
118750
118779
  throw error3;
@@ -118752,7 +118781,7 @@ function registerEventTools(server2) {
118752
118781
  });
118753
118782
  if (snap !== null) {
118754
118783
  const { page, hasMore } = pickPage(snap.docs, args.limit);
118755
- const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data()[orderField]) : null;
118784
+ const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data()[orderField], page.at(-1)?.id) : null;
118756
118785
  const pageItems = dateFiltered && args.workspaceId ? page.filter((item) => item.data().workspaceId === args.workspaceId) : page;
118757
118786
  let events;
118758
118787
  if (args.detail === "full") {
@@ -118783,7 +118812,7 @@ function registerEventTools(server2) {
118783
118812
  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.",
118784
118813
  examples: ["Use after oko_create_webinar to verify setup and return the event URL to the user."],
118785
118814
  notes: [
118786
- "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."
118815
+ "blocks.chat: the panel exists exactly when blocks.chat.enabled is true. blocks.chat.intake says whether it ACCEPTS MESSAGES before anyone touches the live control \u2014 closed (default), open, schedule (at the event time), or session (while an analytics session runs); oko_set_chat_mode overrides it live. blocks.chat.mode and blocks.chat.visibility are legacy and read by nobody \u2014 ignore those two."
118787
118816
  ]
118788
118817
  }),
118789
118818
  inputSchema: { eventId: eventIdField },
@@ -118828,7 +118857,7 @@ function registerEventTools(server2) {
118828
118857
  "workspaceId: optional workspace id; omitted means personal workspace.",
118829
118858
  "videoUrl: optional iframe/source/player URL; normalized by OKO.",
118830
118859
  "eventDate: optional ISO datetime.",
118831
- "chatEnabled: whether the chat block is enabled at creation."
118860
+ "chatEnabled: whether the chat block is on the page at creation. It does NOT open message sending: a new event starts with intake closed, so the chat is visible and silent until the organizer opens it (oko_set_chat_mode) or sets blocks.chat.intake."
118832
118861
  ],
118833
118862
  returns: "new event id, public event URL, and workspaceId.",
118834
118863
  examples: ["Use for a complete create flow, then call oko_configure_blocks and oko_get_event."],
@@ -118841,7 +118870,7 @@ function registerEventTools(server2) {
118841
118870
  eventDate: external_exports.string().datetime().nullable().optional().describe("Optional webinar datetime in ISO format."),
118842
118871
  speakerName: external_exports.string().max(160).nullable().optional().describe("Optional speaker display name."),
118843
118872
  description: external_exports.string().max(3e3).nullable().optional().describe('Optional event description shown under the speaker name. Omit or pass "" for none; the literal text "null" is rejected.'),
118844
- chatEnabled: external_exports.boolean().default(false).describe("Whether the chat block starts enabled.")
118873
+ chatEnabled: external_exports.boolean().default(false).describe("Whether the chat block is on the page at creation; message sending stays closed until opened.")
118845
118874
  },
118846
118875
  outputSchema: createWebinarOutputSchema,
118847
118876
  annotations: writeTool
@@ -118955,6 +118984,7 @@ function registerEventTools(server2) {
118955
118984
  returns: "event id and the list of Firestore fields updated.",
118956
118985
  examples: ["Use after oko_create_webinar to enable chat, bind a CTA/form, and configure other audience blocks."],
118957
118986
  notes: [
118987
+ '\u{1F534} "Turn off the chat" is ambiguous, and the two meanings are not close. chat.enabled=false REMOVES the chat block from the page together with every message people wrote \u2014 irreversible for the audience, and almost never what someone asks for mid-webinar. Stopping new messages is a different tool: oko_set_chat_mode with mode="off" leaves the block and the whole history in place. When the organizer says "turn off the chat", "close the chat", "stop the chat", "disable chat", assume they mean the mode, and ASK before removing the block. The same trap in reverse: "turn the chat back on" after a pause means mode="public", not this field.',
118958
118988
  "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.",
118959
118989
  "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.",
118960
118990
  '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.',
@@ -118965,7 +118995,11 @@ function registerEventTools(server2) {
118965
118995
  }),
118966
118996
  inputSchema: {
118967
118997
  eventId: eventIdField,
118968
- chat: external_exports.object({ enabled: external_exports.boolean().optional(), visibility: external_exports.enum(["always", "hidden"]).optional() }).optional(),
118998
+ chat: external_exports.object({
118999
+ enabled: external_exports.boolean().optional().describe("Whether the chat block is ON THE PAGE. false removes it together with the message history \u2014 see the note above before using it."),
119000
+ intake: external_exports.enum(["closed", "open", "schedule", "session"]).optional().describe("When the chat starts accepting messages before any live override: closed (default \u2014 visible and silent until opened), open, schedule (at the event time), session (while an analytics session runs)."),
119001
+ visibility: external_exports.enum(["always", "hidden"]).optional()
119002
+ }).optional(),
118969
119003
  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(),
118970
119004
  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(),
118971
119005
  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(),
@@ -118984,6 +119018,8 @@ function registerEventTools(server2) {
118984
119018
  const patch = {};
118985
119019
  if (args.chat?.enabled !== void 0)
118986
119020
  patch["blocks.chat.enabled"] = args.chat.enabled;
119021
+ if (args.chat?.intake !== void 0)
119022
+ patch["blocks.chat.intake"] = args.chat.intake;
118987
119023
  if (args.chat?.visibility !== void 0)
118988
119024
  patch["blocks.chat.visibility"] = args.chat.visibility;
118989
119025
  if (args.promoBanner?.enabled !== void 0)
@@ -119042,6 +119078,12 @@ function registerEventTools(server2) {
119042
119078
  if (args.chat?.visibility !== void 0) {
119043
119079
  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.");
119044
119080
  }
119081
+ if (args.chat?.enabled === false) {
119082
+ hints.push('The chat block is now off in setup: it is gone from the page for everyone, together with every message the audience wrote. If the goal was only to stop NEW messages \u2014 which is what "turn off the chat" usually means during or after a webinar \u2014 re-enable it here and use oko_set_chat_mode with mode "off" instead: the block and the whole history stay in place. Worth confirming with the organizer which of the two they meant.');
119083
+ }
119084
+ if (args.chat?.enabled === true && args.chat?.intake === void 0) {
119085
+ hints.push('The chat block is on the page, but message sending stays closed until it is opened: a chat nobody opened is silent by default. Open it now with oko_set_chat_mode mode "public", or set blocks.chat.intake to open / schedule / session so it opens by itself.');
119086
+ }
119045
119087
  const currentBlocks = current.blocks && typeof current.blocks === "object" ? current.blocks : {};
119046
119088
  const bannerHasImage = Boolean(safeString(currentBlocks.promoBanner?.imageUrl) || safeString(currentBlocks.promoBanner?.imageMasterUrl));
119047
119089
  if (args.promoBanner?.enabled === false && bannerHasImage) {
@@ -119339,31 +119381,10 @@ function registerImageTools(server2) {
119339
119381
  const event = await readEvent(eventId, uid, "edit");
119340
119382
  const previous = readImageRef(event, kind, cardId);
119341
119383
  requireConfirm(confirm, "oko_delete_image", { eventId, kind, cardId: cardId ?? null, imageId: previous.imageId });
119342
- if (kind === "promoBanner") {
119343
- await updateDoc(doc(firestore, "events", eventId), {
119344
- "blocks.promoBanner.imageMasterUrl": deleteField(),
119345
- "blocks.promoBanner.imageUrl": deleteField(),
119346
- "blocks.promoBanner.imageVariants": deleteField(),
119347
- "blocks.promoBanner.imageWidth": deleteField(),
119348
- "blocks.promoBanner.imageHeight": deleteField()
119349
- });
119350
- } else if (kind === "videoPlaceholder") {
119351
- await updateDoc(doc(firestore, "events", eventId), {
119352
- "blocks.video.placeholder.imageMasterUrl": deleteField(),
119353
- "blocks.video.placeholder.imageUrl": deleteField(),
119354
- "blocks.video.placeholder.imageVariants": deleteField(),
119355
- "blocks.video.placeholder.imageWidth": deleteField(),
119356
- "blocks.video.placeholder.imageHeight": deleteField()
119357
- });
119358
- } else {
119359
- await updateCardImageInTransaction(eventId, cardId, {
119360
- imageUrl: null,
119361
- imageMasterUrl: null,
119362
- imageVariants: null,
119363
- imageWidth: null,
119364
- imageHeight: null
119365
- });
119366
- }
119384
+ await clearEventImageInTransaction(eventId, kind, cardId, {
119385
+ imageMasterUrl: previous.imageMasterUrl,
119386
+ imageUrl: previous.imageUrl
119387
+ });
119367
119388
  const cleanup = await deleteStorageUrls(eventId, previous.imageMasterUrl, previous.imageUrl);
119368
119389
  return textResult({
119369
119390
  eventId,
@@ -120030,16 +120051,17 @@ function registerLiveTools(server2) {
120030
120051
  server2.registerTool("oko_set_chat_mode", {
120031
120052
  title: "Set chat mode",
120032
120053
  description: toolDescription({
120033
- summary: "Open or close chat sending on the public event page.",
120054
+ summary: "Open or close MESSAGE SENDING on the event page. The chat block itself stays where it is.",
120034
120055
  exampleCall: { "eventId": "Kx9pQ2mR7tL4vB8nW3cY", "mode": "off", "confirm": true },
120035
120056
  args: [
120036
120057
  "eventId: event to control.",
120037
- "mode: public (anyone can send), off (input closed, history stays visible), or null to reset to the product default (public).",
120058
+ "mode: public (anyone can send), off (input closed, history stays visible), or null to fall back to the event's own setting (blocks.chat.intake, closed unless the organizer chose otherwise).",
120038
120059
  "confirm: must be true."
120039
120060
  ],
120040
120061
  returns: "eventId and the effective chat mode.",
120041
- examples: ['Use mode="off" to pause a chat storm; use mode=null or "public" to reopen.'],
120062
+ examples: ['Use mode="off" to pause a chat storm; use mode="public" to reopen.'],
120042
120063
  notes: [
120064
+ '\u{1F534} "Turn off the chat" means one of TWO different things, and picking the wrong one destroys what people wrote. THIS tool stops new messages: the block stays on the page, history stays readable, nothing is lost. The other one \u2014 oko_configure_blocks with chat.enabled=false \u2014 removes the block from the page together with the whole conversation. When the organizer says "turn off the chat", "close the chat", "stop the chat", "mute the chat" during or after a webinar, they almost always mean THIS tool. Ask before removing the block.',
120043
120065
  'This is the full replacement for the legacy pause via oko_set_block_visibility(block="chat"): prefer oko_set_chat_mode.',
120044
120066
  "There are no other levels: the server has exactly two states, sending open or closed. History is always visible; the mode never restricts who can read or hides anyone's messages."
120045
120067
  ]
@@ -120049,7 +120071,7 @@ function registerLiveTools(server2) {
120049
120071
  // LD5a-2 / A8 (owner decision 03.09): "auth" and "owner" were promised
120050
120072
  // here and enforced nowhere — the rule and every client only check
120051
120073
  // `mode !== "off"`. The contract now says what the server does.
120052
- mode: external_exports.enum(["public", "off"]).nullable().describe("Chat mode: public or off; null resets the override to the default."),
120074
+ mode: external_exports.enum(["public", "off"]).nullable().describe("Chat mode: public or off; null drops the live override and falls back to the event setting."),
120053
120075
  confirm: external_exports.boolean().optional().describe("Required true to change the live chat mode.")
120054
120076
  },
120055
120077
  outputSchema: basicEventOutputSchema,
@@ -120126,14 +120148,14 @@ function registerNotificationTools(server2) {
120126
120148
  limit(fetchLimit)
120127
120149
  ];
120128
120150
  if (cursor)
120129
- constraints.splice(1, 0, startAfter(parseTimestampCursor(cursor)));
120151
+ constraints.splice(1, 0, startAfter(...parseTimestampCursor(cursor)));
120130
120152
  const snap = await getDocs(query2(collection(firestore, "userNotifications", uid, "items"), ...constraints));
120131
120153
  const matchingDocs = unreadOnly ? snap.docs.filter((item) => item.data().read !== true) : snap.docs;
120132
120154
  const { page, hasMore: hasMoreInMatches } = pickPage(matchingDocs, max);
120133
120155
  const hasMore = hasMoreInMatches || snap.docs.length >= fetchLimit;
120134
120156
  const cursorSource = hasMoreInMatches ? page.at(-1) : snap.docs.at(-1);
120135
120157
  const notifications = page.map((item) => ({ id: item.id, ...serialize(item.data()) }));
120136
- const nextCursor = hasMore ? timestampCursorFrom(cursorSource?.data().createdAt) : null;
120158
+ const nextCursor = hasMore ? timestampCursorFrom(cursorSource?.data().createdAt, cursorSource?.id) : null;
120137
120159
  return textResult({
120138
120160
  notifications,
120139
120161
  unreadCountOnPage: notifications.filter((item) => item.read !== true).length,
@@ -120459,11 +120481,11 @@ function registerSessionTools(server2) {
120459
120481
  limit(max + 1)
120460
120482
  ];
120461
120483
  if (cursor)
120462
- constraints.splice(1, 0, startAfter(parseTimestampCursor(cursor)));
120484
+ constraints.splice(1, 0, startAfter(...parseTimestampCursor(cursor)));
120463
120485
  const snap = await getDocs(query2(collection(firestore, "events", eventId, "analyticsSessions"), ...constraints));
120464
120486
  const { page, hasMore } = pickPage(snap.docs, max);
120465
120487
  const sessions = page.map((item) => compactAnalyticsSession({ id: item.id, ...item.data() }, { includeTimeline, timelineLimit, includeSnapshots }));
120466
- const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data().startedAt) : null;
120488
+ const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data().startedAt, page.at(-1)?.id) : null;
120467
120489
  return textResult({ sessions, ...cursorMeta(sessions.length, max, hasMore, nextCursor) }, { truncationHint: "Use limit/cursor or keep includeTimeline/includeSnapshots disabled." });
120468
120490
  });
120469
120491
  server2.registerTool("oko_get_session_report", {
@@ -120679,14 +120701,14 @@ function registerTelegramTools(server2) {
120679
120701
  limit(max + 1)
120680
120702
  ];
120681
120703
  if (cursor)
120682
- constraints.splice(2, 0, startAfter(parseTimestampCursor(cursor)));
120704
+ constraints.splice(2, 0, startAfter(...parseTimestampCursor(cursor)));
120683
120705
  const snap = await getDocs(query2(collection(firestore, "telegramPosts"), ...constraints));
120684
120706
  const matching = status ? snap.docs.filter((item) => item.data().status === status) : snap.docs;
120685
120707
  const { page, hasMore: hasMoreInMatches } = pickPage(matching, max);
120686
120708
  const hasMore = hasMoreInMatches || snap.docs.length >= max + 1;
120687
120709
  const cursorSource = hasMoreInMatches ? page.at(-1) : snap.docs.at(-1);
120688
120710
  const posts = page.map((item) => ({ id: item.id, ...serialize(item.data()) }));
120689
- const nextCursor = hasMore ? timestampCursorFrom(cursorSource?.data().createdAt) : null;
120711
+ const nextCursor = hasMore ? timestampCursorFrom(cursorSource?.data().createdAt, cursorSource?.id) : null;
120690
120712
  return textResult({ posts, ...cursorMeta(posts.length, max, hasMore, nextCursor) });
120691
120713
  });
120692
120714
  server2.registerTool("oko_cancel_telegram_post", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okostream/mcp",
3
- "version": "0.12.14",
3
+ "version": "0.12.16",
4
4
  "description": "Control OKO Stream webinars from Claude, Codex, and other MCP clients.",
5
5
  "license": "UNLICENSED",
6
6
  "author": {