@charisol/plexo-mcp 1.0.10 → 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 +5 -0
- package/dist/blogTools.js +14 -11
- package/dist/client/plexoClient.js +76 -0
- package/dist/commerceTools.js +24 -1
- package/dist/donationTools.js +93 -0
- package/dist/index.js +83 -37
- package/dist/wpMigrationTools.js +194 -0
- package/package.json +1 -1
- package/src/blogTools.ts +14 -11
- package/src/client/plexoClient.ts +96 -0
- package/src/commerceTools.ts +23 -1
- package/src/donationTools.ts +98 -0
- package/src/index.ts +84 -38
- package/src/wpMigrationTools.ts +198 -0
package/src/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { PlexoClient } from "./client/plexoClient.js";
|
|
|
10
10
|
import { BLOG_TOOL_DEFS, BLOG_TOOL_NAMES, handleBlogToolCall } from "./blogTools.js";
|
|
11
11
|
import { COMMERCE_TOOL_DEFS, COMMERCE_TOOL_NAMES, handleCommerceToolCall } from "./commerceTools.js";
|
|
12
12
|
import { LMS_TOOL_DEFS, LMS_TOOL_NAMES, handleLmsToolCall } from "./lmsTools.js";
|
|
13
|
+
import { DONATION_TOOL_DEFS, DONATION_TOOL_NAMES, handleDonationToolCall } from "./donationTools.js";
|
|
14
|
+
import { WP_MIGRATION_TOOL_DEFS, WP_MIGRATION_TOOL_NAMES, handleWpMigrationToolCall } from "./wpMigrationTools.js";
|
|
13
15
|
|
|
14
16
|
const server = new Server(
|
|
15
17
|
{
|
|
@@ -31,55 +33,78 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
31
33
|
name: "publish_landing_page",
|
|
32
34
|
description: `Creates, compiles, and publishes a landing page to Plexo in one atomic step. Returns the published live URL and editable visual builder URL.
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
DESIGN QUALITY — read this before writing designJson. Followed correctly it is what separates a premium, bespoke-looking result from a generic AI template; the worked example below demonstrates every rule.
|
|
37
|
+
- Palette: choose one small, deliberate system before writing any row — a near-black/navy/charcoal "ink" plus an off-white/cream "paper" (never pure #000000/#ffffff), one brand accent color used sparingly for CTAs/highlights only, and 2-3 muted grays derived from the ink color for secondary text. Reuse these exact hex values everywhere. Default to this kind of restrained, brand-appropriate palette — NOT a generic dark background with a purple/blue accent — unless the user's prompt clearly calls for that look.
|
|
38
|
+
- Typography: at most three font families — one serif or otherwise distinctive display font for headings (line-height 1.0-1.25, fontWeight 500-600, never 800+), one clean sans-serif for body copy and buttons (paragraphs at line-height 1.6-1.75), and optionally one monospace/condensed font in uppercase with letterSpacing "0.14em"-"0.18em" at a small fontSize (11-13px) reserved only for short eyebrow labels above a heading. Give headings/paragraphs a "maxWidth" (roughly 480-780px) so text never stretches edge-to-edge.
|
|
39
|
+
- Section shell: every section is two layers, using the column's optional 'nestedRows' array (see schema below) — an OUTER row carries the section's full-bleed backgroundColor to the viewport edge with horizontal padding of either "0" or a fluid "clamp(20px, 6vw, 56px)" (never a fixed px value), containing one "100%"-width column whose 'nestedRows' holds an INNER row that caps actual content at maxWidth ~1200-1440px. Alternate background colors between sections (ink, paper, a warm neutral, a soft accent tint) for rhythm instead of leaving every section the same color.
|
|
40
|
+
- Restraint: borderRadius near 0 by default (an editorial, premium feel) — reserve a rounded pill (~100px) only for tags/badges, small radii (2-10px) only for buttons/cards/inputs. Use boxShadow rarely and only as a soft, large, low-opacity shadow tinted with the ink color (never plain black) — most elements should have none.
|
|
41
|
+
- Layout variety: vary column-width splits across sections (an asymmetric ~55/45 hero, a 33.33/33.33/33.33 feature grid, alternating ~60/40 image-and-text rows) instead of repeating the same centered block or identical grid everywhere. Buttons: understated solid-accent fill or a text link with a colored underline/arrow, fontWeight 600, padding ~"14px 26px", sharp-to-small corner radius, specific action copy ("Book a Consultation →") not generic "Click Here". Images: descriptive specific "alt" text, an intentional width/maxWidth so nothing stretches full-bleed by accident.
|
|
42
|
+
|
|
43
|
+
STRUCTURAL RULES:
|
|
35
44
|
The designJson argument MUST be a fully hydrated layout tree containing body style and rows array. There is no shorthand format —
|
|
36
45
|
do NOT send rows shaped like { "type": "products", "content": { "items": [...] } }. That will be REJECTED with a validation error.
|
|
37
|
-
Every row MUST already contain a 'columns' array with percentage widths ('100%', '50%', '33.33%'), and each column MUST contain
|
|
38
|
-
'elements' array
|
|
39
|
-
'spacer', 'form_container')
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
Every row MUST already contain a 'columns' array with percentage widths ('100%', '50%', '33.33%'), and each column MUST contain either
|
|
47
|
+
an 'elements' array of fully-styled component objects ('heading', 'paragraph', 'button', 'card', 'image', 'menu', 'social', 'divider',
|
|
48
|
+
'spacer', 'form_container') and/or a 'nestedRows' array (same shape as a top-level row — id/style/columns — used for the outer/inner
|
|
49
|
+
full-bleed section shell described above; a column may have nestedRows, elements, or both). For any repeated-item section (products,
|
|
50
|
+
features, testimonials, pricing tiers, etc.) YOU must expand each item into its own column with real elements — one column per item —
|
|
51
|
+
rather than passing a list for the server to interpret.
|
|
52
|
+
|
|
53
|
+
EXAMPLE VALID designJson PAYLOAD (full-bleed hero with an asymmetric 55/45 split, plus a full-bleed 3-up feature row) — note the
|
|
54
|
+
restrained ink/paper/terracotta palette, the serif/sans/mono type system, near-zero border radius, and the outer/inner row shell:
|
|
43
55
|
{
|
|
44
56
|
"body": {
|
|
45
|
-
"style": { "backgroundColor": "#
|
|
57
|
+
"style": { "backgroundColor": "#FAF7F1", "color": "#1B2430", "fontFamily": "'Inter', sans-serif", "htmlTitle": "Norrland Coffee Roasters" },
|
|
46
58
|
"rows": [
|
|
47
59
|
{
|
|
48
|
-
"id": "row-hero",
|
|
49
|
-
"style": { "
|
|
60
|
+
"id": "row-hero-outer",
|
|
61
|
+
"style": { "backgroundColor": "#1B2430", "padding": "0 clamp(24px, 6vw, 64px)" },
|
|
50
62
|
"columns": [
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
{ "id": "col-hero-outer", "width": "100%", "elements": [], "nestedRows": [
|
|
64
|
+
{
|
|
65
|
+
"id": "row-hero-inner",
|
|
66
|
+
"style": { "maxWidth": "1200px", "margin": "0 auto" },
|
|
67
|
+
"columns": [
|
|
68
|
+
{ "id": "col-hero-text", "width": "55%", "styles": { "padding": "96px 32px 96px 0" }, "elements": [
|
|
69
|
+
{ "id": "el-eyebrow", "type": "text", "style": { "fontFamily": "'IBM Plex Mono', monospace", "fontSize": "12px", "letterSpacing": "0.16em", "textTransform": "uppercase", "color": "#C4623B" }, "attributes": { "text": "Small-Batch Roastery" } },
|
|
70
|
+
{ "id": "el-heading", "type": "heading", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "48px", "lineHeight": "1.1", "color": "#FAF7F1", "maxWidth": "560px", "margin": "16px 0 0" }, "attributes": { "text": "Coffee, Roasted With Intention" } },
|
|
71
|
+
{ "id": "el-sub", "type": "paragraph", "style": { "fontFamily": "'Inter', sans-serif", "fontSize": "17px", "lineHeight": "1.7", "color": "#B7BEC9", "maxWidth": "480px", "margin": "20px 0 0" }, "attributes": { "text": "Single-origin beans sourced direct from growers, roasted weekly in small batches for a cup that actually tastes like where it came from." } },
|
|
72
|
+
{ "id": "el-cta", "type": "button", "style": { "backgroundColor": "#C4623B", "color": "#FAF7F1", "border": "none", "borderRadius": "2px", "fontFamily": "'Inter', sans-serif", "fontWeight": "600", "fontSize": "15px", "padding": "14px 26px", "margin": "28px 0 0" }, "attributes": { "text": "Shop This Week's Roast →", "href": "/shop" } }
|
|
73
|
+
] },
|
|
74
|
+
{ "id": "col-hero-image", "width": "45%", "elements": [
|
|
75
|
+
{ "id": "el-hero-img", "type": "image", "style": { "width": "100%", "borderRadius": "4px", "boxShadow": "0 18px 34px -14px rgba(27,36,48,0.45)" }, "attributes": { "src": "https://...", "alt": "Roaster pouring fresh-roasted beans into a cooling tray" } }
|
|
76
|
+
] }
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
] }
|
|
60
80
|
]
|
|
61
81
|
},
|
|
62
82
|
{
|
|
63
|
-
"id": "row-
|
|
64
|
-
"style": { "
|
|
83
|
+
"id": "row-features-outer",
|
|
84
|
+
"style": { "backgroundColor": "#FAF7F1", "padding": "0 clamp(24px, 6vw, 64px)" },
|
|
65
85
|
"columns": [
|
|
66
|
-
{ "id": "col-
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
{ "id": "col-features-outer", "width": "100%", "elements": [], "nestedRows": [
|
|
87
|
+
{
|
|
88
|
+
"id": "row-features-inner",
|
|
89
|
+
"style": { "maxWidth": "1200px", "margin": "0 auto", "padding": "80px 0" },
|
|
90
|
+
"columns": [
|
|
91
|
+
{ "id": "col-feature-1", "width": "33.33%", "elements": [
|
|
92
|
+
{ "id": "el-icon-1", "type": "icon", "style": { "color": "#C4623B" }, "attributes": { "iconName": "leaf", "iconSize": 30 } },
|
|
93
|
+
{ "id": "el-h-1", "type": "heading", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "22px", "color": "#1B2430", "margin": "16px 0 0" }, "attributes": { "text": "Direct Trade" } },
|
|
94
|
+
{ "id": "el-p-1", "type": "paragraph", "style": { "fontFamily": "'Inter', sans-serif", "fontSize": "15px", "lineHeight": "1.65", "color": "#5B6472", "margin": "8px 0 0" }, "attributes": { "text": "Every farm relationship is ours, not a broker's." } }
|
|
95
|
+
] },
|
|
96
|
+
{ "id": "col-feature-2", "width": "33.33%", "elements": [
|
|
97
|
+
{ "id": "el-icon-2", "type": "icon", "style": { "color": "#C4623B" }, "attributes": { "iconName": "flame", "iconSize": 30 } },
|
|
98
|
+
{ "id": "el-h-2", "type": "heading", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "22px", "color": "#1B2430", "margin": "16px 0 0" }, "attributes": { "text": "Roasted Weekly" } },
|
|
99
|
+
{ "id": "el-p-2", "type": "paragraph", "style": { "fontFamily": "'Inter', sans-serif", "fontSize": "15px", "lineHeight": "1.65", "color": "#5B6472", "margin": "8px 0 0" }, "attributes": { "text": "Never more than seven days off the roaster." } }
|
|
100
|
+
] },
|
|
101
|
+
{ "id": "col-feature-3", "width": "33.33%", "elements": [
|
|
102
|
+
{ "id": "el-icon-3", "type": "icon", "style": { "color": "#C4623B" }, "attributes": { "iconName": "truck", "iconSize": 30 } },
|
|
103
|
+
{ "id": "el-h-3", "type": "heading", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "22px", "color": "#1B2430", "margin": "16px 0 0" }, "attributes": { "text": "Shipped Fresh" } },
|
|
104
|
+
{ "id": "el-p-3", "type": "paragraph", "style": { "fontFamily": "'Inter', sans-serif", "fontSize": "15px", "lineHeight": "1.65", "color": "#5B6472", "margin": "8px 0 0" }, "attributes": { "text": "Out the door within 24 hours of roasting." } }
|
|
105
|
+
] }
|
|
106
|
+
]
|
|
107
|
+
}
|
|
83
108
|
] }
|
|
84
109
|
]
|
|
85
110
|
}
|
|
@@ -133,6 +158,7 @@ EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
|
|
|
133
158
|
width: { type: "string", description: "Percentage width (e.g. '100%', '50%', '33.33%')." },
|
|
134
159
|
elements: {
|
|
135
160
|
type: "array",
|
|
161
|
+
description: "This column's own content elements. May be empty ([]) when the column instead uses 'nestedRows' below (the full-bleed section-shell pattern) — a column may have elements, nestedRows, or both.",
|
|
136
162
|
items: {
|
|
137
163
|
type: "object",
|
|
138
164
|
required: ["id", "type", "style", "attributes"],
|
|
@@ -146,6 +172,15 @@ EXAMPLE VALID designJson PAYLOAD (hero row, plus a 3-item product grid row):
|
|
|
146
172
|
},
|
|
147
173
|
},
|
|
148
174
|
},
|
|
175
|
+
nestedRows: {
|
|
176
|
+
type: "array",
|
|
177
|
+
description: "OPTIONAL. Nested row(s) inside this column, each the SAME shape as a top-level row (id/style/columns). This is how you build a full-bleed section: the outer row/column carries the background color edge-to-edge, and a nestedRow here caps content at a readable maxWidth. Use this instead of putting a 'maxWidth' directly on the outer row.",
|
|
178
|
+
items: {
|
|
179
|
+
type: "object",
|
|
180
|
+
required: ["id", "style", "columns"],
|
|
181
|
+
description: "Same shape as a top-level row: { id, style, columns: [{ id, width, elements, nestedRows? }] } — nesting may repeat if genuinely needed, but one level (outer band + inner capped row) covers nearly every section.",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
149
184
|
},
|
|
150
185
|
},
|
|
151
186
|
},
|
|
@@ -617,6 +652,8 @@ To edit a page's CONTENT (designJson), use update_template with the same templat
|
|
|
617
652
|
...BLOG_TOOL_DEFS,
|
|
618
653
|
...COMMERCE_TOOL_DEFS,
|
|
619
654
|
...LMS_TOOL_DEFS,
|
|
655
|
+
...DONATION_TOOL_DEFS,
|
|
656
|
+
...WP_MIGRATION_TOOL_DEFS,
|
|
620
657
|
],
|
|
621
658
|
};
|
|
622
659
|
});
|
|
@@ -922,6 +959,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
922
959
|
],
|
|
923
960
|
};
|
|
924
961
|
}
|
|
962
|
+
for (const [names, handler] of [
|
|
963
|
+
[DONATION_TOOL_NAMES, handleDonationToolCall],
|
|
964
|
+
[WP_MIGRATION_TOOL_NAMES, handleWpMigrationToolCall],
|
|
965
|
+
] as const) {
|
|
966
|
+
if (names.has(name)) {
|
|
967
|
+
const result = await handler(name, (args as any) || {}, client);
|
|
968
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
969
|
+
}
|
|
970
|
+
}
|
|
925
971
|
throw new Error(`Unknown tool: ${name}`);
|
|
926
972
|
}
|
|
927
973
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { PlexoClient } from "./client/plexoClient.js";
|
|
2
|
+
|
|
3
|
+
// WordPress → Plexo migration — same tools, same descriptions as the live connector in
|
|
4
|
+
// plexo-web (lib/mcp/wpMigrationTools.ts), executed through plexo-web's REST API.
|
|
5
|
+
// get_wordpress_migration advances the migration one step per call, like the dashboard.
|
|
6
|
+
|
|
7
|
+
export const WP_MIGRATION_TOOL_DEFS = [
|
|
8
|
+
{
|
|
9
|
+
name: "preview_wordpress_migration",
|
|
10
|
+
description: `Checks a WordPress website and previews what moving it to Plexo would involve — WITHOUT changing anything. Finds WordPress even when it's installed in a subfolder or behind a splash/landing page, and returns page/post/media counts, the theme, and what will happen to each plugin (replaced by a Plexo feature, carried over as-is, needs attention, or not needed). Call this first and show the owner the plugin outcomes — especially anything marked NEEDS_ATTENTION (e.g. payment/donation plugins) — before calling start_wordpress_migration.`,
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
url: { type: "string", description: "The site's public address, e.g. 'example.org' or 'https://example.org/blog'. Any page on the site works." },
|
|
15
|
+
},
|
|
16
|
+
required: ["url"],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "start_wordpress_migration",
|
|
21
|
+
description: `Starts moving a WordPress site to a NEW Plexo site (never overwrites an existing one). Copies every page exactly as it looks (theme, Elementor and other page-builder designs, fonts, animations), every blog post into Plexo Blog, the whole media library, and every SEO tag — keeping every web address identical so search rankings and links survive. Forms (Forminator, Contact Form 7, WPForms, Elementor) are reconnected to Plexo Forms. The WordPress site itself is not changed.
|
|
22
|
+
|
|
23
|
+
Requires the owner's explicit confirmation that they own the site (ownershipConfirmed: true) — ask them, never assume. If their account hasn't accepted Plexo's Acceptable Use Policy yet, the call fails with requiresAupAcceptance; ask them to read /legal/acceptable-use and retry with acceptAcceptableUsePolicy: true only if they agree.
|
|
24
|
+
|
|
25
|
+
Returns templateId + jobId. Then call get_wordpress_migration repeatedly (each call advances the migration) until phase is COMPLETED.`,
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
url: { type: "string", description: "The site's public address (same as preview_wordpress_migration)." },
|
|
30
|
+
ownershipConfirmed: { type: "boolean", description: "true only if the owner confirmed they own the site or have permission to move it." },
|
|
31
|
+
acceptAcceptableUsePolicy: { type: "boolean", description: "true only if the owner agreed to Plexo's Acceptable Use Policy (needed once per account)." },
|
|
32
|
+
},
|
|
33
|
+
required: ["url", "ownershipConfirmed"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "get_wordpress_migration",
|
|
38
|
+
description: `Advances a WordPress migration by one step and returns its progress — call it repeatedly until phase is COMPLETED (or PAUSED_ERROR, in which case calling again retries from where it stopped). When COMPLETED, returns the report: what was copied, SEO comparison, what happened to each plugin, and a cutover checklist. Items with status "blocked" (e.g. connecting payments for a donation form) must be done before the owner points their domain at Plexo; tell the owner about them plainly. Omit jobId to get the organization's most recent migration.`,
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
templateId: { type: "string", description: "The Plexo site id returned by start_wordpress_migration." },
|
|
43
|
+
jobId: { type: "string", description: "The migration id returned by start_wordpress_migration." },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "connect_wordpress_migrator_plugin",
|
|
49
|
+
description: `Starts copying what WordPress keeps private (past form entries and applications, donation history, drafts, redirect rules, the team list) for a finished migration. Returns a download link for the read-only "Plexo Migrator" WordPress plugin and a connection code. Tell the owner: install and activate the plugin (Plugins → Add New → Upload Plugin), then paste the code in WordPress under Tools → Plexo Migrator. Then call import_wordpress_private_data.`,
|
|
50
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string" }, jobId: { type: "string" } }, required: ["templateId", "jobId"] },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "import_wordpress_private_data",
|
|
54
|
+
description: `Checks the Plexo Migrator plugin connection and copies the next batch of private data. Call repeatedly until stage is "done". If it returns an error message, relay it to the owner (it says exactly what to do, e.g. "paste the code in Tools → Plexo Migrator"). When done, the result lists the team found on WordPress — offer to invite them.`,
|
|
55
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string" }, jobId: { type: "string" } }, required: ["templateId", "jobId"] },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "get_domain_switch_plan",
|
|
59
|
+
description: `For a finished migration: reads the old domain's live DNS and returns exactly how to point it at Plexo — who hosts the DNS (and whether cancelling the WordPress hosting would delete it), the email provider, records to KEEP unchanged (email, verification), records to ADD for Plexo, step-by-step instructions and a zone file. Connect the domain first with publish_existing_landing_page (CUSTOM, both apex and www) so the certificate records are included. Always relay the warnings to the owner.`,
|
|
60
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string" }, jobId: { type: "string" } }, required: ["templateId", "jobId"] },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "verify_domain_switch",
|
|
64
|
+
description: `Checks on the real domain that Plexo now serves every migrated URL, email records are intact, DNS no longer depends on the old host, and nothing in the migration is blocking. Only tell the owner it's safe to cancel WordPress hosting when safeToCancelWordPress is true. Call get_domain_switch_plan first.`,
|
|
65
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string" }, jobId: { type: "string" } }, required: ["templateId", "jobId"] },
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export const WP_MIGRATION_TOOL_NAMES = new Set(WP_MIGRATION_TOOL_DEFS.map((t) => t.name));
|
|
70
|
+
|
|
71
|
+
const TERMINAL = new Set(["COMPLETED", "FAILED", "CANCELLED"]);
|
|
72
|
+
|
|
73
|
+
function ids(args: any): { templateId: string; jobId: string } {
|
|
74
|
+
const templateId = typeof args?.templateId === "string" ? args.templateId.trim() : "";
|
|
75
|
+
const jobId = typeof args?.jobId === "string" ? args.jobId.trim() : "";
|
|
76
|
+
if (!templateId || !jobId) throw new Error("templateId and jobId are required (from start_wordpress_migration).");
|
|
77
|
+
return { templateId, jobId };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function summarizeReport(report: any) {
|
|
81
|
+
return {
|
|
82
|
+
readyForCutover: report.readyForCutover,
|
|
83
|
+
checklist: report.checklist,
|
|
84
|
+
summary: report.summary,
|
|
85
|
+
seo: report.seo,
|
|
86
|
+
plugins: (report.plugins ?? []).map((p: any) => ({ name: p.name, outcome: p.outcome, explanation: p.explanation })),
|
|
87
|
+
formsReconnected: (report.forms ?? []).map((f: any) => ({ page: f.page, name: f.name, fields: f.fields })),
|
|
88
|
+
brokenLinks: (report.brokenLinks ?? []).slice(0, 20),
|
|
89
|
+
failedFiles: (report.failedFiles ?? []).slice(0, 20),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function handleWpMigrationToolCall(name: string, args: any, client: PlexoClient): Promise<any> {
|
|
94
|
+
const base = client.appBaseUrl;
|
|
95
|
+
switch (name) {
|
|
96
|
+
case "preview_wordpress_migration": {
|
|
97
|
+
if (typeof args?.url !== "string" || !args.url.trim()) throw new Error("url is required.");
|
|
98
|
+
const result = await client.previewWordPressMigration(args.url);
|
|
99
|
+
return { preview: result.preview };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case "start_wordpress_migration": {
|
|
103
|
+
const result = await client.startWordPressMigration({
|
|
104
|
+
sourceUrl: String(args?.url ?? ""),
|
|
105
|
+
ownershipAttested: args?.ownershipConfirmed === true,
|
|
106
|
+
acceptAup: args?.acceptAcceptableUsePolicy === true ? true : undefined,
|
|
107
|
+
});
|
|
108
|
+
if (!result.ok) return { started: false, error: result.error, requiresAupAcceptance: result.requiresAupAcceptance ?? false, existingMigration: result.existing ?? null };
|
|
109
|
+
return {
|
|
110
|
+
started: true,
|
|
111
|
+
templateId: result.templateId,
|
|
112
|
+
jobId: result.jobId,
|
|
113
|
+
dashboardUrl: `${base}/dashboard/migrate/wordpress?site=${result.templateId}&job=${result.jobId}`,
|
|
114
|
+
next: "Call get_wordpress_migration with this templateId and jobId until phase is COMPLETED.",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case "get_wordpress_migration": {
|
|
119
|
+
let templateId = typeof args?.templateId === "string" ? args.templateId : "";
|
|
120
|
+
let jobId = typeof args?.jobId === "string" ? args.jobId : "";
|
|
121
|
+
if (!templateId || !jobId) {
|
|
122
|
+
const { migrations } = await client.listWordPressMigrations();
|
|
123
|
+
if (!migrations?.length) return { found: false, message: "No WordPress migrations yet — start one with start_wordpress_migration." };
|
|
124
|
+
templateId = migrations[0].templateId;
|
|
125
|
+
jobId = migrations[0].id;
|
|
126
|
+
}
|
|
127
|
+
const before = await client.getWordPressMigration(templateId, jobId);
|
|
128
|
+
if (!TERMINAL.has(before.job.phase)) await client.stepWordPressMigration(templateId, jobId);
|
|
129
|
+
const { job, domains } = await client.getWordPressMigration(templateId, jobId);
|
|
130
|
+
const dashboardUrl = `${base}/dashboard/migrate/wordpress?site=${templateId}&job=${jobId}`;
|
|
131
|
+
if (job.phase === "COMPLETED" && job.report) {
|
|
132
|
+
return {
|
|
133
|
+
phase: job.phase,
|
|
134
|
+
templateId,
|
|
135
|
+
jobId,
|
|
136
|
+
dashboardUrl,
|
|
137
|
+
publishedDomains: (domains ?? []).map((d: any) => d.domain),
|
|
138
|
+
report: summarizeReport(job.report),
|
|
139
|
+
next: "Use get_domain_switch_plan to move the owner's domain; keep WordPress running until verify_domain_switch says it's safe.",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
phase: job.phase,
|
|
144
|
+
templateId,
|
|
145
|
+
jobId,
|
|
146
|
+
dashboardUrl,
|
|
147
|
+
counts: job.counts ?? null,
|
|
148
|
+
lastError: job.phase === "PAUSED_ERROR" ? (job.errors ?? []).slice(-1)[0] : undefined,
|
|
149
|
+
next: job.phase === "PAUSED_ERROR" ? "Call get_wordpress_migration again to retry from where it stopped." : "Call get_wordpress_migration again to continue.",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case "get_domain_switch_plan": {
|
|
154
|
+
const { templateId, jobId } = ids(args);
|
|
155
|
+
const result = await client.getDomainSwitchPlan(templateId, jobId);
|
|
156
|
+
return { ...result.plan, connectedDomains: (result.connected ?? []).map((d: any) => d.domain) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case "verify_domain_switch": {
|
|
160
|
+
const { templateId, jobId } = ids(args);
|
|
161
|
+
return (await client.verifyDomainSwitch(templateId, jobId)).verification;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case "connect_wordpress_migrator_plugin": {
|
|
165
|
+
const { templateId, jobId } = ids(args);
|
|
166
|
+
const current = await client.getMigratorConnector(templateId, jobId);
|
|
167
|
+
const code = current.connector?.code ?? (await client.createMigratorConnector(templateId, jobId)).connector.code;
|
|
168
|
+
return {
|
|
169
|
+
pluginDownloadUrl: `${base}/api/v1/wp-migration/plugin`,
|
|
170
|
+
connectionCode: code,
|
|
171
|
+
steps: [
|
|
172
|
+
"Download the plugin (sign in to Plexo in the browser first) and install it: Plugins → Add New → Upload Plugin, then Activate.",
|
|
173
|
+
"In WordPress, go to Tools → Plexo Migrator and paste the connection code.",
|
|
174
|
+
"Then call import_wordpress_private_data.",
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
case "import_wordpress_private_data": {
|
|
180
|
+
const { templateId, jobId } = ids(args);
|
|
181
|
+
let { connector } = await client.getMigratorConnector(templateId, jobId);
|
|
182
|
+
if (connector?.status !== "CONNECTED") {
|
|
183
|
+
connector = (await client.getMigratorConnector(templateId, jobId, true)).connector;
|
|
184
|
+
if (connector?.status !== "CONNECTED") return { connected: false, message: connector?.error ?? "Call connect_wordpress_migrator_plugin first." };
|
|
185
|
+
}
|
|
186
|
+
const { extras } = await client.importMigratorData(templateId, jobId);
|
|
187
|
+
return {
|
|
188
|
+
connected: true,
|
|
189
|
+
stage: extras.stage,
|
|
190
|
+
counts: extras.counts,
|
|
191
|
+
team: extras.stage === "done" ? extras.team : undefined,
|
|
192
|
+
error: extras.error,
|
|
193
|
+
next: extras.stage === "done" ? "Done. Offer to invite the team (admins as admin, others as editor of this site)." : "Call import_wordpress_private_data again to continue.",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
198
|
+
}
|