@charisol/plexo-mcp 1.0.9 → 1.0.10

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.
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LMS_TOOL_NAMES = exports.LMS_TOOL_DEFS = void 0;
4
+ exports.handleLmsToolCall = handleLmsToolCall;
5
+ exports.LMS_TOOL_DEFS = [
6
+ {
7
+ name: "create_funnel",
8
+ description: "Attaches Funnel/LMS capability to an existing root landing page (see create_landing_page / list_landing_pages for how to get one), seeding a single Welcome page. Gated by the organization's funnel slot allowance (get_funnel_addon_slots shows current usage) — fails with the slot summary if none is available.",
9
+ inputSchema: {
10
+ type: "object",
11
+ properties: { templateId: { type: "string", description: "An existing root landing page's template id to turn into a funnel." } },
12
+ required: ["templateId"],
13
+ },
14
+ },
15
+ {
16
+ name: "get_funnel",
17
+ description: "Fetches a funnel's settings: enabled state, signup mode (FREE/PAID), signup product, and welcome-page options.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
21
+ required: ["templateId"],
22
+ },
23
+ },
24
+ {
25
+ name: "update_funnel",
26
+ description: "Updates a funnel's settings. Only the fields provided are changed. Switching signupMode to PAID requires signupProductId (a DIGITAL Commerce product) to already be set or provided here.",
27
+ inputSchema: {
28
+ type: "object",
29
+ properties: {
30
+ templateId: { type: "string", description: "The funnel's home page template id." },
31
+ enabled: { type: "boolean" },
32
+ signupMode: { type: "string", enum: ["FREE", "PAID"] },
33
+ signupProductId: { type: "string", description: "A DIGITAL Commerce product id (see create_commerce_product). Pass an empty string to clear it." },
34
+ requireName: { type: "boolean" },
35
+ collectPhone: { type: "boolean" },
36
+ welcomeRedirectPath: { type: "string", description: "Pass an empty string to clear it." },
37
+ },
38
+ required: ["templateId"],
39
+ },
40
+ },
41
+ {
42
+ name: "delete_funnel",
43
+ description: "Soft-deletes a funnel (enabled:false — hidden from visitors). Pages, enrollments, and progress are kept, so re-enabling later restores everything as-is.",
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
47
+ required: ["templateId"],
48
+ },
49
+ },
50
+ {
51
+ name: "list_funnel_pages",
52
+ description: "Lists a funnel's pages in their display order.",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
56
+ required: ["templateId"],
57
+ },
58
+ },
59
+ {
60
+ name: "create_funnel_page",
61
+ description: "Appends a new page to a funnel. Defaults to kind VIDEO and unlocks after the current last page — use update_funnel_page to customize unlockRules afterward.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ templateId: { type: "string", description: "The funnel's home page template id." },
66
+ title: { type: "string", description: "Defaults to \"Page N\"." },
67
+ kind: { type: "string", enum: ["WELCOME", "VIDEO", "DOCUMENT", "IMAGE"], description: "Defaults to VIDEO." },
68
+ },
69
+ required: ["templateId"],
70
+ },
71
+ },
72
+ {
73
+ name: "update_funnel_page",
74
+ description: "Updates a funnel page's content and/or unlock rules. Only the fields provided are changed. unlockRules shape: { mode: \"ALL\", conditions: [...] } where each condition is one of PREREQUISITE_PAGES ({type,pageIds}), TIME_DRIP_RELATIVE ({type,days}), TIME_DRIP_FIXED_DATE ({type,date}), MANUAL_APPROVAL ({type}) — satisfied via approve_funnel_enrollment_page.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ templateId: { type: "string", description: "The funnel's home page template id." },
79
+ pageId: { type: "string" },
80
+ title: { type: "string" },
81
+ kind: { type: "string", enum: ["WELCOME", "VIDEO", "DOCUMENT", "IMAGE"] },
82
+ contentMode: { type: "string", enum: ["SIMPLE", "RAW_HTML", "ADVANCED"], description: "SIMPLE/RAW_HTML both write to bodyHtml; ADVANCED is a full-builder page and isn't settable from here." },
83
+ bodyHtml: { type: "string", description: "Sanitized server-side. For a WELCOME page this renders as plain text, not markup." },
84
+ youtubeVideoId: { type: "string", description: "Empty string clears it." },
85
+ assetUrl: { type: "string", description: "Already-uploaded file URL (this tool can't accept raw bytes). Empty string clears it." },
86
+ assetFileName: { type: "string" },
87
+ assetMimeType: { type: "string" },
88
+ assetSizeBytes: { type: "number" },
89
+ unlockRules: { type: "object", description: "See this tool's description for the shape." },
90
+ visible: { type: "boolean" },
91
+ },
92
+ required: ["templateId", "pageId"],
93
+ },
94
+ },
95
+ {
96
+ name: "delete_funnel_page",
97
+ description: "Deletes a funnel page and closes the resulting order gap in the remaining pages.",
98
+ inputSchema: {
99
+ type: "object",
100
+ properties: {
101
+ templateId: { type: "string", description: "The funnel's home page template id." },
102
+ pageId: { type: "string" },
103
+ },
104
+ required: ["templateId", "pageId"],
105
+ },
106
+ },
107
+ {
108
+ name: "reorder_funnel_pages",
109
+ description: "Reorders a funnel's pages. orderedPageIds must contain exactly this funnel's current page ids, in the new order.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ templateId: { type: "string", description: "The funnel's home page template id." },
114
+ orderedPageIds: { type: "array", items: { type: "string" } },
115
+ },
116
+ required: ["templateId", "orderedPageIds"],
117
+ },
118
+ },
119
+ {
120
+ name: "list_funnel_enrollments",
121
+ description: "Lists a funnel's learner enrollments, most recently enrolled first.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: { templateId: { type: "string", description: "The funnel's home page template id." } },
125
+ required: ["templateId"],
126
+ },
127
+ },
128
+ {
129
+ name: "get_funnel_enrollment",
130
+ description: "Fetches one learner's enrollment plus their per-page progress.",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ templateId: { type: "string", description: "The funnel's home page template id." },
135
+ email: { type: "string" },
136
+ },
137
+ required: ["templateId", "email"],
138
+ },
139
+ },
140
+ {
141
+ name: "approve_funnel_enrollment_page",
142
+ description: "Marks one page as manually approved for one learner — the way a MANUAL_APPROVAL unlock condition gets satisfied (see update_funnel_page's unlockRules description). Reversible: calling it again just re-stamps the approval.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ templateId: { type: "string", description: "The funnel's home page template id." },
147
+ email: { type: "string" },
148
+ pageId: { type: "string" },
149
+ },
150
+ required: ["templateId", "email", "pageId"],
151
+ },
152
+ },
153
+ {
154
+ name: "set_funnel_enrollment_status",
155
+ description: "Revokes or reactivates a learner's enrollment. Revoked takes effect immediately on their next page load (no session to separately invalidate).",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ templateId: { type: "string", description: "The funnel's home page template id." },
160
+ email: { type: "string" },
161
+ status: { type: "string", enum: ["ACTIVE", "REVOKED"] },
162
+ },
163
+ required: ["templateId", "email", "status"],
164
+ },
165
+ },
166
+ {
167
+ name: "resend_funnel_magic_link",
168
+ description: "Re-sends the passwordless login email to an ACTIVE learner. Rate-limited to once per 60 seconds per learner.",
169
+ inputSchema: {
170
+ type: "object",
171
+ properties: {
172
+ templateId: { type: "string", description: "The funnel's home page template id." },
173
+ email: { type: "string" },
174
+ },
175
+ required: ["templateId", "email"],
176
+ },
177
+ },
178
+ {
179
+ name: "get_funnel_addon_slots",
180
+ description: "Reports the organization's funnel slot usage (plan-included + purchased addon slots vs. funnels currently using one) and any active addon-slot subscriptions. Org-scoped, not per-site.",
181
+ inputSchema: { type: "object", properties: {} },
182
+ },
183
+ ];
184
+ exports.LMS_TOOL_NAMES = new Set(exports.LMS_TOOL_DEFS.map((t) => t.name));
185
+ async function handleLmsToolCall(name, args, client) {
186
+ const templateId = typeof args?.templateId === "string" ? args.templateId : "";
187
+ switch (name) {
188
+ case "create_funnel": {
189
+ if (!templateId)
190
+ throw new Error("templateId is required.");
191
+ const result = await client.createFunnel(templateId);
192
+ return { success: true, funnel: result.funnel };
193
+ }
194
+ case "get_funnel":
195
+ return await client.getFunnel(templateId);
196
+ case "update_funnel": {
197
+ const { templateId: _t, ...payload } = args;
198
+ const result = await client.updateFunnel(templateId, payload);
199
+ return { success: true, funnel: result.funnel };
200
+ }
201
+ case "delete_funnel":
202
+ await client.deleteFunnel(templateId);
203
+ return { success: true };
204
+ case "list_funnel_pages":
205
+ return await client.listFunnelPages(templateId);
206
+ case "create_funnel_page": {
207
+ const result = await client.createFunnelPage(templateId, { title: args.title, kind: args.kind });
208
+ return { success: true, page: result.page };
209
+ }
210
+ case "update_funnel_page": {
211
+ if (!args.pageId)
212
+ throw new Error("pageId is required.");
213
+ const { templateId: _t, pageId, ...payload } = args;
214
+ const result = await client.updateFunnelPage(templateId, pageId, payload);
215
+ return { success: true, page: result.page };
216
+ }
217
+ case "delete_funnel_page":
218
+ if (!args.pageId)
219
+ throw new Error("pageId is required.");
220
+ await client.deleteFunnelPage(templateId, args.pageId);
221
+ return { success: true, deletedPageId: args.pageId };
222
+ case "reorder_funnel_pages": {
223
+ if (!Array.isArray(args.orderedPageIds) || args.orderedPageIds.length === 0)
224
+ throw new Error("orderedPageIds is required.");
225
+ return await client.reorderFunnelPages(templateId, args.orderedPageIds);
226
+ }
227
+ case "list_funnel_enrollments":
228
+ return await client.listFunnelEnrollments(templateId);
229
+ case "get_funnel_enrollment":
230
+ if (!args.email)
231
+ throw new Error("email is required.");
232
+ return await client.getFunnelEnrollment(templateId, args.email);
233
+ case "approve_funnel_enrollment_page": {
234
+ if (!args.email || !args.pageId)
235
+ throw new Error("email and pageId are required.");
236
+ const result = await client.approveFunnelEnrollmentPage(templateId, args.email, args.pageId);
237
+ return { success: true, progress: result.progress };
238
+ }
239
+ case "set_funnel_enrollment_status": {
240
+ if (!args.email || !args.status)
241
+ throw new Error("email and status are required.");
242
+ const result = await client.setFunnelEnrollmentStatus(templateId, args.email, args.status);
243
+ return { success: true, enrollment: result.enrollment };
244
+ }
245
+ case "resend_funnel_magic_link":
246
+ if (!args.email)
247
+ throw new Error("email is required.");
248
+ return await client.resendFunnelMagicLink(templateId, args.email);
249
+ case "get_funnel_addon_slots":
250
+ return await client.getFunnelAddonSlots();
251
+ default:
252
+ throw new Error(`Unknown tool: ${name}`);
253
+ }
254
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@charisol/plexo-mcp",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Model Context Protocol (MCP) Server for Plexo - Landing page generation, email templates, publishing, and analytics.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -570,4 +570,229 @@ export class PlexoClient {
570
570
  async deleteBlogComment(templateId: string, commentId: string): Promise<any> {
571
571
  return this.blogFetch("DELETE", templateId, `/comments/${encodeURIComponent(commentId)}`);
572
572
  }
573
+
574
+ /** Shared request helper for every /api/v1/commerce/{templateId}/... route below — the
575
+ * same admin endpoints the Plexo dashboard's own Commerce UI calls, same Bearer/x-api-key
576
+ * auth as the rest of this client (see resolveCommerceAdmin server-side). */
577
+ private async commerceFetch(method: string, templateId: string, path: string, body?: unknown): Promise<any> {
578
+ this.checkAuth();
579
+ const res = await fetch(`${this.baseUrl}/api/v1/commerce/${encodeURIComponent(templateId)}${path}`, {
580
+ method,
581
+ headers: this.getHeaders(),
582
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
583
+ });
584
+ const data = (await res.json().catch(() => ({}))) as any;
585
+ if (!res.ok) throw new Error(data?.error || `Commerce request failed (Status ${res.status})`);
586
+ return data;
587
+ }
588
+
589
+ async listCommerceProducts(templateId: string, params: { kind?: string; activeOnly?: boolean } = {}): Promise<any> {
590
+ const query = new URLSearchParams();
591
+ if (params.kind) query.set("kind", params.kind);
592
+ if (params.activeOnly) query.set("activeOnly", "true");
593
+ const qs = query.toString();
594
+ return this.commerceFetch("GET", templateId, `/products${qs ? `?${qs}` : ""}`);
595
+ }
596
+
597
+ async getCommerceProduct(templateId: string, productId: string): Promise<any> {
598
+ return this.commerceFetch("GET", templateId, `/products/${encodeURIComponent(productId)}`);
599
+ }
600
+
601
+ async createCommerceProduct(templateId: string, payload: Record<string, any>): Promise<any> {
602
+ return this.commerceFetch("POST", templateId, "/products", payload);
603
+ }
604
+
605
+ async updateCommerceProduct(templateId: string, productId: string, payload: Record<string, any>): Promise<any> {
606
+ return this.commerceFetch("PATCH", templateId, `/products/${encodeURIComponent(productId)}`, payload);
607
+ }
608
+
609
+ async deleteCommerceProduct(templateId: string, productId: string): Promise<any> {
610
+ return this.commerceFetch("DELETE", templateId, `/products/${encodeURIComponent(productId)}`);
611
+ }
612
+
613
+ async getCommerceSettings(templateId: string): Promise<any> {
614
+ return this.commerceFetch("GET", templateId, "/settings");
615
+ }
616
+
617
+ async updateCommerceSettings(templateId: string, payload: Record<string, any>): Promise<any> {
618
+ return this.commerceFetch("PUT", templateId, "/settings", payload);
619
+ }
620
+
621
+ async listCommerceOrders(templateId: string, params: { status?: string; q?: string; page?: number } = {}): Promise<any> {
622
+ const query = new URLSearchParams();
623
+ if (params.status) query.set("status", params.status);
624
+ if (params.q) query.set("q", params.q);
625
+ if (params.page) query.set("page", String(params.page));
626
+ const qs = query.toString();
627
+ return this.commerceFetch("GET", templateId, `/orders${qs ? `?${qs}` : ""}`);
628
+ }
629
+
630
+ async getCommerceOrder(templateId: string, orderId: string): Promise<any> {
631
+ return this.commerceFetch("GET", templateId, `/orders/${encodeURIComponent(orderId)}`);
632
+ }
633
+
634
+ async updateCommerceOrder(templateId: string, orderId: string, fulfillmentStatus: string): Promise<any> {
635
+ return this.commerceFetch("PATCH", templateId, `/orders/${encodeURIComponent(orderId)}`, { fulfillmentStatus });
636
+ }
637
+
638
+ async refundCommerceOrder(templateId: string, orderId: string): Promise<any> {
639
+ return this.commerceFetch("POST", templateId, `/orders/${encodeURIComponent(orderId)}/refund`);
640
+ }
641
+
642
+ async resendDigitalDelivery(templateId: string, orderId: string, deliveryId: string): Promise<any> {
643
+ return this.commerceFetch("POST", templateId, `/orders/${encodeURIComponent(orderId)}/resend-digital-delivery`, { deliveryId });
644
+ }
645
+
646
+ async getCommerceWallet(templateId: string): Promise<any> {
647
+ return this.commerceFetch("GET", templateId, "/wallet");
648
+ }
649
+
650
+ async listCommerceWithdrawals(templateId: string): Promise<any> {
651
+ return this.commerceFetch("GET", templateId, "/wallet/withdrawals");
652
+ }
653
+
654
+ async requestCommerceWithdrawal(templateId: string, payload: { amountCents: number; accountNumber: string; accountHolderName: string; bankName: string }): Promise<any> {
655
+ return this.commerceFetch("POST", templateId, "/wallet/withdrawals", payload);
656
+ }
657
+
658
+ async getCommerceStripeAccessStatus(): Promise<any> {
659
+ this.checkAuth();
660
+ const res = await fetch(`${this.baseUrl}/api/v1/commerce/stripe-access`, { method: "GET", headers: this.getHeaders() });
661
+ const data = (await res.json().catch(() => ({}))) as any;
662
+ if (!res.ok) throw new Error(data?.error || `Failed to fetch Stripe access status (Status ${res.status})`);
663
+ return data;
664
+ }
665
+
666
+ async requestCommerceStripeAccess(payload: { reason?: string; expectedVolume?: string } = {}): Promise<any> {
667
+ this.checkAuth();
668
+ const res = await fetch(`${this.baseUrl}/api/v1/commerce/stripe-access`, {
669
+ method: "POST",
670
+ headers: this.getHeaders(),
671
+ body: JSON.stringify(payload),
672
+ });
673
+ const data = (await res.json().catch(() => ({}))) as any;
674
+ if (!res.ok) throw new Error(data?.error || `Failed to request Stripe access (Status ${res.status})`);
675
+ return data;
676
+ }
677
+
678
+ async listCommerceDeliveryMethods(templateId: string): Promise<any> {
679
+ return this.commerceFetch("GET", templateId, "/delivery-methods");
680
+ }
681
+
682
+ async createCommerceDeliveryMethod(templateId: string, payload: Record<string, any>): Promise<any> {
683
+ return this.commerceFetch("POST", templateId, "/delivery-methods", payload);
684
+ }
685
+
686
+ async updateCommerceDeliveryMethod(templateId: string, id: string, payload: Record<string, any>): Promise<any> {
687
+ return this.commerceFetch("PATCH", templateId, `/delivery-methods/${encodeURIComponent(id)}`, payload);
688
+ }
689
+
690
+ async deleteCommerceDeliveryMethod(templateId: string, id: string): Promise<any> {
691
+ return this.commerceFetch("DELETE", templateId, `/delivery-methods/${encodeURIComponent(id)}`);
692
+ }
693
+
694
+ async listCommerceDiscounts(templateId: string): Promise<any> {
695
+ return this.commerceFetch("GET", templateId, "/discounts");
696
+ }
697
+
698
+ async createCommerceDiscount(templateId: string, payload: Record<string, any>): Promise<any> {
699
+ return this.commerceFetch("POST", templateId, "/discounts", payload);
700
+ }
701
+
702
+ async updateCommerceDiscount(templateId: string, id: string, payload: Record<string, any>): Promise<any> {
703
+ return this.commerceFetch("PATCH", templateId, `/discounts/${encodeURIComponent(id)}`, payload);
704
+ }
705
+
706
+ async deleteCommerceDiscount(templateId: string, id: string): Promise<any> {
707
+ return this.commerceFetch("DELETE", templateId, `/discounts/${encodeURIComponent(id)}`);
708
+ }
709
+
710
+ async listCommerceCustomers(templateId: string, page?: number): Promise<any> {
711
+ return this.commerceFetch("GET", templateId, `/customers${page ? `?page=${page}` : ""}`);
712
+ }
713
+
714
+ /** Shared request helper for every /api/v1/lms/funnels/{templateId}/... route below. */
715
+ private async lmsFetch(method: string, templateId: string, path: string, body?: unknown): Promise<any> {
716
+ this.checkAuth();
717
+ const res = await fetch(`${this.baseUrl}/api/v1/lms/funnels/${encodeURIComponent(templateId)}${path}`, {
718
+ method,
719
+ headers: this.getHeaders(),
720
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
721
+ });
722
+ const data = (await res.json().catch(() => ({}))) as any;
723
+ if (!res.ok) throw new Error(data?.error || `Funnel request failed (Status ${res.status})`);
724
+ return data;
725
+ }
726
+
727
+ async createFunnel(templateId: string): Promise<any> {
728
+ this.checkAuth();
729
+ const res = await fetch(`${this.baseUrl}/api/v1/lms/funnels`, {
730
+ method: "POST",
731
+ headers: this.getHeaders(),
732
+ body: JSON.stringify({ templateId }),
733
+ });
734
+ const data = (await res.json().catch(() => ({}))) as any;
735
+ if (!res.ok) throw new Error(data?.error || `Funnel creation failed (Status ${res.status})`);
736
+ return data;
737
+ }
738
+
739
+ async getFunnel(templateId: string): Promise<any> {
740
+ return this.lmsFetch("GET", templateId, "");
741
+ }
742
+
743
+ async updateFunnel(templateId: string, payload: Record<string, any>): Promise<any> {
744
+ return this.lmsFetch("PATCH", templateId, "", payload);
745
+ }
746
+
747
+ async deleteFunnel(templateId: string): Promise<any> {
748
+ return this.lmsFetch("DELETE", templateId, "");
749
+ }
750
+
751
+ async listFunnelPages(templateId: string): Promise<any> {
752
+ return this.lmsFetch("GET", templateId, "/pages");
753
+ }
754
+
755
+ async createFunnelPage(templateId: string, payload: { title?: string; kind?: string } = {}): Promise<any> {
756
+ return this.lmsFetch("POST", templateId, "/pages", payload);
757
+ }
758
+
759
+ async updateFunnelPage(templateId: string, pageId: string, payload: Record<string, any>): Promise<any> {
760
+ return this.lmsFetch("PATCH", templateId, `/pages/${encodeURIComponent(pageId)}`, payload);
761
+ }
762
+
763
+ async deleteFunnelPage(templateId: string, pageId: string): Promise<any> {
764
+ return this.lmsFetch("DELETE", templateId, `/pages/${encodeURIComponent(pageId)}`);
765
+ }
766
+
767
+ async reorderFunnelPages(templateId: string, orderedPageIds: string[]): Promise<any> {
768
+ return this.lmsFetch("POST", templateId, "/pages/reorder", { orderedPageIds });
769
+ }
770
+
771
+ async listFunnelEnrollments(templateId: string): Promise<any> {
772
+ return this.lmsFetch("GET", templateId, "/enrollments");
773
+ }
774
+
775
+ async getFunnelEnrollment(templateId: string, email: string): Promise<any> {
776
+ return this.lmsFetch("GET", templateId, `/enrollments/${encodeURIComponent(email)}`);
777
+ }
778
+
779
+ async setFunnelEnrollmentStatus(templateId: string, email: string, status: "ACTIVE" | "REVOKED"): Promise<any> {
780
+ return this.lmsFetch("PATCH", templateId, `/enrollments/${encodeURIComponent(email)}`, { status });
781
+ }
782
+
783
+ async approveFunnelEnrollmentPage(templateId: string, email: string, pageId: string): Promise<any> {
784
+ return this.lmsFetch("POST", templateId, `/enrollments/${encodeURIComponent(email)}/approve`, { pageId });
785
+ }
786
+
787
+ async resendFunnelMagicLink(templateId: string, email: string): Promise<any> {
788
+ return this.lmsFetch("POST", templateId, `/enrollments/${encodeURIComponent(email)}/resend-magic-link`);
789
+ }
790
+
791
+ async getFunnelAddonSlots(): Promise<any> {
792
+ this.checkAuth();
793
+ const res = await fetch(`${this.baseUrl}/api/v1/lms/addon-slots`, { method: "GET", headers: this.getHeaders() });
794
+ const data = (await res.json().catch(() => ({}))) as any;
795
+ if (!res.ok) throw new Error(data?.error || `Failed to fetch funnel addon slots (Status ${res.status})`);
796
+ return data;
797
+ }
573
798
  }