@charisol/plexo-mcp 1.0.9 → 1.0.11

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 CHANGED
@@ -16,6 +16,9 @@ Connect **Claude Desktop**, **Claude Web**, **ChatGPT**, **Cursor**, **Windsurf*
16
16
  - 📊 **`get_analytics`**: Fetches total page views, unique visitor counts, and daily timelines.
17
17
  - 👤 **`get_user_profile`**: Checks subscription plan, AI credit balance, and domain usage limits.
18
18
  - 🗑️ **`delete_published_domain`**: Unlinks or deletes published domains.
19
+ - 📝 **Blog, Commerce and Funnels**: full parity with the Plexo dashboard — posts, products, orders, refunds, bank transfers, discounts, delivery, customers and funnel pages.
20
+ - 💚 **Donations**: `list_donations`, `create_donation_campaign`, `update_donation_campaign`, `cancel_monthly_donation`, `adopt_monthly_donors` — one-time and monthly giving with receipts and a donor self-service cancel link.
21
+ - 🔁 **Move from WordPress**: `preview_wordpress_migration`, `start_wordpress_migration`, `get_wordpress_migration` (call repeatedly — each call advances the move), `connect_wordpress_migrator_plugin`, `import_wordpress_private_data`, `get_domain_switch_plan`, `verify_domain_switch` — copies a whole WordPress site (pages, posts, media, theme, forms, donations, SEO, same URLs) and guides the domain switch safely.
19
22
 
20
23
  ---
21
24
 
@@ -69,3 +72,5 @@ If `PLEXO_API_KEY` is omitted, the tool cleanly prompts:
69
72
  - *"Generate a modern tech SaaS landing page for an AI CRM called Acme and publish it using Plexo to subdomain acme-crm"*
70
73
  - *"Show me my page view analytics for the last 7 days"*
71
74
  - *"List all my email templates"*
75
+ - *"Move my WordPress site example.org to Plexo"*
76
+ - *"Add a donation form with $25, $50 and $100 options and monthly giving to my homepage"*
package/dist/blogTools.js CHANGED
@@ -3,26 +3,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BLOG_TOOL_NAMES = exports.BLOG_TOOL_DEFS = void 0;
4
4
  exports.handleBlogToolCall = handleBlogToolCall;
