@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.
- package/dist/server.js +76 -54
- 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.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
|
-
|
|
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
|
-
|
|
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
|
|
117976
|
-
|
|
117977
|
-
|
|
117978
|
-
const
|
|
117979
|
-
|
|
117980
|
-
|
|
117981
|
-
|
|
117982
|
-
}
|
|
117983
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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({
|
|
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
|
-
|
|
119343
|
-
|
|
119344
|
-
|
|
119345
|
-
|
|
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
|
|
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
|
|
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=
|
|
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
|
|
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", {
|