@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 +5 -0
- package/dist/blogTools.js +14 -11
- package/dist/client/plexoClient.js +269 -0
- package/dist/commerceTools.js +523 -0
- package/dist/donationTools.js +93 -0
- package/dist/index.js +145 -38
- package/dist/lmsTools.js +254 -0
- package/dist/wpMigrationTools.js +194 -0
- package/package.json +1 -1
- package/src/blogTools.ts +14 -11
- package/src/client/plexoClient.ts +321 -0
- package/src/commerceTools.ts +538 -0
- package/src/donationTools.ts +98 -0
- package/src/index.ts +146 -39
- package/src/lmsTools.ts +262 -0
- package/src/wpMigrationTools.ts +198 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WP_MIGRATION_TOOL_NAMES = exports.WP_MIGRATION_TOOL_DEFS = void 0;
|
|
4
|
+
exports.handleWpMigrationToolCall = handleWpMigrationToolCall;
|
|
5
|
+
// WordPress → Plexo migration — same tools, same descriptions as the live connector in
|
|
6
|
+
// plexo-web (lib/mcp/wpMigrationTools.ts), executed through plexo-web's REST API.
|
|
7
|
+
// get_wordpress_migration advances the migration one step per call, like the dashboard.
|
|
8
|
+
exports.WP_MIGRATION_TOOL_DEFS = [
|
|
9
|
+
{
|
|
10
|
+
name: "preview_wordpress_migration",
|
|
11
|
+
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.`,
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
url: { type: "string", description: "The site's public address, e.g. 'example.org' or 'https://example.org/blog'. Any page on the site works." },
|
|
16
|
+
},
|
|
17
|
+
required: ["url"],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "start_wordpress_migration",
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
Returns templateId + jobId. Then call get_wordpress_migration repeatedly (each call advances the migration) until phase is COMPLETED.`,
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
url: { type: "string", description: "The site's public address (same as preview_wordpress_migration)." },
|
|
31
|
+
ownershipConfirmed: { type: "boolean", description: "true only if the owner confirmed they own the site or have permission to move it." },
|
|
32
|
+
acceptAcceptableUsePolicy: { type: "boolean", description: "true only if the owner agreed to Plexo's Acceptable Use Policy (needed once per account)." },
|
|
33
|
+
},
|
|
34
|
+
required: ["url", "ownershipConfirmed"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "get_wordpress_migration",
|
|
39
|
+
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.`,
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
templateId: { type: "string", description: "The Plexo site id returned by start_wordpress_migration." },
|
|
44
|
+
jobId: { type: "string", description: "The migration id returned by start_wordpress_migration." },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "connect_wordpress_migrator_plugin",
|
|
50
|
+
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.`,
|
|
51
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string" }, jobId: { type: "string" } }, required: ["templateId", "jobId"] },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "import_wordpress_private_data",
|
|
55
|
+
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.`,
|
|
56
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string" }, jobId: { type: "string" } }, required: ["templateId", "jobId"] },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "get_domain_switch_plan",
|
|
60
|
+
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.`,
|
|
61
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string" }, jobId: { type: "string" } }, required: ["templateId", "jobId"] },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "verify_domain_switch",
|
|
65
|
+
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.`,
|
|
66
|
+
inputSchema: { type: "object", properties: { templateId: { type: "string" }, jobId: { type: "string" } }, required: ["templateId", "jobId"] },
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
exports.WP_MIGRATION_TOOL_NAMES = new Set(exports.WP_MIGRATION_TOOL_DEFS.map((t) => t.name));
|
|
70
|
+
const TERMINAL = new Set(["COMPLETED", "FAILED", "CANCELLED"]);
|
|
71
|
+
function ids(args) {
|
|
72
|
+
const templateId = typeof args?.templateId === "string" ? args.templateId.trim() : "";
|
|
73
|
+
const jobId = typeof args?.jobId === "string" ? args.jobId.trim() : "";
|
|
74
|
+
if (!templateId || !jobId)
|
|
75
|
+
throw new Error("templateId and jobId are required (from start_wordpress_migration).");
|
|
76
|
+
return { templateId, jobId };
|
|
77
|
+
}
|
|
78
|
+
function summarizeReport(report) {
|
|
79
|
+
return {
|
|
80
|
+
readyForCutover: report.readyForCutover,
|
|
81
|
+
checklist: report.checklist,
|
|
82
|
+
summary: report.summary,
|
|
83
|
+
seo: report.seo,
|
|
84
|
+
plugins: (report.plugins ?? []).map((p) => ({ name: p.name, outcome: p.outcome, explanation: p.explanation })),
|
|
85
|
+
formsReconnected: (report.forms ?? []).map((f) => ({ page: f.page, name: f.name, fields: f.fields })),
|
|
86
|
+
brokenLinks: (report.brokenLinks ?? []).slice(0, 20),
|
|
87
|
+
failedFiles: (report.failedFiles ?? []).slice(0, 20),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function handleWpMigrationToolCall(name, args, client) {
|
|
91
|
+
const base = client.appBaseUrl;
|
|
92
|
+
switch (name) {
|
|
93
|
+
case "preview_wordpress_migration": {
|
|
94
|
+
if (typeof args?.url !== "string" || !args.url.trim())
|
|
95
|
+
throw new Error("url is required.");
|
|
96
|
+
const result = await client.previewWordPressMigration(args.url);
|
|
97
|
+
return { preview: result.preview };
|
|
98
|
+
}
|
|
99
|
+
case "start_wordpress_migration": {
|
|
100
|
+
const result = await client.startWordPressMigration({
|
|
101
|
+
sourceUrl: String(args?.url ?? ""),
|
|
102
|
+
ownershipAttested: args?.ownershipConfirmed === true,
|
|
103
|
+
acceptAup: args?.acceptAcceptableUsePolicy === true ? true : undefined,
|
|
104
|
+
});
|
|
105
|
+
if (!result.ok)
|
|
106
|
+
return { started: false, error: result.error, requiresAupAcceptance: result.requiresAupAcceptance ?? false, existingMigration: result.existing ?? null };
|
|
107
|
+
return {
|
|
108
|
+
started: true,
|
|
109
|
+
templateId: result.templateId,
|
|
110
|
+
jobId: result.jobId,
|
|
111
|
+
dashboardUrl: `${base}/dashboard/migrate/wordpress?site=${result.templateId}&job=${result.jobId}`,
|
|
112
|
+
next: "Call get_wordpress_migration with this templateId and jobId until phase is COMPLETED.",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
case "get_wordpress_migration": {
|
|
116
|
+
let templateId = typeof args?.templateId === "string" ? args.templateId : "";
|
|
117
|
+
let jobId = typeof args?.jobId === "string" ? args.jobId : "";
|
|
118
|
+
if (!templateId || !jobId) {
|
|
119
|
+
const { migrations } = await client.listWordPressMigrations();
|
|
120
|
+
if (!migrations?.length)
|
|
121
|
+
return { found: false, message: "No WordPress migrations yet — start one with start_wordpress_migration." };
|
|
122
|
+
templateId = migrations[0].templateId;
|
|
123
|
+
jobId = migrations[0].id;
|
|
124
|
+
}
|
|
125
|
+
const before = await client.getWordPressMigration(templateId, jobId);
|
|
126
|
+
if (!TERMINAL.has(before.job.phase))
|
|
127
|
+
await client.stepWordPressMigration(templateId, jobId);
|
|
128
|
+
const { job, domains } = await client.getWordPressMigration(templateId, jobId);
|
|
129
|
+
const dashboardUrl = `${base}/dashboard/migrate/wordpress?site=${templateId}&job=${jobId}`;
|
|
130
|
+
if (job.phase === "COMPLETED" && job.report) {
|
|
131
|
+
return {
|
|
132
|
+
phase: job.phase,
|
|
133
|
+
templateId,
|
|
134
|
+
jobId,
|
|
135
|
+
dashboardUrl,
|
|
136
|
+
publishedDomains: (domains ?? []).map((d) => d.domain),
|
|
137
|
+
report: summarizeReport(job.report),
|
|
138
|
+
next: "Use get_domain_switch_plan to move the owner's domain; keep WordPress running until verify_domain_switch says it's safe.",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
phase: job.phase,
|
|
143
|
+
templateId,
|
|
144
|
+
jobId,
|
|
145
|
+
dashboardUrl,
|
|
146
|
+
counts: job.counts ?? null,
|
|
147
|
+
lastError: job.phase === "PAUSED_ERROR" ? (job.errors ?? []).slice(-1)[0] : undefined,
|
|
148
|
+
next: job.phase === "PAUSED_ERROR" ? "Call get_wordpress_migration again to retry from where it stopped." : "Call get_wordpress_migration again to continue.",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
case "get_domain_switch_plan": {
|
|
152
|
+
const { templateId, jobId } = ids(args);
|
|
153
|
+
const result = await client.getDomainSwitchPlan(templateId, jobId);
|
|
154
|
+
return { ...result.plan, connectedDomains: (result.connected ?? []).map((d) => d.domain) };
|
|
155
|
+
}
|
|
156
|
+
case "verify_domain_switch": {
|
|
157
|
+
const { templateId, jobId } = ids(args);
|
|
158
|
+
return (await client.verifyDomainSwitch(templateId, jobId)).verification;
|
|
159
|
+
}
|
|
160
|
+
case "connect_wordpress_migrator_plugin": {
|
|
161
|
+
const { templateId, jobId } = ids(args);
|
|
162
|
+
const current = await client.getMigratorConnector(templateId, jobId);
|
|
163
|
+
const code = current.connector?.code ?? (await client.createMigratorConnector(templateId, jobId)).connector.code;
|
|
164
|
+
return {
|
|
165
|
+
pluginDownloadUrl: `${base}/api/v1/wp-migration/plugin`,
|
|
166
|
+
connectionCode: code,
|
|
167
|
+
steps: [
|
|
168
|
+
"Download the plugin (sign in to Plexo in the browser first) and install it: Plugins → Add New → Upload Plugin, then Activate.",
|
|
169
|
+
"In WordPress, go to Tools → Plexo Migrator and paste the connection code.",
|
|
170
|
+
"Then call import_wordpress_private_data.",
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
case "import_wordpress_private_data": {
|
|
175
|
+
const { templateId, jobId } = ids(args);
|
|
176
|
+
let { connector } = await client.getMigratorConnector(templateId, jobId);
|
|
177
|
+
if (connector?.status !== "CONNECTED") {
|
|
178
|
+
connector = (await client.getMigratorConnector(templateId, jobId, true)).connector;
|
|
179
|
+
if (connector?.status !== "CONNECTED")
|
|
180
|
+
return { connected: false, message: connector?.error ?? "Call connect_wordpress_migrator_plugin first." };
|
|
181
|
+
}
|
|
182
|
+
const { extras } = await client.importMigratorData(templateId, jobId);
|
|
183
|
+
return {
|
|
184
|
+
connected: true,
|
|
185
|
+
stage: extras.stage,
|
|
186
|
+
counts: extras.counts,
|
|
187
|
+
team: extras.stage === "done" ? extras.team : undefined,
|
|
188
|
+
error: extras.error,
|
|
189
|
+
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.",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
194
|
+
}
|
package/package.json
CHANGED
package/src/blogTools.ts
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import { PlexoClient } from "./client/plexoClient.js";
|
|
2
2
|
|
|
3
3
|
const BLOG_LAYOUT_EXAMPLE = `
|
|
4
|
+
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.
|
|
5
|
+
|
|
4
6
|
EXAMPLE — a single-post layout with a hero image, title, meta line, and content:
|
|
5
7
|
{
|
|
6
8
|
"body": {
|
|
7
|
-
"style": { "backgroundColor": "#
|
|
9
|
+
"style": { "backgroundColor": "#FAF7F1", "color": "#1B2430", "fontFamily": "'Inter', sans-serif", "htmlTitle": "Blog Post" },
|
|
8
10
|
"rows": [
|
|
9
11
|
{ "id": "row-hero", "style": { "paddingTop": "0px" }, "columns": [
|
|
10
12
|
{ "id": "col-hero", "width": "100%", "elements": [
|
|
11
13
|
{ "id": "el-featured", "type": "blog_featured_image", "style": { "width": "100%", "maxHeight": "420px", "objectFit": "cover" }, "attributes": {} }
|
|
12
14
|
] }
|
|
13
15
|
] },
|
|
14
|
-
{ "id": "row-meta", "style": { "paddingTop": "
|
|
16
|
+
{ "id": "row-meta", "style": { "paddingTop": "40px", "paddingBottom": "8px", "maxWidth": "760px", "margin": "0 auto" }, "columns": [
|
|
15
17
|
{ "id": "col-title", "width": "100%", "elements": [
|
|
16
|
-
{ "id": "el-title", "type": "blog_title", "style": { "fontSize": "40px", "
|
|
17
|
-
{ "id": "el-authordate", "type": "blog_author", "style": { "fontSize": "
|
|
18
|
-
{ "id": "el-date", "type": "blog_date", "style": { "fontSize": "
|
|
18
|
+
{ "id": "el-title", "type": "blog_title", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "40px", "lineHeight": "1.15" }, "attributes": {} },
|
|
19
|
+
{ "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": {} },
|
|
20
|
+
{ "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": {} }
|
|
19
21
|
] }
|
|
20
22
|
] },
|
|
21
|
-
{ "id": "row-body", "style": { "paddingTop": "24px", "paddingBottom": "48px" }, "columns": [
|
|
23
|
+
{ "id": "row-body", "style": { "paddingTop": "24px", "paddingBottom": "48px", "maxWidth": "760px", "margin": "0 auto" }, "columns": [
|
|
22
24
|
{ "id": "col-body", "width": "100%", "elements": [
|
|
23
|
-
{ "id": "el-content", "type": "blog_content", "style": { "fontSize": "17px", "lineHeight": "1.
|
|
25
|
+
{ "id": "el-content", "type": "blog_content", "style": { "fontFamily": "'Inter', sans-serif", "fontSize": "17px", "lineHeight": "1.75", "color": "#1B2430" }, "attributes": {} },
|
|
24
26
|
{ "id": "el-comments", "type": "blog_comments", "style": { "marginTop": "48px" }, "attributes": {} }
|
|
25
27
|
] }
|
|
26
28
|
] }
|
|
@@ -31,14 +33,15 @@ EXAMPLE — a single-post layout with a hero image, title, meta line, and conten
|
|
|
31
33
|
EXAMPLE — a listing layout with a 3-column post grid:
|
|
32
34
|
{
|
|
33
35
|
"body": {
|
|
34
|
-
"style": { "backgroundColor": "#
|
|
36
|
+
"style": { "backgroundColor": "#FAF7F1", "color": "#1B2430", "fontFamily": "'Inter', sans-serif", "htmlTitle": "Blog" },
|
|
35
37
|
"rows": [
|
|
36
|
-
{ "id": "row-heading", "style": { "paddingTop": "
|
|
38
|
+
{ "id": "row-heading", "style": { "paddingTop": "56px", "paddingBottom": "16px" }, "columns": [
|
|
37
39
|
{ "id": "col-heading", "width": "100%", "elements": [
|
|
38
|
-
{ "id": "el-
|
|
40
|
+
{ "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" } },
|
|
41
|
+
{ "id": "el-heading", "type": "heading", "style": { "fontFamily": "'Playfair Display', serif", "fontWeight": "600", "fontSize": "36px", "textAlign": "center", "margin": "8px 0 0" }, "attributes": { "text": "Latest Posts" } }
|
|
39
42
|
] }
|
|
40
43
|
] },
|
|
41
|
-
{ "id": "row-list", "style": { "paddingBottom": "
|
|
44
|
+
{ "id": "row-list", "style": { "paddingBottom": "56px" }, "columns": [
|
|
42
45
|
{ "id": "col-list", "width": "100%", "elements": [
|
|
43
46
|
{ "id": "el-postlist", "type": "blog_post_list", "style": { "gridColumns": "3" }, "attributes": {} }
|
|
44
47
|
] }
|
|
@@ -570,4 +570,325 @@ 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
|
+
}
|
|
798
|
+
|
|
799
|
+
/** Shared request helper for routes outside the blog/commerce/LMS families (WordPress migration, donations). */
|
|
800
|
+
private async apiFetch(method: string, path: string, body?: unknown, label = "Request"): Promise<any> {
|
|
801
|
+
this.checkAuth();
|
|
802
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
803
|
+
method,
|
|
804
|
+
headers: this.getHeaders(),
|
|
805
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
806
|
+
});
|
|
807
|
+
const data = (await res.json().catch(() => ({}))) as any;
|
|
808
|
+
if (!res.ok) throw new Error(data?.error || `${label} failed (Status ${res.status})`);
|
|
809
|
+
return data;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
get appBaseUrl(): string {
|
|
813
|
+
return this.baseUrl;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ── Commerce: bank transfers ────────────────────────────────────────────────
|
|
817
|
+
|
|
818
|
+
async resolveBankTransferOrder(templateId: string, orderId: string, action: "confirm" | "reject", reason?: string): Promise<any> {
|
|
819
|
+
return this.commerceFetch("POST", templateId, `/orders/${encodeURIComponent(orderId)}/bank-transfer`, { action, ...(reason ? { reason } : {}) });
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// ── Donations (/api/v1/commerce/{templateId}/donations) ─────────────────────
|
|
823
|
+
|
|
824
|
+
async listDonations(templateId: string): Promise<any> {
|
|
825
|
+
return this.commerceFetch("GET", templateId, "/donations");
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async createDonationCampaign(templateId: string, payload: Record<string, any>): Promise<any> {
|
|
829
|
+
return this.commerceFetch("POST", templateId, "/donations", payload);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async updateDonationCampaign(templateId: string, campaignId: string, payload: Record<string, any>): Promise<any> {
|
|
833
|
+
return this.commerceFetch("PATCH", templateId, `/donations/${encodeURIComponent(campaignId)}`, payload);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async cancelMonthlyDonation(templateId: string, subscriptionId: string): Promise<any> {
|
|
837
|
+
return this.commerceFetch("DELETE", templateId, `/donations/subscriptions/${encodeURIComponent(subscriptionId)}`);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async adoptMonthlyDonors(templateId: string, campaignId: string): Promise<any> {
|
|
841
|
+
return this.commerceFetch("POST", templateId, "/donations/adopt-monthly-donors", { campaignId });
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ── WordPress migration (/api/v1/wp-migration) ──────────────────────────────
|
|
845
|
+
|
|
846
|
+
async previewWordPressMigration(sourceUrl: string): Promise<any> {
|
|
847
|
+
return this.apiFetch("POST", "/api/v1/wp-migration/detect", { sourceUrl }, "WordPress check");
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async startWordPressMigration(params: { sourceUrl: string; ownershipAttested: boolean; acceptAup?: boolean }): Promise<any> {
|
|
851
|
+
this.checkAuth();
|
|
852
|
+
const res = await fetch(`${this.baseUrl}/api/v1/wp-migration`, { method: "POST", headers: this.getHeaders(), body: JSON.stringify(params) });
|
|
853
|
+
const data = (await res.json().catch(() => ({}))) as any;
|
|
854
|
+
// Recoverable outcomes (needs AUP consent, already running) are returned, not thrown, so the assistant can act on them.
|
|
855
|
+
if (!res.ok && !data?.requiresAupAcceptance && !data?.existing) throw new Error(data?.error || `Migration start failed (Status ${res.status})`);
|
|
856
|
+
return { ok: res.ok, ...data };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async listWordPressMigrations(): Promise<any> {
|
|
860
|
+
return this.apiFetch("GET", "/api/v1/wp-migration", undefined, "Migration list");
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private migrationPath(templateId: string, jobId: string): string {
|
|
864
|
+
return `/api/v1/wp-migration/${encodeURIComponent(templateId)}/${encodeURIComponent(jobId)}`;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async getWordPressMigration(templateId: string, jobId: string): Promise<any> {
|
|
868
|
+
return this.apiFetch("GET", this.migrationPath(templateId, jobId), undefined, "Migration status");
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async stepWordPressMigration(templateId: string, jobId: string): Promise<any> {
|
|
872
|
+
return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/step`, undefined, "Migration step");
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async getMigratorConnector(templateId: string, jobId: string, check = false): Promise<any> {
|
|
876
|
+
return this.apiFetch("GET", `${this.migrationPath(templateId, jobId)}/connector${check ? "?check=1" : ""}`, undefined, "Plugin connection");
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async createMigratorConnector(templateId: string, jobId: string): Promise<any> {
|
|
880
|
+
return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/connector`, undefined, "Plugin connection");
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async importMigratorData(templateId: string, jobId: string): Promise<any> {
|
|
884
|
+
return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/connector/import`, undefined, "Private data import");
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async getDomainSwitchPlan(templateId: string, jobId: string): Promise<any> {
|
|
888
|
+
return this.apiFetch("GET", `${this.migrationPath(templateId, jobId)}/cutover`, undefined, "Domain switch plan");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
async verifyDomainSwitch(templateId: string, jobId: string): Promise<any> {
|
|
892
|
+
return this.apiFetch("POST", `${this.migrationPath(templateId, jobId)}/cutover/verify`, undefined, "Domain check");
|
|
893
|
+
}
|
|
573
894
|
}
|