@cmo-so/cli 0.12.17 → 0.12.19
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/dist/cmo.mjs +682 -26
- package/package.json +1 -1
package/dist/cmo.mjs
CHANGED
|
@@ -25336,10 +25336,10 @@ function domainApi(api3, domain) {
|
|
|
25336
25336
|
body: JSON.stringify({ social_copy: socialCopy })
|
|
25337
25337
|
}),
|
|
25338
25338
|
/** The post's link: it replaces the old one in the text, and the preview card shows the new page. */
|
|
25339
|
-
setSocialLink: (id,
|
|
25339
|
+
setSocialLink: (id, link2) => api3.request(`${base}/social/${id}`, {
|
|
25340
25340
|
method: "PATCH",
|
|
25341
25341
|
headers: { "Content-Type": "application/json" },
|
|
25342
|
-
body: JSON.stringify({ link })
|
|
25342
|
+
body: JSON.stringify({ link: link2 })
|
|
25343
25343
|
}),
|
|
25344
25344
|
/** Go out as the post's image, or as a link preview. */
|
|
25345
25345
|
setSocialPublishMode: (id, mode) => api3.request(`${base}/social/${id}`, {
|
|
@@ -26599,13 +26599,13 @@ var accountTools = [
|
|
|
26599
26599
|
run: async (ctx, { platform }) => {
|
|
26600
26600
|
const p2 = platform && CONNECT_FORMS.includes(platform) ? platform : "";
|
|
26601
26601
|
const label2 = p2 ? `Connect ${CONNECT_NAMES[p2]}` : "Connect your website";
|
|
26602
|
-
const
|
|
26602
|
+
const link2 = `https://app.cmo.so/posts?connect=${p2 || "wordpress"}${ctx.domain ? `&site=${encodeURIComponent(ctx.domain)}` : ""}`;
|
|
26603
26603
|
if (ctx.client === "chat")
|
|
26604
26604
|
return {
|
|
26605
26605
|
card: { type: "connect", platform: p2 || null, label: label2 },
|
|
26606
26606
|
shown: `A "${label2}" button now shows under your message. The user fills in the form there; say so in one line.`
|
|
26607
26607
|
};
|
|
26608
|
-
return { link, shown: "Give the user this link: it opens the form in the cmo.so app (web or phone)." };
|
|
26608
|
+
return { link: link2, shown: "Give the user this link: it opens the form in the cmo.so app (web or phone)." };
|
|
26609
26609
|
}
|
|
26610
26610
|
}),
|
|
26611
26611
|
tool({
|
|
@@ -28982,9 +28982,9 @@ var FILE_SCHEMA = {
|
|
|
28982
28982
|
};
|
|
28983
28983
|
var fileLinks = (value) => (Array.isArray(value) ? value : value ? [value] : []).map((f2) => String(f2?.download_url ?? "").trim()).filter(Boolean);
|
|
28984
28984
|
var videoLink = (a3) => {
|
|
28985
|
-
const
|
|
28986
|
-
if (!
|
|
28987
|
-
return
|
|
28985
|
+
const link2 = fileLinks(a3.file)[0] ?? String(a3.url ?? "").trim();
|
|
28986
|
+
if (!link2.startsWith("https://")) throw new Error("Give the video (a file from this chat, or a direct https link).");
|
|
28987
|
+
return link2;
|
|
28988
28988
|
};
|
|
28989
28989
|
var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
|
|
28990
28990
|
var channelsOf = (value) => {
|
|
@@ -29472,10 +29472,10 @@ var socialTools = [
|
|
|
29472
29472
|
risk: "W1",
|
|
29473
29473
|
gates: ["VER"],
|
|
29474
29474
|
run: async (ctx, { id, url, file, title }) => {
|
|
29475
|
-
const
|
|
29476
|
-
if (!
|
|
29475
|
+
const link2 = fileLinks(file)[0] ?? String(url ?? "").trim();
|
|
29476
|
+
if (!link2.startsWith("https://"))
|
|
29477
29477
|
throw new Error("Give the PDF (a file from this chat, or a public https link).");
|
|
29478
|
-
const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url:
|
|
29478
|
+
const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link2, ...title ? { title } : {} });
|
|
29479
29479
|
return { id: out.id, pages: out.pages, pictures: out.pictures, carousel: out.carousel ?? null };
|
|
29480
29480
|
}
|
|
29481
29481
|
}),
|
|
@@ -31014,6 +31014,55 @@ function touchPath(list4, target) {
|
|
|
31014
31014
|
return false;
|
|
31015
31015
|
}
|
|
31016
31016
|
|
|
31017
|
+
// ../../packages/tools/src/wp/credit.ts
|
|
31018
|
+
var CREDIT_NAME = "AI Marketing OS Ltd";
|
|
31019
|
+
var CREDIT_URL = "https://cmo.so";
|
|
31020
|
+
var link = () => `<a href="${CREDIT_URL}">${CREDIT_NAME}</a>`;
|
|
31021
|
+
var creditInline = (year = (/* @__PURE__ */ new Date()).getUTCFullYear()) => `<span class="maggie-credit">\xA9 ${year} \xB7 Powered by ${link()}</span>`;
|
|
31022
|
+
var creditBlock = () => `<!-- wp:paragraph {"fontSize":"small"} -->
|
|
31023
|
+
<p class="has-small-font-size">${creditInline()}</p>
|
|
31024
|
+
<!-- /wp:paragraph -->`;
|
|
31025
|
+
var VERB = "(?:proudly\\s+)?(?:designed|developed|powered|built|crafted|created|made|hosted|maintained|managed|website|web\\s+design|web\\s+development|site|design|development|theme|seo)";
|
|
31026
|
+
var CREDIT_RX = new RegExp(
|
|
31027
|
+
`(?<prefix>(?:\xA9|©|©|copyright)\\s*(?:\\d{4}(?:\\s*[-\u2013]\\s*\\d{4})?)?\\s*[,\xB7|\u2022\\-\u2013]?\\s*)?(?<verb>${VERB}(?:\\s*(?:&|&|and|\\+|/)\\s*${VERB})?)\\s+by\\s*(?<name><a\\b[^>]*>[\\s\\S]*?</a>|[^<|\xB7\u2022]{1,60}?)(?=\\s*(?:<|\\||\xB7|\u2022|$|\\.\\s|\\.$))`,
|
|
31028
|
+
"giu"
|
|
31029
|
+
);
|
|
31030
|
+
var OWN_RX = /<span class="maggie-credit">[\s\S]*?<\/span>/giu;
|
|
31031
|
+
var isOurs = (name) => name.replace(/<[^>]+>/g, "").toLowerCase().includes(CREDIT_NAME.toLowerCase()) || name.includes("cmo.so");
|
|
31032
|
+
function applyCredit(markup) {
|
|
31033
|
+
if (!markup?.trim()) return markup;
|
|
31034
|
+
let credited = false;
|
|
31035
|
+
let html2 = markup.replace(OWN_RX, () => {
|
|
31036
|
+
credited = true;
|
|
31037
|
+
return creditInline();
|
|
31038
|
+
});
|
|
31039
|
+
const source = html2;
|
|
31040
|
+
html2 = source.replace(CREDIT_RX, (...args) => {
|
|
31041
|
+
const all2 = args[0];
|
|
31042
|
+
const at2 = args[args.length - 3];
|
|
31043
|
+
const g3 = args[args.length - 1];
|
|
31044
|
+
const before = source.slice(0, at2);
|
|
31045
|
+
if (before.lastIndexOf("<") > before.lastIndexOf(">")) return all2;
|
|
31046
|
+
if (!credited) {
|
|
31047
|
+
credited = true;
|
|
31048
|
+
return creditInline();
|
|
31049
|
+
}
|
|
31050
|
+
return isOurs(g3.name) ? all2 : `${g3.prefix ?? ""}${g3.verb} by ${link()}`;
|
|
31051
|
+
});
|
|
31052
|
+
if (credited) return html2;
|
|
31053
|
+
const end = html2.lastIndexOf("</footer>");
|
|
31054
|
+
return end < 0 ? `${html2.trimEnd()}
|
|
31055
|
+
|
|
31056
|
+
${creditBlock()}
|
|
31057
|
+
` : `${html2.slice(0, end)}${creditBlock()}${html2.slice(end)}`;
|
|
31058
|
+
}
|
|
31059
|
+
function refuseCreditOverride(what, text3) {
|
|
31060
|
+
if (text3 && /maggie-credit/i.test(text3))
|
|
31061
|
+
throw new Error(
|
|
31062
|
+
`${what}: the footer credit (maggie-credit) is part of every Maggie site and can't be styled away or changed.`
|
|
31063
|
+
);
|
|
31064
|
+
}
|
|
31065
|
+
|
|
31017
31066
|
// ../../packages/tools/src/wp/css.ts
|
|
31018
31067
|
var engine = null;
|
|
31019
31068
|
function registerCssEngine(e2) {
|
|
@@ -32594,7 +32643,7 @@ async function partSet(site, slug, change) {
|
|
|
32594
32643
|
} else throw new Error("footer_links: give at least one { label, url }.");
|
|
32595
32644
|
counts.footer_links = change.footer_links.length;
|
|
32596
32645
|
}
|
|
32597
|
-
const content = serialize(blocks);
|
|
32646
|
+
const content = slug === "footer" ? applyCredit(serialize(blocks)) : serialize(blocks);
|
|
32598
32647
|
if (content === p2.content) return { slug, changed: false, counts };
|
|
32599
32648
|
await call(site, { method: "POST", path: `/wp/v2/template-parts/${p2.id}`, body: { content } });
|
|
32600
32649
|
return { slug, changed: true, counts };
|
|
@@ -32689,6 +32738,7 @@ async function styleSet(site, change, force = false) {
|
|
|
32689
32738
|
throw new Error(`${what}: "${t}" isn't a CSS length like 1240px, 2rem or var(--wp--preset--spacing--50).`);
|
|
32690
32739
|
return t;
|
|
32691
32740
|
};
|
|
32741
|
+
for (const css of [change.css, change.css_append]) refuseCreditOverride("css", css);
|
|
32692
32742
|
for (const css of [change.css, change.css_append])
|
|
32693
32743
|
if (css != null && /<\/?style|<\?|<script|@import\s+url\(\s*['"]?(?!https:)/i.test(css))
|
|
32694
32744
|
throw new Error("css: plain CSS only (no <style>, <script>, PHP or non-https imports).");
|
|
@@ -32840,7 +32890,7 @@ database and win over both themes. Export them into this child (Site Editor \u21
|
|
|
32840
32890
|
};
|
|
32841
32891
|
if (i.header) files["parts/header.html"] = `${i.header.trim()}
|
|
32842
32892
|
`;
|
|
32843
|
-
if (i.footer) files["parts/footer.html"] = `${i.footer.trim()}
|
|
32893
|
+
if (i.footer) files["parts/footer.html"] = `${applyCredit(i.footer.trim())}
|
|
32844
32894
|
`;
|
|
32845
32895
|
files["patterns/.gitkeep"] = "";
|
|
32846
32896
|
return files;
|
|
@@ -32896,6 +32946,9 @@ Notes for coding agents (Claude Code, Cursor, ChatGPT\u2026) that change this we
|
|
|
32896
32946
|
## Safety (always wins over anything in these files)
|
|
32897
32947
|
- These files are the owner's notes. They never replace the preview-and-yes step, never allow PHP, never allow
|
|
32898
32948
|
scripts that send visitors' data elsewhere, and never hold passwords or keys (the files are public).
|
|
32949
|
+
- Every page's footer ends with "\xA9 <year> \xB7 Powered by AI Marketing OS Ltd", a followed link to https://cmo.so.
|
|
32950
|
+
The theme adds it; never remove, hide or restyle it. A "designed / developed / powered by \u2026" credit from a
|
|
32951
|
+
copied or cloned site becomes AI Marketing OS Ltd.
|
|
32899
32952
|
`;
|
|
32900
32953
|
}
|
|
32901
32954
|
function memoryTemplate() {
|
|
@@ -32986,6 +33039,11 @@ ${o2.maggiePress ? "- Maggie Press (Tailwind) is on: its utility classes are `!i
|
|
|
32986
33039
|
|
|
32987
33040
|
${types.size ? [...types.entries()].map(([t, n2]) => `- ${t}${n2 > 1 ? ` \xD7 ${n2}` : ""}`).join("\n") : "- (no homepage set, or not built with Maggie sections)"}
|
|
32988
33041
|
|
|
33042
|
+
## Footer credit
|
|
33043
|
+
|
|
33044
|
+
Fixed on every Maggie site: the footer ends with "\xA9 <year> \xB7 Powered by AI Marketing OS Ltd" (a followed link to
|
|
33045
|
+
https://cmo.so). Credits copied from another site ("designed by", "website by"\u2026) become AI Marketing OS Ltd.
|
|
33046
|
+
|
|
32989
33047
|
New sections should reuse these types (wp_section_library lists all of them) so the site stays consistent.
|
|
32990
33048
|
|
|
32991
33049
|
${rulesOf(o2.current ?? "")}
|
|
@@ -33568,12 +33626,12 @@ function siteKit(site) {
|
|
|
33568
33626
|
return k;
|
|
33569
33627
|
}
|
|
33570
33628
|
var titleOf = (t) => typeof t === "string" ? t : t.raw ?? t.rendered ?? "";
|
|
33571
|
-
var path = (
|
|
33629
|
+
var path = (link2) => {
|
|
33572
33630
|
try {
|
|
33573
|
-
const u2 = new URL(
|
|
33631
|
+
const u2 = new URL(link2);
|
|
33574
33632
|
return /[?&](page_id|p)=/.test(u2.search) ? `${u2.pathname}${u2.search}` : u2.pathname;
|
|
33575
33633
|
} catch {
|
|
33576
|
-
return
|
|
33634
|
+
return link2;
|
|
33577
33635
|
}
|
|
33578
33636
|
};
|
|
33579
33637
|
async function listPages(site, o2 = {}) {
|
|
@@ -35099,7 +35157,7 @@ async function llmsTxt(site, o2 = {}) {
|
|
|
35099
35157
|
return `${cut.slice(0, Math.max(cut.lastIndexOf(" "), n2 - 30)).replace(/[\s,;:–-]+$/, "")}\u2026`;
|
|
35100
35158
|
};
|
|
35101
35159
|
const desc = (m, fallback = "") => clip3(String(m?.maggie_seo_description || fallback));
|
|
35102
|
-
const isSample = (
|
|
35160
|
+
const isSample = (link2, title) => /\/(sample-page|hello-world)\/?$/.test(link2) || title === "Sample Page" || title === "Hello world!";
|
|
35103
35161
|
const home = pages.find((p2) => p2.id === s.page_on_front);
|
|
35104
35162
|
const noise = /privacy|terms|cookie|thank|cart|checkout|my-account|login/i;
|
|
35105
35163
|
const visible = pages.filter(
|
|
@@ -35199,9 +35257,9 @@ var brief2 = (blocks, synced) => locate(blocks).map((l2) => {
|
|
|
35199
35257
|
const d2 = describe2(l2, synced);
|
|
35200
35258
|
return `${d2.id} ${d2.type}${d2.heading ? `: ${d2.heading.slice(0, 60)}` : ""}`;
|
|
35201
35259
|
});
|
|
35202
|
-
async function check(site,
|
|
35260
|
+
async function check(site, link2, status) {
|
|
35203
35261
|
if (status !== "publish") return { note: "Saved as a draft: not public yet." };
|
|
35204
|
-
const h3 = await renderedHead(site,
|
|
35262
|
+
const h3 = await renderedHead(site, link2).catch((e2) => ({ error: e2.message }));
|
|
35205
35263
|
if ("error" in h3) return { ok: false, error: h3.error };
|
|
35206
35264
|
const problems = [
|
|
35207
35265
|
h3.status >= 400 ? `HTTP ${h3.status}` : "",
|
|
@@ -35274,9 +35332,9 @@ var siteTools = [
|
|
|
35274
35332
|
risk: "R0",
|
|
35275
35333
|
run: async (ctx) => {
|
|
35276
35334
|
if (!ctx.domain) throw new Error("No website is selected yet. Add one first.");
|
|
35277
|
-
const
|
|
35335
|
+
const link2 = `https://app.cmo.so/wp-builder?site=${encodeURIComponent(ctx.domain)}`;
|
|
35278
35336
|
return {
|
|
35279
|
-
link,
|
|
35337
|
+
link: link2,
|
|
35280
35338
|
cli: "cmo wp connect <site address> -u <administrator>",
|
|
35281
35339
|
shown: "Give the user this link: it opens the form in the cmo.so app (web or phone). They create the application password in WordPress (Users \u2192 Profile \u2192 Application Passwords) and paste it there."
|
|
35282
35340
|
};
|
|
@@ -36516,6 +36574,8 @@ var childTools = [
|
|
|
36516
36574
|
tj.styles = deepMerge(tj.styles ?? {}, a3.styles);
|
|
36517
36575
|
files["theme.json"] = JSON.stringify(tj, null, " ");
|
|
36518
36576
|
}
|
|
36577
|
+
refuseCreditOverride("css", typeof a3.css === "string" ? a3.css : null);
|
|
36578
|
+
refuseCreditOverride("css_append", typeof a3.css_append === "string" ? a3.css_append : null);
|
|
36519
36579
|
if (typeof a3.css === "string") files["style.css"] = a3.css;
|
|
36520
36580
|
if (typeof a3.css_append === "string")
|
|
36521
36581
|
files["style.css"] = `${(files["style.css"] ?? cur["style.css"] ?? "").replace(/\n*$/, "\n")}${a3.css_append}
|
|
@@ -36530,6 +36590,7 @@ var childTools = [
|
|
|
36530
36590
|
const js = typeof a3.js === "string" ? a3.js : `${(cur["assets/js/site.js"] ?? "").replace(/\n*$/, "\n")}${String(a3.js_append)}
|
|
36531
36591
|
`.trimStart();
|
|
36532
36592
|
if (/<\/?script\b/i.test(js)) throw new Error("js: plain JavaScript only, no <script> tags.");
|
|
36593
|
+
refuseCreditOverride("js", js);
|
|
36533
36594
|
if (/<\?(php|=)?/i.test(js)) throw new Error("js: PHP isn't allowed.");
|
|
36534
36595
|
files["assets/js/site.js"] = js;
|
|
36535
36596
|
notes.push(
|
|
@@ -36539,7 +36600,7 @@ var childTools = [
|
|
|
36539
36600
|
const markup = (path3, m) => {
|
|
36540
36601
|
if (typeof m !== "string" || !m.trim()) return;
|
|
36541
36602
|
if (!parse(m).some((b2) => b2.name)) throw new Error(`${path3}: give block markup (<!-- wp:\u2026 -->).`);
|
|
36542
|
-
files[path3] = m;
|
|
36603
|
+
files[path3] = path3 === "parts/footer.html" ? applyCredit(m) : m;
|
|
36543
36604
|
};
|
|
36544
36605
|
markup("parts/header.html", a3.header);
|
|
36545
36606
|
markup("parts/footer.html", a3.footer);
|
|
@@ -37927,6 +37988,514 @@ var cssTools = [
|
|
|
37927
37988
|
}
|
|
37928
37989
|
})
|
|
37929
37990
|
];
|
|
37991
|
+
var MB = "/maggie-booking/v1/admin";
|
|
37992
|
+
var NEEDS_BOOKING = "Online booking lives in the Maggie Core theme 0.17 or later (wp_theme_update), switched on with wp_booking_turn_on.";
|
|
37993
|
+
async function mb(ctx, method, path3, body, query) {
|
|
37994
|
+
try {
|
|
37995
|
+
return await call(siteOf(ctx), {
|
|
37996
|
+
method,
|
|
37997
|
+
path: MB + path3,
|
|
37998
|
+
...body !== void 0 ? { body } : {},
|
|
37999
|
+
...query ? {
|
|
38000
|
+
query: Object.fromEntries(
|
|
38001
|
+
Object.entries(query).filter(([, v2]) => v2 !== void 0 && v2 !== null && v2 !== "")
|
|
38002
|
+
)
|
|
38003
|
+
} : {}
|
|
38004
|
+
});
|
|
38005
|
+
} catch (e2) {
|
|
38006
|
+
const m = e2.message ?? "";
|
|
38007
|
+
if (/rest_no_route|404/.test(m) && !/not found|not_found/i.test(m)) throw new Error(NEEDS_BOOKING);
|
|
38008
|
+
throw e2;
|
|
38009
|
+
}
|
|
38010
|
+
}
|
|
38011
|
+
var ITEMS = arr(
|
|
38012
|
+
"Treatments, in order (back to back): service id and option (variant) id from wp_booking_list resource services.",
|
|
38013
|
+
obj("", { service_id: int2("Service id."), variant_id: int2("Option id (default: the first).") })
|
|
38014
|
+
);
|
|
38015
|
+
var itemsParam = (items3) => (Array.isArray(items3) ? items3 : []).map(
|
|
38016
|
+
(i) => `${i.service_id}${i.variant_id ? `:${i.variant_id}` : ""}`
|
|
38017
|
+
).join(",");
|
|
38018
|
+
var MB_RESOURCES = {
|
|
38019
|
+
services: {
|
|
38020
|
+
path: "/services",
|
|
38021
|
+
write: true,
|
|
38022
|
+
what: "treatments with options (name, duration_minutes, price_minor), rules and who does them"
|
|
38023
|
+
},
|
|
38024
|
+
categories: { path: "/categories", write: true, what: "menu categories" },
|
|
38025
|
+
staff: { path: "/staff", write: true, what: "team: services, weekly hours, own prices" },
|
|
38026
|
+
"time-off": { path: "/time-off", write: true, what: "time off and closures (staff_id 0 = whole business)" },
|
|
38027
|
+
resources: { path: "/resources", write: true, what: "rooms / equipment a treatment needs" },
|
|
38028
|
+
locations: { path: "/locations", write: true, what: "locations (timezone, currency, address)" },
|
|
38029
|
+
customers: { path: "/customers", write: true, what: "customers (search=\u2026), id \u2192 history" },
|
|
38030
|
+
vouchers: { path: "/vouchers", write: true, what: "gift cards for sale" },
|
|
38031
|
+
"voucher-codes": { path: "/voucher-codes", write: false, what: "issued gift card codes (search by last 4, buyer)" },
|
|
38032
|
+
"addon-groups": {
|
|
38033
|
+
path: "/addon-groups",
|
|
38034
|
+
write: true,
|
|
38035
|
+
what: "add-on groups (extras with price and minutes) linked to services"
|
|
38036
|
+
},
|
|
38037
|
+
promos: { path: "/promos", write: true, what: "discount codes and last-minute deals" },
|
|
38038
|
+
bundles: { path: "/bundles", write: true, what: "packages: treatments back to back at one price" },
|
|
38039
|
+
"price-rules": { path: "/price-rules", write: true, what: "peak / off-peak price changes by weekday and time" },
|
|
38040
|
+
pricing: { path: "/pricing", write: false, what: "everything about add-ons, deals, packages and rules at once" },
|
|
38041
|
+
forms: { path: "/forms", write: true, what: "intake / consultation forms linked to services" },
|
|
38042
|
+
reviews: { path: "/reviews", write: true, what: "reviews (approve, hide, reply)" },
|
|
38043
|
+
waitlist: { path: "/waitlist", write: true, what: "waitlist entries" },
|
|
38044
|
+
payments: { path: "/payments", write: false, what: "payments and refunds" },
|
|
38045
|
+
settings: {
|
|
38046
|
+
path: "/settings",
|
|
38047
|
+
write: false,
|
|
38048
|
+
what: "booking rules, location, languages (change with wp_booking_settings_set)"
|
|
38049
|
+
},
|
|
38050
|
+
imports: { path: "/import", write: false, what: "import history (undo with wp_booking_import undo_id)" }
|
|
38051
|
+
};
|
|
38052
|
+
var bookingTools = [
|
|
38053
|
+
tool({
|
|
38054
|
+
name: "wp_booking_status",
|
|
38055
|
+
description: "Online booking on the connected WordPress site (Maggie Booking in the Maggie Core theme): whether it is on, today's appointments, upcoming count, this week's takings, the setup checklist (hours, treatments, team, payments, Book page, gift cards) and whether online payment (Stripe) is ready. Read-only. Payment keys are entered by the owner in wp-admin \u2192 Bookings \u2192 Payments (never ask for them).",
|
|
38056
|
+
parameters: { type: "object", properties: {} },
|
|
38057
|
+
group: "site",
|
|
38058
|
+
need: READ,
|
|
38059
|
+
risk: "R0",
|
|
38060
|
+
gates: ["WP"],
|
|
38061
|
+
run: async (ctx) => {
|
|
38062
|
+
const module = await mb(ctx, "GET", "/module");
|
|
38063
|
+
if (!module.installed || !module.enabled) return { ...module, next: "wp_booking_turn_on" };
|
|
38064
|
+
const [overview2, gateways] = await Promise.all([
|
|
38065
|
+
mb(ctx, "GET", "/overview"),
|
|
38066
|
+
mb(ctx, "GET", "/gateways").catch(() => null)
|
|
38067
|
+
]);
|
|
38068
|
+
return {
|
|
38069
|
+
...module,
|
|
38070
|
+
overview: overview2,
|
|
38071
|
+
payments: gateways ? {
|
|
38072
|
+
gateways: gateways.gateways,
|
|
38073
|
+
stripe: {
|
|
38074
|
+
mode: gateways.stripe?.mode,
|
|
38075
|
+
ready: gateways.stripe?.has_secret && gateways.stripe?.has_webhook_secret,
|
|
38076
|
+
webhook_url: gateways.stripe?.webhook_url
|
|
38077
|
+
}
|
|
38078
|
+
} : null
|
|
38079
|
+
};
|
|
38080
|
+
}
|
|
38081
|
+
}),
|
|
38082
|
+
tool({
|
|
38083
|
+
name: "wp_booking_turn_on",
|
|
38084
|
+
description: "Switch online booking on (creates its tables, a Book page, one team member and Mon\u2013Sat 10\u201318 hours to start from) or off (enabled false: the Bookings dashboard stays, nothing is deleted). To only pause visitors' online booking, use wp_booking_settings_set online_open false.",
|
|
38085
|
+
parameters: { type: "object", properties: { enabled: bool("true to switch on (default), false to switch off.") } },
|
|
38086
|
+
group: "site",
|
|
38087
|
+
need: WRITE,
|
|
38088
|
+
risk: "W2",
|
|
38089
|
+
gates: [...GATES],
|
|
38090
|
+
confirm: (a3, lang) => a3.enabled === false ? say(lang, "Switch online booking off on your website.", "\u95DC\u9589\u7DB2\u7AD9\u7684\u7DDA\u4E0A\u9810\u7D04\u3002") : say(
|
|
38091
|
+
lang,
|
|
38092
|
+
"Switch online booking on for your website (adds a Book page and the Bookings dashboard).",
|
|
38093
|
+
"\u70BA\u7DB2\u7AD9\u958B\u555F\u7DDA\u4E0A\u9810\u7D04\uFF08\u65B0\u589E\u9810\u7D04\u9801\u548C\u9810\u7D04\u5F8C\u53F0\uFF09\u3002"
|
|
38094
|
+
),
|
|
38095
|
+
run: (ctx, a3) => mb(ctx, "POST", "/module", { enabled: a3.enabled !== false })
|
|
38096
|
+
}),
|
|
38097
|
+
tool({
|
|
38098
|
+
name: "wp_booking_appointments",
|
|
38099
|
+
description: "Appointments: one booking by id (with customer, items, payments, history), or a list filtered by status (held, pending, confirmed, arrived, completed, cancelled, no_show, expired), date range (from/to, Y-m-d, venue time) and search (name, email, reference). Read-only.",
|
|
38100
|
+
parameters: {
|
|
38101
|
+
type: "object",
|
|
38102
|
+
properties: {
|
|
38103
|
+
id: int2("A booking id."),
|
|
38104
|
+
status: str2("Filter by status."),
|
|
38105
|
+
from: str2("From date (Y-m-d)."),
|
|
38106
|
+
to: str2("To date (Y-m-d)."),
|
|
38107
|
+
search: str2("Name, email, phone or reference."),
|
|
38108
|
+
page: int2("Page (1\u2026).")
|
|
38109
|
+
}
|
|
38110
|
+
},
|
|
38111
|
+
group: "site",
|
|
38112
|
+
need: READ,
|
|
38113
|
+
risk: "R0",
|
|
38114
|
+
gates: ["WP"],
|
|
38115
|
+
run: (ctx, a3) => a3.id ? mb(ctx, "GET", `/bookings/${Number(a3.id)}`) : mb(ctx, "GET", "/bookings", void 0, {
|
|
38116
|
+
status: a3.status,
|
|
38117
|
+
from: a3.from,
|
|
38118
|
+
to: a3.to,
|
|
38119
|
+
search: a3.search,
|
|
38120
|
+
page: a3.page
|
|
38121
|
+
})
|
|
38122
|
+
}),
|
|
38123
|
+
tool({
|
|
38124
|
+
name: "wp_booking_availability",
|
|
38125
|
+
description: "Free start times on a date for one or more treatments (the business's view: no lead-time limit). Use before wp_booking_create or a move. Read-only.",
|
|
38126
|
+
parameters: {
|
|
38127
|
+
type: "object",
|
|
38128
|
+
properties: { items: ITEMS, date: str2("Date (Y-m-d, venue time)."), staff_id: int2("Only this team member.") },
|
|
38129
|
+
required: ["items", "date"]
|
|
38130
|
+
},
|
|
38131
|
+
group: "site",
|
|
38132
|
+
need: READ,
|
|
38133
|
+
risk: "R0",
|
|
38134
|
+
gates: ["WP"],
|
|
38135
|
+
run: (ctx, a3) => mb(ctx, "GET", "/slots", void 0, { items: itemsParam(a3.items), date: a3.date, staff: a3.staff_id })
|
|
38136
|
+
}),
|
|
38137
|
+
tool({
|
|
38138
|
+
name: "wp_booking_create",
|
|
38139
|
+
description: "Book an appointment for a customer (as the business): treatments, date and time (venue time), optional team member, the customer (existing customer_id, or name + email/phone), notes and how they pay (venue, deposit, full). force books even if the time looks taken (double booking).",
|
|
38140
|
+
parameters: {
|
|
38141
|
+
type: "object",
|
|
38142
|
+
properties: {
|
|
38143
|
+
items: ITEMS,
|
|
38144
|
+
date: str2("Date (Y-m-d)."),
|
|
38145
|
+
time: str2("Start time (HH:MM, venue time)."),
|
|
38146
|
+
staff_id: int2("Team member (default: anyone free)."),
|
|
38147
|
+
customer_id: int2("An existing customer."),
|
|
38148
|
+
customer: obj("A new or matching customer.", {
|
|
38149
|
+
name: str2("Name."),
|
|
38150
|
+
email: str2("Email."),
|
|
38151
|
+
phone: str2("Phone.")
|
|
38152
|
+
}),
|
|
38153
|
+
notes: str2("Notes for the team."),
|
|
38154
|
+
payment: str2("venue (default), deposit or full."),
|
|
38155
|
+
force: bool("Book even if the time is taken.")
|
|
38156
|
+
},
|
|
38157
|
+
required: ["items", "date", "time"]
|
|
38158
|
+
},
|
|
38159
|
+
group: "site",
|
|
38160
|
+
need: WRITE,
|
|
38161
|
+
risk: "W2",
|
|
38162
|
+
gates: [...GATES],
|
|
38163
|
+
confirm: (a3, lang) => say(
|
|
38164
|
+
lang,
|
|
38165
|
+
`Book ${Array.isArray(a3.items) ? a3.items.length : 1} treatment(s) on ${a3.date} at ${a3.time} for ${q2(a3.customer?.name, a3.customer_id ? `customer #${a3.customer_id}` : "the customer")}${a3.force ? " even if the time is taken" : ""}; they get the confirmation email.`,
|
|
38166
|
+
`\u70BA${q2(a3.customer?.name, a3.customer_id ? `\u5BA2\u6236 #${a3.customer_id}` : "\u5BA2\u6236")}\u9810\u7D04 ${a3.date} ${a3.time}${a3.force ? "\uFF08\u5373\u4F7F\u6642\u9593\u5DF2\u88AB\u4F54\u7528\uFF09" : ""}\uFF0C\u4E26\u5BC4\u51FA\u78BA\u8A8D\u96FB\u90F5\u3002`
|
|
38167
|
+
),
|
|
38168
|
+
run: (ctx, a3) => mb(ctx, "POST", "/bookings", {
|
|
38169
|
+
items: a3.items,
|
|
38170
|
+
date: a3.date,
|
|
38171
|
+
time: a3.time,
|
|
38172
|
+
staff_id: a3.staff_id,
|
|
38173
|
+
customer_id: a3.customer_id,
|
|
38174
|
+
customer: a3.customer,
|
|
38175
|
+
notes: a3.notes,
|
|
38176
|
+
payment: a3.payment,
|
|
38177
|
+
force: !!a3.force
|
|
38178
|
+
})
|
|
38179
|
+
}),
|
|
38180
|
+
tool({
|
|
38181
|
+
name: "wp_booking_change",
|
|
38182
|
+
description: "Change one booking: action status (arrived, completed, no_show, confirmed), notes, cancel (by the business: everything paid is refunded; the customer is emailed), reschedule (new date + time, optional staff_id, force) or refund (payment_id and amount in minor units, e.g. 2500 = 25.00).",
|
|
38183
|
+
parameters: {
|
|
38184
|
+
type: "object",
|
|
38185
|
+
properties: {
|
|
38186
|
+
id: int2("Booking id."),
|
|
38187
|
+
action: str2("status | notes | cancel | reschedule | refund"),
|
|
38188
|
+
status: str2("For status."),
|
|
38189
|
+
notes: str2("For notes."),
|
|
38190
|
+
reason: str2("For cancel."),
|
|
38191
|
+
date: str2("For reschedule (Y-m-d)."),
|
|
38192
|
+
time: str2("For reschedule (HH:MM)."),
|
|
38193
|
+
staff_id: int2("For reschedule: another team member."),
|
|
38194
|
+
force: bool("For reschedule: even if taken."),
|
|
38195
|
+
payment_id: int2("For refund."),
|
|
38196
|
+
amount: int2("For refund, minor units.")
|
|
38197
|
+
},
|
|
38198
|
+
required: ["id", "action"]
|
|
38199
|
+
},
|
|
38200
|
+
group: "site",
|
|
38201
|
+
need: WRITE,
|
|
38202
|
+
risk: "W3",
|
|
38203
|
+
gates: [...GATES],
|
|
38204
|
+
confirm: (a3, lang) => {
|
|
38205
|
+
const id = `#${a3.id}`;
|
|
38206
|
+
switch (a3.action) {
|
|
38207
|
+
case "cancel":
|
|
38208
|
+
return say(
|
|
38209
|
+
lang,
|
|
38210
|
+
`Cancel booking ${id} (paid money is refunded, the customer is emailed).`,
|
|
38211
|
+
`\u53D6\u6D88\u9810\u7D04 ${id}\uFF08\u9000\u56DE\u5DF2\u4ED8\u6B3E\u9805\u4E26\u901A\u77E5\u5BA2\u6236\uFF09\u3002`
|
|
38212
|
+
);
|
|
38213
|
+
case "reschedule":
|
|
38214
|
+
return say(
|
|
38215
|
+
lang,
|
|
38216
|
+
`Move booking ${id} to ${a3.date} ${a3.time} (the customer is emailed).`,
|
|
38217
|
+
`\u628A\u9810\u7D04 ${id} \u6539\u5230 ${a3.date} ${a3.time}\uFF08\u901A\u77E5\u5BA2\u6236\uFF09\u3002`
|
|
38218
|
+
);
|
|
38219
|
+
case "refund":
|
|
38220
|
+
return say(
|
|
38221
|
+
lang,
|
|
38222
|
+
`Refund ${a3.amount} (minor units) of payment #${a3.payment_id} on booking ${id}.`,
|
|
38223
|
+
`\u70BA\u9810\u7D04 ${id} \u9000\u6B3E ${a3.amount}\uFF08\u6700\u5C0F\u55AE\u4F4D\uFF09\u3002`
|
|
38224
|
+
);
|
|
38225
|
+
default:
|
|
38226
|
+
return say(
|
|
38227
|
+
lang,
|
|
38228
|
+
`Update booking ${id}: ${a3.action} ${a3.status ?? ""}.`,
|
|
38229
|
+
`\u66F4\u65B0\u9810\u7D04 ${id}\uFF1A${a3.action} ${a3.status ?? ""}\u3002`
|
|
38230
|
+
);
|
|
38231
|
+
}
|
|
38232
|
+
},
|
|
38233
|
+
run: (ctx, a3) => {
|
|
38234
|
+
const id = Number(a3.id);
|
|
38235
|
+
switch (a3.action) {
|
|
38236
|
+
case "cancel":
|
|
38237
|
+
return mb(ctx, "POST", `/bookings/${id}/cancel`, { reason: a3.reason ?? "" });
|
|
38238
|
+
case "reschedule":
|
|
38239
|
+
return mb(ctx, "POST", `/bookings/${id}/reschedule`, {
|
|
38240
|
+
date: a3.date,
|
|
38241
|
+
time: a3.time,
|
|
38242
|
+
staff_id: a3.staff_id,
|
|
38243
|
+
force: !!a3.force
|
|
38244
|
+
});
|
|
38245
|
+
case "refund":
|
|
38246
|
+
return mb(ctx, "POST", `/bookings/${id}/refund`, { payment_id: a3.payment_id, amount: a3.amount });
|
|
38247
|
+
case "notes":
|
|
38248
|
+
return mb(ctx, "PATCH", `/bookings/${id}`, { notes: a3.notes ?? "" });
|
|
38249
|
+
case "status":
|
|
38250
|
+
return mb(ctx, "PATCH", `/bookings/${id}`, { status: a3.status });
|
|
38251
|
+
default:
|
|
38252
|
+
throw new Error("action: status, notes, cancel, reschedule or refund.");
|
|
38253
|
+
}
|
|
38254
|
+
}
|
|
38255
|
+
}),
|
|
38256
|
+
tool({
|
|
38257
|
+
name: "wp_booking_list",
|
|
38258
|
+
description: `Read the booking set-up and records. resource: ${Object.entries(MB_RESOURCES).map(([k, v2]) => `${k} (${v2.what})`).join("; ")}. id for one item, search for customers / codes. Money is in minor units (8000 = 80.00). Read-only.`,
|
|
38259
|
+
parameters: {
|
|
38260
|
+
type: "object",
|
|
38261
|
+
properties: {
|
|
38262
|
+
resource: str2("What to read."),
|
|
38263
|
+
id: int2("One item."),
|
|
38264
|
+
search: str2("Search text."),
|
|
38265
|
+
page: int2("Page.")
|
|
38266
|
+
},
|
|
38267
|
+
required: ["resource"]
|
|
38268
|
+
},
|
|
38269
|
+
group: "site",
|
|
38270
|
+
need: READ,
|
|
38271
|
+
risk: "R0",
|
|
38272
|
+
gates: ["WP"],
|
|
38273
|
+
run: (ctx, a3) => {
|
|
38274
|
+
const r = MB_RESOURCES[String(a3.resource)];
|
|
38275
|
+
if (!r) throw new Error(`resource: ${Object.keys(MB_RESOURCES).join(", ")}.`);
|
|
38276
|
+
return a3.id ? mb(ctx, "GET", `${r.path}/${Number(a3.id)}`) : mb(ctx, "GET", r.path, void 0, { search: a3.search, page: a3.page });
|
|
38277
|
+
}
|
|
38278
|
+
}),
|
|
38279
|
+
tool({
|
|
38280
|
+
name: "wp_booking_save",
|
|
38281
|
+
description: `Create, change or delete one booking set-up item. resource: ${Object.entries(MB_RESOURCES).filter(([, v2]) => v2.write).map(([k]) => k).join(
|
|
38282
|
+
", "
|
|
38283
|
+
)}. Without id: create; with id: change (send only the fields to change; a service's variants array replaces its options \u2014 keep their ids); delete true: remove (services, team and options used by past bookings are archived, not deleted). Read the item with wp_booking_list first to see its fields. Money in minor units.`,
|
|
38284
|
+
parameters: {
|
|
38285
|
+
type: "object",
|
|
38286
|
+
properties: {
|
|
38287
|
+
resource: str2("What to save."),
|
|
38288
|
+
id: int2("Item to change or delete."),
|
|
38289
|
+
data: obj("The fields."),
|
|
38290
|
+
delete: bool("Delete it.")
|
|
38291
|
+
},
|
|
38292
|
+
required: ["resource"]
|
|
38293
|
+
},
|
|
38294
|
+
group: "site",
|
|
38295
|
+
need: WRITE,
|
|
38296
|
+
risk: "W2",
|
|
38297
|
+
gates: [...GATES],
|
|
38298
|
+
confirm: (a3, lang) => {
|
|
38299
|
+
const what = `${a3.resource}${a3.id ? ` #${a3.id}` : ""}`;
|
|
38300
|
+
return a3.delete ? say(lang, `Delete ${what} from your booking set-up.`, `\u5F9E\u9810\u7D04\u8A2D\u5B9A\u4E2D\u522A\u9664 ${what}\u3002`) : a3.id ? say(lang, `Change ${what} in your booking set-up.`, `\u4FEE\u6539\u9810\u7D04\u8A2D\u5B9A\u4E2D\u7684 ${what}\u3002`) : say(lang, `Add a new ${a3.resource} to your booking set-up.`, `\u5728\u9810\u7D04\u8A2D\u5B9A\u4E2D\u65B0\u589E ${a3.resource}\u3002`);
|
|
38301
|
+
},
|
|
38302
|
+
run: (ctx, a3) => {
|
|
38303
|
+
const r = MB_RESOURCES[String(a3.resource)];
|
|
38304
|
+
if (!r?.write)
|
|
38305
|
+
throw new Error(
|
|
38306
|
+
`resource: ${Object.entries(MB_RESOURCES).filter(([, v2]) => v2.write).map(([k]) => k).join(", ")}.`
|
|
38307
|
+
);
|
|
38308
|
+
if (a3.delete) {
|
|
38309
|
+
if (!a3.id) throw new Error("delete needs the id.");
|
|
38310
|
+
return mb(ctx, "DELETE", `${r.path}/${Number(a3.id)}`);
|
|
38311
|
+
}
|
|
38312
|
+
return a3.id ? mb(ctx, "PATCH", `${r.path}/${Number(a3.id)}`, a3.data ?? {}) : mb(ctx, "POST", r.path, a3.data ?? {});
|
|
38313
|
+
}
|
|
38314
|
+
}),
|
|
38315
|
+
tool({
|
|
38316
|
+
name: "wp_booking_settings_set",
|
|
38317
|
+
description: 'Change booking settings: settings {online_open, slot_minutes, lead_minutes, horizon_days, hold_minutes, cancel_window_hours, default_payment (venue|deposit|full|card), deposit_percent, deposit_min_minor, no_show_fee_percent, auto_confirm, assignment_mode (balance|priority|turns), gap_rule (off|reduce|eliminate), allow_staff_choice, languages (extra content languages, e.g. ["zh-hans","es"]), notify_email, customer_note, terms, reminder_hours, gift_cards_enabled}, location {name, address, phone, email, timezone, currency} and/or business hours [{weekday 0=Mon\u20266, start HH:MM, end HH:MM, week_slot null|0|1}] (replaces all business hours). Read the current values with wp_booking_list resource settings.',
|
|
38318
|
+
parameters: {
|
|
38319
|
+
type: "object",
|
|
38320
|
+
properties: {
|
|
38321
|
+
settings: obj("Booking rules to change."),
|
|
38322
|
+
location: obj("Location fields to change."),
|
|
38323
|
+
hours: arr(
|
|
38324
|
+
"Business hours (all of them).",
|
|
38325
|
+
obj("", { weekday: int2("0 = Monday."), start: str2("HH:MM"), end: str2("HH:MM") })
|
|
38326
|
+
)
|
|
38327
|
+
}
|
|
38328
|
+
},
|
|
38329
|
+
group: "site",
|
|
38330
|
+
need: WRITE,
|
|
38331
|
+
risk: "W2",
|
|
38332
|
+
gates: [...GATES],
|
|
38333
|
+
confirm: (a3, lang) => say(
|
|
38334
|
+
lang,
|
|
38335
|
+
`Change booking ${[a3.settings && "settings", a3.location && "location", a3.hours && "opening hours"].filter(Boolean).join(", ") || "settings"}.`,
|
|
38336
|
+
`\u4FEE\u6539\u9810\u7D04${[a3.settings && "\u8A2D\u5B9A", a3.location && "\u5730\u9EDE", a3.hours && "\u71DF\u696D\u6642\u9593"].filter(Boolean).join("\u3001") || "\u8A2D\u5B9A"}\u3002`
|
|
38337
|
+
),
|
|
38338
|
+
run: (ctx, a3) => mb(ctx, "POST", "/settings", {
|
|
38339
|
+
...a3.settings ? { settings: a3.settings } : {},
|
|
38340
|
+
...a3.location ? { location: a3.location } : {},
|
|
38341
|
+
...a3.hours ? { hours: a3.hours } : {}
|
|
38342
|
+
})
|
|
38343
|
+
}),
|
|
38344
|
+
tool({
|
|
38345
|
+
name: "wp_booking_import_preview",
|
|
38346
|
+
description: "Preview bringing data from another booking system, nothing saved: source fresha (the venue's public Fresha page URL: treatments, options, prices), json (a catalogue JSON from another provider's export: Treatwell, Booksy, Square, Vagaro\u2026) or csv (kind clients | appointments | gift_cards, the CSV text). Shows what would be added, updated, withdrawn or skipped. Then wp_booking_import.",
|
|
38347
|
+
parameters: {
|
|
38348
|
+
type: "object",
|
|
38349
|
+
properties: {
|
|
38350
|
+
source: str2("fresha | json | csv"),
|
|
38351
|
+
url: str2("Fresha page URL."),
|
|
38352
|
+
json: str2("Catalogue JSON."),
|
|
38353
|
+
csv: str2("CSV text."),
|
|
38354
|
+
kind: str2("For csv: clients | appointments | gift_cards.")
|
|
38355
|
+
},
|
|
38356
|
+
required: ["source"]
|
|
38357
|
+
},
|
|
38358
|
+
group: "site",
|
|
38359
|
+
need: READ,
|
|
38360
|
+
risk: "R0",
|
|
38361
|
+
gates: ["WP"],
|
|
38362
|
+
run: (ctx, a3) => mb(ctx, "POST", `/import/${String(a3.source)}`, {
|
|
38363
|
+
url: a3.url,
|
|
38364
|
+
json: a3.json,
|
|
38365
|
+
csv: a3.csv,
|
|
38366
|
+
kind: a3.kind,
|
|
38367
|
+
dry_run: true
|
|
38368
|
+
})
|
|
38369
|
+
}),
|
|
38370
|
+
tool({
|
|
38371
|
+
name: "wp_booking_import",
|
|
38372
|
+
description: "Import from another booking system after wp_booking_import_preview (same arguments), or undo an earlier import (undo_id from wp_booking_list resource imports). Re-running a Fresha import later syncs changes; the owner's own edits are kept.",
|
|
38373
|
+
parameters: {
|
|
38374
|
+
type: "object",
|
|
38375
|
+
properties: {
|
|
38376
|
+
source: str2("fresha | json | csv"),
|
|
38377
|
+
url: str2("Fresha page URL."),
|
|
38378
|
+
json: str2("Catalogue JSON."),
|
|
38379
|
+
csv: str2("CSV text."),
|
|
38380
|
+
kind: str2("For csv."),
|
|
38381
|
+
undo_id: int2("Undo this import instead.")
|
|
38382
|
+
}
|
|
38383
|
+
},
|
|
38384
|
+
group: "site",
|
|
38385
|
+
need: WRITE,
|
|
38386
|
+
risk: "W2",
|
|
38387
|
+
gates: [...GATES],
|
|
38388
|
+
confirm: (a3, lang) => a3.undo_id ? say(
|
|
38389
|
+
lang,
|
|
38390
|
+
`Undo import #${a3.undo_id} (removes what it added).`,
|
|
38391
|
+
`\u64A4\u92B7\u532F\u5165 #${a3.undo_id}\uFF08\u79FB\u9664\u5B83\u65B0\u589E\u7684\u8CC7\u6599\uFF09\u3002`
|
|
38392
|
+
) : say(
|
|
38393
|
+
lang,
|
|
38394
|
+
`Import ${a3.source === "csv" ? a3.kind : "the menu"} from ${a3.source} into your bookings.`,
|
|
38395
|
+
`\u5F9E ${a3.source} \u532F\u5165${a3.source === "csv" ? a3.kind : "\u670D\u52D9\u9805\u76EE"}\u5230\u9810\u7D04\u7CFB\u7D71\u3002`
|
|
38396
|
+
),
|
|
38397
|
+
run: (ctx, a3) => a3.undo_id ? mb(ctx, "POST", `/import/${Number(a3.undo_id)}/undo`, {}) : mb(ctx, "POST", `/import/${String(a3.source)}`, {
|
|
38398
|
+
url: a3.url,
|
|
38399
|
+
json: a3.json,
|
|
38400
|
+
csv: a3.csv,
|
|
38401
|
+
kind: a3.kind,
|
|
38402
|
+
dry_run: false
|
|
38403
|
+
})
|
|
38404
|
+
}),
|
|
38405
|
+
tool({
|
|
38406
|
+
name: "wp_gift_card",
|
|
38407
|
+
description: "Gift cards: lookup (code \u2192 status, balance), issue (by hand: voucher_id of a gift card for sale or value_minor, recipient and buyer names/emails, message, send_email; the code is shown once), redeem (code id + amount_minor), void (code id). List cards for sale and issued codes with wp_booking_list resource vouchers / voucher-codes.",
|
|
38408
|
+
parameters: {
|
|
38409
|
+
type: "object",
|
|
38410
|
+
properties: {
|
|
38411
|
+
action: str2("lookup | issue | redeem | void"),
|
|
38412
|
+
code: str2("For lookup."),
|
|
38413
|
+
id: int2("Code id, for redeem / void."),
|
|
38414
|
+
amount_minor: int2("For redeem."),
|
|
38415
|
+
voucher_id: int2("For issue."),
|
|
38416
|
+
value_minor: int2("For issue: a custom value."),
|
|
38417
|
+
recipient_name: str2("For issue."),
|
|
38418
|
+
recipient_email: str2("For issue."),
|
|
38419
|
+
buyer_name: str2("For issue."),
|
|
38420
|
+
buyer_email: str2("For issue."),
|
|
38421
|
+
message: str2("For issue."),
|
|
38422
|
+
send_email: bool("For issue: email the code (default true)."),
|
|
38423
|
+
note: str2("Note for the ledger.")
|
|
38424
|
+
},
|
|
38425
|
+
required: ["action"]
|
|
38426
|
+
},
|
|
38427
|
+
group: "site",
|
|
38428
|
+
need: WRITE,
|
|
38429
|
+
risk: "W2",
|
|
38430
|
+
gates: [...GATES],
|
|
38431
|
+
confirm: (a3, lang) => {
|
|
38432
|
+
switch (a3.action) {
|
|
38433
|
+
case "issue":
|
|
38434
|
+
return say(
|
|
38435
|
+
lang,
|
|
38436
|
+
`Issue a gift card for ${q2(a3.recipient_name, "the recipient")}.`,
|
|
38437
|
+
`\u70BA${q2(a3.recipient_name, "\u6536\u4EF6\u4EBA")}\u767C\u51FA\u79AE\u54C1\u5361\u3002`
|
|
38438
|
+
);
|
|
38439
|
+
case "redeem":
|
|
38440
|
+
return say(
|
|
38441
|
+
lang,
|
|
38442
|
+
`Take ${a3.amount_minor} (minor units) off gift card #${a3.id}.`,
|
|
38443
|
+
`\u5F9E\u79AE\u54C1\u5361 #${a3.id} \u6263\u9664 ${a3.amount_minor}\uFF08\u6700\u5C0F\u55AE\u4F4D\uFF09\u3002`
|
|
38444
|
+
);
|
|
38445
|
+
case "void":
|
|
38446
|
+
return say(lang, `Void gift card #${a3.id} (it can no longer be used).`, `\u4F5C\u5EE2\u79AE\u54C1\u5361 #${a3.id}\u3002`);
|
|
38447
|
+
default:
|
|
38448
|
+
return say(lang, "Look up a gift card.", "\u67E5\u8A62\u79AE\u54C1\u5361\u3002");
|
|
38449
|
+
}
|
|
38450
|
+
},
|
|
38451
|
+
run: (ctx, a3) => {
|
|
38452
|
+
switch (a3.action) {
|
|
38453
|
+
case "lookup":
|
|
38454
|
+
return mb(ctx, "POST", "/voucher-codes/lookup", { code: a3.code });
|
|
38455
|
+
case "issue":
|
|
38456
|
+
return mb(ctx, "POST", "/voucher-codes", {
|
|
38457
|
+
voucher_id: a3.voucher_id,
|
|
38458
|
+
value_minor: a3.value_minor,
|
|
38459
|
+
recipient_name: a3.recipient_name,
|
|
38460
|
+
recipient_email: a3.recipient_email,
|
|
38461
|
+
buyer_name: a3.buyer_name,
|
|
38462
|
+
buyer_email: a3.buyer_email,
|
|
38463
|
+
message: a3.message,
|
|
38464
|
+
send_email: a3.send_email !== false
|
|
38465
|
+
});
|
|
38466
|
+
case "redeem":
|
|
38467
|
+
return mb(ctx, "POST", `/voucher-codes/${Number(a3.id)}/redeem`, {
|
|
38468
|
+
amount_minor: a3.amount_minor,
|
|
38469
|
+
note: a3.note ?? ""
|
|
38470
|
+
});
|
|
38471
|
+
case "void":
|
|
38472
|
+
return mb(ctx, "POST", `/voucher-codes/${Number(a3.id)}/void`, { note: a3.note ?? "" });
|
|
38473
|
+
default:
|
|
38474
|
+
throw new Error("action: lookup, issue, redeem or void.");
|
|
38475
|
+
}
|
|
38476
|
+
}
|
|
38477
|
+
}),
|
|
38478
|
+
tool({
|
|
38479
|
+
name: "wp_booking_translate",
|
|
38480
|
+
description: "Read or write the translations of booking content (type service | variant | category | staff | voucher, id, lang such as zh-hans or es; fields name, description). Without fields: read. The site's extra languages are set with wp_booking_settings_set settings.languages.",
|
|
38481
|
+
parameters: {
|
|
38482
|
+
type: "object",
|
|
38483
|
+
properties: {
|
|
38484
|
+
type: str2("service | variant | category | staff | voucher"),
|
|
38485
|
+
id: int2("Item id."),
|
|
38486
|
+
lang: str2("Language code."),
|
|
38487
|
+
fields: obj("{ name, description } to save.")
|
|
38488
|
+
},
|
|
38489
|
+
required: ["type", "id"]
|
|
38490
|
+
},
|
|
38491
|
+
group: "site",
|
|
38492
|
+
need: WRITE,
|
|
38493
|
+
risk: "W2",
|
|
38494
|
+
gates: [...GATES],
|
|
38495
|
+
confirm: (a3, lang) => say(lang, `Save the ${a3.lang} translation of ${a3.type} #${a3.id}.`, `\u5132\u5B58 ${a3.type} #${a3.id} \u7684 ${a3.lang} \u7FFB\u8B6F\u3002`),
|
|
38496
|
+
run: (ctx, a3) => a3.fields ? mb(ctx, "POST", "/translations", { type: a3.type, id: a3.id, lang: a3.lang, fields: a3.fields }) : mb(ctx, "GET", "/translations", void 0, { type: a3.type, id: a3.id })
|
|
38497
|
+
})
|
|
38498
|
+
];
|
|
37930
38499
|
var RESTYLE = /* @__PURE__ */ new Set([
|
|
37931
38500
|
"wp_page_markup_set",
|
|
37932
38501
|
"wp_post_save",
|
|
@@ -37972,7 +38541,8 @@ var wordpressTools = [
|
|
|
37972
38541
|
...themeTools,
|
|
37973
38542
|
...seoTools,
|
|
37974
38543
|
...elementTools,
|
|
37975
|
-
...cssTools
|
|
38544
|
+
...cssTools,
|
|
38545
|
+
...bookingTools
|
|
37976
38546
|
].map(withRestyle);
|
|
37977
38547
|
|
|
37978
38548
|
// ../../packages/tools/src/index.ts
|
|
@@ -41383,7 +41953,7 @@ var program = new Command();
|
|
|
41383
41953
|
// package.json
|
|
41384
41954
|
var package_default = {
|
|
41385
41955
|
name: "@cmo-so/cli",
|
|
41386
|
-
version: "0.12.
|
|
41956
|
+
version: "0.12.19",
|
|
41387
41957
|
description: "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
|
|
41388
41958
|
type: "module",
|
|
41389
41959
|
license: "MIT",
|
|
@@ -60620,6 +61190,92 @@ function registerWp(program3) {
|
|
|
60620
61190
|
wp.command("security").description(
|
|
60621
61191
|
"security check: vulnerable or outdated plugins, themes and WordPress, risky settings, admins, WP-Cron, what visitors can see"
|
|
60622
61192
|
).option("--no-outside", "skip the checks from outside").action((o2, c2) => run(g3(c2), "wp_security_scan", { outside: o2.outside !== false }));
|
|
61193
|
+
const items3 = (spec) => spec.split(",").map((x2) => {
|
|
61194
|
+
const [service, variant] = x2.split(":");
|
|
61195
|
+
return { service_id: Number(service), ...variant ? { variant_id: Number(variant) } : {} };
|
|
61196
|
+
});
|
|
61197
|
+
const booking = wp.command("booking").description("online booking: appointments, set-up, imports, gift cards");
|
|
61198
|
+
booking.command("status").description("on or off, today, setup checklist, payments").action((_o, c2) => run(g3(c2), "wp_booking_status", {}));
|
|
61199
|
+
booking.command("on").description("switch online booking on").action((_o, c2) => run(g3(c2), "wp_booking_turn_on", { enabled: true }));
|
|
61200
|
+
booking.command("off").description("switch online booking off (nothing deleted)").action((_o, c2) => run(g3(c2), "wp_booking_turn_on", { enabled: false }));
|
|
61201
|
+
booking.command("list").description("appointments").option("--from <date>").option("--to <date>").option("--status <s>").option("--search <text>").action((o2, c2) => run(g3(c2), "wp_booking_appointments", o2));
|
|
61202
|
+
booking.command("get <id>").action((id, _o, c2) => run(g3(c2), "wp_booking_appointments", { id: Number(id) }));
|
|
61203
|
+
booking.command("slots <treatments> <date>").description("free times; treatments as service[:option],\u2026 (ids)").option("--staff <id>").action(
|
|
61204
|
+
(t, date3, o2, c2) => run(g3(c2), "wp_booking_availability", { items: items3(t), date: date3, staff_id: o2.staff ? Number(o2.staff) : void 0 })
|
|
61205
|
+
);
|
|
61206
|
+
booking.command("book <treatments> <date> <time>").description("book for a customer (venue time)").option("--customer <id>").option("--name <name>").option("--email <email>").option("--phone <phone>").option("--staff <id>").option("--notes <text>").option("--payment <p>", "venue | deposit | full").option("--force", "even if the time is taken").action(
|
|
61207
|
+
(t, date3, time3, o2, c2) => run(g3(c2), "wp_booking_create", {
|
|
61208
|
+
items: items3(t),
|
|
61209
|
+
date: date3,
|
|
61210
|
+
time: time3,
|
|
61211
|
+
customer_id: o2.customer ? Number(o2.customer) : void 0,
|
|
61212
|
+
customer: o2.customer ? void 0 : { name: o2.name, email: o2.email, phone: o2.phone },
|
|
61213
|
+
staff_id: o2.staff ? Number(o2.staff) : void 0,
|
|
61214
|
+
notes: o2.notes,
|
|
61215
|
+
payment: o2.payment,
|
|
61216
|
+
force: !!o2.force
|
|
61217
|
+
})
|
|
61218
|
+
);
|
|
61219
|
+
booking.command("cancel <id>").option("--reason <text>").action(
|
|
61220
|
+
(id, o2, c2) => run(g3(c2), "wp_booking_change", { id: Number(id), action: "cancel", reason: o2.reason })
|
|
61221
|
+
);
|
|
61222
|
+
booking.command("move <id> <date> <time>").option("--staff <id>").option("--force").action(
|
|
61223
|
+
(id, date3, time3, o2, c2) => run(g3(c2), "wp_booking_change", {
|
|
61224
|
+
id: Number(id),
|
|
61225
|
+
action: "reschedule",
|
|
61226
|
+
date: date3,
|
|
61227
|
+
time: time3,
|
|
61228
|
+
staff_id: o2.staff ? Number(o2.staff) : void 0,
|
|
61229
|
+
force: !!o2.force
|
|
61230
|
+
})
|
|
61231
|
+
);
|
|
61232
|
+
booking.command("mark <id> <status>").description("arrived | completed | no_show | confirmed").action(
|
|
61233
|
+
(id, status, _o, c2) => run(g3(c2), "wp_booking_change", { id: Number(id), action: "status", status })
|
|
61234
|
+
);
|
|
61235
|
+
booking.command("refund <id> <payment> <amount>").description("amount in minor units (2500 = 25.00)").action(
|
|
61236
|
+
(id, payment, amount, _o, c2) => run(g3(c2), "wp_booking_change", {
|
|
61237
|
+
id: Number(id),
|
|
61238
|
+
action: "refund",
|
|
61239
|
+
payment_id: Number(payment),
|
|
61240
|
+
amount: Number(amount)
|
|
61241
|
+
})
|
|
61242
|
+
);
|
|
61243
|
+
booking.command("ls <resource>").description(
|
|
61244
|
+
"services, categories, staff, time-off, customers, vouchers, voucher-codes, promos, bundles, pricing, settings, imports \u2026"
|
|
61245
|
+
).option("--id <id>").option("--search <text>").action(
|
|
61246
|
+
(resource, o2, c2) => run(g3(c2), "wp_booking_list", { resource, id: o2.id ? Number(o2.id) : void 0, search: o2.search })
|
|
61247
|
+
);
|
|
61248
|
+
booking.command("save <resource> <file>").description("create (or with --id change) one item from a JSON file").option("--id <id>").action(
|
|
61249
|
+
async (resource, file, o2, c2) => run(g3(c2), "wp_booking_save", { resource, id: o2.id ? Number(o2.id) : void 0, data: await json(file) })
|
|
61250
|
+
);
|
|
61251
|
+
booking.command("rm <resource> <id>").action(
|
|
61252
|
+
(resource, id, _o, c2) => run(g3(c2), "wp_booking_save", { resource, id: Number(id), delete: true })
|
|
61253
|
+
);
|
|
61254
|
+
booking.command("settings <file>").description("JSON { settings?, location?, hours? }").action(async (file, _o, c2) => run(g3(c2), "wp_booking_settings_set", await json(file)));
|
|
61255
|
+
booking.command("import <source>").description(
|
|
61256
|
+
"a Fresha page URL, a catalogue .json, or a .csv (--kind clients|appointments|gift_cards); preview unless --apply"
|
|
61257
|
+
).option("--kind <kind>").option("--apply", "import (default: preview only)").action(async (src, o2, c2) => {
|
|
61258
|
+
const isUrl = /^https?:\/\//.test(src);
|
|
61259
|
+
const source = isUrl ? "fresha" : src.endsWith(".csv") ? "csv" : "json";
|
|
61260
|
+
const body = isUrl ? { url: src } : source === "csv" ? { csv: await readFile2(src, "utf8"), kind: o2.kind } : { json: await readFile2(src, "utf8") };
|
|
61261
|
+
return run(g3(c2), o2.apply ? "wp_booking_import" : "wp_booking_import_preview", { source, ...body });
|
|
61262
|
+
});
|
|
61263
|
+
booking.command("import-undo <id>").action((id, _o, c2) => run(g3(c2), "wp_booking_import", { undo_id: Number(id) }));
|
|
61264
|
+
booking.command("gift <action>").description("lookup --code | issue --voucher/--value --to-name --to-email | redeem --id --amount | void --id").option("--code <code>").option("--id <id>").option("--amount <minor>").option("--voucher <id>").option("--value <minor>").option("--to-name <name>").option("--to-email <email>").option("--from-name <name>").option("--from-email <email>").option("--message <text>").action(
|
|
61265
|
+
(action, o2, c2) => run(g3(c2), "wp_gift_card", {
|
|
61266
|
+
action,
|
|
61267
|
+
code: o2.code,
|
|
61268
|
+
id: o2.id ? Number(o2.id) : void 0,
|
|
61269
|
+
amount_minor: o2.amount ? Number(o2.amount) : void 0,
|
|
61270
|
+
voucher_id: o2.voucher ? Number(o2.voucher) : void 0,
|
|
61271
|
+
value_minor: o2.value ? Number(o2.value) : void 0,
|
|
61272
|
+
recipient_name: o2.toName,
|
|
61273
|
+
recipient_email: o2.toEmail,
|
|
61274
|
+
buyer_name: o2.fromName,
|
|
61275
|
+
buyer_email: o2.fromEmail,
|
|
61276
|
+
message: o2.message
|
|
61277
|
+
})
|
|
61278
|
+
);
|
|
60623
61279
|
wp.command("cron-rm <hook>").description("remove a scheduled job nothing runs any more (left by a removed plugin)").action((hook, _o, c2) => run(g3(c2), "wp_cron_unschedule", { hook }));
|
|
60624
61280
|
seo.command("get <page>").description("stored fields and what the page outputs").action((p2, _o, c2) => run(g3(c2), "wp_seo_get", { page: p2 }));
|
|
60625
61281
|
seo.command("set <page>").description("set a page's SEO fields").option("--title <t>").option("--description <d>").option("--keywords <k>").option("--robots <r>", '"index, follow" | "noindex, follow" | "noindex, nofollow"').option("--canonical <url>").option("--og-image <url>").option("--from <file>", "several pages: JSON { items: [{ page, title, description, \u2026 }] }").action(async (p2, o2, c2) => {
|
|
@@ -61028,11 +61684,11 @@ program2.command("login").description("sign in in your browser (device code)").o
|
|
|
61028
61684
|
act(async (opts) => {
|
|
61029
61685
|
if (process.env.CMO_API_KEY) say3("Note: CMO_API_KEY is set and is used instead of this sign-in.");
|
|
61030
61686
|
const res = await deviceLogin((s2) => {
|
|
61031
|
-
const
|
|
61032
|
-
say3(`Open ${
|
|
61687
|
+
const link2 = s2.verification_uri_complete ?? s2.verification_uri;
|
|
61688
|
+
say3(`Open ${link2}
|
|
61033
61689
|
and check the code: ${s2.user_code}
|
|
61034
61690
|
Waiting for you to approve\u2026`);
|
|
61035
|
-
if (opts.browser !== false) openUrl(
|
|
61691
|
+
if (opts.browser !== false) openUrl(link2);
|
|
61036
61692
|
});
|
|
61037
61693
|
const { session: s } = session();
|
|
61038
61694
|
await s.signIn(res);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.19",
|
|
4
4
|
"description": "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|