@kolbo/mcp 1.115.0 → 1.116.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/package.json +1 -1
- package/src/apps/widgets/plans.js +21 -6
- package/src/tools/_shared.js +65 -7
package/package.json
CHANGED
|
@@ -24,7 +24,11 @@ const { widgetPage } = require('../html');
|
|
|
24
24
|
* plans: [{ key, name, interval, credits, price, original_price,
|
|
25
25
|
* discount_percent, promo_text, currency, top_up_discount }],
|
|
26
26
|
* credit_packs: [{ …, subscriber_price, is_subscriber }],
|
|
27
|
-
* pricing_url
|
|
27
|
+
* pricing_url,
|
|
28
|
+
* // insufficient_credits only — leads the card (packs out-convert plans
|
|
29
|
+
* // for users who just ran out); every URL carries MCP attribution tags:
|
|
30
|
+
* recommended_pack: { …pack, covers_shortfall, url } | null,
|
|
31
|
+
* pack_url, home_url
|
|
28
32
|
* }
|
|
29
33
|
*/
|
|
30
34
|
|
|
@@ -52,11 +56,15 @@ var state = null;
|
|
|
52
56
|
var interval = 'month';
|
|
53
57
|
|
|
54
58
|
el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
|
|
55
|
-
|
|
59
|
+
function homeUrl() { return (state && state.home_url) || 'https://app.kolbo.ai'; }
|
|
60
|
+
el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink(homeUrl()); };
|
|
61
|
+
// Own handler so the shared untagged logo link (html.js) never fires here.
|
|
62
|
+
el('logo').onclick = function (e) { e.stopPropagation(); window.kolbo.openLink(homeUrl()); };
|
|
56
63
|
|
|
57
64
|
function pricingUrl() {
|
|
58
65
|
return (state && state.pricing_url) || 'https://app.kolbo.ai/pricing';
|
|
59
66
|
}
|
|
67
|
+
function packUrl() { return (state && state.pack_url) || pricingUrl(); }
|
|
60
68
|
function money(v, currency) {
|
|
61
69
|
if (v == null || isNaN(v)) return '';
|
|
62
70
|
var sym = { usd: '$', eur: '\u20ac', gbp: '\u00a3', ils: '\u20aa' }[String(currency || 'usd').toLowerCase()] || '';
|
|
@@ -76,7 +84,9 @@ function planCard(p, opts) {
|
|
|
76
84
|
var note = p.promo_text ? '<div class="k-plan-note">' + esc(p.promo_text) + '</div>'
|
|
77
85
|
: (opts.pack && p.is_subscriber && p.subscriber_price != null ? '<div class="k-plan-note">Subscriber price applied</div>' : '')
|
|
78
86
|
+ (!opts.pack && p.top_up_discount ? '<div class="k-plan-note">Cheaper credit packs included</div>' : '');
|
|
79
|
-
|
|
87
|
+
if (opts.lead) note += '<div class="k-plan-note">One-time top-up, no subscription</div>';
|
|
88
|
+
var primary = opts.pack ? opts.lead : !state.recommended_pack;
|
|
89
|
+
var cta = current ? '' : '<button class="k-btn ' + (primary ? 'primary' : 'ghost') + '" data-open="' + (opts.pack ? 'pack' : 'plan') + '">' + (opts.pack ? 'Buy pack' : 'Upgrade') + ' ' + ICONS.open + '</button>';
|
|
80
90
|
return '<div class="k-plan' + (current ? ' current' : '') + '">' +
|
|
81
91
|
'<div class="k-plan-top"><span class="k-plan-name">' + esc(p.name || p.key || 'Plan') + '</span>' + badge + '</div>' +
|
|
82
92
|
(p.credits != null ? '<div class="k-plan-credits">' + ICONS.sparkle + ' ' + esc(fmtCredits(p.credits)) + ' credits' + (opts.pack ? '' : ' / ' + (isAnnual(p) ? 'year' : 'month')) + '</div>' : '') +
|
|
@@ -85,12 +95,17 @@ function planCard(p, opts) {
|
|
|
85
95
|
|
|
86
96
|
function renderStage() {
|
|
87
97
|
var plans = (state.plans || []).filter(function (p) { return p && (p.price != null || p.credits != null); });
|
|
88
|
-
var
|
|
98
|
+
var lead = state.recommended_pack;
|
|
99
|
+
var packs = (state.credit_packs || []).filter(function (p) { return p && p.price != null && !(lead && lead.key != null && p.key === lead.key); });
|
|
89
100
|
var monthly = plans.filter(function (p) { return !isAnnual(p); });
|
|
90
101
|
var annual = plans.filter(isAnnual);
|
|
91
102
|
var hasToggle = monthly.length && annual.length;
|
|
92
103
|
var shown = hasToggle ? (interval === 'year' ? annual : monthly) : plans;
|
|
93
104
|
var html = '';
|
|
105
|
+
if (lead) {
|
|
106
|
+
html += '<div class="k-pack-head">Quickest fix</div><div class="k-plan-grid">' + planCard(lead, { pack: true, lead: true }) + '</div>' +
|
|
107
|
+
'<div class="k-pack-head">Or upgrade your plan</div>';
|
|
108
|
+
}
|
|
94
109
|
if (hasToggle) {
|
|
95
110
|
html += '<div class="k-plan-toggle">' +
|
|
96
111
|
'<button class="k-toggle-btn' + (interval === 'month' ? ' active' : '') + '" data-interval="month">Monthly</button>' +
|
|
@@ -98,7 +113,7 @@ function renderStage() {
|
|
|
98
113
|
}
|
|
99
114
|
if (shown.length) html += '<div class="k-plan-grid">' + shown.map(function (p) { return planCard(p, {}); }).join('') + '</div>';
|
|
100
115
|
if (packs.length) {
|
|
101
|
-
html += '<div class="k-pack-head">Credit packs</div><div class="k-plan-grid">' +
|
|
116
|
+
html += '<div class="k-pack-head">' + (lead ? 'Other credit packs' : 'Credit packs') + '</div><div class="k-plan-grid">' +
|
|
102
117
|
packs.map(function (p) { return planCard(p, { pack: true }); }).join('') + '</div>';
|
|
103
118
|
}
|
|
104
119
|
if (!html) html = '<div class="k-plan-note">Live plans and prices are on the Kolbo pricing page.</div>';
|
|
@@ -106,7 +121,7 @@ function renderStage() {
|
|
|
106
121
|
var btns = el('stage').querySelectorAll('[data-interval]');
|
|
107
122
|
for (var i = 0; i < btns.length; i++) btns[i].onclick = function () { interval = this.getAttribute('data-interval'); renderStage(); window.kolbo.notifySize(); };
|
|
108
123
|
var opens = el('stage').querySelectorAll('[data-open]');
|
|
109
|
-
for (var j = 0; j < opens.length; j++) opens[j].onclick = function () { window.kolbo.openLink(pricingUrl()); };
|
|
124
|
+
for (var j = 0; j < opens.length; j++) opens[j].onclick = function () { window.kolbo.openLink(this.getAttribute('data-open') === 'pack' ? packUrl() : pricingUrl()); };
|
|
110
125
|
}
|
|
111
126
|
|
|
112
127
|
function boot(sc) {
|
package/src/tools/_shared.js
CHANGED
|
@@ -1163,12 +1163,44 @@ function compactList(items, { fields, cap = 50, total, extra, note, maxChars = M
|
|
|
1163
1163
|
return text;
|
|
1164
1164
|
}
|
|
1165
1165
|
|
|
1166
|
+
// Every link out of the out-of-credits card carries the same tags so MCP
|
|
1167
|
+
// purchases are attributable. kolbo-map's /pricing reads only `category`
|
|
1168
|
+
// (month | annual | one_time) — no per-pack preselect exists, so the pack link
|
|
1169
|
+
// lands on the credit-pack tab.
|
|
1170
|
+
function upgradeCardUrl(path, content, query = {}) {
|
|
1171
|
+
const url = new URL(path, APP_BASE_URL);
|
|
1172
|
+
const params = {
|
|
1173
|
+
...query,
|
|
1174
|
+
utm_source: 'mcp',
|
|
1175
|
+
utm_medium: 'upgrade_card',
|
|
1176
|
+
utm_campaign: 'out_of_credits',
|
|
1177
|
+
utm_content: content,
|
|
1178
|
+
// kolbo-map's stable funnel-join fields (ANALYTICS_ATTRIBUTION_KEYS).
|
|
1179
|
+
attribution_link_id: `mcp_upgrade_card_${content}`,
|
|
1180
|
+
attribution_trigger: 'mcp_insufficient_credits',
|
|
1181
|
+
upgrade_entry: 'mcp_upgrade_card',
|
|
1182
|
+
};
|
|
1183
|
+
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
1184
|
+
return url.toString();
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Smallest pack that covers the shortfall; the largest when none does; the
|
|
1188
|
+
// smallest when the shortfall is unknown.
|
|
1189
|
+
function packForShortfall(packs, shortfall) {
|
|
1190
|
+
const sized = (packs || [])
|
|
1191
|
+
.filter((p) => p && Number.isFinite(p.credits) && p.price != null)
|
|
1192
|
+
.sort((a, b) => a.credits - b.credits);
|
|
1193
|
+
if (!sized.length) return null;
|
|
1194
|
+
return sized.find((p) => !(shortfall > 0) || p.credits >= shortfall) || sized[sized.length - 1];
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1166
1197
|
/**
|
|
1167
1198
|
* Turn a 403 INSUFFICIENT_CREDITS into the plans/upgrade card.
|
|
1168
1199
|
*
|
|
1169
1200
|
* Without this the user just sees the raw API sentence and has to go find the
|
|
1170
|
-
* pricing page themselves. The card states the shortfall,
|
|
1171
|
-
* promo-adjusted plans, and links
|
|
1201
|
+
* pricing page themselves. The card states the shortfall, leads with the
|
|
1202
|
+
* credit pack that covers it, shows live promo-adjusted plans below, and links
|
|
1203
|
+
* to app.kolbo.ai/pricing with MCP attribution tags.
|
|
1172
1204
|
*
|
|
1173
1205
|
* Returns null for any other error so callers can rethrow untouched. The plan
|
|
1174
1206
|
* fetch is best-effort: if it fails we still return a card carrying the
|
|
@@ -1209,30 +1241,56 @@ async function insufficientCreditsResult(client, err, options = {}) {
|
|
|
1209
1241
|
|
|
1210
1242
|
const balance = err.data?.balance ?? data?.credits?.total;
|
|
1211
1243
|
const required = err.data?.required;
|
|
1244
|
+
const shortfall = (Number.isFinite(balance) && Number.isFinite(required))
|
|
1245
|
+
? Math.max(0, required - balance) : undefined;
|
|
1246
|
+
const credit_packs = data?.credit_packs || [];
|
|
1247
|
+
const pack = packForShortfall(credit_packs, shortfall);
|
|
1248
|
+
const pack_url = upgradeCardUrl('/pricing', 'credit_pack', { category: 'one_time' });
|
|
1212
1249
|
const structured = {
|
|
1213
1250
|
widget: 'plans',
|
|
1214
1251
|
reason: 'insufficient_credits',
|
|
1215
1252
|
balance,
|
|
1216
1253
|
required,
|
|
1217
|
-
shortfall
|
|
1218
|
-
? Math.max(0, required - balance) : undefined,
|
|
1254
|
+
shortfall,
|
|
1219
1255
|
current_plan: data?.current_plan || null,
|
|
1256
|
+
// Leads the card: out of credits on the web app, packs convert 56% vs
|
|
1257
|
+
// plans 17.5%. Plans stay below as the alternative.
|
|
1258
|
+
recommended_pack: pack ? {
|
|
1259
|
+
...pack,
|
|
1260
|
+
// Only when the shortfall is known (and `required` is the API's lower
|
|
1261
|
+
// bound, so this is a floor, not a promise).
|
|
1262
|
+
covers_shortfall: shortfall > 0 && pack.credits >= shortfall,
|
|
1263
|
+
url: pack_url,
|
|
1264
|
+
} : null,
|
|
1220
1265
|
plans: data?.plans || [],
|
|
1221
|
-
credit_packs
|
|
1222
|
-
|
|
1266
|
+
credit_packs,
|
|
1267
|
+
// MCP-tagged, not the API's sdk-tagged link: every way out of this card
|
|
1268
|
+
// must be attributable to it.
|
|
1269
|
+
pricing_url: upgradeCardUrl('/pricing', 'plans'),
|
|
1270
|
+
pack_url,
|
|
1271
|
+
home_url: upgradeCardUrl('/', 'home'),
|
|
1223
1272
|
};
|
|
1224
1273
|
|
|
1225
1274
|
// structuredContent shadows the text on widget hosts, so the refusal reason
|
|
1226
1275
|
// has to be inside the object too — an agent that only reads structured
|
|
1227
1276
|
// content must still understand that nothing was generated.
|
|
1228
1277
|
structured.error = err.message;
|
|
1229
|
-
structured._hint = 'The generation did NOT run and nothing was charged. An upgrade card is shown to the user. Tell them they are out of credits and point at the card;
|
|
1278
|
+
structured._hint = 'The generation did NOT run and nothing was charged. An upgrade card is shown to the user. Tell them they are out of credits and point at the card; '
|
|
1279
|
+
+ (pack ? 'the quickest fix is the one-time credit pack it leads with (recommended_pack), with plans as the alternative; ' : '')
|
|
1280
|
+
+ 'do not retry the generation, and do not attempt to purchase anything for them.';
|
|
1230
1281
|
|
|
1231
1282
|
const text = JSON.stringify({
|
|
1232
1283
|
error: err.message,
|
|
1233
1284
|
code: 'INSUFFICIENT_CREDITS',
|
|
1234
1285
|
balance,
|
|
1235
1286
|
required,
|
|
1287
|
+
recommended_pack: structured.recommended_pack && {
|
|
1288
|
+
name: pack.name,
|
|
1289
|
+
credits: pack.credits,
|
|
1290
|
+
price: pack.price,
|
|
1291
|
+
currency: pack.currency,
|
|
1292
|
+
url: pack_url,
|
|
1293
|
+
},
|
|
1236
1294
|
pricing_url: structured.pricing_url,
|
|
1237
1295
|
_hint: structured._hint,
|
|
1238
1296
|
}, null, 2);
|