@okostream/mcp 0.16.0 → 0.17.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/dist/server.js +397 -49
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -116311,7 +116311,7 @@ function sanitizeScopes(scopes) {
|
|
|
116311
116311
|
}
|
|
116312
116312
|
|
|
116313
116313
|
// build/version.js
|
|
116314
|
-
var OKO_MCP_VERSION = "0.
|
|
116314
|
+
var OKO_MCP_VERSION = "0.17.0";
|
|
116315
116315
|
|
|
116316
116316
|
// build/auth.js
|
|
116317
116317
|
var state = null;
|
|
@@ -119025,6 +119025,10 @@ function buildCreateWebinarDocument(input) {
|
|
|
119025
119025
|
createdAt: input.createdAt,
|
|
119026
119026
|
title: input.title.trim(),
|
|
119027
119027
|
pinned: false,
|
|
119028
|
+
// The auto start is ON in every new page (release "sessions", `docs/planning/sessions-and-show-billing.md` §9.5):
|
|
119029
|
+
// the session starts by itself at the start of its plan — the event time until Setup sets another. The flag is a
|
|
119030
|
+
// page setting (organizer-writable), not server state; the canary writes this exact shape through the rules.
|
|
119031
|
+
analyticsSessionMode: "schedule",
|
|
119028
119032
|
video: input.video,
|
|
119029
119033
|
meta: {
|
|
119030
119034
|
eventDate: input.eventDate,
|
|
@@ -119045,6 +119049,12 @@ function buildCreateWebinarDocument(input) {
|
|
|
119045
119049
|
}
|
|
119046
119050
|
};
|
|
119047
119051
|
}
|
|
119052
|
+
function withoutSessionPlan(meta2) {
|
|
119053
|
+
if (!meta2 || typeof meta2 !== "object" || Array.isArray(meta2))
|
|
119054
|
+
return void 0;
|
|
119055
|
+
const { sessionStartAt: _start, sessionEndAt: _end, ...rest } = meta2;
|
|
119056
|
+
return rest;
|
|
119057
|
+
}
|
|
119048
119058
|
var DUPLICATE_TITLE_SUFFIX = " (copy)";
|
|
119049
119059
|
var MAX_EVENT_TITLE_LENGTH = 300;
|
|
119050
119060
|
function duplicateEventTitle(sourceTitle) {
|
|
@@ -119060,8 +119070,13 @@ function buildDuplicateEventDocument(input) {
|
|
|
119060
119070
|
title: duplicateEventTitle(safeString(source.title, "Untitled event")),
|
|
119061
119071
|
duplicatedFrom: input.sourceEventId,
|
|
119062
119072
|
pinned: false,
|
|
119073
|
+
// A copy is a NEW page: the auto start is ON like everywhere else (§9.5), whatever the source's mode was.
|
|
119074
|
+
analyticsSessionMode: "schedule",
|
|
119063
119075
|
video: source.video ?? { originalUrl: null, embedUrl: null },
|
|
119064
|
-
meta
|
|
119076
|
+
// 🔴 The session plan (`meta.sessionStartAt`/`sessionEndAt`) is NOT copied (R6): a copy is a second show with another
|
|
119077
|
+
// date, and a plan set by hand for the old date would read as «the session time does not match the show» on a page
|
|
119078
|
+
// that never had one. The copy's plan is its event time until Setup sets another.
|
|
119079
|
+
meta: withoutSessionPlan(source.meta) ?? { eventDate: null, speakerName: null, description: null },
|
|
119065
119080
|
blocks: source.blocks ?? {},
|
|
119066
119081
|
...Array.isArray(source.forms) ? { forms: source.forms } : {},
|
|
119067
119082
|
// 🔴 The copy does NOT inherit server-owned Telegram markers.
|
|
@@ -119105,6 +119120,124 @@ function eventRuntimeHint(runtime) {
|
|
|
119105
119120
|
];
|
|
119106
119121
|
}
|
|
119107
119122
|
|
|
119123
|
+
// build/tools/scheduleWindow.js
|
|
119124
|
+
var SCHEDULED_SESSION_DEFAULT_MINUTES = 120;
|
|
119125
|
+
var SCHEDULED_SESSION_MIN_MINUTES = 15;
|
|
119126
|
+
var SCHEDULED_SESSION_MAX_MINUTES = 1440;
|
|
119127
|
+
function scheduledDurationMs(raw) {
|
|
119128
|
+
const minutes = typeof raw === "number" && Number.isFinite(raw) ? Math.min(SCHEDULED_SESSION_MAX_MINUTES, Math.max(SCHEDULED_SESSION_MIN_MINUTES, Math.round(raw))) : SCHEDULED_SESSION_DEFAULT_MINUTES;
|
|
119129
|
+
return minutes * 60 * 1e3;
|
|
119130
|
+
}
|
|
119131
|
+
function scheduleWindowPassed(sessionMode, eventDateMs, rawDurationMinutes, nowMs) {
|
|
119132
|
+
if (sessionMode !== "schedule")
|
|
119133
|
+
return false;
|
|
119134
|
+
if (eventDateMs === null)
|
|
119135
|
+
return false;
|
|
119136
|
+
return nowMs >= eventDateMs + scheduledDurationMs(rawDurationMinutes);
|
|
119137
|
+
}
|
|
119138
|
+
|
|
119139
|
+
// build/tools/sessionPlan.js
|
|
119140
|
+
function isRecord(value) {
|
|
119141
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
119142
|
+
}
|
|
119143
|
+
function timestampMillis(value) {
|
|
119144
|
+
if (value instanceof Date)
|
|
119145
|
+
return value.getTime();
|
|
119146
|
+
if (value && typeof value === "object" && typeof value.toMillis === "function") {
|
|
119147
|
+
return value.toMillis();
|
|
119148
|
+
}
|
|
119149
|
+
return null;
|
|
119150
|
+
}
|
|
119151
|
+
function readSessionPlan(event, durationMsOf) {
|
|
119152
|
+
const meta2 = isRecord(event.meta) ? event.meta : {};
|
|
119153
|
+
const startMs = timestampMillis(meta2.sessionStartAt);
|
|
119154
|
+
const endMs = timestampMillis(meta2.sessionEndAt);
|
|
119155
|
+
if (startMs !== null && endMs !== null && endMs > startMs)
|
|
119156
|
+
return { startMs, endMs, source: "meta" };
|
|
119157
|
+
const eventDateMs = timestampMillis(meta2.eventDate);
|
|
119158
|
+
if (eventDateMs === null)
|
|
119159
|
+
return null;
|
|
119160
|
+
return { startMs: eventDateMs, endMs: eventDateMs + durationMsOf(meta2.durationMinutes), source: "event" };
|
|
119161
|
+
}
|
|
119162
|
+
function sessionPlanOf(event) {
|
|
119163
|
+
return readSessionPlan(event, scheduledDurationMs);
|
|
119164
|
+
}
|
|
119165
|
+
var iso = (ms) => new Date(ms).toISOString();
|
|
119166
|
+
function sessionPlanWarnings(event) {
|
|
119167
|
+
const plan = sessionPlanOf(event);
|
|
119168
|
+
if (!plan || plan.source !== "meta")
|
|
119169
|
+
return [];
|
|
119170
|
+
const meta2 = isRecord(event.meta) ? event.meta : {};
|
|
119171
|
+
const eventDateMs = timestampMillis(meta2.eventDate);
|
|
119172
|
+
if (eventDateMs === null)
|
|
119173
|
+
return [];
|
|
119174
|
+
const eventEndMs = eventDateMs + scheduledDurationMs(meta2.durationMinutes);
|
|
119175
|
+
if (plan.startMs === eventDateMs && plan.endMs === eventEndMs)
|
|
119176
|
+
return [];
|
|
119177
|
+
return [{
|
|
119178
|
+
code: "session_plan_differs_from_event",
|
|
119179
|
+
message: `The session plan (${iso(plan.startMs)} to ${iso(plan.endMs)}) is not the event time (${iso(eventDateMs)} to ${iso(eventEndMs)}). The automatic start and the automatic stop follow the PLAN. If the event moved and the session should follow it, clear the plan with oko_update_event sessionStartAt: null and sessionEndAt: null (the plan then equals the event time).`
|
|
119180
|
+
}];
|
|
119181
|
+
}
|
|
119182
|
+
function sessionOutlookWarnings(outlook) {
|
|
119183
|
+
if (outlook.moneyMode === "off")
|
|
119184
|
+
return [];
|
|
119185
|
+
const warnings = [];
|
|
119186
|
+
if (!outlook.entitlement.ok) {
|
|
119187
|
+
warnings.push({
|
|
119188
|
+
code: "session_tier_has_no_sessions",
|
|
119189
|
+
message: outlook.entitlement.reason === "tier" ? `The plan that pays for this page (${outlook.entitlement.currentTier ?? "unknown"}) has no analytics sessions: they come with ${outlook.entitlement.requiredTier ?? "pro"} and above. Start is refused, and the automatic start is skipped silently.` : "The subscription that pays for this page is not active, so Start is refused and the automatic start will not happen. The subscription owner can renew it in the OKO cabinet."
|
|
119190
|
+
});
|
|
119191
|
+
return warnings;
|
|
119192
|
+
}
|
|
119193
|
+
if (outlook.lumens.enoughForPlan === false && outlook.lumens.neededForPlan !== null) {
|
|
119194
|
+
warnings.push({
|
|
119195
|
+
code: "session_lumens_not_enough_for_plan",
|
|
119196
|
+
message: `A session that runs to the end of its plan costs about ${outlook.lumens.neededForPlan} lumens (1 per minute, taken when it ends). The wallet that pays for this workspace holds less: the session would be stopped when the lumens run out. Its owner can top it up. Lumens are checked at Start (a wallet at zero refuses it) and every minute while it runs.`
|
|
119197
|
+
});
|
|
119198
|
+
}
|
|
119199
|
+
return warnings;
|
|
119200
|
+
}
|
|
119201
|
+
function sessionExplanation(args) {
|
|
119202
|
+
const lines = [];
|
|
119203
|
+
if (args.billingMode === "metered") {
|
|
119204
|
+
lines.push("A session costs 1 lumen per minute, taken from the wallet that pays when the session ends (minutes are rounded; under 30 seconds is free). There is no debt: if the wallet runs dry the session is stopped and the rest is forgiven.");
|
|
119205
|
+
} else if (args.billingMode === "free") {
|
|
119206
|
+
lines.push("Session minutes are not being charged for this page right now.");
|
|
119207
|
+
}
|
|
119208
|
+
if (typeof args.plannedEndAt === "number") {
|
|
119209
|
+
lines.push(`The session ends by itself at ${iso(args.plannedEndAt)} (its plan). Move the end with oko_extend_session, or end it earlier with oko_end_session.`);
|
|
119210
|
+
}
|
|
119211
|
+
if (args.clamped) {
|
|
119212
|
+
lines.push("The requested end was moved back to the end of the page's show window: a session cannot run past it.");
|
|
119213
|
+
}
|
|
119214
|
+
return lines;
|
|
119215
|
+
}
|
|
119216
|
+
|
|
119217
|
+
// build/tools/sessionOutlook.js
|
|
119218
|
+
async function fetchSessionOutlook(eventId) {
|
|
119219
|
+
try {
|
|
119220
|
+
const preview = httpsCallable(functions, "previewAnalyticsSession");
|
|
119221
|
+
const result = await preview({ eventId });
|
|
119222
|
+
return { outlook: result.data };
|
|
119223
|
+
} catch (error3) {
|
|
119224
|
+
return { error: error3 instanceof Error ? error3.message : String(error3) };
|
|
119225
|
+
}
|
|
119226
|
+
}
|
|
119227
|
+
async function collectSessionWarnings(eventId, event) {
|
|
119228
|
+
const warnings = sessionPlanWarnings(event);
|
|
119229
|
+
const fetched = await fetchSessionOutlook(eventId);
|
|
119230
|
+
if ("outlook" in fetched) {
|
|
119231
|
+
warnings.push(...sessionOutlookWarnings(fetched.outlook));
|
|
119232
|
+
} else {
|
|
119233
|
+
warnings.push({
|
|
119234
|
+
code: "session_outlook_unavailable",
|
|
119235
|
+
message: `Could not check the payer's plan and lumens for this page's session (${fetched.error}). The write itself went through.`
|
|
119236
|
+
});
|
|
119237
|
+
}
|
|
119238
|
+
return warnings;
|
|
119239
|
+
}
|
|
119240
|
+
|
|
119108
119241
|
// build/tools/liveLayer.js
|
|
119109
119242
|
var LIVE_FIELDS_HINT = "blocks.promoBanner.visibility, blocks.primaryCta.visibility, blocks.cards.visibility and blocks.video.placeholder.enabled are SETUP values. What viewers see right now is `live.<block>.visibleNow` \u2014 `decidedBy` says whether a live override (oko_set_block_visibility) or the setup default decided it. For the chat, `live.chat.accepting` is whether viewers can send a message right now; `blocks.chat.intake` is only the starting state. The live badge and the active analytics session are not resolved here: read oko_get_live_state (ctrl.liveBadge, ctrl.activeSessionId).";
|
|
119110
119243
|
var liveUnreadableHint = (reason) => `blocks.*.visibility and .enabled above are SETUP values, not what viewers see now \u2014 and the live overrides could not be read (${reason}; a key without analytics:read or live:control is always refused here). Do not report a block as shown or hidden from these fields; read oko_get_live_state with a key that holds the scope.`;
|
|
@@ -119166,23 +119299,15 @@ async function readLiveLayer(eventId, data, nowMs) {
|
|
|
119166
119299
|
return { live, hints };
|
|
119167
119300
|
}
|
|
119168
119301
|
|
|
119169
|
-
// build/tools/
|
|
119170
|
-
|
|
119171
|
-
|
|
119172
|
-
var SCHEDULED_SESSION_MAX_MINUTES = 1440;
|
|
119173
|
-
function scheduledDurationMs(raw) {
|
|
119174
|
-
const minutes = typeof raw === "number" && Number.isFinite(raw) ? Math.min(SCHEDULED_SESSION_MAX_MINUTES, Math.max(SCHEDULED_SESSION_MIN_MINUTES, Math.round(raw))) : SCHEDULED_SESSION_DEFAULT_MINUTES;
|
|
119175
|
-
return minutes * 60 * 1e3;
|
|
119176
|
-
}
|
|
119177
|
-
function scheduleWindowPassed(sessionMode, eventDateMs, rawDurationMinutes, nowMs) {
|
|
119178
|
-
if (sessionMode !== "schedule")
|
|
119179
|
-
return false;
|
|
119180
|
-
if (eventDateMs === null)
|
|
119302
|
+
// build/tools/setupValidation.js
|
|
119303
|
+
function scheduleWindowPassedByPlan(event, eventDateMs, rawDurationMinutes, nowMs) {
|
|
119304
|
+
if (event.analyticsSessionMode !== "schedule")
|
|
119181
119305
|
return false;
|
|
119182
|
-
|
|
119306
|
+
const plan = sessionPlanOf(event);
|
|
119307
|
+
if (plan && plan.source === "meta")
|
|
119308
|
+
return nowMs >= plan.endMs;
|
|
119309
|
+
return scheduleWindowPassed(event.analyticsSessionMode, eventDateMs, rawDurationMinutes, nowMs);
|
|
119183
119310
|
}
|
|
119184
|
-
|
|
119185
|
-
// build/tools/setupValidation.js
|
|
119186
119311
|
function validateEventSetup(eventId, event, nowMs = Date.now()) {
|
|
119187
119312
|
const errors = [];
|
|
119188
119313
|
const warnings = [];
|
|
@@ -119204,13 +119329,16 @@ function validateEventSetup(eventId, event, nowMs = Date.now()) {
|
|
|
119204
119329
|
const eventDateMs = toMillis(meta2.eventDate);
|
|
119205
119330
|
if (eventDateMs == null) {
|
|
119206
119331
|
add(suggestions, "date_missing", "meta.eventDate", "Add the webinar date/time if it is known.");
|
|
119207
|
-
} else if (
|
|
119208
|
-
add(errors, "schedule_window_passed", "meta.eventDate", "
|
|
119332
|
+
} else if (scheduleWindowPassedByPlan(event, eventDateMs, meta2.durationMinutes, nowMs)) {
|
|
119333
|
+
add(errors, "schedule_window_passed", "meta.eventDate", "The auto start is on ('schedule') and the session plan (the session time set in Setup, else eventDate + durationMinutes) has already passed, so OKO will not start the analytics session automatically. If the webinar has not taken place yet, set a future eventDate (or session time) with oko_update_event, or start the session by hand with oko_start_session. If it has already taken place, this is expected.");
|
|
119209
119334
|
} else if (event.analyticsSessionMode !== "schedule" && eventDateMs < nowMs - 60 * 60 * 1e3) {
|
|
119210
119335
|
add(warnings, "date_in_past", "meta.eventDate", "Configured webinar date appears to be in the past.");
|
|
119211
119336
|
}
|
|
119212
119337
|
if (scheduleMissesDate(event.analyticsSessionMode, meta2.eventDate)) {
|
|
119213
|
-
add(
|
|
119338
|
+
add(warnings, "schedule_missing_date", "meta.eventDate", "Add the event date: without a date the session does not start by itself (the auto start is on, 'schedule', and OKO has no session time to start it at), and a page cannot be published without a date. A page is born this way until the date is set \u2014 this is a normal draft, not an error. Set meta.eventDate, or switch sessionMode to 'manual' \u2014 oko_update_event accepts both fields in one call.");
|
|
119339
|
+
}
|
|
119340
|
+
for (const warning of sessionPlanWarnings(event)) {
|
|
119341
|
+
add(warnings, warning.code, "meta.sessionStartAt", warning.message);
|
|
119214
119342
|
}
|
|
119215
119343
|
const duplicateFormIds = /* @__PURE__ */ new Set();
|
|
119216
119344
|
const seenFormIds = /* @__PURE__ */ new Set();
|
|
@@ -119349,25 +119477,25 @@ function stateWithin(now, window2) {
|
|
|
119349
119477
|
return "open";
|
|
119350
119478
|
return "after";
|
|
119351
119479
|
}
|
|
119352
|
-
function
|
|
119480
|
+
function isRecord2(value) {
|
|
119353
119481
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
119354
119482
|
}
|
|
119355
119483
|
function toMillis2(value) {
|
|
119356
119484
|
if (typeof value === "number" && Number.isFinite(value))
|
|
119357
119485
|
return value;
|
|
119358
|
-
if (
|
|
119486
|
+
if (isRecord2(value) && typeof value.toMillis === "function") {
|
|
119359
119487
|
const ms = value.toMillis();
|
|
119360
119488
|
return typeof ms === "number" && Number.isFinite(ms) ? ms : null;
|
|
119361
119489
|
}
|
|
119362
119490
|
return null;
|
|
119363
119491
|
}
|
|
119364
119492
|
function windowFromEvent(event, nowMs) {
|
|
119365
|
-
const billing =
|
|
119493
|
+
const billing = isRecord2(event.billing) ? event.billing : {};
|
|
119366
119494
|
const storedStart = toMillis2(billing.windowStart);
|
|
119367
119495
|
const storedEnd = toMillis2(billing.windowEnd);
|
|
119368
119496
|
let window2 = storedStart !== null && storedEnd !== null ? { windowStart: new Date(storedStart), windowEnd: new Date(storedEnd) } : null;
|
|
119369
119497
|
if (!window2) {
|
|
119370
|
-
const meta2 =
|
|
119498
|
+
const meta2 = isRecord2(event.meta) ? event.meta : {};
|
|
119371
119499
|
const eventDateMs = toMillis2(meta2.eventDate);
|
|
119372
119500
|
if (eventDateMs !== null)
|
|
119373
119501
|
window2 = showWindow(new Date(eventDateMs));
|
|
@@ -119385,11 +119513,28 @@ function telegramBlocksPatch(args) {
|
|
|
119385
119513
|
};
|
|
119386
119514
|
}
|
|
119387
119515
|
function assertScheduleHasDate(fresh, patch) {
|
|
119516
|
+
const touchesModeOrDate = "analyticsSessionMode" in patch || "meta.eventDate" in patch || "meta" in patch;
|
|
119517
|
+
if (!touchesModeOrDate)
|
|
119518
|
+
return;
|
|
119388
119519
|
const missing = scheduleMissesDate(valueAfterPatch(fresh, patch, "analyticsSessionMode"), valueAfterPatch(fresh, patch, "meta.eventDate"));
|
|
119389
119520
|
if (!missing)
|
|
119390
119521
|
return;
|
|
119391
119522
|
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.");
|
|
119392
119523
|
}
|
|
119524
|
+
function assertSessionPlanValid(fresh, patch) {
|
|
119525
|
+
if (!("meta.sessionStartAt" in patch) && !("meta.sessionEndAt" in patch) && !("meta" in patch))
|
|
119526
|
+
return;
|
|
119527
|
+
const start = toMillis(valueAfterPatch(fresh, patch, "meta.sessionStartAt"));
|
|
119528
|
+
const end = toMillis(valueAfterPatch(fresh, patch, "meta.sessionEndAt"));
|
|
119529
|
+
if (start === null && end === null)
|
|
119530
|
+
return;
|
|
119531
|
+
if (start === null || end === null) {
|
|
119532
|
+
throw new Error("The session plan needs BOTH sessionStartAt and sessionEndAt: pass the pair in one call, or pass null for both to follow the event time.");
|
|
119533
|
+
}
|
|
119534
|
+
if (end <= start) {
|
|
119535
|
+
throw new Error("sessionEndAt must be later than sessionStartAt.");
|
|
119536
|
+
}
|
|
119537
|
+
}
|
|
119393
119538
|
var DATE_LOCKED_BILLING_STATES = /* @__PURE__ */ new Set(["published", "held"]);
|
|
119394
119539
|
function eventDateLockedRefusal(eventId, windowOpened) {
|
|
119395
119540
|
return {
|
|
@@ -119583,7 +119728,10 @@ function registerEventTools(server2) {
|
|
|
119583
119728
|
"Use after creating/configuring a webinar and before telling the organizer it is ready.",
|
|
119584
119729
|
"Use before live-session prep to catch missing video, pending image variants, and invalid form bindings."
|
|
119585
119730
|
],
|
|
119586
|
-
notes: [
|
|
119731
|
+
notes: [
|
|
119732
|
+
"This tool does not modify the event and does not start/end analytics sessions.",
|
|
119733
|
+
"Session warnings (from the page plan and, when the server answers, the payer's plan and lumens): the session plan differs from the event time \xB7 the lumens will not last the plan \xB7 the plan that pays has no sessions. They are warnings, not errors: the page can be published and shown either way."
|
|
119734
|
+
]
|
|
119587
119735
|
}),
|
|
119588
119736
|
inputSchema: { eventId: eventIdField },
|
|
119589
119737
|
outputSchema: setupValidationOutputSchema,
|
|
@@ -119591,7 +119739,16 @@ function registerEventTools(server2) {
|
|
|
119591
119739
|
}, async ({ eventId }) => {
|
|
119592
119740
|
const { uid } = await authed(["events:read"]);
|
|
119593
119741
|
const data = await readEvent(eventId, uid, "read");
|
|
119594
|
-
|
|
119742
|
+
const report = validateEventSetup(eventId, data, await serverNowMs());
|
|
119743
|
+
const fetched = await fetchSessionOutlook(eventId);
|
|
119744
|
+
const outlookWarnings = "outlook" in fetched ? sessionOutlookWarnings(fetched.outlook) : [{ code: "session_outlook_unavailable", message: `Could not check the payer's plan and lumens for this page's session (${fetched.error}).` }];
|
|
119745
|
+
return textResult({
|
|
119746
|
+
...report,
|
|
119747
|
+
warnings: [
|
|
119748
|
+
...report.warnings,
|
|
119749
|
+
...outlookWarnings.map((warning) => ({ code: warning.code, path: "analyticsSessionMode", message: warning.message }))
|
|
119750
|
+
]
|
|
119751
|
+
});
|
|
119595
119752
|
});
|
|
119596
119753
|
server2.registerTool("oko_create_webinar", {
|
|
119597
119754
|
title: "Create webinar",
|
|
@@ -119642,12 +119799,15 @@ function registerEventTools(server2) {
|
|
|
119642
119799
|
createdAt: serverTimestamp2()
|
|
119643
119800
|
}));
|
|
119644
119801
|
const docRef = created.ref;
|
|
119802
|
+
const sessionWarnings = await collectSessionWarnings(docRef.id, { meta: { eventDate: args.eventDate ? Timestamp.fromDate(new Date(args.eventDate)) : null } });
|
|
119645
119803
|
return textResult({
|
|
119646
119804
|
id: docRef.id,
|
|
119647
119805
|
...eventLinks(docRef.id),
|
|
119648
119806
|
workspaceId,
|
|
119649
119807
|
runtime: created.runtime,
|
|
119808
|
+
warnings: sessionWarnings,
|
|
119650
119809
|
hints: [
|
|
119810
|
+
"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).",
|
|
119651
119811
|
...eventRuntimeHint(created.runtime),
|
|
119652
119812
|
`The page is a draft: viewers of publicUrl see "not published" (a legacy-tier payer's page is published at creation). Do not hand publicUrl out as live until oko_publish_event has published it \u2014 quote first, then confirm with the organizer.`,
|
|
119653
119813
|
"Next: oko_configure_blocks for chat/CTA/placeholder, oko_configure_cards for cards, image uploads, oko_validate_event_setup, then oko_publish_event.",
|
|
@@ -119660,12 +119820,14 @@ function registerEventTools(server2) {
|
|
|
119660
119820
|
description: toolDescription({
|
|
119661
119821
|
summary: "Update safe top-level event/setup metadata fields.",
|
|
119662
119822
|
exampleCall: { "eventId": "Kx9pQ2mR7tL4vB8nW3cY", "title": "Autumn product webinar (rescheduled)", "eventDate": "2026-09-12T07:00:00.000Z", "speakerName": "Jane Doe", "description": "", "durationMinutes": 60 },
|
|
119663
|
-
args: ["eventId plus any subset of title, videoUrl, eventDate, speakerName, description, pinned, sessionMode, durationMinutes."],
|
|
119664
|
-
returns: "event id
|
|
119823
|
+
args: ["eventId plus any subset of title, videoUrl, eventDate, speakerName, description, pinned, sessionMode, durationMinutes, sessionStartAt, sessionEndAt."],
|
|
119824
|
+
returns: "event id, the list of Firestore fields updated, and session warnings (plan versus event time, lumens versus plan, a plan with no sessions).",
|
|
119665
119825
|
examples: ["Use for title/date/speaker/video edits; use oko_configure_blocks for blocks and forms."],
|
|
119666
119826
|
notes: [
|
|
119667
|
-
"
|
|
119668
|
-
"sessionMode 'schedule'
|
|
119827
|
+
"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).",
|
|
119828
|
+
"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.",
|
|
119829
|
+
"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).",
|
|
119830
|
+
"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.",
|
|
119669
119831
|
'eventDate is LOCKED once the page is published and paid (not on the transition plan): changing it is refused with errorCode EVENT_DATE_LOCKED \u2014 every other field in the same call still goes through if eventDate is dropped from it. Before the show window opens: oko_unpublish_event (refunds), change the date, oko_publish_event (new charge). After the window opens, or once the page shows "held"/\u041F\u0440\u043E\u0432\u0435\u0434\u0435\u043D\u043E: the date cannot move at all \u2014 duplicate the page with oko_duplicate_event for the new date.'
|
|
119670
119832
|
]
|
|
119671
119833
|
}),
|
|
@@ -119677,8 +119839,10 @@ function registerEventTools(server2) {
|
|
|
119677
119839
|
speakerName: external_exports.string().max(160).nullable().optional().describe('New speaker display name; pass "" or null to clear (the literal text "null" is rejected).'),
|
|
119678
119840
|
description: external_exports.string().max(3e3).nullable().optional().describe('New description; pass "" or null to clear (the literal text "null" is rejected).'),
|
|
119679
119841
|
pinned: external_exports.boolean().optional().describe("Whether this event is pinned in the dashboard."),
|
|
119680
|
-
sessionMode: external_exports.enum(["manual", "schedule"]).optional().describe("
|
|
119681
|
-
durationMinutes: external_exports.number().int().min(15).max(1440).optional().describe("Event duration in minutes (15-1440)
|
|
119842
|
+
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."),
|
|
119843
|
+
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."),
|
|
119844
|
+
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."),
|
|
119845
|
+
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.")
|
|
119682
119846
|
},
|
|
119683
119847
|
outputSchema: updateOutputSchema,
|
|
119684
119848
|
annotations: writeTool
|
|
@@ -119686,7 +119850,7 @@ function registerEventTools(server2) {
|
|
|
119686
119850
|
const requiredScopes = /* @__PURE__ */ new Set();
|
|
119687
119851
|
if (args.title !== void 0 || args.pinned !== void 0)
|
|
119688
119852
|
requiredScopes.add("events:write");
|
|
119689
|
-
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) {
|
|
119853
|
+
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) {
|
|
119690
119854
|
requiredScopes.add("setup:write");
|
|
119691
119855
|
}
|
|
119692
119856
|
if (requiredScopes.size === 0)
|
|
@@ -119717,14 +119881,21 @@ function registerEventTools(server2) {
|
|
|
119717
119881
|
patch.analyticsSessionMode = args.sessionMode;
|
|
119718
119882
|
if (args.durationMinutes !== void 0)
|
|
119719
119883
|
patch["meta.durationMinutes"] = args.durationMinutes;
|
|
119884
|
+
if (args.sessionStartAt !== void 0)
|
|
119885
|
+
patch["meta.sessionStartAt"] = args.sessionStartAt ? Timestamp.fromDate(new Date(args.sessionStartAt)) : null;
|
|
119886
|
+
if (args.sessionEndAt !== void 0)
|
|
119887
|
+
patch["meta.sessionEndAt"] = args.sessionEndAt ? Timestamp.fromDate(new Date(args.sessionEndAt)) : null;
|
|
119720
119888
|
const nowMs = args.eventDate !== void 0 ? await serverNowMs() : null;
|
|
119721
119889
|
const validate = (fresh, freshPatch) => {
|
|
119722
119890
|
assertScheduleHasDate(fresh, freshPatch);
|
|
119891
|
+
assertSessionPlanValid(fresh, freshPatch);
|
|
119723
119892
|
if (nowMs !== null)
|
|
119724
119893
|
assertDateNotLocked(args.eventId, fresh, freshPatch, nowMs);
|
|
119725
119894
|
};
|
|
119726
119895
|
await writeEventPatchWithCards(args.eventId, patch, null, validate);
|
|
119727
|
-
|
|
119896
|
+
const touchesSession = ["meta.eventDate", "meta.durationMinutes", "meta.sessionStartAt", "meta.sessionEndAt", "analyticsSessionMode"].some((key) => key in patch);
|
|
119897
|
+
const warnings = touchesSession ? await collectSessionWarnings(args.eventId, await readEvent(args.eventId, uid, "read")) : [];
|
|
119898
|
+
return textResult({ id: args.eventId, updated: Object.keys(patch), warnings });
|
|
119728
119899
|
});
|
|
119729
119900
|
server2.registerTool("oko_configure_blocks", {
|
|
119730
119901
|
title: "Configure event blocks",
|
|
@@ -121105,6 +121276,16 @@ async function releaseSessionPointers(eventId, sessionId) {
|
|
|
121105
121276
|
attempts.push(await attemptCleanup("events.activeAnalyticsSessionId", () => updateDoc(doc(firestore, "events", eventId), { activeAnalyticsSessionId: deleteField() })));
|
|
121106
121277
|
return attempts;
|
|
121107
121278
|
}
|
|
121279
|
+
function settlementLine(result) {
|
|
121280
|
+
if (typeof result.minutes !== "number")
|
|
121281
|
+
return null;
|
|
121282
|
+
if (result.billingMode === "free") {
|
|
121283
|
+
return `The session ran ${result.minutes} minute(s); minutes are not being charged for this page right now (0 lumens taken).`;
|
|
121284
|
+
}
|
|
121285
|
+
const charged = typeof result.charged === "number" ? result.charged : 0;
|
|
121286
|
+
const forgiven = typeof result.forgiven === "number" ? result.forgiven : 0;
|
|
121287
|
+
return `Charged ${charged} lumens for ${result.minutes} minute(s)` + (forgiven > 0 ? `; ${forgiven} more were forgiven because the wallet did not hold enough (no debt).` : ".");
|
|
121288
|
+
}
|
|
121108
121289
|
function startSessionRefusal(eventId, body) {
|
|
121109
121290
|
return { ...textResult({ tool: "oko_start_session", eventId, ...body }), isError: true };
|
|
121110
121291
|
}
|
|
@@ -121126,6 +121307,54 @@ function startSessionRefusalResult(eventId, error3) {
|
|
|
121126
121307
|
]
|
|
121127
121308
|
});
|
|
121128
121309
|
}
|
|
121310
|
+
if (info.errorCode === "ENTITLEMENT_REQUIRED") {
|
|
121311
|
+
const inactive = info.reason === "plan_inactive";
|
|
121312
|
+
return startSessionRefusal(eventId, {
|
|
121313
|
+
errorCode: "ENTITLEMENT_REQUIRED",
|
|
121314
|
+
reason: inactive ? "plan_inactive" : "tier",
|
|
121315
|
+
requiredTier: typeof info.requiredTier === "string" ? info.requiredTier : "pro",
|
|
121316
|
+
currentTier: typeof info.currentTier === "string" ? info.currentTier : null,
|
|
121317
|
+
message: text || (inactive ? "OKO refused to start the session: the subscription that pays for this page is not active. Nothing was started or charged." : "OKO refused to start the session: the plan that pays for this page has no analytics sessions. Nothing was started or charged."),
|
|
121318
|
+
whatToDo: inactive ? [
|
|
121319
|
+
"Explain that the subscription owner can renew or buy a plan in the OKO cabinet.",
|
|
121320
|
+
"Do not retry as is: the answer will not change until the subscription does."
|
|
121321
|
+
] : [
|
|
121322
|
+
"Explain that analytics sessions come with the Pro plan and above; the subscription owner can upgrade in the OKO cabinet.",
|
|
121323
|
+
"Do not retry as is: the answer will not change until the plan does."
|
|
121324
|
+
]
|
|
121325
|
+
});
|
|
121326
|
+
}
|
|
121327
|
+
if (info.errorCode === "INSUFFICIENT_TOKENS") {
|
|
121328
|
+
const callerIsPayer = info.callerIsPayer === true;
|
|
121329
|
+
return startSessionRefusal(eventId, {
|
|
121330
|
+
errorCode: "INSUFFICIENT_TOKENS",
|
|
121331
|
+
callerIsPayer,
|
|
121332
|
+
message: text || "OKO refused to start the session: the wallet that pays for this page has 0 lumens. Nothing was started or charged.",
|
|
121333
|
+
whatToDo: [
|
|
121334
|
+
callerIsPayer ? "Explain that a session costs 1 lumen per minute and the wallet is at zero: lumens can be topped up in the OKO cabinet." : "Explain that this workspace is paid for by its owner, whose lumens have run out: ask the owner to top up.",
|
|
121335
|
+
"Do not retry as is: the balance will not change by itself."
|
|
121336
|
+
]
|
|
121337
|
+
});
|
|
121338
|
+
}
|
|
121339
|
+
if (info.reason === "payer_unresolved") {
|
|
121340
|
+
return startSessionRefusal(eventId, {
|
|
121341
|
+
errorCode: "PAYER_UNRESOLVED",
|
|
121342
|
+
message: text || "OKO cannot tell which workspace wallet pays for this event, so a session cannot be charged. Nothing was started.",
|
|
121343
|
+
whatToDo: [
|
|
121344
|
+
"Check the event's workspace with oko_get_event; if the workspace was removed, move the event with oko_move_event.",
|
|
121345
|
+
"Do not retry as is: the answer will not change until the workspace does."
|
|
121346
|
+
]
|
|
121347
|
+
});
|
|
121348
|
+
}
|
|
121349
|
+
if (info.reason === "planned_end_invalid") {
|
|
121350
|
+
return startSessionRefusal(eventId, {
|
|
121351
|
+
errorCode: "PLANNED_END_INVALID",
|
|
121352
|
+
message: text || "The end of the session must be a moment in the future. Nothing was started.",
|
|
121353
|
+
whatToDo: [
|
|
121354
|
+
"Pass endsAt as an ISO datetime later than now, or omit it to end with the page's plan."
|
|
121355
|
+
]
|
|
121356
|
+
});
|
|
121357
|
+
}
|
|
121129
121358
|
if (info.reason === "outside_show_window") {
|
|
121130
121359
|
const state2 = info.window === "before" || info.window === "after" ? info.window : null;
|
|
121131
121360
|
return startSessionRefusal(eventId, {
|
|
@@ -121145,32 +121374,92 @@ function startSessionRefusalResult(eventId, error3) {
|
|
|
121145
121374
|
}
|
|
121146
121375
|
return null;
|
|
121147
121376
|
}
|
|
121377
|
+
function buildExtendRequest(input) {
|
|
121378
|
+
const { eventId, sessionId, endsAt, addMinutes } = input;
|
|
121379
|
+
if (endsAt !== void 0)
|
|
121380
|
+
return { eventId, sessionId, plannedEndAt: Date.parse(endsAt) };
|
|
121381
|
+
return { eventId, sessionId, addMinutes };
|
|
121382
|
+
}
|
|
121383
|
+
function extendSessionRefusalResult(eventId, error3) {
|
|
121384
|
+
if (!error3 || typeof error3 !== "object")
|
|
121385
|
+
return null;
|
|
121386
|
+
const { code, message, details } = error3;
|
|
121387
|
+
if (typeof code !== "string" || !code.startsWith("functions/"))
|
|
121388
|
+
return null;
|
|
121389
|
+
const info = details && typeof details === "object" ? details : {};
|
|
121390
|
+
const text = safeString(message);
|
|
121391
|
+
const refusal4 = (body) => ({ ...textResult({ tool: "oko_extend_session", eventId, ...body }), isError: true });
|
|
121392
|
+
if (info.reason === "session_not_running") {
|
|
121393
|
+
return refusal4({
|
|
121394
|
+
errorCode: "SESSION_NOT_RUNNING",
|
|
121395
|
+
message: text || "No analytics session is running on this page, so there is nothing to extend.",
|
|
121396
|
+
whatToDo: [
|
|
121397
|
+
"Start a session with oko_start_session (it takes an endsAt), or, if it already ended, extend nothing: a finished session is final."
|
|
121398
|
+
]
|
|
121399
|
+
});
|
|
121400
|
+
}
|
|
121401
|
+
if (info.reason === "planned_end_invalid") {
|
|
121402
|
+
return refusal4({
|
|
121403
|
+
errorCode: "PLANNED_END_INVALID",
|
|
121404
|
+
message: text || "The new end must be a moment in the future.",
|
|
121405
|
+
whatToDo: ["Pass endsAt later than now, or addMinutes to move the current end forward."]
|
|
121406
|
+
});
|
|
121407
|
+
}
|
|
121408
|
+
if (info.reason === "outside_show_window") {
|
|
121409
|
+
return refusal4({
|
|
121410
|
+
errorCode: "OUTSIDE_SHOW_WINDOW",
|
|
121411
|
+
message: text || "The show window of this page is over: a session can no longer be extended.",
|
|
121412
|
+
whatToDo: ["End the session with oko_end_session; to run the show on another date, duplicate the page with oko_duplicate_event."]
|
|
121413
|
+
});
|
|
121414
|
+
}
|
|
121415
|
+
return null;
|
|
121416
|
+
}
|
|
121148
121417
|
function registerSessionTools(server2) {
|
|
121149
121418
|
server2.registerTool("oko_start_session", {
|
|
121150
121419
|
title: "Start analytics session",
|
|
121151
121420
|
description: toolDescription({
|
|
121152
|
-
summary: "Start an analytics session using the same Firestore/RTDB pointers as the dashboard.",
|
|
121153
|
-
args: [
|
|
121154
|
-
|
|
121421
|
+
summary: "Start an analytics session using the same Firestore/RTDB pointers as the dashboard. A session measures the show and costs 1 lumen per minute, taken when it ends.",
|
|
121422
|
+
args: [
|
|
121423
|
+
"eventId: event whose analytics session should start.",
|
|
121424
|
+
"endsAt: optional ISO datetime when the session should end. Omit it to end with the page's plan (the session time set in Setup, else the event time); it is never later than the end of the page's show window (the answer says clamped:true when it was moved back).",
|
|
121425
|
+
"confirm: must be true."
|
|
121426
|
+
],
|
|
121427
|
+
returns: "eventId, new sessionId, started=true, plannedEndAt (ms) and endsAt (ISO), billingMode (metered or free), clamped, and an explanation.",
|
|
121155
121428
|
examples: ["Use when the organizer wants analytics tracking to begin; it does not start the video stream."],
|
|
121156
121429
|
notes: [
|
|
121157
121430
|
"OKO Start/End controls analytics sessions only, not stream publishing.",
|
|
121158
|
-
"A
|
|
121431
|
+
"A session ALWAYS has an end: the plan's end (or endsAt); when it comes OKO ends the session by itself, and the closer a show runs to it the more useful oko_extend_session is. If the show starts before the plan, the session is longer than the plan; if it starts after the plan already ended, it gets the plan's length from now.",
|
|
121432
|
+
"Minutes are counted when the session ends (rounded; under 30 seconds is free) and taken from the wallet that pays, at most as much as it holds \u2014 there is no debt. If the lumens run out while it runs, OKO stops the session by itself. Pages can also start their session automatically at the plan's start (sessionMode schedule); Start here is the manual way.",
|
|
121433
|
+
"A new session only starts inside the page's show window (24h before the event date to 48h after). Refusals carry errorCode and whatToDo: PAGE_NOT_PUBLISHED (publish the page first with oko_publish_event), OUTSIDE_SHOW_WINDOW (windowState before/after \u2014 wait for the window, or duplicate the page for another date with oko_duplicate_event), ENTITLEMENT_REQUIRED (reason tier: the plan has no sessions, they come with Pro and above; reason plan_inactive: the subscription is not active), INSUFFICIENT_TOKENS (the paying wallet is at zero; callerIsPayer tells who should top it up), PAYER_UNRESOLVED and PLANNED_END_INVALID (endsAt must be in the future)."
|
|
121159
121434
|
]
|
|
121160
121435
|
}),
|
|
121161
|
-
inputSchema: {
|
|
121436
|
+
inputSchema: {
|
|
121437
|
+
eventId: eventIdField,
|
|
121438
|
+
endsAt: external_exports.string().datetime().optional().describe("Optional ISO datetime for the end of this session; omit to end with the page's plan."),
|
|
121439
|
+
confirm: external_exports.boolean().optional().describe("Required true to start analytics collection session.")
|
|
121440
|
+
},
|
|
121162
121441
|
outputSchema: basicEventOutputSchema,
|
|
121163
121442
|
annotations: writeTool
|
|
121164
|
-
}, async ({ eventId, confirm }) => {
|
|
121443
|
+
}, async ({ eventId, endsAt, confirm }) => {
|
|
121165
121444
|
const { uid } = await authed(["analytics:write", "live:control"]);
|
|
121166
121445
|
const event = await readEvent(eventId, uid, "session");
|
|
121167
|
-
requireConfirm(confirm, "oko_start_session", { eventId, title: event.title ?? null });
|
|
121446
|
+
requireConfirm(confirm, "oko_start_session", { eventId, title: event.title ?? null, endsAt: endsAt ?? null });
|
|
121168
121447
|
if (event.activeAnalyticsSessionId)
|
|
121169
121448
|
throw new Error("An analytics session is already active.");
|
|
121170
121449
|
const start = httpsCallable(functions, "startAnalyticsSessionV3");
|
|
121171
121450
|
try {
|
|
121172
|
-
const result = await start({ eventId });
|
|
121173
|
-
return textResult({
|
|
121451
|
+
const result = await start({ eventId, ...endsAt ? { plannedEndAt: Date.parse(endsAt) } : {} });
|
|
121452
|
+
return textResult({
|
|
121453
|
+
eventId,
|
|
121454
|
+
started: true,
|
|
121455
|
+
...result.data,
|
|
121456
|
+
endsAt: typeof result.data.plannedEndAt === "number" ? new Date(result.data.plannedEndAt).toISOString() : null,
|
|
121457
|
+
explanation: sessionExplanation({
|
|
121458
|
+
billingMode: result.data.billingMode,
|
|
121459
|
+
plannedEndAt: result.data.plannedEndAt,
|
|
121460
|
+
clamped: result.data.clamped
|
|
121461
|
+
})
|
|
121462
|
+
});
|
|
121174
121463
|
} catch (error3) {
|
|
121175
121464
|
const named = startSessionRefusalResult(eventId, error3);
|
|
121176
121465
|
if (named)
|
|
@@ -121181,11 +121470,15 @@ function registerSessionTools(server2) {
|
|
|
121181
121470
|
server2.registerTool("oko_end_session", {
|
|
121182
121471
|
title: "End analytics session",
|
|
121183
121472
|
description: toolDescription({
|
|
121184
|
-
summary: "End the active analytics session
|
|
121473
|
+
summary: "End the active analytics session, persist the session snapshot and settle its minutes.",
|
|
121185
121474
|
args: ["eventId: event with an active analytics session.", "confirm: must be true."],
|
|
121186
|
-
returns: "eventId, sessionId,
|
|
121475
|
+
returns: "eventId, sessionId, ended=true, minutes, charged, forgiven, billingMode, and a plain-language line about the charge.",
|
|
121187
121476
|
examples: ["Use after the webinar analytics window is over and the user confirms the exact event."],
|
|
121188
|
-
notes: [
|
|
121477
|
+
notes: [
|
|
121478
|
+
"Destructive for the active session pointer, but it does not stop the video stream.",
|
|
121479
|
+
"The session's minutes (rounded from its duration; a session under 30 seconds is 0) are 1 lumen each, taken now from the wallet that pays. charged is what was taken; forgiven is what the wallet could not cover \u2014 there is no debt. billingMode free means minutes were not charged (money counting is off for this page). Ending twice is safe: the second call returns the same figures and charges nothing.",
|
|
121480
|
+
"A session also ends by itself at its planned end and when the lumens run out (the report says which). Use oko_extend_session to move the planned end while it runs."
|
|
121481
|
+
]
|
|
121189
121482
|
}),
|
|
121190
121483
|
inputSchema: {
|
|
121191
121484
|
eventId: eventIdField,
|
|
@@ -121203,7 +121496,62 @@ function registerSessionTools(server2) {
|
|
|
121203
121496
|
requireConfirm(confirm, "oko_end_session", { eventId, sessionId, reportLocale: reportLocale ?? null });
|
|
121204
121497
|
const end = httpsCallable(functions, "endAnalyticsSessionV3");
|
|
121205
121498
|
const result = await end({ eventId, sessionId, ...reportLocale ? { locale: reportLocale } : {} });
|
|
121206
|
-
return textResult({ eventId, ...result.data });
|
|
121499
|
+
return textResult({ eventId, ...result.data, billingLine: settlementLine(result.data) });
|
|
121500
|
+
});
|
|
121501
|
+
server2.registerTool("oko_extend_session", {
|
|
121502
|
+
title: "Extend analytics session",
|
|
121503
|
+
description: toolDescription({
|
|
121504
|
+
summary: "Move the planned end of the RUNNING analytics session \u2014 later (the show runs long) or earlier. Analytics never edits page settings, so this is a server call like Start and End. Extending is free; only the minutes the session actually runs are paid.",
|
|
121505
|
+
args: [
|
|
121506
|
+
"eventId: event with an active analytics session.",
|
|
121507
|
+
"endsAt: the new end as an ISO datetime \u2014 OR \u2014",
|
|
121508
|
+
"addMinutes: move the CURRENT planned end by this many minutes (1-1440). Give exactly one of the two.",
|
|
121509
|
+
"confirm: must be true."
|
|
121510
|
+
],
|
|
121511
|
+
returns: "eventId, sessionId, extended=true, plannedEndAt (ms), endsAt (ISO), clamped, windowEndAt.",
|
|
121512
|
+
examples: ["The show is running long: oko_extend_session with addMinutes 30, then confirm."],
|
|
121513
|
+
notes: [
|
|
121514
|
+
"The new end must be in the future and is never later than the end of the page's show window: past it the end is moved back to the window's end and the answer says clamped:true (windowEndAt tells where) \u2014 that is not an error.",
|
|
121515
|
+
"The session ends by itself at its planned end; extending is how a long show keeps its measurement. It also stops by itself when the lumens run out.",
|
|
121516
|
+
"Refusals carry errorCode and whatToDo: SESSION_NOT_RUNNING (no session is running on this page \u2014 start one with oko_start_session), PLANNED_END_INVALID (the end is not in the future), OUTSIDE_SHOW_WINDOW."
|
|
121517
|
+
]
|
|
121518
|
+
}),
|
|
121519
|
+
inputSchema: {
|
|
121520
|
+
eventId: eventIdField,
|
|
121521
|
+
endsAt: external_exports.string().datetime().optional().describe("New end of the running session, ISO datetime."),
|
|
121522
|
+
addMinutes: external_exports.number().int().min(1).max(1440).optional().describe("Move the current planned end by this many minutes."),
|
|
121523
|
+
confirm: external_exports.boolean().optional().describe("Required true to move the end of the running session.")
|
|
121524
|
+
},
|
|
121525
|
+
outputSchema: basicEventOutputSchema,
|
|
121526
|
+
annotations: writeTool
|
|
121527
|
+
}, async ({ eventId, endsAt, addMinutes, confirm }) => {
|
|
121528
|
+
const { uid } = await authed(["analytics:write", "live:control"]);
|
|
121529
|
+
if (endsAt === void 0 === (addMinutes === void 0)) {
|
|
121530
|
+
throw new Error("Give exactly one of endsAt (an ISO datetime) or addMinutes (minutes to add to the current end).");
|
|
121531
|
+
}
|
|
121532
|
+
const event = await readEvent(eventId, uid, "session");
|
|
121533
|
+
const sessionId = safeString(event.activeAnalyticsSessionId);
|
|
121534
|
+
if (!sessionId) {
|
|
121535
|
+
throw new ToolRefusal(extendSessionRefusalResult(eventId, { code: "functions/failed-precondition", details: { reason: "session_not_running" } }));
|
|
121536
|
+
}
|
|
121537
|
+
requireConfirm(confirm, "oko_extend_session", { eventId, sessionId, endsAt: endsAt ?? null, addMinutes: addMinutes ?? null });
|
|
121538
|
+
const extend2 = httpsCallable(functions, "extendAnalyticsSession");
|
|
121539
|
+
try {
|
|
121540
|
+
const result = await extend2(buildExtendRequest({ eventId, sessionId, endsAt, addMinutes }));
|
|
121541
|
+
return textResult({
|
|
121542
|
+
eventId,
|
|
121543
|
+
sessionId,
|
|
121544
|
+
extended: true,
|
|
121545
|
+
...result.data,
|
|
121546
|
+
endsAt: new Date(result.data.plannedEndAt).toISOString(),
|
|
121547
|
+
explanation: sessionExplanation({ billingMode: void 0, plannedEndAt: result.data.plannedEndAt, clamped: result.data.clamped })
|
|
121548
|
+
});
|
|
121549
|
+
} catch (error3) {
|
|
121550
|
+
const named = extendSessionRefusalResult(eventId, error3);
|
|
121551
|
+
if (named)
|
|
121552
|
+
throw new ToolRefusal(named);
|
|
121553
|
+
throw error3;
|
|
121554
|
+
}
|
|
121207
121555
|
});
|
|
121208
121556
|
server2.registerTool("oko_fix_orphan_session", {
|
|
121209
121557
|
title: "Fix orphan analytics session",
|
|
@@ -121729,7 +122077,7 @@ function registerTelegramTools(server2) {
|
|
|
121729
122077
|
// build/tools/unpublication.js
|
|
121730
122078
|
var TOOL2 = "oko_unpublish_event";
|
|
121731
122079
|
var NOTHING_WILL_CHANGE = "The page and its lumens stay exactly as they are now.";
|
|
121732
|
-
function
|
|
122080
|
+
function isRecord3(value) {
|
|
121733
122081
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
121734
122082
|
}
|
|
121735
122083
|
function refusal3(eventId, body) {
|
|
@@ -121779,7 +122127,7 @@ function windowOpenRefusal(eventId, state2, windowStart) {
|
|
|
121779
122127
|
});
|
|
121780
122128
|
}
|
|
121781
122129
|
function previewUnpublish(eventId, event, nowMs) {
|
|
121782
|
-
const billing =
|
|
122130
|
+
const billing = isRecord3(event.billing) ? event.billing : {};
|
|
121783
122131
|
const state2 = billing.state === "published" || billing.state === "held" ? billing.state : "unpublished";
|
|
121784
122132
|
if (state2 === "unpublished")
|
|
121785
122133
|
return { kind: "already" };
|