@okostream/mcp 0.18.2 → 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.
Files changed (3) hide show
  1. package/README.md +19 -10
  2. package/dist/server.js +178 -27
  3. 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 public price list is at
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 lumen balance, the grants and
101
- the ledger of the organizer behind the key (scope `wallet:read`, included in
102
- the `operator` and `full` presets, not in `readOnly`). Keys created before that scope existed get `SCOPE_REQUIRED` and
103
- need a new key. A page in a shared workspace is paid from the workspace
104
- owner's wallet: its balance is told, in the previews and refusals, to whoever
105
- may spend from it; its grants and ledger are the owner's to read.
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 no
141
- longer publishes into (Pro: only the workspace its owner chose as active).
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 the lumen balance and ledger of your own wallet (`oko_wallet`).
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.2";
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 (the plan holds fewer shared workspaces than the owner has \u2014 on Pro only the workspace the owner chose as active takes NEW publications; the owner picks it on the OKO cabinet's Workspaces page, or moves to Max), EDIT_PERMISSION_REQUIRED, EVENT_NOT_FOUND, EVENT_IN_TRASH, EVENT_DATE_LOCKED (from oko_update_event, not this tool).",
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: CF61 has no dry run of its own. With confirm: true the page becomes a draft and the charge is refunded in one step.`,
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 what will be refunded and to whom, and call again with confirm: true after explicit consent."],
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
- "This is how the event date moves on a published page: unpublish (refund), change the date with oko_update_event, publish again (new charge) \u2014 oko_update_event refuses to move the date directly while the page stays published (EVENT_DATE_LOCKED).",
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
- refunded: preview.tokensCharged,
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: ["Unpublishing returns this page's publication charge to the wallet that paid and takes the page back to draft; the live layer (chat, presence, session start) closes immediately.", topUpsNote(preview.topUps)].filter(Boolean).join(" ")
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 refunded = typeof outcome.refunded === "number" ? outcome.refunded : 0;
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` : "its 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 refunds ${charge} to the wallet that paid, because its show window has not opened yet.${topUps ? ` ${topUps}` : ""}`;
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 = 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.`] : [];
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 wallet, the one behind this key. Read-only.",
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 lumens. Plans, lumens and payment details belong to the OKO web cabinet only; never ask the organizer for card data or a password.",
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))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okostream/mcp",
3
- "version": "0.18.2",
3
+ "version": "0.18.3",
4
4
  "description": "Control OKO Stream webinars from Claude, Codex, and other MCP clients.",
5
5
  "license": "UNLICENSED",
6
6
  "author": {