@okostream/mcp 0.18.1 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -10
- package/dist/server.js +201 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,14 +95,21 @@ A new page is a draft: viewers of its public URL see "not published".
|
|
|
95
95
|
`oko_start_session`, `oko_extend_session` and `oko_publish_telegram_post`
|
|
96
96
|
state the charge (or the refund) when called without `confirm`, name the
|
|
97
97
|
wallet that pays, and take nothing until the organizer confirms. The amount in
|
|
98
|
-
the preview is the amount charged; the
|
|
98
|
+
the preview of a charge is the amount charged; the preview of a withdrawal
|
|
99
|
+
(`oko_unpublish_event`, the trash) says "up to", because part of the refund
|
|
100
|
+
may be forfeited and only the answer after confirmation gives the exact
|
|
101
|
+
figures. The public price list is at
|
|
99
102
|
[okostream.app/pricing](https://okostream.app/pricing) and is not copied here.
|
|
100
|
-
- **`oko_wallet` reads the wallet.** It returns the
|
|
101
|
-
|
|
102
|
-
the
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
- **`oko_wallet` reads the plan and the wallet.** It returns the plan (tier,
|
|
104
|
+
status, period end, source), the lumen balance, the grants and the ledger of
|
|
105
|
+
the organizer behind the key, with the link to the cabinet's Billing page
|
|
106
|
+
(scope `wallet:read`, included in the `operator` and `full` presets, not in
|
|
107
|
+
`readOnly`). It cannot buy, change or cancel anything: a plan or lumens are
|
|
108
|
+
bought only in that cabinet. Keys created before that scope existed get
|
|
109
|
+
`SCOPE_REQUIRED` and need a new key. A page in a shared workspace is paid from
|
|
110
|
+
the workspace owner's wallet: its balance is told, in the previews and
|
|
111
|
+
refusals, to whoever may spend from it; its grants and ledger are the owner's
|
|
112
|
+
to read.
|
|
106
113
|
- **Payment and terms are web-only.** Plans and lumens are bought, and terms
|
|
107
114
|
accepted, only in the OKO web cabinet, and no MCP tool collects a password,
|
|
108
115
|
card or bank details, or agreement to terms on anyone's behalf.
|
|
@@ -137,8 +144,10 @@ Refusals are explicit and never require guessing:
|
|
|
137
144
|
workspace owner does not hold another shared workspace, another member, or
|
|
138
145
|
the capability at all. A refused invitation stays valid. Plans are bought only
|
|
139
146
|
in the OKO cabinet, never through MCP.
|
|
140
|
-
- `WORKSPACE_INACTIVE` — `oko_publish_event` in a shared workspace the plan
|
|
141
|
-
|
|
147
|
+
- `WORKSPACE_INACTIVE` — `oko_publish_event` in a shared workspace the plan does
|
|
148
|
+
not hold open (only a new publication is closed): Core — shared workspaces are
|
|
149
|
+
not included (Pro); Pro — one active workspace (the owner's choice, or Max); no
|
|
150
|
+
live subscription — renew.
|
|
142
151
|
|
|
143
152
|
**One install path.** Every supported client — Claude Code, Codex, and any
|
|
144
153
|
MCP client that takes a command with arguments and env — runs the package
|
|
@@ -171,7 +180,7 @@ always works. The result's `sourceVia` says who fetched the image.
|
|
|
171
180
|
- Read analytics, session reports, geography, engagement, and form submissions.
|
|
172
181
|
- Manage workspaces, invitations, notifications, and Telegram publishing
|
|
173
182
|
(5 lumens per post, shown in the confirmation preview).
|
|
174
|
-
- Read
|
|
183
|
+
- Read your own plan, lumen balance and ledger (`oko_wallet`).
|
|
175
184
|
|
|
176
185
|
OKO MCP acts as the organizer represented by the selected access key. It does
|
|
177
186
|
not inherit platform-owner privileges. Keys are scoped and revocable, and
|
package/dist/server.js
CHANGED
|
@@ -70718,7 +70718,7 @@ var GUIDE_RESOURCES = [
|
|
|
70718
70718
|
"- `telegram:write`: link channels and manage Telegram posts.",
|
|
70719
70719
|
"- `workspace:read` and `workspace:write`: workspaces, members, invites, and notifications.",
|
|
70720
70720
|
"- `leads:read`: read form submissions; deletion also requires `setup:write`.",
|
|
70721
|
-
"- `wallet:read`: read the lumen balance, grants and ledger of the organizer's own wallet (`oko_wallet`); nothing can be bought, granted or spent with it.",
|
|
70721
|
+
"- `wallet:read`: read the plan, the lumen balance, grants and ledger of the organizer's own wallet (`oko_wallet`); nothing can be bought, changed, granted or spent with it.",
|
|
70722
70722
|
"",
|
|
70723
70723
|
"Payment and terms are web-only: no tool collects a password, card or bank details, or agreement to terms on anyone's behalf. Plans and lumens are bought, and terms accepted, only in the OKO web cabinet."
|
|
70724
70724
|
].join("\n")
|
|
@@ -116320,7 +116320,7 @@ function sanitizeScopes(scopes) {
|
|
|
116320
116320
|
}
|
|
116321
116321
|
|
|
116322
116322
|
// build/version.js
|
|
116323
|
-
var OKO_MCP_VERSION = "0.18.
|
|
116323
|
+
var OKO_MCP_VERSION = "0.18.3";
|
|
116324
116324
|
|
|
116325
116325
|
// build/auth.js
|
|
116326
116326
|
var state = null;
|
|
@@ -116565,6 +116565,9 @@ function sessionReportUrl(eventId, sessionId) {
|
|
|
116565
116565
|
function inviteUrl(code) {
|
|
116566
116566
|
return `${APP_ORIGIN}/invite/${code}`;
|
|
116567
116567
|
}
|
|
116568
|
+
function billingUrl() {
|
|
116569
|
+
return `${APP_ORIGIN}/app/billing`;
|
|
116570
|
+
}
|
|
116568
116571
|
function eventLinks(eventId) {
|
|
116569
116572
|
return {
|
|
116570
116573
|
url: eventUrl(eventId),
|
|
@@ -118045,8 +118048,18 @@ var notificationsMarkOutputSchema = external_exports.object({
|
|
|
118045
118048
|
markedRead: external_exports.number().int().nonnegative(),
|
|
118046
118049
|
notificationIds: external_exports.array(external_exports.string())
|
|
118047
118050
|
}).passthrough();
|
|
118051
|
+
var walletPlanSchema = external_exports.object({
|
|
118052
|
+
tier: external_exports.enum(["core", "pro", "max", "legacy"]),
|
|
118053
|
+
status: external_exports.enum(["active", "past_due", "canceled_at_period_end", "expired"]),
|
|
118054
|
+
source: external_exports.enum(["lava", "operator", "legacy-exit", "transition-b", "smoke"]).nullable(),
|
|
118055
|
+
periodEnd: external_exports.string().nullable(),
|
|
118056
|
+
graceUntil: external_exports.string().nullable()
|
|
118057
|
+
});
|
|
118048
118058
|
var walletOutputSchema = external_exports.object({
|
|
118049
118059
|
uid: external_exports.string(),
|
|
118060
|
+
billingUrl: external_exports.string().url(),
|
|
118061
|
+
plan: walletPlanSchema.nullable().optional(),
|
|
118062
|
+
planRead: external_exports.enum(["ok", "unreadable"]).optional(),
|
|
118050
118063
|
hasWallet: external_exports.boolean(),
|
|
118051
118064
|
balance: external_exports.number().int().nullable(),
|
|
118052
118065
|
unit: external_exports.literal("lumens"),
|
|
@@ -118165,7 +118178,7 @@ var SCOPE_UNLOCKS = {
|
|
|
118165
118178
|
"workspace:read": "listing workspaces, members, invites, notifications",
|
|
118166
118179
|
"workspace:write": "workspace management, invites, profile edits",
|
|
118167
118180
|
"leads:read": "reading and exporting leads",
|
|
118168
|
-
"wallet:read": "reading the lumen balance, grants and ledger of the organizer's own wallet"
|
|
118181
|
+
"wallet:read": "reading the plan, the lumen balance, grants and ledger of the organizer's own wallet"
|
|
118169
118182
|
};
|
|
118170
118183
|
function presetsWithScope(scope) {
|
|
118171
118184
|
return Object.entries(MCP_PRESETS).filter(([, scopes]) => scopes.includes(scope)).map(([name7]) => name7);
|
|
@@ -119155,7 +119168,7 @@ function registerPublicationTools(server2) {
|
|
|
119155
119168
|
examples: ["Call without confirm, show the organizer the bill from the preview, and call again with confirm: true after explicit consent."],
|
|
119156
119169
|
notes: [
|
|
119157
119170
|
"The wallet belongs to the workspace owner; a member publishing in a shared workspace spends the owner's lumens.",
|
|
119158
|
-
"Refusals carry errorCode and whatToDo: ENTITLEMENT_REQUIRED, INSUFFICIENT_TOKENS, EVENT_DATE_REQUIRED, EVENT_WINDOW_PASSED (the event date is more than 48 hours in the past \u2014 move it forward), PAYER_UNRESOLVED, WORKSPACE_INACTIVE (
|
|
119171
|
+
"Refusals carry errorCode and whatToDo: ENTITLEMENT_REQUIRED, INSUFFICIENT_TOKENS, EVENT_DATE_REQUIRED, EVENT_WINDOW_PASSED (the event date is more than 48 hours in the past \u2014 move it forward), PAYER_UNRESOLVED, WORKSPACE_INACTIVE (only a NEW publication into a shared workspace is closed: Core \u2014 shared workspaces are not included (Pro); Pro \u2014 one active workspace (the owner's choice, or Max); no live subscription \u2014 renew; the owner makes a workspace the active one on the OKO cabinet's Workspaces page), EDIT_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH, EVENT_DATE_LOCKED (from oko_update_event, not this tool).",
|
|
119159
119172
|
'Repeating confirm on a published page charges nothing and repairs viewer-side switches; on a page whose show is over ("held"/\u041F\u0440\u043E\u0432\u0435\u0434\u0435\u043D\u043E) it charges nothing either and starts nothing new \u2014 duplicate the page for a new show.',
|
|
119160
119173
|
"Once published, the event date moves only inside the show window bought here (oko_update_event refuses EVENT_DATE_LOCKED outside it): for another date, unpublish with oko_unpublish_event first while the window has not opened, or duplicate the page."
|
|
119161
119174
|
]
|
|
@@ -121008,6 +121021,36 @@ function topUpsPaid(billing) {
|
|
|
121008
121021
|
function topUpsNote(topUps) {
|
|
121009
121022
|
return topUps > 0 ? `The ${topUps} lumens paid on top of the publication (blocks and control room turned on after it) are NOT returned; publishing again bills those blocks in the new show's bill.` : "";
|
|
121010
121023
|
}
|
|
121024
|
+
var REFUND_UP_TO_NOTE = "Part of the charge may not come back: lumens that would already have burned by now (their plan period is over) are forfeited, not returned. The exact figures are known only after confirmation: the answer carries `refunded` (what came back to the wallet) and `forfeited` (what did not).";
|
|
121025
|
+
function forfeitedNote(forfeited) {
|
|
121026
|
+
return forfeited > 0 ? `${forfeited} lumens of the charge were not returned (forfeited): lumens that would already have burned by now come back nowhere.` : "";
|
|
121027
|
+
}
|
|
121028
|
+
function forfeitedOf(outcome) {
|
|
121029
|
+
const value = outcome.forfeited;
|
|
121030
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
121031
|
+
}
|
|
121032
|
+
function unpublishPreviewNote(topUps) {
|
|
121033
|
+
return [
|
|
121034
|
+
"Unpublishing takes the page back to draft and returns UP TO the publication charge (`refundUpTo`) to the wallet that paid; the live layer (chat, presence, session start) closes immediately.",
|
|
121035
|
+
REFUND_UP_TO_NOTE,
|
|
121036
|
+
"If the page only needs another date inside its show window, oko_update_event moves it with no refund and no new charge \u2014 prefer that to unpublishing.",
|
|
121037
|
+
topUpsNote(topUps)
|
|
121038
|
+
].filter(Boolean).join(" ");
|
|
121039
|
+
}
|
|
121040
|
+
function unpublishAnswerHints(outcome, topUps) {
|
|
121041
|
+
const refunded = typeof outcome.refunded === "number" ? outcome.refunded : 0;
|
|
121042
|
+
const forfeited = forfeitedOf(outcome);
|
|
121043
|
+
if (refunded > 0) {
|
|
121044
|
+
return [
|
|
121045
|
+
`${refunded} lumens were refunded to payerUid ${safeString(outcome.payerUid)}.`,
|
|
121046
|
+
forfeitedNote(forfeited),
|
|
121047
|
+
...topUps > 0 ? [topUpsNote(topUps)] : []
|
|
121048
|
+
].filter(Boolean);
|
|
121049
|
+
}
|
|
121050
|
+
if (outcome.alreadyUnpublished === true)
|
|
121051
|
+
return ["The page was already a draft \u2014 nothing was refunded now."];
|
|
121052
|
+
return [forfeitedNote(forfeited)].filter(Boolean);
|
|
121053
|
+
}
|
|
121011
121054
|
function previewUnpublish(eventId, event, nowMs) {
|
|
121012
121055
|
const billing = isRecord4(event.billing) ? event.billing : {};
|
|
121013
121056
|
const state2 = billing.state === "published" || billing.state === "held" ? billing.state : "unpublished";
|
|
@@ -121067,18 +121110,19 @@ function registerUnpublicationTools(server2) {
|
|
|
121067
121110
|
server2.registerTool("oko_unpublish_event", {
|
|
121068
121111
|
title: "Unpublish event page",
|
|
121069
121112
|
description: toolDescription({
|
|
121070
|
-
summary: `Take a published OKO event page back to draft and refund its publication charge. Works only before the page's show window opens; once it has opened (or the page shows "held"/\u041F\u0440\u043E\u0432\u0435\u0434\u0435\u043D\u043E), the page stays published for good. Without confirm nothing is written: the call returns a preview \u2014 what will happen and how many lumens come back to which wallet, or the refusal if it cannot be done now. Built from the event document, not a server round trip
|
|
121113
|
+
summary: `Take a published OKO event page back to draft and refund its publication charge. Works only before the page's show window opens; once it has opened (or the page shows "held"/\u041F\u0440\u043E\u0432\u0435\u0434\u0435\u043D\u043E), the page stays published for good. Without confirm nothing is written: the call returns a preview \u2014 what will happen and UP TO how many lumens come back (refundUpTo) to which wallet, or the refusal if it cannot be done now. Built from the event document, not a server round trip, so it cannot tell how much of the charge was forfeited (lumens that would already have burned): the exact refunded and forfeited figures come only after confirm. With confirm: true the page becomes a draft and the refund (up to the charge) is paid in one step.`,
|
|
121071
121114
|
args: [
|
|
121072
121115
|
"eventId: event to unpublish.",
|
|
121073
121116
|
"confirm: true only after the organizer has seen the preview and agreed."
|
|
121074
121117
|
],
|
|
121075
|
-
returns: "status unpublished with alreadyUnpublished, refunded, balanceAfter, mirrorsUpdated, plus title, links and hints.",
|
|
121076
|
-
examples: ["Call without confirm, show the organizer
|
|
121118
|
+
returns: "status unpublished with alreadyUnpublished, refunded (what came back), forfeited (what did not, only when above zero), balanceAfter, mirrorsUpdated, plus title, links and hints. The preview (no confirm) carries refundUpTo, not refunded.",
|
|
121119
|
+
examples: ["Call without confirm, show the organizer UP TO how much comes back and to whom (never promise the whole charge), and call again with confirm: true after explicit consent; then tell the exact refunded and forfeited."],
|
|
121077
121120
|
notes: [
|
|
121078
121121
|
"Refunds go to the wallet that paid for this publication (billing.payerUid), which may not be the organizer behind this key in a shared workspace.",
|
|
121122
|
+
"The refund is UP TO the charge: the share of it drawn from lumens that would already have burned (their plan period is over) is forfeited. The preview cannot see that, so it says refundUpTo; the answer after confirm says refunded and forfeited.",
|
|
121079
121123
|
"Only the publication charge comes back. Lumens paid AFTER publication for blocks and the control room turned on later (oko_configure_blocks, oko_configure_cards with confirm) are NOT returned: the preview names them as topUpsNotRefunded, and publishing again bills those blocks in the new show's bill.",
|
|
121080
121124
|
"Publishing again afterwards (oko_publish_event) is a new show and a new charge, with a fresh show window from the new date.",
|
|
121081
|
-
"
|
|
121125
|
+
"A date INSIDE the page's show window moves with oko_update_event directly, with no refund and no new charge \u2014 prefer that when the charge may be partly forfeited. A date outside the window: unpublish (refund, up to the charge), change the date with oko_update_event, publish again (new charge); oko_update_event refuses the move otherwise (EVENT_DATE_LOCKED).",
|
|
121082
121126
|
"One rule for every page: an old page that paid nothing is unpublished the same way before its window opens (the refund is 0 lumens) and refused the same way once it has taken place (WINDOW_OPEN, windowState after).",
|
|
121083
121127
|
"Refusals carry errorCode and whatToDo: NOT_PUBLISHED, DATE_REQUIRED, WINDOW_OPEN, EDIT_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH."
|
|
121084
121128
|
]
|
|
@@ -121115,13 +121159,13 @@ function registerUnpublicationTools(server2) {
|
|
|
121115
121159
|
requireConfirm(confirm, TOOL2, {
|
|
121116
121160
|
eventId,
|
|
121117
121161
|
title,
|
|
121118
|
-
|
|
121162
|
+
refundUpTo: preview.tokensCharged,
|
|
121119
121163
|
topUpsNotRefunded: preview.topUps,
|
|
121120
121164
|
payerUid: preview.payerUid,
|
|
121121
121165
|
showId: preview.showId,
|
|
121122
121166
|
windowStart: preview.windowStart,
|
|
121123
121167
|
windowEnd: preview.windowEnd,
|
|
121124
|
-
note:
|
|
121168
|
+
note: unpublishPreviewNote(preview.topUps)
|
|
121125
121169
|
});
|
|
121126
121170
|
}
|
|
121127
121171
|
}
|
|
@@ -121135,9 +121179,7 @@ function registerUnpublicationTools(server2) {
|
|
|
121135
121179
|
throw new ToolRefusal(named);
|
|
121136
121180
|
throw error3;
|
|
121137
121181
|
}
|
|
121138
|
-
const
|
|
121139
|
-
const topUps = topUpsPaid(isRecord4(event.billing) ? event.billing : {});
|
|
121140
|
-
const hints = refunded > 0 ? [`${refunded} lumens were refunded to payerUid ${safeString(outcome.payerUid)}.`, ...topUps > 0 ? [topUpsNote(topUps)] : []] : outcome.alreadyUnpublished === true ? ["The page was already a draft \u2014 nothing was refunded now."] : [];
|
|
121182
|
+
const hints = unpublishAnswerHints(outcome, topUpsPaid(isRecord4(event.billing) ? event.billing : {}));
|
|
121141
121183
|
return textResult({
|
|
121142
121184
|
...outcome,
|
|
121143
121185
|
title,
|
|
@@ -121151,7 +121193,8 @@ function registerUnpublicationTools(server2) {
|
|
|
121151
121193
|
var TRASH_OUTPUT_SCHEMA = external_exports.object({
|
|
121152
121194
|
trashed: external_exports.string(),
|
|
121153
121195
|
purgeAfter: external_exports.number().nullable(),
|
|
121154
|
-
refunded: external_exports.number().nonnegative().optional()
|
|
121196
|
+
refunded: external_exports.number().nonnegative().optional(),
|
|
121197
|
+
forfeited: external_exports.number().nonnegative().optional()
|
|
121155
121198
|
}).passthrough();
|
|
121156
121199
|
function trashRefundWarning(event, nowMs) {
|
|
121157
121200
|
const billing = event.billing && typeof event.billing === "object" && !Array.isArray(event.billing) ? event.billing : null;
|
|
@@ -121160,9 +121203,17 @@ function trashRefundWarning(event, nowMs) {
|
|
|
121160
121203
|
const window2 = windowFromEvent(event, nowMs);
|
|
121161
121204
|
if (window2 !== null && window2.state !== "before")
|
|
121162
121205
|
return null;
|
|
121163
|
-
const charge = typeof billing.tokensCharged === "number" ? `${billing.tokensCharged} lumens` : "
|
|
121206
|
+
const charge = typeof billing.tokensCharged === "number" ? `${billing.tokensCharged} lumens` : "what the publication paid";
|
|
121164
121207
|
const topUps = topUpsNote(topUpsPaid(billing));
|
|
121165
|
-
return `This page is published. Moving it to the trash also unpublishes it and
|
|
121208
|
+
return `This page is published. Moving it to the trash also unpublishes it and returns up to ${charge} to the wallet that paid, because its show window has not opened yet. ${REFUND_UP_TO_NOTE}${topUps ? ` ${topUps}` : ""}`;
|
|
121209
|
+
}
|
|
121210
|
+
function trashAnswerHints(data) {
|
|
121211
|
+
const refunded = typeof data.refunded === "number" ? data.refunded : 0;
|
|
121212
|
+
const forfeited = forfeitedOf(data);
|
|
121213
|
+
return [
|
|
121214
|
+
...refunded > 0 ? [`${refunded} lumens were refunded to the wallet that paid for this page's publication: it was still inside its show window when it was trashed.`] : [],
|
|
121215
|
+
...forfeited > 0 ? [forfeitedNote(forfeited)] : []
|
|
121216
|
+
];
|
|
121166
121217
|
}
|
|
121167
121218
|
function registerLifecycleTools(server2) {
|
|
121168
121219
|
const moveToTrash = async (toolName, { eventId, confirm }) => {
|
|
@@ -121177,7 +121228,7 @@ function registerLifecycleTools(server2) {
|
|
|
121177
121228
|
const trash = httpsCallable(functions, "trashEvent");
|
|
121178
121229
|
const data = (await trash({ eventId })).data;
|
|
121179
121230
|
const refunded = typeof data.refunded === "number" ? data.refunded : 0;
|
|
121180
|
-
const hints =
|
|
121231
|
+
const hints = trashAnswerHints(data);
|
|
121181
121232
|
return textResult({ ...data, refunded, ...hints.length ? { hints } : {} });
|
|
121182
121233
|
};
|
|
121183
121234
|
server2.registerTool("oko_delete_event", {
|
|
@@ -121189,7 +121240,7 @@ function registerLifecycleTools(server2) {
|
|
|
121189
121240
|
examples: ["Use after showing the exact event title and asking the organizer to confirm moving it to trash."],
|
|
121190
121241
|
notes: [
|
|
121191
121242
|
"Recoverable for 30 days with oko_restore_event. This does not create an event lifecycle status.",
|
|
121192
|
-
"A published, paid page still inside its show window is also unpublished and its charge refunded; the preview (without confirm) names it in `warning`, and the answer's `refunded` field says how much came back."
|
|
121243
|
+
"A published, paid page still inside its show window is also unpublished and UP TO its charge refunded; the preview (without confirm) names it in `warning`, and the answer's `refunded` field says how much came back (`forfeited`, when above zero, how much did not)."
|
|
121193
121244
|
]
|
|
121194
121245
|
}),
|
|
121195
121246
|
inputSchema: { eventId: eventIdField, confirm: external_exports.boolean().optional().describe("Required true to move the event to trash.") },
|
|
@@ -121205,7 +121256,7 @@ function registerLifecycleTools(server2) {
|
|
|
121205
121256
|
examples: ["Use after showing the exact event title and asking the organizer to confirm moving it to trash."],
|
|
121206
121257
|
notes: [
|
|
121207
121258
|
"End any active analytics session first. Restore with oko_restore_event before the purge deadline.",
|
|
121208
|
-
"A published, paid page still inside its show window is also unpublished and its charge refunded; the preview (without confirm) names it in `warning`, and the answer's `refunded` field says how much came back."
|
|
121259
|
+
"A published, paid page still inside its show window is also unpublished and UP TO its charge refunded; the preview (without confirm) names it in `warning`, and the answer's `refunded` field says how much came back (`forfeited`, when above zero, how much did not)."
|
|
121209
121260
|
]
|
|
121210
121261
|
}),
|
|
121211
121262
|
inputSchema: { eventId: eventIdField, confirm: external_exports.boolean().optional().describe("Required true to move the event to trash.") },
|
|
@@ -122772,6 +122823,88 @@ function ledgerRowForOutput(id, raw) {
|
|
|
122772
122823
|
forgiven: isSessionRow ? readCount(raw.forgiven ?? ref2.forgiven) : null
|
|
122773
122824
|
};
|
|
122774
122825
|
}
|
|
122826
|
+
var PLAN_TIERS = ["core", "pro", "max", "legacy"];
|
|
122827
|
+
var PLAN_STATUSES = ["active", "past_due", "canceled_at_period_end", "expired"];
|
|
122828
|
+
var PLAN_SOURCES = ["lava", "operator", "legacy-exit", "transition-b", "smoke"];
|
|
122829
|
+
function momentMs(value) {
|
|
122830
|
+
if (value instanceof Date)
|
|
122831
|
+
return Number.isNaN(value.getTime()) ? null : value.getTime();
|
|
122832
|
+
if (isRecord6(value) && typeof value.toMillis === "function") {
|
|
122833
|
+
const ms = value.toMillis();
|
|
122834
|
+
return typeof ms === "number" && Number.isFinite(ms) ? ms : null;
|
|
122835
|
+
}
|
|
122836
|
+
return null;
|
|
122837
|
+
}
|
|
122838
|
+
function readOrganizerPlan(accessData) {
|
|
122839
|
+
if (!isRecord6(accessData))
|
|
122840
|
+
return { state: "unreadable" };
|
|
122841
|
+
const raw = accessData.plan;
|
|
122842
|
+
if (raw === void 0 || raw === null)
|
|
122843
|
+
return { state: "none" };
|
|
122844
|
+
if (!isRecord6(raw))
|
|
122845
|
+
return { state: "unreadable" };
|
|
122846
|
+
const tier = PLAN_TIERS.find((candidate) => candidate === raw.tier);
|
|
122847
|
+
const status = PLAN_STATUSES.find((candidate) => candidate === raw.status);
|
|
122848
|
+
if (!tier || !status)
|
|
122849
|
+
return { state: "unreadable" };
|
|
122850
|
+
const dates = { periodStart: null, periodEnd: null, graceUntil: null };
|
|
122851
|
+
for (const key of ["periodStart", "periodEnd", "graceUntil"]) {
|
|
122852
|
+
const value = raw[key];
|
|
122853
|
+
if (value === void 0 || value === null)
|
|
122854
|
+
continue;
|
|
122855
|
+
const ms = momentMs(value);
|
|
122856
|
+
if (ms === null)
|
|
122857
|
+
return { state: "unreadable" };
|
|
122858
|
+
dates[key] = ms;
|
|
122859
|
+
}
|
|
122860
|
+
const seats = raw.seatsIncluded;
|
|
122861
|
+
if (seats !== void 0 && seats !== null && !(Number.isInteger(seats) && seats > 0))
|
|
122862
|
+
return { state: "unreadable" };
|
|
122863
|
+
const iso2 = (ms) => ms === null ? null : new Date(ms).toISOString();
|
|
122864
|
+
return {
|
|
122865
|
+
state: "plan",
|
|
122866
|
+
plan: {
|
|
122867
|
+
tier,
|
|
122868
|
+
status,
|
|
122869
|
+
source: PLAN_SOURCES.find((candidate) => candidate === raw.source) ?? null,
|
|
122870
|
+
periodEnd: iso2(dates.periodEnd),
|
|
122871
|
+
graceUntil: iso2(dates.graceUntil)
|
|
122872
|
+
}
|
|
122873
|
+
};
|
|
122874
|
+
}
|
|
122875
|
+
function planAnswer(read) {
|
|
122876
|
+
const url = billingUrl();
|
|
122877
|
+
const reading = read.kind === "access" && read.exists ? readOrganizerPlan(read.data) : { state: "unreadable" };
|
|
122878
|
+
const standing = `This tool only reads. A plan or lumens are bought, changed or cancelled only in the OKO web cabinet (${url}): send the organizer there, and never ask for card details or a password.`;
|
|
122879
|
+
if (reading.state === "unreadable") {
|
|
122880
|
+
return {
|
|
122881
|
+
plan: null,
|
|
122882
|
+
planRead: "unreadable",
|
|
122883
|
+
hints: [
|
|
122884
|
+
`The plan could not be read (the platform refused the read, or the stored plan is malformed). Do not guess it: the organizer sees the real plan in the OKO web cabinet (${url}).`,
|
|
122885
|
+
standing
|
|
122886
|
+
]
|
|
122887
|
+
};
|
|
122888
|
+
}
|
|
122889
|
+
if (reading.state === "none") {
|
|
122890
|
+
return {
|
|
122891
|
+
plan: null,
|
|
122892
|
+
planRead: "ok",
|
|
122893
|
+
hints: [`This organizer has no plan: the paid features and spending lumens stay unavailable until a plan is bought in the OKO web cabinet (${url}). This tool cannot buy one.`, standing]
|
|
122894
|
+
};
|
|
122895
|
+
}
|
|
122896
|
+
const { plan } = reading;
|
|
122897
|
+
const hints = [];
|
|
122898
|
+
if (plan.status === "past_due") {
|
|
122899
|
+
hints.push(`The last renewal payment failed. The plan keeps working until graceUntil${plan.graceUntil ? ` (${plan.graceUntil})` : ""}; the organizer renews it in the OKO web cabinet (${url}) before then.`);
|
|
122900
|
+
} else if (plan.status === "canceled_at_period_end") {
|
|
122901
|
+
hints.push(`The subscription is cancelled: the plan works until periodEnd${plan.periodEnd ? ` (${plan.periodEnd})` : ""} and does not renew. Any change to it is made only in the OKO web cabinet (${url}).`);
|
|
122902
|
+
} else if (plan.status === "expired") {
|
|
122903
|
+
hints.push(`The plan has ended. A new plan is bought only in the OKO web cabinet (${url}).`);
|
|
122904
|
+
}
|
|
122905
|
+
hints.push(standing);
|
|
122906
|
+
return { plan, planRead: "ok", hints };
|
|
122907
|
+
}
|
|
122775
122908
|
function walletReadRefusal() {
|
|
122776
122909
|
return {
|
|
122777
122910
|
...textResult({
|
|
@@ -122788,7 +122921,7 @@ function walletReadRefusal() {
|
|
|
122788
122921
|
};
|
|
122789
122922
|
}
|
|
122790
122923
|
function walletAnswer(input) {
|
|
122791
|
-
const { uid, walletExists, walletData, grantDocs, ledgerDocs, max } = input;
|
|
122924
|
+
const { uid, walletExists, walletData, grantDocs, ledgerDocs, max, planRead } = input;
|
|
122792
122925
|
let balance = null;
|
|
122793
122926
|
if (walletExists) {
|
|
122794
122927
|
balance = readWalletBalance(walletData);
|
|
@@ -122818,6 +122951,7 @@ function walletAnswer(input) {
|
|
|
122818
122951
|
const last = page.at(-1);
|
|
122819
122952
|
const lastAt = isRecord6(last?.data) ? last.data.at : void 0;
|
|
122820
122953
|
const nextCursor = hasMore ? timestampCursorFrom(lastAt, last?.id) : null;
|
|
122954
|
+
const planBlock = planRead ? planAnswer(planRead) : null;
|
|
122821
122955
|
const hints = [];
|
|
122822
122956
|
if (grantDocs !== null && !walletExists)
|
|
122823
122957
|
hints.push("This organizer has no wallet yet: nothing was ever granted. Plans and lumens are bought only in the OKO web cabinet.");
|
|
@@ -122825,8 +122959,12 @@ function walletAnswer(input) {
|
|
|
122825
122959
|
hints.push("Some rows did not read (unreadableGrants / unreadableLedgerRows): tell the organizer, the balance is still the wallet's own figure.");
|
|
122826
122960
|
if (hasMore && nextCursor === null)
|
|
122827
122961
|
hints.push("There are more ledger rows but the page boundary could not be named: the listing stops here.");
|
|
122962
|
+
if (planBlock)
|
|
122963
|
+
hints.push(...planBlock.hints);
|
|
122828
122964
|
return {
|
|
122829
122965
|
uid,
|
|
122966
|
+
billingUrl: billingUrl(),
|
|
122967
|
+
...planBlock ? { plan: planBlock.plan, planRead: planBlock.planRead } : {},
|
|
122830
122968
|
hasWallet: walletExists,
|
|
122831
122969
|
balance,
|
|
122832
122970
|
unit: "lumens",
|
|
@@ -122838,16 +122976,26 @@ function walletAnswer(input) {
|
|
|
122838
122976
|
hints
|
|
122839
122977
|
};
|
|
122840
122978
|
}
|
|
122979
|
+
async function readPlanDocument(uid) {
|
|
122980
|
+
try {
|
|
122981
|
+
const snap = await getDoc(doc(firestore, "organizerAccess", uid));
|
|
122982
|
+
return { kind: "access", exists: snap.exists(), data: snap.data() };
|
|
122983
|
+
} catch (error3) {
|
|
122984
|
+
if (isFirestorePermissionDenied(error3))
|
|
122985
|
+
return { kind: "refused" };
|
|
122986
|
+
throw error3;
|
|
122987
|
+
}
|
|
122988
|
+
}
|
|
122841
122989
|
function registerWalletTools(server2) {
|
|
122842
122990
|
server2.registerTool("oko_wallet", {
|
|
122843
122991
|
title: "Read lumen wallet",
|
|
122844
122992
|
description: toolDescription({
|
|
122845
|
-
summary: "Read the lumen balance, the grants (buckets) and the ledger of the organizer's OWN
|
|
122993
|
+
summary: "Read the plan, the lumen balance, the grants (buckets) and the ledger of the organizer's OWN account, the one behind this key, with the link to the OKO cabinet's Billing page. Read-only: it cannot buy anything.",
|
|
122846
122994
|
args: [
|
|
122847
122995
|
"limit: ledger rows per page, default 20, max 50, newest first.",
|
|
122848
|
-
"cursor: nextCursor returned by the previous page. The grants come with the first page only; the balance with every page."
|
|
122996
|
+
"cursor: nextCursor returned by the previous page. The plan and the grants come with the first page only; the balance with every page."
|
|
122849
122997
|
],
|
|
122850
|
-
returns: 'hasWallet, balance (whole lumens, null when the organizer has no wallet yet), unit "lumens", grants on the first page (id, bucket plan|rollover|topup, amount, remaining, expiresAt, createdAt), ledger rows (id, kind grant|charge|refund|expire|settlement, amount, delta, balanceAfter, by, at, eventId, showId, workspaceId, postId, sessionId, minutes, forgiven), unreadableGrants/unreadableLedgerRows (rows that did not read: say so, never hide them) and cursor pagination metadata
|
|
122998
|
+
returns: `billingUrl (the cabinet's Billing page), plan on the first page (tier core|pro|max|legacy, status active|past_due|canceled_at_period_end|expired, source lava|operator|legacy-exit|transition-b|smoke or null, periodEnd and graceUntil as ISO times or null; plan is null when the organizer has no plan) with planRead ok|unreadable (unreadable: the plan could not be read, so null means unknown, not no plan), hasWallet, balance (whole lumens, null when the organizer has no wallet yet), unit "lumens", grants on the first page (id, bucket plan|rollover|topup, amount, remaining, expiresAt, createdAt), ledger rows (id, kind grant|charge|refund|expire|settlement, amount, delta, balanceAfter, by, at, eventId, showId, workspaceId, postId, sessionId, minutes, forgiven), unreadableGrants/unreadableLedgerRows (rows that did not read: say so, never hide them) and cursor pagination metadata.`,
|
|
122851
122999
|
examples: [
|
|
122852
123000
|
"Use before publishing a page or starting a session when the organizer asks how many lumens are left or what was charged.",
|
|
122853
123001
|
"Use to explain a charge or a refund: every ledger row names what paid (eventId, sessionId, postId) and the balance after it."
|
|
@@ -122855,7 +123003,8 @@ function registerWalletTools(server2) {
|
|
|
122855
123003
|
notes: [
|
|
122856
123004
|
"Needs the wallet:read scope. A key created before that scope existed gets SCOPE_REQUIRED and its scopes cannot be changed: the organizer creates a new key in the OKO cabinet.",
|
|
122857
123005
|
"Shows only the wallet of the organizer behind the key. A page in a shared workspace is paid from the WORKSPACE OWNER's wallet: the publish, session and Telegram previews and refusals name that payer and tell its balance to whoever may spend from it (publish, start a session, send posts); the owner's grants and ledger are theirs to read, not this key's.",
|
|
122858
|
-
"This tool cannot buy, grant, top up or spend
|
|
123006
|
+
"This tool cannot buy, change, cancel, grant, top up or spend anything. A plan and lumens are bought, changed and cancelled in the OKO web cabinet only (billingUrl): send the organizer there, and never ask for card data or a password.",
|
|
123007
|
+
"The plan shown is the organizer's own, the one behind this key. A paid action on a page of a shared workspace is judged by the PAYER's plan (the workspace owner's), which this key cannot read: the refusal of the paid tool names what is missing.",
|
|
122859
123008
|
"Refusals of the paid tools use the same codes as the web: ENTITLEMENT_REQUIRED (the plan does not include the feature) and INSUFFICIENT_TOKENS (not enough lumens); PLATFORM_REJECTED_READ here means the platform refused the read."
|
|
122860
123009
|
]
|
|
122861
123010
|
}),
|
|
@@ -122872,10 +123021,11 @@ function registerWalletTools(server2) {
|
|
|
122872
123021
|
const constraints = [orderBy("at", "desc"), limit(max + 1)];
|
|
122873
123022
|
if (cursor)
|
|
122874
123023
|
constraints.splice(1, 0, startAfter(...parseTimestampCursor(cursor)));
|
|
122875
|
-
const [walletSnap, grantsSnap, ledgerSnap] = await Promise.all([
|
|
123024
|
+
const [walletSnap, grantsSnap, ledgerSnap, planRead] = await Promise.all([
|
|
122876
123025
|
getDoc(doc(firestore, "wallets", uid)),
|
|
122877
123026
|
first ? getDocs(query2(collection(firestore, "wallets", uid, "grants"), orderBy("createdAt", "desc"), limit(GRANTS_LIMIT))) : Promise.resolve(null),
|
|
122878
|
-
getDocs(query2(collection(firestore, "wallets", uid, "ledger"), ...constraints))
|
|
123027
|
+
getDocs(query2(collection(firestore, "wallets", uid, "ledger"), ...constraints)),
|
|
123028
|
+
first ? readPlanDocument(uid) : Promise.resolve(null)
|
|
122879
123029
|
]);
|
|
122880
123030
|
return textResult(walletAnswer({
|
|
122881
123031
|
uid,
|
|
@@ -122883,7 +123033,8 @@ function registerWalletTools(server2) {
|
|
|
122883
123033
|
walletData: walletSnap.data(),
|
|
122884
123034
|
grantDocs: grantsSnap ? grantsSnap.docs.map((item) => ({ id: item.id, data: item.data() })) : null,
|
|
122885
123035
|
ledgerDocs: ledgerSnap.docs.map((item) => ({ id: item.id, data: item.data() })),
|
|
122886
|
-
max
|
|
123036
|
+
max,
|
|
123037
|
+
planRead
|
|
122887
123038
|
}));
|
|
122888
123039
|
} catch (error3) {
|
|
122889
123040
|
if (isFirestorePermissionDenied(error3))
|
|
@@ -122958,6 +123109,7 @@ function workspaceRefusalResult(tool, error3, subject = {}) {
|
|
|
122958
123109
|
}
|
|
122959
123110
|
|
|
122960
123111
|
// build/tools/workspaces.js
|
|
123112
|
+
var INVITE_EMAIL_PATTERN = /^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]{1,64}@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/;
|
|
122961
123113
|
function registerWorkspaceTools(server2) {
|
|
122962
123114
|
server2.registerTool("oko_list_workspaces", {
|
|
122963
123115
|
title: "List workspaces",
|
|
@@ -123198,8 +123350,8 @@ function registerWorkspaceTools(server2) {
|
|
|
123198
123350
|
description: toolDescription({
|
|
123199
123351
|
summary: "List workspace invite links for a workspace owned by the organizer.",
|
|
123200
123352
|
args: ["workspaceId: workspace owned by the organizer."],
|
|
123201
|
-
returns: "invites array with code, claim status, permissions, and inviteUrl.",
|
|
123202
|
-
examples: ["Use before revoking or sharing an invite code."]
|
|
123353
|
+
returns: "invites array with code, claim status, permissions, and inviteUrl; for an invite created with an email also mailStatus (queued | sent | not_sent) and, when not_sent, mailReason (rate_limited | address | not_allowed | undeliverable) \u2014 what the platform did with the letter. No mailStatus on an invite with an email means the platform has not reported yet: do not read it as sent. sent means the provider accepted the letter, not that it was delivered.",
|
|
123354
|
+
examples: ["Use before revoking or sharing an invite code, and to check whether the platform's letter went out."]
|
|
123203
123355
|
}),
|
|
123204
123356
|
inputSchema: { workspaceId: external_exports.string().min(1) },
|
|
123205
123357
|
outputSchema: external_exports.object({ workspaceId: external_exports.string(), invites: external_exports.array(unknownRecordSchema), count: external_exports.number().int().nonnegative() }).passthrough(),
|
|
@@ -123271,20 +123423,23 @@ function registerWorkspaceTools(server2) {
|
|
|
123271
123423
|
server2.registerTool("oko_create_workspace_invite", {
|
|
123272
123424
|
title: "Create workspace invite",
|
|
123273
123425
|
description: toolDescription({
|
|
123274
|
-
summary: "Create a workspace invite for a known OKO workspace.",
|
|
123426
|
+
summary: "Create a workspace invite for a known OKO workspace. When `email` is given, OKO Stream itself emails the invitation to that address: an external effect, not a note.",
|
|
123275
123427
|
args: [
|
|
123276
123428
|
"workspaceId: workspace owned by the authenticated organizer.",
|
|
123277
123429
|
"code: optional invite code, 16-64 chars; omitted means generated (recommended).",
|
|
123278
|
-
"email: optional
|
|
123430
|
+
"email: optional email of the invitee, one plain address (`name@domain`). When given, the PLATFORM sends the invitation letter to it \u2014 the workspace name and the invite link, in the language of the account that owns that address when there is one and it has a language, otherwise in the inviter's language, otherwise English; the `comment` is never put into the letter. A letter cannot be recalled: confirm the exact address with the workspace owner first. Omit it to get only the link.",
|
|
123279
123431
|
// The schema is `workspacePermissionsSchema`: nine booleans, all
|
|
123280
123432
|
// required, no extra keys. "optional whitelist" read as "send the
|
|
123281
123433
|
// ones you want", and every such call was rejected by validation
|
|
123282
123434
|
// before it reached the platform.
|
|
123283
123435
|
"permissions: required object of all nine workspace permission booleans (createEvents, editSetup, deleteEvents, controlLive, moderateChat, startSession, viewAnalytics, downloadReports, sendNotifications) \u2014 every key must be present, extra keys are rejected; set false for the ones the invitee should not get.",
|
|
123284
|
-
"confirm: must be true."
|
|
123436
|
+
"confirm: must be true. Without it the call is refused with a preview that names the address the platform would email."
|
|
123285
123437
|
],
|
|
123286
|
-
returns: "workspaceId, invite code, inviteUrl, and
|
|
123287
|
-
examples: ["Use only after confirming the recipient and permission set with the workspace owner."]
|
|
123438
|
+
returns: "workspaceId, invite code, inviteUrl, created=true, and emailRequested=true when an address was given: the platform queues the letter AFTER the invite exists, and this result does not say the letter was delivered \u2014 what became of it is mailStatus on the invite, read with oko_list_workspace_invites a moment later.",
|
|
123439
|
+
examples: ["Use only after confirming the recipient and permission set with the workspace owner."],
|
|
123440
|
+
notes: [
|
|
123441
|
+
"The invite itself and its inviteUrl are always created and always work. The LETTER is held to limits and is checked again just before it goes out: at most 1 letter an hour for one workspace and address, 3 a day to one address, 20 a day per inviter; and none if the invite was revoked or accepted meanwhile, or the plan has no free seat. When no letter goes out the invite stays valid: pass inviteUrl on yourself \u2014 mailStatus not_sent says so (mailReason not_allowed means the invite cannot be accepted right now, so passing the link on would not help either)."
|
|
123442
|
+
]
|
|
123288
123443
|
}),
|
|
123289
123444
|
inputSchema: {
|
|
123290
123445
|
workspaceId: external_exports.string().min(1),
|
|
@@ -123296,25 +123451,31 @@ function registerWorkspaceTools(server2) {
|
|
|
123296
123451
|
// deliberately stays 4: they accept codes that ALREADY exist, including
|
|
123297
123452
|
// the historical six-character ones.
|
|
123298
123453
|
code: external_exports.string().min(16).max(MAX_WORKSPACE_INVITE_CODE_LENGTH).optional(),
|
|
123299
|
-
email: external_exports.string().email().optional(),
|
|
123454
|
+
email: external_exports.string().max(254).regex(INVITE_EMAIL_PATTERN, "One plain email address (name@domain), no names, lists or spaces.").optional().describe("Optional. When given, OKO Stream emails the invitation to this address."),
|
|
123300
123455
|
comment: external_exports.string().max(500).default(""),
|
|
123301
123456
|
permissions: workspacePermissionsSchema,
|
|
123302
|
-
confirm: external_exports.boolean().optional()
|
|
123457
|
+
confirm: external_exports.boolean().optional().describe("Required true. With an email, true also means the workspace owner has confirmed that address.")
|
|
123303
123458
|
},
|
|
123304
|
-
outputSchema: external_exports.object({ workspaceId: external_exports.string(), code: external_exports.string(), inviteUrl: external_exports.string().url(), created: external_exports.boolean() }).passthrough(),
|
|
123459
|
+
outputSchema: external_exports.object({ workspaceId: external_exports.string(), code: external_exports.string(), inviteUrl: external_exports.string().url(), created: external_exports.boolean(), emailRequested: external_exports.boolean().optional() }).passthrough(),
|
|
123305
123460
|
annotations: writeTool
|
|
123306
123461
|
}, async (args) => {
|
|
123307
123462
|
const { uid } = await authed(["workspace:write"]);
|
|
123308
123463
|
const workspace = await readWorkspaceForOwner(args.workspaceId, uid);
|
|
123309
123464
|
if (!isSharedWorkspaceId(args.workspaceId))
|
|
123310
123465
|
throw new Error(personalWorkspaceLimitMessage("creating invites"));
|
|
123311
|
-
requireConfirm(args.confirm, "oko_create_workspace_invite", {
|
|
123466
|
+
requireConfirm(args.confirm, "oko_create_workspace_invite", {
|
|
123467
|
+
workspaceId: args.workspaceId,
|
|
123468
|
+
code: args.code,
|
|
123469
|
+
email: args.email,
|
|
123470
|
+
...args.email ? { platformEmailsThisAddress: true } : {}
|
|
123471
|
+
});
|
|
123312
123472
|
const code = (args.code?.trim() || randomInviteCode()).toUpperCase();
|
|
123313
123473
|
const url = inviteUrl(code);
|
|
123474
|
+
const email2 = args.email?.trim() ?? "";
|
|
123314
123475
|
await setDoc(doc(firestore, "workspaceInvites", code), {
|
|
123315
123476
|
workspaceId: args.workspaceId,
|
|
123316
123477
|
workspaceName: safeString(workspace.name, "Workspace"),
|
|
123317
|
-
email:
|
|
123478
|
+
email: email2,
|
|
123318
123479
|
comment: args.comment,
|
|
123319
123480
|
permissions: normalizeWorkspacePermissions(args.permissions),
|
|
123320
123481
|
createdBy: uid,
|
|
@@ -123322,7 +123483,7 @@ function registerWorkspaceTools(server2) {
|
|
|
123322
123483
|
claimed: false,
|
|
123323
123484
|
claimedBy: null
|
|
123324
123485
|
});
|
|
123325
|
-
return textResult({ workspaceId: args.workspaceId, code, inviteUrl: url, created: true });
|
|
123486
|
+
return textResult({ workspaceId: args.workspaceId, code, inviteUrl: url, created: true, ...email2 ? { emailRequested: true } : {} });
|
|
123326
123487
|
});
|
|
123327
123488
|
}
|
|
123328
123489
|
|