@cmo-so/mcp 0.1.0 → 0.3.0
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 +1 -1
- package/dist/cmo-mcp.mjs +1381 -221
- package/package.json +1 -1
package/dist/cmo-mcp.mjs
CHANGED
|
@@ -4622,7 +4622,7 @@ var require_core = __commonJS({
|
|
|
4622
4622
|
errorsText(errors = this.errors, { separator = ", ", dataVar = "data" } = {}) {
|
|
4623
4623
|
if (!errors || errors.length === 0)
|
|
4624
4624
|
return "No errors";
|
|
4625
|
-
return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((
|
|
4625
|
+
return errors.map((e) => `${dataVar}${e.instancePath} ${e.message}`).reduce((text3, msg) => text3 + separator + msg);
|
|
4626
4626
|
}
|
|
4627
4627
|
$dataMetaSchema(metaSchema, keywordsJsonPointers) {
|
|
4628
4628
|
const rules = this.RULES.all;
|
|
@@ -7199,7 +7199,7 @@ var require_dist = __commonJS({
|
|
|
7199
7199
|
// package.json
|
|
7200
7200
|
var package_default = {
|
|
7201
7201
|
name: "@cmo-so/cli",
|
|
7202
|
-
version: "0.
|
|
7202
|
+
version: "0.3.0",
|
|
7203
7203
|
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.",
|
|
7204
7204
|
type: "module",
|
|
7205
7205
|
license: "MIT",
|
|
@@ -7283,13 +7283,13 @@ var ApiError = class extends Error {
|
|
|
7283
7283
|
};
|
|
7284
7284
|
|
|
7285
7285
|
// ../../packages/api-client/src/api.ts
|
|
7286
|
-
function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.fetch }) {
|
|
7286
|
+
function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.fetch, streamFetch }) {
|
|
7287
7287
|
const base = baseUrl.replace(/\/+$/, "");
|
|
7288
|
-
async function send(path, init, token) {
|
|
7288
|
+
async function send(path, init, token, via = fetchImpl) {
|
|
7289
7289
|
const headers = new Headers(init.headers);
|
|
7290
7290
|
headers.set("Authorization", `Bearer ${token}`);
|
|
7291
7291
|
headers.set("Accept", "application/json");
|
|
7292
|
-
return
|
|
7292
|
+
return via(`${base}${path}`, { ...init, headers });
|
|
7293
7293
|
}
|
|
7294
7294
|
async function request(path, init = {}) {
|
|
7295
7295
|
let res = await send(path, init, await session2.getAccessToken());
|
|
@@ -7306,15 +7306,32 @@ function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.f
|
|
|
7306
7306
|
}
|
|
7307
7307
|
return data;
|
|
7308
7308
|
}
|
|
7309
|
-
async function
|
|
7309
|
+
async function requestBytes(path) {
|
|
7310
|
+
let res = await send(path, {}, await session2.getAccessToken());
|
|
7311
|
+
if (res.status === 401) {
|
|
7312
|
+
res = await send(path, {}, await session2.getAccessToken({ forceRefresh: true }));
|
|
7313
|
+
if (res.status === 401) {
|
|
7314
|
+
await session2.end();
|
|
7315
|
+
throw new SignInRequiredError();
|
|
7316
|
+
}
|
|
7317
|
+
}
|
|
7318
|
+
if (!res.ok) {
|
|
7319
|
+
const data = await res.json().catch(() => null);
|
|
7320
|
+
throw new ApiError(res.status, errorMessage(data, res.status), res.headers.get("X-Request-ID"));
|
|
7321
|
+
}
|
|
7322
|
+
return { type: res.headers.get("content-type") ?? "image/jpeg", bytes: await res.arrayBuffer() };
|
|
7323
|
+
}
|
|
7324
|
+
async function stream(path, body, onEvent, signal) {
|
|
7310
7325
|
const init = {
|
|
7311
7326
|
method: "POST",
|
|
7312
7327
|
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
7313
|
-
body: JSON.stringify(body)
|
|
7328
|
+
body: JSON.stringify(body),
|
|
7329
|
+
...signal ? { signal } : {}
|
|
7314
7330
|
};
|
|
7315
|
-
|
|
7331
|
+
const via = streamFetch ?? fetchImpl;
|
|
7332
|
+
let res = await send(path, init, await session2.getAccessToken(), via);
|
|
7316
7333
|
if (res.status === 401) {
|
|
7317
|
-
res = await send(path, init, await session2.getAccessToken({ forceRefresh: true }));
|
|
7334
|
+
res = await send(path, init, await session2.getAccessToken({ forceRefresh: true }), via);
|
|
7318
7335
|
if (res.status === 401) {
|
|
7319
7336
|
await session2.end();
|
|
7320
7337
|
throw new SignInRequiredError();
|
|
@@ -7384,8 +7401,87 @@ function createApi({ baseUrl, session: session2, fetch: fetchImpl = globalThis.f
|
|
|
7384
7401
|
},
|
|
7385
7402
|
// Ask Maggie (ADR-015): `history` is opaque; send back what `done` returned.
|
|
7386
7403
|
maggie: {
|
|
7387
|
-
chat: (body, onEvent) => stream("/v1/maggie/chat", body, onEvent),
|
|
7388
|
-
usage: (domain) => request(`/v1/maggie/usage${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`)
|
|
7404
|
+
chat: (body, onEvent, signal) => stream("/v1/maggie/chat", body, onEvent, signal),
|
|
7405
|
+
usage: (domain) => request(`/v1/maggie/usage${domain ? `?domain=${encodeURIComponent(domain)}` : ""}`),
|
|
7406
|
+
// The user's own Maggie Bridge (docs/20 route D): Maggie on their ChatGPT plan.
|
|
7407
|
+
bridge: {
|
|
7408
|
+
/** `live: false`: from the pairing alone, without waking the bridge. */
|
|
7409
|
+
status: (opts = {}) => request(`/v1/maggie/bridge${opts.live === false ? "?live=0" : ""}`),
|
|
7410
|
+
/** Pair in one step: CMO.so issues the bridge its own key with this access (read by default). */
|
|
7411
|
+
pair: (body) => request("/v1/maggie/bridge", {
|
|
7412
|
+
method: "POST",
|
|
7413
|
+
headers: { "Content-Type": "application/json" },
|
|
7414
|
+
body: JSON.stringify(body)
|
|
7415
|
+
}),
|
|
7416
|
+
/** Pair with CMO.so's own bridge in one step (accounts on its list); changes you approve by default. */
|
|
7417
|
+
connectHosted: (access) => request("/v1/maggie/bridge/hosted", {
|
|
7418
|
+
method: "POST",
|
|
7419
|
+
headers: { "Content-Type": "application/json" },
|
|
7420
|
+
body: JSON.stringify(access ? { access } : {})
|
|
7421
|
+
}),
|
|
7422
|
+
setEnabled: (enabled) => request("/v1/maggie/bridge/settings", {
|
|
7423
|
+
method: "POST",
|
|
7424
|
+
headers: { "Content-Type": "application/json" },
|
|
7425
|
+
body: JSON.stringify({ enabled })
|
|
7426
|
+
}),
|
|
7427
|
+
/** Pictures on ChatGPT (on) or Gemini only (off). */
|
|
7428
|
+
setImages: (images) => request("/v1/maggie/bridge/settings", {
|
|
7429
|
+
method: "POST",
|
|
7430
|
+
headers: { "Content-Type": "application/json" },
|
|
7431
|
+
body: JSON.stringify({ images })
|
|
7432
|
+
}),
|
|
7433
|
+
remove: () => request("/v1/maggie/bridge", { method: "DELETE" }),
|
|
7434
|
+
/** Start "Sign in with ChatGPT" for one of the bridge's accounts (the active one by default). */
|
|
7435
|
+
connectChatgpt: (account3) => request("/v1/maggie/bridge/chatgpt", {
|
|
7436
|
+
method: "POST",
|
|
7437
|
+
headers: { "Content-Type": "application/json" },
|
|
7438
|
+
body: JSON.stringify(account3 ? { account: account3 } : {})
|
|
7439
|
+
}),
|
|
7440
|
+
disconnectChatgpt: (account3) => request("/v1/maggie/bridge/chatgpt/logout", {
|
|
7441
|
+
method: "POST",
|
|
7442
|
+
headers: { "Content-Type": "application/json" },
|
|
7443
|
+
body: JSON.stringify(account3 ? { account: account3 } : {})
|
|
7444
|
+
}),
|
|
7445
|
+
/** What Maggie may do in CMO.so; rotates the bridge's key when read / read_write changes. */
|
|
7446
|
+
setAccess: (access) => request("/v1/maggie/bridge/access", {
|
|
7447
|
+
method: "POST",
|
|
7448
|
+
headers: { "Content-Type": "application/json" },
|
|
7449
|
+
body: JSON.stringify({ access })
|
|
7450
|
+
}),
|
|
7451
|
+
/** Which model answers and how hard it thinks; null for the default. */
|
|
7452
|
+
setModel: (body) => request("/v1/maggie/bridge/model", {
|
|
7453
|
+
method: "POST",
|
|
7454
|
+
headers: { "Content-Type": "application/json" },
|
|
7455
|
+
body: JSON.stringify(body)
|
|
7456
|
+
}),
|
|
7457
|
+
models: () => request("/v1/maggie/bridge/models"),
|
|
7458
|
+
/** Add a ChatGPT account to the bridge (then connect it). */
|
|
7459
|
+
addAccount: (label) => request("/v1/maggie/bridge/accounts", {
|
|
7460
|
+
method: "POST",
|
|
7461
|
+
headers: { "Content-Type": "application/json" },
|
|
7462
|
+
body: JSON.stringify({ label: label ?? null })
|
|
7463
|
+
}),
|
|
7464
|
+
/** Make one of the bridge's ChatGPT accounts the one that answers. */
|
|
7465
|
+
chooseAccount: (id) => request(`/v1/maggie/bridge/accounts/${encodeURIComponent(id)}/use`, { method: "POST" }),
|
|
7466
|
+
removeAccount: (id) => request(`/v1/maggie/bridge/accounts/${encodeURIComponent(id)}`, { method: "DELETE" })
|
|
7467
|
+
}
|
|
7468
|
+
},
|
|
7469
|
+
// Help Maggie grow (docs/22): api.cmo.so forwards these to feedback.cmo.so.
|
|
7470
|
+
feedback: {
|
|
7471
|
+
send: (body) => request("/v1/feedback", {
|
|
7472
|
+
method: "POST",
|
|
7473
|
+
headers: { "Content-Type": "application/json" },
|
|
7474
|
+
body: JSON.stringify(body)
|
|
7475
|
+
}),
|
|
7476
|
+
/** The user's own feedback, newest first, with the team's replies. */
|
|
7477
|
+
mine: () => request("/v1/feedback/mine"),
|
|
7478
|
+
/** The user has read the team's latest reply or status change. */
|
|
7479
|
+
seen: (id) => request(`/v1/feedback/${encodeURIComponent(id)}/seen`, { method: "POST" }),
|
|
7480
|
+
/** One of the user's feedback photos as a data: URL (an <Image> source on web and native). */
|
|
7481
|
+
photo: async (id, n) => {
|
|
7482
|
+
const { type, bytes } = await requestBytes(`/v1/feedback/${encodeURIComponent(id)}/photos/${n}`);
|
|
7483
|
+
return dataUrl(type, bytes);
|
|
7484
|
+
}
|
|
7389
7485
|
},
|
|
7390
7486
|
// Web Push for this browser (APP-9). `domains` are the workspaces it shows.
|
|
7391
7487
|
push: {
|
|
@@ -7416,8 +7512,25 @@ function errorMessage(data, status) {
|
|
|
7416
7512
|
}
|
|
7417
7513
|
return `Request failed (${status})`;
|
|
7418
7514
|
}
|
|
7515
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
7516
|
+
function dataUrl(type, bytes) {
|
|
7517
|
+
const b = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
7518
|
+
const parts = [];
|
|
7519
|
+
let chunk = "";
|
|
7520
|
+
for (let i = 0; i < b.length; i += 3) {
|
|
7521
|
+
const n = (b[i] ?? 0) << 16 | (b[i + 1] ?? 0) << 8 | (b[i + 2] ?? 0);
|
|
7522
|
+
chunk += B64.charAt(n >> 18 & 63) + B64.charAt(n >> 12 & 63) + (i + 1 < b.length ? B64.charAt(n >> 6 & 63) : "=") + (i + 2 < b.length ? B64.charAt(n & 63) : "=");
|
|
7523
|
+
if (chunk.length >= 8192) {
|
|
7524
|
+
parts.push(chunk);
|
|
7525
|
+
chunk = "";
|
|
7526
|
+
}
|
|
7527
|
+
}
|
|
7528
|
+
parts.push(chunk);
|
|
7529
|
+
return `data:${type.split(";")[0]?.trim() || "image/jpeg"};base64,${parts.join("")}`;
|
|
7530
|
+
}
|
|
7419
7531
|
|
|
7420
7532
|
// ../../packages/api-client/src/domains.ts
|
|
7533
|
+
var MEMORY_CHANNELS = ["general", "blog", "linkedin", "facebook", "instagram", "google_business"];
|
|
7421
7534
|
function addWebsite(api2, url) {
|
|
7422
7535
|
return api2.request("/v1/domains", {
|
|
7423
7536
|
method: "POST",
|
|
@@ -7501,6 +7614,20 @@ function domainApi(api2, domain) {
|
|
|
7501
7614
|
email: email2
|
|
7502
7615
|
}),
|
|
7503
7616
|
verifyEmailConfirm: (code) => post(`${base}/verify/email/confirm`, { code }),
|
|
7617
|
+
/**
|
|
7618
|
+
* The key to paste into the AI CMO WordPress plugin: once the plugin is connected on the website, it is
|
|
7619
|
+
* verified. Viewable until then (or until it expires); create:false only reads it, new:true replaces it.
|
|
7620
|
+
*/
|
|
7621
|
+
verifyWordpressKey: (opts = {}) => post(
|
|
7622
|
+
`${base}/verify/wordpress/key`,
|
|
7623
|
+
opts
|
|
7624
|
+
),
|
|
7625
|
+
/** The personalised WordPress setup guide: the website's own name / icon / colour, links into its wp-admin, plugin status. */
|
|
7626
|
+
setupGuide: () => api2.request(`${base}/setup-guide`),
|
|
7627
|
+
/** "Ask us": email the CMO.so team (with the account and website) to verify it by hand. */
|
|
7628
|
+
verifyRequest: (note = "") => post(`${base}/verify/request`, { note }),
|
|
7629
|
+
/** Check now: is the plugin connected on the website with that key? */
|
|
7630
|
+
verifyWordpressCheck: () => post(`${base}/verify/wordpress/check`),
|
|
7504
7631
|
/** The TXT record of the user's own claim (never makes a new one). */
|
|
7505
7632
|
dnsRecord: () => api2.request(`${base}/verify/dns`),
|
|
7506
7633
|
/** Look for the TXT record now; verifies the claim when it is there. */
|
|
@@ -7510,16 +7637,30 @@ function domainApi(api2, domain) {
|
|
|
7510
7637
|
// Writes (need autopilot write on the domain).
|
|
7511
7638
|
addIdea: (input) => post(`${base}/ideas`, input),
|
|
7512
7639
|
/** Ideas built on up to 10 photos (base64 JPEG). The server keeps no copy. */
|
|
7513
|
-
ideasFromPhotos: (photos, count,
|
|
7514
|
-
generateIdeas: (count,
|
|
7640
|
+
ideasFromPhotos: (photos, count, brief2 = "") => post(`${base}/ideas/from-photos`, { photos, count, brief: brief2 }),
|
|
7641
|
+
generateIdeas: (count, brief2 = "") => post(`${base}/ideas/generate`, { count, brief: brief2 }),
|
|
7515
7642
|
/** This website's sitemap, or `source` (a sitemap, site or page) on any website. */
|
|
7516
7643
|
importSitemap: (limit = 10, source) => post(`${base}/ideas/import-sitemap`, {
|
|
7517
7644
|
limit,
|
|
7518
7645
|
...source ? { sitemap_url: source } : {}
|
|
7519
7646
|
}),
|
|
7520
7647
|
discardIdea: (id, restore = false) => post(`${base}/ideas/${encodeURIComponent(id)}/discard${restore ? "?restore=true" : ""}`),
|
|
7648
|
+
/** OK a draft (or retry a failed post): it goes to the front of the queue; the post comes back with its eta. */
|
|
7521
7649
|
approveSocial: (id) => post(`${base}/social/${id}/approve`),
|
|
7522
7650
|
skipSocial: (id) => post(`${base}/social/${id}/skip`),
|
|
7651
|
+
/** Post it on LinkedIn now, not at the next posting time (even while posting is paused). */
|
|
7652
|
+
/** Post it now. `background`: the reply comes at once (status "publishing") and it goes out behind the
|
|
7653
|
+
* scenes (a notice when it's out); 409 `picture_busy` while its picture is being made. */
|
|
7654
|
+
publishSocialNow: (id, opts = {}) => post(
|
|
7655
|
+
`${base}/social/${id}/publish`,
|
|
7656
|
+
opts.background ? { background: true } : void 0
|
|
7657
|
+
),
|
|
7658
|
+
/** Take a posted LinkedIn post down (its likes and comments go with it). */
|
|
7659
|
+
/** How a posted Instagram post is doing (read from Instagram, kept 15 minutes; `refresh` reads again). */
|
|
7660
|
+
socialInsights: (id, refresh = false) => api2.request(`${base}/social/${id}/insights${refresh ? "?refresh=true" : ""}`),
|
|
7661
|
+
deletePostedSocial: (id) => api2.request(`${base}/social/${id}/published`, { method: "DELETE" }),
|
|
7662
|
+
/** Change a posted LinkedIn post and put it up again: the old one comes down, the new text goes out now. */
|
|
7663
|
+
republishSocial: (id, socialCopy) => post(`${base}/social/${id}/republish`, socialCopy ? { social_copy: socialCopy } : {}),
|
|
7523
7664
|
restoreSocial: (id) => post(`${base}/social/${id}/restore`),
|
|
7524
7665
|
pauseSocial: () => post(`${base}/social/pause`),
|
|
7525
7666
|
/**
|
|
@@ -7552,10 +7693,10 @@ function domainApi(api2, domain) {
|
|
|
7552
7693
|
/** The template library, searchable. */
|
|
7553
7694
|
socialTemplates: (search = "") => api2.request(`${base}/social/templates?search=${encodeURIComponent(search)}`),
|
|
7554
7695
|
socialTemplatePreview: (key) => api2.request(`${base}/social/templates/${encodeURIComponent(key)}/preview`),
|
|
7555
|
-
socialSetupTemplates: (keys2,
|
|
7696
|
+
socialSetupTemplates: (keys2, writing2) => api2.request(`${base}/social/setup/templates`, {
|
|
7556
7697
|
method: "PUT",
|
|
7557
7698
|
headers: { "Content-Type": "application/json" },
|
|
7558
|
-
body: JSON.stringify({ keys: keys2, writing })
|
|
7699
|
+
body: JSON.stringify({ keys: keys2, writing: writing2 })
|
|
7559
7700
|
}),
|
|
7560
7701
|
socialSetupSchedule: (schedule) => api2.request(`${base}/social/setup/schedule`, {
|
|
7561
7702
|
method: "PUT",
|
|
@@ -7565,16 +7706,174 @@ function domainApi(api2, domain) {
|
|
|
7565
7706
|
/** Maggie drafts the first LinkedIn post (1 credit): from a few lines, or one of your services. */
|
|
7566
7707
|
socialFirstPost: (input) => post(`${base}/social/setup/first-post`, input),
|
|
7567
7708
|
socialSetupDone: () => post(`${base}/social/setup/done`),
|
|
7709
|
+
// LinkedIn v2 (docs/prd/linkedin-journey-v2.md): Maggie's plan instead of the step-by-step setup.
|
|
7710
|
+
/** What is set up now, as a plan (no AI). */
|
|
7711
|
+
socialSetupPlan: () => api2.request(`${base}/social/setup/plan`),
|
|
7712
|
+
/** Maggie prepares what's missing (logo, look, voice, writing, frequency); `force` redoes it. Free. */
|
|
7713
|
+
socialSetupAuto: (timezone, force = false) => post(`${base}/social/setup/auto`, {
|
|
7714
|
+
...timezone ? { timezone } : {},
|
|
7715
|
+
...force ? { force: true } : {}
|
|
7716
|
+
}),
|
|
7717
|
+
socialSetupFrequency: (preset, timezone) => api2.request(`${base}/social/setup/frequency`, {
|
|
7718
|
+
method: "PUT",
|
|
7719
|
+
headers: { "Content-Type": "application/json" },
|
|
7720
|
+
body: JSON.stringify(timezone ? { preset, timezone } : { preset })
|
|
7721
|
+
}),
|
|
7722
|
+
/** "Looks good, start": setup done, and Maggie writes the first week's posts. */
|
|
7723
|
+
socialSetupConfirm: () => post(`${base}/social/setup/confirm`),
|
|
7724
|
+
/** OK every post waiting for you (not a series that hasn't been approved), on one channel or all. */
|
|
7725
|
+
approveAllSocial: (channel) => post(
|
|
7726
|
+
`${base}/social/approve-all${channel ? `?channel=${channel}` : ""}`
|
|
7727
|
+
),
|
|
7728
|
+
/** A Facebook sign-in link: after it the user lands on returnTo with ?facebook=connected (or =error). */
|
|
7729
|
+
connectFacebook: (returnTo = "close") => post(`${base}/social/facebook/connect`, { return_to: returnTo }),
|
|
7730
|
+
/** The Facebook Pages you connected, and the one this website posts to. */
|
|
7731
|
+
facebookPages: () => api2.request(`${base}/social/facebook/pages`),
|
|
7732
|
+
chooseFacebookPage: (connectionId) => api2.request(
|
|
7733
|
+
`${base}/social/facebook/page`,
|
|
7734
|
+
{
|
|
7735
|
+
method: "PUT",
|
|
7736
|
+
headers: { "Content-Type": "application/json" },
|
|
7737
|
+
body: JSON.stringify({ connection_id: connectionId })
|
|
7738
|
+
}
|
|
7739
|
+
),
|
|
7740
|
+
disconnectFacebook: (connectionId) => api2.request(`${base}/social/facebook/${connectionId}`, { method: "DELETE" }),
|
|
7741
|
+
/** Instagram connects through the Facebook sign-in (the accounts linked to your Pages): the same link;
|
|
7742
|
+
* back with ?facebook=connected, then `channels` says whether an Instagram account was found. */
|
|
7743
|
+
connectInstagram: (returnTo = "close") => post(`${base}/social/instagram/connect`, { return_to: returnTo }),
|
|
7744
|
+
/** Your Instagram accounts here (each with the Facebook Page it posts through), and the chosen one. */
|
|
7745
|
+
instagramAccounts: () => api2.request(`${base}/social/instagram/accounts`),
|
|
7746
|
+
chooseInstagramAccount: (connectionId) => api2.request(
|
|
7747
|
+
`${base}/social/instagram/account`,
|
|
7748
|
+
{
|
|
7749
|
+
method: "PUT",
|
|
7750
|
+
headers: { "Content-Type": "application/json" },
|
|
7751
|
+
body: JSON.stringify({ connection_id: connectionId })
|
|
7752
|
+
}
|
|
7753
|
+
),
|
|
7754
|
+
disconnectInstagram: (connectionId) => api2.request(`${base}/social/instagram/${connectionId}`, { method: "DELETE" }),
|
|
7755
|
+
/** A Google sign-in link for this website's Google Business Profile; back with ?google_business=connected
|
|
7756
|
+
* (or none / error&message=…). Every profile the account can post to is saved; this website's is picked. */
|
|
7757
|
+
connectGoogleBusiness: (returnTo = "close") => post(`${base}/social/google_business/connect`, {
|
|
7758
|
+
return_to: returnTo
|
|
7759
|
+
}),
|
|
7760
|
+
/** Your Google Business profiles here (name, address, website), and the one this website posts to. */
|
|
7761
|
+
googleBusinessLocations: () => api2.request(`${base}/social/google_business/locations`),
|
|
7762
|
+
chooseGoogleBusinessLocation: (connectionId) => put(
|
|
7763
|
+
`${base}/social/google_business/location`,
|
|
7764
|
+
{ connection_id: connectionId }
|
|
7765
|
+
),
|
|
7766
|
+
disconnectGoogleBusiness: (connectionId) => api2.request(`${base}/social/google_business/${connectionId}`, { method: "DELETE" }),
|
|
7767
|
+
/** One channel's own pause switch (pauseSocial / resumeSocial hold every channel). */
|
|
7768
|
+
setChannelPaused: (channel, paused) => post(`${base}/social/channels/${channel}/${paused ? "pause" : "resume"}`),
|
|
7769
|
+
/** How often one channel posts. */
|
|
7770
|
+
setChannelFrequency: (channel, preset) => api2.request(
|
|
7771
|
+
`${base}/social/channels/${channel}/frequency`,
|
|
7772
|
+
{ method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ preset }) }
|
|
7773
|
+
),
|
|
7774
|
+
/** Start a new picture for the post in the background (one AI generation); `replace` stops the one
|
|
7775
|
+
* already running; `instruction` (up to 300 characters) steers it: "gold border, our logo". */
|
|
7776
|
+
// `mode: "edit"` changes the post's current picture as `instruction` says. Another picture
|
|
7777
|
+
// being made here: this one waits its turn (status queued).
|
|
7778
|
+
// `count: 4` makes a set of 4 pictures from one storyboard (one AI generation), with words on each in
|
|
7779
|
+
// `textEffect` (an id from socialTextEffects; omit = Maggie picks).
|
|
7780
|
+
regenerateSocialImage: (id, opts = {}) => post(`${base}/social/${id}/image/regenerate`, {
|
|
7781
|
+
...opts.style ? { style: opts.style } : {},
|
|
7782
|
+
...opts.replace ? { replace: true } : {},
|
|
7783
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {},
|
|
7784
|
+
...opts.mode === "edit" ? { mode: "edit" } : {},
|
|
7785
|
+
...opts.count && opts.count > 1 ? { count: opts.count } : {},
|
|
7786
|
+
...opts.textEffect ? { text_effect: opts.textEffect } : {}
|
|
7787
|
+
}),
|
|
7788
|
+
/** Change a picture Maggie made without a new one (free): the words on each picture, its style, its text
|
|
7789
|
+
* style, the order of a set, or drop pictures from it. `words` follow the new order when `order` is given. */
|
|
7790
|
+
editSocialPicture: (id, changes) => api2.request(`${base}/social/${id}/picture`, {
|
|
7791
|
+
method: "PATCH",
|
|
7792
|
+
headers: { "Content-Type": "application/json" },
|
|
7793
|
+
body: JSON.stringify(changes)
|
|
7794
|
+
}),
|
|
7795
|
+
/** A title and a line on your own photo(s) in your look (Maggie plans the words; no AI picture, free). */
|
|
7796
|
+
wordsOnPhoto: (id, style = "real_photo") => post(`${base}/social/${id}/picture/words`, { style }),
|
|
7797
|
+
/** The brand's picture look (made the first time it's asked for). */
|
|
7798
|
+
socialLook: () => api2.request(`${base}/social/look`),
|
|
7799
|
+
updateSocialLook: (changes) => put(`${base}/social/look`, changes),
|
|
7800
|
+
/** Maggie looks again and suggests a new look (one AI generation). */
|
|
7801
|
+
suggestSocialLook: () => post(`${base}/social/look/suggest`, {}),
|
|
7802
|
+
/** A sample picture of a style in the brand's look: a data: URL. */
|
|
7803
|
+
socialLookSample: (style) => api2.request(`${base}/social/look/sample?style=${encodeURIComponent(style)}`),
|
|
7804
|
+
/** The text effects for the words on a set of pictures, each with a sample picture. */
|
|
7805
|
+
socialTextEffects: () => api2.request(`${base}/social/text-effects`),
|
|
7806
|
+
/** Your own photo (base64 JPEG) as the post's picture: no AI, no charge; only before it goes out.
|
|
7807
|
+
* With `instruction`, Maggie changes the photo as it says, in the background (one AI generation). */
|
|
7808
|
+
uploadSocialPhoto: (id, photo, opts = {}) => post(`${base}/social/${id}/image`, {
|
|
7809
|
+
photo,
|
|
7810
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {}
|
|
7811
|
+
}),
|
|
7812
|
+
/** A new picture for every post here without one (or these), one after another in the background. */
|
|
7813
|
+
fillSocialImages: (opts = {}) => post(
|
|
7814
|
+
`${base}/social/images/fill`,
|
|
7815
|
+
{
|
|
7816
|
+
...opts.item_ids ? { item_ids: opts.item_ids } : {},
|
|
7817
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {},
|
|
7818
|
+
...opts.count && opts.count > 1 ? { count: opts.count } : {},
|
|
7819
|
+
...opts.textEffect ? { text_effect: opts.textEffect } : {}
|
|
7820
|
+
}
|
|
7821
|
+
),
|
|
7822
|
+
/** Stop the picture being made (it's thrown away; the next in line starts). `all` empties the line too. */
|
|
7823
|
+
stopSocialImage: (all = false) => api2.request(`${base}/social/image-job${all ? "?all=true" : ""}`, {
|
|
7824
|
+
method: "DELETE"
|
|
7825
|
+
}),
|
|
7826
|
+
/** Maggie changes the post as asked, in 3-255 characters (one AI generation). */
|
|
7827
|
+
rewriteSocial: (id, instruction) => post(`${base}/social/${id}/rewrite`, { instruction }),
|
|
7828
|
+
/** Maggie plans and writes a series; it waits for one OK. */
|
|
7829
|
+
/** A series of `count` (1-7) posts on one theme; they are ordinary posts (Review), labelled with the series. */
|
|
7830
|
+
createSocialSeries: (input) => post(`${base}/social/series`, input),
|
|
7831
|
+
approveSocialSeries: (id) => post(`${base}/social/series/${id}/approve`),
|
|
7832
|
+
/** A post from photos (and a line about them): it waits for your OK like any other. */
|
|
7833
|
+
socialFromPhotos: (photos, brief2 = "", channels) => post(`${base}/social/from-photos`, {
|
|
7834
|
+
photos,
|
|
7835
|
+
brief: brief2,
|
|
7836
|
+
...channels ? { channels } : {}
|
|
7837
|
+
}),
|
|
7838
|
+
// In the background: the answer is the job at once; watch `write_jobs` in GET /social.
|
|
7839
|
+
startSocialFromPhotos: (photos, brief2 = "", channels) => post(`${base}/social/from-photos`, {
|
|
7840
|
+
photos,
|
|
7841
|
+
brief: brief2,
|
|
7842
|
+
background: true,
|
|
7843
|
+
...channels ? { channels } : {}
|
|
7844
|
+
}),
|
|
7845
|
+
startSocialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
|
|
7846
|
+
source_post_id: sourcePostId,
|
|
7847
|
+
background: true,
|
|
7848
|
+
...channels ? { channels } : {}
|
|
7849
|
+
}),
|
|
7850
|
+
startSocialPlan: (days, timezone, channels) => post(`${base}/social/plan`, {
|
|
7851
|
+
days,
|
|
7852
|
+
timezone,
|
|
7853
|
+
background: true,
|
|
7854
|
+
...channels ? { channels } : {}
|
|
7855
|
+
}),
|
|
7856
|
+
startWriteSocialIdea: (id, channels) => post(`${base}/social/ideas/${id}/write`, {
|
|
7857
|
+
background: true,
|
|
7858
|
+
...channels ? { channels } : {}
|
|
7859
|
+
}),
|
|
7568
7860
|
/** Disconnect the website's LinkedIn account. */
|
|
7569
7861
|
disconnectLinkedIn: (connectionId) => api2.request(`${base}/social/linkedin/${connectionId}`, { method: "DELETE" }),
|
|
7570
7862
|
/** Published articles a LinkedIn post can be made from. */
|
|
7571
7863
|
socialSources: (search = "") => api2.request(
|
|
7572
7864
|
`${base}/social/sources?search=${encodeURIComponent(search)}`
|
|
7573
7865
|
),
|
|
7574
|
-
/** Maggie writes a
|
|
7575
|
-
socialFromPost: (sourcePostId) => post(`${base}/social/from-post`, {
|
|
7866
|
+
/** Maggie writes a post about one article for each channel; `failed` lists a channel that could not be written while the others were. */
|
|
7867
|
+
socialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
|
|
7868
|
+
source_post_id: sourcePostId,
|
|
7869
|
+
...channels ? { channels } : {}
|
|
7870
|
+
}),
|
|
7576
7871
|
/** Drafts for the next 1-7 days from the best recent articles. */
|
|
7577
|
-
socialPlan: (days, timezone) => post(`${base}/social/plan`, {
|
|
7872
|
+
socialPlan: (days, timezone, channels) => post(`${base}/social/plan`, {
|
|
7873
|
+
days,
|
|
7874
|
+
timezone,
|
|
7875
|
+
...channels ? { channels } : {}
|
|
7876
|
+
}),
|
|
7578
7877
|
/** Campaign ideas waiting to be written (this person's, newest campaign first). */
|
|
7579
7878
|
socialIdeas: (page = 1, perPage = 20) => api2.request(
|
|
7580
7879
|
`${base}/social/ideas?page=${page}&per_page=${perPage}`
|
|
@@ -7594,14 +7893,41 @@ function domainApi(api2, domain) {
|
|
|
7594
7893
|
`${base}/social/campaigns/${id}/start`,
|
|
7595
7894
|
options
|
|
7596
7895
|
),
|
|
7597
|
-
/**
|
|
7598
|
-
|
|
7896
|
+
/**
|
|
7897
|
+
* Maggie writes the post for this idea (1 credit); it waits in Upcoming.
|
|
7898
|
+
* With `written` ({ linkedin: { body } }, from socialIdeaBrief), the user's own
|
|
7899
|
+
* assistant wrote it: saved through the same checks, for the same credit.
|
|
7900
|
+
*/
|
|
7901
|
+
writeSocialIdea: (id, written, channels) => post(
|
|
7902
|
+
`${base}/social/ideas/${id}/write`,
|
|
7903
|
+
written || channels ? { ...written ? { written } : {}, ...channels ? { channels } : {} } : void 0
|
|
7904
|
+
),
|
|
7905
|
+
/** What Maggie would write this idea's post from: the prompt, JSON shape and length limits (free). */
|
|
7906
|
+
socialIdeaBrief: (id) => api2.request(`${base}/social/ideas/${id}/brief`),
|
|
7599
7907
|
/** Change a LinkedIn post's text before it goes out. */
|
|
7600
7908
|
updateSocial: (id, socialCopy) => api2.request(`${base}/social/${id}`, {
|
|
7601
7909
|
method: "PATCH",
|
|
7602
7910
|
headers: { "Content-Type": "application/json" },
|
|
7603
7911
|
body: JSON.stringify({ social_copy: socialCopy })
|
|
7604
7912
|
}),
|
|
7913
|
+
/** The post's link: it replaces the old one in the text, and the preview card shows the new page. */
|
|
7914
|
+
setSocialLink: (id, link) => api2.request(`${base}/social/${id}`, {
|
|
7915
|
+
method: "PATCH",
|
|
7916
|
+
headers: { "Content-Type": "application/json" },
|
|
7917
|
+
body: JSON.stringify({ link })
|
|
7918
|
+
}),
|
|
7919
|
+
/** Go out as the post's image, or as a link preview. */
|
|
7920
|
+
setSocialPublishMode: (id, mode) => api2.request(`${base}/social/${id}`, {
|
|
7921
|
+
method: "PATCH",
|
|
7922
|
+
headers: { "Content-Type": "application/json" },
|
|
7923
|
+
body: JSON.stringify({ publish_mode: mode })
|
|
7924
|
+
}),
|
|
7925
|
+
/** A Google Business post's button (LEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, CALL); "" = Learn more. */
|
|
7926
|
+
setSocialButton: (id, ctaType) => api2.request(`${base}/social/${id}`, {
|
|
7927
|
+
method: "PATCH",
|
|
7928
|
+
headers: { "Content-Type": "application/json" },
|
|
7929
|
+
body: JSON.stringify({ cta_type: ctaType })
|
|
7930
|
+
}),
|
|
7605
7931
|
resumeSocial: () => post(`${base}/social/resume`),
|
|
7606
7932
|
pauseAutopilot: () => post(`${base}/autopilot/pause`),
|
|
7607
7933
|
resumeAutopilot: () => post(`${base}/autopilot/resume`),
|
|
@@ -7609,6 +7935,9 @@ function domainApi(api2, domain) {
|
|
|
7609
7935
|
visibility: () => api2.request(`${base}/visibility`),
|
|
7610
7936
|
searchConsole: (period2 = "30d") => api2.request(`${base}/search-console?period=${period2}`),
|
|
7611
7937
|
searchConsoleQueries: (period2 = "30d", filter = "top", limit = 20) => api2.request(`${base}/search-console/queries?period=${period2}&filter=${filter}&limit=${limit}`),
|
|
7938
|
+
/** A Google sign-in link that connects Search Console for this website (open it in a
|
|
7939
|
+
* browser; back at returnTo with ?gsc=connected or ?gsc=error&message=…). 10 minutes. */
|
|
7940
|
+
connectSearchConsole: (returnTo = "close") => post(`${base}/search-console/connect`, { return_to: returnTo }),
|
|
7612
7941
|
/** Fresh numbers from Google now; at most once an hour per website. */
|
|
7613
7942
|
syncSearchConsole: () => post(`${base}/search-console/sync`),
|
|
7614
7943
|
contentPerformance: (sort = "clicks", band, limit = 20) => api2.request(
|
|
@@ -7631,6 +7960,8 @@ function domainApi(api2, domain) {
|
|
|
7631
7960
|
planCampaign: (id) => post(`${base}/social/campaigns/${id}/plan`),
|
|
7632
7961
|
pauseCampaign: (id) => post(`${base}/social/campaigns/${id}/pause`),
|
|
7633
7962
|
resumeCampaign: (id) => post(`${base}/social/campaigns/${id}/resume`),
|
|
7963
|
+
/** End a series: its posts that haven't gone out are dropped; published ones stay on LinkedIn. */
|
|
7964
|
+
endCampaign: (id) => post(`${base}/social/campaigns/${id}/end`),
|
|
7634
7965
|
setApprovalMode: (mode) => api2.request(`${base}/social/approval-mode`, {
|
|
7635
7966
|
method: "PUT",
|
|
7636
7967
|
headers: { "Content-Type": "application/json" },
|
|
@@ -7647,6 +7978,22 @@ function domainApi(api2, domain) {
|
|
|
7647
7978
|
}),
|
|
7648
7979
|
/** What Maggie changed for you on this website, newest first (docs/18 MT-SEC-6). */
|
|
7649
7980
|
maggieActions: (limit = 30) => api2.request(`${base}/maggie/actions?limit=${limit}`),
|
|
7981
|
+
// Maggie Memory (docs/21): the user's own memory on this website.
|
|
7982
|
+
maggieMemory: () => api2.request(`${base}/maggie/memory`),
|
|
7983
|
+
maggieMemorySettings: (s) => put(`${base}/maggie/memory/settings`, s),
|
|
7984
|
+
addMaggieMemory: (item2) => post(`${base}/maggie/memory/items`, item2),
|
|
7985
|
+
updateMaggieMemory: (id, patch) => api2.request(`${base}/maggie/memory/items/${encodeURIComponent(id)}`, {
|
|
7986
|
+
method: "PATCH",
|
|
7987
|
+
headers: { "Content-Type": "application/json" },
|
|
7988
|
+
body: JSON.stringify(patch)
|
|
7989
|
+
}),
|
|
7990
|
+
deleteMaggieMemory: (id) => api2.request(`${base}/maggie/memory/items/${encodeURIComponent(id)}`, { method: "DELETE" }),
|
|
7991
|
+
/** Learn from recent activity now; `full` rebuilds the learned items from scratch. */
|
|
7992
|
+
learnMaggieMemory: (o = {}) => post(`${base}/maggie/memory/learn`, o),
|
|
7993
|
+
/** Forget what was learned (one channel, or all); the user's own items stay. */
|
|
7994
|
+
clearMaggieMemory: (channel) => api2.request(`${base}/maggie/memory${channel ? `?channel=${encodeURIComponent(channel)}` : ""}`, {
|
|
7995
|
+
method: "DELETE"
|
|
7996
|
+
}),
|
|
7650
7997
|
// docs/19 P3: autopilot cadence and publish-now.
|
|
7651
7998
|
autopilot: () => api2.request(`${base}/autopilot`),
|
|
7652
7999
|
/** Posts per day for pushed blogs (one of `choices`, up to the plan's limit). */
|
|
@@ -7810,6 +8157,11 @@ function createSession({ oauth: oauth2, store, skewMs = 6e4, now = Date.now, onS
|
|
|
7810
8157
|
|
|
7811
8158
|
// ../../packages/domain/src/copy/maggie.ts
|
|
7812
8159
|
var q = (s) => `\u201C${String(s ?? "").slice(0, 120)}\u201D`;
|
|
8160
|
+
var named = (title, id) => {
|
|
8161
|
+
if (title != null && String(title).trim()) return ` ${q(title)}`;
|
|
8162
|
+
const s = String(id ?? "").trim();
|
|
8163
|
+
return s && s.length <= 8 ? ` #${s}` : "";
|
|
8164
|
+
};
|
|
7813
8165
|
var list = (xs, n = 5) => {
|
|
7814
8166
|
const a = (Array.isArray(xs) ? xs : xs == null ? [] : [xs]).map(String);
|
|
7815
8167
|
return a.length > n ? `${a.slice(0, n).join(", ")} +${a.length - n}` : a.join(", ");
|
|
@@ -7892,41 +8244,45 @@ var confirmCopy = {
|
|
|
7892
8244
|
"zh-Hant": (a) => `\u70BA ${list(a.target_audience)} \u5EFA\u7ACB\u54C1\u724C\u8A9E\u6C23 ${q(a.name ?? "\u65B0\u8A9E\u6C23")}\u3002`
|
|
7893
8245
|
},
|
|
7894
8246
|
brand_voice_update_style: {
|
|
7895
|
-
en: (a) => `Change how the brand voice
|
|
7896
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u54C1\u724C\u8A9E\u6C23
|
|
8247
|
+
en: (a) => `Change how the brand voice${named(a.name, a.id)} writes: ${keys(a).filter((k) => k !== "id" && k !== "name").join(", ")}.`,
|
|
8248
|
+
"zh-Hant": (a) => `\u4FEE\u6539\u54C1\u724C\u8A9E\u6C23${named(a.name, a.id)} \u7684\u5BEB\u6CD5\uFF1A${keys(a).filter((k) => k !== "id" && k !== "name").join("\u3001")}\u3002`
|
|
7897
8249
|
},
|
|
7898
8250
|
brand_voice_set_default: {
|
|
7899
|
-
en: (a) => `Make
|
|
7900
|
-
"zh-Hant": (a) => `\u628A
|
|
8251
|
+
en: (a) => `Make${named(a.name, a.id)} your default brand voice.`,
|
|
8252
|
+
"zh-Hant": (a) => `\u628A${named(a.name, a.id)} \u8A2D\u70BA\u9810\u8A2D\u54C1\u724C\u8A9E\u6C23\u3002`
|
|
7901
8253
|
},
|
|
7902
8254
|
brand_voice_delete: {
|
|
7903
|
-
en: (a) => `Delete the brand voice
|
|
7904
|
-
"zh-Hant": (a) => `\u522A\u9664\u54C1\u724C\u8A9E\u6C23
|
|
8255
|
+
en: (a) => `Delete the brand voice${named(a.name, a.id)}. This can't be undone.`,
|
|
8256
|
+
"zh-Hant": (a) => `\u522A\u9664\u54C1\u724C\u8A9E\u6C23${named(a.name, a.id)}\uFF0C\u522A\u9664\u5F8C\u7121\u6CD5\u6062\u5FA9\u3002`
|
|
7905
8257
|
},
|
|
7906
8258
|
// ------------------------------------------------------------------- blog
|
|
7907
8259
|
draft_update: {
|
|
7908
|
-
en: (a) => `Change the draft
|
|
7909
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u8349\u7A3F
|
|
8260
|
+
en: (a) => `Change the draft${named(a.title_for_card, a.id)}: ${keys(a).filter((k) => !["id", "title_for_card"].includes(k)).join(", ")}.`,
|
|
8261
|
+
"zh-Hant": (a) => `\u4FEE\u6539\u8349\u7A3F${named(a.title_for_card, a.id)}\uFF1A${keys(a).filter((k) => !["id", "title_for_card"].includes(k)).join("\u3001")}\u3002`
|
|
7910
8262
|
},
|
|
7911
8263
|
draft_rewrite: {
|
|
7912
|
-
en: (a) => a.mode === "fix" ? `Let Maggie fix the draft
|
|
7913
|
-
"zh-Hant": (a) => a.mode === "fix" ? `\u8B93 Maggie \u4FEE\u6B63\u8349\u7A3F
|
|
8264
|
+
en: (a) => a.mode === "fix" ? `Let Maggie fix the draft${named(a.title, a.id)} and save it.` : `Let Maggie change the draft${named(a.title, a.id)}: ${a.instruction}. The new text is saved.`,
|
|
8265
|
+
"zh-Hant": (a) => a.mode === "fix" ? `\u8B93 Maggie \u4FEE\u6B63\u8349\u7A3F${named(a.title, a.id)} \u4E26\u5132\u5B58\u3002` : `\u8B93 Maggie \u4FEE\u6539\u8349\u7A3F${named(a.title, a.id)}\uFF1A${a.instruction}\u3002\u65B0\u5167\u5BB9\u6703\u88AB\u5132\u5B58\u3002`
|
|
8266
|
+
},
|
|
8267
|
+
draft_rewrite_save: {
|
|
8268
|
+
en: (a) => `Save your ${a.mode === "fix" ? "fixed" : "edited"} version of the draft${named(a.title, a.id)} (${String(a.markdown ?? "").length} characters). It replaces the current text.`,
|
|
8269
|
+
"zh-Hant": (a) => `\u5132\u5B58\u4F60${a.mode === "fix" ? "\u4FEE\u6B63" : "\u4FEE\u6539"}\u5F8C\u7684\u8349\u7A3F${named(a.title, a.id)}\uFF08${String(a.markdown ?? "").length} \u5B57\u5143\uFF09\uFF0C\u53D6\u4EE3\u76EE\u524D\u5167\u5BB9\u3002`
|
|
7914
8270
|
},
|
|
7915
8271
|
draft_delete: {
|
|
7916
|
-
en: (a) => `Delete the draft
|
|
7917
|
-
"zh-Hant": (a) => `\u522A\u9664\u8349\u7A3F
|
|
8272
|
+
en: (a) => `Delete the draft${named(a.title, a.id)}. Its idea is dropped too and the credit is not refunded.`,
|
|
8273
|
+
"zh-Hant": (a) => `\u522A\u9664\u8349\u7A3F${named(a.title, a.id)}\uFF0C\u5C0D\u61C9\u7684 idea \u4E5F\u6703\u4E1F\u68C4\uFF0C\u984D\u5EA6\u4E0D\u9000\u3002`
|
|
7918
8274
|
},
|
|
7919
8275
|
plan_move: {
|
|
7920
|
-
en: (a) => `Move
|
|
7921
|
-
"zh-Hant": (a) => `\u628A
|
|
8276
|
+
en: (a) => `Move${named(a.title, a.id)} ${a.to === "top" ? "to the top of" : a.to} the plan.`,
|
|
8277
|
+
"zh-Hant": (a) => `\u628A${named(a.title, a.id)} ${a.to === "top" ? "\u79FB\u5230\u8A08\u5283\u6700\u524D" : a.to === "up" ? "\u5728\u8A08\u5283\u88E1\u4E0A\u79FB" : "\u5728\u8A08\u5283\u88E1\u4E0B\u79FB"}\u3002`
|
|
7922
8278
|
},
|
|
7923
8279
|
plan_update: {
|
|
7924
|
-
en: (a) => `Change the planned post
|
|
7925
|
-
"zh-Hant": (a) => `\u4FEE\u6539\u8A08\u5283\u4E2D\u7684\u6587\u7AE0
|
|
8280
|
+
en: (a) => `Change the planned post${named(a.title_for_card, a.id)}.`,
|
|
8281
|
+
"zh-Hant": (a) => `\u4FEE\u6539\u8A08\u5283\u4E2D\u7684\u6587\u7AE0${named(a.title_for_card, a.id)}\u3002`
|
|
7926
8282
|
},
|
|
7927
8283
|
plan_remove: {
|
|
7928
|
-
en: (a) => `Take
|
|
7929
|
-
"zh-Hant": (a) => `\u628A
|
|
8284
|
+
en: (a) => `Take${named(a.title, a.id)} out of the plan (its idea can be restored).`,
|
|
8285
|
+
"zh-Hant": (a) => `\u628A${named(a.title, a.id)} \u79FB\u51FA\u8A08\u5283\uFF08idea \u53EF\u4EE5\u6062\u5FA9\uFF09\u3002`
|
|
7930
8286
|
},
|
|
7931
8287
|
idea_add: { en: (a) => `Add the idea ${q(a.url || a.text)}.`, "zh-Hant": (a) => `\u52A0\u5165 idea ${q(a.url || a.text)}\u3002` },
|
|
7932
8288
|
ideas_generate: {
|
|
@@ -7938,24 +8294,24 @@ var confirmCopy = {
|
|
|
7938
8294
|
"zh-Hant": (a) => `\u5F9E ${a.sitemap_url || "\u4F60\u7684 sitemap"} \u5C0E\u5165\u6700\u591A ${a.limit || 10} \u500B idea\u3002`
|
|
7939
8295
|
},
|
|
7940
8296
|
idea_update: {
|
|
7941
|
-
en: (a) => `Change the idea
|
|
7942
|
-
"zh-Hant": (a) => `\u4FEE\u6539 idea
|
|
8297
|
+
en: (a) => `Change the idea${named(a.title_for_card, a.id)}.`,
|
|
8298
|
+
"zh-Hant": (a) => `\u4FEE\u6539 idea${named(a.title_for_card, a.id)}\u3002`
|
|
7943
8299
|
},
|
|
7944
8300
|
idea_priority: {
|
|
7945
|
-
en: (a) => `${Number(a.delta) > 0 ? "Raise" : "Lower"} the priority of
|
|
7946
|
-
"zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}
|
|
8301
|
+
en: (a) => `${Number(a.delta) > 0 ? "Raise" : "Lower"} the priority of${named(a.title, a.id)}.`,
|
|
8302
|
+
"zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}${named(a.title, a.id)} \u7684\u512A\u5148\u7D1A\u3002`
|
|
7947
8303
|
},
|
|
7948
8304
|
idea_plan: {
|
|
7949
|
-
en: (a) => `Add
|
|
7950
|
-
"zh-Hant": (a) => `\u628A
|
|
8305
|
+
en: (a) => `Add${named(a.title, a.id)} to the end of the plan.`,
|
|
8306
|
+
"zh-Hant": (a) => `\u628A${named(a.title, a.id)} \u52A0\u5230\u8A08\u5283\u6700\u5F8C\u3002`
|
|
7951
8307
|
},
|
|
7952
8308
|
idea_discard: {
|
|
7953
|
-
en: (a) => `Remove the idea
|
|
7954
|
-
"zh-Hant": (a) => `\u522A\u9664 idea
|
|
8309
|
+
en: (a) => `Remove the idea${named(a.title, a.id)}.`,
|
|
8310
|
+
"zh-Hant": (a) => `\u522A\u9664 idea${named(a.title, a.id)}\u3002`
|
|
7955
8311
|
},
|
|
7956
8312
|
idea_restore: {
|
|
7957
|
-
en: (a) => `Bring back the idea
|
|
7958
|
-
"zh-Hant": (a) => `\u6062\u5FA9 idea
|
|
8313
|
+
en: (a) => `Bring back the idea${named(a.title, a.id)}.`,
|
|
8314
|
+
"zh-Hant": (a) => `\u6062\u5FA9 idea${named(a.title, a.id)}\u3002`
|
|
7959
8315
|
},
|
|
7960
8316
|
autopilot_set_paused: {
|
|
7961
8317
|
en: (a) => a.paused ? "Pause publishing blog posts." : "Resume publishing blog posts.",
|
|
@@ -7966,8 +8322,8 @@ var confirmCopy = {
|
|
|
7966
8322
|
"zh-Hant": (a) => `\u6BCF\u5929\u767C\u4F48 ${a.posts_per_day} \u7BC7\u90E8\u843D\u683C\u6587\u7AE0\u3002`
|
|
7967
8323
|
},
|
|
7968
8324
|
post_publish_now: {
|
|
7969
|
-
en: (a) => `Publish
|
|
7970
|
-
"zh-Hant": (a) => `\u7ACB\u5373\u628A
|
|
8325
|
+
en: (a) => `Publish${named(a.title, a.id)} to your website now. Everyone can see it once it's out.`,
|
|
8326
|
+
"zh-Hant": (a) => `\u7ACB\u5373\u628A${named(a.title, a.id)} \u767C\u4F48\u5230\u4F60\u7684\u7DB2\u7AD9\uFF0C\u767C\u51FA\u5F8C\u6240\u6709\u4EBA\u90FD\u770B\u5F97\u5230\u3002`
|
|
7971
8327
|
},
|
|
7972
8328
|
rewrite_request: {
|
|
7973
8329
|
en: (a) => `Rewrite the live article ${a.url}${a.instructions ? ` (${String(a.instructions).slice(0, 80)})` : ""}. The published page will change.`,
|
|
@@ -7981,11 +8337,11 @@ var confirmCopy = {
|
|
|
7981
8337
|
social_approve: {
|
|
7982
8338
|
en: (a) => {
|
|
7983
8339
|
const n = ids(a);
|
|
7984
|
-
return n.length > 1 ? `Approve ${n.length} LinkedIn posts: ${n.map((i) => `#${i}`).join(", ")}. Each goes
|
|
8340
|
+
return n.length > 1 ? `Approve ${n.length} LinkedIn posts: ${n.map((i) => `#${i}`).join(", ")}. Each goes out at your next posting times, where your network can see it.` : `Approve LinkedIn post #${n[0]}. It goes out at your next posting time, where your network can see it.`;
|
|
7985
8341
|
},
|
|
7986
8342
|
"zh-Hant": (a) => {
|
|
7987
8343
|
const n = ids(a);
|
|
7988
|
-
return n.length > 1 ? `\u6279\u51C6 ${n.length} \u7BC7 LinkedIn \u8CBC\u6587\uFF1A${n.map((i) => `#${i}`).join("\u3001")}\u3002\u6BCF\u7BC7\
|
|
8344
|
+
return n.length > 1 ? `\u6279\u51C6 ${n.length} \u7BC7 LinkedIn \u8CBC\u6587\uFF1A${n.map((i) => `#${i}`).join("\u3001")}\u3002\u6BCF\u7BC7\u5728\u63A5\u4E0B\u4F86\u7684\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002` : `\u6279\u51C6 LinkedIn \u8CBC\u6587 #${n[0]}\u3002\u5B83\u6703\u5728\u4E0B\u4E00\u500B\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002`;
|
|
7989
8345
|
}
|
|
7990
8346
|
},
|
|
7991
8347
|
social_skip: { en: (a) => `Skip LinkedIn post #${a.id}.`, "zh-Hant": (a) => `\u8DF3\u904E LinkedIn \u8CBC\u6587 #${a.id}\u3002` },
|
|
@@ -7993,21 +8349,29 @@ var confirmCopy = {
|
|
|
7993
8349
|
en: (a) => `Bring back LinkedIn post #${a.id}.`,
|
|
7994
8350
|
"zh-Hant": (a) => `\u6062\u5FA9 LinkedIn \u8CBC\u6587 #${a.id}\u3002`
|
|
7995
8351
|
},
|
|
8352
|
+
social_image_new: {
|
|
8353
|
+
en: (a) => `Make a new picture for LinkedIn post #${a.id}${a.instruction ? `: ${String(a.instruction).slice(0, 300)}` : ""} (1 AI generation, in the background)${a.replace ? ", stopping the one being made for another post" : ""}.`,
|
|
8354
|
+
"zh-Hant": (a) => `\u70BA LinkedIn \u8CBC\u6587 #${a.id} \u751F\u6210\u65B0\u5716\u7247${a.instruction ? `\uFF1A${String(a.instruction).slice(0, 300)}` : ""}\uFF08\u7528 1 \u6B21 AI \u751F\u6210\uFF0C\u5728\u80CC\u666F\u9032\u884C\uFF09${a.replace ? "\uFF0C\u4E26\u505C\u6B62\u6B63\u5728\u70BA\u53E6\u4E00\u7BC7\u751F\u6210\u7684\u5716\u7247" : ""}\u3002`
|
|
8355
|
+
},
|
|
8356
|
+
social_image_stop: {
|
|
8357
|
+
en: () => "Stop the LinkedIn picture being made; the post keeps the one it had.",
|
|
8358
|
+
"zh-Hant": () => "\u505C\u6B62\u6B63\u5728\u751F\u6210\u7684 LinkedIn \u5716\u7247\uFF1B\u8CBC\u6587\u4FDD\u7559\u539F\u4F86\u7684\u5716\u7247\u3002"
|
|
8359
|
+
},
|
|
7996
8360
|
social_move: {
|
|
7997
|
-
en: (a) => `Move LinkedIn post #${a.id} ${a.to === "top" ? "to the
|
|
7998
|
-
"zh-Hant": (a) => `\u628A LinkedIn \u8CBC\u6587 #${a.id} ${a.to === "top" ? "\u79FB\u5230\
|
|
8361
|
+
en: (a) => `Move LinkedIn post #${a.id} ${a.to === "top" ? "to the top of" : a.to} Upcoming.`,
|
|
8362
|
+
"zh-Hant": (a) => `\u628A LinkedIn \u8CBC\u6587 #${a.id} ${a.to === "top" ? "\u79FB\u5230\u5373\u5C07\u767C\u51FA\u7684\u6700\u524D" : a.to === "up" ? "\u4E0A\u79FB" : "\u4E0B\u79FB"}\u3002`
|
|
7999
8363
|
},
|
|
8000
8364
|
social_set_paused: {
|
|
8001
8365
|
en: (a) => a.paused ? "Pause all LinkedIn posting." : "Resume LinkedIn posting.",
|
|
8002
8366
|
"zh-Hant": (a) => a.paused ? "\u66AB\u505C\u6240\u6709 LinkedIn \u767C\u6587\u3002" : "\u6062\u5FA9 LinkedIn \u767C\u6587\u3002"
|
|
8003
8367
|
},
|
|
8004
8368
|
social_from_article: {
|
|
8005
|
-
en: (a) => `Write a LinkedIn post about ${q(a.title || a.source_id)}; it
|
|
8006
|
-
"zh-Hant": (a) => `\u6839\u64DA ${q(a.title || a.source_id)} \u5BEB\u4E00\u7BC7 LinkedIn \u8CBC\u6587\uFF0C\
|
|
8369
|
+
en: (a) => `Write a LinkedIn post about ${q(a.title || a.source_id)}; it waits for your OK unless your LinkedIn posts go out on their own.`,
|
|
8370
|
+
"zh-Hant": (a) => `\u6839\u64DA ${q(a.title || a.source_id)} \u5BEB\u4E00\u7BC7 LinkedIn \u8CBC\u6587\uFF1B\u9664\u975E\u4F60\u8A2D\u5B9A\u8CBC\u6587\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6\u3002`
|
|
8007
8371
|
},
|
|
8008
8372
|
social_plan_days: {
|
|
8009
|
-
en: (a) => `Write LinkedIn posts for the next ${a.days} day${Number(a.days) === 1 ? "" : "s"} from your best articles.`,
|
|
8010
|
-
"zh-Hant": (a) => `\u7528\u4F60\u6700\u597D\u7684\u6587\u7AE0\u5BEB\u672A\u4F86 ${a.days} \u5929\u7684 LinkedIn \u8CBC\u6587\u3002`
|
|
8373
|
+
en: (a) => `Write LinkedIn posts for the next ${a.days} day${Number(a.days) === 1 ? "" : "s"} from your best articles; they wait for your OK unless your LinkedIn posts go out on their own.`,
|
|
8374
|
+
"zh-Hant": (a) => `\u7528\u4F60\u6700\u597D\u7684\u6587\u7AE0\u5BEB\u672A\u4F86 ${a.days} \u5929\u7684 LinkedIn \u8CBC\u6587\uFF1B\u9664\u975E\u4F60\u8A2D\u5B9A\u8CBC\u6587\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6\u3002`
|
|
8011
8375
|
},
|
|
8012
8376
|
social_idea_add: {
|
|
8013
8377
|
en: (a) => `Add the LinkedIn idea ${q(a.title)}.`,
|
|
@@ -8022,12 +8386,30 @@ var confirmCopy = {
|
|
|
8022
8386
|
"zh-Hant": (a) => `\u522A\u9664 LinkedIn idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}\u3002`
|
|
8023
8387
|
},
|
|
8024
8388
|
social_idea_write: {
|
|
8025
|
-
en: (a) => `Write the LinkedIn post for idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}.`,
|
|
8026
|
-
"zh-Hant": (a) => `\u70BA idea #${a.id}${a.title ? ` ${q(a.title)}` : ""} \u5BEB LinkedIn \u8CBC\u6587\u3002`
|
|
8389
|
+
en: (a) => `Write the LinkedIn post for idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}; your own idea's post waits for your OK unless your LinkedIn posts go out on their own.`,
|
|
8390
|
+
"zh-Hant": (a) => `\u70BA idea #${a.id}${a.title ? ` ${q(a.title)}` : ""} \u5BEB LinkedIn \u8CBC\u6587\uFF1B\u4F60\u81EA\u5DF1 idea \u7684\u8CBC\u6587\uFF0C\u9664\u975E\u4F60\u8A2D\u5B9A\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6\u3002`
|
|
8391
|
+
},
|
|
8392
|
+
social_idea_save: {
|
|
8393
|
+
en: (a) => `Save this LinkedIn post for idea #${a.id} (one AI generation): ${q(a.text)}`,
|
|
8394
|
+
"zh-Hant": (a) => `\u70BA idea #${a.id} \u5132\u5B58\u9019\u5247 LinkedIn \u8CBC\u6587\uFF08\u4E00\u6B21 AI \u751F\u6210\uFF09\uFF1A${q(a.text)}`
|
|
8027
8395
|
},
|
|
8028
8396
|
campaign_start: {
|
|
8029
|
-
en: (a) => `Start the
|
|
8030
|
-
"zh-Hant": (a) => `${a.start_date ? `\u5F9E ${a.start_date}` : "\u5F9E\u4ECA\u5929"}\u958B\u59CB\
|
|
8397
|
+
en: (a) => `Start the series${named(a.name, a.id)}${a.start_date ? ` on ${a.start_date}` : " today"}, at most ${a.daily_limit || 1} post${Number(a.daily_limit || 1) === 1 ? "" : "s"} a day. Its posts go out on LinkedIn in order.`,
|
|
8398
|
+
"zh-Hant": (a) => `${a.start_date ? `\u5F9E ${a.start_date}` : "\u5F9E\u4ECA\u5929"}\u958B\u59CB\u7CFB\u5217${named(a.name, a.id)}\uFF0C\u6BCF\u5929\u6700\u591A ${a.daily_limit || 1} \u7BC7\uFF0C\u8CBC\u6587\u6703\u6309\u9806\u5E8F\u5728 LinkedIn \u767C\u51FA\u3002`
|
|
8399
|
+
},
|
|
8400
|
+
// ---------------------------------------------------------- Maggie Memory
|
|
8401
|
+
memory_remember: {
|
|
8402
|
+
en: (a) => `Remember${a.channel === "blog" ? " for blog posts" : a.channel === "linkedin" ? " for LinkedIn" : ""}: ${String(a.text ?? "").slice(0, 160)}`,
|
|
8403
|
+
"zh-Hant": (a) => `\u8A18\u4F4F${a.channel === "blog" ? "\uFF08\u90E8\u843D\u683C\uFF09" : a.channel === "linkedin" ? "\uFF08LinkedIn\uFF09" : ""}\uFF1A${String(a.text ?? "").slice(0, 160)}`
|
|
8404
|
+
},
|
|
8405
|
+
memory_forget: {
|
|
8406
|
+
en: (a) => `Forget${a.text_for_card ? ` ${q(a.text_for_card)}` : " this"} from your Maggie Memory.`,
|
|
8407
|
+
"zh-Hant": (a) => `\u5F9E\u4F60\u7684 Maggie \u8A18\u61B6\u4E2D\u5FD8\u8A18${a.text_for_card ? ` ${q(a.text_for_card)}` : "\u9019\u4E00\u689D"}\u3002`
|
|
8408
|
+
},
|
|
8409
|
+
// ---------------------------------------------------------- Help Maggie grow
|
|
8410
|
+
feedback_send: {
|
|
8411
|
+
en: (a) => `Send this to the cmo.so team: ${q(String(a.summary ?? "").slice(0, 200))}`,
|
|
8412
|
+
"zh-Hant": (a) => `\u628A\u9019\u5247\u56DE\u994B\u9001\u7D66 cmo.so \u5718\u968A\uFF1A${q(String(a.summary ?? "").slice(0, 200))}`
|
|
8031
8413
|
},
|
|
8032
8414
|
social_voice_learn: {
|
|
8033
8415
|
en: () => "Learn your LinkedIn voice from this post and save it as your default.",
|
|
@@ -8057,17 +8439,29 @@ var confirmCopy = {
|
|
|
8057
8439
|
en: () => "Make a link for you to connect LinkedIn.",
|
|
8058
8440
|
"zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
|
|
8059
8441
|
},
|
|
8442
|
+
social_approve_all: {
|
|
8443
|
+
en: () => "Approve every LinkedIn post waiting for your OK. They go out one by one at your posting times, where your network can see them.",
|
|
8444
|
+
"zh-Hant": () => "\u6279\u51C6\u6240\u6709\u7B49\u4F60\u78BA\u8A8D\u7684 LinkedIn \u8CBC\u6587\u3002\u5B83\u5011\u6703\u5728\u63A5\u4E0B\u4F86\u7684\u767C\u6587\u6642\u9593\u9010\u7BC7\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002"
|
|
8445
|
+
},
|
|
8446
|
+
series_create: {
|
|
8447
|
+
en: (a) => `Plan and write a LinkedIn series about ${q(a.brief)}: ${a.count || 3} posts that read as a series. Each waits in Review for your OK. Uses one AI generation a post.`,
|
|
8448
|
+
"zh-Hant": (a) => `\u898F\u5283\u4E26\u5BEB\u4E00\u500B LinkedIn \u7CFB\u5217\uFF1A${q(a.brief)}\uFF0C\u5171 ${a.count || 3} \u7BC7\uFF0C\u5167\u5BB9\u9023\u8CAB\u3002\u6BCF\u7BC7\u5728 Review \u7B49\u4F60\u78BA\u8A8D\u3002\u6BCF\u7BC7\u7528\u4E00\u6B21 AI \u751F\u6210\u3002`
|
|
8449
|
+
},
|
|
8450
|
+
series_approve: {
|
|
8451
|
+
en: (a) => `Approve the LinkedIn series${named(a.name, a.id)}. Its posts go out in order at your posting times, where your network can see them.`,
|
|
8452
|
+
"zh-Hant": (a) => `\u6279\u51C6 LinkedIn \u7CFB\u5217${named(a.name, a.id)}\u3002\u5B83\u7684\u8CBC\u6587\u6703\u5728\u4F60\u7684\u767C\u6587\u6642\u9593\u4F9D\u5E8F\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002`
|
|
8453
|
+
},
|
|
8060
8454
|
campaign_create: {
|
|
8061
|
-
en: (a) => `
|
|
8062
|
-
"zh-Hant": (a) => `\
|
|
8455
|
+
en: (a) => `Plan the LinkedIn series ${q(a.name)}.`,
|
|
8456
|
+
"zh-Hant": (a) => `\u898F\u5283 LinkedIn \u7CFB\u5217 ${q(a.name)}\u3002`
|
|
8063
8457
|
},
|
|
8064
8458
|
campaign_pause: {
|
|
8065
|
-
en: (a) => `${a.paused ? "Pause" : "Resume"} the
|
|
8066
|
-
"zh-Hant": (a) => `${a.paused ? "\u66AB\u505C" : "\u6062\u5FA9"}\
|
|
8459
|
+
en: (a) => `${a.paused ? "Pause" : "Resume"} the series${named(a.name, a.id)}.`,
|
|
8460
|
+
"zh-Hant": (a) => `${a.paused ? "\u66AB\u505C" : "\u6062\u5FA9"}\u7CFB\u5217${named(a.name, a.id)}\u3002`
|
|
8067
8461
|
},
|
|
8068
8462
|
social_approval_mode_set: {
|
|
8069
|
-
en: (a) => a.mode === "auto" ? "Post LinkedIn
|
|
8070
|
-
"zh-Hant": (a) => a.mode === "auto" ? "LinkedIn \
|
|
8463
|
+
en: (a) => a.mode === "auto" ? "Post LinkedIn posts without asking you first. Your network sees them at the posting times." : "Ask you before each LinkedIn post goes out.",
|
|
8464
|
+
"zh-Hant": (a) => a.mode === "auto" ? "LinkedIn \u8CBC\u6587\u4E0D\u518D\u5148\u554F\u4F60\uFF0C\u5230\u767C\u6587\u6642\u9593\u76F4\u63A5\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002" : "\u6BCF\u7BC7 LinkedIn \u8CBC\u6587\u767C\u51FA\u524D\u5148\u554F\u4F60\u3002"
|
|
8071
8465
|
},
|
|
8072
8466
|
// ---------------------------------------------------------------- reports
|
|
8073
8467
|
gsc_sync: {
|
|
@@ -8172,7 +8566,7 @@ function ws(ctx) {
|
|
|
8172
8566
|
if (!ctx.domain) throw new Error("No website is selected yet. Add one first.");
|
|
8173
8567
|
return domainApi(ctx.api, ctx.domain);
|
|
8174
8568
|
}
|
|
8175
|
-
var clip = (
|
|
8569
|
+
var clip = (text3, n = 280) => typeof text3 === "string" ? text3.length > n ? `${text3.slice(0, n)}\u2026` : text3 : text3 ?? null;
|
|
8176
8570
|
var plain = (html) => String(html ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
8177
8571
|
var untrusted = (value) => ({ untrusted: value });
|
|
8178
8572
|
function upsert(list2, item2, key) {
|
|
@@ -8520,9 +8914,9 @@ var blogTools = [
|
|
|
8520
8914
|
id: { type: "string" },
|
|
8521
8915
|
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8522
8916
|
instruction: { type: "string", description: "What to change, for mode edit" },
|
|
8523
|
-
title: { type: "string", description: "
|
|
8917
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8524
8918
|
},
|
|
8525
|
-
required: ["id", "mode"]
|
|
8919
|
+
required: ["id", "mode", "title"]
|
|
8526
8920
|
},
|
|
8527
8921
|
group: "blog",
|
|
8528
8922
|
need: write,
|
|
@@ -8538,13 +8932,73 @@ var blogTools = [
|
|
|
8538
8932
|
return { saved: true, lost_links: out.lost_links, ...untrusted({ preview: clip(out.markdown, 600) }) };
|
|
8539
8933
|
}
|
|
8540
8934
|
}),
|
|
8935
|
+
tool({
|
|
8936
|
+
name: "draft_rewrite_brief",
|
|
8937
|
+
description: "For an assistant that writes with its own model: a draft's whole Markdown and the rules for rewriting it (mode fix: formatting only; edit: the user's change). Write the new version yourself, then save it with draft_rewrite_save. Free.",
|
|
8938
|
+
parameters: {
|
|
8939
|
+
type: "object",
|
|
8940
|
+
properties: {
|
|
8941
|
+
id: { type: "string" },
|
|
8942
|
+
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8943
|
+
instruction: { type: "string", description: "What to change, for mode edit" }
|
|
8944
|
+
},
|
|
8945
|
+
required: ["id", "mode"]
|
|
8946
|
+
},
|
|
8947
|
+
group: "blog",
|
|
8948
|
+
need: read,
|
|
8949
|
+
risk: "R0",
|
|
8950
|
+
assistantWrites: true,
|
|
8951
|
+
run: async (ctx, { id, mode, instruction }) => {
|
|
8952
|
+
const p = await ws(ctx).post(id);
|
|
8953
|
+
if (!p.editable || !p.markdown) throw new Error("Only drafts that are not published yet can be rewritten.");
|
|
8954
|
+
if (mode === "edit" && !instruction?.trim()) throw new Error("Say what to change.");
|
|
8955
|
+
return {
|
|
8956
|
+
id: p.id,
|
|
8957
|
+
title: p.title,
|
|
8958
|
+
mode,
|
|
8959
|
+
task: rewriteTask(mode, instruction),
|
|
8960
|
+
links_to_keep: links(p.markdown).length,
|
|
8961
|
+
...untrusted({ markdown: p.markdown })
|
|
8962
|
+
};
|
|
8963
|
+
}
|
|
8964
|
+
}),
|
|
8965
|
+
tool({
|
|
8966
|
+
name: "draft_rewrite_save",
|
|
8967
|
+
description: "Save a draft you rewrote from draft_rewrite_brief: the whole new Markdown. It is checked like Maggie's own rewrites (not much shorter; a fix keeps every link and image).",
|
|
8968
|
+
parameters: {
|
|
8969
|
+
type: "object",
|
|
8970
|
+
properties: {
|
|
8971
|
+
id: { type: "string" },
|
|
8972
|
+
mode: { type: "string", enum: ["fix", "edit"] },
|
|
8973
|
+
markdown: { type: "string", description: "The whole rewritten post" },
|
|
8974
|
+
title: { type: "string", description: "The draft's title, for the approval card" }
|
|
8975
|
+
},
|
|
8976
|
+
required: ["id", "mode", "markdown"]
|
|
8977
|
+
},
|
|
8978
|
+
group: "blog",
|
|
8979
|
+
need: write,
|
|
8980
|
+
risk: "W1",
|
|
8981
|
+
assistantWrites: true,
|
|
8982
|
+
run: async (ctx, { id, mode, markdown }) => {
|
|
8983
|
+
const api2 = ws(ctx);
|
|
8984
|
+
const p = await api2.post(id);
|
|
8985
|
+
if (!p.editable || !p.markdown) throw new Error("Only drafts that are not published yet can be rewritten.");
|
|
8986
|
+
const check2 = checkRewrite(p.markdown, String(markdown ?? ""), mode === "fix" ? "fix" : "edit");
|
|
8987
|
+
if (check2.error) throw new Error(check2.error);
|
|
8988
|
+
await api2.updatePost(id, { markdown });
|
|
8989
|
+
return { saved: true, lost_links: check2.lost };
|
|
8990
|
+
}
|
|
8991
|
+
}),
|
|
8541
8992
|
tool({
|
|
8542
8993
|
name: "draft_delete",
|
|
8543
8994
|
description: "Delete a draft. Its idea is dropped too and the credit it used is not refunded. Ask which one; never delete several at once.",
|
|
8544
8995
|
parameters: {
|
|
8545
8996
|
type: "object",
|
|
8546
|
-
properties: {
|
|
8547
|
-
|
|
8997
|
+
properties: {
|
|
8998
|
+
id: { type: "string" },
|
|
8999
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9000
|
+
},
|
|
9001
|
+
required: ["id", "title"]
|
|
8548
9002
|
},
|
|
8549
9003
|
group: "blog",
|
|
8550
9004
|
need: write,
|
|
@@ -8559,9 +9013,9 @@ var blogTools = [
|
|
|
8559
9013
|
properties: {
|
|
8560
9014
|
id: { type: "string", description: "The planned post's id" },
|
|
8561
9015
|
to: { type: "string", enum: moves },
|
|
8562
|
-
title: { type: "string", description: "
|
|
9016
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8563
9017
|
},
|
|
8564
|
-
required: ["id", "to"]
|
|
9018
|
+
required: ["id", "to", "title"]
|
|
8565
9019
|
},
|
|
8566
9020
|
group: "blog",
|
|
8567
9021
|
need: write,
|
|
@@ -8592,8 +9046,11 @@ var blogTools = [
|
|
|
8592
9046
|
description: "Take a planned post out of the plan; its idea can be restored later.",
|
|
8593
9047
|
parameters: {
|
|
8594
9048
|
type: "object",
|
|
8595
|
-
properties: {
|
|
8596
|
-
|
|
9049
|
+
properties: {
|
|
9050
|
+
id: { type: "string" },
|
|
9051
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9052
|
+
},
|
|
9053
|
+
required: ["id", "title"]
|
|
8597
9054
|
},
|
|
8598
9055
|
group: "blog",
|
|
8599
9056
|
need: write,
|
|
@@ -8650,9 +9107,9 @@ var blogTools = [
|
|
|
8650
9107
|
need: write,
|
|
8651
9108
|
risk: "W2",
|
|
8652
9109
|
gates: ["LLM"],
|
|
8653
|
-
run: async (ctx, { url, text:
|
|
8654
|
-
if (!url && !
|
|
8655
|
-
return ws(ctx).addIdea(url ? { url } : { text:
|
|
9110
|
+
run: async (ctx, { url, text: text3 }) => {
|
|
9111
|
+
if (!url && !text3) throw new Error("Need a link or a sentence.");
|
|
9112
|
+
return ws(ctx).addIdea(url ? { url } : { text: text3 });
|
|
8656
9113
|
}
|
|
8657
9114
|
}),
|
|
8658
9115
|
tool({
|
|
@@ -8670,7 +9127,7 @@ var blogTools = [
|
|
|
8670
9127
|
need: write,
|
|
8671
9128
|
risk: "W2",
|
|
8672
9129
|
gates: ["LLM"],
|
|
8673
|
-
run: async (ctx, { count, brief }) => ws(ctx).generateIdeas(int(count, 1, 10, 3),
|
|
9130
|
+
run: async (ctx, { count, brief: brief2 }) => ws(ctx).generateIdeas(int(count, 1, 10, 3), brief2)
|
|
8674
9131
|
}),
|
|
8675
9132
|
tool({
|
|
8676
9133
|
name: "ideas_import_sitemap",
|
|
@@ -8715,9 +9172,9 @@ var blogTools = [
|
|
|
8715
9172
|
properties: {
|
|
8716
9173
|
id: { type: "string" },
|
|
8717
9174
|
delta: { type: "integer", minimum: -1, maximum: 1 },
|
|
8718
|
-
title: { type: "string", description: "
|
|
9175
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
8719
9176
|
},
|
|
8720
|
-
required: ["id", "delta"]
|
|
9177
|
+
required: ["id", "delta", "title"]
|
|
8721
9178
|
},
|
|
8722
9179
|
group: "blog",
|
|
8723
9180
|
need: write,
|
|
@@ -8729,8 +9186,11 @@ var blogTools = [
|
|
|
8729
9186
|
description: "Add a waiting idea to the end of the plan (the plan holds at most 10).",
|
|
8730
9187
|
parameters: {
|
|
8731
9188
|
type: "object",
|
|
8732
|
-
properties: {
|
|
8733
|
-
|
|
9189
|
+
properties: {
|
|
9190
|
+
id: { type: "string" },
|
|
9191
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9192
|
+
},
|
|
9193
|
+
required: ["id", "title"]
|
|
8734
9194
|
},
|
|
8735
9195
|
group: "blog",
|
|
8736
9196
|
need: write,
|
|
@@ -8742,8 +9202,11 @@ var blogTools = [
|
|
|
8742
9202
|
description: "Remove one idea so it won't be written (it can be restored).",
|
|
8743
9203
|
parameters: {
|
|
8744
9204
|
type: "object",
|
|
8745
|
-
properties: {
|
|
8746
|
-
|
|
9205
|
+
properties: {
|
|
9206
|
+
id: { type: "string" },
|
|
9207
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9208
|
+
},
|
|
9209
|
+
required: ["id", "title"]
|
|
8747
9210
|
},
|
|
8748
9211
|
group: "blog",
|
|
8749
9212
|
need: write,
|
|
@@ -8755,8 +9218,11 @@ var blogTools = [
|
|
|
8755
9218
|
description: "Bring back a discarded idea (ideas_list status discarded).",
|
|
8756
9219
|
parameters: {
|
|
8757
9220
|
type: "object",
|
|
8758
|
-
properties: {
|
|
8759
|
-
|
|
9221
|
+
properties: {
|
|
9222
|
+
id: { type: "string" },
|
|
9223
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9224
|
+
},
|
|
9225
|
+
required: ["id", "title"]
|
|
8760
9226
|
},
|
|
8761
9227
|
group: "blog",
|
|
8762
9228
|
need: write,
|
|
@@ -8773,6 +9239,24 @@ var blogTools = [
|
|
|
8773
9239
|
run: async (ctx, { paused }) => paused ? ws(ctx).pauseAutopilot() : ws(ctx).resumeAutopilot()
|
|
8774
9240
|
})
|
|
8775
9241
|
];
|
|
9242
|
+
var KEEP = "Keep every link and image exactly: same URLs, same order. Keep the language of the post. Return the whole post as Markdown, not a summary and not a fragment.";
|
|
9243
|
+
var LINK = /\]\((\S+?)(?:\s+"[^"]*")?\)/g;
|
|
9244
|
+
var links = (markdown) => [...markdown.matchAll(LINK)].map((m) => m[1]);
|
|
9245
|
+
function rewriteTask(mode, instruction) {
|
|
9246
|
+
return mode === "fix" ? "Fix only the Markdown formatting of this blog post: broken headings (e.g. '#Title', a heading glued to a paragraph), bold/italic markers left open, list items that lost their marker or spacing, unclosed code fences, malformed links or images, missing blank lines between blocks. Do not change, add or remove any wording. " + KEEP : `Edit this blog post as the owner asks: ${instruction ?? ""}
|
|
9247
|
+
Change only what that needs; keep the rest as it is. ${KEEP}`;
|
|
9248
|
+
}
|
|
9249
|
+
function checkRewrite(before, after, mode) {
|
|
9250
|
+
const lost = links(before).filter((u) => !after.includes(u)).length;
|
|
9251
|
+
if (after.length < before.length * (mode === "fix" ? 0.8 : 0.3))
|
|
9252
|
+
return {
|
|
9253
|
+
error: "The new version is much shorter than the draft, so nothing was saved. Send the whole post.",
|
|
9254
|
+
lost
|
|
9255
|
+
};
|
|
9256
|
+
if (mode === "fix" && lost)
|
|
9257
|
+
return { error: `The fixed version dropped ${lost} link or image, so nothing was saved. Keep every one.`, lost };
|
|
9258
|
+
return { lost };
|
|
9259
|
+
}
|
|
8776
9260
|
|
|
8777
9261
|
// ../../packages/tools/src/brand.ts
|
|
8778
9262
|
var write2 = { resource: "overview", level: "write" };
|
|
@@ -9095,8 +9579,11 @@ var brandTools = [
|
|
|
9095
9579
|
description: "Delete one of the user's brand voices. Cannot be undone; put its name on the card.",
|
|
9096
9580
|
parameters: {
|
|
9097
9581
|
type: "object",
|
|
9098
|
-
properties: {
|
|
9099
|
-
|
|
9582
|
+
properties: {
|
|
9583
|
+
id: { type: "string" },
|
|
9584
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9585
|
+
},
|
|
9586
|
+
required: ["id", "name"]
|
|
9100
9587
|
},
|
|
9101
9588
|
group: "brand",
|
|
9102
9589
|
need: write2,
|
|
@@ -9123,6 +9610,65 @@ function brandGaps(b) {
|
|
|
9123
9610
|
return { complete: gaps.length === 0, gaps };
|
|
9124
9611
|
}
|
|
9125
9612
|
|
|
9613
|
+
// ../../packages/tools/src/feedback.ts
|
|
9614
|
+
var account = { resource: "account", level: "read" };
|
|
9615
|
+
var TYPES = ["bug", "quality", "idea", "praise", "other"];
|
|
9616
|
+
var text = (v, n) => String(v ?? "").trim().slice(0, n);
|
|
9617
|
+
var feedbackTools = [
|
|
9618
|
+
tool({
|
|
9619
|
+
name: "feedback_send",
|
|
9620
|
+
description: "Send feedback to the cmo.so team: a bug, something not good enough, an idea, or something the user loves. Use it when the user asks to tell the team, reports a problem with CMO.so or with you, or wants a feature; you may offer it when the user points out that you got something wrong. Write the summary in one sentence of the user's own point; put what happened in details.",
|
|
9621
|
+
parameters: {
|
|
9622
|
+
type: "object",
|
|
9623
|
+
properties: {
|
|
9624
|
+
type: {
|
|
9625
|
+
type: "string",
|
|
9626
|
+
enum: TYPES,
|
|
9627
|
+
description: "bug: did something wrong \xB7 quality: works but not good enough \xB7 idea: a new feature \xB7 praise \xB7 other"
|
|
9628
|
+
},
|
|
9629
|
+
summary: { type: "string", description: "One sentence, \u2264200 characters" },
|
|
9630
|
+
details: { type: "string", description: "What happened, \u22644000 characters" },
|
|
9631
|
+
expected: { type: "string", description: "For a bug: what the user expected" },
|
|
9632
|
+
actual: { type: "string", description: "For a bug: what happened instead" }
|
|
9633
|
+
},
|
|
9634
|
+
required: ["type", "summary"]
|
|
9635
|
+
},
|
|
9636
|
+
group: "account",
|
|
9637
|
+
need: account,
|
|
9638
|
+
risk: "W1",
|
|
9639
|
+
run: async (ctx, a) => {
|
|
9640
|
+
const f = await ctx.api.feedback.send({
|
|
9641
|
+
type: TYPES.find((t) => t === a.type) ?? "other",
|
|
9642
|
+
summary: text(a.summary, 200),
|
|
9643
|
+
...a.details ? { details: text(a.details, 4e3) } : {},
|
|
9644
|
+
...a.expected ? { expected: text(a.expected, 1e3) } : {},
|
|
9645
|
+
...a.actual ? { actual: text(a.actual, 1e3) } : {},
|
|
9646
|
+
domain: ctx.domain,
|
|
9647
|
+
source: ctx.client ?? "chat"
|
|
9648
|
+
});
|
|
9649
|
+
return { sent: true, id: f.id, status: f.status };
|
|
9650
|
+
}
|
|
9651
|
+
}),
|
|
9652
|
+
tool({
|
|
9653
|
+
name: "feedback_list",
|
|
9654
|
+
description: "The feedback the user sent to the cmo.so team, newest first: what it was about, its status (new, reviewing, planned, fixed, wont_fix, duplicate) and the team's reply.",
|
|
9655
|
+
parameters: none,
|
|
9656
|
+
group: "account",
|
|
9657
|
+
need: account,
|
|
9658
|
+
risk: "R0",
|
|
9659
|
+
run: async (ctx) => ({
|
|
9660
|
+
items: (await ctx.api.feedback.mine()).items.slice(0, 20).map((f) => ({
|
|
9661
|
+
type: f.type,
|
|
9662
|
+
summary: clip(f.summary, 200),
|
|
9663
|
+
status: f.status,
|
|
9664
|
+
reply: clip(f.reply, 600),
|
|
9665
|
+
new_reply: f.unread,
|
|
9666
|
+
sent: f.created_at
|
|
9667
|
+
}))
|
|
9668
|
+
})
|
|
9669
|
+
})
|
|
9670
|
+
];
|
|
9671
|
+
|
|
9126
9672
|
// ../../packages/tools/src/insights.ts
|
|
9127
9673
|
var read2 = { resource: "report", level: "read" };
|
|
9128
9674
|
var write3 = { resource: "report", level: "write" };
|
|
@@ -9214,14 +9760,14 @@ var insightTools = [
|
|
|
9214
9760
|
];
|
|
9215
9761
|
|
|
9216
9762
|
// ../../packages/tools/src/me.ts
|
|
9217
|
-
var
|
|
9763
|
+
var account2 = { resource: "account", level: "read" };
|
|
9218
9764
|
var meTools = [
|
|
9219
9765
|
tool({
|
|
9220
9766
|
name: "api_keys_list",
|
|
9221
9767
|
description: "The user's API keys made on ai.cmo.so: name, first characters, read or read-write, last used. Never the key itself; making or revoking keys is done on ai.cmo.so (dashboard_link api_access).",
|
|
9222
9768
|
parameters: none,
|
|
9223
9769
|
group: "account",
|
|
9224
|
-
need:
|
|
9770
|
+
need: account2,
|
|
9225
9771
|
risk: "R0",
|
|
9226
9772
|
run: async ({ api: api2 }) => (await api2.account.keys()).items
|
|
9227
9773
|
}),
|
|
@@ -9230,7 +9776,7 @@ var meTools = [
|
|
|
9230
9776
|
description: "Apps connected to the user's account (ChatGPT, Claude, the cmo.so app\u2026), what they may do and when they were last used. `current` is the one in this conversation.",
|
|
9231
9777
|
parameters: none,
|
|
9232
9778
|
group: "account",
|
|
9233
|
-
need:
|
|
9779
|
+
need: account2,
|
|
9234
9780
|
risk: "R0",
|
|
9235
9781
|
run: async ({ api: api2 }) => (await api2.account.apps()).items.map((a) => ({
|
|
9236
9782
|
grant_id: a.grant_id,
|
|
@@ -9253,7 +9799,7 @@ var meTools = [
|
|
|
9253
9799
|
required: ["grant_id", "app"]
|
|
9254
9800
|
},
|
|
9255
9801
|
group: "account",
|
|
9256
|
-
need:
|
|
9802
|
+
need: account2,
|
|
9257
9803
|
risk: "W3",
|
|
9258
9804
|
run: async ({ api: api2 }, { grant_id }) => api2.account.revokeApp(String(grant_id))
|
|
9259
9805
|
}),
|
|
@@ -9262,7 +9808,7 @@ var meTools = [
|
|
|
9262
9808
|
description: "The user's latest invoices across the subscriptions they pay for: date, amount, status and a link to view or download each.",
|
|
9263
9809
|
parameters: none,
|
|
9264
9810
|
group: "billing",
|
|
9265
|
-
need:
|
|
9811
|
+
need: account2,
|
|
9266
9812
|
risk: "R0",
|
|
9267
9813
|
run: async ({ api: api2 }) => (await api2.account.invoices()).items.slice(0, 15).map((i) => ({
|
|
9268
9814
|
website: i.domain,
|
|
@@ -9278,7 +9824,7 @@ var meTools = [
|
|
|
9278
9824
|
description: "Change the user's display name (the only profile field Maggie changes; email and password are on ai.cmo.so).",
|
|
9279
9825
|
parameters: { type: "object", properties: { display_name: { type: "string" } }, required: ["display_name"] },
|
|
9280
9826
|
group: "account",
|
|
9281
|
-
need:
|
|
9827
|
+
need: account2,
|
|
9282
9828
|
risk: "W1",
|
|
9283
9829
|
idempotent: true,
|
|
9284
9830
|
run: async ({ api: api2 }, { display_name }) => api2.account.rename(String(display_name).slice(0, 80))
|
|
@@ -9288,7 +9834,7 @@ var meTools = [
|
|
|
9288
9834
|
description: "Which app notifications the user gets: LinkedIn posts to review (social_review), LinkedIn posts that failed (social_failed), new blog posts published (published).",
|
|
9289
9835
|
parameters: none,
|
|
9290
9836
|
group: "account",
|
|
9291
|
-
need:
|
|
9837
|
+
need: account2,
|
|
9292
9838
|
risk: "R0",
|
|
9293
9839
|
run: async ({ api: api2 }) => api2.account.notifications()
|
|
9294
9840
|
}),
|
|
@@ -9304,13 +9850,99 @@ var meTools = [
|
|
|
9304
9850
|
}
|
|
9305
9851
|
},
|
|
9306
9852
|
group: "account",
|
|
9307
|
-
need:
|
|
9853
|
+
need: account2,
|
|
9308
9854
|
risk: "W1",
|
|
9309
9855
|
idempotent: true,
|
|
9310
9856
|
run: async ({ api: api2 }, prefs) => api2.account.setNotifications(prefs)
|
|
9311
9857
|
})
|
|
9312
9858
|
];
|
|
9313
9859
|
|
|
9860
|
+
// ../../packages/tools/src/memory.ts
|
|
9861
|
+
var own = { resource: "overview", level: "read" };
|
|
9862
|
+
var SECTIONS = ["prefer", "always", "never"];
|
|
9863
|
+
var KINDS = ["tone", "length", "structure", "format", "wording", "topic", "image", "cta", "approval", "other"];
|
|
9864
|
+
var brief = (m) => ({
|
|
9865
|
+
enabled: m.enabled,
|
|
9866
|
+
channels: Object.fromEntries(
|
|
9867
|
+
MEMORY_CHANNELS.map((ch) => [
|
|
9868
|
+
ch,
|
|
9869
|
+
{
|
|
9870
|
+
summary: m.channels[ch]?.summary ?? "",
|
|
9871
|
+
items: (m.channels[ch]?.items ?? []).map((i) => ({
|
|
9872
|
+
id: i.id,
|
|
9873
|
+
section: i.section,
|
|
9874
|
+
kind: i.kind,
|
|
9875
|
+
text: clip(i.text, 200),
|
|
9876
|
+
source: i.source
|
|
9877
|
+
}))
|
|
9878
|
+
}
|
|
9879
|
+
])
|
|
9880
|
+
)
|
|
9881
|
+
});
|
|
9882
|
+
var memoryTools = [
|
|
9883
|
+
tool({
|
|
9884
|
+
name: "memory_get",
|
|
9885
|
+
description: "The user's Maggie Memory on this website: their saved taste per channel (general, blog, linkedin, facebook, instagram, google_business) as prefer / always / never items, each with an id. Already in your instructions when it is on; read it to answer what you remember or to find an id to forget.",
|
|
9886
|
+
parameters: none,
|
|
9887
|
+
group: "memory",
|
|
9888
|
+
need: own,
|
|
9889
|
+
risk: "R0",
|
|
9890
|
+
run: async (ctx) => brief(await ws(ctx).maggieMemory())
|
|
9891
|
+
}),
|
|
9892
|
+
tool({
|
|
9893
|
+
name: "memory_remember",
|
|
9894
|
+
description: 'Save one lasting preference the user stated ("remember\u2026", "always\u2026", "never\u2026", "from now on\u2026") to their Maggie Memory, so future posts, drafts and images follow it. One short actionable sentence; channel general unless it is about blog posts, LinkedIn or Facebook only. Not for one-off requests.',
|
|
9895
|
+
parameters: {
|
|
9896
|
+
type: "object",
|
|
9897
|
+
properties: {
|
|
9898
|
+
channel: { type: "string", enum: [...MEMORY_CHANNELS] },
|
|
9899
|
+
section: {
|
|
9900
|
+
type: "string",
|
|
9901
|
+
enum: SECTIONS,
|
|
9902
|
+
description: "prefer: a leaning \xB7 always: do it every time \xB7 never: don't do or use it"
|
|
9903
|
+
},
|
|
9904
|
+
text: { type: "string", description: "The whole rule, \u2264160 characters, e.g. \u201CNever use emojis.\u201D" },
|
|
9905
|
+
kind: { type: "string", enum: KINDS }
|
|
9906
|
+
},
|
|
9907
|
+
required: ["channel", "section", "text"]
|
|
9908
|
+
},
|
|
9909
|
+
group: "memory",
|
|
9910
|
+
need: own,
|
|
9911
|
+
risk: "W1",
|
|
9912
|
+
run: async (ctx, { channel, section, text: text3, kind }) => {
|
|
9913
|
+
const ch = MEMORY_CHANNELS.find((c) => c === channel) ?? "general";
|
|
9914
|
+
const sec = SECTIONS.includes(String(section)) ? section : "prefer";
|
|
9915
|
+
const m = await ws(ctx).addMaggieMemory({
|
|
9916
|
+
channel: ch,
|
|
9917
|
+
section: sec,
|
|
9918
|
+
text: String(text3 ?? "").trim().slice(0, 160),
|
|
9919
|
+
...kind && KINDS.includes(kind) ? { kind } : {}
|
|
9920
|
+
});
|
|
9921
|
+
return { saved: true, enabled: m.enabled };
|
|
9922
|
+
}
|
|
9923
|
+
}),
|
|
9924
|
+
tool({
|
|
9925
|
+
name: "memory_forget",
|
|
9926
|
+
description: "Remove one item from the user's Maggie Memory (ids from memory_get).",
|
|
9927
|
+
parameters: {
|
|
9928
|
+
type: "object",
|
|
9929
|
+
properties: {
|
|
9930
|
+
id: { type: "string" },
|
|
9931
|
+
text_for_card: { type: "string", description: "The item's text, for the card" }
|
|
9932
|
+
},
|
|
9933
|
+
required: ["id"]
|
|
9934
|
+
},
|
|
9935
|
+
group: "memory",
|
|
9936
|
+
need: own,
|
|
9937
|
+
risk: "W1",
|
|
9938
|
+
run: async (ctx, { id }) => {
|
|
9939
|
+
await ws(ctx).deleteMaggieMemory(String(id));
|
|
9940
|
+
return { forgotten: true };
|
|
9941
|
+
}
|
|
9942
|
+
})
|
|
9943
|
+
];
|
|
9944
|
+
var MEMORY_TOOLS = new Set(memoryTools.map((t) => t.name));
|
|
9945
|
+
|
|
9314
9946
|
// ../../packages/tools/src/publishing.ts
|
|
9315
9947
|
var read3 = { resource: "autopilot", level: "read" };
|
|
9316
9948
|
var write4 = { resource: "autopilot", level: "write" };
|
|
@@ -9386,7 +10018,7 @@ var publishingTools = [
|
|
|
9386
10018
|
}),
|
|
9387
10019
|
tool({
|
|
9388
10020
|
name: "rewrite_request",
|
|
9389
|
-
description: "Rewrite one
|
|
10021
|
+
description: "Rewrite one post or page on the user's website and update it there (same URL). Works for any post on their WordPress, including its blog subdomain (blog.example.com for example.com), whether CMO.so wrote it or not: it is found by its URL. Uses one post credit. Optional instructions (what to change, e.g. 'add 5 FAQs at the end') and target keywords.",
|
|
9390
10022
|
parameters: {
|
|
9391
10023
|
type: "object",
|
|
9392
10024
|
properties: {
|
|
@@ -9548,9 +10180,21 @@ var reportTools = [
|
|
|
9548
10180
|
};
|
|
9549
10181
|
}
|
|
9550
10182
|
}),
|
|
10183
|
+
tool({
|
|
10184
|
+
name: "gsc_connect_link",
|
|
10185
|
+
description: "A Google sign-in link for the user to connect Google Search Console for this website (or reconnect). Give them the link to open; never ask for their Google password. They must sign in with a Google account that can see this website in Search Console; if it can't, the page says which account and how to add it.",
|
|
10186
|
+
parameters: none,
|
|
10187
|
+
group: "reports",
|
|
10188
|
+
need: { resource: "autopilot", level: "write" },
|
|
10189
|
+
risk: "W1",
|
|
10190
|
+
run: async (ctx) => {
|
|
10191
|
+
const out = await ws(ctx).connectSearchConsole("close");
|
|
10192
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10193
|
+
}
|
|
10194
|
+
}),
|
|
9551
10195
|
tool({
|
|
9552
10196
|
name: "gsc_performance_get",
|
|
9553
|
-
description: "Google Search Console for the period (30d, 90d or 12m): clicks, impressions, CTR and average position, with the change against the period before, the daily trend and the top pages.",
|
|
10197
|
+
description: "Google Search Console for the period (30d, 90d or 12m): clicks, impressions, CTR and average position, with the change against the period before (clicks and impressions as a ratio: -0.25 = 25% fewer, null when there was nothing before; ctr in points as a fraction; position in places, lower is better), the daily trend and the top pages.",
|
|
9554
10198
|
parameters: { type: "object", properties: { period: { type: "string", enum: periods } } },
|
|
9555
10199
|
group: "reports",
|
|
9556
10200
|
need: read4,
|
|
@@ -9717,8 +10361,26 @@ var reportTools = [
|
|
|
9717
10361
|
var read5 = { resource: "autopilot", level: "read" };
|
|
9718
10362
|
var write5 = { resource: "autopilot", level: "write" };
|
|
9719
10363
|
var moves2 = ["up", "down", "top"];
|
|
10364
|
+
var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
10365
|
+
var channelsParam = {
|
|
10366
|
+
type: "array",
|
|
10367
|
+
items: { type: "string", enum: CHANNEL_KEYS },
|
|
10368
|
+
description: "Which channels to write for: linkedin, facebook. Leave out to write for every connected channel (one post each, one credit each); give one only when the user names it."
|
|
10369
|
+
};
|
|
10370
|
+
var channelsOf = (value) => {
|
|
10371
|
+
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
10372
|
+
return list2.length ? list2 : void 0;
|
|
10373
|
+
};
|
|
10374
|
+
var writing = (job) => ({
|
|
10375
|
+
job_id: job.id,
|
|
10376
|
+
status: "writing",
|
|
10377
|
+
posts_expected: job.expected,
|
|
10378
|
+
channels: job.channels,
|
|
10379
|
+
note: "Maggie is writing in the background (a minute or two). Tell the user they'll be told when it's ready; social_write_status says when it's done."
|
|
10380
|
+
});
|
|
9720
10381
|
var item = (i) => ({
|
|
9721
10382
|
id: i.id,
|
|
10383
|
+
channel: i.provider || "linkedin",
|
|
9722
10384
|
status: i.status,
|
|
9723
10385
|
eta: i.eta ?? null,
|
|
9724
10386
|
scheduled_at: i.scheduled_at,
|
|
@@ -9730,99 +10392,391 @@ var item = (i) => ({
|
|
|
9730
10392
|
});
|
|
9731
10393
|
var socialTools = [
|
|
9732
10394
|
tool({
|
|
9733
|
-
name: "social_list",
|
|
9734
|
-
description: "The user's LinkedIn:
|
|
10395
|
+
name: "social_list",
|
|
10396
|
+
description: "The user's social channels (LinkedIn, a Facebook Page): which are connected or paused, approval mode, the queue (drafts to review and queued posts, each with its channel and when it should go out), campaigns, and recent posts.",
|
|
10397
|
+
parameters: none,
|
|
10398
|
+
group: "social",
|
|
10399
|
+
need: read5,
|
|
10400
|
+
risk: "R0",
|
|
10401
|
+
run: async (ctx) => {
|
|
10402
|
+
const s = await ws(ctx).social();
|
|
10403
|
+
return {
|
|
10404
|
+
connected: s.connected,
|
|
10405
|
+
channels: (s.channels ?? []).map((c) => ({
|
|
10406
|
+
channel: c.key,
|
|
10407
|
+
connected: c.connected,
|
|
10408
|
+
account: c.account?.display_name ?? null,
|
|
10409
|
+
paused: c.paused,
|
|
10410
|
+
needs_page: c.needs_page,
|
|
10411
|
+
per_week: c.frequency?.per_week ?? null
|
|
10412
|
+
})),
|
|
10413
|
+
account: s.linkedin ? { name: s.linkedin.display_name, status: s.linkedin.status } : null,
|
|
10414
|
+
posting_paused: s.posting_paused,
|
|
10415
|
+
approval_mode: s.approval_mode,
|
|
10416
|
+
setup_done: s.setup_done ?? null,
|
|
10417
|
+
queue: s.queue.map(item),
|
|
10418
|
+
campaigns: (s.campaigns ?? []).map((c) => ({
|
|
10419
|
+
id: c.id,
|
|
10420
|
+
name: c.name,
|
|
10421
|
+
ideas: c.ideas,
|
|
10422
|
+
written: c.written,
|
|
10423
|
+
ready: c.ready,
|
|
10424
|
+
started: c.started,
|
|
10425
|
+
start_date: c.start_date,
|
|
10426
|
+
daily_limit: c.daily_limit
|
|
10427
|
+
})),
|
|
10428
|
+
posted: s.posted.slice(0, 5).map(item)
|
|
10429
|
+
};
|
|
10430
|
+
}
|
|
10431
|
+
}),
|
|
10432
|
+
tool({
|
|
10433
|
+
name: "social_update_copy",
|
|
10434
|
+
description: "Change the text of a social post that hasn't gone out yet (a draft or queued post).",
|
|
10435
|
+
parameters: {
|
|
10436
|
+
type: "object",
|
|
10437
|
+
properties: { id: { type: "integer" }, text: { type: "string", description: "The whole new post text" } },
|
|
10438
|
+
required: ["id", "text"]
|
|
10439
|
+
},
|
|
10440
|
+
group: "social",
|
|
10441
|
+
need: write5,
|
|
10442
|
+
risk: "W1",
|
|
10443
|
+
gates: ["VER"],
|
|
10444
|
+
run: async (ctx, { id, text: text3 }) => {
|
|
10445
|
+
const out = await ws(ctx).updateSocial(Number(id), String(text3 ?? ""));
|
|
10446
|
+
return { id: out.id, status: out.status };
|
|
10447
|
+
}
|
|
10448
|
+
}),
|
|
10449
|
+
tool({
|
|
10450
|
+
name: "social_approve",
|
|
10451
|
+
description: "Approve LinkedIn post drafts: each goes to the front of the user's queue and out at the next posting times. Pass every id to approve in one call.",
|
|
10452
|
+
parameters: {
|
|
10453
|
+
type: "object",
|
|
10454
|
+
properties: { ids: { type: "array", items: { type: "integer" }, description: "The drafts to approve" } },
|
|
10455
|
+
required: ["ids"]
|
|
10456
|
+
},
|
|
10457
|
+
group: "social",
|
|
10458
|
+
need: write5,
|
|
10459
|
+
risk: "W3",
|
|
10460
|
+
gates: ["VER", "LI"],
|
|
10461
|
+
run: async (ctx, { ids: ids2, id }) => {
|
|
10462
|
+
const list2 = (Array.isArray(ids2) && ids2.length ? ids2 : id != null ? [id] : []).map(Number).slice(0, 20);
|
|
10463
|
+
if (!list2.length) throw new Error("Which post?");
|
|
10464
|
+
const api2 = ws(ctx);
|
|
10465
|
+
const results = [];
|
|
10466
|
+
for (const n of list2) {
|
|
10467
|
+
try {
|
|
10468
|
+
await api2.approveSocial(n);
|
|
10469
|
+
results.push({ id: n, ok: true });
|
|
10470
|
+
} catch (e) {
|
|
10471
|
+
results.push({ id: n, ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
10472
|
+
}
|
|
10473
|
+
}
|
|
10474
|
+
return { approved: results.filter((r) => r.ok).length, results };
|
|
10475
|
+
}
|
|
10476
|
+
}),
|
|
10477
|
+
tool({
|
|
10478
|
+
name: "social_skip",
|
|
10479
|
+
description: "Skip a LinkedIn post (it won't be posted; it can be restored).",
|
|
10480
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10481
|
+
group: "social",
|
|
10482
|
+
need: write5,
|
|
10483
|
+
risk: "W1",
|
|
10484
|
+
gates: ["VER"],
|
|
10485
|
+
run: async (ctx, { id }) => ws(ctx).skipSocial(Number(id))
|
|
10486
|
+
}),
|
|
10487
|
+
tool({
|
|
10488
|
+
name: "social_restore",
|
|
10489
|
+
description: "Bring back a skipped LinkedIn post.",
|
|
10490
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10491
|
+
group: "social",
|
|
10492
|
+
need: write5,
|
|
10493
|
+
risk: "W1",
|
|
10494
|
+
gates: ["VER"],
|
|
10495
|
+
run: async (ctx, { id }) => ws(ctx).restoreSocial(Number(id))
|
|
10496
|
+
}),
|
|
10497
|
+
tool({
|
|
10498
|
+
name: "social_image_new",
|
|
10499
|
+
description: "Make a new picture for a social post that hasn't gone out, or change its current one (mode: edit, as `instruction` says). Uses 1 AI generation. It is made in the background, on the user's own ChatGPT when their Maggie Bridge has images on, else by CMO.so; it takes up to a couple of minutes and appears on the post by itself (check with social_image_status). One picture is made at a time per website: while another is being made this one waits in line (status queued). It fails with image_busy only when this post already has one being made or in line; pass replace: true only when the user agrees to stop the one being made and make this one now. A post written from an article gets its own picture (from its text and the article's title) and then goes out as that picture. Every new picture carries a short title and line set on it; count: 4 makes a set of 4 pictures that tell one story (still 1 AI generation, drawn as one storyboard and cut); LinkedIn and Facebook post a set together as a collage. text_effect picks the style of those words (an id from social_text_effects); leave it out to let Maggie pick. style picks the picture style (an id from social_look: photo_headline, editorial, duotone, illustrated, bold_type, infographic); leave it out and Maggie takes turns with the brand's 3. bold_type and infographic make no AI picture (big words or a list in the brand's look) and cost nothing.",
|
|
10500
|
+
parameters: {
|
|
10501
|
+
type: "object",
|
|
10502
|
+
properties: {
|
|
10503
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10504
|
+
replace: { type: "boolean", description: "Stop the picture being made for another post first" },
|
|
10505
|
+
instruction: {
|
|
10506
|
+
type: "string",
|
|
10507
|
+
description: "What the user wants in the picture, in their words (up to 300 characters), e.g. 'gold border, our logo, Future 100 \xB7 2025'. Leave out when they gave no direction."
|
|
10508
|
+
},
|
|
10509
|
+
mode: {
|
|
10510
|
+
type: "string",
|
|
10511
|
+
enum: ["new", "edit"],
|
|
10512
|
+
description: "edit: change the post's current picture as the instruction says (it must have one)"
|
|
10513
|
+
},
|
|
10514
|
+
count: {
|
|
10515
|
+
type: "integer",
|
|
10516
|
+
description: "1 (default) or 4: a set of 4 pictures telling one story, a line of words on each"
|
|
10517
|
+
},
|
|
10518
|
+
text_effect: {
|
|
10519
|
+
type: "string",
|
|
10520
|
+
description: "For a set: the words' style id from social_text_effects (omit = Maggie picks)"
|
|
10521
|
+
},
|
|
10522
|
+
style: {
|
|
10523
|
+
type: "string",
|
|
10524
|
+
description: "Picture style id from social_look (omit = Maggie picks from the brand's 3)"
|
|
10525
|
+
}
|
|
10526
|
+
},
|
|
10527
|
+
required: ["id"]
|
|
10528
|
+
},
|
|
10529
|
+
group: "social",
|
|
10530
|
+
need: write5,
|
|
10531
|
+
risk: "W2",
|
|
10532
|
+
gates: ["VER", "CR"],
|
|
10533
|
+
run: async (ctx, { id, replace, instruction, mode, count, text_effect, style }) => {
|
|
10534
|
+
const out = await ws(ctx).regenerateSocialImage(Number(id), {
|
|
10535
|
+
replace: replace === true,
|
|
10536
|
+
...typeof style === "string" && style.trim() && mode !== "edit" ? { style: style.trim() } : {},
|
|
10537
|
+
...mode === "edit" ? { mode: "edit" } : {},
|
|
10538
|
+
...Number(count) === 4 && mode !== "edit" ? { count: 4 } : {},
|
|
10539
|
+
...typeof text_effect === "string" && text_effect.trim() ? { textEffect: text_effect.trim() } : {},
|
|
10540
|
+
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
10541
|
+
});
|
|
10542
|
+
return {
|
|
10543
|
+
id: out.id,
|
|
10544
|
+
image_job: out.image_job,
|
|
10545
|
+
note: out.image_job?.status === "queued" ? "In line after the picture being made now; it shows on the post when ready." : "Making it in the background; it shows on the post when ready."
|
|
10546
|
+
};
|
|
10547
|
+
}
|
|
10548
|
+
}),
|
|
10549
|
+
tool({
|
|
10550
|
+
name: "social_images_fill",
|
|
10551
|
+
description: "A new picture for every social post here that hasn't gone out and has none (or only these ids), made one after another in the background: 1 AI generation each. A LinkedIn and a Facebook post of the same idea share one picture. Says how many are in line, how many already were, and why it stopped early (the line is full, or the day's AI generations ran out). Preview the count first.",
|
|
10552
|
+
parameters: {
|
|
10553
|
+
type: "object",
|
|
10554
|
+
properties: {
|
|
10555
|
+
ids: { type: "array", items: { type: "integer" }, description: "Only these posts (from social_list)" },
|
|
10556
|
+
instruction: { type: "string", description: "What the user wants in every picture, up to 300 characters" }
|
|
10557
|
+
}
|
|
10558
|
+
},
|
|
10559
|
+
group: "social",
|
|
10560
|
+
need: write5,
|
|
10561
|
+
risk: "W2",
|
|
10562
|
+
gates: ["VER", "CR"],
|
|
10563
|
+
run: async (ctx, { ids: ids2, instruction }) => {
|
|
10564
|
+
const out = await ws(ctx).fillSocialImages({
|
|
10565
|
+
...Array.isArray(ids2) && ids2.length ? { item_ids: ids2.map(Number) } : {},
|
|
10566
|
+
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
10567
|
+
});
|
|
10568
|
+
return { queued: out.queued, already: out.skipped, stopped_because: out.message, line: out.image_jobs.length };
|
|
10569
|
+
}
|
|
10570
|
+
}),
|
|
10571
|
+
tool({
|
|
10572
|
+
name: "social_text_effects",
|
|
10573
|
+
description: "The styles for the words on a set of pictures (social_image_new with count 4): id, name, family and a sample picture URL. Use when the user wants to choose how the words look ('bold sticker', 'neon', 'gold').",
|
|
9735
10574
|
parameters: none,
|
|
9736
10575
|
group: "social",
|
|
9737
10576
|
need: read5,
|
|
9738
10577
|
risk: "R0",
|
|
9739
10578
|
run: async (ctx) => {
|
|
9740
|
-
const
|
|
10579
|
+
const out = await ws(ctx).socialTextEffects();
|
|
10580
|
+
return { effects: out.effects.map(({ id, name, family }) => ({ id, name, family })), auto: out.auto };
|
|
10581
|
+
}
|
|
10582
|
+
}),
|
|
10583
|
+
tool({
|
|
10584
|
+
name: "social_look",
|
|
10585
|
+
description: "The brand's picture look on this website (made the first time it's asked for): the 3 picture styles Maggie takes turns with, the 5 text styles she picks from per picture, two fonts, the colours (ink, paper, primary, accent, muted), a short description of how its photos look, and things never to show; plus every style, font and text style that can be chosen. Read it before changing the look or when the user asks how their pictures look.",
|
|
10586
|
+
parameters: none,
|
|
10587
|
+
group: "social",
|
|
10588
|
+
need: read5,
|
|
10589
|
+
risk: "R0",
|
|
10590
|
+
run: async (ctx) => {
|
|
10591
|
+
const l = await ws(ctx).socialLook();
|
|
9741
10592
|
return {
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
name:
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
started: c.started,
|
|
9755
|
-
start_date: c.start_date,
|
|
9756
|
-
daily_limit: c.daily_limit
|
|
9757
|
-
})),
|
|
9758
|
-
posted: s.posted.slice(0, 5).map(item)
|
|
10593
|
+
styles: l.styles.map((s) => ({ id: s.id, name: s.name, about: s.about })),
|
|
10594
|
+
text_effects: l.text_effects.map((e) => ({ id: e.id, name: e.name, family: e.family })),
|
|
10595
|
+
fonts: l.fonts,
|
|
10596
|
+
colours: l.colours,
|
|
10597
|
+
description: l.brand_suffix,
|
|
10598
|
+
never: l.never,
|
|
10599
|
+
why: l.suggested.reason ?? null,
|
|
10600
|
+
can_choose: {
|
|
10601
|
+
styles: l.catalogue.styles.map((s) => ({ id: s.id, name: s.name, led: s.led, needs_photo: s.needs_photo })),
|
|
10602
|
+
fonts: l.catalogue.fonts,
|
|
10603
|
+
text_effect_families: [...new Set(l.catalogue.text_effects.map((e) => e.family))]
|
|
10604
|
+
}
|
|
9759
10605
|
};
|
|
9760
10606
|
}
|
|
9761
10607
|
}),
|
|
9762
10608
|
tool({
|
|
9763
|
-
name: "
|
|
9764
|
-
description: "Change the text of
|
|
10609
|
+
name: "social_look_update",
|
|
10610
|
+
description: "Change the brand's picture look: the 3 styles Maggie takes turns with (style_ids, 1-3), the text styles (text_effect_ids, 1-5, ids from social_text_effects), fonts, colours (hex), the description of how photos look, things never to show, or reset: true (back to Maggie's suggestion). New pictures use it; pictures already made keep theirs (social_picture_edit changes one). Preview the change first.",
|
|
9765
10611
|
parameters: {
|
|
9766
10612
|
type: "object",
|
|
9767
|
-
properties: {
|
|
9768
|
-
|
|
10613
|
+
properties: {
|
|
10614
|
+
style_ids: { type: "array", items: { type: "string" }, description: "1-3 picture style ids" },
|
|
10615
|
+
text_effect_ids: { type: "array", items: { type: "string" }, description: "1-5 text style ids" },
|
|
10616
|
+
font_display: { type: "string", description: "Font for big words (from social_look can_choose.fonts)" },
|
|
10617
|
+
font_body: { type: "string", description: "Font for lines and lists" },
|
|
10618
|
+
colours: {
|
|
10619
|
+
type: "object",
|
|
10620
|
+
description: "Any of these as #RRGGBB",
|
|
10621
|
+
properties: {
|
|
10622
|
+
ink: { type: "string", description: "Text on paper" },
|
|
10623
|
+
paper: { type: "string", description: "The light ground" },
|
|
10624
|
+
primary: { type: "string", description: "The brand colour" },
|
|
10625
|
+
accent: { type: "string", description: "A second colour" },
|
|
10626
|
+
muted: { type: "string", description: "Small text" }
|
|
10627
|
+
}
|
|
10628
|
+
},
|
|
10629
|
+
description: { type: "string", description: "How the brand's photos look, up to 600 characters" },
|
|
10630
|
+
never: { type: "array", items: { type: "string" }, description: "Things never to show (the whole list)" },
|
|
10631
|
+
reset: { type: "boolean", description: "Back to Maggie's suggested look" }
|
|
10632
|
+
}
|
|
9769
10633
|
},
|
|
9770
10634
|
group: "social",
|
|
9771
10635
|
need: write5,
|
|
9772
10636
|
risk: "W1",
|
|
9773
10637
|
gates: ["VER"],
|
|
9774
|
-
run: async (ctx,
|
|
9775
|
-
const
|
|
9776
|
-
|
|
10638
|
+
run: async (ctx, a) => {
|
|
10639
|
+
const fonts = {
|
|
10640
|
+
...a.font_display ? { display: a.font_display } : {},
|
|
10641
|
+
...a.font_body ? { body: a.font_body } : {}
|
|
10642
|
+
};
|
|
10643
|
+
const l = await ws(ctx).updateSocialLook({
|
|
10644
|
+
...a.style_ids ? { style_ids: a.style_ids } : {},
|
|
10645
|
+
...a.text_effect_ids ? { text_effect_ids: a.text_effect_ids } : {},
|
|
10646
|
+
...Object.keys(fonts).length ? { fonts } : {},
|
|
10647
|
+
...a.colours ? { colours: a.colours } : {},
|
|
10648
|
+
...typeof a.description === "string" ? { brand_suffix: a.description } : {},
|
|
10649
|
+
...a.never ? { never: a.never } : {},
|
|
10650
|
+
...a.reset ? { reset: true } : {}
|
|
10651
|
+
});
|
|
10652
|
+
return {
|
|
10653
|
+
styles: l.styles.map((s) => s.id),
|
|
10654
|
+
text_effects: l.text_effects.map((e) => e.id),
|
|
10655
|
+
fonts: l.fonts,
|
|
10656
|
+
colours: l.colours
|
|
10657
|
+
};
|
|
9777
10658
|
}
|
|
9778
10659
|
}),
|
|
9779
10660
|
tool({
|
|
9780
|
-
name: "
|
|
9781
|
-
description: "
|
|
10661
|
+
name: "social_picture_edit",
|
|
10662
|
+
description: "Change a picture Maggie made for a post that hasn't gone out, without making a new one (free, a few seconds): the words on each picture (title up to 60 characters, line up to 90), its picture style, its text style, or for a set the order (indexes, 0 = first) or pictures to drop. Only posts whose picture says editable in social_list; a photo style needs the picture's photos (has_photos). Changing the words of one picture: send every picture's words in order. Preview first.",
|
|
9782
10663
|
parameters: {
|
|
9783
10664
|
type: "object",
|
|
9784
|
-
properties: {
|
|
9785
|
-
|
|
10665
|
+
properties: {
|
|
10666
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10667
|
+
words: {
|
|
10668
|
+
type: "array",
|
|
10669
|
+
items: {
|
|
10670
|
+
type: "object",
|
|
10671
|
+
properties: { headline: { type: "string" }, sub: { type: "string" } },
|
|
10672
|
+
required: ["headline"]
|
|
10673
|
+
},
|
|
10674
|
+
description: "The words on each picture, in order (in the new order when order is given)"
|
|
10675
|
+
},
|
|
10676
|
+
style: { type: "string", description: "A picture style id from social_look" },
|
|
10677
|
+
text_effect: { type: "string", description: "A text style id from social_text_effects" },
|
|
10678
|
+
order: { type: "array", items: { type: "integer" }, description: "The pictures in their new order" },
|
|
10679
|
+
remove: { type: "array", items: { type: "integer" }, description: "Pictures to drop from a set" }
|
|
10680
|
+
},
|
|
10681
|
+
required: ["id"]
|
|
9786
10682
|
},
|
|
9787
10683
|
group: "social",
|
|
9788
10684
|
need: write5,
|
|
9789
|
-
risk: "
|
|
9790
|
-
gates: ["VER"
|
|
9791
|
-
run: async (ctx, {
|
|
9792
|
-
const
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
} catch (e) {
|
|
9801
|
-
results.push({ id: n, ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
9802
|
-
}
|
|
9803
|
-
}
|
|
9804
|
-
return { approved: results.filter((r) => r.ok).length, results };
|
|
10685
|
+
risk: "W1",
|
|
10686
|
+
gates: ["VER"],
|
|
10687
|
+
run: async (ctx, { id, words, style, text_effect, order, remove: remove2 }) => {
|
|
10688
|
+
const out = await ws(ctx).editSocialPicture(Number(id), {
|
|
10689
|
+
...Array.isArray(words) ? { words: words.map((w) => ({ headline: String(w.headline ?? ""), sub: String(w.sub ?? "") })) } : {},
|
|
10690
|
+
...style ? { style } : {},
|
|
10691
|
+
...text_effect ? { text_effect } : {},
|
|
10692
|
+
...Array.isArray(order) ? { order: order.map(Number) } : {},
|
|
10693
|
+
...Array.isArray(remove2) ? { remove: remove2.map(Number) } : {}
|
|
10694
|
+
});
|
|
10695
|
+
return { id: out.id, pictures: out.image_urls?.length ? out.image_urls : [out.image_url], picture: out.picture };
|
|
9805
10696
|
}
|
|
9806
10697
|
}),
|
|
9807
10698
|
tool({
|
|
9808
|
-
name: "
|
|
9809
|
-
description: "
|
|
9810
|
-
parameters: {
|
|
10699
|
+
name: "social_photo_words",
|
|
10700
|
+
description: "Put a title and a line on the user's own photo(s) on a post, in the brand's look: Maggie writes the words from the post (no AI picture, free). Only for a post whose picture is the user's own photo (own_photo in social_list). style: real_photo (default, the photo framed with the words below), product_hero (the words on a card), or another photo style. Then social_picture_edit changes the words. Preview first.",
|
|
10701
|
+
parameters: {
|
|
10702
|
+
type: "object",
|
|
10703
|
+
properties: {
|
|
10704
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10705
|
+
style: {
|
|
10706
|
+
type: "string",
|
|
10707
|
+
description: "real_photo (default), product_hero, photo_headline, editorial, duotone"
|
|
10708
|
+
}
|
|
10709
|
+
},
|
|
10710
|
+
required: ["id"]
|
|
10711
|
+
},
|
|
9811
10712
|
group: "social",
|
|
9812
10713
|
need: write5,
|
|
9813
10714
|
risk: "W1",
|
|
9814
10715
|
gates: ["VER"],
|
|
9815
|
-
run: async (ctx, { id }) =>
|
|
10716
|
+
run: async (ctx, { id, style }) => {
|
|
10717
|
+
const out = await ws(ctx).wordsOnPhoto(Number(id), style || "real_photo");
|
|
10718
|
+
return { id: out.id, picture: out.picture, pictures: out.image_urls?.length ? out.image_urls : [out.image_url] };
|
|
10719
|
+
}
|
|
9816
10720
|
}),
|
|
9817
10721
|
tool({
|
|
9818
|
-
name: "
|
|
9819
|
-
description: "
|
|
9820
|
-
parameters: {
|
|
10722
|
+
name: "social_post_insights",
|
|
10723
|
+
description: "How a posted Instagram post is doing: views, reach, likes, comments, saves and shares, read from Instagram (kept 15 minutes; refresh: true reads again). Saves and shares per reach are the best signs of a good post. available: false says why (not posted yet, deleted on Instagram, the account needs reconnecting).",
|
|
10724
|
+
parameters: {
|
|
10725
|
+
type: "object",
|
|
10726
|
+
properties: {
|
|
10727
|
+
id: { type: "integer", description: "A posted Instagram post (from social_list posted)" },
|
|
10728
|
+
refresh: { type: "boolean", description: "Read the numbers again now" }
|
|
10729
|
+
},
|
|
10730
|
+
required: ["id"]
|
|
10731
|
+
},
|
|
10732
|
+
group: "social",
|
|
10733
|
+
need: read5,
|
|
10734
|
+
risk: "R0",
|
|
10735
|
+
run: async (ctx, { id, refresh }) => ws(ctx).socialInsights(Number(id), refresh === true)
|
|
10736
|
+
}),
|
|
10737
|
+
tool({
|
|
10738
|
+
name: "social_write_status",
|
|
10739
|
+
description: "Posts Maggie is writing in the background for this user on this website (from social_from_article, social_plan_days, social_idea_write or the app), and the ones finished in the last 10 minutes: running, done (the new post ids) or failed (why), and any channel that couldn't be written. Look once when the user asks, not in a loop.",
|
|
10740
|
+
parameters: none,
|
|
10741
|
+
group: "social",
|
|
10742
|
+
need: read5,
|
|
10743
|
+
risk: "R0",
|
|
10744
|
+
run: async (ctx) => ({
|
|
10745
|
+
jobs: ((await ws(ctx).social()).write_jobs ?? []).map((j) => ({
|
|
10746
|
+
id: j.id,
|
|
10747
|
+
kind: j.kind,
|
|
10748
|
+
status: j.status,
|
|
10749
|
+
channels: j.channels,
|
|
10750
|
+
expected: j.expected,
|
|
10751
|
+
written: j.item_ids,
|
|
10752
|
+
queued: j.queued,
|
|
10753
|
+
...j.failed?.length ? { failed: j.failed } : {},
|
|
10754
|
+
...j.error ? { error: j.error } : {},
|
|
10755
|
+
started_at: j.started_at
|
|
10756
|
+
}))
|
|
10757
|
+
})
|
|
10758
|
+
}),
|
|
10759
|
+
tool({
|
|
10760
|
+
name: "social_image_status",
|
|
10761
|
+
description: "The picture being made on this website and those waiting in line (which post, new or edit), or how the last one ended (done, failed with why, stopped) and who drew it (chatgpt or gemini).",
|
|
10762
|
+
parameters: none,
|
|
10763
|
+
group: "social",
|
|
10764
|
+
need: read5,
|
|
10765
|
+
risk: "R0",
|
|
10766
|
+
run: async (ctx) => {
|
|
10767
|
+
const s = await ws(ctx).social();
|
|
10768
|
+
return { image_job: s.image_job ?? null, line: s.image_jobs ?? [] };
|
|
10769
|
+
}
|
|
10770
|
+
}),
|
|
10771
|
+
tool({
|
|
10772
|
+
name: "social_image_stop",
|
|
10773
|
+
description: "Stop the picture being made on this website (the post keeps the picture it had; the next in line starts). all: true also empties the line.",
|
|
10774
|
+
parameters: { type: "object", properties: { all: { type: "boolean" } } },
|
|
9821
10775
|
group: "social",
|
|
9822
10776
|
need: write5,
|
|
9823
10777
|
risk: "W1",
|
|
9824
10778
|
gates: ["VER"],
|
|
9825
|
-
run: async (ctx, {
|
|
10779
|
+
run: async (ctx, { all }) => ws(ctx).stopSocialImage(all === true)
|
|
9826
10780
|
}),
|
|
9827
10781
|
tool({
|
|
9828
10782
|
name: "social_move",
|
|
@@ -9840,16 +10794,24 @@ var socialTools = [
|
|
|
9840
10794
|
}),
|
|
9841
10795
|
tool({
|
|
9842
10796
|
name: "social_set_paused",
|
|
9843
|
-
description: "Pause or resume
|
|
9844
|
-
parameters: {
|
|
10797
|
+
description: "Pause or resume the user's social posting: one channel (linkedin or facebook), or every channel when no channel is given.",
|
|
10798
|
+
parameters: {
|
|
10799
|
+
type: "object",
|
|
10800
|
+
properties: { paused: { type: "boolean" }, channel: { type: "string", enum: CHANNEL_KEYS } },
|
|
10801
|
+
required: ["paused"]
|
|
10802
|
+
},
|
|
9845
10803
|
group: "social",
|
|
9846
10804
|
need: write5,
|
|
9847
10805
|
risk: "W1",
|
|
9848
|
-
run: async (ctx, { paused }) =>
|
|
10806
|
+
run: async (ctx, { paused, channel }) => {
|
|
10807
|
+
const key = channelsOf(channel ? [channel] : [])?.[0];
|
|
10808
|
+
if (key) return ws(ctx).setChannelPaused(key, Boolean(paused));
|
|
10809
|
+
return paused ? ws(ctx).pauseSocial() : ws(ctx).resumeSocial();
|
|
10810
|
+
}
|
|
9849
10811
|
}),
|
|
9850
10812
|
tool({
|
|
9851
10813
|
name: "social_sources_list",
|
|
9852
|
-
description: "Published articles a
|
|
10814
|
+
description: "Published articles a social post can be made from (optionally matching a search).",
|
|
9853
10815
|
parameters: { type: "object", properties: { search: { type: "string" } } },
|
|
9854
10816
|
group: "social",
|
|
9855
10817
|
need: read5,
|
|
@@ -9858,29 +10820,31 @@ var socialTools = [
|
|
|
9858
10820
|
}),
|
|
9859
10821
|
tool({
|
|
9860
10822
|
name: "social_from_article",
|
|
9861
|
-
description: "Maggie writes a
|
|
10823
|
+
description: "Maggie writes a post about one published article (id from social_sources_list) for each channel. It waits for the user's OK as a draft, unless their approval mode is auto (then it goes to the front of the queue). Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
|
|
9862
10824
|
parameters: {
|
|
9863
10825
|
type: "object",
|
|
9864
|
-
properties: {
|
|
9865
|
-
|
|
10826
|
+
properties: {
|
|
10827
|
+
source_id: { type: "string" },
|
|
10828
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
10829
|
+
channels: channelsParam
|
|
10830
|
+
},
|
|
10831
|
+
required: ["source_id", "title"]
|
|
9866
10832
|
},
|
|
9867
10833
|
group: "social",
|
|
9868
10834
|
need: write5,
|
|
9869
10835
|
risk: "W2",
|
|
9870
10836
|
gates: ["VER", "LI", "LLM"],
|
|
9871
|
-
run: async (ctx, { source_id }) =>
|
|
9872
|
-
const out = await ws(ctx).socialFromPost(String(source_id));
|
|
9873
|
-
return { id: out.id, status: out.status, text: clip(out.social_copy, 300) };
|
|
9874
|
-
}
|
|
10837
|
+
run: async (ctx, { source_id, channels }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels))).job)
|
|
9875
10838
|
}),
|
|
9876
10839
|
tool({
|
|
9877
10840
|
name: "social_plan_days",
|
|
9878
|
-
description: "Maggie writes
|
|
10841
|
+
description: "Maggie writes posts for the next 1\u20137 days from the best recent articles, on each channel. They wait for the user's OK as drafts, unless the user's approval mode is auto (then they are queued in order). Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
|
|
9879
10842
|
parameters: {
|
|
9880
10843
|
type: "object",
|
|
9881
10844
|
properties: {
|
|
9882
10845
|
days: { type: "integer", minimum: 1, maximum: 7 },
|
|
9883
|
-
timezone: { type: "string", description: "IANA timezone, if known" }
|
|
10846
|
+
timezone: { type: "string", description: "IANA timezone, if known" },
|
|
10847
|
+
channels: channelsParam
|
|
9884
10848
|
},
|
|
9885
10849
|
required: ["days"]
|
|
9886
10850
|
},
|
|
@@ -9888,10 +10852,7 @@ var socialTools = [
|
|
|
9888
10852
|
need: write5,
|
|
9889
10853
|
risk: "W2",
|
|
9890
10854
|
gates: ["VER", "LI", "LLM"],
|
|
9891
|
-
run: async (ctx, { days, timezone }) =>
|
|
9892
|
-
const out = await ws(ctx).socialPlan(int(days, 1, 7, 3), timezone || "UTC");
|
|
9893
|
-
return { written: out.items.length, items: out.items.map(item) };
|
|
9894
|
-
}
|
|
10855
|
+
run: async (ctx, { days, timezone, channels }) => writing((await ws(ctx).startSocialPlan(int(days, 1, 7, 3), timezone || "UTC", channelsOf(channels))).job)
|
|
9895
10856
|
}),
|
|
9896
10857
|
tool({
|
|
9897
10858
|
name: "social_ideas_list",
|
|
@@ -9919,21 +10880,21 @@ var socialTools = [
|
|
|
9919
10880
|
}),
|
|
9920
10881
|
tool({
|
|
9921
10882
|
name: "social_idea_add",
|
|
9922
|
-
description: "Add an idea for a
|
|
10883
|
+
description: "Add an idea for a social post (not written yet; writing it later uses a credit per channel).",
|
|
9923
10884
|
parameters: {
|
|
9924
10885
|
type: "object",
|
|
9925
|
-
properties: { title: { type: "string" }, summary: { type: "string" } },
|
|
10886
|
+
properties: { title: { type: "string" }, summary: { type: "string" }, channels: channelsParam },
|
|
9926
10887
|
required: ["title"]
|
|
9927
10888
|
},
|
|
9928
10889
|
group: "social",
|
|
9929
10890
|
need: write5,
|
|
9930
10891
|
risk: "W1",
|
|
9931
10892
|
gates: ["VER", "LI"],
|
|
9932
|
-
run: async (ctx, idea) => ws(ctx).addSocialIdea(idea)
|
|
10893
|
+
run: async (ctx, { channels, ...idea }) => ws(ctx).addSocialIdea({ ...idea, channels: channelsOf(channels) })
|
|
9933
10894
|
}),
|
|
9934
10895
|
tool({
|
|
9935
10896
|
name: "social_idea_update",
|
|
9936
|
-
description: "Change a
|
|
10897
|
+
description: "Change a social idea's title or summary.",
|
|
9937
10898
|
parameters: {
|
|
9938
10899
|
type: "object",
|
|
9939
10900
|
properties: { id: { type: "integer" }, title: { type: "string" }, summary: { type: "string" } },
|
|
@@ -9947,11 +10908,14 @@ var socialTools = [
|
|
|
9947
10908
|
}),
|
|
9948
10909
|
tool({
|
|
9949
10910
|
name: "social_idea_delete",
|
|
9950
|
-
description: "Delete a
|
|
10911
|
+
description: "Delete a social idea that hasn't been written.",
|
|
9951
10912
|
parameters: {
|
|
9952
10913
|
type: "object",
|
|
9953
|
-
properties: {
|
|
9954
|
-
|
|
10914
|
+
properties: {
|
|
10915
|
+
id: { type: "integer" },
|
|
10916
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10917
|
+
},
|
|
10918
|
+
required: ["id", "title"]
|
|
9955
10919
|
},
|
|
9956
10920
|
group: "social",
|
|
9957
10921
|
need: write5,
|
|
@@ -9961,21 +10925,79 @@ var socialTools = [
|
|
|
9961
10925
|
}),
|
|
9962
10926
|
tool({
|
|
9963
10927
|
name: "social_idea_write",
|
|
9964
|
-
description: "Maggie writes
|
|
10928
|
+
description: "Maggie writes one idea's post for each channel (1 credit per post). The user's own idea's posts wait for their OK (queued straight away only in approval mode auto); a campaign's wait for the campaign to start. Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
|
|
9965
10929
|
parameters: {
|
|
9966
10930
|
type: "object",
|
|
9967
|
-
properties: {
|
|
9968
|
-
|
|
10931
|
+
properties: {
|
|
10932
|
+
id: { type: "integer" },
|
|
10933
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
10934
|
+
channels: channelsParam
|
|
10935
|
+
},
|
|
10936
|
+
required: ["id", "title"]
|
|
9969
10937
|
},
|
|
9970
10938
|
group: "social",
|
|
9971
10939
|
need: write5,
|
|
9972
10940
|
risk: "W2",
|
|
9973
10941
|
gates: ["VER", "LI", "CR", "LLM"],
|
|
9974
|
-
run: async (ctx, { id }) => ws(ctx).
|
|
10942
|
+
run: async (ctx, { id, channels }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels))).job)
|
|
10943
|
+
}),
|
|
10944
|
+
tool({
|
|
10945
|
+
name: "social_idea_brief",
|
|
10946
|
+
description: "For an assistant that writes with its own model: what Maggie would write one idea's posts from, one per connected channel (the instructions, the brand's voice and template, each channel's length limit and the JSON shape). Write the posts yourself, then save them with social_idea_save. Free.",
|
|
10947
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10948
|
+
group: "social",
|
|
10949
|
+
need: read5,
|
|
10950
|
+
risk: "R0",
|
|
10951
|
+
assistantWrites: true,
|
|
10952
|
+
run: async (ctx, { id }) => {
|
|
10953
|
+
const b = await ws(ctx).socialIdeaBrief(Number(id));
|
|
10954
|
+
return {
|
|
10955
|
+
id: Number(id),
|
|
10956
|
+
channels: b.platforms,
|
|
10957
|
+
limits: b.limits,
|
|
10958
|
+
limit: b.limits.linkedin ?? null,
|
|
10959
|
+
json_schema: b.json_schema,
|
|
10960
|
+
// Built by CMO.so from the user's idea, brand voice and template.
|
|
10961
|
+
brief: b.prompt
|
|
10962
|
+
};
|
|
10963
|
+
}
|
|
10964
|
+
}),
|
|
10965
|
+
tool({
|
|
10966
|
+
name: "social_idea_save",
|
|
10967
|
+
description: "Save the posts you wrote from social_idea_brief (1 credit per post, like Maggie writing them). They get the same checks, links and limits. The user's own ideas go to the queue; a campaign's wait for the campaign to start.",
|
|
10968
|
+
parameters: {
|
|
10969
|
+
type: "object",
|
|
10970
|
+
properties: {
|
|
10971
|
+
id: { type: "integer" },
|
|
10972
|
+
posts: {
|
|
10973
|
+
type: "object",
|
|
10974
|
+
description: "One body per channel from the brief (only the channels you wrote)",
|
|
10975
|
+
properties: { linkedin: { type: "string" }, facebook: { type: "string" } }
|
|
10976
|
+
},
|
|
10977
|
+
text: { type: "string", description: "Only a LinkedIn post: its body (older use; prefer posts)" },
|
|
10978
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10979
|
+
},
|
|
10980
|
+
required: ["id", "title"]
|
|
10981
|
+
},
|
|
10982
|
+
group: "social",
|
|
10983
|
+
need: write5,
|
|
10984
|
+
risk: "W2",
|
|
10985
|
+
gates: ["VER", "LI", "CR"],
|
|
10986
|
+
assistantWrites: true,
|
|
10987
|
+
run: async (ctx, { id, text: text3, posts }) => {
|
|
10988
|
+
const written = {};
|
|
10989
|
+
for (const [channel, body] of Object.entries(posts ?? {})) {
|
|
10990
|
+
if (CHANNEL_KEYS.includes(channel) && String(body ?? "").trim())
|
|
10991
|
+
written[channel] = { body: String(body).trim() };
|
|
10992
|
+
}
|
|
10993
|
+
if (!Object.keys(written).length && String(text3 ?? "").trim()) written.linkedin = { body: String(text3).trim() };
|
|
10994
|
+
if (!Object.keys(written).length) throw new Error("Write the posts first.");
|
|
10995
|
+
return ws(ctx).writeSocialIdea(Number(id), written);
|
|
10996
|
+
}
|
|
9975
10997
|
}),
|
|
9976
10998
|
tool({
|
|
9977
10999
|
name: "campaign_list",
|
|
9978
|
-
description: "The user's
|
|
11000
|
+
description: "The user's social campaigns: ideas written so far, whether ready or started, start day and posts a day.",
|
|
9979
11001
|
parameters: none,
|
|
9980
11002
|
group: "social",
|
|
9981
11003
|
need: read5,
|
|
@@ -9984,16 +11006,16 @@ var socialTools = [
|
|
|
9984
11006
|
}),
|
|
9985
11007
|
tool({
|
|
9986
11008
|
name: "campaign_start",
|
|
9987
|
-
description: "Start a ready campaign (every idea written): its posts go out on
|
|
11009
|
+
description: "Start a ready campaign (every idea written): its posts go out on its channels in order from start_date (default today), at most daily_limit a day.",
|
|
9988
11010
|
parameters: {
|
|
9989
11011
|
type: "object",
|
|
9990
11012
|
properties: {
|
|
9991
11013
|
id: { type: "integer" },
|
|
9992
11014
|
start_date: { type: "string", description: "YYYY-MM-DD" },
|
|
9993
11015
|
daily_limit: { type: "integer", minimum: 1, maximum: 5 },
|
|
9994
|
-
name: { type: "string", description: "
|
|
11016
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9995
11017
|
},
|
|
9996
|
-
required: ["id"]
|
|
11018
|
+
required: ["id", "name"]
|
|
9997
11019
|
},
|
|
9998
11020
|
group: "social",
|
|
9999
11021
|
need: write5,
|
|
@@ -10041,13 +11063,65 @@ var socialTools = [
|
|
|
10041
11063
|
gates: ["VER"],
|
|
10042
11064
|
run: async (ctx, c) => ws(ctx).createCampaign(c)
|
|
10043
11065
|
}),
|
|
11066
|
+
tool({
|
|
11067
|
+
name: "social_approve_all",
|
|
11068
|
+
description: "Approve every post waiting for the user's OK (not a series that hasn't been approved), on one channel or all: they go out one by one at each channel's next posting times.",
|
|
11069
|
+
parameters: { type: "object", properties: { channel: { type: "string", enum: CHANNEL_KEYS } } },
|
|
11070
|
+
group: "social",
|
|
11071
|
+
need: write5,
|
|
11072
|
+
risk: "W3",
|
|
11073
|
+
gates: ["VER", "LI"],
|
|
11074
|
+
run: async (ctx, { channel }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0])
|
|
11075
|
+
}),
|
|
11076
|
+
tool({
|
|
11077
|
+
name: "series_create",
|
|
11078
|
+
description: "Maggie plans and writes a series: 1-7 posts on one theme (brief) that read as a series, for each channel. They are ordinary posts: each waits in Review for the user's OK (social_approve), labelled with the series. Uses one AI generation a post.",
|
|
11079
|
+
parameters: {
|
|
11080
|
+
type: "object",
|
|
11081
|
+
properties: {
|
|
11082
|
+
brief: { type: "string", description: "What the series is about, in the user's words" },
|
|
11083
|
+
count: { type: "integer", minimum: 1, maximum: 7, description: "How many posts (3 if not said)" },
|
|
11084
|
+
channels: channelsParam
|
|
11085
|
+
},
|
|
11086
|
+
required: ["brief"]
|
|
11087
|
+
},
|
|
11088
|
+
group: "social",
|
|
11089
|
+
need: write5,
|
|
11090
|
+
risk: "W2",
|
|
11091
|
+
gates: ["VER", "LI", "LLM"],
|
|
11092
|
+
run: async (ctx, { brief: brief2, count, channels }) => ws(ctx).createSocialSeries({
|
|
11093
|
+
brief: String(brief2 ?? ""),
|
|
11094
|
+
count: Math.max(1, Math.min(7, Number(count) || 3)),
|
|
11095
|
+
channels: channelsOf(channels)
|
|
11096
|
+
})
|
|
11097
|
+
}),
|
|
11098
|
+
tool({
|
|
11099
|
+
name: "series_approve",
|
|
11100
|
+
description: "Approve a written series: its posts go out in order at the user's posting times on each channel (their own posts go first).",
|
|
11101
|
+
parameters: {
|
|
11102
|
+
type: "object",
|
|
11103
|
+
properties: {
|
|
11104
|
+
id: { type: "integer" },
|
|
11105
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
11106
|
+
},
|
|
11107
|
+
required: ["id"]
|
|
11108
|
+
},
|
|
11109
|
+
group: "social",
|
|
11110
|
+
need: write5,
|
|
11111
|
+
risk: "W3",
|
|
11112
|
+
gates: ["VER", "LI"],
|
|
11113
|
+
run: async (ctx, { id }) => ws(ctx).approveSocialSeries(Number(id))
|
|
11114
|
+
}),
|
|
10044
11115
|
tool({
|
|
10045
11116
|
name: "campaign_plan",
|
|
10046
11117
|
description: "Maggie plans a campaign's LinkedIn post ideas with AI (uses 1 credit).",
|
|
10047
11118
|
parameters: {
|
|
10048
11119
|
type: "object",
|
|
10049
|
-
properties: {
|
|
10050
|
-
|
|
11120
|
+
properties: {
|
|
11121
|
+
id: { type: "integer" },
|
|
11122
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
11123
|
+
},
|
|
11124
|
+
required: ["id", "name"]
|
|
10051
11125
|
},
|
|
10052
11126
|
group: "social",
|
|
10053
11127
|
need: write5,
|
|
@@ -10066,9 +11140,9 @@ var socialTools = [
|
|
|
10066
11140
|
properties: {
|
|
10067
11141
|
id: { type: "integer" },
|
|
10068
11142
|
paused: { type: "boolean" },
|
|
10069
|
-
name: { type: "string", description: "
|
|
11143
|
+
name: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
10070
11144
|
},
|
|
10071
|
-
required: ["id", "paused"]
|
|
11145
|
+
required: ["id", "paused", "name"]
|
|
10072
11146
|
},
|
|
10073
11147
|
group: "social",
|
|
10074
11148
|
need: write5,
|
|
@@ -10162,9 +11236,9 @@ var socialTools = [
|
|
|
10162
11236
|
run: async (ctx, { keys: keys2, ...options }) => {
|
|
10163
11237
|
const api2 = ws(ctx);
|
|
10164
11238
|
const current = (await api2.socialSetup()).writing;
|
|
10165
|
-
const
|
|
10166
|
-
for (const [k, v] of Object.entries(options)) if (v !== void 0)
|
|
10167
|
-
return api2.socialSetupTemplates((keys2 ?? []).slice(0, 10),
|
|
11239
|
+
const writing2 = { ...current };
|
|
11240
|
+
for (const [k, v] of Object.entries(options)) if (v !== void 0) writing2[k] = v;
|
|
11241
|
+
return api2.socialSetupTemplates((keys2 ?? []).slice(0, 10), writing2);
|
|
10168
11242
|
}
|
|
10169
11243
|
}),
|
|
10170
11244
|
tool({
|
|
@@ -10232,6 +11306,65 @@ var socialTools = [
|
|
|
10232
11306
|
const out = await ws(ctx).connectLinkedIn("close");
|
|
10233
11307
|
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10234
11308
|
}
|
|
11309
|
+
}),
|
|
11310
|
+
tool({
|
|
11311
|
+
name: "facebook_connect_link",
|
|
11312
|
+
description: "A link for the user to connect a Facebook Page (or reconnect). Give them the link to open; never ask for their Facebook password. If they manage several Pages they pick this website's one in the app afterwards.",
|
|
11313
|
+
parameters: none,
|
|
11314
|
+
group: "social",
|
|
11315
|
+
need: write5,
|
|
11316
|
+
risk: "W1",
|
|
11317
|
+
gates: ["VER"],
|
|
11318
|
+
run: async (ctx) => {
|
|
11319
|
+
const out = await ws(ctx).connectFacebook("close");
|
|
11320
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11321
|
+
}
|
|
11322
|
+
}),
|
|
11323
|
+
tool({
|
|
11324
|
+
name: "google_business_connect_link",
|
|
11325
|
+
description: "A link for the user to connect their Google Business Profile (or reconnect): they sign in with the Google account that manages the profile; every profile it can post to is saved and the one whose website is this site is picked (else they pick it in the app). Only verified profiles can post. Give them the link to open; never ask for a password. Google posts are short local updates with one photo and a button; Google checks each one before it shows.",
|
|
11326
|
+
parameters: none,
|
|
11327
|
+
group: "social",
|
|
11328
|
+
need: write5,
|
|
11329
|
+
risk: "W1",
|
|
11330
|
+
gates: ["VER"],
|
|
11331
|
+
run: async (ctx) => {
|
|
11332
|
+
const out = await ws(ctx).connectGoogleBusiness("close");
|
|
11333
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11334
|
+
}
|
|
11335
|
+
}),
|
|
11336
|
+
tool({
|
|
11337
|
+
name: "social_google_button",
|
|
11338
|
+
description: "Set the button on a Google Business post that hasn't gone out: LEARN_MORE (default, opens the post's link), BOOK (a booking link), ORDER, SHOP, SIGN_UP, or CALL (calls the profile's phone number; needs no link). Pick the one that matches what the post asks people to do. Preview first.",
|
|
11339
|
+
parameters: {
|
|
11340
|
+
type: "object",
|
|
11341
|
+
properties: {
|
|
11342
|
+
id: { type: "integer", description: "A Google Business post (from social_list)" },
|
|
11343
|
+
button: { type: "string", enum: ["LEARN_MORE", "BOOK", "ORDER", "SHOP", "SIGN_UP", "CALL"] }
|
|
11344
|
+
},
|
|
11345
|
+
required: ["id", "button"]
|
|
11346
|
+
},
|
|
11347
|
+
group: "social",
|
|
11348
|
+
need: write5,
|
|
11349
|
+
risk: "W1",
|
|
11350
|
+
gates: ["VER"],
|
|
11351
|
+
run: async (ctx, { id, button }) => {
|
|
11352
|
+
const out = await ws(ctx).setSocialButton(Number(id), button);
|
|
11353
|
+
return { id: out.id, button: out.cta_type ?? "LEARN_MORE", link: out.cta_url ?? null };
|
|
11354
|
+
}
|
|
11355
|
+
}),
|
|
11356
|
+
tool({
|
|
11357
|
+
name: "instagram_connect_link",
|
|
11358
|
+
description: "A link for the user to connect Instagram (or reconnect). Instagram connects through the Facebook sign-in: only an Instagram professional account (Business or Creator) linked to one of their Facebook Pages can be posted to, so tell them that first. Give them the link to open; never ask for a password. Every Instagram post goes out with its picture (no links in it).",
|
|
11359
|
+
parameters: none,
|
|
11360
|
+
group: "social",
|
|
11361
|
+
need: write5,
|
|
11362
|
+
risk: "W1",
|
|
11363
|
+
gates: ["VER"],
|
|
11364
|
+
run: async (ctx) => {
|
|
11365
|
+
const out = await ws(ctx).connectInstagram("close");
|
|
11366
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11367
|
+
}
|
|
10235
11368
|
})
|
|
10236
11369
|
];
|
|
10237
11370
|
|
|
@@ -10382,7 +11515,9 @@ var tools = [
|
|
|
10382
11515
|
...blogTools,
|
|
10383
11516
|
...publishingTools,
|
|
10384
11517
|
...socialTools,
|
|
10385
|
-
...teamTools
|
|
11518
|
+
...teamTools,
|
|
11519
|
+
...memoryTools,
|
|
11520
|
+
...feedbackTools
|
|
10386
11521
|
];
|
|
10387
11522
|
var toolByName = new Map(tools.map((t) => [t.name, t]));
|
|
10388
11523
|
function allowed(t, a) {
|
|
@@ -17441,6 +18576,23 @@ function hmacSigner(secret) {
|
|
|
17441
18576
|
const body = b64(canonical(approval));
|
|
17442
18577
|
return `${body}.${mac(body)}`;
|
|
17443
18578
|
},
|
|
18579
|
+
open(token, principal, now = Date.now()) {
|
|
18580
|
+
if (!token) return { ok: false, reason: "missing" };
|
|
18581
|
+
const [body, sig] = token.split(".");
|
|
18582
|
+
if (!body || !sig) return { ok: false, reason: "bad" };
|
|
18583
|
+
const want = Buffer.from(mac(body));
|
|
18584
|
+
const got = Buffer.from(sig);
|
|
18585
|
+
if (want.length !== got.length || !timingSafeEqual(want, got)) return { ok: false, reason: "bad" };
|
|
18586
|
+
let approval;
|
|
18587
|
+
try {
|
|
18588
|
+
approval = JSON.parse(unb64(body));
|
|
18589
|
+
} catch {
|
|
18590
|
+
return { ok: false, reason: "bad" };
|
|
18591
|
+
}
|
|
18592
|
+
if (!(approval.expires > now)) return { ok: false, reason: "expired" };
|
|
18593
|
+
if (approval.principal !== principal) return { ok: false, reason: "changed" };
|
|
18594
|
+
return { ok: true, approval };
|
|
18595
|
+
},
|
|
17444
18596
|
verify(token, expect, now = Date.now()) {
|
|
17445
18597
|
if (!token) return { ok: false, reason: "missing" };
|
|
17446
18598
|
const [body, sig] = token.split(".");
|
|
@@ -17465,7 +18617,7 @@ var idempotencyKey = (approvalId, call, request) => createHash("sha256").update(
|
|
|
17465
18617
|
|
|
17466
18618
|
// ../../packages/mcp/src/widgets.ts
|
|
17467
18619
|
var WIDGET_MIME = "text/html;profile=mcp-app";
|
|
17468
|
-
var VERSION = "
|
|
18620
|
+
var VERSION = "v2";
|
|
17469
18621
|
var WIDGETS = {
|
|
17470
18622
|
results: {
|
|
17471
18623
|
uri: `ui://widget/results-${VERSION}.html`,
|
|
@@ -17624,7 +18776,7 @@ function show(d) {
|
|
|
17624
18776
|
if (!d) return; data = d;
|
|
17625
18777
|
if (d.workspace) workspace = d.workspace;
|
|
17626
18778
|
const root = document.getElementById("root"); root.textContent = "";
|
|
17627
|
-
root.append(...[VIEW === "results" ? results(d) : VIEW === "posts" ? posts(d) : social(d)].flat());
|
|
18779
|
+
root.append(...[VIEW === "results" ? results(d) : VIEW === "posts" ? posts(d) : social(d)].flat(3));
|
|
17628
18780
|
resize();
|
|
17629
18781
|
}
|
|
17630
18782
|
(async () => {
|
|
@@ -17712,16 +18864,16 @@ function withIdempotency(api2, approvalId) {
|
|
|
17712
18864
|
}
|
|
17713
18865
|
};
|
|
17714
18866
|
}
|
|
17715
|
-
var
|
|
18867
|
+
var text2 = (value) => [
|
|
17716
18868
|
{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value) }
|
|
17717
18869
|
];
|
|
17718
18870
|
function ok(result, extra) {
|
|
17719
18871
|
const value = result ?? { ok: true };
|
|
17720
18872
|
const structured = value && typeof value === "object" && !Array.isArray(value) ? value : { items: value };
|
|
17721
|
-
return { content:
|
|
18873
|
+
return { content: text2(value), structuredContent: { ...structured, ...extra } };
|
|
17722
18874
|
}
|
|
17723
18875
|
function fail(message) {
|
|
17724
|
-
return { content:
|
|
18876
|
+
return { content: text2({ error: message }), isError: true };
|
|
17725
18877
|
}
|
|
17726
18878
|
function errorMessage2(e) {
|
|
17727
18879
|
if (e instanceof SignInRequiredError) return "The sign-in has expired. Reconnect CMO.so and try again.";
|
|
@@ -17755,8 +18907,8 @@ function createMcpServer(deps) {
|
|
|
17755
18907
|
return deps.access ? deps.access(domain, load) : load();
|
|
17756
18908
|
}
|
|
17757
18909
|
async function resolveWorkspace(t, given) {
|
|
17758
|
-
const
|
|
17759
|
-
if (
|
|
18910
|
+
const named2 = typeof given === "string" ? given.trim().toLowerCase() : "";
|
|
18911
|
+
if (named2) return named2.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
17760
18912
|
const list2 = (await me()).workspaces ?? [];
|
|
17761
18913
|
if (list2.length === 1) return list2[0]?.domain ?? null;
|
|
17762
18914
|
if (t && isAccountTool(t)) return null;
|
|
@@ -17789,17 +18941,32 @@ function createMcpServer(deps) {
|
|
|
17789
18941
|
const t = toolByName.get(name);
|
|
17790
18942
|
if (!t || excluded(name)) return fail(`There is no tool called ${name}.`);
|
|
17791
18943
|
try {
|
|
17792
|
-
|
|
18944
|
+
let approval = null;
|
|
18945
|
+
if (isWrite(t) && args.confirm === true) {
|
|
18946
|
+
if (deps.previewOnly)
|
|
18947
|
+
return fail(
|
|
18948
|
+
"Don't confirm this yourself: the user confirms changes in the CMO.so app. Tell them what the preview will do and stop."
|
|
18949
|
+
);
|
|
18950
|
+
const o = signer.open(typeof args.confirmation === "string" ? args.confirmation : void 0, principal);
|
|
18951
|
+
if (!o.ok)
|
|
18952
|
+
return fail(
|
|
18953
|
+
o.reason === "missing" ? "Call this tool without confirm first to get a preview and a confirmation." : o.reason === "expired" ? "The confirmation expired. Get a new preview and ask the user again." : "This confirmation isn't valid. Get a new preview and ask the user again."
|
|
18954
|
+
);
|
|
18955
|
+
if (o.approval.calls.length !== 1 || o.approval.calls[0]?.name !== t.name)
|
|
18956
|
+
return fail("This confirmation is for a different action. Get a new preview for this one.");
|
|
18957
|
+
approval = o.approval;
|
|
18958
|
+
}
|
|
18959
|
+
const domain = approval ? approval.domain : await resolveWorkspace(t, args.workspace);
|
|
17793
18960
|
const access = await loadAccess(domain);
|
|
17794
18961
|
if (!allowed(t, access))
|
|
17795
18962
|
return fail(
|
|
17796
18963
|
access.keyAccess === "read" && isWrite(t) ? "This key can only read. Reconnect with write access to make changes." : `You don't have permission to do this on ${domain ?? "this website"}.`
|
|
17797
18964
|
);
|
|
17798
|
-
const callArgs = toolArgs(args);
|
|
17799
|
-
const ctx = { api: api2, domain };
|
|
18965
|
+
const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
|
|
18966
|
+
const ctx = { api: api2, domain, client: "mcp" };
|
|
17800
18967
|
if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
|
|
17801
|
-
|
|
17802
|
-
|
|
18968
|
+
if (!approval) {
|
|
18969
|
+
const calls = [{ name: t.name, args: callArgs }];
|
|
17803
18970
|
const summary2 = t.confirm?.(callArgs, "en") ?? t.description;
|
|
17804
18971
|
const cost = await previewCost(api2, domain, t);
|
|
17805
18972
|
return ok({
|
|
@@ -17808,26 +18975,19 @@ function createMcpServer(deps) {
|
|
|
17808
18975
|
risk: t.risk,
|
|
17809
18976
|
workspace: domain,
|
|
17810
18977
|
confirmation: signer.sign({ calls, principal, domain }),
|
|
17811
|
-
next: "Show the preview to the user. If they agree, call this tool again with
|
|
18978
|
+
next: "Show the preview to the user. If they agree, call this tool again with confirm: true and this confirmation (it runs exactly what was previewed). Nothing has changed yet."
|
|
17812
18979
|
});
|
|
17813
18980
|
}
|
|
17814
|
-
const v = signer.verify(typeof args.confirmation === "string" ? args.confirmation : void 0, {
|
|
17815
|
-
calls,
|
|
17816
|
-
principal,
|
|
17817
|
-
domain
|
|
17818
|
-
});
|
|
17819
|
-
if (!v.ok)
|
|
17820
|
-
return fail(
|
|
17821
|
-
v.reason === "missing" ? "Call this tool without confirm first to get a preview and a confirmation." : v.reason === "expired" ? "The confirmation expired. Get a new preview and ask the user again." : "The confirmation does not match these arguments. Get a new preview and ask the user again."
|
|
17822
|
-
);
|
|
17823
18981
|
const summary = t.confirm?.(callArgs, "en") ?? t.description;
|
|
17824
18982
|
try {
|
|
17825
|
-
const result = await t.run({ api: withIdempotency(api2,
|
|
17826
|
-
|
|
18983
|
+
const result = await t.run({ api: withIdempotency(api2, approval.id), domain, client: "mcp" }, callArgs);
|
|
18984
|
+
const added = t.name === "website_add" ? result?.domain : null;
|
|
18985
|
+
const where = typeof added === "string" && added ? added : domain;
|
|
18986
|
+
await deps.audit?.({ domain: where, actions: [{ tool: t.name, summary, ok: true, action_id: approval.id }] }).catch(() => void 0);
|
|
17827
18987
|
return ok(result);
|
|
17828
18988
|
} catch (e) {
|
|
17829
18989
|
const error2 = errorMessage2(e);
|
|
17830
|
-
await deps.audit?.({ domain, actions: [{ tool: t.name, summary, ok: false, error: error2, action_id:
|
|
18990
|
+
await deps.audit?.({ domain, actions: [{ tool: t.name, summary, ok: false, error: error2, action_id: approval.id }] }).catch(() => void 0);
|
|
17831
18991
|
return fail(error2);
|
|
17832
18992
|
}
|
|
17833
18993
|
} catch (e) {
|