5
5
  const BLOG_LAYOUT_EXAMPLE = `
6
+ DESIGN QUALITY applies here exactly as in publish_landing_page's description: pick one small ink/paper/accent palette and a serif-heading + sans-body (+ optional uppercase-mono eyebrow) type system, keep borderRadius near 0, use boxShadow sparingly, and give blog_content a constrained maxWidth (~680-760px) so long-form reading stays comfortable — do not default to generic #ffffff/Inter/fontWeight-800.
7
+
6
8
  EXAMPLE — a single-post layout with a hero image, title, meta line, and content:
7
9
  {
8
10
  "body": {
9
- "style": { "backgroundColor": "#ffffff", "color": "#1e293b", "fontFamily": "Inter, sans-serif", "htmlTitle": "Blog Post" },
11
+ "style": { "backgroundColor": "#FAF7F1", "color": "#1B2430", "fontFamily": "'Inter', sans-serif", "htmlTitle": "Blog Post" },
10
12
  "rows": [
11
13
  { "id": "row-hero", "style": { "paddingTop": "0px" }, "columns": [
12
14
  { "id": "col-hero", "width": "100%", "elements": [
13
15
  { "id": "el-featured", "type": "blog_featured_image", "style": { "width": "100%", "maxHeight": "420px", "objectFit": "cover" }, "attributes": {} }
14
16
  ] }
15
17
  ] },
16
- { "id": "row-meta", "style": { "paddingTop": "32px", "paddingBottom": "8px" }, "columns": [
18
+ { "id": "row-meta", "style": { "paddingTop": "40px", "paddingBottom": "8px", "maxWidth": "760px", "margin": "0 auto" }, "columns": [
17
19
  { "id": "col-title", "width": "100%", "elements": [
18
- { "id": "el-title", "type": "blog_title", "style": { "fontSize": "40px", "fontWeight": "800" }, "attributes": {} },
19
- { "id": "el-authordate", "type": "blog_author", "style": { "fontSize": "14px", "color": "#64748b", "display": "inline-block", "marginRight": "12px" }, "attributes": {} },
20
- { "id": "el-date", "type": "blog_date", "style": { "fontSize": "14px", "color": "#64748b", "display": "inline-block" }, "attributes": {} }
20
+ { "id": "el-title", "type": "blog_title", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "40px", "lineHeight": "1.15" }, "attributes": {} },
21
+ { "id": "el-authordate", "type": "blog_author", "style": { "fontFamily": "'IBM Plex Mono', monospace", "fontSize": "12px", "letterSpacing": "0.1em", "textTransform": "uppercase", "color": "#5B6472", "display": "inline-block", "marginRight": "12px", "marginTop": "16px" }, "attributes": {} },
22
+ { "id": "el-date", "type": "blog_date", "style": { "fontFamily": "'IBM Plex Mono', monospace", "fontSize": "12px", "letterSpacing": "0.1em", "textTransform": "uppercase", "color": "#5B6472", "display": "inline-block" }, "attributes": {} }
21
23
  ] }
22
24
  ] },
23
- { "id": "row-body", "style": { "paddingTop": "24px", "paddingBottom": "48px" }, "columns": [
25
+ { "id": "row-body", "style": { "paddingTop": "24px", "paddingBottom": "48px", "maxWidth": "760px", "margin": "0 auto" }, "columns": [
24
26
  { "id": "col-body", "width": "100%", "elements": [
25
- { "id": "el-content", "type": "blog_content", "style": { "fontSize": "17px", "lineHeight": "1.7" }, "attributes": {} },
27
+ { "id": "el-content", "type": "blog_content", "style": { "fontFamily": "'Inter', sans-serif", "fontSize": "17px", "lineHeight": "1.75", "color": "#1B2430" }, "attributes": {} },
26
28
  { "id": "el-comments", "type": "blog_comments", "style": { "marginTop": "48px" }, "attributes": {} }
27
29
  ] }
28
30
  ] }
@@ -33,14 +35,15 @@ EXAMPLE — a single-post layout with a hero image, title, meta line, and conten
33
35
  EXAMPLE — a listing layout with a 3-column post grid:
34
36
  {
35
37
  "body": {
36
- "style": { "backgroundColor": "#ffffff", "color": "#1e293b", "fontFamily": "Inter, sans-serif", "htmlTitle": "Blog" },
38
+ "style": { "backgroundColor": "#FAF7F1", "color": "#1B2430", "fontFamily": "'Inter', sans-serif", "htmlTitle": "Blog" },
37
39
  "rows": [
38
- { "id": "row-heading", "style": { "paddingTop": "48px", "paddingBottom": "16px" }, "columns": [
40
+ { "id": "row-heading", "style": { "paddingTop": "56px", "paddingBottom": "16px" }, "columns": [
39
41
  { "id": "col-heading", "width": "100%", "elements": [
40
- { "id": "el-heading", "type": "heading", "style": { "fontSize": "36px", "fontWeight": "800", "textAlign": "center" }, "attributes": { "text": "Latest Posts" } }
42
+ { "id": "el-eyebrow", "type": "text", "style": { "fontFamily": "'IBM Plex Mono', monospace", "fontSize": "12px", "letterSpacing": "0.16em", "textTransform": "uppercase", "color": "#C4623B", "textAlign": "center" }, "attributes": { "text": "Journal" } },
43
+ { "id": "el-heading", "type": "heading", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "36px", "textAlign": "center", "margin": "8px 0 0" }, "attributes": { "text": "Latest Posts" } }
41
44
  ] }
42
45
  ] },
43
- { "id": "row-list", "style": { "paddingBottom": "48px" }, "columns": [
46
+ { "id": "row-list", "style": { "paddingBottom": "56px" }, "columns": [
44
47
  { "id": "col-list", "width": "100%", "elements": [
45
48
  { "id": "el-postlist", "type": "blog_post_list", "style": { "gridColumns": "3" }, "attributes": {} }
46
49
  ] }
@@ -450,5 +450,274 @@ class PlexoClient {
450
450
  async deleteBlogComment(templateId, commentId) {
451
451
  return this.blogFetch("DELETE", templateId, `/comments/${encodeURIComponent(commentId)}`);
452
452
  }
453
+ /** Shared request helper for every /api/v1/commerce/{templateId}/... route below — the
454
+ * same admin endpoints the Plexo dashboard's own Commerce UI calls, same Bearer/x-api-key
455
+ * auth as the rest of this client (see resolveCommerceAdmin server-side). */
456
+ async commerceFetch(method, templateId, path, body) {
457
+ this.checkAuth();
458
+ const res = await fetch(`${this.baseUrl}/api/v1/commerce/${encodeURIComponent(templateId)}${path}`, {
459
+ method,
460
+ headers: this.getHeaders(),
461
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
462
+ });
463
+ const data = (await res.json().catch(() => ({})));
464
+ if (!res.ok)
465
+ throw new Error(data?.error || `Commerce request failed (Status ${res.status})`);
466
+ return data;
467
+ }
468
+ async listCommerceProducts(templateId, params = {}) {
469
+ const query = new URLSearchParams();
470
+ if (params.kind)
471
+ query.set("kind", params.kind);
472
+ if (params.activeOnly)
473
+ query.set("activeOnly", "true");
474
+ const qs = query.toString();
475
+ return this.commerceFetch("GET", templateId, `/products${qs ? `?${qs}` : ""}`);
476
+ }
477
+ async getCommerceProduct(templateId, productId) {
478
+ return this.commerceFetch("GET", templateId, `/products/${encodeURIComponent(productId)}`);
479
+ }
480
+ async createCommerceProduct(templateId, payload) {
481
+ return this.commerceFetch("POST", templateId, "/products", payload);
482
+ }
483
+ async updateCommerceProduct(templateId, productId, payload) {
484
+ return this.commerceFetch("PATCH", templateId, `/products/${encodeURIComponent(productId)}`, payload);
485
+ }
486
+ async deleteCommerceProduct(templateId, productId) {
487
+ return this.commerceFetch("DELETE", templateId, `/products/${encodeURIComponent(productId)}`);
488
+ }
489
+ async getCommerceSettings(templateId) {
490
+ return this.commerceFetch("GET", templateId, "/settings");
491
+ }
492
+ async updateCommerceSettings(templateId, payload) {
493
+ return this.commerceFetch("PUT", templateId, "/settings", payload);
494
+ }
495
+ async listCommerceOrders(templateId, params = {}) {
496
+ const query = new URLSearchParams();
497
+ if (params.status)
498
+ query.set("status", params.status);
499
+ if (params.q)
500
+ query.set("q", params.q);
501
+ if (params.page)
502
+ query.set("page", String(params.page));
503
+ const qs = query.toString();
504
+ return this.commerceFetch("GET", templateId, `/orders${qs ? `?${qs}` : ""}`);
505
+ }
506
+ async getCommerceOrder(templateId, orderId) {
507
+ return this.commerceFetch("GET", templateId, `/orders/${encodeURIComponent(orderId)}`);
508
+ }
509
+ async updateCommerceOrder(templateId, orderId, fulfillmentStatus) {
510
+ return this.commerceFetch("PATCH", templateId, `/orders/${encodeURIComponent(orderId)}`, { fulfillmentStatus });
511
+ }
512
+ async refundCommerceOrder(templateId, orderId) {
513
+ return this.commerceFetch("POST", templateId, `/orders/${encodeURIComponent(orderId)}/refund`);
514
+ }
515
+ async resendDigitalDelivery(templateId, orderId, deliveryId) {
516
+ return this.commerceFetch("POST", templateId, `/orders/${encodeURIComponent(orderId)}/resend-digital-delivery`, { deliveryId });
517
+ }
518
+ async getCommerceWallet(templateId) {
519
+ return this.commerceFetch("GET", templateId, "/wallet");
520
+ }
521
+ async listCommerceWithdrawals(templateId) {
522
+ return this.commerceFetch("GET", templateId, "/wallet/withdrawals");
523
+ }
524
+ async requestCommerceWithdrawal(templateId, payload) {
525
+ return this.commerceFetch("POST", templateId, "/wallet/withdrawals", payload);
526
+ }
527
+ async getCommerceStripeAccessStatus() {
528
+ this.checkAuth();
529
+ const res = await fetch(`${this.baseUrl}/api/v1/commerce/stripe-access`, { method: "GET", headers: this.getHeaders() });
530
+ const data = (await res.json().catch(() => ({})));
531
+ if (!res.ok)
532
+ throw new Error(data?.error || `Failed to fetch Stripe access status (Status ${res.status})`);
533
+ return data;
534
+ }
535
+ async requestCommerceStripeAccess(payload = {}) {
536
+ this.checkAuth();
537
+ const res = await fetch(`${this.baseUrl}/api/v1/commerce/stripe-access`, {
538
+ method: "POST",
539
+ headers: this.getHeaders(),
540
+ body: JSON.stringify(payload),
541
+ });
542
+ const data = (await res.json().catch(() => ({})));
543
+ if (!res.ok)
544
+ throw new Error(data?.error || `Failed to request Stripe access (Status ${res.status})`);
545
+ return data;
546
+ }
547
+ async listCommerceDeliveryMethods(templateId) {
548
+ return this.commerceFetch("GET", templateId, "/delivery-methods");
549
+ }
550
+ async createCommerceDeliveryMethod(templateId, payload) {
551
+ return this.commerceFetch("POST", templateId, "/delivery-methods", payload);
552
+ }
553
+ async updateCommerceDeliveryMethod(templateId, id, payload) {
554
+ return this.commerceFetch("PATCH", templateId, `/delivery-methods/${encodeURIComponent(id)}`, payload);
555
+ }
556
+ async deleteCommerceDeliveryMethod(templateId, id) {
557
+ return this.commerceFetch("DELETE", templateId, `/delivery-methods/${encodeURIComponent(id)}`);
558
+ }
559
+ async listCommerceDiscounts(templateId) {
560
+ return this.commerceFetch("GET", templateId, "/discounts");
561
+ }
562
+ async createCommerceDiscount(templateId, payload) {
563
+ return this.commerceFetch("POST", templateId, "/discounts", payload);
564
+ }
565
+ async updateCommerceDiscount(templateId, id, payload) {
566
+ return this.commerceFetch("PATCH", templateId, `/discounts/${encodeURIComponent(id)}`, payload);
567
+ }
568
+ async deleteCommerceDiscount(templateId, id) {
569
+ return this.commerceFetch("DELETE", templateId, `/discounts/${encodeURIComponent(id)}`);
570
+ }
571
+ async listCommerceCustomers(templateId, page) {
572
+ return this.commerceFetch("GET", templateId, `/customers${page ? `?page=${page}` : ""}`);
573
+ }
574
+ /** Shared request helper for every /api/v1/lms/funnels/{templateId}/... route below. */
575
+ async lmsFetch(method, templateId, path, body) {
576
+ this.checkAuth();
577
+ const res = await fetch(`${this.baseUrl}/api/v1/lms/funnels/${encodeURIComponent(templateId)}${path}`, {
578
+ method,
579
+ headers: this.getHeaders(),
580
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
581
+ });
582
+ const data = (await res.json().catch(() => ({})));
583
+ if (!res.ok)
584
+ throw new Error(data?.error || `Funnel request failed (Status ${res.status})`);
585
+ return data;
586
+ }
587
+ async createFunnel(templateId) {
588
+ this.checkAuth();
589
+ const res = await fetch(`${this.baseUrl}/api/v1/lms/funnels`, {
590
+ method: "POST",
591
+ headers: this.getHeaders(),
592
+ body: JSON.stringify({ templateId }),
593
+ });
594
+ const data = (await res.json().catch(() => ({})));
595
+ if (!res.ok)
596
+ throw new Error(data?.error || `Funnel creation failed (Status ${res.status})`);
597
+ return data;
598
+ }
599
+ async getFunnel(templateId) {
600
+ return this.lmsFetch("GET", templateId, "");
601
+ }
602
+ async updateFunnel(templateId, payload) {
603
+ return this.lmsFetch("PATCH", templateId, "", payload);
604
+ }
605
+ async deleteFunnel(templateId) {
606
+ return this.lmsFetch("DELETE", templateId, "");
607
+ }
608
+ async listFunnelPages(templateId) {
609
+ return this.lmsFetch("GET", templateId, "/pages");
610
+ }
611
+ async createFunnelPage(templateId, payload = {}) {
612
+ return this.lmsFetch("POST", templateId, "/pages", payload);
613
+ }
614
+ async updateFunnelPage(templateId, pageId, payload) {
615
+ return this.lmsFetch("PATCH", templateId, `/pages/${encodeURIComponent(pageId)}`, payload);
616
+ }
617
+ async deleteFunnelPage(templateId, pageId) {
618
+ return this.lmsFetch("DELETE", templateId, `/pages/${encodeURIComponent(pageId)}`);
619
+ }
620
+ async reorderFunnelPages(templateId, orderedPageIds) {
621
+ return this.lmsFetch("POST", templateId, "/pages/reorder", { orderedPageIds });
622
+ }
623
+ async listFunnelEnrollments(templateId) {
624
+ return this.lmsFetch("GET", templateId, "/enrollments");
625
+ }
626
+ async getFunnelEnrollment(templateId, email) {
627
+ return this.lmsFetch("GET", templateId, `/enrollments/${encodeURIComponent(email)}`);
628
+ }
629
+ async setFunnelEnrollmentStatus(templateId, email, status) {
630
+ return this.lmsFetch("PATCH", templateId, `/enrollments/${encodeURIComponent(email)}`, { status });
631
+ }
632
+ async approveFunnelEnrollmentPage(templateId, email, pageId) {
633
+ return this.lmsFetch("POST", templateId, `/enrollments/${encodeURIComponent(email)}/approve`, { pageId });
634
+ }
635
+ async resendFunnelMagicLink(templateId, email) {
636
+ return this.lmsFetch("POST", templateId, `/enrollments/${encodeURIComponent(email)}/resend-magic-link`);
637
+ }
638
+ async getFunnelAddonSlots() {
639
+ this.checkAuth();
640
+ const res = await fetch(`${this.baseUrl}/api/v1/lms/addon-slots`, { method: "GET", headers: this.getHeaders() });
641
+ const data = (await res.json().catch(() => ({})));
642
+ if (!res.ok)
643
+ throw new Error(data?.error || `Failed to fetch funnel addon slots (Status ${res.status})`);
644
+ return data;
645
+ }
646
+ /** Shared request helper for routes outside the blog/commerce/LMS families (WordPress migration, donations). */
647
+ async apiFetch(method, path, body, label = "Request") {
648
+ this.checkAuth();
649
+ const res = await fetch(`${this.baseUrl}${path}`, {
650
+ method,
651
+ headers: this.getHeaders(),
652
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
653
+ });
654
+ const data = (await res.json().catch(() => ({})));
655
+ if (!res.ok)
656
+ throw new Error(data?.error || `${label} failed (Status ${res.status})`);
657
+ return data;
658
+ }
659
+ get appBaseUrl() {
660
+ return this.baseUrl;
661
+ }
662
+ // ── Commerce: bank transfers ────────────────────────────────────────────────
663
+ async resolveBankTransferOrder(templateId, orderId, action, reason) {
664
+ return this.commerceFetch("POST", templateId, `/orders/${encodeURIComponent(orderId)}/bank-transfer`, { action, ...(reason ? { reason } : {}) });
665
+ }
666
+ // ── Donations (/api/v1/commerce/{templateId}/donations) ─────────────────────
667
+ async listDonations(templateId) {
668
+ return this.commerceFetch("GET", templateId, "/donations");
669
+ }
670
+ async createDonationCampaign(templateId, payload) {
671
+ return this.commerceFetch("POST", templateId, "/donations", payload);
672
+ }
673
+ async updateDonationCampaign(templateId, campaignId, payload) {
674
+ return this.commerceFetch("PATCH", templateId, `/donations/${encodeURIComponent(campaignId)}`, payload);
675
+ }
676
+ async cancelMonthlyDonation(templateId, subscriptionId) {
677
+ return this.commerceFetch("DELETE", templateId, `/donations/subscriptions/${encodeURIComponent(subscriptionId)}`);
678
+ }
679
+ async adoptMonthlyDonors(templateId, campaignId) {
680
+ return this.commerceFetch("POST", templateId, "/donations/adopt-monthly-donors", { campaignId });
681
+ }
682
+ // ── WordPress migration (/api/v1/wp-migration) ──────────────────────────────
683
+ async previewWordPressMigration(sourceUrl) {
684
+ return this.apiFetch("POST", "/api/v1/wp-migration/detect", { sourceUrl }, "WordPress check");
685
+ }
686
+ async startWordPressMigration(params) {
687
+ this.checkAuth();
688
+ const res = await fetch(`${this.baseUrl}/api/v1/wp-migration`, { method: "POST", headers: this.getHeaders(), body: JSON.stringify(params) });
689
+ const data = (await res.json().catch(() => ({})));
690
+ // Recoverable outcomes (needs AUP consent, already running) are returned, not thrown, so the assistant can act on them.
691
+ if (!res.ok && !data?.requiresAupAcceptance && !data?.existing)
692
+ throw new Error(data?.error || `Migration start failed (Status ${res.status})`);
693
+ return { ok: res.ok, ...data };
694
+ }
695
+ async listWordPressMigrations() {
696
+ return this.apiFetch("GET", "/api/v1/wp-migration", undefined, "Migration list");
697
+ }
698
+ migrationPath(templateId, jobId) {
699
+ return `/api/v1/wp-migration/${encodeURIComponent(templateId)}/${encodeURIComponent(jobId)}`;
700
+ }
701
+ async getWordPressMigration(templateId, jobId) {
702
+ return this.apiFetch("GET", this.migrationPath(templateId, jobId), undefined, "Migration status");
703
+ }
704
+ async stepWordPressMigration(templateId, jobId) {
705
+ return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/step`, undefined, "Migration step");
706
+ }
707
+ async getMigratorConnector(templateId, jobId, check = false) {
708
+ return this.apiFetch("GET", `${this.migrationPath(templateId, jobId)}/connector${check ? "?check=1" : ""}`, undefined, "Plugin connection");
709
+ }
710
+ async createMigratorConnector(templateId, jobId) {
711
+ return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/connector`, undefined, "Plugin connection");
712
+ }
713
+ async importMigratorData(templateId, jobId) {
714
+ return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/connector/import`, undefined, "Private data import");
715
+ }
716
+ async getDomainSwitchPlan(templateId, jobId) {
717
+ return this.apiFetch("GET", `${this.migrationPath(templateId, jobId)}/cutover`, undefined, "Domain switch plan");
718
+ }
719
+ async verifyDomainSwitch(templateId, jobId) {
720
+ return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/cutover/verify`, undefined, "Domain check");
721
+ }
453
722
  }
454
723
  exports.PlexoClient = PlexoClient;